Skip to content

Commit

Permalink
fix(files): Ensure renaming state is correctly reset
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
susnux committed Oct 7, 2024
1 parent 4144309 commit b01ec7c
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 60 deletions.
70 changes: 12 additions & 58 deletions apps/files/src/components/FileEntry/FileEntryName.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
},
Expand Down
83 changes: 81 additions & 2 deletions apps/files/src/store/renaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit b01ec7c

Please sign in to comment.