-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(files): allow to ignore warning to change file type
* Missing pieces of #46528 * Add checkbox to not show this dialog again * Add user config as suggested by designers in files settings to reenable or diable this behavior. * Fix behavior of dialog: It says "keep .ext" but it does not keep the extension but cancels the operation. From the button label the user expects that the operation is continued but with the old extension. * Added more test coverage by adding component tests. Signed-off-by: Ferdinand Thiessen <[email protected]>
- Loading branch information
Showing
10 changed files
with
452 additions
and
179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,184 +3,165 @@ | |
* 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 { FileType, NodeStatus } from '@nextcloud/files' | ||
import { DialogBuilder } from '@nextcloud/dialogs' | ||
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' | ||
|
||
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) | ||
Check warning on line 37 in apps/files/src/store/renaming.ts
|
||
* @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 | ||
|
||
const 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 for files | ||
const oldExtension = extname(oldName) | ||
const newExtension = extname(newName) | ||
if (oldExtension !== newExtension && this.renamingNode.type === FileType.File) { | ||
const proceed = await showWarningDialog(oldExtension, newExtension) | ||
if (!proceed) { | ||
return false | ||
} | ||
}) | ||
|
||
// 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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.