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')} );