From 441c2b780f062d715a34f2f7ee3d86f545fccddd Mon Sep 17 00:00:00 2001 From: Montassar Ghanmy Date: Sat, 17 Jun 2023 15:37:33 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A2=20Shared=20with=20me=20filters=20(?= =?UTF-8?q?#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ feat: shared with me filters 🐛 Fix try to access the service with init 🌐 feat: shared with me i18n 🌐russian translation --------- Co-authored-by: Anton SHEPILOV --- .../node/src/cli/cmds/users_cmds/remove.ts | 88 -------- .../services/documents/entities/drive-file.ts | 2 +- .../documents/services/access-check.ts | 17 +- .../drive-file-dto-builder.ts | 26 +-- .../src/services/documents/services/index.ts | 16 +- .../node/src/services/documents/types.ts | 1 + .../documents/web/controllers/documents.ts | 4 +- .../node/test/e2e/documents/documents.spec.ts | 32 ++- .../web/dto/drive-file-dto-builder.test.ts | 22 +- tdrive/frontend/public/locales/en.json | 15 +- tdrive/frontend/public/locales/fr.json | 106 ++++----- tdrive/frontend/public/locales/ru.json | 15 +- .../features/drive/api-client/api-client.ts | 17 +- .../drive/hooks/use-drive-actions.tsx | 63 ++---- .../hooks/use-shared-with-me-drive-items.tsx | 50 +++-- .../drive/state/shared-with-me-filter.tsx | 17 ++ .../app/views/client/body/drive/browser.tsx | 4 + .../views/client/body/drive/context-menu.tsx | 202 +++++++++++++----- .../client/body/drive/shared-files-table.tsx | 189 ++++++++-------- 19 files changed, 482 insertions(+), 404 deletions(-) delete mode 100644 tdrive/backend/node/src/cli/cmds/users_cmds/remove.ts rename tdrive/backend/node/src/services/documents/{web/dto => services}/drive-file-dto-builder.ts (83%) create mode 100644 tdrive/frontend/src/app/features/drive/state/shared-with-me-filter.tsx diff --git a/tdrive/backend/node/src/cli/cmds/users_cmds/remove.ts b/tdrive/backend/node/src/cli/cmds/users_cmds/remove.ts deleted file mode 100644 index 15fa3932..00000000 --- a/tdrive/backend/node/src/cli/cmds/users_cmds/remove.ts +++ /dev/null @@ -1,88 +0,0 @@ -import yargs from "yargs"; -import ora from "ora"; -import tdrive from "../../../tdrive"; -import Table from "cli-table"; -import { exit } from "process"; -import gr from "../../../services/global-resolver"; - -/** - * Merge command parameters. Check the builder definition below for more details. - */ -type CLIArgs = { - id: string; -}; - -const services = [ - "storage", - "counter", - "message-queue", - "platform-services", - "user", - "search", - "database", - "webserver", - "statistics", - "applications", - "auth", - "realtime", - "websocket", -]; - -const command: yargs.CommandModule = { - command: "remove", - describe: "command that allow you to remove one user", - builder: { - id: { - default: "", - type: "string", - description: "User ID", - }, - }, - handler: async argv => { - const tableBefore = new Table({ - head: ["User ID", "Username", "Deleted"], - colWidths: [40, 40, 10], - }); - const tableAfter = new Table({ - head: ["User ID", "Username", "Deleted"], - colWidths: [40, 40, 10], - }); - const spinner = ora({ text: "Retrieving user" }).start(); - - const platform = await tdrive.run(services); - await gr.doInit(platform); - - const user = await gr.services.users.get({ id: argv.id }); - - if (!user) { - console.error("Error: You need to provide User ID"); - spinner.stop(); - return; - } - - if (user) { - // Table before - tableBefore.push([user.id, user.username_canonical, user.deleted]); - - await gr.services.users.anonymizeAndDelete( - { id: user.id }, - { - user: { id: user.id, server_request: true }, - }, - ); - - const finalUser = await gr.services.users.get({ id: argv.id }); - - // Table after - tableAfter.push([finalUser.id, finalUser.username_canonical, finalUser.deleted]); - - spinner.stop(); - } - - exit(); - - return; - }, -}; - -export default command; 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 90b89737..c27a94a7 100644 --- a/tdrive/backend/node/src/services/documents/entities/drive-file.ts +++ b/tdrive/backend/node/src/services/documents/entities/drive-file.ts @@ -85,7 +85,7 @@ export type AccessInformation = { entities: AuthEntity[]; }; -type AuthEntity = { +export type AuthEntity = { type: "user" | "channel" | "company" | "folder"; id: string | "parent"; level: publicAccessLevel | DriveFileAccessLevel; 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 cafb4898..dd0c64e2 100644 --- a/tdrive/backend/node/src/services/documents/services/access-check.ts +++ b/tdrive/backend/node/src/services/documents/services/access-check.ts @@ -3,7 +3,7 @@ import _ from "lodash"; import { logger } from "../../../core/platform/framework"; import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; import globalResolver from "../../global-resolver"; -import { AccessInformation, DriveFile } from "../entities/drive-file"; +import { AccessInformation, AuthEntity, DriveFile } from "../entities/drive-file"; import { CompanyExecutionContext, DriveFileAccessLevel } from "../types"; /** @@ -341,11 +341,20 @@ export const getSharedByUser = ( for (const idx in accessInfo?.entities) { const entity = accessInfo.entities[idx]; if (entity.type === "user" && entity.id === context.user?.id) { - return entity.grantor; + return getGrantorAndThrowIfEmpty(entity); } - if (entity.type === "company" && entity.id === context.company.id) { - return entity.grantor; + if (entity.type === "company" && entity.id === context.company.id && entity.level != "none") { + console.log("company", entity); + return getGrantorAndThrowIfEmpty(entity); } } return null; }; + +const getGrantorAndThrowIfEmpty = (entity: AuthEntity) => { + if (!entity.grantor) { + logger.warn(`For the file permissions ${entity.id} grantor is not defined`); + // throw CrudException.badGateway(`For the file permissions ${entity.id} grantor is not defined`); + } + return entity.grantor; +}; diff --git a/tdrive/backend/node/src/services/documents/web/dto/drive-file-dto-builder.ts b/tdrive/backend/node/src/services/documents/services/drive-file-dto-builder.ts similarity index 83% rename from tdrive/backend/node/src/services/documents/web/dto/drive-file-dto-builder.ts rename to tdrive/backend/node/src/services/documents/services/drive-file-dto-builder.ts index 5f04a5e3..a7811f70 100644 --- a/tdrive/backend/node/src/services/documents/web/dto/drive-file-dto-builder.ts +++ b/tdrive/backend/node/src/services/documents/services/drive-file-dto-builder.ts @@ -1,17 +1,19 @@ -import { DriveFile } from "../../entities/drive-file"; -import { DriveFileDTO } from "./drive-file-dto"; +import { DriveFile } from "../entities/drive-file"; +import { DriveFileDTO } from "../web/dto/drive-file-dto"; import { ListResult, Paginable, Pagination, -} from "../../../../core/platform/framework/api/crud-service"; +} from "../../../core/platform/framework/api/crud-service"; import _ from "lodash"; -import { CompanyExecutionContext } from "../../types"; -import globalResolver from "../../../../services/global-resolver"; -import User from "../../../../services/user/entities/user"; -import { getSharedByUser } from "../../services/access-check"; +import { CompanyExecutionContext } from "../types"; +import User from "../../user/entities/user"; +import { getSharedByUser } from "./access-check"; +import globalResolver from "../../../services/global-resolver"; export class DriveFileDTOBuilder { + static VIEW_SHARED_WITH_ME = "shared_with_me"; + private views: Map = new Map([ [ "default", @@ -35,7 +37,7 @@ export class DriveFileDTOBuilder { ], ], [ - "shared_with_me", + DriveFileDTOBuilder.VIEW_SHARED_WITH_ME, [ "id", "name", @@ -51,7 +53,6 @@ export class DriveFileDTOBuilder { ], ], ]); - usersService = globalResolver.services?.users; public async build( files: ListResult, @@ -59,8 +60,6 @@ export class DriveFileDTOBuilder { fields?: string[], view?: string, ): Promise> { - const file = new DriveFile(); - file.id = "1"; if (view) { fields = this.views.get(view); } @@ -110,9 +109,10 @@ export class DriveFileDTOBuilder { }); } - private async fetchUsers(ids: string[]) { + async fetchUsers(ids: string[]) { + ids.filter(id => id != null); return ( - await this.usersService.list( + await globalResolver.services.users.list( { limitStr: ids.length.toString(), } as Pagination, diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 01647fe9..dfa6bb05 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -783,9 +783,7 @@ export class DocumentsService { limitStr: "100", }, $in: [ - ...(options.onlyDirectlyShared - ? [["access_entities", [context.user.id, context.company.id]] as inType] - : []), + ...(options.onlyDirectlyShared ? [["access_entities", [context.user.id]] as inType] : []), ...(options.company_id ? [["company_id", [options.company_id]] as inType] : []), ...(options.creator ? [["creator", [options.creator]] as inType] : []), ...(options.mime_type @@ -834,9 +832,17 @@ export class DocumentsService { }); return new ListResult(result.type, filteredResult, result.nextPage); - } else { - return result; } + + if (options.onlyUploadedNotByMe) { + const filteredResult = await this.filter(result.getEntities(), async item => { + return item.creator != context.user.id; + }); + + return new ListResult(result.type, filteredResult, result.nextPage); + } + + return result; }; private async filter(arr, callback) { diff --git a/tdrive/backend/node/src/services/documents/types.ts b/tdrive/backend/node/src/services/documents/types.ts index a8969daa..ddc4dd99 100644 --- a/tdrive/backend/node/src/services/documents/types.ts +++ b/tdrive/backend/node/src/services/documents/types.ts @@ -48,6 +48,7 @@ export type SearchDocumentsOptions = { view?: string; fields?: string[]; onlyDirectlyShared?: boolean; + onlyUploadedNotByMe?: boolean; }; export type SearchDocumentsBody = { 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 3d5f8089..a336c606 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -20,7 +20,7 @@ import { SearchDocumentsOptions, } from "../../types"; import { DriveFileDTO } from "../dto/drive-file-dto"; -import { DriveFileDTOBuilder } from "../dto/drive-file-dto-builder"; +import { DriveFileDTOBuilder } from "../../services/drive-file-dto-builder"; export class DocumentsController { private driveFileDTOBuilder = new DriveFileDTOBuilder(); @@ -154,7 +154,9 @@ export class DocumentsController { const options: SearchDocumentsOptions = { ...request.body, company_id: request.body.company_id || context.company.id, + view: DriveFileDTOBuilder.VIEW_SHARED_WITH_ME, onlyDirectlyShared: true, + onlyUploadedNotByMe: true, }; if (!Object.keys(options).length) { diff --git a/tdrive/backend/node/test/e2e/documents/documents.spec.ts b/tdrive/backend/node/test/e2e/documents/documents.spec.ts index c15b133d..7f60db7f 100644 --- a/tdrive/backend/node/test/e2e/documents/documents.spec.ts +++ b/tdrive/backend/node/test/e2e/documents/documents.spec.ts @@ -171,7 +171,7 @@ describe("the Drive feature", () => { }); it("did search for an item and check that all the fields for 'shared_with_me' view", async () => { - jest.setTimeout(10000); + jest.setTimeout(20000); //given:: user uploaded one doc and give permission to another user const oneUser = await TestHelpers.getInstance(platform, true); const anotherUser = await TestHelpers.getInstance(platform, true); @@ -187,7 +187,7 @@ describe("the Drive feature", () => { }); await oneUser.updateDocument(doc.id, doc); - await new Promise(resolve => setTimeout(resolve, 3000)); + await new Promise(resolve => setTimeout(resolve, 5000)); //when:: user search for a doc const searchResponse = await anotherUser.searchDocument({ view: "shared_with_me" }); @@ -211,6 +211,34 @@ describe("the Drive feature", () => { expect(actual.shared_by?.first_name).toEqual(oneUser.user.first_name); }); + it("'shared_with_me' shouldn't return files uploaded by me", async () => { + jest.setTimeout(20000); + //given:: user uploaded one doc and give permission to another user + const oneUser = await TestHelpers.getInstance(platform, true); + const anotherUser = await TestHelpers.getInstance(platform, true); + const doc = await oneUser.uploadRandomFileAndCreateDocument(); + await new Promise(r => setTimeout(r, 3000)); + //give permissions to the file + doc.access_info.entities.push({ + type: "user", + id: anotherUser.user.id, + level: "read", + grantor: null, + }); + await oneUser.updateDocument(doc.id, doc); + //another user also uploaded several files + await anotherUser.uploadRandomFileAndCreateDocument(); + + await new Promise(resolve => setTimeout(resolve, 5000)); + + //when:: user search for a doc + const searchResponse = await anotherUser.sharedWithMeDocuments({}); + + //then:: + expect(searchResponse.entities?.length).toEqual(1); + const actual = searchResponse.entities[0]; + }) + it("did search for an item that doesn't exist", async () => { await createItem(); diff --git a/tdrive/backend/node/test/unit/core/services/documents/web/dto/drive-file-dto-builder.test.ts b/tdrive/backend/node/test/unit/core/services/documents/web/dto/drive-file-dto-builder.test.ts index 5c7f12a0..8cfd4177 100644 --- a/tdrive/backend/node/test/unit/core/services/documents/web/dto/drive-file-dto-builder.test.ts +++ b/tdrive/backend/node/test/unit/core/services/documents/web/dto/drive-file-dto-builder.test.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { describe } from "@jest/globals"; import { mock } from 'jest-mock-extended'; -import { DriveFileDTOBuilder } from "../../../../../../../src/services/documents/web/dto/drive-file-dto-builder"; +import { DriveFileDTOBuilder } from "../../../../../../../src/services/documents/services/drive-file-dto-builder"; import { ListResult } from "../../../../../../../src/core/platform/framework/api/crud-service"; import { DriveFile } from "../../../../../../../src/services/documents/entities/drive-file"; import { CompanyExecutionContext } from "../../../../../../../src/services/applications/web/types"; @@ -9,13 +9,11 @@ import { UserServiceImpl } from "../../../../../../../src/services/user/services describe("Drive File DTO Builder Test", () => { - const subj = new DriveFileDTOBuilder(); - - it("The dto object will contain selected fields with the data ", async () => { //given const fields = ["id", "name"]; const file = newFile("file_id", "file_name"); + const subj = new DriveFileDTOBuilder(); //when @@ -37,9 +35,9 @@ describe("Drive File DTO Builder Test", () => { //given const fields = ["id", "name"]; const file = newFile("file_id", "file_name", "file_parent_id"); + const subj = new DriveFileDTOBuilder(); //when - let transformedFiles = await subj.build( new ListResult("drive_files", [file]), context, @@ -56,9 +54,10 @@ describe("Drive File DTO Builder Test", () => { it("When there is no fields set then whole object should be copied", async () => { //given const file = newFile("file_id", "file_name", "file_parent_id"); + const subj = new DriveFileDTOBuilder(); + //when - let transformedFiles = await subj.build( new ListResult("drive_files", [file]), context @@ -77,6 +76,8 @@ describe("Drive File DTO Builder Test", () => { //given const file = newFile("file_id", "file_name"); let fields = ["id", "name", "unknown_property"]; + const subj = new DriveFileDTOBuilder(); + //when let transformedFiles = await subj.build( @@ -98,8 +99,9 @@ describe("Drive File DTO Builder Test", () => { let file_creator = newUser(); const file = newFile("file_id", "file_name", "parent_id", file_creator.id); let fields = ["id", "name", "created_by"]; - subj.usersService = mock(); - subj.usersService.list = jest.fn().mockReturnValue(new ListResult("users", [file_creator])); + const usersService = mock() as UserServiceImpl; + const subj = new DriveFileDTOBuilder() + subj.fetchUsers = jest.fn().mockReturnValue([file_creator]); //when let transformedFiles = await subj.build( @@ -122,8 +124,8 @@ describe("Drive File DTO Builder Test", () => { let file_shared_by = newUser(); const file = newFile("file_id", "file_name", "parent_id", null, file_shared_by.id); let fields = ["id", "name", "shared_by"]; - subj.usersService = mock(); - subj.usersService.list = jest.fn().mockReturnValue(new ListResult("users", [file_shared_by])); + const subj = new DriveFileDTOBuilder(); + subj.fetchUsers = jest.fn().mockReturnValue([file_shared_by]); //when let transformedFiles = await subj.build( diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index 790116fd..46d06b7c 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -257,5 +257,18 @@ "components.item_context_menu.trash.empty": "Empty trash", "components.item_context_menu.add_documents": "Add document or folder", "components.item_context_menu.download_folder": "Download folder", - "components.item_context_menu.go_to_trash": "Go to trash" + "components.item_context_menu.go_to_trash": "Go to trash", + "components.item_context_menu.all": "All", + "components.item_context_menu.today": "Today", + "components.item_context_menu.last_week": "Last week", + "components.item_context_menu.last_month": "Last month", + "scenes.app.shared_with_me.shared_with_me": "Shared with me", + "scenes.app.shared_with_me.file_type": "File type", + "scenes.app.shared_with_me.people": "People", + "scenes.app.shared_with_me.last_modified": "Last modified", + "scenes.app.shared_with_me.documents": "Documents", + "scenes.app.shared_with_me.name": "Name", + "scenes.app.shared_with_me.shared_by": "Shared by", + "scenes.app.shared_with_me.shared_date": "Shared date", + "scenes.app.shared_with_me.edit": "Edit" } \ No newline at end of file diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json index f1107b8d..4ca04d9d 100644 --- a/tdrive/frontend/public/locales/fr.json +++ b/tdrive/frontend/public/locales/fr.json @@ -211,54 +211,64 @@ "components.side_menu.trash": "Poubelle", "components.side_menu.home": "Disque partagé", "components.side_menu.my_drive": "Mon disque", - - - - "components.searchpopup.soon": "This feature is coming soon \uD83D\uDE80", - "components.side_menu.buttons.upload": "Upload", - "components.side_menu.buttons.create": "Create", - "components.side_menu.buttons.empty_trash": "Empty 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", - "scenes.app.drive.folders": "Folders", - "scenes.app.drive.used": "used in this folder", + "components.searchpopup.soon": "Cette fonctionnalité arrive bientôt \uD83D\uDE80", + "components.side_menu.buttons.upload": "Télécharger", + "components.side_menu.buttons.create": "Créer", + "components.side_menu.buttons.empty_trash": "Vider la 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", + "scenes.app.drive.folders": "Dossiers", + "scenes.app.drive.used": "utilisé dans ce dossier", "scenes.app.drive.documents": "Documents", - "scenes.app.drive.context_menu": "More", - "components.disk_usage.used": "used,", - "components.disk_usage.in_trash": "in trash", - "components.create_modal.create_folder_or_doc": "Create document or folder", - "components.create_modal.create_folder": "Create a folder", - "components.create_modal.upload_files": "Upload files from device", - "components.create_modal.upload_folders": "Upload folders from device", - "components.create_modal.create_link": "Create a link file", - "components.create_folder_modal.hint": "Choose a name for the new folder.", - "components.create_folder_modal.placeholder": "Folder name", - "components.create_link_modal.hint": "Link name", - "components.create_link_modal.button": "Create link", - "components.pending_file_list.estimation.end": "Will end", - "components.pending_file_list.estimation.approximations": "Waiting for time approximations...", - "components.item_context_menu.preview": "Preview", - "components.item_context_menu.download": "Download", - "components.item_context_menu.rename": "Rename", - "components.item_context_menu.manage_access": "Manage access", - "components.item_context_menu.share": "Share", - "components.item_context_menu.copy_link": "Copy public link", - "components.item_context_menu.copy_link.success": "Public link copied to clipboard", + "scenes.app.drive.context_menu": "Plus", + "components.disk_usage.used": "utilisé,", + "components.disk_usage.in_trash": "dans la corbeille", + "components.create_modal.create_folder_or_doc": "Créer un document ou un dossier", + "components.create_modal.create_folder": "Créer un dossier", + "components.create_modal.upload_files": "Télécharger des fichiers depuis l'appareil", + "components.create_modal.upload_folders": "Télécharger des dossiers depuis l'appareil", + "components.create_modal.create_link": "Créer un lien", + "components.create_folder_modal.hint": "Choisissez un nom pour le nouveau dossier.", + "components.create_folder_modal.placeholder": "Nom du dossier", + "components.create_link_modal.hint": "Nom du lien", + "components.create_link_modal.button": "Créer le lien", + "components.pending_file_list.estimation.end": "Se terminera", + "components.pending_file_list.estimation.approximations": "En attente d'estimations de temps...", + "components.item_context_menu.preview": "Aperçu", + "components.item_context_menu.download": "Télécharger", + "components.item_context_menu.rename": "Renommer", + "components.item_context_menu.manage_access": "Gérer l'accès", + "components.item_context_menu.share": "Partager", + "components.item_context_menu.copy_link": "Copier le lien public", + "components.item_context_menu.copy_link.success": "Lien public copié dans le presse-papiers", "components.item_context_menu.versions": "Versions", - "components.item_context_menu.move": "Move", - "components.item_context_menu.move.modal_header": "Move", - "components.item_context_menu.move_to_trash": "Move to trash", - "components.item_context_menu.delete": "Delete", - "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", - "components.item_context_menu.clear_selection": "Clear selection", - "components.item_context_menu.delete_multiple": "Delete", - "components.item_context_menu.to_trash_multiple": "Move selected items to trash", - "components.item_context_menu.trash.exit": "Exit trash", - "components.item_context_menu.trash.empty": "Empty trash", - "components.item_context_menu.add_documents": "Add document or folder", - "components.item_context_menu.download_folder": "Download folder", - "components.item_context_menu.go_to_trash": "Go to trash" + "components.item_context_menu.move": "Déplacer", + "components.item_context_menu.move.modal_header": "Déplacer", + "components.item_context_menu.move_to_trash": "Déplacer vers la corbeille", + "components.item_context_menu.delete": "Supprimer", + "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", + "components.item_context_menu.clear_selection": "Effacer la sélection", + "components.item_context_menu.delete_multiple": "Supprimer", + "components.item_context_menu.to_trash_multiple": "Déplacer les éléments sélectionnés vers la corbeille", + "components.item_context_menu.trash.exit": "Quitter la corbeille", + "components.item_context_menu.trash.empty": "Vider la corbeille", + "components.item_context_menu.add_documents": "Ajouter un document ou un dossier", + "components.item_context_menu.download_folder": "Télécharger le dossier", + "components.item_context_menu.go_to_trash": "Aller à la corbeille", + "components.item_context_menu.all": "Tous", + "components.item_context_menu.today": "Aujourd'hui", + "components.item_context_menu.last_week": "La semaine dernière", + "components.item_context_menu.last_month": "Le mois dernier", + "scenes.app.shared_with_me.shared_with_me": "Partagé avec moi", + "scenes.app.shared_with_me.file_type": "Type de fichier", + "scenes.app.shared_with_me.people": "Personnes", + "scenes.app.shared_with_me.last_modified": "Dernière modification", + "scenes.app.shared_with_me.documents": "Documents", + "scenes.app.shared_with_me.name": "Nom", + "scenes.app.shared_with_me.shared_by": "Partagé par", + "scenes.app.shared_with_me.shared_date": "Date de partage", + "scenes.app.shared_with_me.edit": "Modifier" } \ No newline at end of file diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json index ad11fba3..1f3c2b39 100644 --- a/tdrive/frontend/public/locales/ru.json +++ b/tdrive/frontend/public/locales/ru.json @@ -257,5 +257,18 @@ "components.item_context_menu.trash.empty": "Очистить корзину", "components.item_context_menu.add_documents": "Добавить документ в папку", "components.item_context_menu.download_folder": "Скачать папку", - "components.item_context_menu.go_to_trash": "Перейти в корзину" + "components.item_context_menu.go_to_trash": "Перейти в корзину", + "components.item_context_menu.all": "Все", + "components.item_context_menu.today": "Сегодня", + "components.item_context_menu.last_week": "За неделю", + "components.item_context_menu.last_month": "За месяц", + "scenes.app.shared_with_me.shared_with_me": "Доступные мне", + "scenes.app.shared_with_me.file_type": "Тип файла", + "scenes.app.shared_with_me.people": "Люди", + "scenes.app.shared_with_me.last_modified": "Последнее изменение", + "scenes.app.shared_with_me.documents": "Файлы", + "scenes.app.shared_with_me.name": "Название", + "scenes.app.shared_with_me.shared_by": "Кто открыл доступ", + "scenes.app.shared_with_me.shared_date": "Дата предоставления доступа", + "scenes.app.shared_with_me.edit": "Редактировать" } \ No newline at end of file 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 4841952f..7b4a40f1 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 @@ -17,6 +17,10 @@ export type SearchDocumentsBody = { creator?: string; added?: string; }; +export type sharedWithMeFilterBody = { + mime_type?: string; + creator?: string; +}; let publicLinkToken: null | string = null; let tdriveTabToken: null | string = null; @@ -132,12 +136,12 @@ export class DriveApiClient { ); } - static async search(searchString: string, view?: string,options?: BaseSearchOptions) { + static async search(searchString: string, view?: string, options?: BaseSearchOptions) { const companyId = options?.company_id ? options.company_id : Workspace.currentGroupId; const query = `/internal/services/documents/v1/companies/${companyId}/search`; const searchData = { search: searchString, - view: view + view: view, }; const res = await Api.post(query, searchData); this.logger.debug( @@ -149,11 +153,14 @@ export class DriveApiClient { return res; } - static async sharedWithMe(options?: BaseSearchOptions) { + static async sharedWithMe(filter?: any, options?: BaseSearchOptions) { const companyId = options?.company_id ? options.company_id : Workspace.currentGroupId; const query = `/internal/services/documents/v1/companies/${companyId}/shared-with-me`; - const filterData = {}; - const res = await Api.post(query, filterData); + const filterData = { mime_type: filter.mimeType, creator: filter.creator, view: "shared_with_me" }; + const res = await Api.post( + query, + filterData, + ); this.logger.debug( `Drive shared with me by filter "${JSON.stringify(filterData)}". Found`, res.entities.length, 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 4f13b603..87cb0dbc 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 @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { useRecoilCallback } from 'recoil'; import { DriveApiClient } from '../api-client/api-client'; import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store'; -import { DriveItem, DriveItemDetails, DriveItemVersion } from '../types'; +import { DriveItem, DriveItemVersion } from '../types'; /** * Returns the children of a drive item @@ -18,59 +18,20 @@ export const useDriveActions = () => { ({ set, snapshot }) => async (parentId: string) => { if (parentId) { - if (parentId == "shared-with-me") { - const details = { - path: [ - { - id: "shared-with-me", - name: "Shared with me" - } - ], - item: { - id: "root", - parent_id: "", - company_id: "", - workspace_id: "", - name: "Shared with me", - size: 0, - description: "", - tags: [], - in_trash: false, - is_directory: true, - extension: "", - added: "", - last_modified: "", - last_version_cache: {}, - access_info: {}, - }, - versions: [], - children: [], - access: "manage", - websockets: [ - { - room: "/companies/aa7ffdd0-fadb-11ed-b891-510fe3501b2b/documents/item/root", - token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYTU0Y2YyMC1mYWRiLTExZWQtYjg5MS01MTBmZTM1MDFiMmIiLCJuYW1lIjoiL2NvbXBhbmllcy9hYTdmZmRkMC1mYWRiLTExZWQtYjg5MS01MTBmZTM1MDFiMmIvZG9jdW1lbnRzL2l0ZW0vcm9vdCIsImlhdCI6MTY4ODk1MDk2OCwibmJmIjoxNjg2MjcyNTA4fQ.H6BFRcLG3Op32sqKi45Pf1s2YKcVbMxGZGPJal06l1g" - } - ] - }; + try { + const details = await DriveApiClient.get(companyId, parentId); set(DriveItemChildrenAtom(parentId), details.children); - return details; - } else { - try { - const details = await DriveApiClient.get(companyId, parentId); - set(DriveItemChildrenAtom(parentId), details.children); - set(DriveItemAtom(parentId), details); - for (const child of details.children) { - const currentValue = snapshot.getLoadable(DriveItemAtom(child.id)).contents; - if (!currentValue) { - //only update if not already in cache to avoid concurrent updates - set(DriveItemAtom(child.id), { item: child }); - } + set(DriveItemAtom(parentId), details); + for (const child of details.children) { + const currentValue = snapshot.getLoadable(DriveItemAtom(child.id)).contents; + if (!currentValue) { + //only update if not already in cache to avoid concurrent updates + set(DriveItemAtom(child.id), { item: child }); } - return details; - } catch (e) { - ToasterService.error('Unable to load your files.'); } + return details; + } catch (e) { + ToasterService.error('Unable to load your files.'); } } }, diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-shared-with-me-drive-items.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-shared-with-me-drive-items.tsx index 5407a538..452ded83 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-shared-with-me-drive-items.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-shared-with-me-drive-items.tsx @@ -7,16 +7,16 @@ import _ from 'lodash'; import { useRecoilState, useRecoilValue } from 'recoil'; import { SharedWithMeDriveItemsResultsState } from '../state/shared-with-me-drive-items-result'; import { SearchInputState } from '../../search/state/search-input'; +import { SharedWithMeFilterState } from '../state/shared-with-me-filter'; export const useSharedWithMeDriveItemsLoading = () => { return useRecoilValue(LoadingState('useSearchDriveItems')); }; -let currentQuery = ''; - export const useSharedWithMeDriveItems = () => { const companyId = useRouterCompany(); const searchInput = useRecoilValue(SearchInputState); + const sharedFilter = useRecoilValue(SharedWithMeFilterState); const [loading, setLoading] = useRecoilState(LoadingState('useSearchDriveItems')); const [items, setItems] = useRecoilState(SharedWithMeDriveItemsResultsState(companyId)); @@ -34,11 +34,24 @@ export const useSharedWithMeDriveItems = () => { const refresh = async () => { setLoading(true); - const query = searchInput.query; - currentQuery = query; + let filter:any = {...sharedFilter}; + if (filter.date) { + if (filter.date === "today") { + filter = { ...filter, added_lt: '', added_gt: '' }; + } + if (filter.date === "last_week") { + const today = new Date(); + const lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + filter = { ...filter, added_lt: lastWeek.toISOString(), added_gt: '' }; + } + if (filter.date === "last_month") { + const today = new Date(); + const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()); + filter = { ...filter, added_lt: lastMonth.toISOString(), added_gt: '' }; + } + } - const response = await DriveApiClient.sharedWithMe(opt); - console.log("response is: ", response); + const response = await DriveApiClient.sharedWithMe(filter, opt); const results = response.entities || []; const update = { @@ -47,9 +60,6 @@ export const useSharedWithMeDriveItems = () => { // nextPage: response.next_page_token, }; - if (currentQuery !== query) { - return; - } setItems(update); setLoading(false); }; @@ -62,18 +72,18 @@ export const useSharedWithMeDriveItems = () => { useGlobalEffect( 'useSearchDriveItems', () => { - (async () => { - setLoading(true); - if (searchInput.query) { - delayRequest('useSearchDriveItems', async () => { - await refresh(); - }); - } else { - refresh(); - } - })(); + (async () => { + setLoading(true); + if (sharedFilter.mimeType) { + delayRequest('useSearchDriveItems', async () => { + await refresh(); + }); + } else { + refresh(); + } + })(); }, - [searchInput.channelId, searchInput.workspaceId], + [sharedFilter, searchInput.channelId, searchInput.workspaceId], ); return { loading, driveItems: [...items.results], loadMore, refresh }; diff --git a/tdrive/frontend/src/app/features/drive/state/shared-with-me-filter.tsx b/tdrive/frontend/src/app/features/drive/state/shared-with-me-filter.tsx new file mode 100644 index 00000000..dacda8b7 --- /dev/null +++ b/tdrive/frontend/src/app/features/drive/state/shared-with-me-filter.tsx @@ -0,0 +1,17 @@ +import { string } from 'prop-types'; +import { atom } from 'recoil'; + +export type SharedWithMeFilter = { + mimeType: string; + creator: string; + date: string; +}; + +export const SharedWithMeFilterState = atom({ + key: 'SharedWithMeFilterState', + default: { + mimeType: '', + creator: '', + date: '', + }, +}); \ No newline at end of file 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 c6705a36..ded3c420 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -77,6 +77,7 @@ export default memo( const uploadZoneRef = useRef(null); const setCreationModalState = useSetRecoilState(CreateModalAtom); + const [checked, setChecked] = useRecoilState(DriveItemSelectedList); const setParentId = useCallback( @@ -130,6 +131,9 @@ export default memo( <> {viewId == 'shared-with-me' ? ( <> + }> + + ) : ( 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 d798a8b0..52c5e00a 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 @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { DriveCurrentFolderAtom } from './browser'; import { ConfirmDeleteModalAtom } from './modals/confirm-delete'; @@ -16,7 +16,10 @@ import { DriveItemSelectedList } from '@features/drive/state/store'; import { DriveItem, DriveItemDetails } from '@features/drive/types'; import { ToasterService } from '@features/global/services/toaster-service'; import { copyToClipboard } from '@features/global/utils/CopyClipboard'; -import Languages from "features/global/services/languages-service"; +import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter'; +import { getCurrentUserList, getUser } from '@features/users/hooks/use-user-list'; +import _ from 'lodash'; +import Languages from 'features/global/services/languages-service'; /** * This will build the context menu in different contexts @@ -90,7 +93,9 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s hide: !item.access_info.public?.level || item.access_info.public?.level === 'none', onClick: () => { copyToClipboard(getPublicLink(item || parent?.item)); - ToasterService.success(Languages.t('components.item_context_menu.copy_link.success')); + ToasterService.success( + Languages.t('components.item_context_menu.copy_link.success'), + ); }, }, { @@ -108,7 +113,9 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s open: true, parent_id: inTrash ? 'root' : item.parent_id, mode: 'move', - title: Languages.t('components.item_context_menu.move.modal_header') + ` '${item.name}'`, + title: + Languages.t('components.item_context_menu.move.modal_header') + + ` '${item.name}'`, onSelected: async ids => { await update( { @@ -254,7 +261,9 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s parent?.item?.access_info?.public?.level === 'none', onClick: () => { copyToClipboard(getPublicLink(item || parent?.item)); - ToasterService.success(Languages.t('components.item_context_menu.copy_link.success')); + ToasterService.success( + Languages.t('components.item_context_menu.copy_link.success'), + ); }, }, { type: 'separator', hide: inTrash || parent.access === 'read' }, @@ -297,63 +306,138 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s ); }; -export const onBuildFileTypeContextMenu = () => { - const menuItems = [ - { - type: 'menu', - text: 'PDF', - }, - { - type: 'menu', - text: 'DOC', - }, - { - type: 'menu', - text: 'PNG', - }, +export const useOnBuildFileTypeContextMenu = () => { + const [filter, setFilter] = useRecoilState(SharedWithMeFilterState); + const mimeTypes = [ + { key: Languages.t('components.item_context_menu.all'), value: '' }, + { key: 'PDF', value: 'application/pdf' }, + { key: 'DOC', value: 'application/msword' }, + { key: 'PNG', value: 'image/png' }, ]; - return menuItems; + return useCallback(() => { + const menuItems = mimeTypes.map(item => { + return { + type: 'menu', + text: item.key, + onClick: () => { + setFilter(prevFilter => { + const newFilter = { + ...prevFilter, + mimeType: item.value, + }; + return newFilter; + }); + }, + }; + }); + return menuItems; + }, [setFilter]); }; -export const onBuildPeopleContextMenu = () => { - const menuItems = [ - { - type: 'menu', - text: 'Dwho', - }, - ]; - return menuItems; + +export const useOnBuildPeopleContextMenu = () => { + const [filter, setFilter] = useRecoilState(SharedWithMeFilterState); + const [_userList, setUserList] = useState(getCurrentUserList()); + let userList = _userList; + userList = _.uniqBy(userList, 'id'); + return useCallback(() => { + const menuItems = userList.map(user => { + return { + type: 'menu', + text: user.first_name, + onClick: () => { + setFilter(prevFilter => { + const newFilter = { + ...prevFilter, + creator: user.id ?? '', + }; + return newFilter; + }); + }, + }; + }); + return menuItems; + }, [setFilter]); }; -export const onBuildDateContextMenu = () => { - const menuItems = [ - { - type: 'menu', - text: 'All', - }, - { - type: 'menu', - text: 'Today', - }, - { - type: 'menu', - text: 'Last week', - }, - { - type: 'menu', - text: 'Last month', - }, - { - type: 'menu', - text: 'Range', - }, - ]; - return menuItems; + +export const useOnBuildDateContextMenu = () => { + const [filter, setFilter] = useRecoilState(SharedWithMeFilterState); + return useCallback(() => { + const menuItems = [ + { + type: 'menu', + text: Languages.t('components.item_context_menu.all'), + onClick: () => { + setFilter(prevFilter => { + const newFilter = { + ...prevFilter, + date: '', + }; + return newFilter; + }); + }, + }, + { + type: 'menu', + text: Languages.t('components.item_context_menu.today'), + onClick: () => { + setFilter(prevFilter => { + const newFilter = { + ...prevFilter, + date: 'today', + }; + return newFilter; + }); + }, + }, + { + type: 'menu', + text: Languages.t('components.item_context_menu.last_week'), + onClick: () => { + setFilter(prevFilter => { + const newFilter = { + ...prevFilter, + date: 'last_week', + }; + return newFilter; + }); + }, + }, + { + type: 'menu', + text: Languages.t('components.item_context_menu.last_month'), + onClick: () => { + setFilter(prevFilter => { + const newFilter = { + ...prevFilter, + date: 'last_month', + }; + return newFilter; + }); + }, + }, + ]; + return menuItems; + }, [setFilter]); }; -export const onBuildFileContextMenu = (id: string) => { - const menuItems = [ - { - type: 'menu', - text: 'Download' +export const useOnBuildFileContextMenu = () => { + const { download } = useDriveActions(); + const { open: preview } = useDrivePreview(); + return useCallback( + (item: DriveItem) => { + const menuItems = [ + { + type: 'menu', + text: Languages.t('components.item_context_menu.preview'), + onClick: () => preview(item), + }, + { + type: 'menu', + text: Languages.t('components.item_context_menu.download'), + onClick: () => download(item.id), + }, + ]; + return menuItems; }, - ]; - return menuItems; + [download, preview], + ); }; diff --git a/tdrive/frontend/src/app/views/client/body/drive/shared-files-table.tsx b/tdrive/frontend/src/app/views/client/body/drive/shared-files-table.tsx index f85bfcc1..c32b8de4 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/shared-files-table.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/shared-files-table.tsx @@ -1,139 +1,138 @@ -import { useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { ChevronDownIcon, ChevronUpIcon, DotsHorizontalIcon } from '@heroicons/react/outline'; import { Button } from '@atoms/button/button'; import { Base, BaseSmall, Title } from '@atoms/text'; import Menu from '@components/menus/menu'; +import { useRecoilState } from 'recoil'; import { - onBuildFileTypeContextMenu, - onBuildPeopleContextMenu, - onBuildDateContextMenu, - onBuildFileContextMenu, + useOnBuildFileTypeContextMenu, + useOnBuildPeopleContextMenu, + useOnBuildDateContextMenu, + useOnBuildFileContextMenu, } from './context-menu'; import { useSharedWithMeDriveItems } from '@features/drive/hooks/use-shared-with-me-drive-items'; +import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter'; +import MenusManager from '@components/menus/menus-manager.jsx'; +import Languages from '@features/global/services/languages-service'; export const SharedFilesTable = () => { const { driveItems, loading } = useSharedWithMeDriveItems(); - const [sortBy, setSortBy] = useState('name'); - const [sortOrder, setSortOrder] = useState('asc'); + const [filter, setFilter] = useRecoilState(SharedWithMeFilterState); - const handleSort = (column: string) => { - if (sortBy === column) { - setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); - } else { - setSortBy(column); - setSortOrder('asc'); - } - }; + // FILTER HOOKS + const buildFileTypeContextMenu = useOnBuildFileTypeContextMenu(); + const buildPeopleContextMen = useOnBuildPeopleContextMenu(); + const buildFileContextMenu = useOnBuildFileContextMenu(); + const buildDateContextMenu = useOnBuildDateContextMenu(); - const renderSortIcon = (column: string) => { - if (sortBy === column) { - if (sortOrder === 'asc') { - return ; - } else { - return ; - } - } - return null; + const fileAddedDate = (timestamp: number) => { + const [formattedDate, formattedTime] = new Date(timestamp).toLocaleString().split(', '); + return formattedDate; }; return ( - <> - Shared with me +
+ {Languages.t('scenes.app.shared_with_me.shared_with_me')} {/* Filters */}
+
+ +
- onBuildFileTypeContextMenu()}> - - +
- onBuildPeopleContextMenu()}> - - -
- -
- onBuildDateContextMenu()}> - - +
- Documents: + {Languages.t('scenes.app.drive.documents')}:
{!loading && - driveItems - .sort((a: any, b: any) => { - // Perform sorting based on the selected column and order - console.log("a is: ", a); - console.log("b is: ", b); - if (sortBy === 'name') { - return sortOrder === 'asc' - ? a.name.localeCompare(b.name) - : b.name.localeCompare(a.name); - } - return 0; // No sorting by default - }) - .map((file: any, index: any) => ( - ( + + - - - - - ))} + {file.name} + + + + + + ))}
- handleSort('name')}> - Name {renderSortIcon('name')} - + {Languages.t('scenes.app.shared_with_me.name')} - handleSort('name')}> - Shared By {renderSortIcon('name')} - + {Languages.t('scenes.app.shared_with_me.shared_by')} - handleSort('name')}> - Shared Date {renderSortIcon('name')} - + Shared Date{Languages.t('scenes.app.shared_with_me.shared_date')} - Edit + {Languages.t('scenes.app.shared_with_me.edit')}
- - {file.name} - Dwho2023-05-05 - - -
{file.creator?.id}{fileAddedDate(file.added)} + + +
- +
); };