diff --git a/Documentation/docs/plugins.md b/Documentation/docs/plugins.md
index 581acaabe..78754b12a 100644
--- a/Documentation/docs/plugins.md
+++ b/Documentation/docs/plugins.md
@@ -145,6 +145,15 @@ ended with and without a new version.
See operations `beginEditing` and `updateEditing` in `tdrive/backend/node/src/services/documents/services/index.ts` for operations available on that key.
+Basic flow is:
+ - Call `beginEditing`
+ - If a key previously was set for that file, uses the `/check` endpoint of the plugin to update the key status first
+ - Twake Drive generates an editing session key and returns it (or returns the existing one).
+ - Optionally call `updateEditing?keepEditing=true` with a file stream to generate intermediary versions
+ - End the session by calling `updateEditing` (without `keepEditing=true`). If a file is provided, a new (and final) FileVersion
+ is created with that content. Otherwise, the editing session is cleared without creating a new version.
+
+
#### API to expose by the application
Authentication from the Twake Drive backend is a JWT with the property `type` being
@@ -161,7 +170,81 @@ multiple parallel requests for the same key gracefully.
- `{ status: 'unknown' }`: the key isn't known and maybe used for a new session
- `{ status: 'updated' }`: the key needed updating but is now invalid
- `{ status: 'expired' }`: the key was already used in a finished session and can't be used again
- - `{ status: 'live' }`: the key is valid and current and should be used again for the same file
+ - `{ status: 'live' }`: the key is valid and current and should be used again for the same file (if multiple writers are allowed)
+
+#### Example flow of editing session
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor User as User/Browser
+ participant Front as Frontend
+ box Server side
+ participant Back as Backend
+ participant Conn as onlyoffice
connector
+ participant OO as Only Office
Edition Server
+ end
+
+ User->>Front: Open in
editor
+ Front->>Back: Get plugin config
+ Front->>User: Open plugin config
editor.edition_url
+
+ User->>Conn: Open editor_url
(proxied by backend)
+ Conn->>Back: beginEditing
+ alt existing key
+ Back->>Conn: checkSessionStatus
+ Conn->>OO: getForgotten
+ note over Conn, OO: recover forgotten
process, and new key
+ Conn->>OO: info command
+ note right of Conn: decide status of key
live or stale
+ note over Conn, OO: detect ended but
not changed keys
+ note over Conn, OO: normal callback processing with
update if required
+ OO->>Conn: callback with key status
+ Conn->>Back: key status
(live/expired/updated/etc)
+ end
+ activate Back
+ Back->>Conn: editing_session_key
+ Conn->>User: HTML host for Editor with
special callback URL
+ User->>OO: Load JS Editor directly from OO server
+ activate User
+ loop User editing
+ User->>User: Furious Document
Editing
+ User-->>OO: Periodic saving
+ end
+ deactivate User
+ note left of User: Closes editor
+ note over User,OO: 10 seconds after last user closes their editor
+ OO->>Conn: callback to save the new version
or close without changes
+ Conn->>Back: updateEditing?keepEditing=false
with URL to new version from OO
+ deactivate Back
+```
+
+#### Batch processing of unknown keys
+
+Periodically, the plugin, and twake drive, should run batch cleanup operations on editing session keys
+to ensure they are live, or removed, as they may block other operations until then.
+
+Here is an example initiated by the plugin:
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor User as User/Browser
+ participant Front as Frontend
+ box Server side
+ participant Back as Backend
+ participant Conn as onlyoffice
connector
+ participant OO as Only Office
Edition Server
+ end
+
+ alt Periodic scheduled task
+ Conn->>OO: getForgottenList
+ loop Each forgotten file
+ Conn->>Back: Save new version
end editing session
+ Conn->>OO: Delete forgotten file
+ end
+ end
+```
### Example: OnlyOffice plugin
diff --git a/tdrive/backend/node/src/cli/cmds/editing_session_cmds/parse.ts b/tdrive/backend/node/src/cli/cmds/editing_session_cmds/parse.ts
new file mode 100644
index 000000000..5329a13a3
--- /dev/null
+++ b/tdrive/backend/node/src/cli/cmds/editing_session_cmds/parse.ts
@@ -0,0 +1,34 @@
+import yargs from "yargs";
+
+import { NonPlatformCommandYargsBuilder } from "../../utils/non-plaform-command-yargs-builder";
+import { EditingSessionKeyFormat } from "../../../services/documents/entities/drive-file";
+
+interface ParseArguments {
+ editing_session_key: string;
+}
+
+const command: yargs.CommandModule = {
+ command: "parse ",
+ describe: `
+ Parse the provided editing_session_key and output json data (to stderr)
+ `.trim(),
+
+ builder: {
+ ...NonPlatformCommandYargsBuilder,
+ },
+ handler: async argv => {
+ const args = argv as unknown as ParseArguments;
+ const parsed = EditingSessionKeyFormat.parse(decodeURIComponent("" + args.editing_session_key));
+ console.error(
+ JSON.stringify(
+ {
+ ageH: (new Date().getTime() - parsed.timestamp.getTime()) / (60 * 60 * 1000),
+ ...parsed,
+ },
+ null,
+ 2,
+ ),
+ );
+ },
+};
+export default command;
diff --git a/tdrive/connectors/onlyoffice-connector/src/controllers/browser-editor.controller.ts b/tdrive/connectors/onlyoffice-connector/src/controllers/browser-editor.controller.ts
index 7b9126123..d2c82065d 100644
--- a/tdrive/connectors/onlyoffice-connector/src/controllers/browser-editor.controller.ts
+++ b/tdrive/connectors/onlyoffice-connector/src/controllers/browser-editor.controller.ts
@@ -22,6 +22,7 @@ interface RequestEditorQuery {
office_token: string;
company_id: string;
file_id: string;
+ drive_file_id: string;
}
/**
@@ -102,6 +103,7 @@ class BrowserEditorController {
makeURLTo.editorAbsolute({
token,
file_id,
+ drive_file_id,
editing_session_key: editingSessionKey,
company_id,
preview,
@@ -131,7 +133,7 @@ class BrowserEditorController {
throw new Error('Cant start editing without "editing session key"');
}
- const initResponse = await editorService.init(company_id, file_name, file_id, user, preview, drive_file_id || file_id);
+ const initResponse = await editorService.init(company_id, file_name, file_id, user, preview, drive_file_id);
const inPageToken = jwt.sign(
{
diff --git a/tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts b/tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts
index a5186f888..3a7ad59c2 100644
--- a/tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts
+++ b/tdrive/connectors/onlyoffice-connector/src/controllers/onlyoffice.controller.ts
@@ -11,9 +11,14 @@ import * as Utils from '@/utils';
interface RequestQuery {
company_id: string;
file_id: string;
+ drive_file_id: string;
token: string;
}
+interface RenameRequestBody {
+ name: string;
+}
+
/** These expose a OnlyOffice document storage service methods, called by the OnlyOffice document editing service
* to load and save files
*/
@@ -29,15 +34,27 @@ class OnlyOfficeController {
const { token } = req.query;
const officeTokenPayload = jwt.verify(token, CREDENTIALS_SECRET) as OfficeToken;
- const { company_id, file_id, in_page_token } = officeTokenPayload;
+ const { company_id, file_id, drive_file_id, in_page_token } = officeTokenPayload;
// check token is an in_page_token
if (!in_page_token) throw new Error('Invalid token, must be a in_page_token');
+ let fileId = file_id;
+ if (drive_file_id) {
+ //Get the drive file
+ const driveFile = await driveService.get({
+ company_id,
+ drive_file_id,
+ });
+ if (driveFile) {
+ fileId = driveFile?.item?.last_version_cache?.file_metadata?.external_id;
+ }
+ }
+
if (!file_id) throw new Error(`File id is missing in the last version cache for ${JSON.stringify(file_id)}`);
const file = await fileService.download({
company_id,
- file_id: file_id,
+ file_id: fileId,
});
file.pipe(res);
@@ -132,6 +149,25 @@ class OnlyOfficeController {
next(error || 'error');
}
};
+
+ /** This route is called directly by the inline JS in the editor page, called by the client-side OO editor component */
+ public rename = async (req: Request<{}, {}, RenameRequestBody, RequestQuery>, res: Response, next: NextFunction): Promise => {
+ try {
+ const { token } = req.query;
+ const officeTokenPayload = jwt.verify(token, CREDENTIALS_SECRET) as OfficeToken;
+ const { company_id, drive_file_id } = officeTokenPayload;
+ const { name } = req.body;
+
+ if (!drive_file_id) throw new Error('OO Rename request missing drive_file_id');
+ if (!name) throw new Error('OO Rename request missing name');
+
+ const result = await driveService.update({ company_id, drive_file_id, changes: { name } });
+ res.send(result);
+ } catch (error) {
+ logger.error(`OO Rename request root error`, { error });
+ next(error || 'error');
+ }
+ };
}
export default OnlyOfficeController;
diff --git a/tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts b/tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts
index 7aef5de3a..c23aedb4b 100644
--- a/tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts
+++ b/tdrive/connectors/onlyoffice-connector/src/interfaces/editor.interface.ts
@@ -17,7 +17,7 @@ export type EditConfigInitResult = {
onlyoffice_server: string;
color: string;
company_id: string;
- file_id: string;
+ drive_file_id: string;
file_version_id: string;
filename: string;
file_type: string;
@@ -33,6 +33,7 @@ export interface IEditorService {
user: UserType,
preview: boolean,
file_id: string,
+ drive_file_id: string,
) => Promise;
}
diff --git a/tdrive/connectors/onlyoffice-connector/src/routes/index.ts b/tdrive/connectors/onlyoffice-connector/src/routes/index.ts
index 930c6e120..c930d71aa 100644
--- a/tdrive/connectors/onlyoffice-connector/src/routes/index.ts
+++ b/tdrive/connectors/onlyoffice-connector/src/routes/index.ts
@@ -41,7 +41,15 @@ export function mountRoutes(app: Application) {
export const makeURLTo = {
rootAbsolute: () => Utils.joinURL([SERVER_ORIGIN, SERVER_PREFIX]),
assets: () => Utils.joinURL([SERVER_PREFIX, 'assets']),
- editorAbsolute(params: { token: string; file_id: string; editing_session_key: string; company_id: string; preview: string; office_token: string }) {
+ editorAbsolute(params: {
+ token: string;
+ drive_file_id: string;
+ file_id: string;
+ editing_session_key: string;
+ company_id: string;
+ preview: string;
+ office_token: string;
+ }) {
return Utils.joinURL([SERVER_ORIGIN ?? '', SERVER_PREFIX, 'editor'], params);
},
};
diff --git a/tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts b/tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts
index 5faa09c75..921157cd0 100644
--- a/tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts
+++ b/tdrive/connectors/onlyoffice-connector/src/routes/onlyoffice.route.ts
@@ -11,5 +11,6 @@ export const OnlyOfficeRoutes = {
const controller = new OnlyOfficeController();
router.get(`/:mode/read`, requirementsMiddleware, controller.read.bind(controller));
router.post(`/:mode/callback`, requirementsMiddleware, controller.ooCallback.bind(controller));
+ router.post(`/:mode/rename`, requirementsMiddleware, controller.rename.bind(controller));
},
};
diff --git a/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts
index 2199bb661..ebb2bf658 100644
--- a/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts
+++ b/tdrive/connectors/onlyoffice-connector/src/services/drive.service.ts
@@ -36,6 +36,28 @@ class DriveService implements IDriveService {
}
};
+ public update = async (params: {
+ company_id: string;
+ drive_file_id: string;
+ changes: Partial;
+ completeResult?: boolean;
+ }): Promise<(typeof params)['changes']> => {
+ try {
+ const { company_id, drive_file_id } = params;
+ const resource = await apiService.post<(typeof params)['changes'], ReturnType>({
+ url: makeNonEditingSessionItemUrl(company_id, drive_file_id),
+ payload: params.changes,
+ });
+ if (params.completeResult) return resource;
+ const result = {};
+ Object.keys(params.changes).forEach(k => (result[k] = resource[k]));
+ return result;
+ } catch (error) {
+ logger.error('Failed to update file metadata: ', error.stack);
+ return Promise.reject();
+ }
+ };
+
public createVersion = async (params: {
company_id: string;
drive_file_id: string;
diff --git a/tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts
index 5cd82f719..12794a0a1 100644
--- a/tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts
+++ b/tdrive/connectors/onlyoffice-connector/src/services/editor.service.ts
@@ -10,7 +10,7 @@ class EditorService implements IEditorService {
file_version_id: string,
user: UserType,
preview: boolean,
- file_id: string,
+ drive_file_id: string,
): Promise => {
const { color, mode: fileMode } = this.getFileMode(file_name);
let [, extension] = Utils.splitFilename(file_name);
@@ -18,7 +18,7 @@ class EditorService implements IEditorService {
extension = extension.toLocaleLowerCase();
return {
color,
- file_id,
+ drive_file_id,
file_version_id,
file_type: extension,
filename: file_name,
diff --git a/tdrive/connectors/onlyoffice-connector/src/views/index.eta b/tdrive/connectors/onlyoffice-connector/src/views/index.eta
index 6d9b9ec0d..fa1c3fd43 100644
--- a/tdrive/connectors/onlyoffice-connector/src/views/index.eta
+++ b/tdrive/connectors/onlyoffice-connector/src/views/index.eta
@@ -5,6 +5,7 @@
+ Twake Drive
@@ -22,18 +23,24 @@
$('#onlyoffice_container').html("");
+ const callbackQueryString = '?drive_file_id=<%= it.drive_file_id %>&company_id=<%= it.company_id %>&token=<%= it.token %>';
let doc = {
title: "<%= it.filename %>",
- url: `${window.baseURL}read?file_id=<%= it.file_id %>&company_id=<%= it.company_id %>&token=<%= it.token %>`,
+ url: `${window.baseURL}read${callbackQueryString}`,
fileType: "<%= it.file_type %>",
key: "<%= it.docId %>",
- token: "<%= it.file_id %>",
+ token: "<%= it.drive_file_id %>",
permissions: {
download: true,
edit: <%= it.editable %>,
preview: <%= it.preview %>,
}
}
+ function updateTitle(title) {
+ const el = document.querySelector('head title');
+ if (el) el.innerText = title + ' — Twake Drive';
+ }
+ updateTitle(doc.title);
window.docEditor = new DocsAPI.DocEditor('onlyoffice_container_instance', {
scrollSensitivity: window.mode === 'text' ? 100 : 40,
@@ -41,10 +48,28 @@
height: '100%',
documentType: window.mode,
document: doc,
- token: "<%= it.file_id %>",
+ token: "<%= it.drive_file_id %>",
type: screen.width < 600 ? 'mobile' : 'desktop',
+ events: {
+ onRequestRename: function (event) {
+ let name = event.data;
+ const prevExtension = /\.[^.]+$/.exec(doc.title);
+ if (prevExtension) name += prevExtension[0];
+ $.
+ post(`${window.baseURL}rename${callbackQueryString}`, { name }).
+ done((changed) => {
+ if (changed.name != name)
+ window.docEditor.showMessage(`✅ Renamed to: ${changed.name}`);
+ }).
+ fail(() => window.docEditor.showMessage('🚨 Error renaming the file'));
+ },
+ onMetaChange: function (event) {
+ const { title } = event.data;
+ if (title) updateTitle(title);
+ },
+ },
editorConfig: {
- callbackUrl: `${window.baseURL}callback?file_id=<%= it.file_id %>&company_id=<%= it.company_id %>&token=<%= it.token %>`,
+ callbackUrl: `${window.baseURL}callback${callbackQueryString}`,
lang: window.user.language,
user: {
id: window.user.id,
diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json
index 0dd0aec96..fafac203d 100644
--- a/tdrive/frontend/public/locales/en.json
+++ b/tdrive/frontend/public/locales/en.json
@@ -20,10 +20,6 @@
"compenents.ConfirmTrashModalContent_move_to_trash": "Move to trash",
"compenents.ConfirmTrashModalContent_move_to_trash_desc": "Click 'Move to trash' to move the selected items to the trash folder. You can restore them later from the trash.",
"compenents.ConfirmTrashModalContent_to_trash": "to trash",
- "compenents.ProprietiesModalContent_name": "Name",
- "compenents.ProprietiesModalContent_place_holder": "Document or folder name",
- "compenents.ProprietiesModalContent_rename": "Rename",
- "compenents.ProprietiesModalContent_update_button": "Update name",
"compenents.VersionModalContent_create": "Create version",
"compenents.VersionModalContent_donwload": "Download",
"compenents.VersionModalContent_version": "Versions of",
@@ -100,7 +96,6 @@
"components.item_context_menu.to_trash_multiple": "Move selected items to trash",
"components.item_context_menu.today": "Today",
"components.item_context_menu.trash.empty": "Empty trash",
- "components.item_context_menu.trash.exit": "Exit trash",
"components.item_context_menu.versions": "Versions",
"components.locked_features.locked_drive_popup.subtitle": "In the free version, you can store only 6GB.",
"components.locked_features.locked_drive_popup.title": "You have just reached the drive limit",
@@ -127,6 +122,10 @@
"components.open_desktop_popup.subtitle": "Opened in Tdrive app",
"components.pending_file_list.estimation.approximations": "Waiting for time approximations...",
"components.pending_file_list.estimation.end": "Will end",
+ "components.PropertiesModalContent_name": "Name",
+ "components.PropertiesModalContent_place_holder": "Document or folder name",
+ "components.PropertiesModalContent_rename": "Rename",
+ "components.PropertiesModalContent_update_button": "Update name",
"components.public-link-access-level-create": "Anyone with the link will have access to",
"components.public-link-access-level-delete": "Delete link",
"components.public-link-access-level-update-subtitle": "Anyone with the link has access to",
diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json
index 11adc2244..e2f320393 100644
--- a/tdrive/frontend/public/locales/fr.json
+++ b/tdrive/frontend/public/locales/fr.json
@@ -218,10 +218,10 @@
"components.SelectorModalContent_no_items": "Pas de fichier sélectionné",
"components.SelectorModalContent_select": "sélectionné(s)",
"components.SelectorModalContent_files": "fichier(s)",
- "compenents.ProprietiesModalContent_rename": "Renommer",
- "compenents.ProprietiesModalContent_name": "Nom",
- "compenents.ProprietiesModalContent_place_holder": "Nom du fichier ou du document",
- "compenents.ProprietiesModalContent_update_button": "Renommer",
+ "components.PropertiesModalContent_rename": "Renommer",
+ "components.PropertiesModalContent_name": "Nom",
+ "components.PropertiesModalContent_place_holder": "Nom du fichier ou du document",
+ "components.PropertiesModalContent_update_button": "Renommer",
"compenents.ConfirmTrashModalContent_move": "Déplacer",
"compenents.ConfirmTrashModalContent_to_trash": "vers la corbeille",
"compenents.ConfirmTrashModalContent_items_to_trash": "éléments vers la corbeille",
@@ -337,7 +337,6 @@
"components.item_context_menu.clear_selection": "Annuler la sélection",
"components.item_context_menu.delete_multiple": "Supprimer",
"components.item_context_menu.to_trash_multiple": "Supprimer",
- "components.item_context_menu.trash.exit": "Quitter la corbeille",
"components.item_context_menu.trash.empty": "Vider la corbeille",
"components.item_context_menu.add_documents": "Ajouter un document ou un dossier",
"components.item_context_menu.download_folder": "Télécharger le dossier",
diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json
index f855dfc11..3098a8f98 100644
--- a/tdrive/frontend/public/locales/ru.json
+++ b/tdrive/frontend/public/locales/ru.json
@@ -20,10 +20,6 @@
"compenents.ConfirmTrashModalContent_move_to_trash": "Удалить",
"compenents.ConfirmTrashModalContent_move_to_trash_desc": "Нажмите 'Удалить' чтобы переместить выделенные элементы в корзину. Вы сможете восстановить их позже.",
"compenents.ConfirmTrashModalContent_to_trash": "в корзину",
- "compenents.ProprietiesModalContent_name": "Имя",
- "compenents.ProprietiesModalContent_place_holder": "Имя папки или документа",
- "compenents.ProprietiesModalContent_rename": "Переименовать",
- "compenents.ProprietiesModalContent_update_button": "Переименовать",
"compenents.VersionModalContent_create": "Создать версию",
"compenents.VersionModalContent_donwload": "Скачать",
"compenents.VersionModalContent_version": "Версии ...",
@@ -100,7 +96,6 @@
"components.item_context_menu.to_trash_multiple": "Удалить",
"components.item_context_menu.today": "Сегодня",
"components.item_context_menu.trash.empty": "Очистить корзину",
- "components.item_context_menu.trash.exit": "Выйти из корзины",
"components.item_context_menu.versions": "История изменений",
"components.locked_features.locked_drive_popup.subtitle": "В бесплатной версии можно хранить только 6 ГБ.",
"components.locked_features.locked_drive_popup.title": "Вы не можете загружать больше файлов",
@@ -127,6 +122,10 @@
"components.open_desktop_popup.subtitle": "открыто в приложении Tdrive",
"components.pending_file_list.estimation.approximations": "Примерное время ожидания...",
"components.pending_file_list.estimation.end": "Закончится через",
+ "components.PropertiesModalContent_name": "Имя",
+ "components.PropertiesModalContent_place_holder": "Имя папки или документа",
+ "components.PropertiesModalContent_rename": "Переименовать",
+ "components.PropertiesModalContent_update_button": "Переименовать",
"components.public-link-access-level-create": "Все кто имеет ссылку буду иметь доступ",
"components.public-link-access-level-delete": "Удалить ссылку",
"components.public-link-access-level-update-subtitle": "У каждого, кто имеет ссылку, есть доступ к",
diff --git a/tdrive/frontend/public/locales/vn.json b/tdrive/frontend/public/locales/vn.json
index b40aa0720..71528743e 100644
--- a/tdrive/frontend/public/locales/vn.json
+++ b/tdrive/frontend/public/locales/vn.json
@@ -233,10 +233,10 @@
"components.SelectorModalContent_no_items": "Không có mục nào được chọn",
"components.SelectorModalContent_select": "Đã chọn",
"components.SelectorModalContent_files": "tệp",
- "compenents.ProprietiesModalContent_rename": "Đổi tên",
- "compenents.ProprietiesModalContent_name": "Tên",
- "compenents.ProprietiesModalContent_place_holder": "Tên tài liệu hoặc thư mục",
- "compenents.ProprietiesModalContent_update_button": "Cập nhật tên",
+ "components.PropertiesModalContent_rename": "Đổi tên",
+ "components.PropertiesModalContent_name": "Tên",
+ "components.PropertiesModalContent_place_holder": "Tên tài liệu hoặc thư mục",
+ "components.PropertiesModalContent_update_button": "Cập nhật tên",
"compenents.ConfirmTrashModalContent_move": "Di chuyển",
"compenents.ConfirmTrashModalContent_to_trash": "vào thùng rác",
"compenents.ConfirmTrashModalContent_items_to_trash": "mục vào thùng rác",
@@ -326,7 +326,6 @@
"components.item_context_menu.clear_selection": "Xóa lựa chọn",
"components.item_context_menu.delete_multiple": "Xóa",
"components.item_context_menu.to_trash_multiple": "Di chuyển các mục đã chọn vào thùng rác",
- "components.item_context_menu.trash.exit": "Thoát thùng rác",
"components.item_context_menu.trash.empty": "Làm trống thùng rác",
"components.item_context_menu.add_documents": "Thêm tài liệu hoặc thư mục",
"components.item_context_menu.download_folder": "Tải xuống thư mục",
diff --git a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx
index 1880d56a5..a909022b2 100644
--- a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx
+++ b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx
@@ -268,12 +268,6 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
//Add parent related menus
const newMenuActions: any[] = inTrash
? [
- {
- type: 'menu',
- text: Languages.t('components.item_context_menu.trash.exit'),
- onClick: () => setParentId('root'),
- },
- { type: 'separator' },
{
type: 'menu',
text: Languages.t('components.item_context_menu.trash.empty'),
diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/properties/index.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/properties/index.tsx
index e3c75e8b6..3272ef9f6 100644
--- a/tdrive/frontend/src/app/views/client/body/drive/modals/properties/index.tsx
+++ b/tdrive/frontend/src/app/views/client/body/drive/modals/properties/index.tsx
@@ -4,7 +4,7 @@ import { Input } from '@atoms/input/input-text';
import { Modal, ModalContent } from '@atoms/modal';
import { useDriveActions } from '@features/drive/hooks/use-drive-actions';
import { useDriveItem } from '@features/drive/hooks/use-drive-item';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { atom, useRecoilState } from 'recoil';
import Languages from '@features/global/services/languages-service';
@@ -38,27 +38,67 @@ const PropertiesModalContent = ({ id, onClose }: { id: string; onClose: () => vo
const { update } = useDriveActions();
const [loading, setLoading] = useState(false);
const [name, setName] = useState('');
+ const inputRef = useRef(null);
useEffect(() => {
refresh(id);
}, []);
+ useEffect(() => {
+ setTimeout(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ const lastDot = inputRef.current.value.lastIndexOf('.');
+ const endRange = lastDot >= 0 ? lastDot : inputRef.current.value.length;
+ inputRef.current.setSelectionRange(0, endRange);
+ }
+ }, 100);
+ }, []);
+
useEffect(() => {
if (!name) setName(item?.name || '');
}, [item?.name]);
+ const doSave = async () => {
+ setLoading(true);
+ if (item) {
+ let finalName = name;
+ //TODO: Confirm rename if extension changed ?
+ if (!item?.is_directory) {
+ //TODO: Why do we trim extensions on folders ?
+ const lastDotIndex = finalName.lastIndexOf('.');
+ if (lastDotIndex !== -1) {
+ const fileExtension = name.slice(lastDotIndex);
+ finalName = finalName.slice(0, lastDotIndex) + fileExtension;
+ }
+ }
+ await update({ name: finalName }, id, item.parent_id);
+ }
+ onClose();
+ setLoading(false);
+ }
+
return (
setName(e.target.value)}
- placeholder={Languages.t('compenents.ProprietiesModalContent_place_holder')}
+ onKeyUp={({ key }) => {
+ if (!loading) {
+ if (key === 'Enter')
+ doSave();
+ else if (key === "Escape")
+ onClose();
+ }
+ }}
+ placeholder={Languages.t('components.PropertiesModalContent_place_holder')}
/>
}
/>
@@ -68,24 +108,9 @@ const PropertiesModalContent = ({ id, onClose }: { id: string; onClose: () => vo
className="float-right mt-4"
theme="primary"
loading={loading}
- onClick={async () => {
- setLoading(true);
- if (item) {
- let finalName = name;
- if (!item?.is_directory) {
- const lastDotIndex = finalName.lastIndexOf('.');
- if (lastDotIndex !== -1) {
- const fileExtension = name.slice(lastDotIndex);
- finalName = finalName.slice(0, lastDotIndex) + fileExtension;
- }
- }
- await update({ name: finalName }, id, item.parent_id);
- }
- onClose();
- setLoading(false);
- }}
+ onClick={doSave}
>
- {Languages.t('compenents.ProprietiesModalContent_update_button')}
+ {Languages.t('components.PropertiesModalContent_update_button')}
);