🛠️ Shared with me feature flag (#681)
🛠️ Shared with me feature flag (#681)
This commit is contained in:
@@ -128,8 +128,8 @@
|
||||
"defaultLanguage": "DRIVE_DEFAULT_LANGUAGE",
|
||||
"defaultCompany": "DRIVE_DEFAULT_COMPANY",
|
||||
"defaultUserQuota": "DRIVE_DEFAULT_USER_QUOTA",
|
||||
"featureSearchUsers": "ENABLE_FEATURE_SEARCH_USERS",
|
||||
"featureDisplayEmail": "ENABLE_FEATURE_DISPLAY_EMAIL",
|
||||
"featureUserQuota": "ENABLE_FEATURE_USER_QUOTA"
|
||||
"featureUserQuota": "ENABLE_FEATURE_USER_QUOTA",
|
||||
"featureManageAccess": "ENABLE_FEATURE_MANAGE_ACCESS"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"mongodb": {
|
||||
"uri": "mongodb://mongo:27017",
|
||||
"database": "tdrive"
|
||||
},
|
||||
}
|
||||
},
|
||||
"message-queue": {
|
||||
"// possible 'type' values are": "'amqp' or 'local'",
|
||||
@@ -114,9 +114,9 @@
|
||||
},
|
||||
"drive": {
|
||||
"featureSharedDrive": true,
|
||||
"featureSearchUsers": true,
|
||||
"featureDisplayEmail": true,
|
||||
"featureUserQuota": false,
|
||||
"featureManageAccess": true,
|
||||
"defaultCompany": "00000000-0000-4000-0000-000000000000",
|
||||
"defaultUserQuota": 200000000
|
||||
},
|
||||
|
||||
@@ -79,6 +79,9 @@ export class DocumentsService {
|
||||
defaultQuota: number = config.has("drive.defaultUserQuota")
|
||||
? config.get("drive.defaultUserQuota")
|
||||
: 0;
|
||||
manageAccessEnabled: boolean = config.has("drive.featureManageAccess")
|
||||
? config.get("drive.featureManageAccess")
|
||||
: false;
|
||||
logger: TdriveLogger = getLogger("Documents Service");
|
||||
|
||||
async init(): Promise<this> {
|
||||
@@ -556,33 +559,38 @@ export class DocumentsService {
|
||||
oldParent = item.parent_id;
|
||||
}
|
||||
if (key === "access_info") {
|
||||
const sharedWith = content.access_info.entities.filter(
|
||||
info =>
|
||||
info.type === "user" &&
|
||||
info.id !== context.user.id &&
|
||||
!item.access_info.entities.find(entity => entity.id === info.id),
|
||||
);
|
||||
// if manage access is disabled, we don't allow changing access level
|
||||
if (!this.manageAccessEnabled) {
|
||||
delete content.access_info;
|
||||
} else if (content.access_info) {
|
||||
const sharedWith = content.access_info.entities.filter(
|
||||
info =>
|
||||
info.type === "user" &&
|
||||
info.id !== context.user.id &&
|
||||
!item.access_info.entities.find(entity => entity.id === info.id),
|
||||
);
|
||||
|
||||
item.access_info = content.access_info;
|
||||
item.access_info = content.access_info;
|
||||
|
||||
if (sharedWith.length > 0) {
|
||||
// Notify the user that the document has been shared with them
|
||||
this.logger.info("Notifying user that the document has been shared with them: ", {
|
||||
sharedWith,
|
||||
});
|
||||
gr.services.documents.engine.notifyDocumentShared({
|
||||
context,
|
||||
item,
|
||||
notificationEmitter: context.user.id,
|
||||
notificationReceiver: sharedWith[0].id,
|
||||
if (sharedWith.length > 0) {
|
||||
// Notify the user that the document has been shared with them
|
||||
this.logger.info("Notifying user that the document has been shared with them: ", {
|
||||
sharedWith,
|
||||
});
|
||||
gr.services.documents.engine.notifyDocumentShared({
|
||||
context,
|
||||
item,
|
||||
notificationEmitter: context.user.id,
|
||||
notificationReceiver: sharedWith[0].id,
|
||||
});
|
||||
}
|
||||
|
||||
item.access_info.entities.forEach(info => {
|
||||
if (!info.grantor) {
|
||||
info.grantor = context.user.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
item.access_info.entities.forEach(info => {
|
||||
if (!info.grantor) {
|
||||
info.grantor = context.user.id;
|
||||
}
|
||||
});
|
||||
} else if (key === "name") {
|
||||
renamedTo = item.name = await getItemName(
|
||||
content.parent_id || item.parent_id,
|
||||
|
||||
@@ -54,10 +54,6 @@ export function formatCompany(
|
||||
[CompanyFeaturesEnum.CHAT_EDIT_FILES]: true,
|
||||
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: true,
|
||||
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: true,
|
||||
// use the config value for this one
|
||||
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: JSON.parse(
|
||||
config.get("drive.featureSearchUsers") || "true",
|
||||
),
|
||||
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: JSON.parse(
|
||||
config.get("drive.featureSharedDrive") || "true",
|
||||
),
|
||||
@@ -67,6 +63,9 @@ export function formatCompany(
|
||||
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]: JSON.parse(
|
||||
config.get("drive.featureUserQuota") || "false",
|
||||
),
|
||||
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]: JSON.parse(
|
||||
config.get("drive.featureManageAccess") || "true",
|
||||
),
|
||||
},
|
||||
{
|
||||
...(res.plan?.features || {}),
|
||||
@@ -76,8 +75,6 @@ export function formatCompany(
|
||||
},
|
||||
);
|
||||
|
||||
console.log("🚀🚀 res.plan.features: ", res.plan.features);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,10 +91,10 @@ export const companyObjectSchema = {
|
||||
[CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: { type: "boolean" },
|
||||
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: { type: "boolean" },
|
||||
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: { type: "boolean" },
|
||||
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: { type: "boolean" },
|
||||
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: { type: "boolean" },
|
||||
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]: { type: "boolean" },
|
||||
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]: { type: "boolean" },
|
||||
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]: { type: "boolean" },
|
||||
guests: { type: "number" }, // to rename or delete
|
||||
members: { type: "number" }, // to rename or delete
|
||||
storage: { type: "number" }, // to rename or delete
|
||||
|
||||
@@ -83,10 +83,10 @@ export enum CompanyFeaturesEnum {
|
||||
CHAT_EDIT_FILES = "chat:edit_files",
|
||||
CHAT_UNLIMITED_STORAGE = "chat:unlimited_storage",
|
||||
COMPANY_INVITE_MEMBER = "company:invite_member",
|
||||
COMPANY_SEARCH_USERS = "company:search_users",
|
||||
COMPANY_SHARED_DRIVE = "company:shared_drive",
|
||||
COMPANY_DISPLAY_EMAIL = "company:display_email",
|
||||
COMPANY_USER_QUOTA = "company:user_quota",
|
||||
COMPANY_MANAGE_ACCESS = "company:managed_access",
|
||||
}
|
||||
|
||||
export type CompanyFeaturesObject = {
|
||||
@@ -96,10 +96,10 @@ export type CompanyFeaturesObject = {
|
||||
[CompanyFeaturesEnum.CHAT_EDIT_FILES]?: boolean;
|
||||
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]?: boolean;
|
||||
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]?: boolean;
|
||||
[CompanyFeaturesEnum.COMPANY_SEARCH_USERS]?: boolean;
|
||||
[CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]?: boolean;
|
||||
[CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]?: boolean;
|
||||
[CompanyFeaturesEnum.COMPANY_USER_QUOTA]?: boolean;
|
||||
[CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]?: boolean;
|
||||
};
|
||||
|
||||
export type CompanyLimitsObject = {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
|
||||
import { init, TestPlatform } from "../setup";
|
||||
import UserApi from "../common/user-api";
|
||||
import config from "config";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
jest.mock("config");
|
||||
|
||||
describe("The Drive feature", () => {
|
||||
let platform: TestPlatform;
|
||||
let configHasSpy: jest.SpyInstance;
|
||||
let configGetSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mocking config to disable manage access for the drive feature
|
||||
configHasSpy = jest.spyOn(config, "has");
|
||||
configGetSpy = jest.spyOn(config, "get");
|
||||
|
||||
configHasSpy.mockImplementation((setting: string) => {
|
||||
return jest.requireActual("config").has(setting);
|
||||
});
|
||||
configGetSpy.mockImplementation((setting: string) => {
|
||||
if (setting === "drive.featureManageAccess") {
|
||||
return false; // Disable manage access
|
||||
}
|
||||
return jest.requireActual("config").get(setting);
|
||||
});
|
||||
|
||||
// Initialize platform with required services
|
||||
platform = await init({
|
||||
services: [
|
||||
"webserver",
|
||||
"database",
|
||||
"applications",
|
||||
"search",
|
||||
"storage",
|
||||
"message-queue",
|
||||
"user",
|
||||
"search",
|
||||
"files",
|
||||
"messages",
|
||||
"auth",
|
||||
"channels",
|
||||
"counter",
|
||||
"statistics",
|
||||
"platform-services",
|
||||
"documents",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Tear down platform after each test
|
||||
await platform?.tearDown();
|
||||
platform = null;
|
||||
});
|
||||
|
||||
it("Shared with me should not contain files when manage access is off", async () => {
|
||||
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
|
||||
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
|
||||
|
||||
// Upload files by the uploader user
|
||||
const files = await oneUser.uploadAllFilesOneByOne();
|
||||
|
||||
// Wait for file processing
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
|
||||
// Share the file with recipient user
|
||||
await anotherUser.shareWithPermissions(files[1], anotherUser.user.id, "read");
|
||||
await new Promise(r => setTimeout(r, 3000)); // Wait for sharing process
|
||||
|
||||
// Check if the shared file appears in recipient's "shared with me" section
|
||||
const sharedDocs = await anotherUser.browseDocuments("shared_with_me");
|
||||
|
||||
// Validate that there are no shared files due to manage access being off
|
||||
expect(sharedDocs.children.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -8,10 +8,10 @@ export enum FeatureNames {
|
||||
EDIT_FILES = 'chat:edit_files',
|
||||
UNLIMITED_STORAGE = 'chat:unlimited_storage', //Currently inactive
|
||||
COMPANY_INVITE_MEMBER = 'company:invite_member',
|
||||
COMPANY_SEARCH_USERS = 'company:search_users',
|
||||
COMPANY_SHARED_DRIVE = 'company:shared_drive',
|
||||
COMPANY_DISPLAY_EMAIL = 'company:display_email',
|
||||
COMPANY_USER_QUOTA = 'company:user_quota',
|
||||
COMPANY_MANAGE_ACCESS = 'company:managed_access',
|
||||
}
|
||||
|
||||
export type FeatureValueType = boolean | number;
|
||||
@@ -27,10 +27,10 @@ availableFeaturesWithDefaults.set(FeatureNames.EDIT_FILES, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.UNLIMITED_STORAGE, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_INVITE_MEMBER, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_INVITE_MEMBER, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SEARCH_USERS, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SHARED_DRIVE, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_DISPLAY_EMAIL, true);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_USER_QUOTA, false);
|
||||
availableFeaturesWithDefaults.set(FeatureNames.COMPANY_MANAGE_ACCESS, true);
|
||||
|
||||
/**
|
||||
* ChannelServiceImpl that allow you to manage feature flipping in Tdrive using react feature toggles
|
||||
|
||||
@@ -27,6 +27,9 @@ import useRouterCompany from '@features/router/hooks/use-router-company';
|
||||
import _, { set } from 'lodash';
|
||||
import Languages from 'features/global/services/languages-service';
|
||||
import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';
|
||||
import FeatureTogglesService, {
|
||||
FeatureNames,
|
||||
} from '@features/global/services/feature-toggles-service';
|
||||
|
||||
/**
|
||||
* This will build the context menu in different contexts
|
||||
@@ -89,7 +92,11 @@ export const useOnBuildContextMenu = (
|
||||
type: 'menu',
|
||||
icon: 'users-alt',
|
||||
text: Languages.t('components.item_context_menu.manage_access'),
|
||||
hide: access === 'read' || getPublicLinkToken() || inTrash,
|
||||
hide:
|
||||
access === 'read' ||
|
||||
getPublicLinkToken() ||
|
||||
inTrash ||
|
||||
!FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS),
|
||||
onClick: () => setAccessModalState({ open: true, id: item.id }),
|
||||
},
|
||||
{ type: 'separator', hide: inTrash },
|
||||
|
||||
@@ -58,7 +58,7 @@ const AccessModalContent = (props: {
|
||||
}
|
||||
>
|
||||
<div className={loading ? 'opacity-50' : ''}>
|
||||
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_SEARCH_USERS) && (
|
||||
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS) && (
|
||||
<InternalUsersAccessManager id={id} disabled={access !== 'manage'} onCloseModal={props.onCloseModal} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,7 @@ export default () => {
|
||||
<CloudIcon className="w-5 h-5 mr-4" /> {Languages.t('components.side_menu.home')}
|
||||
</Button>
|
||||
)}
|
||||
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_SEARCH_USERS) && (
|
||||
{FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS) && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
history.push(
|
||||
|
||||
Reference in New Issue
Block a user