✨ Shared with me browser view (#82)
* ✨shared with me page, move browser view backend
This commit is contained in:
@@ -157,6 +157,7 @@ export const getAccessLevel = async (
|
|||||||
: (await isCompanyAdmin(context))
|
: (await isCompanyAdmin(context))
|
||||||
? "manage"
|
? "manage"
|
||||||
: "write";
|
: "write";
|
||||||
|
if (id === "shared_with_me") return "read";
|
||||||
|
|
||||||
//If it is my personal folder, I have full access
|
//If it is my personal folder, I have full access
|
||||||
if (context?.user?.id && id.startsWith("user_")) {
|
if (context?.user?.id && id.startsWith("user_")) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
TYPE as DriveTdriveTabRepoType,
|
TYPE as DriveTdriveTabRepoType,
|
||||||
} from "../entities/drive-tdrive-tab";
|
} from "../entities/drive-tdrive-tab";
|
||||||
import {
|
import {
|
||||||
|
BrowseDetails,
|
||||||
CompanyExecutionContext,
|
CompanyExecutionContext,
|
||||||
DocumentsMessageQueueRequest,
|
DocumentsMessageQueueRequest,
|
||||||
DriveExecutionContext,
|
DriveExecutionContext,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
getItemName,
|
getItemName,
|
||||||
getPath,
|
getPath,
|
||||||
getVirtualFoldersNames,
|
getVirtualFoldersNames,
|
||||||
|
isSharedWithMeFolder,
|
||||||
isVirtualFolder,
|
isVirtualFolder,
|
||||||
updateItemSize,
|
updateItemSize,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
@@ -87,6 +89,27 @@ export class DocumentsService {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
browse = async (
|
||||||
|
id: string,
|
||||||
|
options: SearchDocumentsOptions,
|
||||||
|
context: DriveExecutionContext & { public_token?: string },
|
||||||
|
): Promise<BrowseDetails> => {
|
||||||
|
if (isSharedWithMeFolder(id)) {
|
||||||
|
const children = await this.search(options, context);
|
||||||
|
return {
|
||||||
|
access: "read",
|
||||||
|
children: children.getEntities(),
|
||||||
|
nextPage: children.nextPage,
|
||||||
|
path: [] as Array<DriveFile>,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
nextPage: null,
|
||||||
|
...(await this.get(id, context)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a drive element
|
* Fetches a drive element
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExecutionContext } from "../../core/platform/framework/api/crud-service";
|
import { ExecutionContext, Paginable } from "../../core/platform/framework/api/crud-service";
|
||||||
import { DriveFile } from "./entities/drive-file";
|
import { DriveFile } from "./entities/drive-file";
|
||||||
import { FileVersion } from "./entities/file-version";
|
import { FileVersion } from "./entities/file-version";
|
||||||
import { SortType } from "../../core/platform/services/search/api";
|
import { SortType } from "../../core/platform/services/search/api";
|
||||||
@@ -25,11 +25,14 @@ export type DriveItemDetails = {
|
|||||||
access: DriveFileAccessLevel | "none";
|
access: DriveFileAccessLevel | "none";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BrowseDetails = DriveItemDetails & { nextPage: Paginable };
|
||||||
|
|
||||||
export type DriveFileAccessLevel = "read" | "write" | "manage";
|
export type DriveFileAccessLevel = "read" | "write" | "manage";
|
||||||
export type publicAccessLevel = "write" | "read" | "none";
|
export type publicAccessLevel = "write" | "read" | "none";
|
||||||
|
|
||||||
export type RootType = "root";
|
export type RootType = "root";
|
||||||
export type TrashType = "trash";
|
export type TrashType = "trash";
|
||||||
|
export type SharedWithMeType = "shared_with_me";
|
||||||
|
|
||||||
export type DownloadZipBodyRequest = {
|
export type DownloadZipBodyRequest = {
|
||||||
items: string[];
|
items: string[];
|
||||||
|
|||||||
@@ -17,13 +17,24 @@ import { stopWords } from "./const";
|
|||||||
import { DriveFile } from "./entities/drive-file";
|
import { DriveFile } from "./entities/drive-file";
|
||||||
import { DriveFileMetadata, FileVersion } from "./entities/file-version";
|
import { DriveFileMetadata, FileVersion } from "./entities/file-version";
|
||||||
import { checkAccess, generateAccessToken } from "./services/access-check";
|
import { checkAccess, generateAccessToken } from "./services/access-check";
|
||||||
import { CompanyExecutionContext, DriveExecutionContext, RootType, TrashType } from "./types";
|
import {
|
||||||
|
CompanyExecutionContext,
|
||||||
|
DriveExecutionContext,
|
||||||
|
RootType,
|
||||||
|
SharedWithMeType,
|
||||||
|
TrashType,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const ROOT: RootType = "root";
|
const ROOT: RootType = "root";
|
||||||
const TRASH: TrashType = "trash";
|
const TRASH: TrashType = "trash";
|
||||||
|
const SHARED_WITH_ME: SharedWithMeType = "shared_with_me";
|
||||||
|
|
||||||
export const isVirtualFolder = (id: string) => {
|
export const isVirtualFolder = (id: string) => {
|
||||||
return id === ROOT || id === TRASH || id.startsWith("user_");
|
return id === ROOT || id === TRASH || id.startsWith("user_") || id == SHARED_WITH_ME;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSharedWithMeFolder = (id: string) => {
|
||||||
|
return id === SHARED_WITH_ME;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getVirtualFoldersNames = async (id: string, context: DriveExecutionContext) => {
|
export const getVirtualFoldersNames = async (id: string, context: DriveExecutionContext) => {
|
||||||
|
|||||||
@@ -141,6 +141,42 @@ export class DocumentsController {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse file, special endpoint for TDrive application widget.
|
||||||
|
* Returns the current folder with the filtered content
|
||||||
|
*
|
||||||
|
* @param {FastifyRequest} request
|
||||||
|
* @returns {Promise<DriveItemDetails>}
|
||||||
|
*/
|
||||||
|
browse = async (
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Params: ItemRequestParams;
|
||||||
|
Body: SearchDocumentsBody;
|
||||||
|
Querystring: PaginationQueryParameters & { public_token?: string };
|
||||||
|
}>,
|
||||||
|
): Promise<DriveItemDetails & { websockets: ResourceWebsocket[] }> => {
|
||||||
|
const context = getDriveExecutionContext(request);
|
||||||
|
const { id } = request.params;
|
||||||
|
|
||||||
|
const options: SearchDocumentsOptions = {
|
||||||
|
...request.body,
|
||||||
|
company_id: request.body.company_id || context.company.id,
|
||||||
|
view: DriveFileDTOBuilder.VIEW_SHARED_WITH_ME,
|
||||||
|
onlyDirectlyShared: true,
|
||||||
|
onlyUploadedNotByMe: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(await globalResolver.services.documents.documents.browse(id, options, context)),
|
||||||
|
websockets: request.currentUser?.id
|
||||||
|
? globalResolver.platformServices.realtime.sign(
|
||||||
|
[{ room: `/companies/${context.company.id}/documents/item/${id}` }],
|
||||||
|
request.currentUser?.id,
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
sharedWithMe = async (
|
sharedWithMe = async (
|
||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Params: RequestParams;
|
Params: RequestParams;
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next)
|
|||||||
handler: documentsController.sharedWithMe.bind(documentsController),
|
handler: documentsController.sharedWithMe.bind(documentsController),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
method: "POST",
|
||||||
|
url: `${baseUrl}/browse/:id`,
|
||||||
|
preValidation: [fastify.authenticate],
|
||||||
|
handler: documentsController.browse.bind(documentsController),
|
||||||
|
});
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `${baseUrl}/tabs/:tab_id`,
|
url: `${baseUrl}/tabs/:tab_id`,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {v1 as uuidv1} from "uuid";
|
|||||||
import {TestDbService} from "../utils.prepare.db";
|
import {TestDbService} from "../utils.prepare.db";
|
||||||
import {DriveFile} from "../../../src/services/documents/entities/drive-file";
|
import {DriveFile} from "../../../src/services/documents/entities/drive-file";
|
||||||
import {FileVersion} from "../../../src/services/documents/entities/file-version";
|
import {FileVersion} from "../../../src/services/documents/entities/file-version";
|
||||||
import {SearchResultMockClass} from "./entities/mock_entities";
|
import { DriveItemDetailsMockClass, SearchResultMockClass } from "./entities/mock_entities";
|
||||||
import {logger} from "../../../src/core/platform/framework";
|
import {logger} from "../../../src/core/platform/framework";
|
||||||
|
|
||||||
export default class TestHelpers {
|
export default class TestHelpers {
|
||||||
@@ -56,7 +56,7 @@ export default class TestHelpers {
|
|||||||
return helpers;
|
return helpers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFiles() {
|
async uploadFiles(parent_id = "root") {
|
||||||
return Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFile(f)));
|
return Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFile(f)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,24 +93,25 @@ export default class TestHelpers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async uploadFileAndCreateDocument(
|
async uploadFileAndCreateDocument(
|
||||||
filename: string
|
filename: string,
|
||||||
|
parent_id = "root"
|
||||||
) {
|
) {
|
||||||
return this.uploadFile(filename).then(f => this.createDocumentFromFile(f));
|
return this.uploadFile(filename).then(f => this.createDocumentFromFile(f, parent_id));
|
||||||
};
|
};
|
||||||
|
|
||||||
async uploadRandomFileAndCreateDocument() {
|
async uploadRandomFileAndCreateDocument(parent_id = "root") {
|
||||||
return this.uploadRandomFile().then(f => this.createDocumentFromFile(f));
|
return this.uploadRandomFile().then(f => this.createDocumentFromFile(f, parent_id));
|
||||||
};
|
};
|
||||||
|
|
||||||
async uploadAllFilesAndCreateDocuments() {
|
async uploadAllFilesAndCreateDocuments(parent_id = "root") {
|
||||||
return await Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFileAndCreateDocument(f)))
|
return await Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFileAndCreateDocument(f, parent_id)))
|
||||||
};
|
};
|
||||||
|
|
||||||
async uploadAllFilesOneByOne() {
|
async uploadAllFilesOneByOne(parent_id = "root") {
|
||||||
const files: Array<DriveFile> = [];
|
const files: Array<DriveFile> = [];
|
||||||
for (const idx in TestHelpers.ALL_FILES) {
|
for (const idx in TestHelpers.ALL_FILES) {
|
||||||
const f = await this.uploadFile(TestHelpers.ALL_FILES[idx]);
|
const f = await this.uploadFile(TestHelpers.ALL_FILES[idx]);
|
||||||
const doc = await this.createDocumentFromFile(f);
|
const doc = await this.createDocumentFromFile(f, parent_id);
|
||||||
files.push(doc);
|
files.push(doc);
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
@@ -136,11 +137,12 @@ export default class TestHelpers {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async createDocumentFromFile(
|
async createDocumentFromFile(
|
||||||
file: File
|
file: File,
|
||||||
|
parent_id = "root",
|
||||||
) {
|
) {
|
||||||
const item = {
|
const item = {
|
||||||
name: file.metadata.name,
|
name: file.metadata.name,
|
||||||
parent_id: "root",
|
parent_id: parent_id,
|
||||||
company_id: file.company_id,
|
company_id: file.company_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,7 +160,7 @@ export default class TestHelpers {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async updateDocument(
|
async updateDocument(
|
||||||
id: string | "root" | "trash",
|
id: string | "root" | "trash" | "shared_with_me",
|
||||||
item: Partial<DriveFile>
|
item: Partial<DriveFile>
|
||||||
) {
|
) {
|
||||||
return await this.platform.app.inject({
|
return await this.platform.app.inject({
|
||||||
@@ -188,6 +190,34 @@ export default class TestHelpers {
|
|||||||
response.body)
|
response.body)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async browseDocuments (
|
||||||
|
id: string,
|
||||||
|
payload: Record<string, any>
|
||||||
|
){
|
||||||
|
const response = await this.platform.app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `${TestHelpers.DOC_URL}/companies/${this.platform.workspace.company_id}/browse/${id}`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${this.jwt}`,
|
||||||
|
},
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
return deserialize<DriveItemDetailsMockClass>(
|
||||||
|
DriveItemDetailsMockClass,
|
||||||
|
response.body)
|
||||||
|
};
|
||||||
|
|
||||||
|
async getDocument(platform: TestPlatform, id: string | "root" | "trash" | "shared_with_me") {
|
||||||
|
return await platform.app.inject({
|
||||||
|
method: "GET",
|
||||||
|
url: `${TestHelpers.DOC_URL}/companies/${platform.workspace.company_id}/item/${id}`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${this.jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
async sharedWithMeDocuments (
|
async sharedWithMeDocuments (
|
||||||
payload: Record<string, any>
|
payload: Record<string, any>
|
||||||
){
|
){
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { describe, beforeEach, afterEach, it, expect, afterAll } from "@jest/globals";
|
||||||
|
import { init, TestPlatform } from "../setup";
|
||||||
|
import { TestDbService } from "../utils.prepare.db";
|
||||||
|
import TestHelpers from "../common/common_test_helpers";
|
||||||
|
|
||||||
|
describe("The Documents Browser Window and API", () => {
|
||||||
|
let platform: TestPlatform;
|
||||||
|
let currentUser: TestHelpers;
|
||||||
|
let dbService: TestDbService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
platform = await init({
|
||||||
|
services: [
|
||||||
|
"webserver",
|
||||||
|
"database",
|
||||||
|
"applications",
|
||||||
|
"search",
|
||||||
|
"storage",
|
||||||
|
"message-queue",
|
||||||
|
"user",
|
||||||
|
"files",
|
||||||
|
"auth",
|
||||||
|
"realtime",
|
||||||
|
"statistics",
|
||||||
|
"platform-services",
|
||||||
|
"documents",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
currentUser = await TestHelpers.getInstance(platform);
|
||||||
|
dbService = await TestDbService.getInstance(platform, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await platform?.tearDown();
|
||||||
|
platform = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("My Drive", () => {
|
||||||
|
|
||||||
|
it("Should successfully upload filed to the 'Shared Drive' and browse them", async () => {
|
||||||
|
const myDriveId = "user_" + currentUser.user.id;
|
||||||
|
const result = await currentUser.uploadAllFilesOneByOne(myDriveId);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.entries()).toBeDefined();
|
||||||
|
expect(Array.from(result.entries())).toHaveLength(TestHelpers.ALL_FILES.length);
|
||||||
|
|
||||||
|
|
||||||
|
const docs = await currentUser.browseDocuments(myDriveId, {});
|
||||||
|
expect(docs).toBeDefined();
|
||||||
|
expect(docs.children).toBeDefined();
|
||||||
|
expect(docs.children.length).toEqual(TestHelpers.ALL_FILES.length)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not be visible for other users", async () => {
|
||||||
|
const myDriveId = "user_" + currentUser.user.id;
|
||||||
|
const anotherUser = await TestHelpers.getInstance(platform, true);
|
||||||
|
await currentUser.uploadAllFilesOneByOne(myDriveId);
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
|
||||||
|
const docs = await currentUser.browseDocuments(myDriveId, {});
|
||||||
|
expect(docs).toBeDefined();
|
||||||
|
expect(docs.children).toBeDefined();
|
||||||
|
expect(docs.children.length).toEqual(TestHelpers.ALL_FILES.length)
|
||||||
|
|
||||||
|
const anotherUserDocs = await anotherUser.searchDocument({});
|
||||||
|
expect(anotherUserDocs).toBeDefined();
|
||||||
|
expect(anotherUserDocs.entities).toBeDefined();
|
||||||
|
expect(anotherUserDocs.entities.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Shared Drive", () => {
|
||||||
|
|
||||||
|
it("Should successfully upload filed to the 'Shared Drive' and browse them", async () => {
|
||||||
|
const result = await currentUser.uploadAllFilesOneByOne("root");
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.entries()).toBeDefined();
|
||||||
|
expect(Array.from(result.entries())).toHaveLength(TestHelpers.ALL_FILES.length);
|
||||||
|
|
||||||
|
|
||||||
|
const docs = await currentUser.browseDocuments("root", {});
|
||||||
|
expect(docs).toBeDefined();
|
||||||
|
expect(docs.children).toBeDefined();
|
||||||
|
expect(docs.children.length).toEqual(TestHelpers.ALL_FILES.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Shared With Me", () => {
|
||||||
|
it("Shouldn't contain files uploaded to the user folder", async () => {
|
||||||
|
const sharedWIthMeFolder = "shared_with_me";
|
||||||
|
await currentUser.uploadAllFilesOneByOne("user_" + currentUser.user.id);
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
|
||||||
|
let docs = await currentUser.browseDocuments(sharedWIthMeFolder, {});
|
||||||
|
expect(docs).toBeDefined();
|
||||||
|
expect(docs.children?.length).toEqual(0)
|
||||||
|
|
||||||
|
await currentUser.uploadAllFilesOneByOne("root");
|
||||||
|
docs = await currentUser.browseDocuments("shared_with_me", {});
|
||||||
|
|
||||||
|
expect(docs).toBeDefined();
|
||||||
|
expect(docs.children?.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should contain files that were shared with the user", async () => {
|
||||||
|
const sharedWIthMeFolder = "shared_with_me";
|
||||||
|
const oneUser = await TestHelpers.getInstance(platform, true);
|
||||||
|
const anotherUser = await TestHelpers.getInstance(platform, true);
|
||||||
|
let files = await oneUser.uploadAllFilesOneByOne();
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
|
||||||
|
//then:: files are not searchable for user without permissions
|
||||||
|
expect((await anotherUser.browseDocuments("shared_with_me", {})).children).toHaveLength(0);
|
||||||
|
|
||||||
|
//give permissions to the file
|
||||||
|
files[0].access_info.entities.push({
|
||||||
|
type: "user",
|
||||||
|
id: anotherUser.user.id,
|
||||||
|
level: "read",
|
||||||
|
grantor: null,
|
||||||
|
});
|
||||||
|
await oneUser.updateDocument(files[0].id, files[0]);
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
//then file become searchable
|
||||||
|
expect((await anotherUser.browseDocuments("shared_with_me", {})).children).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user