2023-10-13 05:30:34 -04:00
|
|
|
<!--
|
2024-05-28 10:42:42 -04:00
|
|
|
- SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
|
|
|
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
-->
|
2023-10-13 05:30:34 -04:00
|
|
|
<template>
|
|
|
|
|
<!-- Rename input -->
|
|
|
|
|
<form v-if="isRenaming"
|
2024-07-25 08:29:31 -04:00
|
|
|
ref="renameForm"
|
2024-04-14 04:14:58 -04:00
|
|
|
v-on-click-outside="onRename"
|
2023-10-13 05:30:34 -04:00
|
|
|
: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.esc="stopRenaming" />
|
|
|
|
|
</form>
|
|
|
|
|
|
2023-11-20 09:03:15 -05:00
|
|
|
<component :is="linkTo.is"
|
|
|
|
|
v-else
|
2023-10-13 05:30:34 -04:00
|
|
|
ref="basename"
|
|
|
|
|
class="files-list__row-name-link"
|
|
|
|
|
data-cy-files-list-row-name-link
|
2024-11-11 13:56:07 -05:00
|
|
|
v-bind="linkTo.params">
|
2024-07-25 16:51:12 -04:00
|
|
|
<!-- Filename -->
|
2024-11-11 13:56:07 -05:00
|
|
|
<span class="files-list__row-name-text" dir="auto">
|
2024-07-22 11:54:54 -04:00
|
|
|
<!-- Keep the filename stuck to the extension to avoid whitespace rendering issues-->
|
|
|
|
|
<span class="files-list__row-name-" v-text="basename" />
|
2025-07-16 02:56:00 -04:00
|
|
|
<span v-if="userConfigStore.userConfig.show_files_extensions" class="files-list__row-name-ext" v-text="extension" />
|
2023-10-13 05:30:34 -04:00
|
|
|
</span>
|
2023-11-20 09:03:15 -05:00
|
|
|
</component>
|
2023-10-13 05:30:34 -04:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts">
|
2024-07-25 19:42:31 -04:00
|
|
|
import type { FileAction, Node } from '@nextcloud/files'
|
2023-12-13 10:28:07 -05:00
|
|
|
import type { PropType } from 'vue'
|
|
|
|
|
|
2024-04-29 12:22:00 -04:00
|
|
|
import { showError, showSuccess } from '@nextcloud/dialogs'
|
2024-07-25 19:42:31 -04:00
|
|
|
import { FileType, NodeStatus } from '@nextcloud/files'
|
2023-10-13 05:30:34 -04:00
|
|
|
import { translate as t } from '@nextcloud/l10n'
|
2024-07-25 19:42:31 -04:00
|
|
|
import { defineComponent, inject } from 'vue'
|
2023-10-13 05:30:34 -04:00
|
|
|
|
|
|
|
|
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
|
|
|
|
|
2025-07-16 02:56:00 -04:00
|
|
|
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
|
2024-11-14 19:51:28 -05:00
|
|
|
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
|
2025-07-16 02:56:00 -04:00
|
|
|
import { useNavigation } from '../../composables/useNavigation.ts'
|
2023-10-13 05:30:34 -04:00
|
|
|
import { useRenamingStore } from '../../store/renaming.ts'
|
2025-07-16 02:56:00 -04:00
|
|
|
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
|
|
|
|
|
import { useUserConfigStore } from '../../store/userconfig.ts'
|
2024-06-10 15:21:26 -04:00
|
|
|
import logger from '../../logger.ts'
|
2023-10-13 05:30:34 -04:00
|
|
|
|
2024-04-29 12:22:00 -04:00
|
|
|
export default defineComponent({
|
2023-10-13 05:30:34 -04:00
|
|
|
name: 'FileEntryName',
|
|
|
|
|
|
|
|
|
|
components: {
|
|
|
|
|
NcTextField,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
props: {
|
2024-07-22 11:54:54 -04:00
|
|
|
/**
|
|
|
|
|
* The filename without extension
|
|
|
|
|
*/
|
|
|
|
|
basename: {
|
2023-10-13 05:30:34 -04:00
|
|
|
type: String,
|
|
|
|
|
required: true,
|
|
|
|
|
},
|
2024-07-22 11:54:54 -04:00
|
|
|
/**
|
|
|
|
|
* The extension of the filename
|
|
|
|
|
*/
|
2023-10-13 05:30:34 -04:00
|
|
|
extension: {
|
|
|
|
|
type: String,
|
|
|
|
|
required: true,
|
|
|
|
|
},
|
|
|
|
|
nodes: {
|
|
|
|
|
type: Array as PropType<Node[]>,
|
|
|
|
|
required: true,
|
|
|
|
|
},
|
|
|
|
|
source: {
|
|
|
|
|
type: Object as PropType<Node>,
|
|
|
|
|
required: true,
|
|
|
|
|
},
|
2023-10-13 10:49:54 -04:00
|
|
|
gridMode: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false,
|
|
|
|
|
},
|
2023-10-13 05:30:34 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setup() {
|
2024-11-14 17:24:47 -05:00
|
|
|
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
|
|
|
|
|
const { currentView } = useNavigation(true)
|
2024-07-25 19:43:52 -04:00
|
|
|
const { directory } = useRouteParameters()
|
2024-11-14 19:51:28 -05:00
|
|
|
const filesListWidth = useFileListWidth()
|
2023-10-13 05:30:34 -04:00
|
|
|
const renamingStore = useRenamingStore()
|
2025-07-16 02:56:00 -04:00
|
|
|
const userConfigStore = useUserConfigStore()
|
2024-06-21 09:48:37 -04:00
|
|
|
|
2024-07-25 19:42:31 -04:00
|
|
|
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
|
|
|
|
|
|
2023-10-13 05:30:34 -04:00
|
|
|
return {
|
2024-06-21 09:48:37 -04:00
|
|
|
currentView,
|
2024-07-25 19:42:31 -04:00
|
|
|
defaultFileAction,
|
2024-07-25 19:43:52 -04:00
|
|
|
directory,
|
2024-11-14 19:51:28 -05:00
|
|
|
filesListWidth,
|
2024-06-21 09:48:37 -04:00
|
|
|
|
2023-10-13 05:30:34 -04:00
|
|
|
renamingStore,
|
2025-07-16 02:56:00 -04:00
|
|
|
userConfigStore,
|
2023-10-13 05:30:34 -04:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
computed: {
|
|
|
|
|
isRenaming() {
|
|
|
|
|
return this.renamingStore.renamingNode === this.source
|
|
|
|
|
},
|
|
|
|
|
isRenamingSmallScreen() {
|
|
|
|
|
return this.isRenaming && this.filesListWidth < 512
|
|
|
|
|
},
|
|
|
|
|
newName: {
|
2025-02-22 07:25:18 -05:00
|
|
|
get(): string {
|
|
|
|
|
return this.renamingStore.newNodeName
|
2023-10-13 05:30:34 -04:00
|
|
|
},
|
2025-02-22 07:25:18 -05:00
|
|
|
set(newName: string) {
|
|
|
|
|
this.renamingStore.newNodeName = newName
|
2023-10-13 05:30:34 -04:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renameLabel() {
|
|
|
|
|
const matchLabel: Record<FileType, string> = {
|
2024-07-25 16:51:12 -04:00
|
|
|
[FileType.File]: t('files', 'Filename'),
|
2023-10-13 05:30:34 -04:00
|
|
|
[FileType.Folder]: t('files', 'Folder name'),
|
|
|
|
|
}
|
|
|
|
|
return matchLabel[this.source.type]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
linkTo() {
|
2024-06-13 09:06:12 -04:00
|
|
|
if (this.source.status === NodeStatus.FAILED) {
|
2023-10-13 05:30:34 -04:00
|
|
|
return {
|
|
|
|
|
is: 'span',
|
2023-11-20 09:03:15 -05:00
|
|
|
params: {
|
|
|
|
|
title: t('files', 'This node is unavailable'),
|
|
|
|
|
},
|
2023-10-13 05:30:34 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-14 17:24:47 -05:00
|
|
|
if (this.defaultFileAction) {
|
2024-07-25 19:42:31 -04:00
|
|
|
const displayName = this.defaultFileAction.displayName([this.source], this.currentView)
|
2023-10-13 05:30:34 -04:00
|
|
|
return {
|
2024-07-25 19:42:31 -04:00
|
|
|
is: 'button',
|
2023-11-20 09:03:15 -05:00
|
|
|
params: {
|
2024-07-25 19:42:31 -04:00
|
|
|
'aria-label': displayName,
|
2023-11-20 09:03:15 -05:00
|
|
|
title: displayName,
|
|
|
|
|
tabindex: '0',
|
|
|
|
|
},
|
2023-10-13 05:30:34 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-25 19:42:31 -04:00
|
|
|
// nothing interactive here, there is no default action
|
|
|
|
|
// so if not even the download action works we only can show the list entry
|
2023-10-13 05:30:34 -04:00
|
|
|
return {
|
|
|
|
|
is: 'span',
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
watch: {
|
|
|
|
|
/**
|
2024-07-25 16:51:12 -04:00
|
|
|
* If renaming starts, select the filename
|
2023-10-13 05:30:34 -04:00
|
|
|
* in the input, without the extension.
|
|
|
|
|
* @param renaming
|
|
|
|
|
*/
|
2023-12-21 20:31:17 -05:00
|
|
|
isRenaming: {
|
|
|
|
|
immediate: true,
|
|
|
|
|
handler(renaming: boolean) {
|
|
|
|
|
if (renaming) {
|
|
|
|
|
this.startRenaming()
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-10-13 05:30:34 -04:00
|
|
|
},
|
|
|
|
|
|
2024-07-25 08:29:31 -04:00
|
|
|
newName() {
|
|
|
|
|
// Check validity of the new name
|
2023-10-13 05:30:34 -04:00
|
|
|
const newName = this.newName.trim?.() || ''
|
2024-07-25 08:29:31 -04:00
|
|
|
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
|
|
|
|
|
if (!input) {
|
|
|
|
|
return
|
2023-10-13 05:30:34 -04:00
|
|
|
}
|
2024-07-22 12:02:41 -04:00
|
|
|
|
2024-07-25 08:29:31 -04:00
|
|
|
let validity = getFilenameValidity(newName)
|
|
|
|
|
// Checking if already exists
|
|
|
|
|
if (validity === '' && this.checkIfNodeExists(newName)) {
|
|
|
|
|
validity = t('files', 'Another entry with the same name already exists.')
|
2023-10-13 05:30:34 -04:00
|
|
|
}
|
2024-07-25 08:29:31 -04:00
|
|
|
this.$nextTick(() => {
|
|
|
|
|
if (this.isRenaming) {
|
|
|
|
|
input.setCustomValidity(validity)
|
|
|
|
|
input.reportValidity()
|
|
|
|
|
}
|
|
|
|
|
})
|
2023-10-13 05:30:34 -04:00
|
|
|
},
|
2024-07-25 08:29:31 -04:00
|
|
|
},
|
2024-04-29 12:22:00 -04:00
|
|
|
|
2024-07-25 08:29:31 -04:00
|
|
|
methods: {
|
2024-04-29 12:22:00 -04:00
|
|
|
checkIfNodeExists(name: string) {
|
2023-10-13 05:30:34 -04:00
|
|
|
return this.nodes.find(node => node.basename === name && node !== this.source)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
startRenaming() {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
// Using split to get the true string length
|
2024-07-25 08:29:31 -04:00
|
|
|
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
|
2023-10-13 05:30:34 -04:00
|
|
|
if (!input) {
|
|
|
|
|
logger.error('Could not find the rename input')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
input.focus()
|
2024-07-25 08:29:31 -04:00
|
|
|
const length = this.source.basename.length - (this.source.extension ?? '').length
|
|
|
|
|
input.setSelectionRange(0, length)
|
2023-10-13 05:30:34 -04:00
|
|
|
|
|
|
|
|
// Trigger a keyup event to update the input validity
|
|
|
|
|
input.dispatchEvent(new Event('keyup'))
|
|
|
|
|
})
|
|
|
|
|
},
|
2024-07-25 08:29:31 -04:00
|
|
|
|
2023-10-13 05:30:34 -04:00
|
|
|
stopRenaming() {
|
|
|
|
|
if (!this.isRenaming) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset the renaming store
|
|
|
|
|
this.renamingStore.$reset()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Rename and move the file
|
|
|
|
|
async onRename() {
|
|
|
|
|
const newName = this.newName.trim?.() || ''
|
2024-07-25 08:29:31 -04:00
|
|
|
const form = this.$refs.renameForm as HTMLFormElement
|
|
|
|
|
if (!form.checkValidity()) {
|
|
|
|
|
showError(t('files', 'Invalid filename.') + ' ' + getFilenameValidity(newName))
|
2023-10-13 05:30:34 -04:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-25 08:29:31 -04:00
|
|
|
const oldName = this.source.basename
|
2024-12-06 10:09:57 -05:00
|
|
|
if (newName === oldName) {
|
|
|
|
|
this.stopRenaming()
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-10-13 05:30:34 -04:00
|
|
|
|
|
|
|
|
try {
|
2024-08-22 15:49:06 -04:00
|
|
|
const status = await this.renamingStore.rename()
|
|
|
|
|
if (status) {
|
2025-02-22 07:25:18 -05:00
|
|
|
showSuccess(
|
|
|
|
|
t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName: this.source.basename }),
|
|
|
|
|
)
|
2024-08-22 15:49:06 -04:00
|
|
|
this.$nextTick(() => {
|
2024-11-14 17:24:47 -05:00
|
|
|
const nameContainer = this.$refs.basename as HTMLElement | undefined
|
|
|
|
|
nameContainer?.focus()
|
2024-08-22 15:49:06 -04:00
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Was cancelled - meaning the renaming state is just reset
|
|
|
|
|
}
|
2023-10-13 05:30:34 -04:00
|
|
|
} catch (error) {
|
2024-08-22 15:49:06 -04:00
|
|
|
logger.error(error as Error)
|
|
|
|
|
showError((error as Error).message)
|
2024-07-25 19:42:31 -04:00
|
|
|
// And ensure we reset to the renaming state
|
|
|
|
|
this.startRenaming()
|
2023-10-13 05:30:34 -04:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
t,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
</script>
|
2024-07-25 19:42:31 -04:00
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
button.files-list__row-name-link {
|
|
|
|
|
background-color: unset;
|
|
|
|
|
border: none;
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
// No active styles - handled by the row entry
|
|
|
|
|
background-color: unset !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|