🍄 Separate trash for the personal and shared drive (#189)

This commit is contained in:
Montassar Ghanmy
2023-10-06 17:05:07 +01:00
committed by GitHub
parent f0dcf34bbe
commit edfd765d57
34 changed files with 440 additions and 185 deletions
@@ -5,7 +5,7 @@ import { defer, Subject, throwError, timer } from "rxjs";
import { concat, delayWhen, retryWhen, take, tap } from "rxjs/operators"; import { concat, delayWhen, retryWhen, take, tap } from "rxjs/operators";
import { UpsertOptions } from ".."; import { UpsertOptions } from "..";
import { logger } from "../../../../../../framework"; import { logger } from "../../../../../../framework";
import { getEntityDefinition, unwrapIndexes, unwrapPrimarykey } from "../../utils"; import { getEntityDefinition, unwrapPrimarykey } from "../../utils";
import { EntityDefinition, ColumnDefinition, ObjectType } from "../../types"; import { EntityDefinition, ColumnDefinition, ObjectType } from "../../types";
import { AbstractConnector } from "../abstract-connector"; import { AbstractConnector } from "../abstract-connector";
import { import {
@@ -451,22 +451,6 @@ export class CassandraConnector extends AbstractConnector<
const instance = new (entityType as any)(); const instance = new (entityType as any)();
const { columnsDefinition, entityDefinition } = getEntityDefinition(instance); const { columnsDefinition, entityDefinition } = getEntityDefinition(instance);
const pk = unwrapPrimarykey(entityDefinition);
const indexes = unwrapIndexes(entityDefinition);
if (
Object.keys(filters).some(key => pk.indexOf(key) < 0) &&
Object.keys(filters).some(key => indexes.indexOf(key) < 0)
) {
//Filter not in primary key
throw new Error(
`All filter parameters must be defined in entity primary key,
got: ${JSON.stringify(Object.keys(filters))}
on table ${entityDefinition.name} but pk is ${JSON.stringify(pk)},
instance was ${JSON.stringify(instance)}`,
);
}
const query = buildSelectQuery<Table>( const query = buildSelectQuery<Table>(
entityType as unknown as ObjectType<Table>, entityType as unknown as ObjectType<Table>,
filters, filters,
@@ -1,6 +1,6 @@
import { FindOptions } from "../../repository/repository"; import { FindOptions } from "../../repository/repository";
import { ObjectType } from "../../types"; import { ObjectType } from "../../types";
import { getEntityDefinition, secureOperators } from "../../utils"; import { getEntityDefinition, secureOperators, filteringRequired } from "../../utils";
import { transformValueToDbString } from "./typeTransforms"; import { transformValueToDbString } from "./typeTransforms";
export function buildSelectQuery<Entity>( export function buildSelectQuery<Entity>(
@@ -17,12 +17,17 @@ export function buildSelectQuery<Entity>(
): string { ): string {
const instance = new (entityType as any)(); const instance = new (entityType as any)();
const { columnsDefinition, entityDefinition } = getEntityDefinition(instance); const { columnsDefinition, entityDefinition } = getEntityDefinition(instance);
let allowFiltering = false;
const where = Object.keys(filters) const where = Object.keys(filters)
.map(key => { .map(key => {
let result: string; let result: string;
const filter = filters[key]; const filter = filters[key];
if (filteringRequired(key)) {
allowFiltering = true;
}
if (!filter) { if (!filter) {
return; return;
} }
@@ -77,7 +82,7 @@ export function buildSelectQuery<Entity>(
whereClause.trim().length ? "WHERE " + whereClause : "" whereClause.trim().length ? "WHERE " + whereClause : ""
} ${orderByClause.trim().length ? "ORDER BY " + orderByClause : ""}` } ${orderByClause.trim().length ? "ORDER BY " + orderByClause : ""}`
.trimEnd() .trimEnd()
.concat(";"); .concat(allowFiltering ? " ALLOW FILTERING;" : ";");
return query; return query;
} }
@@ -3,7 +3,7 @@ import { UpsertOptions } from "..";
import { ListResult, Paginable, Pagination } from "../../../../../../framework/api/crud-service"; import { ListResult, Paginable, Pagination } from "../../../../../../framework/api/crud-service";
import { FindOptions } from "../../repository/repository"; import { FindOptions } from "../../repository/repository";
import { ColumnDefinition, EntityDefinition, ObjectType } from "../../types"; import { ColumnDefinition, EntityDefinition, ObjectType } from "../../types";
import { getEntityDefinition, unwrapIndexes, unwrapPrimarykey } from "../../utils"; import { getEntityDefinition, unwrapPrimarykey } from "../../utils";
import { AbstractConnector } from "../abstract-connector"; import { AbstractConnector } from "../abstract-connector";
import { buildSelectQuery } from "./query-builder"; import { buildSelectQuery } from "./query-builder";
import { transformValueFromDbString, transformValueToDbString } from "./typeTransforms"; import { transformValueFromDbString, transformValueToDbString } from "./typeTransforms";
@@ -186,24 +186,6 @@ export class MongoConnector extends AbstractConnector<MongoConnectionOptions, mo
const instance = new (entityType as any)(); const instance = new (entityType as any)();
const { columnsDefinition, entityDefinition } = getEntityDefinition(instance); const { columnsDefinition, entityDefinition } = getEntityDefinition(instance);
const pk = unwrapPrimarykey(entityDefinition);
const indexes = unwrapIndexes(entityDefinition);
if (
Object.keys(filters).some(key => pk.indexOf(key) < 0) &&
Object.keys(filters).some(key => indexes.indexOf(key) < 0)
) {
//Filter not in primary key
throw Error(
"All filter parameters must be defined in entity primary key, got: " +
JSON.stringify(Object.keys(filters)) +
" on table " +
entityDefinition.name +
" but pk is " +
JSON.stringify(pk),
);
}
const db = await this.getDatabase(); const db = await this.getDatabase();
const collection = db.collection(`${entityDefinition.name}`); const collection = db.collection(`${entityDefinition.name}`);
@@ -106,3 +106,12 @@ export function toMongoDbOrderable(timeuuid?: string): string {
const time_str = [uuid_arr[2], uuid_arr[1], uuid_arr[0], uuid_arr[3], uuid_arr[4]].join("-"); const time_str = [uuid_arr[2], uuid_arr[1], uuid_arr[0], uuid_arr[3], uuid_arr[4]].join("-");
return time_str; return time_str;
} }
/**
* Check if filtering is necessary
* @param {string} key
* @returns {boolean} Returns true if key is "is_in_trash" or "scope", otherwise returns false.
*/
export const filteringRequired = (key: string) => {
return key === "is_in_trash" || key === "scope";
};
@@ -5,9 +5,13 @@ import { FileVersion } from "./file-version";
import search from "./drive-file.search"; import search from "./drive-file.search";
export const TYPE = "drive_files"; export const TYPE = "drive_files";
export type DriveScope = "personal" | "shared";
@Entity(TYPE, { @Entity(TYPE, {
globalIndexes: [["company_id", "parent_id"]], globalIndexes: [
["company_id", "parent_id"],
["company_id", "is_in_trash"],
],
primaryKey: [["company_id"], "id"], primaryKey: [["company_id"], "id"],
type: TYPE, type: TYPE,
search, search,
@@ -73,6 +77,10 @@ export class DriveFile {
@Column("last_version_cache", "encoded_json") @Column("last_version_cache", "encoded_json")
last_version_cache: Partial<FileVersion>; last_version_cache: Partial<FileVersion>;
@Type(() => String)
@Column("scope", "string")
scope: DriveScope;
} }
export type AccessInformation = { export type AccessInformation = {
@@ -169,28 +169,61 @@ export const getAccessLevel = async (
repository: Repository<DriveFile>, repository: Repository<DriveFile>,
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string }, context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
): Promise<DriveFileAccessLevel | "none"> => { ): Promise<DriveFileAccessLevel | "none"> => {
const isAdmin = !context?.user?.id || (await isCompanyAdmin(context));
const isMember = !context?.user?.id || (await isCompanyMember(context)); const isMember = !context?.user?.id || (await isCompanyMember(context));
if (!id || id === "root") { if (!id || id === "root") {
if (!context?.user?.id || (await isCompanyGuest(context))) { if (!context?.user?.id || (await isCompanyGuest(context))) {
return "none"; return "none";
} else {
if (isMember) return "read";
return "manage";
} }
} }
if (id === "trash")
return (await isCompanyGuest(context)) || !context?.user?.id if (id.startsWith("user_")) {
? "none" if (await isCompanyApplication(context)) return "read";
: (await isCompanyAdmin(context)) }
? "manage"
: "write"; if (context?.user?.id) {
/**
* Drive root directories access management
*/
if (id === "user_" + context.user?.id) return "manage";
if (id === "trash_" + context.user?.id) return "manage";
if (id === "root") {
if (isAdmin) return "manage";
return "read";
}
if (id === "trash") {
if (isAdmin) return "manage";
return "read";
}
if (id === "shared_with_me") return "read"; if (id === "shared_with_me") return "read";
//If it is my personal folder, I have full access /**
if (id.startsWith("user_")) { * Entity based access management
if (id === "user_" + context.user?.id) return "manage"; */
if (await isCompanyApplication(context)) return "manage";
return "none"; if (!item) {
item = await repository.findOne({
id,
company_id: context.company.id,
});
}
if (!item) {
throw Error("Drive item doesn't exist");
}
if (item.scope === "personal" && item.creator == context.user.id) return "manage";
if (item.scope === "shared") {
if (isAdmin) return "manage";
if (isMember) return "read";
}
} }
let publicToken = context.public_token; let publicToken = context.public_token;
@@ -208,13 +241,6 @@ export const getAccessLevel = async (
throw Error("Drive item doesn't exist"); throw Error("Drive item doesn't exist");
} }
if (await isCompanyApplication(context)) {
if (!id.startsWith("user_") && isMember && item.creator != context.user.id) {
return "read";
}
return "manage";
}
/* /*
* Specific user or channel rule is applied first. Then less restrictive level will be chosen * Specific user or channel rule is applied first. Then less restrictive level will be chosen
* between the parent folder and company accesses. * between the parent folder and company accesses.
@@ -383,6 +409,30 @@ export const getSharedByUser = (
return null; return null;
}; };
export const getItemScope = async (
item: DriveFile | null,
repository: Repository<DriveFile>,
context: CompanyExecutionContext,
): Promise<"personal" | "shared"> => {
let scope: "personal" | "shared";
if (item.parent_id === "user_" + context.user?.id) {
scope = "personal";
} else if (item.parent_id === "root") {
scope = "shared";
} else {
const driveItemParent = await repository.findOne(
{
company_id: context.company.id,
id: item.parent_id,
},
{},
context,
);
scope = driveItemParent.scope;
}
return scope;
};
const getGrantorAndThrowIfEmpty = (entity: AuthEntity) => { const getGrantorAndThrowIfEmpty = (entity: AuthEntity) => {
if (!entity.grantor) { if (!entity.grantor) {
logger.warn(`For the file permissions ${entity.id} grantor is not defined`); logger.warn(`For the file permissions ${entity.id} grantor is not defined`);
@@ -34,6 +34,7 @@ export class DriveFileDTOBuilder {
"access_info", "access_info",
"content_keywords", "content_keywords",
"creator", "creator",
"scope",
], ],
], ],
[ [
@@ -40,12 +40,14 @@ import {
isSharedWithMeFolder, isSharedWithMeFolder,
isVirtualFolder, isVirtualFolder,
updateItemSize, updateItemSize,
isInTrash,
} from "../utils"; } from "../utils";
import { import {
checkAccess, checkAccess,
getAccessLevel, getAccessLevel,
hasAccessLevel, hasAccessLevel,
makeStandaloneAccessLevel, makeStandaloneAccessLevel,
getItemScope,
} from "./access-check"; } from "./access-check";
import { websocketEventBus } from "../../../core/platform/services/realtime/bus"; import { websocketEventBus } from "../../../core/platform/services/realtime/bus";
@@ -153,7 +155,6 @@ export class DocumentsService {
throw Error("user does not have access to this item"); throw Error("user does not have access to this item");
} }
} catch (error) { } catch (error) {
console.log(error);
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item"); this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
throw new CrudException("User does not have access to this item or its children", 401); throw new CrudException("User does not have access to this item or its children", 401);
} }
@@ -179,7 +180,22 @@ export class DocumentsService {
await this.repository.find( await this.repository.find(
{ {
company_id: context.company.id, company_id: context.company.id,
...(id.includes(this.TRASH)
? {
is_in_trash: true,
...(id == this.TRASH
? {
scope: "shared",
}
: {
scope: "personal",
creator: context?.user?.id,
}),
}
: {
parent_id: id, parent_id: id,
is_in_trash: false,
}),
}, },
{}, {},
context, context,
@@ -257,6 +273,7 @@ export class DocumentsService {
try { try {
const driveItem = getDefaultDriveItem(content, context); const driveItem = getDefaultDriveItem(content, context);
const driveItemVersion = getDefaultDriveItemVersion(version, context); const driveItemVersion = getDefaultDriveItemVersion(version, context);
driveItem.scope = await getItemScope(driveItem, this.repository, context);
const hasAccess = await checkAccess( const hasAccess = await checkAccess(
driveItem.parent_id, driveItem.parent_id,
@@ -387,7 +404,7 @@ export class DocumentsService {
throw Error("content mismatch"); throw Error("content mismatch");
} }
const updatable = ["access_info", "name", "tags", "parent_id", "description"]; const updatable = ["access_info", "name", "tags", "parent_id", "description", "is_in_trash"];
for (const key of updatable) { for (const key of updatable) {
if ((content as any)[key]) { if ((content as any)[key]) {
@@ -434,6 +451,9 @@ export class DocumentsService {
await updateItemSize(item.parent_id, this.repository, context); await updateItemSize(item.parent_id, this.repository, context);
if (oldParent) { if (oldParent) {
item.scope = await getItemScope(item, this.repository, context);
this.repository.save(item);
await updateItemSize(oldParent, this.repository, context); await updateItemSize(oldParent, this.repository, context);
this.notifyWebsocket(oldParent, context); this.notifyWebsocket(oldParent, context);
} }
@@ -529,6 +549,7 @@ export class DocumentsService {
const previousParentId = item.parent_id; const previousParentId = item.parent_id;
if ( if (
(await isInTrash(item, this.repository, context)) ||
item.parent_id === this.TRASH || item.parent_id === this.TRASH ||
(await getPath(item.parent_id, this.repository, true, context))[0].id === this.TRASH (await getPath(item.parent_id, this.repository, true, context))[0].id === this.TRASH
) { ) {
@@ -568,7 +589,7 @@ export class DocumentsService {
await this.repository.remove(item); await this.repository.remove(item);
} else { } else {
//This item is not in trash, we move it to trash //This item is not in trash, we move it to trash
item.parent_id = this.TRASH; item.is_in_trash = true;
await this.update(item.id, item, context); await this.update(item.id, item, context);
} }
await updateItemSize(previousParentId, this.repository, context); await updateItemSize(previousParentId, this.repository, context);
@@ -579,6 +600,61 @@ export class DocumentsService {
await this.notifyWebsocket("trash", context); await this.notifyWebsocket("trash", context);
}; };
/**
* restore a Drive Document and its children
*
* @param {string} id - the item id
* @param {DriveExecutionContext} context - the execution context
* @returns {Promise<void>}
*/
restore = async (
id: string | RootType | TrashType,
item?: DriveFile,
context?: DriveExecutionContext,
): Promise<void> => {
if (!id) {
//We can't remove the root folder
return;
}
item =
item ||
(await this.repository.findOne({
company_id: context.company.id,
id,
}));
if (!item) {
this.logger.error("item to delete not found");
throw new CrudException("Drive item not found", 404);
}
try {
if (!(await checkAccess(item.id, item, "manage", this.repository, context))) {
this.logger.error("user does not have access drive item ", id);
throw Error("user does not have access to this item");
}
} catch (error) {
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
throw new CrudException("User does not have access to this item or its children", 401);
}
if (isInTrash(item, this.repository, context)) {
if (item.is_in_trash != true) {
if (item.scope === "personal") {
item.parent_id = "user_" + context.user.id;
} else {
item.parent_id = "root";
}
} else {
item.is_in_trash = false;
}
}
this.repository.save(item);
this.notifyWebsocket("trash", context);
};
/** /**
* Create a Drive item version * Create a Drive item version
* *
@@ -30,7 +30,13 @@ const TRASH: TrashType = "trash";
const SHARED_WITH_ME: SharedWithMeType = "shared_with_me"; 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_") || id == SHARED_WITH_ME; return (
id === ROOT ||
id === TRASH ||
id.startsWith("trash_") ||
id.startsWith("user_") ||
id == SHARED_WITH_ME
);
}; };
export const isSharedWithMeFolder = (id: string) => { export const isSharedWithMeFolder = (id: string) => {
@@ -72,6 +78,7 @@ export const getDefaultDriveItem = (
last_modified: new Date().getTime(), last_modified: new Date().getTime(),
parent_id: item.parent_id || "root", parent_id: item.parent_id || "root",
content_keywords: item.content_keywords || "", content_keywords: item.content_keywords || "",
scope: "personal",
description: item.description || "", description: item.description || "",
access_info: item.access_info || { access_info: item.access_info || {
entities: [ entities: [
@@ -163,7 +170,17 @@ export const calculateItemSize = async (
): Promise<number> => { ): Promise<number> => {
if (item.id === "trash") { if (item.id === "trash") {
const trashedItems = await repository.find( const trashedItems = await repository.find(
{ company_id: context.company.id, parent_id: "trash" }, { company_id: context.company.id, is_in_trash: true, scope: "shared" },
{},
context,
);
return trashedItems.getEntities().reduce((acc, curr) => acc + curr.size, 0);
}
if (item.id === "trash_" + context.user.id) {
const trashedItems = await repository.find(
{ company_id: context.company.id, is_in_trash: true, scope: "personal" },
{}, {},
context, context,
); );
@@ -173,7 +190,7 @@ export const calculateItemSize = async (
if (isVirtualFolder(item.id) || !item) { if (isVirtualFolder(item.id) || !item) {
const rootFolderItems = await repository.find( const rootFolderItems = await repository.find(
{ company_id: context.company.id, parent_id: item.id || "root" }, { company_id: context.company.id, parent_id: item.id || "root", is_in_trash: false },
{}, {},
context, context,
); );
@@ -186,6 +203,7 @@ export const calculateItemSize = async (
{ {
company_id: context.company.id, company_id: context.company.id,
parent_id: item.id, parent_id: item.id,
is_in_trash: false,
}, },
{}, {},
context, context,
@@ -561,3 +579,26 @@ export function isFileType(
const fileExtensions = [extension, ...secondaryExtensions]; const fileExtensions = [extension, ...secondaryExtensions];
return fileExtensions.some(e => requiredExtensions.includes(e)); return fileExtensions.some(e => requiredExtensions.includes(e));
} }
export const isInTrash = async (
item: DriveFile,
repository: Repository<DriveFile>,
context: CompanyExecutionContext,
) => {
if (item.is_in_trash === true) {
return true;
}
if (isVirtualFolder(item.parent_id)) {
return false; // Stop condition
}
// Retrieve the parent item
const parentItem = await repository.findOne({
company_id: context.company.id,
id: item.parent_id,
});
// Recursively check the parent item
return isInTrash(parentItem, repository, context);
};
@@ -114,6 +114,29 @@ export class DocumentsController {
} }
}; };
/**
* Restore a DriveFile item from the trashÒ
*
* @param {FastifyRequest} request
* @param {FastifyReply} reply
* @returns {Promise<void>}
*/
restore = async (
request: FastifyRequest<{ Params: ItemRequestParams; Querystring: { public_token?: string } }>,
reply: FastifyReply,
): Promise<void> => {
try {
const context = getDriveExecutionContext(request);
await globalResolver.services.documents.documents.restore(request.params.id, null, context);
reply.status(200).send();
} catch (error) {
logger.error({ error: `${error}` }, "Failed to restore drive item");
throw new CrudException("Failed to restore drive item", 500);
}
};
/** /**
* Lists the drive root folder. * Lists the drive root folder.
* *
@@ -51,6 +51,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next)
handler: documentsController.delete.bind(documentsController), handler: documentsController.delete.bind(documentsController),
}); });
fastify.route({
method: "POST",
url: `${serviceUrl}/:id/restore`,
preValidation: [fastify.authenticateOptional],
handler: documentsController.restore.bind(documentsController),
});
fastify.route({ fastify.route({
method: "GET", method: "GET",
url: `${serviceUrl}/:id/user/:user_id/access`, url: `${serviceUrl}/:id/user/:user_id/access`,
@@ -83,6 +83,7 @@ const documentSchema = {
last_user: { type: "string" }, last_user: { type: "string" },
attachements: { type: "array" }, attachements: { type: "array" },
last_version_cache: fileVersionSchema, last_version_cache: fileVersionSchema,
scope: { type: "string" },
}, },
}; };
@@ -29,6 +29,7 @@ export class DriveFileMockClass {
access_info: MockAccessInformation; access_info: MockAccessInformation;
creator: string; creator: string;
is_directory: boolean; is_directory: boolean;
scope: "personal" | "shared";
created_by: Record<string, any>; created_by: Record<string, any>;
shared_by: Record<string, any>; shared_by: Record<string, any>;
} }
@@ -59,11 +59,12 @@ describe("the Drive feature", () => {
const createItem = async (): Promise<DriveFileMockClass> => { const createItem = async (): Promise<DriveFileMockClass> => {
await TestDbService.getInstance(platform, true); await TestDbService.getInstance(platform, true);
const scope: "personal" | "shared" = "shared";
const item = { const item = {
name: "new test file", name: "new test file",
parent_id: "root", parent_id: "root",
company_id: platform.workspace.company_id, company_id: platform.workspace.company_id,
scope,
}; };
const version = {}; const version = {};
@@ -141,8 +142,9 @@ describe("the Drive feature", () => {
DriveItemDetailsMockClass, DriveItemDetailsMockClass,
listTrashResponse.body, listTrashResponse.body,
); );
expect(listTrashResult.item.name).toEqual("Trash"); expect(listTrashResult.item.name).toEqual("Trash");
expect(createItemResult).toBeDefined();
expect(createItemResult.scope).toEqual("shared");
expect(listTrashResult.children.some(({ id }) => id === createItemResult.id)).toBeTruthy(); expect(listTrashResult.children.some(({ id }) => id === createItemResult.id)).toBeTruthy();
}); });
@@ -62,9 +62,7 @@ describe("the public links feature", () => {
platform = null; platform = null;
}); });
describe("Basic Flow", () => { describe("Basic Flow", () => {
const createItem = async (): Promise<DriveFileMockClass> => { const createItem = async (): Promise<DriveFileMockClass> => {
await TestHelpers.getInstance(platform, true, { companyRole: "admin" }); await TestHelpers.getInstance(platform, true, { companyRole: "admin" });
@@ -284,33 +282,7 @@ describe("the public links feature", () => {
}); });
}); });
describe("Share file from My Drive", () => {
it("Share file from some folder", async () => {
const user = await TestHelpers.getInstance(platform, true);
const anotherUser = await TestHelpers.getInstance(platform, true);
//create directory in "My Drive" and upload a file
const directory = await user.createDirectory("user_" + user.user.id);
const doc = await user.createRandomDocument(directory.id);
//check that another user doesn't see any file
expect((await anotherUser.getDocument(doc.id)).statusCode).toBe(401);
//share file with the public link
await user.shareWithPublicLink(doc, "read");
const token = await anotherUser.getPublicLinkAccessToken(doc);
anotherUser.jwt = token.value;
await anotherUser.getDocumentOKCheck(doc.id);
});
});
describe("Download Folder from shared link", () => { describe("Download Folder from shared link", () => {
it("Share folder", async () => { it("Share folder", async () => {
const user = await TestHelpers.getInstance(platform, true); const user = await TestHelpers.getInstance(platform, true);
const anotherUser = await TestHelpers.getInstance(platform, true); const anotherUser = await TestHelpers.getInstance(platform, true);
@@ -332,9 +304,5 @@ describe("the public links feature", () => {
expect((await anotherUser.getFolder(doc.id)).statusCode).toBe(200); expect((await anotherUser.getFolder(doc.id)).statusCode).toBe(200);
}); });
}); });
}); });
+4
View File
@@ -236,6 +236,7 @@
"hooks.use-drive-actions.unable_create_file":"Unable to create a new file.", "hooks.use-drive-actions.unable_create_file":"Unable to create a new file.",
"hooks.use-drive-actions.unable_download_file":"Unable to download this files.", "hooks.use-drive-actions.unable_download_file":"Unable to download this files.",
"hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.", "hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.",
"hooks.use-drive-actions.unable_restore_file":"Unable to restore this item.",
"hooks.use-drive-actions.unable_update_file":"Unable to update this file.", "hooks.use-drive-actions.unable_update_file":"Unable to update this file.",
"general.resume": "Resume", "general.resume": "Resume",
"general.pause": "Pause", "general.pause": "Pause",
@@ -264,6 +265,8 @@
"components.side_menu.buttons.upload": "Upload", "components.side_menu.buttons.upload": "Upload",
"components.side_menu.buttons.create": "Create", "components.side_menu.buttons.create": "Create",
"components.side_menu.buttons.empty_trash": "Empty trash", "components.side_menu.buttons.empty_trash": "Empty trash",
"components.header_path.shared_trash": "Shared Trash",
"components.header_path.my_trash": "My Trash",
"scenes.app.drive.nothing": "Nothing here.", "scenes.app.drive.nothing": "Nothing here.",
"scenes.app.drive.drag_and_drop": "Drag and drop files to upload them or click on the 'Add document' button.", "scenes.app.drive.drag_and_drop": "Drag and drop files to upload them or click on the 'Add document' button.",
"scenes.app.drive.add_doc": "Add document or folder", "scenes.app.drive.add_doc": "Add document or folder",
@@ -297,6 +300,7 @@
"components.item_context_menu.move.modal_header": "Move", "components.item_context_menu.move.modal_header": "Move",
"components.item_context_menu.move_to_trash": "Delete", "components.item_context_menu.move_to_trash": "Delete",
"components.item_context_menu.delete": "Delete", "components.item_context_menu.delete": "Delete",
"components.item_context_menu.restore": "Restore",
"components.item_context_menu.move_multiple": "Move selected items", "components.item_context_menu.move_multiple": "Move selected items",
"components.item_context_menu.move_multiple.modal_header": "Move selected items", "components.item_context_menu.move_multiple.modal_header": "Move selected items",
"components.item_context_menu.download_multiple": "Download selected items", "components.item_context_menu.download_multiple": "Download selected items",
+4
View File
@@ -220,6 +220,7 @@
"hooks.use-drive-actions.unable_create_file":"Impossible de créer ce fichier", "hooks.use-drive-actions.unable_create_file":"Impossible de créer ce fichier",
"hooks.use-drive-actions.unable_download_file":"Impossible de télécharger ce fichier", "hooks.use-drive-actions.unable_download_file":"Impossible de télécharger ce fichier",
"hooks.use-drive-actions.unable_remove_file":"Impossible de supprimer ce fichier", "hooks.use-drive-actions.unable_remove_file":"Impossible de supprimer ce fichier",
"hooks.use-drive-actions.unable_restore_file":"Impossible de restaurer cet élément.",
"hooks.use-drive-actions.unable_update_file":"Impossible de mettre à jour ce fichier", "hooks.use-drive-actions.unable_update_file":"Impossible de mettre à jour ce fichier",
"general.resume": "Reprendre", "general.resume": "Reprendre",
"general.pause": "Pause", "general.pause": "Pause",
@@ -264,6 +265,8 @@
"components.side_menu.buttons.upload": "Télécharger", "components.side_menu.buttons.upload": "Télécharger",
"components.side_menu.buttons.create": "Créer", "components.side_menu.buttons.create": "Créer",
"components.side_menu.buttons.empty_trash": "Vider la corbeille", "components.side_menu.buttons.empty_trash": "Vider la corbeille",
"components.header_path.shared_trash": "Corbeille partagée",
"components.header_path.my_trash": "Ma corbeille",
"scenes.app.drive.nothing": "Rien ici.", "scenes.app.drive.nothing": "Rien ici.",
"scenes.app.drive.drag_and_drop": "Faites glisser et déposez des fichiers pour les télécharger ou cliquez sur le bouton 'Ajouter un document'.", "scenes.app.drive.drag_and_drop": "Faites glisser et déposez des fichiers pour les télécharger ou cliquez sur le bouton 'Ajouter un document'.",
"scenes.app.drive.add_doc": "Ajouter un document ou un dossier", "scenes.app.drive.add_doc": "Ajouter un document ou un dossier",
@@ -297,6 +300,7 @@
"components.item_context_menu.move.modal_header": "Déplacer", "components.item_context_menu.move.modal_header": "Déplacer",
"components.item_context_menu.move_to_trash": "Supprimer", "components.item_context_menu.move_to_trash": "Supprimer",
"components.item_context_menu.delete": "Supprimer", "components.item_context_menu.delete": "Supprimer",
"components.item_context_menu.restore": "Restaurer",
"components.item_context_menu.move_multiple": "Déplacer les éléments sélectionnés", "components.item_context_menu.move_multiple": "Déplacer les éléments sélectionnés",
"components.item_context_menu.move_multiple.modal_header": "Déplacer les éléments sélectionnés", "components.item_context_menu.move_multiple.modal_header": "Déplacer les éléments sélectionnés",
"components.item_context_menu.download_multiple": "Télécharger les éléments sélectionnés", "components.item_context_menu.download_multiple": "Télécharger les éléments sélectionnés",
+4
View File
@@ -220,6 +220,7 @@
"hooks.use-drive-actions.unable_create_file":"Unable to create a new file.", "hooks.use-drive-actions.unable_create_file":"Unable to create a new file.",
"hooks.use-drive-actions.unable_download_file":"Unable to download this files.", "hooks.use-drive-actions.unable_download_file":"Unable to download this files.",
"hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.", "hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.",
"hooks.use-drive-actions.unable_restore_file":"Unable to restore this item.",
"hooks.use-drive-actions.unable_update_file":"Unable to update this file.", "hooks.use-drive-actions.unable_update_file":"Unable to update this file.",
"general.resume": "Продолжить", "general.resume": "Продолжить",
"general.pause": "Пауза", "general.pause": "Пауза",
@@ -264,6 +265,8 @@
"components.side_menu.buttons.upload": "Загрузить", "components.side_menu.buttons.upload": "Загрузить",
"components.side_menu.buttons.create": "Создать", "components.side_menu.buttons.create": "Создать",
"components.side_menu.buttons.empty_trash": "Очистить козину", "components.side_menu.buttons.empty_trash": "Очистить козину",
"components.header_path.shared_trash": "Корзина \"Общего диска\"",
"components.header_path.my_trash": "Корзина \"Моего диска\"",
"scenes.app.drive.nothing": "Здесь ничего нет.", "scenes.app.drive.nothing": "Здесь ничего нет.",
"scenes.app.drive.drag_and_drop": "Перенесите сюда файлы чтобы загрузить их или нажмите кнопку \"Добавить файл\"", "scenes.app.drive.drag_and_drop": "Перенесите сюда файлы чтобы загрузить их или нажмите кнопку \"Добавить файл\"",
"scenes.app.drive.add_doc": "Добавить файл или папку", "scenes.app.drive.add_doc": "Добавить файл или папку",
@@ -297,6 +300,7 @@
"components.item_context_menu.move.modal_header": "Переместить", "components.item_context_menu.move.modal_header": "Переместить",
"components.item_context_menu.move_to_trash": "Отправить в корзину", "components.item_context_menu.move_to_trash": "Отправить в корзину",
"components.item_context_menu.delete": "Удалить", "components.item_context_menu.delete": "Удалить",
"components.item_context_menu.restore": "Restore",
"components.item_context_menu.move_multiple": "Переместить все", "components.item_context_menu.move_multiple": "Переместить все",
"components.item_context_menu.move_multiple.modal_header": "Переместить выбранные елементы", "components.item_context_menu.move_multiple.modal_header": "Переместить выбранные елементы",
"components.item_context_menu.download_multiple": "Скачать все", "components.item_context_menu.download_multiple": "Скачать все",
@@ -83,6 +83,13 @@ export class DriveApiClient {
); );
} }
static async restore(companyId: string, id: string | 'trash' | '') {
return await Api.post<any, any>(
`/internal/services/documents/v1/companies/${companyId}/item/${id}/restore${appendTdriveToken()}`,
{}
);
}
static async update(companyId: string, id: string, update: Partial<DriveItem>) { static async update(companyId: string, id: string, update: Partial<DriveItem>) {
return await Api.post<Partial<DriveItem>, DriveItem>( return await Api.post<Partial<DriveItem>, DriveItem>(
`/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`, `/internal/services/documents/v1/companies/${companyId}/item/${id}${appendTdriveToken()}`,
@@ -96,6 +96,18 @@ export const useDriveActions = () => {
[refresh], [refresh],
); );
const restore = useCallback(
async (id: string, parentId: string) => {
try {
await DriveApiClient.restore(companyId, id);
await refresh(parentId || '');
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_restore_file'));
}
},
[refresh],
);
const update = useCallback( const update = useCallback(
async (update: Partial<DriveItem>, id: string, parentId: string) => { async (update: Partial<DriveItem>, id: string, parentId: string) => {
try { try {
@@ -127,5 +139,5 @@ export const useDriveActions = () => {
[refresh], [refresh],
); );
return { create, refresh, download, downloadZip, remove, update, updateLevel }; return { create, refresh, download, downloadZip, remove, restore, update, updateLevel };
}; };
@@ -19,7 +19,7 @@ export const useDriveItem = (id: string) => {
const item = useRecoilValue(DriveItemAtom(id)); const item = useRecoilValue(DriveItemAtom(id));
const children = useRecoilValue(DriveItemChildrenAtom(id)); const children = useRecoilValue(DriveItemChildrenAtom(id));
const [loading, setLoading] = useRecoilState(LoadingStateInitTrue('useDriveItem-' + id)); const [loading, setLoading] = useRecoilState(LoadingStateInitTrue('useDriveItem-' + id));
const { refresh: refreshItem, create, update: _update, updateLevel: _updateLevel, remove: _remove } = useDriveActions(); const { refresh: refreshItem, create, update: _update, updateLevel: _updateLevel, remove: _remove, restore: _restore } = useDriveActions();
const { uploadVersion: _uploadVersion } = useDriveUpload(); const { uploadVersion: _uploadVersion } = useDriveUpload();
const refresh = useCallback( const refresh = useCallback(
@@ -44,6 +44,16 @@ export const useDriveItem = (id: string) => {
setLoading(false); setLoading(false);
}, [id, setLoading, refresh, item?.item?.parent_id]); }, [id, setLoading, refresh, item?.item?.parent_id]);
const restore = useCallback(async () => {
setLoading(true);
try {
await _restore(id, item?.item?.parent_id || '');
} catch (e) {
ToasterService.error('Unable to restore this item.');
}
setLoading(false);
}, [id, setLoading, refresh, item?.item?.parent_id]);
const update = useCallback( const update = useCallback(
async (update: Partial<DriveItem>) => { async (update: Partial<DriveItem>) => {
setLoading(true); setLoading(true);
@@ -83,7 +93,7 @@ export const useDriveItem = (id: string) => {
[companyId, id, setLoading, refresh, item?.item?.parent_id], [companyId, id, setLoading, refresh, item?.item?.parent_id],
); );
const inTrash = id === 'trash' || item?.path?.some(i => i.parent_id === 'trash'); const inTrash = id.includes('trash') || item?.path?.some(i => i?.parent_id?.includes('trash')) || item?.item?.is_in_trash;
const sharedWithMe = id =="shared_with_me"; const sharedWithMe = id =="shared_with_me";
return { return {
@@ -31,7 +31,6 @@ export const useDrivePreviewModal = () => {
const close = () => { const close = () => {
setStatus({ item: null, loading: true }); setStatus({ item: null, loading: true });
history.push(RouterServices.generateRouteFromState({companyId: company, itemId: ""}));
} }
return { open, close, isOpen: !!status.item }; return { open, close, isOpen: !!status.item };
@@ -21,7 +21,7 @@ export type DriveItem = {
workspace_id: string; workspace_id: string;
parent_id: string; parent_id: string;
in_trash: boolean; is_in_trash: boolean;
is_directory: boolean; is_directory: boolean;
name: string; name: string;
extension: string; extension: string;
@@ -35,6 +35,7 @@ export type DriveItem = {
access_info: DriveItemAccessInfo; access_info: DriveItemAccessInfo;
size: number; size: number;
scope: string;
}; };
export type DriveFileAccessLevel = 'none' | 'read' | 'write' | 'manage'; export type DriveFileAccessLevel = 'none' | 'read' | 'write' | 'manage';
@@ -27,6 +27,7 @@ export type ClientStateType = {
companyId?: string; companyId?: string;
viewId?: string; viewId?: string;
itemId?: string; itemId?: string;
dirId?: string;
workspaceId?: string; workspaceId?: string;
channelId?: string; channelId?: string;
messageId?: string; messageId?: string;
@@ -52,6 +53,7 @@ class RouterServices extends Observable {
'/client/:companyId/v/:viewId', '/client/:companyId/v/:viewId',
'/client/:companyId/preview/:itemId', '/client/:companyId/preview/:itemId',
'/client/:companyId/v/:viewId/preview/:itemId', '/client/:companyId/v/:viewId/preview/:itemId',
'/client/:companyId/v/:viewId/d/:dirId',
'/client/:companyId/w/:workspaceId', '/client/:companyId/w/:workspaceId',
'/client/:companyId/w/:workspaceId/c/:channelId', '/client/:companyId/w/:workspaceId/c/:channelId',
'/client/:companyId/w/:workspaceId/c/:channelId/t/:threadId', '/client/:companyId/w/:workspaceId/c/:channelId/t/:threadId',
@@ -185,6 +187,7 @@ class RouterServices extends Observable {
companyId: match?.params?.companyId || '', companyId: match?.params?.companyId || '',
viewId: match?.params?.viewId || '', viewId: match?.params?.viewId || '',
itemId: match?.params?.itemId || '', itemId: match?.params?.itemId || '',
dirId: match?.params?.dirId || '',
workspaceId: match?.params?.workspaceId || '', workspaceId: match?.params?.workspaceId || '',
channelId: match?.params?.channelId || '', channelId: match?.params?.channelId || '',
messageId: match?.params?.messageId || '', messageId: match?.params?.messageId || '',
@@ -291,6 +294,7 @@ class RouterServices extends Observable {
(state.companyId ? `/${state.companyId}` : '') + (state.companyId ? `/${state.companyId}` : '') +
(state.viewId ? `/v/${state.viewId}` : '') + (state.viewId ? `/v/${state.viewId}` : '') +
(state.itemId ? `/preview/${state.itemId}` : '') + (state.itemId ? `/preview/${state.itemId}` : '') +
(state.dirId ? `/d/${state.dirId}` : '') +
(state.sharedWithMe ? `/shared-with-me` : '') + (state.sharedWithMe ? `/shared-with-me` : '') +
(state.workspaceId ? `/w/${state.workspaceId}` : '') + (state.workspaceId ? `/w/${state.workspaceId}` : '') +
(state.channelId ? `/c/${state.channelId}` : '') + (state.channelId ? `/c/${state.channelId}` : '') +
@@ -16,6 +16,11 @@ export const RoutePreviewSelector = selector<string>({
get: ({ get }) => get(RouterState)?.itemId || '', get: ({ get }) => get(RouterState)?.itemId || '',
}); });
export const RouteDirectorySelector = selector<string>({
key: 'RouterDirectorySelector',
get: ({ get }) => get(RouterState)?.dirId || '',
});
export const RouterWorkspaceSelector = selector<string>({ export const RouterWorkspaceSelector = selector<string>({
key: 'RouterWorkspaceSelector', key: 'RouterWorkspaceSelector',
get: ({ get }) => get(RouterState)?.workspaceId || '', get: ({ get }) => get(RouterState)?.workspaceId || '',
@@ -113,13 +113,6 @@ export default memo(
[_setParentId], [_setParentId],
); );
//In case we are kicked out of the current folder, we need to reset the parent id
useEffect(() => {
if (!loading && !path?.length && !inPublicSharing && !sharedWithMe) setParentId('user_'+user?.id);
}, [path, loading, setParentId]);
useEffect(() => { useEffect(() => {
setChecked({}); setChecked({});
refresh(parentId); refresh(parentId);
@@ -19,6 +19,7 @@ import { ToasterService } from '@features/global/services/toaster-service';
import { copyToClipboard } from '@features/global/utils/CopyClipboard'; import { copyToClipboard } from '@features/global/utils/CopyClipboard';
import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter'; import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter';
import { getCurrentUserList } from '@features/users/hooks/use-user-list'; 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 RouterServices from '@features/router/services/router-service';
import useRouterCompany from '@features/router/hooks/use-router-company'; import useRouterCompany from '@features/router/hooks/use-router-company';
import _ from 'lodash'; import _ from 'lodash';
@@ -35,7 +36,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }), DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'root' }),
); );
const { download, downloadZip, update } = useDriveActions(); const { download, downloadZip, update, restore } = useDriveActions();
const setCreationModalState = useSetRecoilState(CreateModalAtom); const setCreationModalState = useSetRecoilState(CreateModalAtom);
const setSelectorModalState = useSetRecoilState(SelectorModalAtom); const setSelectorModalState = useSetRecoilState(SelectorModalAtom);
const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom); const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom);
@@ -45,6 +46,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
const setPropertiesModalState = useSetRecoilState(PropertiesModalAtom); const setPropertiesModalState = useSetRecoilState(PropertiesModalAtom);
const setUsersModalState = useSetRecoilState(UsersModalAtom); const setUsersModalState = useSetRecoilState(UsersModalAtom);
const { open: preview } = useDrivePreview(); const { open: preview } = useDrivePreview();
const { viewId } = useRouteState();
const company = useRouterCompany(); const company = useRouterCompany();
function getIdsFromArray(arr: DriveItem[]): string[] { function getIdsFromArray(arr: DriveItem[]): string[] {
@@ -56,7 +58,8 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
if (!parent || !parent.access) return []; if (!parent || !parent.access) return [];
try { try {
const inTrash = parent.path?.[0]?.id === 'trash'; const inTrash = parent.path?.[0]?.id.includes('trash') || viewId?.includes("trash");
const isPersonal = item?.scope === 'personal';
const selectedCount = checked.length; const selectedCount = checked.length;
let menu: any[] = []; let menu: any[] = [];
@@ -75,7 +78,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.share'), text: Languages.t('components.item_context_menu.share'),
hide: access === 'read' || getPublicLinkToken(), hide: access === 'read' || getPublicLinkToken() || inTrash,
onClick: () => setAccessModalState({ open: true, id: item.id }), onClick: () => setAccessModalState({ open: true, id: item.id }),
}, },
{ {
@@ -106,13 +109,13 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.manage_access'), text: Languages.t('components.item_context_menu.manage_access'),
hide: access === 'read' || getPublicLinkToken(), hide: access === 'read' || getPublicLinkToken() || inTrash,
onClick: () => setAccessModalState({ open: true, id: item.id }), onClick: () => setAccessModalState({ open: true, id: item.id }),
}, },
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.move'), text: Languages.t('components.item_context_menu.move'),
hide: access === 'read', hide: access === 'read' || inTrash,
onClick: () => onClick: () =>
setSelectorModalState({ setSelectorModalState({
open: true, open: true,
@@ -135,13 +138,13 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.rename'), text: Languages.t('components.item_context_menu.rename'),
hide: access === 'read', hide: access === 'read' || inTrash,
onClick: () => setPropertiesModalState({ open: true, id: item.id }), onClick: () => setPropertiesModalState({ open: true, id: item.id }),
}, },
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.copy_link'), text: Languages.t('components.item_context_menu.copy_link'),
hide: !item.access_info.public?.level || item.access_info.public?.level === 'none', hide: !item.access_info.public?.level || item.access_info.public?.level === 'none' || inTrash,
onClick: () => { onClick: () => {
copyToClipboard(getPublicLink(item || parent?.item)); copyToClipboard(getPublicLink(item || parent?.item));
ToasterService.success( ToasterService.success(
@@ -152,10 +155,10 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.versions'), text: Languages.t('components.item_context_menu.versions'),
hide: item.is_directory, hide: item.is_directory || inTrash,
onClick: () => setVersionModal({ open: true, id: item.id }), onClick: () => setVersionModal({ open: true, id: item.id }),
}, },
{ type: 'separator', hide: access !== 'manage' }, { type: 'separator', hide: access !== 'manage' || inTrash, },
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.move_to_trash'), text: Languages.t('components.item_context_menu.move_to_trash'),
@@ -163,6 +166,16 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
hide: inTrash || access !== 'manage', hide: inTrash || access !== 'manage',
onClick: () => setConfirmTrashModalState({ open: true, items: [item] }), onClick: () => setConfirmTrashModalState({ open: true, items: [item] }),
}, },
{
type: 'menu',
text: Languages.t('components.item_context_menu.restore'),
className: 'error',
hide: !inTrash || (access !== 'manage' && !isPersonal),
onClick: () => {
const parentId = item.is_in_trash ? viewId || '' : item.parent_id;
restore(item.id, parentId);
},
},
{ {
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.delete'), text: Languages.t('components.item_context_menu.delete'),
@@ -257,7 +270,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
type: 'menu', type: 'menu',
text: Languages.t('components.item_context_menu.trash.empty'), text: Languages.t('components.item_context_menu.trash.empty'),
className: 'error', className: 'error',
hide: parent.item!.id != 'trash' || parent.access !== 'manage', hide: !inTrash,
onClick: () => { onClick: () => {
setConfirmDeleteModalState({ setConfirmDeleteModalState({
open: true, open: true,
@@ -57,7 +57,7 @@ export const DocumentRow = ({
const device = getDevice(); const device = getDevice();
console.log("DEVICE:: " + device); console.log("DEVICE:: " + device);
if (device != "ios" && device != "android") { if (device != "ios" && device != "android") {
history.push(RouterServices.generateRouteFromState({companyId: company, itemId: item.id})); history.push(RouterServices.generateRouteFromState({companyId: company, itemId: item.id, dirId: ''}));
open(item); open(item);
} }
}; };
@@ -5,7 +5,11 @@ import { useEffect, useState } from 'react';
import { PublicIcon } from './components/public-icon'; import { PublicIcon } from './components/public-icon';
import MenusManager from '@components/menus/menus-manager.jsx'; import MenusManager from '@components/menus/menus-manager.jsx';
import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
import Languages from "features/global/services/languages-service"; import { useDriveItem } from '@features/drive/hooks/use-drive-item';
import Languages from 'features/global/services/languages-service';
import { useHistory } from 'react-router-dom';
import useRouterCompany from '@features/router/hooks/use-router-company';
import RouterServices from '@features/router/services/router-service';
export default ({ export default ({
path: livePath, path: livePath,
@@ -17,18 +21,36 @@ export default ({
setParentId: (id: string) => void; setParentId: (id: string) => void;
}) => { }) => {
const [savedPath, setSavedPath] = useState<DriveItem[]>([]); const [savedPath, setSavedPath] = useState<DriveItem[]>([]);
const { user } = useCurrentUser();
const history = useHistory();
const company = useRouterCompany();
useEffect(() => { useEffect(() => {
if (livePath) setSavedPath(livePath); if (livePath) setSavedPath(livePath);
}, [livePath]); }, [livePath]);
const path = livePath || savedPath; const path = livePath || savedPath;
return <PathRender inTrash={inTrash || false} path={path} onClick={id => setParentId(id)} />; return (
<PathRender
inTrash={inTrash || false}
path={path}
onClick={(viewId, dirId) => {
history.push(
RouterServices.generateRouteFromState({
companyId: company,
viewId,
dirId
}),
);
setParentId(dirId? dirId: viewId)
}}
/>
);
}; };
function cutFileName(name: any) { function cutFileName(name: any) {
if (typeof name !== "undefined" ){ if (typeof name !== 'undefined') {
if (name.length >= 30) { if (name.length >= 30) {
return name.substring(0,30)+" ..."; return name.substring(0, 30) + ' ...';
} else { } else {
return name; return name;
} }
@@ -43,7 +65,7 @@ export const PathRender = ({
}: { }: {
path: DriveItem[]; path: DriveItem[];
inTrash: boolean; inTrash: boolean;
onClick: (id: string) => void; onClick: (viewId: string, dirId: string) => void;
}) => { }) => {
const pathLength = (path || []).reduce((acc, curr) => acc + curr.name.length, 0); const pathLength = (path || []).reduce((acc, curr) => acc + curr.name.length, 0);
@@ -100,30 +122,61 @@ const PathItem = ({
item: Partial<DriveItem>; item: Partial<DriveItem>;
last?: boolean; last?: boolean;
first?: boolean; first?: boolean;
onClick: (id: string) => void; onClick: (viewId: string, dirId: string) => void;
}) => { }) => {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { viewId } = RouterServices.getStateFromRoute();
const { access: trashAccess } = useDriveItem('trash');
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<a <a
href="#" href="#"
className="text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white" className="text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white"
onClick={evt => { onClick={evt => {
const driveMenuItems = [
{
type: 'menu',
text: Languages.t('components.side_menu.home',),
onClick: () => onClick('root', ''),
},
{
type: 'menu',
text: Languages.t('components.side_menu.my_drive'),
onClick: () => onClick('user_' + user?.id, ''),
},
];
const trashMenuItems = [
{
type: 'menu',
text: Languages.t('components.header_path.my_trash'),
onClick: () => onClick('trash_' + user?.id, ''),
},
{
type: 'menu',
text: Languages.t('components.header_path.shared_trash'),
onClick: () => onClick('trash', ''),
hide: trashAccess === 'read',
},
];
if (first && user?.id) { if (first && user?.id) {
MenusManager.openMenu( if (viewId?.includes('trash')) {
[ MenusManager.openMenu(trashMenuItems, { x: evt.clientX, y: evt.clientY }, 'center');
{ type: 'menu', text: Languages.t('components.side_menu.home'), onClick: () => onClick('root') },
{ type: 'menu', text: Languages.t('components.side_menu.my_drive'), onClick: () => onClick('user_' + user?.id) },
],
{ x: evt.clientX, y: evt.clientY },
'center',
);
} else { } else {
onClick(item?.id || ''); MenusManager.openMenu(driveMenuItems, { x: evt.clientX, y: evt.clientY }, 'center');
}
} else {
onClick(viewId || '',item?.id || '');
} }
}} }}
> >
<Title>{cutFileName(item?.name) || ''}</Title> <Title>
{viewId?.includes('trash_') && first && Languages.t('components.header_path.my_trash')}
{viewId === 'trash' && first && Languages.t('components.header_path.shared_trash')}
{(viewId === 'trash' || viewId?.includes('trash_')) && !first && (cutFileName(item?.name) || '')}
{!viewId?.includes('trash') && (cutFileName(item?.name) || '')}
</Title>
</a> </a>
{item?.access_info?.public?.level && item?.access_info?.public?.level !== 'none' && ( {item?.access_info?.public?.level && item?.access_info?.public?.level !== 'none' && (
<PublicIcon className="h-5 w-5 ml-2" /> <PublicIcon className="h-5 w-5 ml-2" />
@@ -7,6 +7,7 @@ import { DriveItemSelectedList } from '@features/drive/state/store';
import { DriveItem } from '@features/drive/types'; import { DriveItem } from '@features/drive/types';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { atom, useRecoilState } from 'recoil'; import { atom, useRecoilState } from 'recoil';
import RouterServices from '@features/router/services/router-service';
export type ConfirmDeleteModalType = { export type ConfirmDeleteModalType = {
open: boolean; open: boolean;
@@ -40,6 +41,7 @@ const ConfirmDeleteModalContent = ({ items }: { items: DriveItem[] }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [state, setState] = useRecoilState(ConfirmDeleteModalAtom); const [state, setState] = useRecoilState(ConfirmDeleteModalAtom);
const [, setSelected] = useRecoilState(DriveItemSelectedList); const [, setSelected] = useRecoilState(DriveItemSelectedList);
const { viewId } = RouterServices.getStateFromRoute();
useEffect(() => { useEffect(() => {
refresh(items[0].id); refresh(items[0].id);
@@ -64,7 +66,7 @@ const ConfirmDeleteModalContent = ({ items }: { items: DriveItem[] }) => {
onClick={async () => { onClick={async () => {
setLoading(true); setLoading(true);
for (const item of items) { for (const item of items) {
await remove(item.id, item.parent_id); await remove(item.id, viewId || "");
} }
setLoading(false); setLoading(false);
setSelected({}); setSelected({});
@@ -111,9 +111,9 @@ const PublicLinkOptions = (props: {
}; };
/* useEffect(() => { useEffect(() => {
props.onChangePassword(usePassword ? password : ''); props.onChangePassword(usePassword ? password : '');
}, [usePassword, password]); */ }, [usePassword, password]);
useEffect(() => { useEffect(() => {
props.onChangeExpiration(useExpiration ? expiration : 0); props.onChangeExpiration(useExpiration ? expiration : 0);
@@ -144,18 +144,6 @@ const PublicLinkOptions = (props: {
className="max-w-xs" className="max-w-xs"
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
onBlur={handlePasswordBlur}
// changes password only when press enter
onKeyPress={e => {
if (e.key === 'Enter') {
props.onChangePassword(password);
e.preventDefault();
if (password) {
copyToClipboard(password);
ToasterService.success(Languages.t('components.public-link-security_password_copied'));
}
}
}}
// saves and copies password // saves and copies password
onClick={() => { onClick={() => {
if (password) copyToClipboard(password); if (password) copyToClipboard(password);
@@ -13,6 +13,7 @@ import { CreateModal, CreateModalAtom } from '../body/drive/modals/create';
import { Button } from '@atoms/button/button'; import { Button } from '@atoms/button/button';
import Languages from "features/global/services/languages-service"; import Languages from "features/global/services/languages-service";
import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
import RouterServices from '@features/router/services/router-service';
export const CreateModalWithUploadZones = ({ initialParentId }: { initialParentId?: string }) => { export const CreateModalWithUploadZones = ({ initialParentId }: { initialParentId?: string }) => {
const companyId = useRouterCompany(); const companyId = useRouterCompany();
@@ -25,8 +26,6 @@ export const CreateModalWithUploadZones = ({ initialParentId }: { initialParentI
DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'user_'+user?.id }), DriveCurrentFolderAtom({ initialFolderId: initialParentId || 'user_'+user?.id }),
); );
console.log("Upload Zone:: " + parentId);
return ( return (
<> <>
<UploadZone <UploadZone
@@ -81,12 +80,14 @@ export const CreateModalWithUploadZones = ({ initialParentId }: { initialParentI
export default () => { export default () => {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { viewId } = RouterServices.getStateFromRoute();
const [parentId, _] = useRecoilState(DriveCurrentFolderAtom({ initialFolderId: 'user_'+user?.id })); const [parentId, _] = useRecoilState(DriveCurrentFolderAtom({ initialFolderId: 'user_'+user?.id }));
const { access, item, inTrash } = useDriveItem(parentId); const { access, item } = useDriveItem(parentId);
const { children: trashChildren } = useDriveItem('trash'); const { children: trashChildren } = useDriveItem(viewId === 'trash' ? 'trash' : 'trash_'+user?.id);
const uploadZoneRef = useRef<UploadZone | null>(null); const uploadZoneRef = useRef<UploadZone | null>(null);
const { uploadTree } = useDriveUpload(); const { uploadTree } = useDriveUpload();
const companyId = useRouterCompany(); const companyId = useRouterCompany();
const inTrash = viewId?.includes("trash") || false;
const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom); const setConfirmDeleteModalState = useSetRecoilState(ConfirmDeleteModalAtom);
const setCreationModalState = useSetRecoilState(CreateModalAtom); const setCreationModalState = useSetRecoilState(CreateModalAtom);
@@ -101,7 +102,7 @@ export default () => {
<div className="p-4"> <div className="p-4">
<CreateModalWithUploadZones initialParentId={parentId} /> <CreateModalWithUploadZones initialParentId={parentId} />
{inTrash && access === 'manage' && ( {inTrash && (
<> <>
<Button <Button
onClick={() => onClick={() =>
@@ -113,6 +114,7 @@ export default () => {
size="lg" size="lg"
theme="danger" theme="danger"
className="w-full mb-2 justify-center" className="w-full mb-2 justify-center"
disabled={!(trashChildren.length > 0)}
> >
<TruckIcon className="w-5 h-5 mr-2" /> { Languages.t('components.side_menu.buttons.empty_trash') } <TruckIcon className="w-5 h-5 mr-2" /> { Languages.t('components.side_menu.buttons.empty_trash') }
</Button> </Button>
@@ -11,7 +11,6 @@ import {
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import { useEffect } from 'react'; import { useEffect } from 'react';
import useRouterCompany from '@features/router/hooks/use-router-company'; import useRouterCompany from '@features/router/hooks/use-router-company';
import useRouterWorkspace from '@features/router/hooks/use-router-workspace';
import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { Title } from '../../../atoms/text'; import { Title } from '../../../atoms/text';
@@ -21,17 +20,15 @@ import Account from '../common/account';
import AppGrid from '../common/app-grid'; import AppGrid from '../common/app-grid';
import DiskUsage from '../common/disk-usage'; import DiskUsage from '../common/disk-usage';
import Actions from './actions'; import Actions from './actions';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import RouterServices from '@features/router/services/router-service'; import RouterServices from '@features/router/services/router-service';
import Languages from "features/global/services/languages-service"; import Languages from "features/global/services/languages-service";
export default () => { export default () => {
const history = useHistory(); const history = useHistory();
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const location = useLocation();
const company = useRouterCompany(); const company = useRouterCompany();
const workspace = useRouterWorkspace(); const { viewId, itemId, dirId } = RouterServices.getStateFromRoute();
const { viewId, itemId } = RouterServices.getStateFromRoute();
const [parentId, setParentId] = useRecoilState( const [parentId, setParentId] = useRecoilState(
DriveCurrentFolderAtom({ initialFolderId: viewId || 'user_'+user?.id }), DriveCurrentFolderAtom({ initialFolderId: viewId || 'user_'+user?.id }),
); );
@@ -46,7 +43,8 @@ export default () => {
useEffect(() => { useEffect(() => {
!itemId && viewId && setParentId(viewId); !itemId && !dirId && viewId && setParentId(viewId);
dirId && viewId && setParentId(dirId);
}, [viewId, itemId]); }, [viewId, itemId]);
return ( return (
<div className="grow flex flex-col overflow-auto -m-4 p-4 relative"> <div className="grow flex flex-col overflow-auto -m-4 p-4 relative">
@@ -68,15 +66,15 @@ export default () => {
<div className="mt-4" /> <div className="mt-4" />
<Title>Drive</Title> <Title>Drive</Title>
<Button <Button
onClick={() => {history.push(RouterServices.generateRouteFromState({companyId: company, viewId: 'user_' + user?.id})); setParentId('user_' + user?.id)}} onClick={() => {history.push(RouterServices.generateRouteFromState({companyId: company, viewId: 'user_' + user?.id, itemId: '', dirId: ''})); setParentId('user_' + user?.id)}}
size="lg" size="lg"
theme="white" theme="white"
className={'w-full mb-1 ' + (folderType === 'personal' && (viewId == '' || 'user_' + user?.id) ? activeClass : '')} className={'w-full mb-1 ' + (folderType === 'personal' && (viewId == '' || viewId == 'user_' + user?.id) ? activeClass : '')}
> >
<UserIcon className="w-5 h-5 mr-4" /> {Languages.t('components.side_menu.my_drive')} <UserIcon className="w-5 h-5 mr-4" /> {Languages.t('components.side_menu.my_drive')}
</Button> </Button>
<Button <Button
onClick={() => {history.push(RouterServices.generateRouteFromState({companyId: company, viewId: "root"})); setParentId('root')}} onClick={() => {history.push(RouterServices.generateRouteFromState({companyId: company, viewId: "root", itemId: '', dirId: ''})); setParentId('root')}}
size="lg" size="lg"
theme="white" theme="white"
className={'w-full mb-1 ' + (folderType === 'home' && viewId == 'root' ? activeClass : '')} className={'w-full mb-1 ' + (folderType === 'home' && viewId == 'root' ? activeClass : '')}
@@ -84,7 +82,7 @@ export default () => {
<CloudIcon className="w-5 h-5 mr-4" /> {Languages.t('components.side_menu.home')} <CloudIcon className="w-5 h-5 mr-4" /> {Languages.t('components.side_menu.home')}
</Button> </Button>
<Button <Button
onClick={() => {history.push(RouterServices.generateRouteFromState({companyId: company, viewId: "shared_with_me"})); setParentId('shared_with_me')}} onClick={() => {history.push(RouterServices.generateRouteFromState({companyId: company, viewId: "shared_with_me", itemId: '', dirId: ''})); setParentId('shared_with_me')}}
size="lg" size="lg"
theme="white" theme="white"
className={'w-full mb-1 ' + (folderType === 'shared' && viewId == 'shared_with_me'? activeClass : '')} className={'w-full mb-1 ' + (folderType === 'shared' && viewId == 'shared_with_me'? activeClass : '')}
@@ -109,16 +107,14 @@ export default () => {
</Button> </Button>
</> </>
)} )}
{rootAccess === 'manage' && (
<Button <Button
onClick={() =>{history.push(RouterServices.generateRouteFromState({companyId: company, viewId: ""}));setParentId('trash')}} onClick={() =>{history.push(RouterServices.generateRouteFromState({companyId: company, viewId: 'trash_'+user?.id, itemId: '', dirId: ''}));setParentId('trash_'+user?.id)}}
size="lg" size="lg"
theme="white" theme="white"
className={'w-full mb-1 ' + (folderType === 'trash' && viewId == ''? activeClass : '')} className={'w-full mb-1 ' + (viewId?.includes("trash")? activeClass : '')}
> >
<TrashIcon className="w-5 h-5 mr-4 text-rose-500" /> {Languages.t('components.side_menu.trash')} <TrashIcon className="w-5 h-5 mr-4 text-rose-500" /> {Languages.t('components.side_menu.trash')}
</Button> </Button>
)}
{false && ( {false && (
<> <>
@@ -83,7 +83,7 @@ export const DrivePreview: React.FC<DrivePreviewProps> = ({ items }) => {
if (device != 'ios' && device != 'android') { if (device != 'ios' && device != 'android') {
close(); close();
history.push( history.push(
RouterServices.generateRouteFromState({ companyId: company, viewId: '', itemId: item.id }), RouterServices.generateRouteFromState({ companyId: company, viewId: '', itemId: item.id, dirId: '' }),
); );
open(item); open(item);
} }