🔀 Merge branch '525-515-548-523-onlyoffice-rework' into release/v1.0.4-rc2
This commit is contained in:
@@ -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<br>connector
|
||||
participant OO as Only Office<br>Edition Server
|
||||
end
|
||||
|
||||
User->>Front: Open in<br>editor
|
||||
Front->>Back: Get plugin config
|
||||
Front->>User: Open plugin config<br>editor.edition_url
|
||||
|
||||
User->>Conn: Open editor_url<br>(proxied by backend)
|
||||
Conn->>Back: beginEditing
|
||||
alt existing key
|
||||
Back->>Conn: checkSessionStatus
|
||||
Conn->>OO: getForgotten
|
||||
note over Conn, OO: recover forgotten<br>process, and new key
|
||||
Conn->>OO: info command
|
||||
note right of Conn: decide status of key<br>live or stale
|
||||
note over Conn, OO: detect ended but<br>not changed keys
|
||||
note over Conn, OO: normal callback processing with<br>update if required
|
||||
OO->>Conn: callback with key status
|
||||
Conn->>Back: key status<br>(live/expired/updated/etc)
|
||||
end
|
||||
activate Back
|
||||
Back->>Conn: editing_session_key
|
||||
Conn->>User: HTML host for Editor with<br>special callback URL
|
||||
User->>OO: Load JS Editor directly from OO server
|
||||
activate User
|
||||
loop User editing
|
||||
User->>User: Furious Document<br>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<br>or close without changes
|
||||
Conn->>Back: updateEditing?keepEditing=false<br>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<br>connector
|
||||
participant OO as Only Office<br>Edition Server
|
||||
end
|
||||
|
||||
alt Periodic scheduled task
|
||||
Conn->>OO: getForgottenList
|
||||
loop Each forgotten file
|
||||
Conn->>Back: Save new version<br>end editing session
|
||||
Conn->>OO: Delete forgotten file
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Example: OnlyOffice plugin
|
||||
|
||||
|
||||
@@ -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<unknown, unknown> = {
|
||||
command: "parse <editing_session_key>",
|
||||
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;
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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<void> => {
|
||||
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;
|
||||
|
||||
@@ -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<EditConfigInitResult>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,6 +36,28 @@ class DriveService implements IDriveService {
|
||||
}
|
||||
};
|
||||
|
||||
public update = async (params: {
|
||||
company_id: string;
|
||||
drive_file_id: string;
|
||||
changes: Partial<DriveFileType['item']>;
|
||||
completeResult?: boolean;
|
||||
}): Promise<(typeof params)['changes']> => {
|
||||
try {
|
||||
const { company_id, drive_file_id } = params;
|
||||
const resource = await apiService.post<(typeof params)['changes'], ReturnType<DriveService['update']>>({
|
||||
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;
|
||||
|
||||
@@ -10,7 +10,7 @@ class EditorService implements IEditorService {
|
||||
file_version_id: string,
|
||||
user: UserType,
|
||||
preview: boolean,
|
||||
file_id: string,
|
||||
drive_file_id: string,
|
||||
): Promise<EditConfigInitResult> => {
|
||||
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,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="<%= it.onlyoffice_server %>web-apps/apps/api/documents/api.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no,shrink-to-fit=no" />
|
||||
<title>Twake Drive</title>
|
||||
</head>
|
||||
|
||||
<body style="height: 100%">
|
||||
@@ -22,18 +23,24 @@
|
||||
|
||||
$('#onlyoffice_container').html("<div id='onlyoffice_container_instance'></div>");
|
||||
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "У каждого, кто имеет ссылку, есть доступ к",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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<HTMLInputElement>(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 (
|
||||
<ModalContent
|
||||
title={Languages.t('compenents.ProprietiesModalContent_rename') + ' ' + item?.name}
|
||||
title={Languages.t('components.PropertiesModalContent_rename') + ' ' + item?.name}
|
||||
>
|
||||
<InputLabel
|
||||
className="mt-4"
|
||||
label={Languages.t('compenents.ProprietiesModalContent_name')}
|
||||
label={Languages.t('components.PropertiesModalContent_name')}
|
||||
input={
|
||||
<Input
|
||||
value={name}
|
||||
inputRef={inputRef}
|
||||
onChange={e => 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')}
|
||||
</Button>
|
||||
</ModalContent>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user