🔢 Shared with me filters (#79)

 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 <ashepilov@linagora.com>
This commit is contained in:
Montassar Ghanmy
2023-06-17 15:37:33 +01:00
committed by GitHub
parent fddd3b4ebd
commit 441c2b780f
19 changed files with 482 additions and 404 deletions
@@ -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<unknown, CLIArgs> = {
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;
@@ -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;
@@ -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;
};
@@ -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<string, string[]> = 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<DriveFile>,
@@ -59,8 +60,6 @@ export class DriveFileDTOBuilder {
fields?: string[],
view?: string,
): Promise<ListResult<DriveFileDTO>> {
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,
@@ -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) {
@@ -48,6 +48,7 @@ export type SearchDocumentsOptions = {
view?: string;
fields?: string[];
onlyDirectlyShared?: boolean;
onlyUploadedNotByMe?: boolean;
};
export type SearchDocumentsBody = {
@@ -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) {
@@ -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();
@@ -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<DriveFile>("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<DriveFile>("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<UserServiceImpl>();
subj.usersService.list = jest.fn().mockReturnValue(new ListResult("users", [file_creator]));
const usersService = mock<UserServiceImpl>() 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<UserServiceImpl>();
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(
+14 -1
View File
@@ -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"
}
+58 -48
View File
@@ -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"
}
+14 -1
View File
@@ -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": "Редактировать"
}
@@ -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<SearchDocumentsBody, { entities: DriveItem[] }>(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<SearchDocumentsBody, { entities: DriveItem[] }>(query, filterData);
const filterData = { mime_type: filter.mimeType, creator: filter.creator, view: "shared_with_me" };
const res = await Api.post<sharedWithMeFilterBody, { entities: DriveItem[] }>(
query,
filterData,
);
this.logger.debug(
`Drive shared with me by filter "${JSON.stringify(filterData)}". Found`,
res.entities.length,
@@ -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.');
}
}
},
@@ -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 };
@@ -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<SharedWithMeFilter>({
key: 'SharedWithMeFilterState',
default: {
mimeType: '',
creator: '',
date: '',
},
});
@@ -77,6 +77,7 @@ export default memo(
const uploadZoneRef = useRef<UploadZone | null>(null);
const setCreationModalState = useSetRecoilState(CreateModalAtom);
const [checked, setChecked] = useRecoilState(DriveItemSelectedList);
const setParentId = useCallback(
@@ -130,6 +131,9 @@ export default memo(
<>
{viewId == 'shared-with-me' ? (
<>
<Suspense fallback={<></>}>
<DrivePreview />
</Suspense>
<SharedFilesTable />
</>
) : (
@@ -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],
);
};
@@ -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 <ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />;
} else {
return <ChevronUpIcon className="h-4 w-4 ml-2 -mr-1" />;
}
}
return null;
const fileAddedDate = (timestamp: number) => {
const [formattedDate, formattedTime] = new Date(timestamp).toLocaleString().split(', ');
return formattedDate;
};
return (
<>
<Title className="mb-4 block">Shared with me</Title>
<div>
<Title className="mb-4 block">{Languages.t('scenes.app.shared_with_me.shared_with_me')}</Title>
{/* Filters */}
<div className="flex items-center space-x-4 mb-6">
<div className="">
<Button
theme="secondary"
className="flex items-center"
onClick={evt => {
MenusManager.openMenu(
buildFileTypeContextMenu(),
{ x: evt.clientX, y: evt.clientY },
'center',
);
}}
>
<span>{filter.mimeType ? filter.mimeType : Languages.t('scenes.app.shared_with_me.file_type')}</span>
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
</Button>
</div>
<div className="flex items-center space-x-2">
<Menu menu={() => onBuildFileTypeContextMenu()}>
<Button theme="secondary" className="flex items-center">
<span>File Type</span>
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
</Button>
</Menu>
<Button
theme="secondary"
className="flex items-center"
onClick={evt => {
MenusManager.openMenu(
buildPeopleContextMen(),
{ x: evt.clientX, y: evt.clientY },
'center',
);
}}
>
<span>{Languages.t('scenes.app.shared_with_me.people')}</span>
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
</Button>
</div>
<div className="flex items-center space-x-2">
<Menu menu={() => onBuildPeopleContextMenu()}>
<Button theme="secondary" className="flex items-center">
<span>People</span>
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
</Button>
</Menu>
</div>
<div className="flex items-center space-x-2">
<Menu menu={() => onBuildDateContextMenu()}>
<Button theme="secondary" className="flex items-center">
<span>Last Modified</span>
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
</Button>
</Menu>
<Button
theme="secondary"
className="flex items-center"
onClick={evt => {
MenusManager.openMenu(
buildDateContextMenu(),
{ x: evt.clientX, y: evt.clientY },
'center',
);
}}
>
<span>{Languages.t('scenes.app.shared_with_me.last_modified')}</span>
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
</Button>
</div>
</div>
<Title className="mb-4 block">Documents:</Title>
<Title className="mb-4 block">{Languages.t('scenes.app.drive.documents')}:</Title>
<div className="relative overflow-x-auto shadow-md sm:rounded-lg">
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-blue-500 dark:text-white">
<tr>
<th scope="col" className="px-6 py-3">
<span className="flex" onClick={() => handleSort('name')}>
Name {renderSortIcon('name')}
</span>
<span className="flex">{Languages.t('scenes.app.shared_with_me.name')}</span>
</th>
<th scope="col" className="px-6 py-3">
<span className="flex" onClick={() => handleSort('name')}>
Shared By {renderSortIcon('name')}
</span>
<span className="flex">{Languages.t('scenes.app.shared_with_me.shared_by')}</span>
</th>
<th scope="col" className="px-6 py-3">
<span className="flex" onClick={() => handleSort('name')}>
Shared Date {renderSortIcon('name')}
</span>
<span className="flex">Shared Date{Languages.t('scenes.app.shared_with_me.shared_date')}</span>
</th>
<th scope="col" className="px-6 py-3">
<span className="sr-only">Edit</span>
<span className="sr-only">{Languages.t('scenes.app.shared_with_me.edit')}</span>
</th>
</tr>
</thead>
<tbody>
{!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) => (
<tr
key={index}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
driveItems.map((file: any, index: any) => (
<tr
key={index}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
>
<th
scope="row"
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<th
scope="row"
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
{file.name}
</th>
<td className="px-6 py-4">Dwho</td>
<td className="px-6 py-4">2023-05-05</td>
<td className="px-6 py-4 text-right">
<Menu menu={onBuildFileContextMenu(file.id)}>
<Button
theme={'secondary'}
size="sm"
className={'!rounded-full '}
icon={DotsHorizontalIcon}
/>
</Menu>
</td>
</tr>
))}
{file.name}
</th>
<td className="px-6 py-4">{file.creator?.id}</td>
<td className="px-6 py-4">{fileAddedDate(file.added)}</td>
<td className="px-6 py-4 text-right">
<Menu menu={buildFileContextMenu(file)}>
<Button
theme={'secondary'}
size="sm"
className={'!rounded-full '}
icon={DotsHorizontalIcon}
/>
</Menu>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
</div>
);
};