Skip to content

Commit 373107b

Browse files
authored
Merge pull request #50979 from nextcloud/feat/ignore-warning-files
feat(files): allow to ignore warning to change file type
2 parents 0427d5d + 4896c51 commit 373107b

18 files changed

+580
-186
lines changed

apps/files/lib/Service/UserConfig.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ class UserConfig {
1818
'default' => true,
1919
'allowed' => [true, false],
2020
],
21+
[
22+
// Whether to show the "confirm file extension change" warning
23+
'key' => 'show_dialog_file_extension',
24+
'default' => true,
25+
'allowed' => [true, false],
26+
],
2127
[
2228
// Whether to show the hidden files or not in the files list
2329
'key' => 'show_hidden',

apps/files/src/components/FileEntry/FileEntryName.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
<component :is="linkTo.is"
2424
v-else
2525
ref="basename"
26-
:aria-hidden="isRenaming"
2726
class="files-list__row-name-link"
2827
data-cy-files-list-row-name-link
2928
v-bind="linkTo.params">
@@ -117,11 +116,11 @@ export default defineComponent({
117116
return this.isRenaming && this.filesListWidth < 512
118117
},
119118
newName: {
120-
get() {
121-
return this.renamingStore.newName
119+
get(): string {
120+
return this.renamingStore.newNodeName
122121
},
123-
set(newName) {
124-
this.renamingStore.newName = newName
122+
set(newName: string) {
123+
this.renamingStore.newNodeName = newName
125124
},
126125
},
127126
@@ -249,7 +248,9 @@ export default defineComponent({
249248
try {
250249
const status = await this.renamingStore.rename()
251250
if (status) {
252-
showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
251+
showSuccess(
252+
t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName: this.source.basename }),
253+
)
253254
this.$nextTick(() => {
254255
const nameContainer = this.$refs.basename as HTMLElement | undefined
255256
nameContainer?.focus()

apps/files/src/eventbus.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ declare module '@nextcloud/event-bus' {
1616
'files:node:created': Node
1717
'files:node:deleted': Node
1818
'files:node:updated': Node
19+
'files:node:rename': Node
1920
'files:node:renamed': Node
2021
'files:node:moved': { node: Node, oldSource: string }
2122

apps/files/src/store/renaming.ts

Lines changed: 144 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -3,184 +3,165 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55
import type { Node } from '@nextcloud/files'
6-
import type { RenamingStore } from '../types'
76

87
import axios, { isAxiosError } from '@nextcloud/axios'
98
import { emit, subscribe } from '@nextcloud/event-bus'
109
import { FileType, NodeStatus } from '@nextcloud/files'
11-
import { DialogBuilder } from '@nextcloud/dialogs'
1210
import { t } from '@nextcloud/l10n'
11+
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
1312
import { basename, dirname, extname } from 'path'
1413
import { defineStore } from 'pinia'
1514
import logger from '../logger'
16-
import Vue from 'vue'
17-
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
18-
import IconCheck from '@mdi/svg/svg/check.svg?raw'
19-
20-
let isDialogVisible = false
21-
22-
const showWarningDialog = (oldExtension: string, newExtension: string): Promise<boolean> => {
23-
if (isDialogVisible) {
24-
return Promise.resolve(false)
25-
}
26-
27-
isDialogVisible = true
28-
29-
let message
30-
31-
if (!oldExtension && newExtension) {
32-
message = t(
33-
'files',
34-
'Adding the file extension "{new}" may render the file unreadable.',
35-
{ new: newExtension },
36-
)
37-
} else if (!newExtension) {
38-
message = t(
39-
'files',
40-
'Removing the file extension "{old}" may render the file unreadable.',
41-
{ old: oldExtension },
42-
)
43-
} else {
44-
message = t(
45-
'files',
46-
'Changing the file extension from "{old}" to "{new}" may render the file unreadable.',
47-
{ old: oldExtension, new: newExtension },
48-
)
49-
}
50-
51-
return new Promise((resolve) => {
52-
const dialog = new DialogBuilder()
53-
.setName(t('files', 'Change file extension'))
54-
.setText(message)
55-
.setButtons([
56-
{
57-
label: t('files', 'Keep {oldextension}', { oldextension: oldExtension }),
58-
icon: IconCancel,
59-
type: 'secondary',
60-
callback: () => {
61-
isDialogVisible = false
62-
resolve(false)
63-
},
15+
import Vue, { defineAsyncComponent, ref } from 'vue'
16+
import { useUserConfigStore } from './userconfig'
17+
18+
export const useRenamingStore = defineStore('renaming', () => {
19+
/**
20+
* The currently renamed node
21+
*/
22+
const renamingNode = ref<Node>()
23+
/**
24+
* The new name of the currently renamed node
25+
*/
26+
const newNodeName = ref('')
27+
28+
/**
29+
* Internal flag to only allow calling `rename` once.
30+
*/
31+
const isRenaming = ref(false)
32+
33+
/**
34+
* Execute the renaming.
35+
* This will rename the node set as `renamingNode` to the configured new name `newName`.
36+
*
37+
* @return true if success, false if skipped (e.g. new and old name are the same)
38+
* @throws Error if renaming fails, details are set in the error message
39+
*/
40+
async function rename(): Promise<boolean> {
41+
if (renamingNode.value === undefined) {
42+
throw new Error('No node is currently being renamed')
43+
}
44+
45+
// Only rename once so we use this as some kind of mutex
46+
if (isRenaming.value) {
47+
return false
48+
}
49+
isRenaming.value = true
50+
51+
const node = renamingNode.value
52+
Vue.set(node, 'status', NodeStatus.LOADING)
53+
54+
const userConfig = useUserConfigStore()
55+
56+
let newName = newNodeName.value.trim()
57+
const oldName = node.basename
58+
const oldExtension = extname(oldName)
59+
const newExtension = extname(newName)
60+
// Check for extension change for files
61+
if (node.type === FileType.File
62+
&& oldExtension !== newExtension
63+
&& userConfig.userConfig.show_dialog_file_extension
64+
&& !(await showFileExtensionDialog(oldExtension, newExtension))
65+
) {
66+
// user selected to use the old extension
67+
newName = basename(newName, newExtension) + oldExtension
68+
}
69+
70+
const oldEncodedSource = node.encodedSource
71+
try {
72+
if (oldName === newName) {
73+
return false
74+
}
75+
76+
// rename the node
77+
node.rename(newName)
78+
logger.debug('Moving file to', { destination: node.encodedSource, oldEncodedSource })
79+
// create MOVE request
80+
await axios({
81+
method: 'MOVE',
82+
url: oldEncodedSource,
83+
headers: {
84+
Destination: node.encodedSource,
85+
Overwrite: 'F',
6486
},
65-
{
66-
label: newExtension.length ? t('files', 'Use {newextension}', { newextension: newExtension }) : t('files', 'Remove extension'),
67-
icon: IconCheck,
68-
type: 'primary',
69-
callback: () => {
70-
isDialogVisible = false
71-
resolve(true)
72-
},
73-
},
74-
])
75-
.build()
76-
77-
dialog.show().then(() => {
78-
dialog.hide()
79-
})
80-
})
81-
}
82-
83-
export const useRenamingStore = function(...args) {
84-
const store = defineStore('renaming', {
85-
state: () => ({
86-
renamingNode: undefined,
87-
newName: '',
88-
} as RenamingStore),
89-
90-
actions: {
91-
/**
92-
* Execute the renaming.
93-
* This will rename the node set as `renamingNode` to the configured new name `newName`.
94-
* @return true if success, false if skipped (e.g. new and old name are the same)
95-
* @throws Error if renaming fails, details are set in the error message
96-
*/
97-
async rename(): Promise<boolean> {
98-
if (this.renamingNode === undefined) {
99-
throw new Error('No node is currently being renamed')
100-
}
101-
102-
const newName = this.newName.trim?.() || ''
103-
const oldName = this.renamingNode.basename
104-
const oldEncodedSource = this.renamingNode.encodedSource
105-
106-
// Check for extension change for files
107-
const oldExtension = extname(oldName)
108-
const newExtension = extname(newName)
109-
if (oldExtension !== newExtension && this.renamingNode.type === FileType.File) {
110-
const proceed = await showWarningDialog(oldExtension, newExtension)
111-
if (!proceed) {
112-
return false
113-
}
87+
})
88+
89+
// Success 🎉
90+
emit('files:node:updated', node)
91+
emit('files:node:renamed', node)
92+
emit('files:node:moved', {
93+
node,
94+
oldSource: `${dirname(node.source)}/${oldName}`,
95+
})
96+
97+
// Reset the state not changed
98+
if (renamingNode.value === node) {
99+
$reset()
100+
}
101+
102+
return true
103+
} catch (error) {
104+
logger.error('Error while renaming file', { error })
105+
// Rename back as it failed
106+
node.rename(oldName)
107+
if (isAxiosError(error)) {
108+
// TODO: 409 means current folder does not exist, redirect ?
109+
if (error?.response?.status === 404) {
110+
throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
111+
} else if (error?.response?.status === 412) {
112+
throw new Error(t(
113+
'files',
114+
'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.',
115+
{
116+
newName,
117+
dir: basename(renamingNode.value!.dirname),
118+
},
119+
))
114120
}
121+
}
122+
// Unknown error
123+
throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
124+
} finally {
125+
Vue.set(node, 'status', undefined)
126+
isRenaming.value = false
127+
}
128+
}
115129

116-
if (oldName === newName) {
117-
return false
118-
}
130+
/**
131+
* Reset the store state
132+
*/
133+
function $reset(): void {
134+
newNodeName.value = ''
135+
renamingNode.value = undefined
136+
}
119137

120-
const node = this.renamingNode
121-
Vue.set(node, 'status', NodeStatus.LOADING)
122-
123-
try {
124-
// rename the node
125-
this.renamingNode.rename(newName)
126-
logger.debug('Moving file to', { destination: this.renamingNode.encodedSource, oldEncodedSource })
127-
// create MOVE request
128-
await axios({
129-
method: 'MOVE',
130-
url: oldEncodedSource,
131-
headers: {
132-
Destination: this.renamingNode.encodedSource,
133-
Overwrite: 'F',
134-
},
135-
})
136-
137-
// Success 🎉
138-
emit('files:node:updated', this.renamingNode as Node)
139-
emit('files:node:renamed', this.renamingNode as Node)
140-
emit('files:node:moved', {
141-
node: this.renamingNode as Node,
142-
oldSource: `${dirname(this.renamingNode.source)}/${oldName}`,
143-
})
144-
this.$reset()
145-
return true
146-
} catch (error) {
147-
logger.error('Error while renaming file', { error })
148-
// Rename back as it failed
149-
this.renamingNode.rename(oldName)
150-
if (isAxiosError(error)) {
151-
// TODO: 409 means current folder does not exist, redirect ?
152-
if (error?.response?.status === 404) {
153-
throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
154-
} else if (error?.response?.status === 412) {
155-
throw new Error(t(
156-
'files',
157-
'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.',
158-
{
159-
newName,
160-
dir: basename(this.renamingNode.dirname),
161-
},
162-
))
163-
}
164-
}
165-
// Unknown error
166-
throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
167-
} finally {
168-
Vue.set(node, 'status', undefined)
169-
}
170-
},
171-
},
138+
// Make sure we only register the listeners once
139+
subscribe('files:node:rename', (node: Node) => {
140+
renamingNode.value = node
141+
newNodeName.value = node.basename
172142
})
173143

174-
const renamingStore = store(...args)
144+
return {
145+
$reset,
175146

176-
// Make sure we only register the listeners once
177-
if (!renamingStore._initialized) {
178-
subscribe('files:node:rename', function(node: Node) {
179-
renamingStore.renamingNode = node
180-
renamingStore.newName = node.basename
181-
})
182-
renamingStore._initialized = true
147+
newNodeName,
148+
rename,
149+
renamingNode,
183150
}
151+
})
184152

185-
return renamingStore
153+
/**
154+
* Show a dialog asking user for confirmation about changing the file extension.
155+
*
156+
* @param oldExtension the old file name extension
157+
* @param newExtension the new file name extension
158+
*/
159+
async function showFileExtensionDialog(oldExtension: string, newExtension: string): Promise<boolean> {
160+
const { promise, resolve } = Promise.withResolvers<boolean>()
161+
spawnDialog(
162+
defineAsyncComponent(() => import('../views/DialogConfirmFileExtension.vue')),
163+
{ oldExtension, newExtension },
164+
(useNewExtension: unknown) => resolve(Boolean(useNewExtension)),
165+
)
166+
return await promise
186167
}

apps/files/src/store/userconfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const initialUserConfig = loadState<UserConfig>('files', 'config', {
1717
sort_favorites_first: true,
1818
sort_folders_first: true,
1919
grid_view: false,
20+
21+
show_dialog_file_extension: true,
2022
})
2123

2224
export const useUserConfigStore = defineStore('userconfig', () => {

0 commit comments

Comments
 (0)