From edfd765d57fbdb187eacc31ae7cb48f9c7e17692 Mon Sep 17 00:00:00 2001 From: Montassar Ghanmy Date: Fri, 6 Oct 2023 17:05:07 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8D=84=20Separate=20trash=20for=20the=20p?= =?UTF-8?q?ersonal=20and=20shared=20drive=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orm/connectors/cassandra/cassandra.ts | 18 +--- .../orm/connectors/cassandra/query-builder.ts | 9 +- .../orm/connectors/mongodb/mongodb.ts | 20 +--- .../services/database/services/orm/utils.ts | 9 ++ .../services/documents/entities/drive-file.ts | 10 +- .../documents/services/access-check.ts | 90 ++++++++++++++---- .../services/drive-file-dto-builder.ts | 1 + .../src/services/documents/services/index.ts | 84 ++++++++++++++++- .../node/src/services/documents/utils.ts | 47 +++++++++- .../documents/web/controllers/documents.ts | 23 +++++ .../node/src/services/documents/web/routes.ts | 7 ++ .../src/services/documents/web/schemas.ts | 1 + .../test/e2e/common/entities/mock_entities.ts | 1 + .../node/test/e2e/documents/documents.spec.ts | 6 +- .../test/e2e/documents/public-links.spec.ts | 36 +------- tdrive/frontend/public/locales/en.json | 4 + tdrive/frontend/public/locales/fr.json | 4 + tdrive/frontend/public/locales/ru.json | 4 + .../features/drive/api-client/api-client.ts | 7 ++ .../drive/hooks/use-drive-actions.tsx | 14 ++- .../features/drive/hooks/use-drive-item.tsx | 14 ++- .../features/drive/hooks/use-drive-preview.ts | 1 - .../frontend/src/app/features/drive/types.ts | 3 +- .../router/services/router-service.ts | 4 + .../router/state/selectors/router-selector.ts | 5 + .../app/views/client/body/drive/browser.tsx | 7 -- .../views/client/body/drive/context-menu.tsx | 33 +++++-- .../body/drive/documents/document-row.tsx | 2 +- .../views/client/body/drive/header-path.tsx | 91 +++++++++++++++---- .../drive/modals/confirm-delete/index.tsx | 4 +- .../update-access/public-link-access.tsx | 16 +--- .../src/app/views/client/side-bar/actions.tsx | 12 ++- .../src/app/views/client/side-bar/index.tsx | 36 ++++---- .../app/views/client/viewer/drive-preview.tsx | 2 +- 34 files changed, 440 insertions(+), 185 deletions(-) diff --git a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/cassandra.ts b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/cassandra.ts index 32bb6f1c..b1b3b39a 100644 --- a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/cassandra.ts +++ b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/cassandra.ts @@ -5,7 +5,7 @@ import { defer, Subject, throwError, timer } from "rxjs"; import { concat, delayWhen, retryWhen, take, tap } from "rxjs/operators"; import { UpsertOptions } from ".."; import { logger } from "../../../../../../framework"; -import { getEntityDefinition, unwrapIndexes, unwrapPrimarykey } from "../../utils"; +import { getEntityDefinition, unwrapPrimarykey } from "../../utils"; import { EntityDefinition, ColumnDefinition, ObjectType } from "../../types"; import { AbstractConnector } from "../abstract-connector"; import { @@ -451,22 +451,6 @@ export class CassandraConnector extends AbstractConnector< const instance = new (entityType as any)(); const { columnsDefinition, entityDefinition } = getEntityDefinition(instance); - const pk = unwrapPrimarykey(entityDefinition); - const indexes = unwrapIndexes(entityDefinition); - - if ( - Object.keys(filters).some(key => pk.indexOf(key) < 0) && - Object.keys(filters).some(key => indexes.indexOf(key) < 0) - ) { - //Filter not in primary key - throw new Error( - `All filter parameters must be defined in entity primary key, - got: ${JSON.stringify(Object.keys(filters))} - on table ${entityDefinition.name} but pk is ${JSON.stringify(pk)}, - instance was ${JSON.stringify(instance)}`, - ); - } - const query = buildSelectQuery( entityType as unknown as ObjectType
, filters, diff --git a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/query-builder.ts b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/query-builder.ts index 553cac5f..5b8d28b3 100644 --- a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/query-builder.ts +++ b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/cassandra/query-builder.ts @@ -1,6 +1,6 @@ import { FindOptions } from "../../repository/repository"; import { ObjectType } from "../../types"; -import { getEntityDefinition, secureOperators } from "../../utils"; +import { getEntityDefinition, secureOperators, filteringRequired } from "../../utils"; import { transformValueToDbString } from "./typeTransforms"; export function buildSelectQuery( @@ -17,12 +17,17 @@ export function buildSelectQuery( ): string { const instance = new (entityType as any)(); const { columnsDefinition, entityDefinition } = getEntityDefinition(instance); + let allowFiltering = false; const where = Object.keys(filters) .map(key => { let result: string; const filter = filters[key]; + if (filteringRequired(key)) { + allowFiltering = true; + } + if (!filter) { return; } @@ -77,7 +82,7 @@ export function buildSelectQuery( whereClause.trim().length ? "WHERE " + whereClause : "" } ${orderByClause.trim().length ? "ORDER BY " + orderByClause : ""}` .trimEnd() - .concat(";"); + .concat(allowFiltering ? " ALLOW FILTERING;" : ";"); return query; } diff --git a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/mongodb/mongodb.ts b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/mongodb/mongodb.ts index 26aced6d..c8ffe2c4 100644 --- a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/mongodb/mongodb.ts +++ b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/mongodb/mongodb.ts @@ -3,7 +3,7 @@ import { UpsertOptions } from ".."; import { ListResult, Paginable, Pagination } from "../../../../../../framework/api/crud-service"; import { FindOptions } from "../../repository/repository"; import { ColumnDefinition, EntityDefinition, ObjectType } from "../../types"; -import { getEntityDefinition, unwrapIndexes, unwrapPrimarykey } from "../../utils"; +import { getEntityDefinition, unwrapPrimarykey } from "../../utils"; import { AbstractConnector } from "../abstract-connector"; import { buildSelectQuery } from "./query-builder"; import { transformValueFromDbString, transformValueToDbString } from "./typeTransforms"; @@ -186,24 +186,6 @@ export class MongoConnector extends AbstractConnector pk.indexOf(key) < 0) && - Object.keys(filters).some(key => indexes.indexOf(key) < 0) - ) { - //Filter not in primary key - throw Error( - "All filter parameters must be defined in entity primary key, got: " + - JSON.stringify(Object.keys(filters)) + - " on table " + - entityDefinition.name + - " but pk is " + - JSON.stringify(pk), - ); - } - const db = await this.getDatabase(); const collection = db.collection(`${entityDefinition.name}`); diff --git a/tdrive/backend/node/src/core/platform/services/database/services/orm/utils.ts b/tdrive/backend/node/src/core/platform/services/database/services/orm/utils.ts index 91abd369..22f51b94 100644 --- a/tdrive/backend/node/src/core/platform/services/database/services/orm/utils.ts +++ b/tdrive/backend/node/src/core/platform/services/database/services/orm/utils.ts @@ -106,3 +106,12 @@ export function toMongoDbOrderable(timeuuid?: string): string { const time_str = [uuid_arr[2], uuid_arr[1], uuid_arr[0], uuid_arr[3], uuid_arr[4]].join("-"); return time_str; } + +/** + * Check if filtering is necessary + * @param {string} key + * @returns {boolean} Returns true if key is "is_in_trash" or "scope", otherwise returns false. + */ +export const filteringRequired = (key: string) => { + return key === "is_in_trash" || key === "scope"; +}; diff --git a/tdrive/backend/node/src/services/documents/entities/drive-file.ts b/tdrive/backend/node/src/services/documents/entities/drive-file.ts index c27a94a7..6b96f567 100644 --- a/tdrive/backend/node/src/services/documents/entities/drive-file.ts +++ b/tdrive/backend/node/src/services/documents/entities/drive-file.ts @@ -5,9 +5,13 @@ import { FileVersion } from "./file-version"; import search from "./drive-file.search"; export const TYPE = "drive_files"; +export type DriveScope = "personal" | "shared"; @Entity(TYPE, { - globalIndexes: [["company_id", "parent_id"]], + globalIndexes: [ + ["company_id", "parent_id"], + ["company_id", "is_in_trash"], + ], primaryKey: [["company_id"], "id"], type: TYPE, search, @@ -73,6 +77,10 @@ export class DriveFile { @Column("last_version_cache", "encoded_json") last_version_cache: Partial; + + @Type(() => String) + @Column("scope", "string") + scope: DriveScope; } export type AccessInformation = { diff --git a/tdrive/backend/node/src/services/documents/services/access-check.ts b/tdrive/backend/node/src/services/documents/services/access-check.ts index 5c14daab..5a54b010 100644 --- a/tdrive/backend/node/src/services/documents/services/access-check.ts +++ b/tdrive/backend/node/src/services/documents/services/access-check.ts @@ -169,28 +169,61 @@ export const getAccessLevel = async ( repository: Repository, context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string }, ): Promise => { + const isAdmin = !context?.user?.id || (await isCompanyAdmin(context)); const isMember = !context?.user?.id || (await isCompanyMember(context)); + if (!id || id === "root") { if (!context?.user?.id || (await isCompanyGuest(context))) { return "none"; - } else { - if (isMember) return "read"; - return "manage"; } } - if (id === "trash") - return (await isCompanyGuest(context)) || !context?.user?.id - ? "none" - : (await isCompanyAdmin(context)) - ? "manage" - : "write"; - if (id === "shared_with_me") return "read"; - //If it is my personal folder, I have full access if (id.startsWith("user_")) { + if (await isCompanyApplication(context)) return "read"; + } + + if (context?.user?.id) { + /** + * Drive root directories access management + */ if (id === "user_" + context.user?.id) return "manage"; - if (await isCompanyApplication(context)) return "manage"; - return "none"; + if (id === "trash_" + context.user?.id) return "manage"; + + if (id === "root") { + if (isAdmin) return "manage"; + + return "read"; + } + + if (id === "trash") { + if (isAdmin) return "manage"; + + return "read"; + } + + if (id === "shared_with_me") return "read"; + + /** + * Entity based access management + */ + + if (!item) { + item = await repository.findOne({ + id, + company_id: context.company.id, + }); + } + + if (!item) { + throw Error("Drive item doesn't exist"); + } + + if (item.scope === "personal" && item.creator == context.user.id) return "manage"; + + if (item.scope === "shared") { + if (isAdmin) return "manage"; + if (isMember) return "read"; + } } let publicToken = context.public_token; @@ -208,13 +241,6 @@ export const getAccessLevel = async ( throw Error("Drive item doesn't exist"); } - if (await isCompanyApplication(context)) { - if (!id.startsWith("user_") && isMember && item.creator != context.user.id) { - return "read"; - } - return "manage"; - } - /* * Specific user or channel rule is applied first. Then less restrictive level will be chosen * between the parent folder and company accesses. @@ -383,6 +409,30 @@ export const getSharedByUser = ( return null; }; +export const getItemScope = async ( + item: DriveFile | null, + repository: Repository, + context: CompanyExecutionContext, +): Promise<"personal" | "shared"> => { + let scope: "personal" | "shared"; + if (item.parent_id === "user_" + context.user?.id) { + scope = "personal"; + } else if (item.parent_id === "root") { + scope = "shared"; + } else { + const driveItemParent = await repository.findOne( + { + company_id: context.company.id, + id: item.parent_id, + }, + {}, + context, + ); + scope = driveItemParent.scope; + } + return scope; +}; + const getGrantorAndThrowIfEmpty = (entity: AuthEntity) => { if (!entity.grantor) { logger.warn(`For the file permissions ${entity.id} grantor is not defined`); diff --git a/tdrive/backend/node/src/services/documents/services/drive-file-dto-builder.ts b/tdrive/backend/node/src/services/documents/services/drive-file-dto-builder.ts index a7811f70..8dce215e 100644 --- a/tdrive/backend/node/src/services/documents/services/drive-file-dto-builder.ts +++ b/tdrive/backend/node/src/services/documents/services/drive-file-dto-builder.ts @@ -34,6 +34,7 @@ export class DriveFileDTOBuilder { "access_info", "content_keywords", "creator", + "scope", ], ], [ diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 2bc22466..a3033d87 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -40,12 +40,14 @@ import { isSharedWithMeFolder, isVirtualFolder, updateItemSize, + isInTrash, } from "../utils"; import { checkAccess, getAccessLevel, hasAccessLevel, makeStandaloneAccessLevel, + getItemScope, } from "./access-check"; import { websocketEventBus } from "../../../core/platform/services/realtime/bus"; @@ -153,7 +155,6 @@ export class DocumentsService { throw Error("user does not have access to this item"); } } catch (error) { - console.log(error); this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item"); throw new CrudException("User does not have access to this item or its children", 401); } @@ -179,7 +180,22 @@ export class DocumentsService { await this.repository.find( { company_id: context.company.id, - parent_id: id, + ...(id.includes(this.TRASH) + ? { + is_in_trash: true, + ...(id == this.TRASH + ? { + scope: "shared", + } + : { + scope: "personal", + creator: context?.user?.id, + }), + } + : { + parent_id: id, + is_in_trash: false, + }), }, {}, context, @@ -257,6 +273,7 @@ export class DocumentsService { try { const driveItem = getDefaultDriveItem(content, context); const driveItemVersion = getDefaultDriveItemVersion(version, context); + driveItem.scope = await getItemScope(driveItem, this.repository, context); const hasAccess = await checkAccess( driveItem.parent_id, @@ -387,7 +404,7 @@ export class DocumentsService { throw Error("content mismatch"); } - const updatable = ["access_info", "name", "tags", "parent_id", "description"]; + const updatable = ["access_info", "name", "tags", "parent_id", "description", "is_in_trash"]; for (const key of updatable) { if ((content as any)[key]) { @@ -434,6 +451,9 @@ export class DocumentsService { await updateItemSize(item.parent_id, this.repository, context); if (oldParent) { + item.scope = await getItemScope(item, this.repository, context); + this.repository.save(item); + await updateItemSize(oldParent, this.repository, context); this.notifyWebsocket(oldParent, context); } @@ -529,6 +549,7 @@ export class DocumentsService { const previousParentId = item.parent_id; if ( + (await isInTrash(item, this.repository, context)) || item.parent_id === this.TRASH || (await getPath(item.parent_id, this.repository, true, context))[0].id === this.TRASH ) { @@ -568,7 +589,7 @@ export class DocumentsService { await this.repository.remove(item); } else { //This item is not in trash, we move it to trash - item.parent_id = this.TRASH; + item.is_in_trash = true; await this.update(item.id, item, context); } await updateItemSize(previousParentId, this.repository, context); @@ -579,6 +600,61 @@ export class DocumentsService { await this.notifyWebsocket("trash", context); }; + /** + * restore a Drive Document and its children + * + * @param {string} id - the item id + * @param {DriveExecutionContext} context - the execution context + * @returns {Promise} + */ + restore = async ( + id: string | RootType | TrashType, + item?: DriveFile, + context?: DriveExecutionContext, + ): Promise => { + if (!id) { + //We can't remove the root folder + return; + } + + item = + item || + (await this.repository.findOne({ + company_id: context.company.id, + id, + })); + + if (!item) { + this.logger.error("item to delete not found"); + throw new CrudException("Drive item not found", 404); + } + + try { + if (!(await checkAccess(item.id, item, "manage", this.repository, context))) { + this.logger.error("user does not have access drive item ", id); + throw Error("user does not have access to this item"); + } + } catch (error) { + this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item"); + throw new CrudException("User does not have access to this item or its children", 401); + } + + if (isInTrash(item, this.repository, context)) { + if (item.is_in_trash != true) { + if (item.scope === "personal") { + item.parent_id = "user_" + context.user.id; + } else { + item.parent_id = "root"; + } + } else { + item.is_in_trash = false; + } + } + this.repository.save(item); + + this.notifyWebsocket("trash", context); + }; + /** * Create a Drive item version * diff --git a/tdrive/backend/node/src/services/documents/utils.ts b/tdrive/backend/node/src/services/documents/utils.ts index 2fe6e9e8..b92aad48 100644 --- a/tdrive/backend/node/src/services/documents/utils.ts +++ b/tdrive/backend/node/src/services/documents/utils.ts @@ -30,7 +30,13 @@ const TRASH: TrashType = "trash"; const SHARED_WITH_ME: SharedWithMeType = "shared_with_me"; export const isVirtualFolder = (id: string) => { - return id === ROOT || id === TRASH || id.startsWith("user_") || id == SHARED_WITH_ME; + return ( + id === ROOT || + id === TRASH || + id.startsWith("trash_") || + id.startsWith("user_") || + id == SHARED_WITH_ME + ); }; export const isSharedWithMeFolder = (id: string) => { @@ -72,6 +78,7 @@ export const getDefaultDriveItem = ( last_modified: new Date().getTime(), parent_id: item.parent_id || "root", content_keywords: item.content_keywords || "", + scope: "personal", description: item.description || "", access_info: item.access_info || { entities: [ @@ -163,7 +170,17 @@ export const calculateItemSize = async ( ): Promise => { if (item.id === "trash") { const trashedItems = await repository.find( - { company_id: context.company.id, parent_id: "trash" }, + { company_id: context.company.id, is_in_trash: true, scope: "shared" }, + {}, + context, + ); + + return trashedItems.getEntities().reduce((acc, curr) => acc + curr.size, 0); + } + + if (item.id === "trash_" + context.user.id) { + const trashedItems = await repository.find( + { company_id: context.company.id, is_in_trash: true, scope: "personal" }, {}, context, ); @@ -173,7 +190,7 @@ export const calculateItemSize = async ( if (isVirtualFolder(item.id) || !item) { const rootFolderItems = await repository.find( - { company_id: context.company.id, parent_id: item.id || "root" }, + { company_id: context.company.id, parent_id: item.id || "root", is_in_trash: false }, {}, context, ); @@ -186,6 +203,7 @@ export const calculateItemSize = async ( { company_id: context.company.id, parent_id: item.id, + is_in_trash: false, }, {}, context, @@ -561,3 +579,26 @@ export function isFileType( const fileExtensions = [extension, ...secondaryExtensions]; return fileExtensions.some(e => requiredExtensions.includes(e)); } + +export const isInTrash = async ( + item: DriveFile, + repository: Repository, + context: CompanyExecutionContext, +) => { + if (item.is_in_trash === true) { + return true; + } + + if (isVirtualFolder(item.parent_id)) { + return false; // Stop condition + } + + // Retrieve the parent item + const parentItem = await repository.findOne({ + company_id: context.company.id, + id: item.parent_id, + }); + + // Recursively check the parent item + return isInTrash(parentItem, repository, context); +}; diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index 8cf87a25..9034d88b 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -114,6 +114,29 @@ export class DocumentsController { } }; + /** + * Restore a DriveFile item from the trashÒ + * + * @param {FastifyRequest} request + * @param {FastifyReply} reply + * @returns {Promise} + */ + restore = async ( + request: FastifyRequest<{ Params: ItemRequestParams; Querystring: { public_token?: string } }>, + reply: FastifyReply, + ): Promise => { + try { + const context = getDriveExecutionContext(request); + + await globalResolver.services.documents.documents.restore(request.params.id, null, context); + + reply.status(200).send(); + } catch (error) { + logger.error({ error: `${error}` }, "Failed to restore drive item"); + throw new CrudException("Failed to restore drive item", 500); + } + }; + /** * Lists the drive root folder. * diff --git a/tdrive/backend/node/src/services/documents/web/routes.ts b/tdrive/backend/node/src/services/documents/web/routes.ts index ef5a8c82..76717868 100644 --- a/tdrive/backend/node/src/services/documents/web/routes.ts +++ b/tdrive/backend/node/src/services/documents/web/routes.ts @@ -51,6 +51,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) handler: documentsController.delete.bind(documentsController), }); + fastify.route({ + method: "POST", + url: `${serviceUrl}/:id/restore`, + preValidation: [fastify.authenticateOptional], + handler: documentsController.restore.bind(documentsController), + }); + fastify.route({ method: "GET", url: `${serviceUrl}/:id/user/:user_id/access`, diff --git a/tdrive/backend/node/src/services/documents/web/schemas.ts b/tdrive/backend/node/src/services/documents/web/schemas.ts index e29653b9..102384e9 100644 --- a/tdrive/backend/node/src/services/documents/web/schemas.ts +++ b/tdrive/backend/node/src/services/documents/web/schemas.ts @@ -83,6 +83,7 @@ const documentSchema = { last_user: { type: "string" }, attachements: { type: "array" }, last_version_cache: fileVersionSchema, + scope: { type: "string" }, }, }; diff --git a/tdrive/backend/node/test/e2e/common/entities/mock_entities.ts b/tdrive/backend/node/test/e2e/common/entities/mock_entities.ts index a98737eb..a6edb0fc 100644 --- a/tdrive/backend/node/test/e2e/common/entities/mock_entities.ts +++ b/tdrive/backend/node/test/e2e/common/entities/mock_entities.ts @@ -29,6 +29,7 @@ export class DriveFileMockClass { access_info: MockAccessInformation; creator: string; is_directory: boolean; + scope: "personal" | "shared"; created_by: Record; shared_by: Record; } diff --git a/tdrive/backend/node/test/e2e/documents/documents.spec.ts b/tdrive/backend/node/test/e2e/documents/documents.spec.ts index 36407fe4..9febc0fb 100644 --- a/tdrive/backend/node/test/e2e/documents/documents.spec.ts +++ b/tdrive/backend/node/test/e2e/documents/documents.spec.ts @@ -59,11 +59,12 @@ describe("the Drive feature", () => { const createItem = async (): Promise => { await TestDbService.getInstance(platform, true); - + const scope: "personal" | "shared" = "shared"; const item = { name: "new test file", parent_id: "root", company_id: platform.workspace.company_id, + scope, }; const version = {}; @@ -141,8 +142,9 @@ describe("the Drive feature", () => { DriveItemDetailsMockClass, listTrashResponse.body, ); - expect(listTrashResult.item.name).toEqual("Trash"); + expect(createItemResult).toBeDefined(); + expect(createItemResult.scope).toEqual("shared"); expect(listTrashResult.children.some(({ id }) => id === createItemResult.id)).toBeTruthy(); }); diff --git a/tdrive/backend/node/test/e2e/documents/public-links.spec.ts b/tdrive/backend/node/test/e2e/documents/public-links.spec.ts index b42cf607..e3f9a084 100644 --- a/tdrive/backend/node/test/e2e/documents/public-links.spec.ts +++ b/tdrive/backend/node/test/e2e/documents/public-links.spec.ts @@ -62,11 +62,9 @@ describe("the public links feature", () => { platform = null; }); - describe("Basic Flow", () => { - const createItem = async (): Promise => { - await TestHelpers.getInstance(platform, true, {companyRole: "admin"}); + await TestHelpers.getInstance(platform, true, { companyRole: "admin" }); const item = { name: "public file", @@ -284,33 +282,7 @@ describe("the public links feature", () => { }); }); - describe("Share file from My Drive", () => { - - it("Share file from some folder", async () => { - const user = await TestHelpers.getInstance(platform, true); - const anotherUser = await TestHelpers.getInstance(platform, true); - - //create directory in "My Drive" and upload a file - const directory = await user.createDirectory("user_" + user.user.id); - const doc = await user.createRandomDocument(directory.id); - - //check that another user doesn't see any file - expect((await anotherUser.getDocument(doc.id)).statusCode).toBe(401); - - //share file with the public link - await user.shareWithPublicLink(doc, "read"); - - const token = await anotherUser.getPublicLinkAccessToken(doc); - - anotherUser.jwt = token.value; - await anotherUser.getDocumentOKCheck(doc.id); - - }); - - }); - describe("Download Folder from shared link", () => { - it("Share folder", async () => { const user = await TestHelpers.getInstance(platform, true); const anotherUser = await TestHelpers.getInstance(platform, true); @@ -332,9 +304,5 @@ describe("the public links feature", () => { expect((await anotherUser.getFolder(doc.id)).statusCode).toBe(200); }); - - }); - - - }); +}); diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index 3f3682d0..2ec357bc 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -236,6 +236,7 @@ "hooks.use-drive-actions.unable_create_file":"Unable to create a new file.", "hooks.use-drive-actions.unable_download_file":"Unable to download this files.", "hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.", + "hooks.use-drive-actions.unable_restore_file":"Unable to restore this item.", "hooks.use-drive-actions.unable_update_file":"Unable to update this file.", "general.resume": "Resume", "general.pause": "Pause", @@ -264,6 +265,8 @@ "components.side_menu.buttons.upload": "Upload", "components.side_menu.buttons.create": "Create", "components.side_menu.buttons.empty_trash": "Empty trash", + "components.header_path.shared_trash": "Shared Trash", + "components.header_path.my_trash": "My Trash", "scenes.app.drive.nothing": "Nothing here.", "scenes.app.drive.drag_and_drop": "Drag and drop files to upload them or click on the 'Add document' button.", "scenes.app.drive.add_doc": "Add document or folder", @@ -297,6 +300,7 @@ "components.item_context_menu.move.modal_header": "Move", "components.item_context_menu.move_to_trash": "Delete", "components.item_context_menu.delete": "Delete", + "components.item_context_menu.restore": "Restore", "components.item_context_menu.move_multiple": "Move selected items", "components.item_context_menu.move_multiple.modal_header": "Move selected items", "components.item_context_menu.download_multiple": "Download selected items", diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json index 4c9e82b6..94cef709 100644 --- a/tdrive/frontend/public/locales/fr.json +++ b/tdrive/frontend/public/locales/fr.json @@ -220,6 +220,7 @@ "hooks.use-drive-actions.unable_create_file":"Impossible de créer ce fichier", "hooks.use-drive-actions.unable_download_file":"Impossible de télécharger ce fichier", "hooks.use-drive-actions.unable_remove_file":"Impossible de supprimer ce fichier", + "hooks.use-drive-actions.unable_restore_file":"Impossible de restaurer cet élément.", "hooks.use-drive-actions.unable_update_file":"Impossible de mettre à jour ce fichier", "general.resume": "Reprendre", "general.pause": "Pause", @@ -264,6 +265,8 @@ "components.side_menu.buttons.upload": "Télécharger", "components.side_menu.buttons.create": "Créer", "components.side_menu.buttons.empty_trash": "Vider la corbeille", + "components.header_path.shared_trash": "Corbeille partagée", + "components.header_path.my_trash": "Ma corbeille", "scenes.app.drive.nothing": "Rien ici.", "scenes.app.drive.drag_and_drop": "Faites glisser et déposez des fichiers pour les télécharger ou cliquez sur le bouton 'Ajouter un document'.", "scenes.app.drive.add_doc": "Ajouter un document ou un dossier", @@ -297,6 +300,7 @@ "components.item_context_menu.move.modal_header": "Déplacer", "components.item_context_menu.move_to_trash": "Supprimer", "components.item_context_menu.delete": "Supprimer", + "components.item_context_menu.restore": "Restaurer", "components.item_context_menu.move_multiple": "Déplacer les éléments sélectionnés", "components.item_context_menu.move_multiple.modal_header": "Déplacer les éléments sélectionnés", "components.item_context_menu.download_multiple": "Télécharger les éléments sélectionnés", diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json index bff8909f..9378b8e8 100644 --- a/tdrive/frontend/public/locales/ru.json +++ b/tdrive/frontend/public/locales/ru.json @@ -220,6 +220,7 @@ "hooks.use-drive-actions.unable_create_file":"Unable to create a new file.", "hooks.use-drive-actions.unable_download_file":"Unable to download this files.", "hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.", + "hooks.use-drive-actions.unable_restore_file":"Unable to restore this item.", "hooks.use-drive-actions.unable_update_file":"Unable to update this file.", "general.resume": "Продолжить", "general.pause": "Пауза", @@ -264,6 +265,8 @@ "components.side_menu.buttons.upload": "Загрузить", "components.side_menu.buttons.create": "Создать", "components.side_menu.buttons.empty_trash": "Очистить козину", + "components.header_path.shared_trash": "Корзина \"Общего диска\"", + "components.header_path.my_trash": "Корзина \"Моего диска\"", "scenes.app.drive.nothing": "Здесь ничего нет.", "scenes.app.drive.drag_and_drop": "Перенесите сюда файлы чтобы загрузить их или нажмите кнопку \"Добавить файл\"", "scenes.app.drive.add_doc": "Добавить файл или папку", @@ -297,6 +300,7 @@ "components.item_context_menu.move.modal_header": "Переместить", "components.item_context_menu.move_to_trash": "Отправить в корзину", "components.item_context_menu.delete": "Удалить", + "components.item_context_menu.restore": "Restore", "components.item_context_menu.move_multiple": "Переместить все", "components.item_context_menu.move_multiple.modal_header": "Переместить выбранные елементы", "components.item_context_menu.download_multiple": "Скачать все", diff --git a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts index f3236ccb..ba0bc37e 100644 --- a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts +++ b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts @@ -83,6 +83,13 @@ export class DriveApiClient { ); } + static async restore(companyId: string, id: string | 'trash' | '') { + return await Api.post( + `/internal/services/documents/v1/companies/${companyId}/item/${id}/restore${appendTdriveToken()}`, + {} + ); + } + static async update(companyId: string, id: string, update: Partial) { return await Api.post, DriveItem>( `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`, diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx index c4a0dbac..8d2059da 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx @@ -96,6 +96,18 @@ export const useDriveActions = () => { [refresh], ); + const restore = useCallback( + async (id: string, parentId: string) => { + try { + await DriveApiClient.restore(companyId, id); + await refresh(parentId || ''); + } catch (e) { + ToasterService.error(Languages.t('hooks.use-drive-actions.unable_restore_file')); + } + }, + [refresh], + ); + const update = useCallback( async (update: Partial, id: string, parentId: string) => { try { @@ -127,5 +139,5 @@ export const useDriveActions = () => { [refresh], ); - return { create, refresh, download, downloadZip, remove, update, updateLevel }; + return { create, refresh, download, downloadZip, remove, restore, update, updateLevel }; }; diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx index 2a5bc1b1..c1fb2e16 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-item.tsx @@ -19,7 +19,7 @@ export const useDriveItem = (id: string) => { const item = useRecoilValue(DriveItemAtom(id)); const children = useRecoilValue(DriveItemChildrenAtom(id)); const [loading, setLoading] = useRecoilState(LoadingStateInitTrue('useDriveItem-' + id)); - const { refresh: refreshItem, create, update: _update, updateLevel: _updateLevel, remove: _remove } = useDriveActions(); + const { refresh: refreshItem, create, update: _update, updateLevel: _updateLevel, remove: _remove, restore: _restore } = useDriveActions(); const { uploadVersion: _uploadVersion } = useDriveUpload(); const refresh = useCallback( @@ -44,6 +44,16 @@ export const useDriveItem = (id: string) => { setLoading(false); }, [id, setLoading, refresh, item?.item?.parent_id]); + const restore = useCallback(async () => { + setLoading(true); + try { + await _restore(id, item?.item?.parent_id || ''); + } catch (e) { + ToasterService.error('Unable to restore this item.'); + } + setLoading(false); + }, [id, setLoading, refresh, item?.item?.parent_id]); + const update = useCallback( async (update: Partial) => { setLoading(true); @@ -83,7 +93,7 @@ export const useDriveItem = (id: string) => { [companyId, id, setLoading, refresh, item?.item?.parent_id], ); - const inTrash = id === 'trash' || item?.path?.some(i => i.parent_id === 'trash'); + const inTrash = id.includes('trash') || item?.path?.some(i => i?.parent_id?.includes('trash')) || item?.item?.is_in_trash; const sharedWithMe = id =="shared_with_me"; return { diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-preview.ts b/tdrive/frontend/src/app/features/drive/hooks/use-drive-preview.ts index ebab9c82..d917fdb5 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-preview.ts +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-preview.ts @@ -31,7 +31,6 @@ export const useDrivePreviewModal = () => { const close = () => { setStatus({ item: null, loading: true }); - history.push(RouterServices.generateRouteFromState({companyId: company, itemId: ""})); } return { open, close, isOpen: !!status.item }; diff --git a/tdrive/frontend/src/app/features/drive/types.ts b/tdrive/frontend/src/app/features/drive/types.ts index 8a2d540a..4356ea3b 100644 --- a/tdrive/frontend/src/app/features/drive/types.ts +++ b/tdrive/frontend/src/app/features/drive/types.ts @@ -21,7 +21,7 @@ export type DriveItem = { workspace_id: string; parent_id: string; - in_trash: boolean; + is_in_trash: boolean; is_directory: boolean; name: string; extension: string; @@ -35,6 +35,7 @@ export type DriveItem = { access_info: DriveItemAccessInfo; size: number; + scope: string; }; export type DriveFileAccessLevel = 'none' | 'read' | 'write' | 'manage'; diff --git a/tdrive/frontend/src/app/features/router/services/router-service.ts b/tdrive/frontend/src/app/features/router/services/router-service.ts index 0d55298c..aa131db8 100644 --- a/tdrive/frontend/src/app/features/router/services/router-service.ts +++ b/tdrive/frontend/src/app/features/router/services/router-service.ts @@ -27,6 +27,7 @@ export type ClientStateType = { companyId?: string; viewId?: string; itemId?: string; + dirId?: string; workspaceId?: string; channelId?: string; messageId?: string; @@ -52,6 +53,7 @@ class RouterServices extends Observable { '/client/:companyId/v/:viewId', '/client/:companyId/preview/:itemId', '/client/:companyId/v/:viewId/preview/:itemId', + '/client/:companyId/v/:viewId/d/:dirId', '/client/:companyId/w/:workspaceId', '/client/:companyId/w/:workspaceId/c/:channelId', '/client/:companyId/w/:workspaceId/c/:channelId/t/:threadId', @@ -185,6 +187,7 @@ class RouterServices extends Observable { companyId: match?.params?.companyId || '', viewId: match?.params?.viewId || '', itemId: match?.params?.itemId || '', + dirId: match?.params?.dirId || '', workspaceId: match?.params?.workspaceId || '', channelId: match?.params?.channelId || '', messageId: match?.params?.messageId || '', @@ -291,6 +294,7 @@ class RouterServices extends Observable { (state.companyId ? `/${state.companyId}` : '') + (state.viewId ? `/v/${state.viewId}` : '') + (state.itemId ? `/preview/${state.itemId}` : '') + + (state.dirId ? `/d/${state.dirId}` : '') + (state.sharedWithMe ? `/shared-with-me` : '') + (state.workspaceId ? `/w/${state.workspaceId}` : '') + (state.channelId ? `/c/${state.channelId}` : '') + diff --git a/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts b/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts index 0464fcef..aec091c0 100644 --- a/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts +++ b/tdrive/frontend/src/app/features/router/state/selectors/router-selector.ts @@ -16,6 +16,11 @@ export const RoutePreviewSelector = selector({ get: ({ get }) => get(RouterState)?.itemId || '', }); +export const RouteDirectorySelector = selector({ + key: 'RouterDirectorySelector', + get: ({ get }) => get(RouterState)?.dirId || '', +}); + export const RouterWorkspaceSelector = selector({ key: 'RouterWorkspaceSelector', get: ({ get }) => get(RouterState)?.workspaceId || '', diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index 746f6188..cff60fbe 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -113,13 +113,6 @@ export default memo( [_setParentId], ); - - - //In case we are kicked out of the current folder, we need to reset the parent id - useEffect(() => { - if (!loading && !path?.length && !inPublicSharing && !sharedWithMe) setParentId('user_'+user?.id); - }, [path, loading, setParentId]); - useEffect(() => { setChecked({}); refresh(parentId); diff --git a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx index 3bd7772d..3cc001a3 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx @@ -19,6 +19,7 @@ import { ToasterService } from '@features/global/services/toaster-service'; import { copyToClipboard } from '@features/global/utils/CopyClipboard'; import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter'; import { getCurrentUserList } from '@features/users/hooks/use-user-list'; +import useRouteState from 'app/features/router/hooks/use-route-state'; import RouterServices from '@features/router/services/router-service'; import useRouterCompany from '@features/router/hooks/use-router-company'; import _ from 'lodash'; @@ -35,7 +36,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }), ); - const { download, downloadZip, update } = useDriveActions(); + const { download, downloadZip, update, restore } = useDriveActions(); const setCreationModalState = useSetRecoilState(CreateModalAtom); const setSelectorModalState = useSetRecoilState(SelectorModalAtom); const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom); @@ -45,6 +46,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s const setPropertiesModalState = useSetRecoilState(PropertiesModalAtom); const setUsersModalState = useSetRecoilState(UsersModalAtom); const { open: preview } = useDrivePreview(); + const { viewId } = useRouteState(); const company = useRouterCompany(); function getIdsFromArray(arr: DriveItem[]): string[] { @@ -56,7 +58,8 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s if (!parent || !parent.access) return []; try { - const inTrash = parent.path?.[0]?.id === 'trash'; + const inTrash = parent.path?.[0]?.id.includes('trash') || viewId?.includes("trash"); + const isPersonal = item?.scope === 'personal'; const selectedCount = checked.length; let menu: any[] = []; @@ -75,7 +78,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s { type: 'menu', text: Languages.t('components.item_context_menu.share'), - hide: access === 'read' || getPublicLinkToken(), + hide: access === 'read' || getPublicLinkToken() || inTrash, onClick: () => setAccessModalState({ open: true, id: item.id }), }, { @@ -106,13 +109,13 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s { type: 'menu', text: Languages.t('components.item_context_menu.manage_access'), - hide: access === 'read' || getPublicLinkToken(), + hide: access === 'read' || getPublicLinkToken() || inTrash, onClick: () => setAccessModalState({ open: true, id: item.id }), }, { type: 'menu', text: Languages.t('components.item_context_menu.move'), - hide: access === 'read', + hide: access === 'read' || inTrash, onClick: () => setSelectorModalState({ open: true, @@ -135,13 +138,13 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s { type: 'menu', text: Languages.t('components.item_context_menu.rename'), - hide: access === 'read', + hide: access === 'read' || inTrash, onClick: () => setPropertiesModalState({ open: true, id: item.id }), }, { type: 'menu', text: Languages.t('components.item_context_menu.copy_link'), - hide: !item.access_info.public?.level || item.access_info.public?.level === 'none', + hide: !item.access_info.public?.level || item.access_info.public?.level === 'none' || inTrash, onClick: () => { copyToClipboard(getPublicLink(item || parent?.item)); ToasterService.success( @@ -152,10 +155,10 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s { type: 'menu', text: Languages.t('components.item_context_menu.versions'), - hide: item.is_directory, + hide: item.is_directory || inTrash, onClick: () => setVersionModal({ open: true, id: item.id }), }, - { type: 'separator', hide: access !== 'manage' }, + { type: 'separator', hide: access !== 'manage' || inTrash, }, { type: 'menu', text: Languages.t('components.item_context_menu.move_to_trash'), @@ -163,6 +166,16 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s hide: inTrash || access !== 'manage', onClick: () => setConfirmTrashModalState({ open: true, items: [item] }), }, + { + type: 'menu', + text: Languages.t('components.item_context_menu.restore'), + className: 'error', + hide: !inTrash || (access !== 'manage' && !isPersonal), + onClick: () => { + const parentId = item.is_in_trash ? viewId || '' : item.parent_id; + restore(item.id, parentId); + }, + }, { type: 'menu', text: Languages.t('components.item_context_menu.delete'), @@ -257,7 +270,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s type: 'menu', text: Languages.t('components.item_context_menu.trash.empty'), className: 'error', - hide: parent.item!.id != 'trash' || parent.access !== 'manage', + hide: !inTrash, onClick: () => { setConfirmDeleteModalState({ open: true, diff --git a/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx b/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx index c03a4e52..37ae306c 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx @@ -57,7 +57,7 @@ export const DocumentRow = ({ const device = getDevice(); console.log("DEVICE:: " + device); if (device != "ios" && device != "android") { - history.push(RouterServices.generateRouteFromState({companyId: company, itemId: item.id})); + history.push(RouterServices.generateRouteFromState({companyId: company, itemId: item.id, dirId: ''})); open(item); } }; diff --git a/tdrive/frontend/src/app/views/client/body/drive/header-path.tsx b/tdrive/frontend/src/app/views/client/body/drive/header-path.tsx index cc573078..b8f9564e 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/header-path.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/header-path.tsx @@ -5,7 +5,11 @@ import { useEffect, useState } from 'react'; import { PublicIcon } from './components/public-icon'; import MenusManager from '@components/menus/menus-manager.jsx'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; -import Languages from "features/global/services/languages-service"; +import { useDriveItem } from '@features/drive/hooks/use-drive-item'; +import Languages from 'features/global/services/languages-service'; +import { useHistory } from 'react-router-dom'; +import useRouterCompany from '@features/router/hooks/use-router-company'; +import RouterServices from '@features/router/services/router-service'; export default ({ path: livePath, @@ -17,18 +21,36 @@ export default ({ setParentId: (id: string) => void; }) => { const [savedPath, setSavedPath] = useState([]); + const { user } = useCurrentUser(); + const history = useHistory(); + const company = useRouterCompany(); useEffect(() => { if (livePath) setSavedPath(livePath); }, [livePath]); const path = livePath || savedPath; - return setParentId(id)} />; + return ( + { + history.push( + RouterServices.generateRouteFromState({ + companyId: company, + viewId, + dirId + }), + ); + setParentId(dirId? dirId: viewId) + }} + /> + ); }; -function cutFileName (name: any){ - if (typeof name !== "undefined" ){ - if (name.length >= 30){ - return name.substring(0,30)+" ..."; +function cutFileName(name: any) { + if (typeof name !== 'undefined') { + if (name.length >= 30) { + return name.substring(0, 30) + ' ...'; } else { return name; } @@ -43,7 +65,7 @@ export const PathRender = ({ }: { path: DriveItem[]; inTrash: boolean; - onClick: (id: string) => void; + onClick: (viewId: string, dirId: string) => void; }) => { const pathLength = (path || []).reduce((acc, curr) => acc + curr.name.length, 0); @@ -100,30 +122,61 @@ const PathItem = ({ item: Partial; last?: boolean; first?: boolean; - onClick: (id: string) => void; + onClick: (viewId: string, dirId: string) => void; }) => { const { user } = useCurrentUser(); + const { viewId } = RouterServices.getStateFromRoute(); + const { access: trashAccess } = useDriveItem('trash'); return (
{ - if (first && user?.id) { - MenusManager.openMenu( - [ - { type: 'menu', text: Languages.t('components.side_menu.home'), onClick: () => onClick('root') }, - { type: 'menu', text: Languages.t('components.side_menu.my_drive'), onClick: () => onClick('user_' + user?.id) }, - ], - { x: evt.clientX, y: evt.clientY }, - 'center', - ); + const driveMenuItems = [ + { + type: 'menu', + text: Languages.t('components.side_menu.home',), + onClick: () => onClick('root', ''), + }, + { + type: 'menu', + text: Languages.t('components.side_menu.my_drive'), + onClick: () => onClick('user_' + user?.id, ''), + }, + ]; + + const trashMenuItems = [ + { + type: 'menu', + text: Languages.t('components.header_path.my_trash'), + onClick: () => onClick('trash_' + user?.id, ''), + }, + { + type: 'menu', + text: Languages.t('components.header_path.shared_trash'), + onClick: () => onClick('trash', ''), + hide: trashAccess === 'read', + }, + ]; + + if (first && user?.id) { + if (viewId?.includes('trash')) { + MenusManager.openMenu(trashMenuItems, { x: evt.clientX, y: evt.clientY }, 'center'); + } else { + MenusManager.openMenu(driveMenuItems, { x: evt.clientX, y: evt.clientY }, 'center'); + } } else { - onClick(item?.id || ''); + onClick(viewId || '',item?.id || ''); } }} > - {cutFileName(item?.name) || ''} + + {viewId?.includes('trash_') && first && Languages.t('components.header_path.my_trash')} + {viewId === 'trash' && first && Languages.t('components.header_path.shared_trash')} + {(viewId === 'trash' || viewId?.includes('trash_')) && !first && (cutFileName(item?.name) || '')} + {!viewId?.includes('trash') && (cutFileName(item?.name) || '')} + {item?.access_info?.public?.level && item?.access_info?.public?.level !== 'none' && ( diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/confirm-delete/index.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/confirm-delete/index.tsx index b6e034ac..d31ad1c2 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/modals/confirm-delete/index.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/confirm-delete/index.tsx @@ -7,6 +7,7 @@ import { DriveItemSelectedList } from '@features/drive/state/store'; import { DriveItem } from '@features/drive/types'; import { useEffect, useState } from 'react'; import { atom, useRecoilState } from 'recoil'; +import RouterServices from '@features/router/services/router-service'; export type ConfirmDeleteModalType = { open: boolean; @@ -40,6 +41,7 @@ const ConfirmDeleteModalContent = ({ items }: { items: DriveItem[] }) => { const [loading, setLoading] = useState(false); const [state, setState] = useRecoilState(ConfirmDeleteModalAtom); const [, setSelected] = useRecoilState(DriveItemSelectedList); + const { viewId } = RouterServices.getStateFromRoute(); useEffect(() => { refresh(items[0].id); @@ -64,7 +66,7 @@ const ConfirmDeleteModalContent = ({ items }: { items: DriveItem[] }) => { onClick={async () => { setLoading(true); for (const item of items) { - await remove(item.id, item.parent_id); + await remove(item.id, viewId || ""); } setLoading(false); setSelected({}); diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/public-link-access.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/public-link-access.tsx index 091dae0a..071fdb5f 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/public-link-access.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/public-link-access.tsx @@ -111,9 +111,9 @@ const PublicLinkOptions = (props: { }; - /* useEffect(() => { + useEffect(() => { props.onChangePassword(usePassword ? password : ''); - }, [usePassword, password]); */ + }, [usePassword, password]); useEffect(() => { props.onChangeExpiration(useExpiration ? expiration : 0); @@ -144,18 +144,6 @@ const PublicLinkOptions = (props: { className="max-w-xs" value={password} onChange={e => setPassword(e.target.value)} - onBlur={handlePasswordBlur} - // changes password only when press enter - onKeyPress={e => { - if (e.key === 'Enter') { - props.onChangePassword(password); - e.preventDefault(); - if (password) { - copyToClipboard(password); - ToasterService.success(Languages.t('components.public-link-security_password_copied')); - } - } - }} // saves and copies password onClick={() => { if (password) copyToClipboard(password); diff --git a/tdrive/frontend/src/app/views/client/side-bar/actions.tsx b/tdrive/frontend/src/app/views/client/side-bar/actions.tsx index 7c2dea06..eb449740 100644 --- a/tdrive/frontend/src/app/views/client/side-bar/actions.tsx +++ b/tdrive/frontend/src/app/views/client/side-bar/actions.tsx @@ -13,6 +13,7 @@ import { CreateModal, CreateModalAtom } from '../body/drive/modals/create'; import { Button } from '@atoms/button/button'; import Languages from "features/global/services/languages-service"; import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; +import RouterServices from '@features/router/services/router-service'; export const CreateModalWithUploadZones = ({ initialParentId }: { initialParentId?: string }) => { const companyId = useRouterCompany(); @@ -25,8 +26,6 @@ export const CreateModalWithUploadZones = ({ initialParentId }: { initialParentI DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'user_'+user?.id }), ); - console.log("Upload Zone:: " + parentId); - return ( <> { const { user } = useCurrentUser(); + const { viewId } = RouterServices.getStateFromRoute(); const [parentId, _] = useRecoilState(DriveCurrentFolderAtom({ initialFolderId: 'user_'+user?.id })); - const { access, item, inTrash } = useDriveItem(parentId); - const { children: trashChildren } = useDriveItem('trash'); + const { access, item } = useDriveItem(parentId); + const { children: trashChildren } = useDriveItem(viewId === 'trash' ? 'trash' : 'trash_'+user?.id); const uploadZoneRef = useRef(null); const { uploadTree } = useDriveUpload(); const companyId = useRouterCompany(); + const inTrash = viewId?.includes("trash") || false; const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom); const setCreationModalState = useSetRecoilState(CreateModalAtom); @@ -101,7 +102,7 @@ export default () => {
- {inTrash && access === 'manage' && ( + {inTrash && ( <> diff --git a/tdrive/frontend/src/app/views/client/side-bar/index.tsx b/tdrive/frontend/src/app/views/client/side-bar/index.tsx index fa3547d3..217a8286 100644 --- a/tdrive/frontend/src/app/views/client/side-bar/index.tsx +++ b/tdrive/frontend/src/app/views/client/side-bar/index.tsx @@ -11,7 +11,6 @@ import { } from '@heroicons/react/outline'; import { useEffect } from 'react'; import useRouterCompany from '@features/router/hooks/use-router-company'; -import useRouterWorkspace from '@features/router/hooks/use-router-workspace'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { useRecoilState } from 'recoil'; import { Title } from '../../../atoms/text'; @@ -21,17 +20,15 @@ import Account from '../common/account'; import AppGrid from '../common/app-grid'; import DiskUsage from '../common/disk-usage'; import Actions from './actions'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import RouterServices from '@features/router/services/router-service'; import Languages from "features/global/services/languages-service"; export default () => { const history = useHistory(); const { user } = useCurrentUser(); - const location = useLocation(); const company = useRouterCompany(); - const workspace = useRouterWorkspace(); - const { viewId, itemId } = RouterServices.getStateFromRoute(); + const { viewId, itemId, dirId } = RouterServices.getStateFromRoute(); const [parentId, setParentId] = useRecoilState( DriveCurrentFolderAtom({ initialFolderId: viewId || 'user_'+user?.id }), ); @@ -46,7 +43,8 @@ export default () => { useEffect(() => { - !itemId && viewId && setParentId(viewId); + !itemId && !dirId && viewId && setParentId(viewId); + dirId && viewId && setParentId(dirId); }, [viewId, itemId]); return (
@@ -68,15 +66,15 @@ export default () => {
Drive )} - {rootAccess === 'manage' && ( - - )} + {false && ( <> diff --git a/tdrive/frontend/src/app/views/client/viewer/drive-preview.tsx b/tdrive/frontend/src/app/views/client/viewer/drive-preview.tsx index 07f7566d..19c8aaa5 100644 --- a/tdrive/frontend/src/app/views/client/viewer/drive-preview.tsx +++ b/tdrive/frontend/src/app/views/client/viewer/drive-preview.tsx @@ -83,7 +83,7 @@ export const DrivePreview: React.FC = ({ items }) => { if (device != 'ios' && device != 'android') { close(); history.push( - RouterServices.generateRouteFromState({ companyId: company, viewId: '', itemId: item.id }), + RouterServices.generateRouteFromState({ companyId: company, viewId: '', itemId: item.id, dirId: '' }), ); open(item); }