From e76da87916f363971445207ae5089d89ca2cfa5b Mon Sep 17 00:00:00 2001 From: Montassar Ghanmy Date: Mon, 15 Jul 2024 22:56:34 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Sort=20files=20chronologically=20+?= =?UTF-8?q?=20infinite=20scroll=20(#535)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orm/connectors/mongodb/mongodb.ts | 18 +- .../postgres/postgres-query-builder.ts | 9 +- .../services/orm/repository/repository.ts | 7 + .../node/src/services/console/service.ts | 16 +- .../src/services/documents/services/index.ts | 40 +++- .../node/src/services/documents/types.ts | 16 ++ .../documents/web/controllers/documents.ts | 28 ++- .../documents-pagination-sorting.spec.ts | 156 ++++++++++++++ .../postgres/postgres-query-builder.test.ts | 51 +++++ .../app/atoms/icons-agnostic/assets/sort.svg | 3 + .../icons-agnostic/icons-agnostic.stories.tsx | 2 + .../src/app/atoms/icons-agnostic/index.tsx | 2 + .../src/app/components/menus/menu.jsx | 8 + .../features/drive/api-client/api-client.ts | 16 +- .../drive/hooks/use-drive-actions.tsx | 54 +++-- .../features/drive/hooks/use-drive-item.tsx | 65 +++++- .../features/drive/hooks/use-drive-preview.ts | 8 +- .../features/drive/hooks/use-drive-upload.tsx | 4 +- .../src/app/features/drive/state/store.ts | 19 +- .../frontend/src/app/features/drive/types.ts | 17 ++ .../app/views/client/body/drive/browser.tsx | 203 ++++++++++-------- .../views/client/body/drive/context-menu.tsx | 89 +++++++- .../body/drive/documents/document-row.tsx | 17 +- .../drive/documents/folder-row-skeleton.tsx | 23 ++ .../app/views/client/viewer/drive-preview.tsx | 1 + 25 files changed, 719 insertions(+), 153 deletions(-) create mode 100644 tdrive/backend/node/test/e2e/documents/documents-pagination-sorting.spec.ts create mode 100644 tdrive/frontend/src/app/atoms/icons-agnostic/assets/sort.svg create mode 100644 tdrive/frontend/src/app/views/client/body/drive/documents/folder-row-skeleton.tsx 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 16d5dc29..598f419c 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 @@ -264,12 +264,22 @@ export class MongoConnector extends AbstractConnector { options, ); - const sort: any = {}; + let sort: any = {}; for (const key of entityDefinition.options.primaryKey.slice(1)) { const defaultOrder = (columnsDefinition[key as string].options.order || "ASC") === "ASC" ? 1 : -1; sort[key as string] = (options?.pagination?.reversed ? -1 : 1) * defaultOrder; } + if (options?.sort) { + sort = options.sort + ? Object.fromEntries( + Object.entries(options.sort).map(([field, direction]) => [ + field, + direction === "asc" ? 1 : -1, + ]), + ) + : {}; + } logger.debug(`services.database.orm.mongodb.find - Query: ${JSON.stringify(query)}`); @@ -277,7 +287,11 @@ export class MongoConnector extends AbstractConnector { .find(query) .sort(sort) .skip(Math.max(0, parseInt(options.pagination.page_token || "0"))) - .limit(Math.max(0, parseInt(options.pagination.limitStr || "100"))); + .limit(Math.max(0, parseInt(options.pagination.limitStr || "100"))) + .collation({ + locale: "en_US", + numericOrdering: true, + }); const entities: Table[] = []; while (await cursor.hasNext()) { diff --git a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/postgres/postgres-query-builder.ts b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/postgres/postgres-query-builder.ts index 33ed35cf..7d13f095 100644 --- a/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/postgres/postgres-query-builder.ts +++ b/tdrive/backend/node/src/core/platform/services/database/services/orm/connectors/postgres/postgres-query-builder.ts @@ -88,13 +88,20 @@ export class PostgresQueryBuilder { if (whereClause && whereClause.endsWith("AND ")) whereClause = whereClause.slice(0, -4); // ==== ORDER BY ===== - const orderByClause = `${entityDefinition.options.primaryKey + let orderByClause = `${entityDefinition.options.primaryKey .slice(1) .map( (key: string) => `${key} ${(columnsDefinition[key].options.order || "ASC") === "ASC" ? "DESC" : "ASC"}`, )}`; + // ==== ORDER BY CUSTOM COLUMN ===== + if (options?.sort) { + orderByClause = Object.keys(options.sort) + .map(key => `${key} ${options.sort[key].toUpperCase()}`) + .join(", "); + } + // ==== PAGING ===== let limit = 100; let offset = 0; diff --git a/tdrive/backend/node/src/core/platform/services/database/services/orm/repository/repository.ts b/tdrive/backend/node/src/core/platform/services/database/services/orm/repository/repository.ts index dea74959..38854a09 100644 --- a/tdrive/backend/node/src/core/platform/services/database/services/orm/repository/repository.ts +++ b/tdrive/backend/node/src/core/platform/services/database/services/orm/repository/repository.ts @@ -22,6 +22,12 @@ export type inType = [string, Array]; // eslint-disable-next-line @typescript-eslint/no-explicit-any type likeType = [string, any]; +type SortDirection = "asc" | "desc"; + +type SortOption = { + [field: string]: SortDirection; +}; + export type FindOptions = { pagination?: Pagination; $lt?: comparisonType[]; @@ -34,6 +40,7 @@ export type FindOptions = { $in?: inType[]; $nin?: inType[]; $like?: likeType[]; + sort?: SortOption; }; /** diff --git a/tdrive/backend/node/src/services/console/service.ts b/tdrive/backend/node/src/services/console/service.ts index e4d72a3c..ce3ab559 100644 --- a/tdrive/backend/node/src/services/console/service.ts +++ b/tdrive/backend/node/src/services/console/service.ts @@ -35,14 +35,14 @@ export class ConsoleServiceImpl implements TdriveServiceProvider { this.consoleOptions = { type: type, - authority: s.authority, - client_id: s.client_id, - client_secret: s.client_secret, - audience: s.audience, - issuer: s.issuer, - jwks_uri: s.jwks_uri, - redirect_uris: s.redirect_uris, - disable_account_creation: s.disable_account_creation, + authority: s?.authority, + client_id: s?.client_id, + client_secret: s?.client_secret, + audience: s?.audience, + issuer: s?.issuer, + jwks_uri: s?.jwks_uri, + redirect_uris: s?.redirect_uris, + disable_account_creation: s?.disable_account_creation, }; this.consoleOptions.type = type; diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 364052ca..7d4afe05 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -29,8 +29,10 @@ import { DriveFileAccessLevel, DriveItemDetails, DriveTdriveTab, + PaginateDocumentBody, RootType, SearchDocumentsOptions, + SortDocumentsBody, TrashType, } from "../types"; import { @@ -105,6 +107,8 @@ export class DocumentsService { browse = async ( id: string, options: SearchDocumentsOptions, + sort: SortDocumentsBody, + paginate: PaginateDocumentBody, context: DriveExecutionContext & { public_token?: string }, ): Promise => { if (isSharedWithMeFolder(id)) { @@ -112,7 +116,7 @@ export class DocumentsService { } else { return { nextPage: null, - ...(await this.get(id, context)), + ...(await this.get(id, context, false, sort, paginate)), }; } }; @@ -154,6 +158,9 @@ export class DocumentsService { get = async ( id: string, context: DriveExecutionContext & { public_token?: string }, + all?: boolean, + sort?: SortDocumentsBody, + paginate?: PaginateDocumentBody, ): Promise => { if (!context) { this.logger.error("invalid context"); @@ -206,7 +213,26 @@ export class DocumentsService { ) ).getEntities(); - //Get children if it is a directory + const sortFieldMapping = { + name: "name", + date: "last_modified", + size: "size", + }; + let sortField = {}; + sortField[sortFieldMapping[sort?.by] || "last_modified"] = sort?.order || "desc"; + + const dbType = await globalResolver.database.getConnector().getType(); + + // Initialize pagination + let pagination; + + if (paginate) { + const { page, limit } = paginate; + const pageNumber = dbType === "mongodb" ? page : page / limit + 1; + + pagination = new Pagination(`${pageNumber}`, `${limit}`, false); + } + let children = isDirectory ? ( await this.repository.find( @@ -229,7 +255,15 @@ export class DocumentsService { is_in_trash: false, }), }, - {}, + all + ? {} + : { + sort: { + is_directory: "desc", + ...sortField, + }, + pagination, + }, context, ) ).getEntities() diff --git a/tdrive/backend/node/src/services/documents/types.ts b/tdrive/backend/node/src/services/documents/types.ts index a96136f3..99c6abec 100644 --- a/tdrive/backend/node/src/services/documents/types.ts +++ b/tdrive/backend/node/src/services/documents/types.ts @@ -59,6 +59,12 @@ export type SearchDocumentsOptions = { pagination?: Paginable; }; +export type BrowseDocumentsOptions = { + filter?: SearchDocumentsBody; + sort?: SortDocumentsBody; + paginate?: PaginateDocumentBody; +}; + export type SearchDocumentsBody = { search?: string; company_id?: string; @@ -73,6 +79,16 @@ export type SearchDocumentsBody = { fields?: string[]; }; +export type SortDocumentsBody = { + by: string; + order: string; +}; + +export type PaginateDocumentBody = { + page: number; + limit: number; +}; + export type DocumentsMessageQueueRequest = { item: DriveFile; version: FileVersion; 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 72bbc572..53c45eeb 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -9,16 +9,19 @@ import { CompanyUserRole, PaginationQueryParameters } from "../../../../utils/ty import { DriveFile } from "../../entities/drive-file"; import { FileVersion } from "../../entities/file-version"; import { + BrowseDocumentsOptions, CompanyExecutionContext, DriveExecutionContext, DriveFileAccessLevel, DriveItemDetails, DriveTdriveTab, ItemRequestParams, + PaginateDocumentBody, ItemRequestByEditingSessionKeyParams, RequestParams, SearchDocumentsBody, SearchDocumentsOptions, + SortDocumentsBody, } from "../../types"; import { DriveFileDTO } from "../dto/drive-file-dto"; import { DriveFileDTOBuilder } from "../../services/drive-file-dto-builder"; @@ -196,7 +199,7 @@ export class DocumentsController { browse = async ( request: FastifyRequest<{ Params: ItemRequestParams; - Body: SearchDocumentsBody; + Body: BrowseDocumentsOptions; Querystring: PaginationQueryParameters & { public_token?: string }; }>, ): Promise => { @@ -204,15 +207,24 @@ export class DocumentsController { const { id } = request.params; const options: SearchDocumentsOptions = { - ...request.body, - company_id: request.body.company_id || context.company.id, + ...request.body.filter, + company_id: request.body.filter?.company_id || context.company.id, view: DriveFileDTOBuilder.VIEW_SHARED_WITH_ME, onlyDirectlyShared: true, onlyUploadedNotByMe: true, }; + const sortOptions: SortDocumentsBody = request.body.sort; + const paginateOptions: PaginateDocumentBody = request.body.paginate; + return { - ...(await globalResolver.services.documents.documents.browse(id, options, context)), + ...(await globalResolver.services.documents.documents.browse( + id, + options, + sortOptions, + paginateOptions, + context, + )), }; }; @@ -446,11 +458,12 @@ export class DocumentsController { downloadZip = async ( request: FastifyRequest<{ Params: RequestParams; - Querystring: { token?: string; items: string; public_token?: string }; + Querystring: { token?: string; items: string; public_token?: string; is_directory?: boolean }; }>, reply: FastifyReply, ): Promise => { const context = getDriveExecutionContext(request); + const isDirectory = request.query.is_directory || false; let ids = (request.query.items || "").split(","); const token = request.query.token; @@ -466,6 +479,11 @@ export class DocumentsController { ids = items.children.map(item => item.id); } + if (isDirectory) { + const items = await globalResolver.services.documents.documents.get(ids[0], context, true); + ids = items.children.map(item => item.id); + } + try { const archive = await globalResolver.services.documents.documents.createZip(ids, context); diff --git a/tdrive/backend/node/test/e2e/documents/documents-pagination-sorting.spec.ts b/tdrive/backend/node/test/e2e/documents/documents-pagination-sorting.spec.ts new file mode 100644 index 00000000..42c05a48 --- /dev/null +++ b/tdrive/backend/node/test/e2e/documents/documents-pagination-sorting.spec.ts @@ -0,0 +1,156 @@ +import { describe, beforeAll, afterAll, it, expect } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import UserApi from "../common/user-api"; + +describe("The Documents Browser Window and API", () => { + let platform: TestPlatform; + let currentUser: UserApi; + + beforeAll(async () => { + platform = await init({ + services: [ + "webserver", + "database", + "applications", + "search", + "storage", + "message-queue", + "user", + "files", + "auth", + "statistics", + "platform-services", + "documents", + ], + }); + currentUser = await UserApi.getInstance(platform); + }); + + afterAll(async () => { + await platform?.tearDown(); + platform = null; + }); + + describe("Pagination and Sorting", () => { + it("Should paginate documents correctly", async () => { + const myDriveId = "user_" + currentUser.user.id; + await currentUser.uploadAllFilesOneByOne(myDriveId); + + let page = 1; + const limit = 2; + let docs = await currentUser.browseDocuments(myDriveId, { + paginate: { page, limit }, + }); + expect(docs).toBeDefined(); + expect(docs.children).toHaveLength(limit); + + page = 2; + docs = await currentUser.browseDocuments(myDriveId, { + paginate: { page, limit }, + }); + expect(docs).toBeDefined(); + expect(docs.children).toHaveLength(limit); + + page = 3; + docs = await currentUser.browseDocuments(myDriveId, { + paginate: { page, limit }, + }); + expect(docs).toBeDefined(); + expect(docs.children.length).toBeLessThanOrEqual(limit); + }); + + it("Should sort documents by name in ascending order", async () => { + const myDriveId = "user_" + currentUser.user.id; + await currentUser.uploadAllFilesOneByOne(myDriveId); + + const sortBy = "name"; + const sortOrder = "asc"; + const docs = await currentUser.browseDocuments(myDriveId, { + sort: { by: sortBy, order: sortOrder }, + }); + expect(docs).toBeDefined(); + + const isSorted = docs.children.every((item, i, arr) => !i || arr[i - 1].name <= item.name); + expect(isSorted).toBe(true); + }); + + it("Should sort documents by name in descending order", async () => { + const myDriveId = "user_" + currentUser.user.id; + await currentUser.uploadAllFilesOneByOne(myDriveId); + + const sortBy = "name"; + const sortOrder = "desc"; + const docs = await currentUser.browseDocuments(myDriveId, { + sort: { by: sortBy, order: sortOrder }, + }); + expect(docs).toBeDefined(); + + const isSorted = docs.children.every((item, i, arr) => !i || arr[i - 1].name >= item.name); + expect(isSorted).toBe(true); + }); + + it("Should sort documents by date in ascending order", async () => { + const myDriveId = "user_" + currentUser.user.id; + await currentUser.uploadAllFilesOneByOne(myDriveId); + + const sortBy = "date"; + const sortOrder = "asc"; + const docs = await currentUser.browseDocuments(myDriveId, { + sort: { by: sortBy, order: sortOrder }, + }); + expect(docs).toBeDefined(); + + const isSorted = docs.children.every( + (item, i, arr) => !i || new Date(arr[i - 1].added) <= new Date(item.added), + ); + expect(isSorted).toBe(true); + }); + + it("Should sort documents by date in descending order", async () => { + const myDriveId = "user_" + currentUser.user.id; + await currentUser.uploadAllFilesOneByOne(myDriveId); + + const sortBy = "date"; + const sortOrder = "desc"; + const docs = await currentUser.browseDocuments(myDriveId, { + sort: { by: sortBy, order: sortOrder }, + }); + expect(docs).toBeDefined(); + + const isSorted = docs.children.every( + (item, i, arr) => !i || new Date(arr[i - 1].added) >= new Date(item.added), + ); + expect(isSorted).toBe(true); + }); + + it("Should sort documents by size in ascending order", async () => { + const myDriveId = "user_" + currentUser.user.id; + await currentUser.uploadAllFilesOneByOne(myDriveId); + + const sortBy = "size"; + const sortOrder = "asc"; + const docs = await currentUser.browseDocuments(myDriveId, { + sort: { by: sortBy, order: sortOrder }, + }); + expect(docs).toBeDefined(); + + const isSorted = docs.children.every((item, i, arr) => !i || arr[i - 1].size <= item.size); + expect(isSorted).toBe(true); + }); + + it("Should sort documents by size in descending order", async () => { + const myDriveId = "user_" + currentUser.user.id; + await currentUser.uploadAllFilesOneByOne(myDriveId); + + const sortBy = "size"; + const sortOrder = "desc"; + const docs = await currentUser.browseDocuments(myDriveId, { + sort: { by: sortBy, order: sortOrder }, + }); + expect(docs).toBeDefined(); + + const isSorted = docs.children.every((item, i, arr) => !i || arr[i - 1].size >= item.size); + expect(isSorted).toBe(true); + }); + }); +}); diff --git a/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/postgres/postgres-query-builder.test.ts b/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/postgres/postgres-query-builder.test.ts index 6380a351..62f7e846 100644 --- a/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/postgres/postgres-query-builder.test.ts +++ b/tdrive/backend/node/test/unit/core/services/database/services/orm/connectors/postgres/postgres-query-builder.test.ts @@ -8,6 +8,20 @@ import { comparisonType } from "../../../../../../../../../src/core/platform/services/database/services/orm/repository/repository"; +type SortDirection = "asc" | "desc"; + +type SortOption = { + [field: string]: SortDirection; +}; + +interface FindOptions { + sort?: SortOption; + pagination?: { + limitStr: string; + page_token: string; + }; +} + describe('The PostgresQueryBuilder', () => { const subj: PostgresQueryBuilder = new PostgresQueryBuilder(""); @@ -103,6 +117,43 @@ describe('The PostgresQueryBuilder', () => { expect(query[1]).toEqual([entity.company_id, entity.id]); }); + test('buildSelect query with sort option', async () => { + //given + const options = { sort: { added: "asc" as SortDirection, id: "desc" as SortDirection } }; + + //when + const query = subj.buildSelect(TestDbEntity, null, options); + + //then + expect(normalizeWhitespace(query[0] as string)).toBe(`SELECT * FROM "test_table" ORDER BY added ASC, id DESC LIMIT 100 OFFSET 0`); + expect(query[1]).toEqual([]); + }); + + test('buildSelect query with pagination', async () => { + //given + const options = { pagination: { limitStr: "50", page_token: "2" } }; + + //when + const query = subj.buildSelect(TestDbEntity, null, options); + + //then + expect(normalizeWhitespace(query[0] as string)).toBe(`SELECT * FROM "test_table" ORDER BY id DESC LIMIT 50 OFFSET 100`); + expect(query[1]).toEqual([]); + }); + + test('buildSelect query with filter, sort, and pagination', async () => { + //given + const filter = { id: randomUUID(), company_id: randomUUID() }; + const options = { sort: { added: "asc" as SortDirection, id: "desc" as SortDirection }, pagination: { limitStr: "10", page_token: "3" } }; + + //when + const query = subj.buildSelect(TestDbEntity, filter as { [key: string]: any }, options); + + //then + expect(normalizeWhitespace(query[0] as string)).toBe(`SELECT * FROM "test_table" WHERE id = $1 AND company_id = $2 ORDER BY added ASC, id DESC LIMIT 10 OFFSET 30`); + expect(query[1]).toEqual([filter.id, filter.company_id]); + }); + test('buildInsert', async () => { //given const entity = newTestDbEntity(); diff --git a/tdrive/frontend/src/app/atoms/icons-agnostic/assets/sort.svg b/tdrive/frontend/src/app/atoms/icons-agnostic/assets/sort.svg new file mode 100644 index 00000000..c6797c5c --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-agnostic/assets/sort.svg @@ -0,0 +1,3 @@ + + + diff --git a/tdrive/frontend/src/app/atoms/icons-agnostic/icons-agnostic.stories.tsx b/tdrive/frontend/src/app/atoms/icons-agnostic/icons-agnostic.stories.tsx index 40644a80..ffcd08e0 100644 --- a/tdrive/frontend/src/app/atoms/icons-agnostic/icons-agnostic.stories.tsx +++ b/tdrive/frontend/src/app/atoms/icons-agnostic/icons-agnostic.stories.tsx @@ -22,6 +22,7 @@ import { StatusCheckIcon, UsersIcon, CheckOutlineIcon, + SortIcon, } from '@atoms/icons-agnostic/index'; export default { @@ -69,6 +70,7 @@ const Template: ComponentStory = () => { } title="StatusCheckDouble" /> } title="StatusCheck" /> } title="Users" /> + } title="Sort" /> ); diff --git a/tdrive/frontend/src/app/atoms/icons-agnostic/index.tsx b/tdrive/frontend/src/app/atoms/icons-agnostic/index.tsx index 8145185f..62d64976 100644 --- a/tdrive/frontend/src/app/atoms/icons-agnostic/index.tsx +++ b/tdrive/frontend/src/app/atoms/icons-agnostic/index.tsx @@ -20,6 +20,7 @@ import { ReactComponent as X } from './assets/x.svg'; import { ReactComponent as StatusCheckDouble } from './assets/status-check-double.svg'; import { ReactComponent as StatusCheck } from './assets/status-check.svg'; import { ReactComponent as Users } from './assets/users.svg'; +import { ReactComponent as Sort } from './assets/sort.svg'; export const CopyIcon = (props: ComponentProps<'svg'>) => ; export const DeleteIcon = (props: ComponentProps<'svg'>) => ; @@ -43,3 +44,4 @@ export const StatusCheckDoubleIcon = (props: ComponentProps<'svg'>) => ( ); export const StatusCheckIcon = (props: ComponentProps<'svg'>) => ; export const UsersIcon = (props: ComponentProps<'svg'>) => ; +export const SortIcon = (props: ComponentProps<'svg'>) => ; diff --git a/tdrive/frontend/src/app/components/menus/menu.jsx b/tdrive/frontend/src/app/components/menus/menu.jsx index ca94c793..673fe051 100755 --- a/tdrive/frontend/src/app/components/menus/menu.jsx +++ b/tdrive/frontend/src/app/components/menus/menu.jsx @@ -26,6 +26,7 @@ export default class Menu extends React.Component { super(props); this.state = { isMenuOpen: false, + by: '' }; this.open = false; // Added initialization for open state this.container = React.createRef(); // Ref for container div @@ -94,6 +95,10 @@ export default class Menu extends React.Component { return true; } + if (this.props.sortData !== nextProps.sortData) { + return true; + } + return false; } @@ -118,7 +123,10 @@ export default class Menu extends React.Component { }} className={this.props.className} > + <> {this.props.children} + + ); } 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 0be3717b..3d2981bf 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 @@ -1,5 +1,5 @@ import Api from '../../global/framework/api-service'; -import { BrowseFilter, DriveItem, DriveItemDetails, DriveItemVersion } from '../types'; +import { BrowseFilter, BrowsePaginate, BrowseQuery, BrowseSort, DriveItem, DriveItemDetails, DriveItemVersion } from '../types'; import Workspace from '@deprecated/workspaces/workspaces'; import Logger from 'features/global/framework/logger-service'; import { JWTDataType } from 'app/features/auth/jwt-storage-service'; @@ -70,10 +70,14 @@ export class DriveApiClient { ); } - static async browse(companyId: string, id: string | 'trash' | '', filter: BrowseFilter) { - return await Api.post( + static async browse(companyId: string, id: string | 'trash' | '', filter: BrowseFilter, sort: BrowseSort, paginate: BrowsePaginate) { + return await Api.post( `/internal/services/documents/v1/companies/${companyId}/browse/${id}${appendTdriveToken()}`, - filter, + { + filter, + sort, + paginate + }, ); } @@ -134,11 +138,11 @@ export class DriveApiClient { return Api.route(`/internal/services/documents/v1/companies/${companyId}/item/${id}/download`); } - static async getDownloadZipUrl(companyId: string, ids: string[]) { + static async getDownloadZipUrl(companyId: string, ids: string[], isDirectory?: boolean) { // const { token } = await DriveApiClient.getDownloadToken(companyId, ids); return Api.route( `/internal/services/documents/v1/companies/${companyId}/item/download/zip` + - `?items=${ids.join(',')}` + `?items=${ids.join(',')}&is_directory=${isDirectory}`, ); } 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 65d17401..a49d0cc9 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 @@ -1,9 +1,9 @@ import { ToasterService } from '@features/global/services/toaster-service'; import useRouterCompany from '@features/router/hooks/use-router-company'; import { useCallback } from 'react'; -import { useRecoilValue, useRecoilCallback } from 'recoil'; +import { useRecoilValue, useRecoilCallback, useRecoilState } from 'recoil'; import { DriveApiClient } from '../api-client/api-client'; -import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store'; +import { DriveItemAtom, DriveItemChildrenAtom, DriveItemPagination, DriveItemSort } from '../state/store'; import { BrowseFilter, DriveItem, DriveItemVersion } from '../types'; import { SharedWithMeFilterState } from '../state/shared-with-me-filter'; import Languages from 'features/global/services/languages-service'; @@ -16,18 +16,26 @@ import { useUserQuota } from 'features/users/hooks/use-user-quota'; export const useDriveActions = () => { const companyId = useRouterCompany(); const sharedFilter = useRecoilValue(SharedWithMeFilterState); + const sortItem = useRecoilValue(DriveItemSort); + const [paginateItem, _] = useRecoilState(DriveItemPagination); const { getQuota } = useUserQuota(); const refresh = useRecoilCallback( ({ set, snapshot }) => - async (parentId: string) => { + async (parentId: string, resetPagination?: boolean) => { if (parentId) { const filter: BrowseFilter = { company_id: companyId, mime_type: sharedFilter.mimeType.value, }; + let pagination = await snapshot.getPromise(DriveItemPagination); + + if (resetPagination) { + pagination = { page: 0, limit: pagination.limit }; + set(DriveItemPagination, pagination); + } try { - const details = await DriveApiClient.browse(companyId, parentId, filter); + const details = await DriveApiClient.browse(companyId, parentId, filter, sortItem, pagination); set(DriveItemChildrenAtom(parentId), details.children); set(DriveItemAtom(parentId), details); for (const child of details.children) { @@ -40,10 +48,12 @@ export const useDriveActions = () => { return details; } catch (e) { ToasterService.error(Languages.t('hooks.use-drive-actions.unable_load_file')); + } finally { + set(DriveItemPagination, { page: pagination.limit, limit: pagination.limit }); } } }, - [companyId], + [companyId, sortItem], ); const create = useCallback( @@ -54,7 +64,7 @@ export const useDriveActions = () => { try { const driveFile = await DriveApiClient.create(companyId, { item, version }); - await refresh(driveFile.parent_id); + await refresh(driveFile.parent_id, true); await getQuota(); return driveFile; @@ -89,9 +99,9 @@ export const useDriveActions = () => { ); const downloadZip = useCallback( - async (ids: string[]) => { + async (ids: string[], isDirectory?: boolean) => { try { - const url = await DriveApiClient.getDownloadZipUrl(companyId, ids); + const url = await DriveApiClient.getDownloadZipUrl(companyId, ids, isDirectory); (window as any).open(url, '_blank').focus(); } catch (e) { ToasterService.error(Languages.t('hooks.use-drive-actions.unable_download_file')); @@ -129,9 +139,9 @@ export const useDriveActions = () => { async (update: Partial, id: string, parentId: string) => { try { await DriveApiClient.update(companyId, id, update); - await refresh(id || ''); - await refresh(parentId || ''); - if (update?.parent_id !== parentId) await refresh(update?.parent_id || ''); + await refresh(id || '', true); + await refresh(parentId || '', true); + if (update?.parent_id !== parentId) await refresh(update?.parent_id || '', true); } catch (e) { ToasterService.error(Languages.t('hooks.use-drive-actions.unable_update_file')); } @@ -156,5 +166,25 @@ export const useDriveActions = () => { [refresh], ); - return { create, refresh, download, downloadZip, remove, restore, update, updateLevel }; + const nextPage = useRecoilCallback( + ({ snapshot }) => + async (parentId: string) => { + const filter: BrowseFilter = { + company_id: companyId, + mime_type: sharedFilter.mimeType.value, + }; + const pagination = await snapshot.getPromise(DriveItemPagination); + const details = await DriveApiClient.browse( + companyId, + parentId, + filter, + sortItem, + pagination + ); + return details; + }, + [paginateItem, refresh], + ); + + return { create, refresh, download, downloadZip, remove, restore, update, updateLevel, nextPage }; }; 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 d8be1c09..7f2716ca 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 @@ -1,9 +1,9 @@ import { ToasterService } from '@features/global/services/toaster-service'; import { LoadingStateInitTrue } from '@features/global/state/atoms/Loading'; import useRouterCompany from '@features/router/hooks/use-router-company'; -import { useCallback } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store'; +import { useCallback, useState } from 'react'; +import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; +import { DriveItemAtom, DriveItemChildrenAtom, DriveItemPagination } from '../state/store'; import { DriveItem } from '../types'; import { useDriveActions } from './use-drive-actions'; import { useDriveUpload } from './use-drive-upload'; @@ -17,8 +17,10 @@ import short from 'short-uuid'; export const useDriveItem = (id: string) => { const companyId = useRouterCompany(); const item = useRecoilValue(DriveItemAtom(id)); - const children = useRecoilValue(DriveItemChildrenAtom(id)); + // const children = useRecoilValue(DriveItemChildrenAtom(id)); + const [children, setChildren] = useRecoilState(DriveItemChildrenAtom(id)); const [loading, setLoading] = useRecoilState(LoadingStateInitTrue('useDriveItem-' + id)); + const [_, setPaginateItem] = useRecoilState(DriveItemPagination); const { refresh: refreshItem, create, @@ -26,19 +28,20 @@ export const useDriveItem = (id: string) => { updateLevel: _updateLevel, remove: _remove, restore: _restore, + nextPage, } = useDriveActions(); const { uploadVersion: _uploadVersion } = useDriveUpload(); - const refresh = useCallback( - async (parentId: string) => { + async (parentId: string, resetPagination?: boolean) => { setLoading(true); try { - await refreshItem(parentId); + setPaginateItem(prev => ({ ...prev, page: 0 })); + await refreshItem(parentId, resetPagination); } finally { setLoading(false); } }, - [setLoading, refreshItem], + [id, setLoading, refreshItem], ); const remove = useCallback(async () => { @@ -100,6 +103,51 @@ export const useDriveItem = (id: string) => { [companyId, id, setLoading, refresh, item?.item?.parent_id], ); + const loadNextPage = useRecoilCallback( + ({ set, snapshot }) => + async (id: string) => { + // get current pagination state + const pagination = await snapshot.getPromise(DriveItemPagination); + + // if end is true, do not load more + if (pagination.lastPage === true) return; + setLoading(true); + + try { + const details = await nextPage(id); + if (details.children.length === 0) { + set(DriveItemPagination, prev => ({ + ...prev, + lastPage: true, + })); + } + // set children and remove duplicates + setChildren(prev => [ + ...prev, + ...details.children.filter( + (item, index, self) => + index === self.findIndex(t => t.id === item.id), + ), + ]); + } catch (e) { + // set pagination end to true + set(DriveItemPagination, prev => ({ + ...prev, + lastPage: true, + })); + console.log('error loading next page: ', e); + ToasterService.error('Unable to load more items.'); + } finally { + set(DriveItemPagination, prev => ({ + ...prev, + page: (prev.page + prev.limit), + })); + } + setLoading(false); + }, + [id, nextPage], + ); + const inTrash = id.includes('trash') || item?.path?.some(i => i?.parent_id?.includes('trash')) || @@ -123,6 +171,7 @@ export const useDriveItem = (id: string) => { updateLevel, remove, refresh, + loadNextPage, }; }; 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 d917fdb5..2b84c8fe 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 @@ -29,11 +29,17 @@ export const useDrivePreviewModal = () => { } }; + const openWithId: (id: string) => void = (id: string) => { + DriveApiClient.get(company, id).then((item) => { + open(item?.item); + }); + } + const close = () => { setStatus({ item: null, loading: true }); } - return { open, close, isOpen: !!status.item }; + return { open, close, isOpen: !!status.item, openWithId }; }; export const useDrivePreview = () => { diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index 2b9e841b..33c63afd 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -50,7 +50,7 @@ export const useDriveUpload = () => { // Create all directories logger.debug("Start creating directories ..."); const filesPerParentId = await FileUploadService.createDirectories(tree.tree, context); - await refresh(context.parentId); + await refresh(context.parentId, true); logger.debug("All directories created"); // Upload files into directories @@ -90,7 +90,7 @@ export const useDriveUpload = () => { }, }); } - await refresh(context.parentId); + await refresh(context.parentId, true); }; const uploadFromUrl = ( diff --git a/tdrive/frontend/src/app/features/drive/state/store.ts b/tdrive/frontend/src/app/features/drive/state/store.ts index 1e143195..8c45b4fc 100644 --- a/tdrive/frontend/src/app/features/drive/state/store.ts +++ b/tdrive/frontend/src/app/features/drive/state/store.ts @@ -1,5 +1,5 @@ import { atomFamily, atom } from 'recoil'; -import { DriveItem, DriveItemDetails } from '../types'; +import { BrowsePaginate, BrowseSort, DriveItem, DriveItemDetails } from '../types'; export const DriveItemChildrenAtom = atomFamily({ key: 'DriveItemChildrenAtom', @@ -15,3 +15,20 @@ export const DriveItemSelectedList = atom<{[key: string]: boolean }>({ key: 'DriveItemSelectedList', default: {} }); + +export const DriveItemSort = atom({ + key: 'DriveItemSort', + default: { + by: 'date', + order: 'desc', + }, +}); + +export const DriveItemPagination = atom({ + key: 'DriveItemPagination', + default: { + page: 0, + limit: 15, + lastPage: false, + }, +}); diff --git a/tdrive/frontend/src/app/features/drive/types.ts b/tdrive/frontend/src/app/features/drive/types.ts index 70228458..c50743d0 100644 --- a/tdrive/frontend/src/app/features/drive/types.ts +++ b/tdrive/frontend/src/app/features/drive/types.ts @@ -1,8 +1,25 @@ +export type BrowseQuery = { + filter: BrowseFilter; + sort: BrowseSort; + paginate: BrowsePaginate; +} + export type BrowseFilter = { company_id: string; mime_type?: string; } +export type BrowseSort = { + by: string; + order: string; +} + +export type BrowsePaginate = { + page: number; + limit: number; + lastPage?: boolean; +}; + export type DriveItemDetails = { item: DriveItem; versions: DriveItemVersion[]; 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 0dc2e560..5ba8b7f7 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -7,21 +7,23 @@ import UploadZone from '@components/uploads/upload-zone'; import { setTdriveTabToken } from '@features/drive/api-client/api-client'; import { useDriveItem } from '@features/drive/hooks/use-drive-item'; import { useDriveUpload } from '@features/drive/hooks/use-drive-upload'; -import { DriveItemSelectedList } from '@features/drive/state/store'; +import { DriveItemSelectedList, DriveItemSort } from '@features/drive/state/store'; import { formatBytes } from '@features/drive/utils'; import useRouterCompany from '@features/router/hooks/use-router-company'; -import _ from 'lodash'; +import _, { set } from 'lodash'; import { memo, Suspense, useCallback, useEffect, useRef, useState } from 'react'; -import { atomFamily, useRecoilState, useSetRecoilState } from 'recoil'; +import { atomFamily, useRecoilState, useSetRecoilState, useRecoilValue } from 'recoil'; import { DrivePreview } from '../../viewer/drive-preview'; import { useOnBuildContextMenu, useOnBuildFileTypeContextMenu, useOnBuildPeopleContextMenu, useOnBuildDateContextMenu, + useOnBuildSortContextMenu, } from './context-menu'; import {DocumentRow, DocumentRowOverlay} from './documents/document-row'; import { FolderRow } from './documents/folder-row'; +import { FolderRowSkeleton } from './documents/folder-row-skeleton'; import HeaderPath from './header-path'; import { ConfirmDeleteModal } from './modals/confirm-delete'; import { ConfirmTrashModal } from './modals/confirm-trash'; @@ -46,6 +48,8 @@ import { ConfirmModalAtom } from './modals/confirm-move/index'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { ConfirmModal } from './modals/confirm-move'; import { useHistory } from 'react-router-dom'; +import { SortIcon } from 'app/atoms/icons-agnostic'; +import { useDrivePreview, useDrivePreviewLoading } from 'app/features/drive/hooks/use-drive-preview'; export const DriveCurrentFolderAtom = atomFamily< string, @@ -70,19 +74,32 @@ export default memo( const { user } = useCurrentUser(); const companyId = useRouterCompany(); const history = useHistory(); - const role = user ? (user?.companies || []).find(company => company?.company.id === companyId)?.role : "member"; + const role = user + ? (user?.companies || []).find(company => company?.company.id === companyId)?.role + : 'member'; setTdriveTabToken(tdriveTabContextToken || null); const [filter, __] = useRecoilState(SharedWithMeFilterState); - const { viewId, dirId } = useRouteState(); + const { viewId, dirId, itemId } = useRouteState(); + const { status } = useDrivePreview(); + const { openWithId, close } = useDrivePreview(); + const [sortLabel] = useRecoilState(DriveItemSort) + const { loading: isModalLoading } = useDrivePreviewLoading(); const [parentId, _setParentId] = useRecoilState( - DriveCurrentFolderAtom({ context: context, initialFolderId: dirId || viewId || initialParentId || 'user_'+user?.id }), + DriveCurrentFolderAtom({ + context: context, + initialFolderId: dirId || viewId || initialParentId || 'user_' + user?.id, + }), ); // set the initial view to the user's home directory useEffect(() => { - !dirId && !viewId && history.push(RouterServices.generateRouteFromState({viewId: parentId})); + !dirId && + !viewId && + history.push(RouterServices.generateRouteFromState({ viewId: parentId })); }, [viewId, dirId]); + + const [loadingParentChange, setLoadingParentChange] = useState(false); const { sharedWithMe, @@ -94,6 +111,7 @@ export default memo( children, loading: loadingParent, path, + loadNextPage, } = useDriveItem(parentId); const { uploadTree } = useDriveUpload(); @@ -131,10 +149,6 @@ export default memo( if (item?.id) setUploadModalState({ open: true, parent_id: item.id }); }, [item?.id, setUploadModalState]); - const selectedCount = Object.values(checked).filter(v => v).length; - const folders = children - .filter(i => i.is_directory) - .sort((a, b) => a.name.localeCompare(b.name)); const documents = ( item?.is_directory === false ? //We use this hack for public shared single file @@ -142,27 +156,28 @@ export default memo( ? [item] : [] : children - ) - .filter(i => !i.is_directory) - .sort((a, b) => a.name.localeCompare(b.name)); + ).filter(i => !i.is_directory); + + const selectedCount = Object.values(checked).filter(v => v).length; const onBuildContextMenu = useOnBuildContextMenu(children, initialParentId); + const onBuildSortContextMenu = useOnBuildSortContextMenu(); - const handleDragOver = (event: { preventDefault: () => void; }) => { + const handleDragOver = (event: { preventDefault: () => void }) => { event.preventDefault(); - } - const handleDrop = async (event: {dataTransfer: any; preventDefault: () => void;}) => { + }; + const handleDrop = async (event: { dataTransfer: any; preventDefault: () => void }) => { event.preventDefault(); const dataTransfer = event.dataTransfer; - if (dataTransfer) { - const tree = await getFilesTree(dataTransfer); - setCreationModalState({ parent_id: '', open: false }); - await uploadTree(tree, { - companyId, - parentId, - }); - } - } + if (dataTransfer) { + const tree = await getFilesTree(dataTransfer); + setCreationModalState({ parent_id: '', open: false }); + await uploadTree(tree, { + companyId, + parentId, + }); + } + }; const buildFileTypeContextMenu = useOnBuildFileTypeContextMenu(); const buildPeopleContextMen = useOnBuildPeopleContextMenu(); @@ -170,24 +185,24 @@ export default memo( const setConfirmModalState = useSetRecoilState(ConfirmModalAtom); const [activeIndex, setActiveIndex] = useState(null); const [activeChild, setActiveChild] = useState(null); - const {update} = useDriveActions(); + const { update } = useDriveActions(); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, - }) + }), ); const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); - function handleDragStart(event:any) { + function handleDragStart(event: any) { setActiveIndex(event.active.id); setActiveChild(event.active.data.current.child.props.item); } - function handleDragEnd(event:any) { + function handleDragEnd(event: any) { setActiveIndex(null); setActiveChild(null); - if (event.over){ + if (event.over) { setConfirmModalState({ open: true, parent_id: inTrash ? 'root' : event.over.data.current.child.props.item.id, @@ -204,35 +219,49 @@ export default memo( event.active.data.current.child.props.item.parent_id, ); }, - }) + }); } - } function draggableMarkup(index: number, child: any) { const commonProps = { key: index, className: - (index === 0 ? 'rounded-t-md ' : '') + - (index === documents.length - 1 ? 'rounded-b-md ' : ''), + (index === 0 ? 'rounded-t-md ' : '') + + (index === documents.length - 1 ? 'rounded-b-md ' : ''), item: child, checked: checked[child.id] || false, - onCheck: (v: boolean) => - setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)), + onCheck: (v: boolean) => setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)), onBuildContextMenu: () => onBuildContextMenu(details, child), inPublicSharing, }; - return ( - isMobile ? ( - - ) : ( - - - - ) + return isMobile ? ( + + ) : ( + + + ); } + // Infinite scroll + const scrollViwer = useRef(null); + + const handleScroll = async () => { + const scrollTop = scrollViwer.current?.scrollTop || 0; + const scrollHeight = scrollViwer.current?.scrollHeight || 0; + const clientHeight = scrollViwer.current?.clientHeight || 0; + if (scrollTop > 0 && scrollTop + clientHeight >= scrollHeight) { + await loadNextPage(parentId); + } + }; + + useEffect(() => { + if(!loading) scrollViwer.current?.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + scrollViwer.current?.removeEventListener('scroll', handleScroll); + }; + }, [parentId, loading]); return ( <> @@ -363,6 +392,19 @@ export default memo( {formatBytes(item?.size || 0)} {Languages.t('scenes.app.drive.used')} )} + + onBuildSortContextMenu()} sortData={sortLabel}> + {' '} + + {viewId !== 'shared_with_me' && ( onBuildContextMenu(details)}> {' '} @@ -380,43 +422,7 @@ export default memo( -
- {folders.length > 0 && ( - <> - - {Languages.t('scenes.app.drive.folders')} - - - {folders.map((child, index) => ( - - { - const route = RouterServices.generateRouteFromState({ - dirId: child.id, - }); - history.push(route); - if (inPublicSharing) return setParentId(child.id); - }} - checked={checked[child.id] || false} - onCheck={v => - setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)) - } - onBuildContextMenu={() => onBuildContextMenu(details, child)} - /> - - ))} -
- - )} - - {Languages.t('scenes.app.drive.documents')} - +
{documents.length === 0 && !loading && (
@@ -437,19 +443,46 @@ export default memo( )}
)} - - {documents.map((child, index) => draggableMarkup(index, child))} + {children.map((child, index) => + child.is_directory ? ( + + { + const route = RouterServices.generateRouteFromState({ + dirId: child.id, + }); + history.push(route); + if (inPublicSharing) return setParentId(child.id); + }} + checked={checked[child.id] || false} + onCheck={v => + setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)) + } + onBuildContextMenu={() => onBuildContextMenu(details, child)} + /> + + ) : ( + draggableMarkup(index, child) + ), + )} {activeIndex ? ( ) : null} + {loading && }
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 c05f2536..4185694c 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 @@ -15,7 +15,7 @@ import { DriveApiClient, getPublicLinkToken } from '@features/drive/api-client/a import { useDriveActions } from '@features/drive/hooks/use-drive-actions'; import { getPublicLink } from '@features/drive/hooks/use-drive-item'; import { useDrivePreview } from '@features/drive/hooks/use-drive-preview'; -import { DriveItemSelectedList } from '@features/drive/state/store'; +import { DriveItemSelectedList, DriveItemSort } 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'; @@ -24,7 +24,7 @@ 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'; +import _, { set } from 'lodash'; import Languages from 'features/global/services/languages-service'; import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers'; @@ -301,9 +301,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s hide: inTrash, onClick: () => { if (parent.children && parent.children.length > 0) { - const idsFromArray = getIdsFromArray(parent.children); - console.log("Download zip file with docs: " + idsFromArray); - downloadZip(idsFromArray); + downloadZip([parent.item!.id]); } else if (parent.item) { console.log("Download folder itself"); download(parent.item.id); @@ -519,3 +517,84 @@ export const useOnBuildFileContextMenu = () => { [download, preview], ); }; + +export const useOnBuildSortContextMenu = () => { + const [sortItem, setSortItem] = useRecoilState(DriveItemSort); + return useCallback(() => { + const menuItems = [ + { + type: 'menu', + text: 'Date', + icon: sortItem.by === 'date' ? 'check' : 'sort-check', + onClick: () => { + // keep the old value for sortItem and change the by value + setSortItem(prevSortItem => { + const newSortItem = { + ...prevSortItem, + by: 'date', + }; + return newSortItem; + }); + }, + }, + { + type: 'menu', + text: 'Name', + icon: sortItem.by === 'name' ? 'check' : 'sort-check', + onClick: () => { + setSortItem(prevSortItem => { + const newSortItem = { + ...prevSortItem, + by: 'name', + }; + return newSortItem; + }); + }, + }, + { + type: 'menu', + text: 'Size', + icon: sortItem.by === 'size' ? 'check' : 'sort-check', + onClick: () => { + setSortItem(prevSortItem => { + const newSortItem = { + ...prevSortItem, + by: 'size', + }; + return newSortItem; + }); + }, + }, + {type:"separator"}, + { + type: 'menu', + text: 'Ascending', + icon: sortItem.order === 'asc' ? 'check' : 'sort-check', + onClick: () => { + setSortItem(prevSortItem => { + const newSortItem = { + ...prevSortItem, + order: 'asc', + }; + return newSortItem; + }); + }, + }, + { + type: 'menu', + text: 'Descending', + icon: sortItem.order === 'desc' ? 'check' : 'sort-check', + onClick: () => { + setSortItem(prevSortItem => { + const newSortItem = { + ...prevSortItem, + order: 'desc', + }; + return newSortItem; + }); + }, + } + ]; + return menuItems; + }, [sortItem]); +}; 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 048a2aa4..059fded5 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 @@ -27,24 +27,13 @@ export const DocumentRow = ({ }: DriveItemProps) => { const history = useHistory(); const [hover, setHover] = useState(false); - const { open, close } = useDrivePreview(); + const { open, close, isOpen } = useDrivePreview(); const company = useRouterCompany(); - const { itemId } = useRouteState(); - - useEffect(() => { - // close the preview if the item is not set or the user navigated away - if (!itemId) { - close(); - } - // open the preview if the item is set - if (itemId == item.id) { - open(item); - } - }, [itemId]); const preview = () => { + open(item); history.push(RouterServices.generateRouteFromState({ companyId: company, itemId: item.id })); - if (inPublicSharing) open(item); + // history.push(RouterServices.generateRouteFromState({ companyId: company, itemId: item.id })); }; return ( diff --git a/tdrive/frontend/src/app/views/client/body/drive/documents/folder-row-skeleton.tsx b/tdrive/frontend/src/app/views/client/body/drive/documents/folder-row-skeleton.tsx new file mode 100644 index 00000000..b8aeec57 --- /dev/null +++ b/tdrive/frontend/src/app/views/client/body/drive/documents/folder-row-skeleton.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +export const FolderRowSkeleton = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; 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 d76b2610..fb4e2ef3 100644 --- a/tdrive/frontend/src/app/views/client/viewer/drive-preview.tsx +++ b/tdrive/frontend/src/app/views/client/viewer/drive-preview.tsx @@ -104,6 +104,7 @@ export const DrivePreview: React.FC = ({ items }) => { className="z-10 cursor-pointer absolute right-5 top-5 w-20 h-20 text-white hover:text-black rounded-full p-1 bg-gray-500 hover:bg-white bg-opacity-25" onClick={() => { close(); + // small delay to allow the modal to close history.push(RouterServices.generateRouteFromState({ companyId: company, itemId: '' })); }} />