diff options
Diffstat (limited to 'apps/files/src/components/FileEntry.vue')
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 264 |
1 files changed, 15 insertions, 249 deletions
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 7ff6186a6e3..40a271aa972 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -40,8 +40,8 @@ <FileEntryCheckbox v-if="visible" :display-name="displayName" :fileid="fileid" - :loading="isLoading" - :nodes="nodes"/> + :is-loading="isLoading" + :nodes="nodes" /> <!-- Link to file --> <td class="files-list__row-name" data-cy-files-list-row-name> @@ -51,38 +51,13 @@ :dragover="dragover" @click.native="execDefaultAction" /> - <!-- Rename input --> - <form v-if="isRenaming" - v-on-click-outside="stopRenaming" - :aria-hidden="!isRenaming" - :aria-label="t('files', 'Rename file')" - class="files-list__row-rename" - @submit.prevent.stop="onRename"> - <NcTextField ref="renameInput" - :label="renameLabel" - :autofocus="true" - :minlength="1" - :required="true" - :value.sync="newName" - enterkeyhint="done" - @keyup="checkInputValidity" - @keyup.esc="stopRenaming" /> - </form> - - <a v-else - ref="basename" - :aria-hidden="isRenaming" - class="files-list__row-name-link" - data-cy-files-list-row-name-link - v-bind="linkTo" - @click="execDefaultAction"> - <!-- File name --> - <span class="files-list__row-name-text"> - <!-- Keep the displayName stuck to the extension to avoid whitespace rendering issues--> - <span class="files-list__row-name-" v-text="displayName" /> - <span class="files-list__row-name-ext" v-text="extension" /> - </span> - </a> + <FileEntryName ref="name" + :display-name="displayName" + :extension="extension" + :files-list-width="filesListWidth" + :nodes="nodes" + :source="source" + @click="execDefaultAction" /> </td> <!-- Actions --> @@ -131,20 +106,15 @@ <script lang="ts"> import type { PropType } from 'vue' -import { emit } from '@nextcloud/event-bus' import { extname, join } from 'path' import { FileType, formatFileSize, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files' import { getUploader } from '@nextcloud/upload' -import { loadState } from '@nextcloud/initial-state' -import { showError, showSuccess } from '@nextcloud/dialogs' +import { showError } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import { vOnClickOutside } from '@vueuse/components' -import axios from '@nextcloud/axios' import moment from '@nextcloud/moment' import Vue from 'vue' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' - import { action as sidebarAction } from '../actions/sidebarAction.ts' import { getDragAndDropPreview } from '../utils/dragUtils.ts' import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction.ts' @@ -158,22 +128,21 @@ import { useSelectionStore } from '../store/selection.ts' import CustomElementRender from './CustomElementRender.vue' import FileEntryActions from './FileEntry/FileEntryActions.vue' import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue' +import FileEntryName from './FileEntry/FileEntryName.vue' import FileEntryPreview from './FileEntry/FileEntryPreview.vue' import logger from '../logger.js' Vue.directive('onClickOutside', vOnClickOutside) -const forbiddenCharacters = loadState('files', 'forbiddenCharacters', '') as string - export default Vue.extend({ name: 'FileEntry', components: { CustomElementRender, FileEntryActions, - FileEntryPreview, - NcTextField, FileEntryCheckbox, + FileEntryName, + FileEntryPreview, }, props: { @@ -222,8 +191,6 @@ export default Vue.extend({ return { loading: '', dragover: false, - - NodeStatus, } }, @@ -322,37 +289,6 @@ export default Vue.extend({ return '' }, - linkTo() { - if (this.source.attributes.failed) { - return { - title: t('files', 'This node is unavailable'), - is: 'span', - } - } - - const enabledDefaultActions = this.$refs?.actions?.enabledDefaultActions - if (enabledDefaultActions?.length > 0) { - const action = enabledDefaultActions[0] - const displayName = action.displayName([this.source], this.currentView) - return { - title: displayName, - role: 'button', - } - } - - if (this.source?.permissions & Permission.READ) { - return { - download: this.source.basename, - href: this.source.source, - title: t('files', 'Download file {name}', { name: this.displayName }), - } - } - - return { - is: 'span', - } - }, - draggingFiles() { return this.draggingStore.dragging }, @@ -363,28 +299,12 @@ export default Vue.extend({ return this.selectedFiles.includes(this.fileid) }, - renameLabel() { - const matchLabel: Record<FileType, string> = { - [FileType.File]: t('files', 'File name'), - [FileType.Folder]: t('files', 'Folder name'), - } - return matchLabel[this.source.type] - }, - isRenaming() { return this.renamingStore.renamingNode === this.source }, isRenamingSmallScreen() { return this.isRenaming && this.filesListWidth < 512 }, - newName: { - get() { - return this.renamingStore.newName - }, - set(newName) { - this.renamingStore.newName = newName - }, - }, isActive() { return this.fileid === this.currentFileId?.toString?.() @@ -434,17 +354,6 @@ export default Vue.extend({ source() { this.resetState() }, - - /** - * If renaming starts, select the file name - * in the input, without the extension. - * @param renaming - */ - isRenaming(renaming) { - if (renaming) { - this.startRenaming() - } - }, }, beforeDestroy() { @@ -462,149 +371,6 @@ export default Vue.extend({ this.openedMenu = false }, - /** - * Check if the file name is valid and update the - * input validity using browser's native validation. - * @param event the keyup event - */ - checkInputValidity(event?: KeyboardEvent) { - const input = event.target as HTMLInputElement - const newName = this.newName.trim?.() || '' - logger.debug('Checking input validity', { newName }) - try { - this.isFileNameValid(newName) - input.setCustomValidity('') - input.title = '' - } catch (e) { - input.setCustomValidity(e.message) - input.title = e.message - } finally { - input.reportValidity() - } - }, - isFileNameValid(name) { - const trimmedName = name.trim() - if (trimmedName === '.' || trimmedName === '..') { - throw new Error(t('files', '"{name}" is an invalid file name.', { name })) - } else if (trimmedName.length === 0) { - throw new Error(t('files', 'File name cannot be empty.')) - } else if (trimmedName.indexOf('/') !== -1) { - throw new Error(t('files', '"/" is not allowed inside a file name.')) - } else if (trimmedName.match(OC.config.blacklist_files_regex)) { - throw new Error(t('files', '"{name}" is not an allowed filetype.', { name })) - } else if (this.checkIfNodeExists(name)) { - throw new Error(t('files', '{newName} already exists.', { newName: name })) - } - - const toCheck = trimmedName.split('') - toCheck.forEach(char => { - if (forbiddenCharacters.indexOf(char) !== -1) { - throw new Error(this.t('files', '"{char}" is not allowed inside a file name.', { char })) - } - }) - - return true - }, - checkIfNodeExists(name) { - return this.nodes.find(node => node.basename === name && node !== this.source) - }, - - startRenaming() { - this.$nextTick(() => { - // Using split to get the true string length - const extLength = (this.source.extension || '').split('').length - const length = this.source.basename.split('').length - extLength - const input = this.$refs.renameInput?.$refs?.inputField?.$refs?.input - if (!input) { - logger.error('Could not find the rename input') - return - } - input.setSelectionRange(0, length) - input.focus() - - // Trigger a keyup event to update the input validity - input.dispatchEvent(new Event('keyup')) - }) - }, - stopRenaming() { - if (!this.isRenaming) { - return - } - - // Reset the renaming store - this.renamingStore.$reset() - }, - - // Rename and move the file - async onRename() { - const oldName = this.source.basename - const oldEncodedSource = this.source.encodedSource - const newName = this.newName.trim?.() || '' - if (newName === '') { - showError(t('files', 'Name cannot be empty')) - return - } - - if (oldName === newName) { - this.stopRenaming() - return - } - - // Checking if already exists - if (this.checkIfNodeExists(newName)) { - showError(t('files', 'Another entry with the same name already exists')) - return - } - - // Set loading state - this.loading = 'renaming' - Vue.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, - }, - }) - - // Success 🎉 - emit('files:node:updated', this.source) - emit('files:node:renamed', this.source) - showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName })) - - // Reset the renaming store - this.stopRenaming() - this.$nextTick(() => { - this.$refs.basename.focus() - }) - } catch (error) { - logger.error('Error while renaming file', { error }) - this.source.rename(oldName) - this.$refs.renameInput.focus() - - // 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.currentDir })) - return - } - - // Unknown error - showError(t('files', 'Could not rename "{oldName}"', { oldName })) - } finally { - this.loading = false - Vue.set(this.source, 'status', undefined) - } - }, - // Open the actions menu on right click onRightClick(event) { // If already opened, fallback to default browser @@ -621,8 +387,8 @@ export default Vue.extend({ event.stopPropagation() }, - execDefaultAction() { - this.$refs.actions.execDefaultAction() + execDefaultAction(...args) { + this.$refs.actions.execDefaultAction(...args) }, openDetailsIfAvailable(event) { |