🐛 Fix infinite scroll for shared with me (#641)

This commit is contained in:
Montassar Ghanmy
2024-09-05 14:30:27 +01:00
committed by GitHub
parent 492b636e93
commit f14ec8bd2d
10 changed files with 253 additions and 70 deletions
@@ -211,7 +211,12 @@ export default class MongoSearch extends SearchAdapter implements SearchAdapterI
logger.info(`Search query: ${JSON.stringify(query)}`); logger.info(`Search query: ${JSON.stringify(query)}`);
console.log(query); console.log(query);
let cursor = collection.find(query).sort(sort); const sortMapped: any = sort
? Object.fromEntries(
Object.entries(sort).map(([field, direction]) => [field, direction === "asc" ? 1 : -1]),
)
: {};
let cursor = collection.find(query).sort(sortMapped);
if (project) { if (project) {
cursor = cursor.project(project); cursor = cursor.project(project);
} }
@@ -14,6 +14,7 @@ export default {
access_entities: entity.access_info?.entities?.filter(e => e.level != "none").map(e => e.id), access_entities: entity.access_info?.entities?.filter(e => e.level != "none").map(e => e.id),
last_modified: entity.last_modified, last_modified: entity.last_modified,
mime_type: entity.last_version_cache?.file_metadata?.mime, mime_type: entity.last_version_cache?.file_metadata?.mime,
size: entity.last_version_cache?.file_metadata?.size,
}), }),
mongoMapping: { mongoMapping: {
text: { text: {
@@ -29,10 +29,8 @@ import {
DriveFileAccessLevel, DriveFileAccessLevel,
DriveItemDetails, DriveItemDetails,
DriveTdriveTab, DriveTdriveTab,
PaginateDocumentBody,
RootType, RootType,
SearchDocumentsOptions, SearchDocumentsOptions,
SortDocumentsBody,
TrashType, TrashType,
} from "../types"; } from "../types";
import { import {
@@ -61,6 +59,7 @@ import archiver from "archiver";
import internal from "stream"; import internal from "stream";
import config from "config"; import config from "config";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { SortType } from "src/core/platform/services/search/api";
export class DocumentsService { export class DocumentsService {
version: "1"; version: "1";
@@ -107,8 +106,6 @@ export class DocumentsService {
browse = async ( browse = async (
id: string, id: string,
options: SearchDocumentsOptions, options: SearchDocumentsOptions,
sort: SortDocumentsBody,
paginate: PaginateDocumentBody,
context: DriveExecutionContext & { public_token?: string }, context: DriveExecutionContext & { public_token?: string },
): Promise<BrowseDetails> => { ): Promise<BrowseDetails> => {
if (isSharedWithMeFolder(id)) { if (isSharedWithMeFolder(id)) {
@@ -116,7 +113,7 @@ export class DocumentsService {
} else { } else {
return { return {
nextPage: null, nextPage: null,
...(await this.get(id, context, false, sort, paginate)), ...(await this.get(id, options, context, false)),
}; };
} }
}; };
@@ -125,17 +122,23 @@ export class DocumentsService {
options: SearchDocumentsOptions, options: SearchDocumentsOptions,
context: DriveExecutionContext & { public_token?: string }, context: DriveExecutionContext & { public_token?: string },
): Promise<BrowseDetails> => { ): Promise<BrowseDetails> => {
const result = []; if (options.pagination) {
let fileList: ListResult<DriveFile>; if (options.pagination.page_token == "1") {
do { delete options.pagination.page_token;
fileList = await this.search(options, context); }
result.push(...fileList.getEntities()); }
options.pagination = fileList.nextPage;
} while (fileList.nextPage?.page_token); if (options.sort) {
options.sort = this.getSortFieldMapping(options.sort);
}
const fileList: ListResult<DriveFile> = await this.search(options, context);
const result = fileList.getEntities();
return { return {
access: "read", access: "read",
children: result, children: result,
nextPage: null, nextPage: fileList.nextPage,
path: [] as Array<DriveFile>, path: [] as Array<DriveFile>,
}; };
}; };
@@ -157,10 +160,9 @@ export class DocumentsService {
*/ */
get = async ( get = async (
id: string, id: string,
options: SearchDocumentsOptions,
context: DriveExecutionContext & { public_token?: string }, context: DriveExecutionContext & { public_token?: string },
all?: boolean, all?: boolean,
sort?: SortDocumentsBody,
paginate?: PaginateDocumentBody,
): Promise<DriveItemDetails> => { ): Promise<DriveItemDetails> => {
if (!context) { if (!context) {
this.logger.error("invalid context"); this.logger.error("invalid context");
@@ -213,24 +215,21 @@ export class DocumentsService {
) )
).getEntities(); ).getEntities();
const sortFieldMapping = { let sortField = {};
name: "name", if (options?.sort) {
date: "last_modified", sortField = this.getSortFieldMapping(options.sort);
size: "size", }
};
const sortField = {};
sortField[sortFieldMapping[sort?.by] || "last_modified"] = sort?.order || "desc";
const dbType = await globalResolver.database.getConnector().getType(); const dbType = await globalResolver.database.getConnector().getType();
// Initialize pagination // Initialize pagination
let pagination; let pagination;
if (paginate) { if (options?.pagination) {
const { page, limit } = paginate; const { page_token, limitStr } = options.pagination;
const pageNumber = dbType === "mongodb" ? page : page / limit + 1; const pageNumber =
dbType === "mongodb" ? parseInt(page_token) : parseInt(page_token) / parseInt(limitStr) + 1;
pagination = new Pagination(`${pageNumber}`, `${limit}`, false); pagination = new Pagination(`${pageNumber}`, `${limitStr}`, false);
} }
let children = isDirectory let children = isDirectory
@@ -1031,7 +1030,7 @@ export class DocumentsService {
context: DriveExecutionContext, context: DriveExecutionContext,
): Promise<string> => { ): Promise<string> => {
for (const id of ids) { for (const id of ids) {
const item = await this.get(id, context); const item = await this.get(id, null, context);
if (!item) { if (!item) {
throw new CrudException("Drive item not found", 404); throw new CrudException("Drive item not found", 404);
} }
@@ -1085,7 +1084,7 @@ export class DocumentsService {
size: number; size: number;
}; };
}> => { }> => {
const item = await this.get(id, context); const item = await this.get(id, null, context);
if (item.item.is_directory) { if (item.item.is_directory) {
return { archive: await this.createZip([id], context) }; return { archive: await this.createZip([id], context) };
@@ -1317,4 +1316,16 @@ export class DocumentsService {
throw new CrudException(`Not enough space: ${size}, ${leftQuota}.`, 403); throw new CrudException(`Not enough space: ${size}, ${leftQuota}.`, 403);
} }
}; };
getSortFieldMapping = (sort: SortType) => {
const sortFieldMapping = {
name: "name",
date: "last_modified",
size: "size",
};
const sortField = {};
sortField[sortFieldMapping[sort?.by] || "last_modified"] = sort?.order || "desc";
return sortField;
};
} }
@@ -61,8 +61,8 @@ export type SearchDocumentsOptions = {
export type BrowseDocumentsOptions = { export type BrowseDocumentsOptions = {
filter?: SearchDocumentsBody; filter?: SearchDocumentsBody;
sort?: SortDocumentsBody; sort?: SortType;
paginate?: PaginateDocumentBody; paginate?: Paginable;
}; };
export type SearchDocumentsBody = { export type SearchDocumentsBody = {
@@ -85,7 +85,7 @@ export type SortDocumentsBody = {
}; };
export type PaginateDocumentBody = { export type PaginateDocumentBody = {
page: number; page?: string;
limit: number; limit: number;
}; };
@@ -16,12 +16,10 @@ import {
DriveItemDetails, DriveItemDetails,
DriveTdriveTab, DriveTdriveTab,
ItemRequestParams, ItemRequestParams,
PaginateDocumentBody,
ItemRequestByEditingSessionKeyParams, ItemRequestByEditingSessionKeyParams,
RequestParams, RequestParams,
SearchDocumentsBody, SearchDocumentsBody,
SearchDocumentsOptions, SearchDocumentsOptions,
SortDocumentsBody,
} from "../../types"; } from "../../types";
import { DriveFileDTO } from "../dto/drive-file-dto"; import { DriveFileDTO } from "../dto/drive-file-dto";
import { DriveFileDTOBuilder } from "../../services/drive-file-dto-builder"; import { DriveFileDTOBuilder } from "../../services/drive-file-dto-builder";
@@ -146,7 +144,7 @@ export class DocumentsController {
): Promise<DriveItemDetails> => { ): Promise<DriveItemDetails> => {
const context = getDriveExecutionContext(request); const context = getDriveExecutionContext(request);
return await globalResolver.services.documents.documents.get(null, context); return await globalResolver.services.documents.documents.get(null, null, context);
}; };
/** /**
@@ -165,7 +163,7 @@ export class DocumentsController {
const { id } = request.params; const { id } = request.params;
return { return {
...(await globalResolver.services.documents.documents.get(id, context)), ...(await globalResolver.services.documents.documents.get(id, null, context)),
}; };
}; };
@@ -212,19 +210,12 @@ export class DocumentsController {
view: DriveFileDTOBuilder.VIEW_SHARED_WITH_ME, view: DriveFileDTOBuilder.VIEW_SHARED_WITH_ME,
onlyDirectlyShared: true, onlyDirectlyShared: true,
onlyUploadedNotByMe: true, onlyUploadedNotByMe: true,
sort: request.body.sort,
pagination: request.body.paginate,
}; };
const sortOptions: SortDocumentsBody = request.body.sort;
const paginateOptions: PaginateDocumentBody = request.body.paginate;
return { return {
...(await globalResolver.services.documents.documents.browse( ...(await globalResolver.services.documents.documents.browse(id, options, context)),
id,
options,
sortOptions,
paginateOptions,
context,
)),
}; };
}; };
@@ -475,12 +466,17 @@ export class DocumentsController {
); );
if (ids[0] === "root") { if (ids[0] === "root") {
const items = await globalResolver.services.documents.documents.get(ids[0], context); const items = await globalResolver.services.documents.documents.get(ids[0], null, context);
ids = items.children.map(item => item.id); ids = items.children.map(item => item.id);
} }
if (isDirectory === true) { if (isDirectory === true) {
const items = await globalResolver.services.documents.documents.get(ids[0], context, true); const items = await globalResolver.services.documents.documents.get(
ids[0],
null,
context,
true,
);
ids = items.children.map(item => item.id); ids = items.children.map(item => item.id);
} }
@@ -594,11 +590,16 @@ export class DocumentsController {
type: string; type: string;
}; };
}> { }> {
const document = await globalResolver.services.documents.documents.get(req.body.document_id, { const document = await globalResolver.services.documents.documents.get(
public_token: req.body.token + (req.body.token_password ? "+" + req.body.token_password : ""), req.body.document_id,
user: null, null,
company: { id: req.body.company_id }, {
}); public_token:
req.body.token + (req.body.token_password ? "+" + req.body.token_password : ""),
user: null,
company: { id: req.body.company_id },
},
);
if (!document || !document.access || document.access === "none") if (!document || !document.access || document.access === "none")
throw new CrudException("You don't have access to this document", 401); throw new CrudException("You don't have access to this document", 401);
@@ -37,10 +37,15 @@ export class DriveFileMockClass {
} }
export class DriveItemDetailsMockClass { export class DriveItemDetailsMockClass {
path: string[]; path: string[];
item: DriveFileMockClass; item: DriveFileMockClass;
children: DriveFileMockClass[]; children: DriveFileMockClass[];
versions: Record<string, unknown>[]; versions: Record<string, unknown>[];
nextPage?: {
page_token: string;
limitStr: string;
reversed: boolean;
};
} }
export class SearchResultMockClass { export class SearchResultMockClass {
@@ -36,27 +36,27 @@ describe("The Documents Browser Window and API", () => {
const myDriveId = "user_" + currentUser.user.id; const myDriveId = "user_" + currentUser.user.id;
await currentUser.uploadAllFilesOneByOne(myDriveId); await currentUser.uploadAllFilesOneByOne(myDriveId);
let page = 1; let page_token = "1";
const limit = 2; const limitStr = "2";
let docs = await currentUser.browseDocuments(myDriveId, { let docs = await currentUser.browseDocuments(myDriveId, {
paginate: { page, limit }, paginate: { page_token, limitStr },
}); });
expect(docs).toBeDefined(); expect(docs).toBeDefined();
expect(docs.children).toHaveLength(limit); expect(docs.children).toHaveLength(parseInt(limitStr));
page = 2; page_token = "2";
docs = await currentUser.browseDocuments(myDriveId, { docs = await currentUser.browseDocuments(myDriveId, {
paginate: { page, limit }, paginate: { page_token, limitStr },
}); });
expect(docs).toBeDefined(); expect(docs).toBeDefined();
expect(docs.children).toHaveLength(limit); expect(docs.children).toHaveLength(parseInt(limitStr));
page = 3; page_token = "3";
docs = await currentUser.browseDocuments(myDriveId, { docs = await currentUser.browseDocuments(myDriveId, {
paginate: { page, limit }, paginate: { page_token, limitStr },
}); });
expect(docs).toBeDefined(); expect(docs).toBeDefined();
expect(docs.children.length).toBeLessThanOrEqual(limit); expect(docs.children.length).toBeLessThanOrEqual(parseInt(limitStr));
}); });
it("Should sort documents by name in ascending order", async () => { it("Should sort documents by name in ascending order", async () => {
@@ -152,5 +152,158 @@ describe("The Documents Browser Window and API", () => {
const isSorted = docs.children.every((item, i, arr) => !i || arr[i - 1].size >= item.size); const isSorted = docs.children.every((item, i, arr) => !i || arr[i - 1].size >= item.size);
expect(isSorted).toBe(true); expect(isSorted).toBe(true);
}); });
it("Should paginate shared with me ", async () => {
const sharedWIthMeFolder = "shared_with_me";
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
let files = await oneUser.uploadAllFilesOneByOne();
for (const file of files) {
await oneUser.shareWithPermissions(file, anotherUser.user.id, "read");
}
// wait for files to be indexed
await new Promise(r => setTimeout(r, 5000));
let page_token: any = "1";
const limitStr = "2";
let docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
paginate: { page_token, limitStr },
});
expect(docs).toBeDefined();
expect(docs.children).toHaveLength(parseInt(limitStr));
page_token = docs.nextPage?.page_token || "2";
docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
paginate: { page_token, limitStr },
});
expect(docs).toBeDefined();
expect(docs.children).toHaveLength(parseInt(limitStr));
page_token = docs.nextPage?.page_token || "3";
docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
paginate: { page_token, limitStr },
});
expect(docs).toBeDefined();
expect(docs.children.length).toBeLessThanOrEqual(parseInt(limitStr));
});
it("Should sort shared with me by name in ascending order", async () => {
const sharedWIthMeFolder = "shared_with_me";
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
let files = await oneUser.uploadAllFilesOneByOne();
for (const file of files) {
await oneUser.shareWithPermissions(file, anotherUser.user.id, "read");
}
const sortBy = "name";
const sortOrder = "asc";
const docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
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 shared with me by name in descending order", async () => {
const sharedWIthMeFolder = "shared_with_me";
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
let files = await oneUser.uploadAllFilesOneByOne();
for (const file of files) {
await oneUser.shareWithPermissions(file, anotherUser.user.id, "read");
}
const sortBy = "name";
const sortOrder = "desc";
const docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
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 shared with me by size in ascending order", async () => {
const sharedWIthMeFolder = "shared_with_me";
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
let files = await oneUser.uploadAllFilesOneByOne();
for (const file of files) {
await oneUser.shareWithPermissions(file, anotherUser.user.id, "read");
}
const sortBy = "size";
const sortOrder = "asc";
const docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
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 shared with me by size in descending order", async () => {
const sharedWIthMeFolder = "shared_with_me";
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
let files = await oneUser.uploadAllFilesOneByOne();
for (const file of files) {
await oneUser.shareWithPermissions(file, anotherUser.user.id, "read");
}
const sortBy = "size";
const sortOrder = "desc";
const docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
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 shared with me by date in ascending order", async () => {
const sharedWIthMeFolder = "shared_with_me";
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
let files = await oneUser.uploadAllFilesOneByOne();
for (const file of files) {
await oneUser.shareWithPermissions(file, anotherUser.user.id, "read");
}
const sortBy = "date";
const sortOrder = "asc";
const docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
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 shared with me by date in descending order", async () => {
const sharedWIthMeFolder = "shared_with_me";
const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" });
let files = await oneUser.uploadAllFilesOneByOne();
for (const file of files) {
await oneUser.shareWithPermissions(file, anotherUser.user.id, "read");
}
const sortBy = "date";
const sortOrder = "desc";
const docs = await anotherUser.browseDocuments(sharedWIthMeFolder, {
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;
});
}); });
}); });
@@ -41,6 +41,7 @@ services:
- "5432:5432" - "5432:5432"
node: node:
# Use the build context in the current directory
build: build:
context: . context: .
dockerfile: docker/tdrive-node/Dockerfile dockerfile: docker/tdrive-node/Dockerfile
@@ -76,7 +76,10 @@ export class DriveApiClient {
{ {
filter, filter,
sort, sort,
paginate paginate: {
page_token: paginate.page.toString(),
limitStr: paginate.limit.toString(),
}
}, },
); );
} }
@@ -1,7 +1,10 @@
export type BrowseQuery = { export type BrowseQuery = {
filter: BrowseFilter; filter: BrowseFilter;
sort: BrowseSort; sort: BrowseSort;
paginate: BrowsePaginate; paginate: {
page_token: string;
limitStr: string;
};
} }
export type BrowseFilter = { export type BrowseFilter = {