✨ Sort files chronologically + infinite scroll (#535)
This commit is contained in:
+16
-2
@@ -264,12 +264,22 @@ export class MongoConnector extends AbstractConnector<MongoConnectionOptions> {
|
||||
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<MongoConnectionOptions> {
|
||||
.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()) {
|
||||
|
||||
+8
-1
@@ -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;
|
||||
|
||||
+7
@@ -22,6 +22,12 @@ export type inType = [string, Array<any>];
|
||||
// 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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<BrowseDetails> => {
|
||||
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<DriveItemDetails> => {
|
||||
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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DriveItemDetails> => {
|
||||
@@ -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<void> => {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+51
@@ -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();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.16 8.385C5.16 8.21393 5.22796 8.04988 5.34892 7.92892C5.46988 7.80795 5.63393 7.74 5.805 7.74H7.095C7.26606 7.74 7.43012 7.80795 7.55108 7.92892C7.67205 8.04988 7.74 8.21393 7.74 8.385C7.74 8.55606 7.67205 8.72012 7.55108 8.84108C7.43012 8.96204 7.26606 9.03 7.095 9.03H5.805C5.63393 9.03 5.46988 8.96204 5.34892 8.84108C5.22796 8.72012 5.16 8.55606 5.16 8.385ZM2.58 4.515C2.58 4.34394 2.64796 4.17988 2.76892 4.05892C2.88988 3.93795 3.05394 3.87 3.225 3.87H9.675C9.84606 3.87 10.0101 3.93795 10.1311 4.05892C10.252 4.17988 10.32 4.34394 10.32 4.515C10.32 4.68606 10.252 4.85012 10.1311 4.97108C10.0101 5.09205 9.84606 5.16 9.675 5.16H3.225C3.05394 5.16 2.88988 5.09205 2.76892 4.97108C2.64796 4.85012 2.58 4.68606 2.58 4.515ZM0 0.645C0 0.473935 0.0679553 0.309877 0.188916 0.188916C0.309877 0.0679553 0.473935 0 0.645 0H12.255C12.4261 0 12.5901 0.0679553 12.7111 0.188916C12.832 0.309877 12.9 0.473935 12.9 0.645C12.9 0.816065 12.832 0.980123 12.7111 1.10108C12.5901 1.22204 12.4261 1.29 12.255 1.29H0.645C0.473935 1.29 0.309877 1.22204 0.188916 1.10108C0.0679553 0.980123 0 0.816065 0 0.645Z" fill="#6A6A6A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -22,6 +22,7 @@ import {
|
||||
StatusCheckIcon,
|
||||
UsersIcon,
|
||||
CheckOutlineIcon,
|
||||
SortIcon,
|
||||
} from '@atoms/icons-agnostic/index';
|
||||
|
||||
export default {
|
||||
@@ -69,6 +70,7 @@ const Template: ComponentStory<any> = () => {
|
||||
<Icon icon={<StatusCheckDoubleIcon />} title="StatusCheckDouble" />
|
||||
<Icon icon={<StatusCheckIcon />} title="StatusCheck" />
|
||||
<Icon icon={<UsersIcon />} title="Users" />
|
||||
<Icon icon={<SortIcon />} title="Sort" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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'>) => <CopySvg {...props} />;
|
||||
export const DeleteIcon = (props: ComponentProps<'svg'>) => <DeleteSvg {...props} />;
|
||||
@@ -43,3 +44,4 @@ export const StatusCheckDoubleIcon = (props: ComponentProps<'svg'>) => (
|
||||
);
|
||||
export const StatusCheckIcon = (props: ComponentProps<'svg'>) => <StatusCheck {...props} />;
|
||||
export const UsersIcon = (props: ComponentProps<'svg'>) => <Users {...props} />;
|
||||
export const SortIcon = (props: ComponentProps<'svg'>) => <Sort {...props} />;
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<BrowseFilter, DriveItemDetails>(
|
||||
static async browse(companyId: string, id: string | 'trash' | '', filter: BrowseFilter, sort: BrowseSort, paginate: BrowsePaginate) {
|
||||
return await Api.post<BrowseQuery, DriveItemDetails>(
|
||||
`/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}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DriveItem>, 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 };
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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<DriveItem[], string>({
|
||||
key: 'DriveItemChildrenAtom',
|
||||
@@ -15,3 +15,20 @@ export const DriveItemSelectedList = atom<{[key: string]: boolean }>({
|
||||
key: 'DriveItemSelectedList',
|
||||
default: {}
|
||||
});
|
||||
|
||||
export const DriveItemSort = atom<BrowseSort>({
|
||||
key: 'DriveItemSort',
|
||||
default: {
|
||||
by: 'date',
|
||||
order: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
export const DriveItemPagination = atom<BrowsePaginate>({
|
||||
key: 'DriveItemPagination',
|
||||
default: {
|
||||
page: 0,
|
||||
limit: 15,
|
||||
lastPage: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 ? (
|
||||
<DocumentRow {...commonProps} />
|
||||
) : (
|
||||
<Draggable id={index} key={index}>
|
||||
<DocumentRow {...commonProps} />
|
||||
</Draggable>
|
||||
)
|
||||
return isMobile ? (
|
||||
<DocumentRow {...commonProps} />
|
||||
) : (
|
||||
<Draggable id={index}>
|
||||
<DocumentRow {...commonProps} />
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
// Infinite scroll
|
||||
const scrollViwer = useRef<HTMLDivElement>(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')}
|
||||
</BaseSmall>
|
||||
)}
|
||||
|
||||
<Menu menu={() => onBuildSortContextMenu()} sortData={sortLabel}>
|
||||
{' '}
|
||||
<Button theme="outline" className="ml-4 flex flex-row items-center">
|
||||
<SortIcon
|
||||
className={`h-4 w-4 mr-2 -ml-1 ${
|
||||
sortLabel.order === 'asc' ? 'transform rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
<span>By {sortLabel.by}</span>
|
||||
<ChevronDownIcon className="h-4 w-4 ml-2 -mr-1" />
|
||||
</Button>
|
||||
</Menu>
|
||||
{viewId !== 'shared_with_me' && (
|
||||
<Menu menu={() => onBuildContextMenu(details)}>
|
||||
{' '}
|
||||
@@ -380,43 +422,7 @@ export default memo(
|
||||
</div>
|
||||
|
||||
<DndContext sensors={sensors} onDragEnd={handleDragEnd} onDragStart={handleDragStart}>
|
||||
<div className="grow overflow-auto">
|
||||
{folders.length > 0 && (
|
||||
<>
|
||||
<Title className="mb-2 block">
|
||||
{Languages.t('scenes.app.drive.folders')}
|
||||
</Title>
|
||||
|
||||
{folders.map((child, index) => (
|
||||
<Droppable id={index} key={index}>
|
||||
<FolderRow
|
||||
key={index}
|
||||
className={
|
||||
(index === 0 ? 'rounded-t-md ' : '') +
|
||||
(index === folders.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={child}
|
||||
onClick={() => {
|
||||
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)}
|
||||
/>
|
||||
</Droppable>
|
||||
))}
|
||||
<div className="my-6" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Title className="mb-2 block">{Languages.t('scenes.app.drive.documents')}</Title>
|
||||
|
||||
<div className="grow overflow-auto" ref={scrollViwer}>
|
||||
{documents.length === 0 && !loading && (
|
||||
<div className="mt-4 text-center border-2 border-dashed rounded-md p-8">
|
||||
<Subtitle className="block mb-2">
|
||||
@@ -437,19 +443,46 @@ export default memo(
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{documents.map((child, index) => draggableMarkup(index, child))}
|
||||
{children.map((child, index) =>
|
||||
child.is_directory ? (
|
||||
<Droppable id={index} key={index}>
|
||||
<FolderRow
|
||||
key={index}
|
||||
className={
|
||||
(index === 0 ? 'rounded-t-md ' : '') +
|
||||
(index === children.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={child}
|
||||
onClick={() => {
|
||||
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)}
|
||||
/>
|
||||
</Droppable>
|
||||
) : (
|
||||
draggableMarkup(index, child)
|
||||
),
|
||||
)}
|
||||
<DragOverlay>
|
||||
{activeIndex ? (
|
||||
<DocumentRowOverlay
|
||||
className={
|
||||
(activeIndex === 0 ? 'rounded-t-md ' : '') +
|
||||
(activeIndex === documents.length - 1 ? 'rounded-b-md ' : '')
|
||||
(activeIndex === children.length - 1 ? 'rounded-b-md ' : '')
|
||||
}
|
||||
item={activeChild}
|
||||
></DocumentRowOverlay>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
{loading && <FolderRowSkeleton />}
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
export const FolderRowSkeleton = () => {
|
||||
return (
|
||||
<div className="flex flex-row items-center border border-zinc-200 dark:border-zinc-800 -mt-px px-4 py-3 cursor-pointer animate-pulse">
|
||||
<div className="mr-2 -ml-1">
|
||||
<div className="bg-gray-300 h-5 w-5 rounded-full"></div>
|
||||
</div>
|
||||
<div className="grow text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<div className="bg-gray-300 h-6 w-3/4 rounded"></div>
|
||||
</div>
|
||||
<div className="shrink-0 ml-4">
|
||||
<div className="bg-gray-300 h-5 w-5 rounded-full"></div>
|
||||
</div>
|
||||
<div className="shrink-0 ml-4 text-right minWidth80">
|
||||
<div className="bg-gray-300 h-4 w-20 rounded"></div>
|
||||
</div>
|
||||
<div className="shrink-0 ml-4">
|
||||
<div className="bg-gray-300 h-8 w-8 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -104,6 +104,7 @@ export const DrivePreview: React.FC<DrivePreviewProps> = ({ 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: '' }));
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user