aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-08-22 21:49:06 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-10-15 18:20:16 +0200
commitef5a9cf00d0842e8e359ddf99a3f492421f09220 (patch)
treea238354977ee2da1ad9d7c9e2ad6d93fa8280d51
parentcd3dc1719b8e84526f2c99634f0dc8bf16380579 (diff)
downloadnextcloud-server-ef5a9cf00d0842e8e359ddf99a3f492421f09220.tar.gz
nextcloud-server-ef5a9cf00d0842e8e359ddf99a3f492421f09220.zip
fix(files): Ensure renaming state is correctly reset
Problem: Is a node is renamed and the new name is out of the current visible list of nodes the component will be recycled, this means the props will change, so when the `onRename` functions is about to reset the state the `this.source` will point to a different node. To fix this, but also to separate business logic from visual representation, the logic is moved into the renaming store and the component is only responsible for rendering. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--apps/files/src/components/FileEntry/FileEntryName.vue70
-rw-r--r--apps/files/src/store/renaming.ts83
-rw-r--r--cypress/e2e/files/files-renaming.cy.ts30
3 files changed, 122 insertions, 61 deletions
diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue
index cf425127282..5236af0cfad 100644
--- a/apps/files/src/components/FileEntry/FileEntryName.vue
+++ b/apps/files/src/components/FileEntry/FileEntryName.vue
@@ -41,12 +41,9 @@
import type { FileAction, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
-import axios, { isAxiosError } from '@nextcloud/axios'
import { showError, showSuccess } from '@nextcloud/dialogs'
-import { emit } from '@nextcloud/event-bus'
import { FileType, NodeStatus } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
-import { dirname } from '@nextcloud/paths'
import { defineComponent, inject } from 'vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
@@ -245,66 +242,23 @@ export default defineComponent({
}
const oldName = this.source.basename
- const oldEncodedSource = this.source.encodedSource
- if (oldName === newName) {
- this.stopRenaming()
- return
- }
-
- // Set loading state
- this.$set(this.source, 'status', NodeStatus.LOADING)
- // Update node
- this.source.rename(newName)
-
- logger.debug('Moving file to', { destination: this.source.encodedSource, oldEncodedSource })
try {
- await axios({
- method: 'MOVE',
- url: oldEncodedSource,
- headers: {
- Destination: this.source.encodedSource,
- Overwrite: 'F',
- },
- })
-
- // Success 🎉
- emit('files:node:updated', this.source)
- emit('files:node:renamed', this.source)
- emit('files:node:moved', {
- node: this.source,
- oldSource: `${dirname(this.source.source)}/${oldName}`,
- })
- showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
-
- // Reset the renaming store
- this.stopRenaming()
- this.$nextTick(() => {
- const nameContainter = this.$refs.basename as HTMLElement | undefined
- nameContainter?.focus()
- })
+ const status = await this.renamingStore.rename()
+ if (status) {
+ showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
+ this.$nextTick(() => {
+ const nameContainter = this.$refs.basename as HTMLElement | undefined
+ nameContainter?.focus()
+ })
+ } else {
+ // Was cancelled - meaning the renaming state is just reset
+ }
} catch (error) {
- logger.error('Error while renaming file', { error })
- // Rename back as it failed
- this.source.rename(oldName)
+ logger.error(error as Error)
+ showError((error as Error).message)
// And ensure we reset to the renaming state
this.startRenaming()
-
- if (isAxiosError(error)) {
- // TODO: 409 means current folder does not exist, redirect ?
- if (error?.response?.status === 404) {
- showError(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
- return
- } else if (error?.response?.status === 412) {
- showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.directory }))
- return
- }
- }
-
- // Unknown error
- showError(t('files', 'Could not rename "{oldName}"', { oldName }))
- } finally {
- this.$set(this.source, 'status', undefined)
}
},
diff --git a/apps/files/src/store/renaming.ts b/apps/files/src/store/renaming.ts
index 3782b75e3a4..ff8ac3ba8da 100644
--- a/apps/files/src/store/renaming.ts
+++ b/apps/files/src/store/renaming.ts
@@ -2,17 +2,96 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { defineStore } from 'pinia'
-import { subscribe } from '@nextcloud/event-bus'
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 { t } from '@nextcloud/l10n'
+import { basename, dirname } from 'path'
+import { defineStore } from 'pinia'
+import logger from '../logger'
+import Vue from 'vue'
+
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
+ 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',
+ },
+ })
+
+ // 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)
+ }
+ },
+ },
})
const renamingStore = store(...args)
diff --git a/cypress/e2e/files/files-renaming.cy.ts b/cypress/e2e/files/files-renaming.cy.ts
index 9ac96a398b8..77f23a00459 100644
--- a/cypress/e2e/files/files-renaming.cy.ts
+++ b/cypress/e2e/files/files-renaming.cy.ts
@@ -4,7 +4,7 @@
*/
import type { User } from '@nextcloud/cypress'
-import { getRowForFile, haveValidity, triggerActionForFile } from './FilesUtils'
+import { getRowForFile, haveValidity, renameFile, triggerActionForFile } from './FilesUtils'
describe('files: Rename nodes', { testIsolation: true }, () => {
let user: User
@@ -115,4 +115,32 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
.findByRole('img', { name: 'File is loading' })
.should('not.exist')
})
+
+ /**
+ * This is a regression test of: https://github.com/nextcloud/server/issues/47438
+ * The issue was that the renaming state was not reset when the new name moved the file out of the view of the current files list
+ * due to virtual scrolling the renaming state was not changed then by the UI events (as the component was taken out of DOM before any event handling).
+ */
+ it('correctly resets renaming state', () => {
+ for (let i = 1; i <= 20; i++) {
+ cy.uploadContent(user, new Blob([]), 'text/plain', `/file${i}.txt`)
+ }
+ cy.viewport(1200, 500) // 500px is smaller then 20 * 50 which is the place that the files take up
+ cy.login(user)
+ cy.visit('/apps/files')
+
+ getRowForFile('file.txt').should('be.visible')
+ // Z so it is shown last
+ renameFile('file.txt', 'zzz.txt')
+ // not visible any longer
+ getRowForFile('zzz.txt').should('not.be.visible')
+ // scroll file list to bottom
+ cy.get('[data-cy-files-list]').scrollTo('bottom')
+ cy.screenshot()
+ // The file is no longer in rename state
+ getRowForFile('zzz.txt')
+ .should('be.visible')
+ .findByRole('textbox', { name: 'Filename' })
+ .should('not.exist')
+ })
})