mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 16:26:59 -04:00
Merge pull request #51015 from nextcloud/backport/50979/stable31
[stable31] feat(files): allow to ignore warning to change file type
This commit is contained in:
commit
f7fc17c2c9
18 changed files with 581 additions and 179 deletions
|
|
@ -18,6 +18,12 @@ class UserConfig {
|
|||
'default' => true,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to show the "confirm file extension change" warning
|
||||
'key' => 'show_dialog_file_extension',
|
||||
'default' => true,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to show the hidden files or not in the files list
|
||||
'key' => 'show_hidden',
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
<component :is="linkTo.is"
|
||||
v-else
|
||||
ref="basename"
|
||||
:aria-hidden="isRenaming"
|
||||
class="files-list__row-name-link"
|
||||
data-cy-files-list-row-name-link
|
||||
v-bind="linkTo.params">
|
||||
|
|
@ -117,11 +116,11 @@ export default defineComponent({
|
|||
return this.isRenaming && this.filesListWidth < 512
|
||||
},
|
||||
newName: {
|
||||
get() {
|
||||
return this.renamingStore.newName
|
||||
get(): string {
|
||||
return this.renamingStore.newNodeName
|
||||
},
|
||||
set(newName) {
|
||||
this.renamingStore.newName = newName
|
||||
set(newName: string) {
|
||||
this.renamingStore.newNodeName = newName
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -249,7 +248,9 @@ export default defineComponent({
|
|||
try {
|
||||
const status = await this.renamingStore.rename()
|
||||
if (status) {
|
||||
showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
|
||||
showSuccess(
|
||||
t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName: this.source.basename }),
|
||||
)
|
||||
this.$nextTick(() => {
|
||||
const nameContainer = this.$refs.basename as HTMLElement | undefined
|
||||
nameContainer?.focus()
|
||||
|
|
|
|||
1
apps/files/src/eventbus.d.ts
vendored
1
apps/files/src/eventbus.d.ts
vendored
|
|
@ -18,6 +18,7 @@ declare module '@nextcloud/event-bus' {
|
|||
'files:node:created': Node
|
||||
'files:node:deleted': Node
|
||||
'files:node:updated': Node
|
||||
'files:node:rename': Node
|
||||
'files:node:renamed': Node
|
||||
'files:node:moved': { node: Node, oldSource: string }
|
||||
|
||||
|
|
|
|||
|
|
@ -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/dist/Functions/dialog.js'
|
||||
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'
|
||||
import Vue, { defineAsyncComponent, ref } from 'vue'
|
||||
import { useUserConfigStore } from './userconfig'
|
||||
|
||||
let isDialogVisible = false
|
||||
export const useRenamingStore = defineStore('renaming', () => {
|
||||
/**
|
||||
* The currently renamed node
|
||||
*/
|
||||
const renamingNode = ref<Node>()
|
||||
/**
|
||||
* The new name of the currently renamed node
|
||||
*/
|
||||
const newNodeName = ref('')
|
||||
|
||||
const showWarningDialog = (oldExtension: string, newExtension: string): Promise<boolean> => {
|
||||
if (isDialogVisible) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
/**
|
||||
* Internal flag to only allow calling `rename` once.
|
||||
*/
|
||||
const isRenaming = ref(false)
|
||||
|
||||
isDialogVisible = true
|
||||
/**
|
||||
* 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')
|
||||
}
|
||||
|
||||
let message
|
||||
// Only rename once so we use this as some kind of mutex
|
||||
if (isRenaming.value) {
|
||||
return false
|
||||
}
|
||||
isRenaming.value = true
|
||||
|
||||
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 },
|
||||
)
|
||||
}
|
||||
const node = renamingNode.value
|
||||
Vue.set(node, 'status', NodeStatus.LOADING)
|
||||
|
||||
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)
|
||||
},
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
// Success 🎉
|
||||
emit('files:node:updated', node)
|
||||
emit('files:node:renamed', node)
|
||||
emit('files:node:moved', {
|
||||
node,
|
||||
oldSource: `${dirname(node.source)}/${oldName}`,
|
||||
})
|
||||
|
||||
export const useRenamingStore = function(...args) {
|
||||
const store = defineStore('renaming', {
|
||||
state: () => ({
|
||||
renamingNode: undefined,
|
||||
newName: '',
|
||||
} as RenamingStore),
|
||||
// Reset the state not changed
|
||||
if (renamingNode.value === node) {
|
||||
$reset()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (oldName === newName) {
|
||||
return false
|
||||
}
|
||||
|
||||
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',
|
||||
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),
|
||||
},
|
||||
})
|
||||
|
||||
// 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)
|
||||
))
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
// Unknown error
|
||||
throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
|
||||
} finally {
|
||||
Vue.set(node, 'status', undefined)
|
||||
isRenaming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const renamingStore = store(...args)
|
||||
/**
|
||||
* Reset the store state
|
||||
*/
|
||||
function $reset(): void {
|
||||
newNodeName.value = ''
|
||||
renamingNode.value = undefined
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
subscribe('files:node:rename', (node: Node) => {
|
||||
renamingNode.value = node
|
||||
newNodeName.value = node.basename
|
||||
})
|
||||
|
||||
return renamingStore
|
||||
return {
|
||||
$reset,
|
||||
|
||||
newNodeName,
|
||||
rename,
|
||||
renamingNode,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ const initialUserConfig = loadState<UserConfig>('files', 'config', {
|
|||
sort_favorites_first: true,
|
||||
sort_folders_first: true,
|
||||
grid_view: false,
|
||||
|
||||
show_dialog_file_extension: true,
|
||||
})
|
||||
|
||||
export const useUserConfigStore = defineStore('userconfig', () => {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export interface PathOptions {
|
|||
export interface UserConfig {
|
||||
[key: string]: boolean|undefined
|
||||
|
||||
show_dialog_file_extension: boolean,
|
||||
show_hidden: boolean
|
||||
crop_image_previews: boolean
|
||||
sort_favorites_first: boolean
|
||||
|
|
|
|||
161
apps/files/src/views/DialogConfirmFileExtension.cy.ts
Normal file
161
apps/files/src/views/DialogConfirmFileExtension.cy.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import DialogConfirmFileExtension from './DialogConfirmFileExtension.vue'
|
||||
import { useUserConfigStore } from '../store/userconfig'
|
||||
|
||||
describe('DialogConfirmFileExtension', () => {
|
||||
it('renders with both extensions', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('heading')
|
||||
.should('contain.text', 'Change file extension')
|
||||
cy.get('@dialog')
|
||||
.findByRole('checkbox', { name: /Do not show this dialog again/i })
|
||||
.should('exist')
|
||||
.and('not.be.checked')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep .old' })
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Use .new' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('renders without old extension', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep without extension' })
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Use .new' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('renders without new extension', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep .old' })
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Remove extension' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('emits correct value on keep old', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
}).as('component')
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Keep .old' })
|
||||
.click()
|
||||
cy.get('@component')
|
||||
.its('wrapper')
|
||||
.should((wrapper) => expect(wrapper.emitted('close')).to.eql([[false]]))
|
||||
})
|
||||
|
||||
it('emits correct value on use new', () => {
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})],
|
||||
},
|
||||
}).as('component')
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('button', { name: 'Use .new' })
|
||||
.click()
|
||||
cy.get('@component')
|
||||
.its('wrapper')
|
||||
.should((wrapper) => expect(wrapper.emitted('close')).to.eql([[true]]))
|
||||
})
|
||||
|
||||
it('updates user config when checking the checkbox', () => {
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: cy.spy,
|
||||
})
|
||||
|
||||
cy.mount(DialogConfirmFileExtension, {
|
||||
propsData: {
|
||||
oldExtension: '.old',
|
||||
newExtension: '.new',
|
||||
},
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
}).as('component')
|
||||
|
||||
cy.findByRole('dialog')
|
||||
.as('dialog')
|
||||
.should('be.visible')
|
||||
cy.get('@dialog')
|
||||
.findByRole('checkbox', { name: /Do not show this dialog again/i })
|
||||
.check({ force: true })
|
||||
|
||||
cy.wrap(useUserConfigStore())
|
||||
.its('update')
|
||||
.should('have.been.calledWith', 'show_dialog_file_extension', false)
|
||||
})
|
||||
})
|
||||
92
apps/files/src/views/DialogConfirmFileExtension.vue
Normal file
92
apps/files/src/views/DialogConfirmFileExtension.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IDialogButton } from '@nextcloud/dialogs'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useUserConfigStore } from '../store/userconfig.ts'
|
||||
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
|
||||
import svgIconCancel from '@mdi/svg/svg/cancel.svg?raw'
|
||||
import svgIconCheck from '@mdi/svg/svg/check.svg?raw'
|
||||
|
||||
const props = defineProps<{
|
||||
oldExtension?: string
|
||||
newExtension?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close', v: boolean): void
|
||||
}>()
|
||||
|
||||
const userConfigStore = useUserConfigStore()
|
||||
const dontShowAgain = computed({
|
||||
get: () => !userConfigStore.userConfig.show_dialog_file_extension,
|
||||
set: (value: boolean) => userConfigStore.update('show_dialog_file_extension', !value),
|
||||
})
|
||||
|
||||
const buttons = computed<IDialogButton[]>(() => [
|
||||
{
|
||||
label: props.oldExtension
|
||||
? t('files', 'Keep {old}', { old: props.oldExtension })
|
||||
: t('files', 'Keep without extension'),
|
||||
icon: svgIconCancel,
|
||||
type: 'secondary',
|
||||
callback: () => closeDialog(false),
|
||||
},
|
||||
{
|
||||
label: props.newExtension
|
||||
? t('files', 'Use {new}', { new: props.newExtension })
|
||||
: t('files', 'Remove extension'),
|
||||
icon: svgIconCheck,
|
||||
type: 'primary',
|
||||
callback: () => closeDialog(true),
|
||||
},
|
||||
])
|
||||
|
||||
/** Open state of the dialog */
|
||||
const open = ref(true)
|
||||
|
||||
/**
|
||||
* Close the dialog and emit the response
|
||||
* @param value User selected response
|
||||
*/
|
||||
function closeDialog(value: boolean) {
|
||||
emit('close', value)
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcDialog :buttons="buttons"
|
||||
:open="open"
|
||||
:can-close="false"
|
||||
:name="t('files', 'Change file extension')"
|
||||
size="small">
|
||||
<p v-if="newExtension && oldExtension">
|
||||
{{ t('files', 'Changing the file extension from "{old}" to "{new}" may render the file unreadable.', { old: oldExtension, new: newExtension }) }}
|
||||
</p>
|
||||
<p v-else-if="oldExtension">
|
||||
{{ t('files', 'Removing the file extension "{old}" may render the file unreadable.', { old: oldExtension }) }}
|
||||
</p>
|
||||
<p v-else-if="newExtension">
|
||||
{{ t('files', 'Adding the file extension "{new}" may render the file unreadable.', { new: newExtension }) }}
|
||||
</p>
|
||||
|
||||
<NcCheckboxRadioSwitch v-model="dontShowAgain"
|
||||
class="dialog-confirm-file-extension__checkbox"
|
||||
type="checkbox">
|
||||
{{ t('files', 'Do not show this dialog again.') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</NcDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-confirm-file-extension__checkbox {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -83,6 +83,15 @@
|
|||
</em>
|
||||
</NcAppSettingsSection>
|
||||
|
||||
<NcAppSettingsSection id="warning" :name="t('files', 'Warnings')">
|
||||
<em>{{ t('files', 'Prevent warning dialogs from open or reenable them.') }}</em>
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
:checked="userConfig.show_dialog_file_extension"
|
||||
@update:checked="setConfig('show_dialog_file_extension', $event)">
|
||||
{{ t('files', 'Show a warning dialog when changing a file extension.') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</NcAppSettingsSection>
|
||||
|
||||
<NcAppSettingsSection id="shortcuts"
|
||||
:name="t('files', 'Keyboard shortcuts')">
|
||||
<em>{{ t('files', 'Speed up your Files experience with these quick shortcuts.') }}</em>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
|
|||
})
|
||||
|
||||
it('shows accessible loading information', () => {
|
||||
const { resolve, promise } = Promise.withResolvers()
|
||||
const { resolve, promise } = Promise.withResolvers<void>()
|
||||
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
|
|||
.should('not.exist')
|
||||
|
||||
cy.log('Resolve promise to preoceed with MOVE request')
|
||||
.then(() => resolve(null))
|
||||
.then(() => resolve())
|
||||
|
||||
// Ensure the request is done (file renamed)
|
||||
cy.wait('@moveFile')
|
||||
|
|
@ -194,7 +194,7 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
|
|||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('shows warning on extension change', () => {
|
||||
it('shows warning on extension change - select new extension', () => {
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
|
||||
triggerActionForFile('file.txt', 'rename')
|
||||
|
|
@ -202,39 +202,60 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
|
|||
.findByRole('textbox', { name: 'Filename' })
|
||||
.should('be.visible')
|
||||
.type('{selectAll}file.md')
|
||||
.should(haveValidity(''))
|
||||
.type('{enter}')
|
||||
|
||||
// See warning dialog
|
||||
cy.findByRole('dialog', { name: 'Change file extension' })
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: /use/i })
|
||||
.findByRole('button', { name: 'Use .md' })
|
||||
.click()
|
||||
|
||||
// See it is renamed
|
||||
getRowForFile('file.md').should('be.visible')
|
||||
})
|
||||
|
||||
it('shows warning on extension change and allow cancellation', () => {
|
||||
it('shows warning on extension change - select old extension', () => {
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
|
||||
triggerActionForFile('file.txt', 'rename')
|
||||
getRowForFile('file.txt')
|
||||
.findByRole('textbox', { name: 'Filename' })
|
||||
.should('be.visible')
|
||||
.type('{selectAll}file.md')
|
||||
.should(haveValidity(''))
|
||||
.type('{selectAll}document.md')
|
||||
.type('{enter}')
|
||||
|
||||
// See warning dialog
|
||||
cy.findByRole('dialog', { name: 'Change file extension' })
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: /keep/i })
|
||||
.findByRole('button', { name: 'Keep .txt' })
|
||||
.click()
|
||||
|
||||
// See it is not renamed
|
||||
// See it is renamed
|
||||
getRowForFile('document.txt').should('be.visible')
|
||||
})
|
||||
|
||||
it('shows warning on extension removal', () => {
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
getRowForFile('file.md').should('not.exist')
|
||||
|
||||
triggerActionForFile('file.txt', 'rename')
|
||||
getRowForFile('file.txt')
|
||||
.findByRole('textbox', { name: 'Filename' })
|
||||
.should('be.visible')
|
||||
.type('{selectAll}file')
|
||||
.type('{enter}')
|
||||
|
||||
cy.findByRole('dialog', { name: 'Change file extension' })
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: 'Keep .txt' })
|
||||
.should('be.visible')
|
||||
cy.findByRole('dialog', { name: 'Change file extension' })
|
||||
.findByRole('button', { name: 'Remove extension' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
// See it is renamed
|
||||
getRowForFile('file').should('be.visible')
|
||||
getRowForFile('file.txt').should('not.exist')
|
||||
})
|
||||
|
||||
it('does not show warning on folder renaming with a dot', () => {
|
||||
|
|
|
|||
2
dist/7425-7425.js
vendored
Normal file
2
dist/7425-7425.js
vendored
Normal file
File diff suppressed because one or more lines are too long
123
dist/7425-7425.js.license
vendored
Normal file
123
dist/7425-7425.js.license
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
SPDX-License-Identifier: MIT
|
||||
SPDX-License-Identifier: ISC
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
SPDX-License-Identifier: BSD-3-Clause
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
|
||||
SPDX-FileCopyrightText: escape-html developers
|
||||
SPDX-FileCopyrightText: Tobias Koppers @sokra
|
||||
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
|
||||
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
|
||||
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-FileCopyrightText: Matt Zabriskie
|
||||
SPDX-FileCopyrightText: John-David Dalton <john.david.dalton@gmail.com> (http://allyoucanleet.com/)
|
||||
SPDX-FileCopyrightText: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
|
||||
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
|
||||
SPDX-FileCopyrightText: Guillaume Chau
|
||||
SPDX-FileCopyrightText: GitHub Inc.
|
||||
SPDX-FileCopyrightText: Feross Aboukhadijeh
|
||||
SPDX-FileCopyrightText: Evan You
|
||||
SPDX-FileCopyrightText: Eduardo San Martin Morote
|
||||
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
|
||||
SPDX-FileCopyrightText: David Clark
|
||||
SPDX-FileCopyrightText: Christoph Wurst
|
||||
SPDX-FileCopyrightText: Austin Andrews
|
||||
SPDX-FileCopyrightText: Anthony Fu <https://github.com/antfu>
|
||||
SPDX-FileCopyrightText: Andris Reinman
|
||||
|
||||
|
||||
This file is generated from multiple sources. Included packages:
|
||||
- @mdi/svg
|
||||
- version: 7.4.47
|
||||
- license: Apache-2.0
|
||||
- @nextcloud/auth
|
||||
- version: 2.4.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/axios
|
||||
- version: 2.5.1
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/browser-storage
|
||||
- version: 0.4.0
|
||||
- license: GPL-3.0-or-later
|
||||
- semver
|
||||
- version: 7.6.3
|
||||
- license: ISC
|
||||
- @nextcloud/event-bus
|
||||
- version: 3.3.1
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/initial-state
|
||||
- version: 2.2.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/l10n
|
||||
- version: 3.1.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/router
|
||||
- version: 3.0.1
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/vue
|
||||
- version: 8.22.0
|
||||
- license: AGPL-3.0-or-later
|
||||
- @vue/devtools-api
|
||||
- version: 6.6.3
|
||||
- license: MIT
|
||||
- @vueuse/core
|
||||
- version: 11.1.0
|
||||
- license: MIT
|
||||
- @vueuse/shared
|
||||
- version: 11.1.0
|
||||
- license: MIT
|
||||
- axios
|
||||
- version: 1.7.9
|
||||
- license: MIT
|
||||
- base64-js
|
||||
- version: 1.5.1
|
||||
- license: MIT
|
||||
- css-loader
|
||||
- version: 7.1.2
|
||||
- license: MIT
|
||||
- dompurify
|
||||
- version: 3.1.7
|
||||
- license: (MPL-2.0 OR Apache-2.0)
|
||||
- escape-html
|
||||
- version: 1.0.3
|
||||
- license: MIT
|
||||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 7.6.4
|
||||
- license: MIT
|
||||
- ieee754
|
||||
- version: 1.2.1
|
||||
- license: BSD-3-Clause
|
||||
- lodash.get
|
||||
- version: 4.4.2
|
||||
- license: MIT
|
||||
- node-gettext
|
||||
- version: 3.0.0
|
||||
- license: MIT
|
||||
- buffer
|
||||
- version: 6.0.3
|
||||
- license: MIT
|
||||
- pinia
|
||||
- version: 2.3.1
|
||||
- license: MIT
|
||||
- process
|
||||
- version: 0.11.10
|
||||
- license: MIT
|
||||
- style-loader
|
||||
- version: 4.0.0
|
||||
- license: MIT
|
||||
- tabbable
|
||||
- version: 6.2.0
|
||||
- license: MIT
|
||||
- vue-loader
|
||||
- version: 15.11.1
|
||||
- license: MIT
|
||||
- vue
|
||||
- version: 2.7.16
|
||||
- license: MIT
|
||||
- nextcloud
|
||||
- version: 1.0.0
|
||||
- license: AGPL-3.0-or-later
|
||||
1
dist/7425-7425.js.map
vendored
Normal file
1
dist/7425-7425.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/7425-7425.js.map.license
vendored
Symbolic link
1
dist/7425-7425.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
7425-7425.js.license
|
||||
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue