diff options
Diffstat (limited to 'apps/files/src/store/renaming.ts')
-rw-r--r-- | apps/files/src/store/renaming.ts | 317 |
1 files changed, 153 insertions, 164 deletions
diff --git a/apps/files/src/store/renaming.ts b/apps/files/src/store/renaming.ts index 2ec73837f82..fc61be3bd3b 100644 --- a/apps/files/src/store/renaming.ts +++ b/apps/files/src/store/renaming.ts @@ -3,184 +3,173 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { Node } from '@nextcloud/files' -import type { RenamingStore } from '../types' import axios, { isAxiosError } from '@nextcloud/axios' import { emit, subscribe } from '@nextcloud/event-bus' -import { NodeStatus } from '@nextcloud/files' -import { DialogBuilder } from '@nextcloud/dialogs' +import { FileType, NodeStatus } from '@nextcloud/files' import { t } from '@nextcloud/l10n' +import { spawnDialog } from '@nextcloud/vue/functions/dialog' import { basename, dirname, extname } from 'path' import { defineStore } from 'pinia' import logger from '../logger' -import Vue from 'vue' -import IconCancel from '@mdi/svg/svg/cancel.svg?raw' -import IconCheck from '@mdi/svg/svg/check.svg?raw' - -let isDialogVisible = false - -const showWarningDialog = (oldExtension: string, newExtension: string): Promise<boolean> => { - if (isDialogVisible) { - return Promise.resolve(false) - } - - isDialogVisible = true - - let message - - if (!oldExtension && newExtension) { - message = t( - 'files', - 'Adding the file extension "{new}" may render the file unreadable.', - { new: newExtension }, - ) - } else if (!newExtension) { - message = t( - 'files', - 'Removing the file extension "{old}" may render the file unreadable.', - { old: oldExtension }, - ) - } else { - message = t( - 'files', - 'Changing the file extension from "{old}" to "{new}" may render the file unreadable.', - { old: oldExtension, new: newExtension }, - ) - } - - return new Promise((resolve) => { - const dialog = new DialogBuilder() - .setName(t('files', 'Change file extension')) - .setText(message) - .setButtons([ - { - label: t('files', 'Keep {oldextension}', { oldextension: oldExtension }), - icon: IconCancel, - type: 'secondary', - callback: () => { - isDialogVisible = false - resolve(false) - }, +import Vue, { defineAsyncComponent, ref } from 'vue' +import { useUserConfigStore } from './userconfig' +import { fetchNode } from '../services/WebdavClient' + +export const useRenamingStore = defineStore('renaming', () => { + /** + * The currently renamed node + */ + const renamingNode = ref<Node>() + /** + * The new name of the currently renamed node + */ + const newNodeName = ref('') + + /** + * Internal flag to only allow calling `rename` once. + */ + const isRenaming = ref(false) + + /** + * Execute the renaming. + * This will rename the node set as `renamingNode` to the configured new name `newName`. + * + * @return true if success, false if skipped (e.g. new and old name are the same) + * @throws Error if renaming fails, details are set in the error message + */ + async function rename(): Promise<boolean> { + if (renamingNode.value === undefined) { + throw new Error('No node is currently being renamed') + } + + // Only rename once so we use this as some kind of mutex + if (isRenaming.value) { + return false + } + isRenaming.value = true + + let node = renamingNode.value + Vue.set(node, 'status', NodeStatus.LOADING) + + const userConfig = useUserConfigStore() + + let newName = newNodeName.value.trim() + const oldName = node.basename + const oldExtension = extname(oldName) + const newExtension = extname(newName) + // Check for extension change for files + if (node.type === FileType.File + && oldExtension !== newExtension + && userConfig.userConfig.show_dialog_file_extension + && !(await showFileExtensionDialog(oldExtension, newExtension)) + ) { + // user selected to use the old extension + newName = basename(newName, newExtension) + oldExtension + } + + const oldEncodedSource = node.encodedSource + try { + if (oldName === newName) { + return false + } + + // rename the node + node.rename(newName) + logger.debug('Moving file to', { destination: node.encodedSource, oldEncodedSource }) + // create MOVE request + await axios({ + method: 'MOVE', + url: oldEncodedSource, + headers: { + Destination: node.encodedSource, + Overwrite: 'F', }, - { - label: newExtension.length ? t('files', 'Use {newextension}', { newextension: newExtension }) : t('files', 'Remove extension'), - icon: IconCheck, - type: 'primary', - callback: () => { - isDialogVisible = false - resolve(true) - }, - }, - ]) - .build() - - dialog.show().then(() => { - dialog.hide() - }) - }) -} - -export const useRenamingStore = function(...args) { - const store = defineStore('renaming', { - state: () => ({ - renamingNode: undefined, - newName: '', - } as RenamingStore), - - actions: { - /** - * Execute the renaming. - * This will rename the node set as `renamingNode` to the configured new name `newName`. - * @return true if success, false if skipped (e.g. new and old name are the same) - * @throws Error if renaming fails, details are set in the error message - */ - async rename(): Promise<boolean> { - if (this.renamingNode === undefined) { - throw new Error('No node is currently being renamed') - } - - const newName = this.newName.trim?.() || '' - const oldName = this.renamingNode.basename - const oldEncodedSource = this.renamingNode.encodedSource - - // Check for extension change - const oldExtension = extname(oldName) - const newExtension = extname(newName) - if (oldExtension !== newExtension) { - const proceed = await showWarningDialog(oldExtension, newExtension) - if (!proceed) { - return false - } + }) + + // Update mime type if extension changed + // as other related informations might have changed + // on the backend but it is really hard to know on the front + if (oldExtension !== newExtension) { + node = await fetchNode(node.path) + } + + // Success 🎉 + emit('files:node:updated', node) + emit('files:node:renamed', node) + emit('files:node:moved', { + node, + oldSource: `${dirname(node.source)}/${oldName}`, + }) + + // Reset the state not changed + if (renamingNode.value === node) { + $reset() + } + + return true + } catch (error) { + logger.error('Error while renaming file', { error }) + // Rename back as it failed + node.rename(oldName) + if (isAxiosError(error)) { + // TODO: 409 means current folder does not exist, redirect ? + if (error?.response?.status === 404) { + throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName })) + } else if (error?.response?.status === 412) { + throw new Error(t( + 'files', + 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', + { + newName, + dir: basename(renamingNode.value!.dirname), + }, + )) } + } + // Unknown error + throw new Error(t('files', 'Could not rename "{oldName}"', { oldName })) + } finally { + Vue.set(node, 'status', undefined) + isRenaming.value = false + } + } - if (oldName === newName) { - return false - } + /** + * Reset the store state + */ + function $reset(): void { + newNodeName.value = '' + renamingNode.value = undefined + } - const node = this.renamingNode - Vue.set(node, 'status', NodeStatus.LOADING) - - try { - // rename the node - this.renamingNode.rename(newName) - logger.debug('Moving file to', { destination: this.renamingNode.encodedSource, oldEncodedSource }) - // create MOVE request - await axios({ - method: 'MOVE', - url: oldEncodedSource, - headers: { - Destination: this.renamingNode.encodedSource, - Overwrite: 'F', - }, - }) - - // Success 🎉 - emit('files:node:updated', this.renamingNode as Node) - emit('files:node:renamed', this.renamingNode as Node) - emit('files:node:moved', { - node: this.renamingNode as Node, - oldSource: `${dirname(this.renamingNode.source)}/${oldName}`, - }) - this.$reset() - return true - } catch (error) { - logger.error('Error while renaming file', { error }) - // Rename back as it failed - this.renamingNode.rename(oldName) - if (isAxiosError(error)) { - // TODO: 409 means current folder does not exist, redirect ? - if (error?.response?.status === 404) { - throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName })) - } else if (error?.response?.status === 412) { - throw new Error(t( - 'files', - 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', - { - newName, - dir: basename(this.renamingNode.dirname), - }, - )) - } - } - // Unknown error - throw new Error(t('files', 'Could not rename "{oldName}"', { oldName })) - } finally { - Vue.set(node, 'status', undefined) - } - }, - }, + // Make sure we only register the listeners once + subscribe('files:node:rename', (node: Node) => { + renamingNode.value = node + newNodeName.value = node.basename }) - const renamingStore = store(...args) + return { + $reset, - // Make sure we only register the listeners once - if (!renamingStore._initialized) { - subscribe('files:node:rename', function(node: Node) { - renamingStore.renamingNode = node - renamingStore.newName = node.basename - }) - renamingStore._initialized = true + newNodeName, + rename, + renamingNode, } +}) - return renamingStore +/** + * Show a dialog asking user for confirmation about changing the file extension. + * + * @param oldExtension the old file name extension + * @param newExtension the new file name extension + */ +async function showFileExtensionDialog(oldExtension: string, newExtension: string): Promise<boolean> { + const { promise, resolve } = Promise.withResolvers<boolean>() + spawnDialog( + defineAsyncComponent(() => import('../views/DialogConfirmFileExtension.vue')), + { oldExtension, newExtension }, + (useNewExtension: unknown) => resolve(Boolean(useNewExtension)), + ) + return await promise } |