Skip to content

Commit 87dc061

Browse files
authored
Merge pull request #46767 from nextcloud/fix/files-new-file-node
2 parents 40c72d3 + dd88fa0 commit 87dc061

7 files changed

Lines changed: 198 additions & 176 deletions

File tree

apps/files/src/components/FileEntry/FileEntryName.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
class="files-list__row-name-link"
2828
data-cy-files-list-row-name-link
2929
v-bind="linkTo.params">
30-
<!-- File name -->
30+
<!-- Filename -->
3131
<span class="files-list__row-name-text">
3232
<!-- Keep the filename stuck to the extension to avoid whitespace rendering issues-->
3333
<span class="files-list__row-name-" v-text="basename" />
@@ -123,7 +123,7 @@ export default defineComponent({
123123
124124
renameLabel() {
125125
const matchLabel: Record<FileType, string> = {
126-
[FileType.File]: t('files', 'File name'),
126+
[FileType.File]: t('files', 'Filename'),
127127
[FileType.Folder]: t('files', 'Folder name'),
128128
}
129129
return matchLabel[this.source.type]
@@ -173,7 +173,7 @@ export default defineComponent({
173173
174174
watch: {
175175
/**
176-
* If renaming starts, select the file name
176+
* If renaming starts, select the filename
177177
* in the input, without the extension.
178178
* @param renaming
179179
*/

apps/files/src/components/NewNodeDialog.vue

Lines changed: 112 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -8,205 +8,150 @@
88
:open="open"
99
close-on-click-outside
1010
out-transition
11-
@update:open="onClose">
11+
@update:open="emit('close', null)">
1212
<template #actions>
1313
<NcButton data-cy-files-new-node-dialog-submit
1414
type="primary"
15-
:disabled="!isUniqueName"
16-
@click="onCreate">
15+
:disabled="validity !== ''"
16+
@click="submit">
1717
{{ t('files', 'Create') }}
1818
</NcButton>
1919
</template>
20-
<form @submit.prevent="onCreate">
21-
<NcTextField ref="input"
20+
<form ref="formElement"
21+
class="new-node-dialog__form"
22+
@submit.prevent="emit('close', localDefaultName)">
23+
<NcTextField ref="nameInput"
2224
data-cy-files-new-node-dialog-input
23-
class="dialog__input"
24-
:error="!isUniqueName"
25-
:helper-text="errorMessage"
25+
:error="validity !== ''"
26+
:helper-text="validity"
2627
:label="label"
27-
:value.sync="localDefaultName"
28-
@keyup="checkInputValidity" />
28+
:value.sync="localDefaultName" />
2929
</form>
3030
</NcDialog>
3131
</template>
3232

33-
<script lang="ts">
34-
import type { PropType } from 'vue'
35-
36-
import { defineComponent } from 'vue'
37-
import { translate as t } from '@nextcloud/l10n'
33+
<script setup lang="ts">
34+
import type { ComponentPublicInstance, PropType } from 'vue'
3835
import { getUniqueName } from '@nextcloud/files'
39-
import { loadState } from '@nextcloud/initial-state'
36+
import { t } from '@nextcloud/l10n'
37+
import { extname } from 'path'
38+
import { nextTick, onMounted, ref, watch, watchEffect } from 'vue'
39+
import { getFilenameValidity } from '../utils/filenameValidity.ts'
4040
4141
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
4242
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
4343
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
44-
import logger from '../logger.js'
45-
46-
interface ICanFocus {
47-
focus: () => void
48-
}
4944
50-
const forbiddenCharacters = loadState<string[]>('files', 'forbiddenCharacters', [])
51-
52-
export default defineComponent({
53-
name: 'NewNodeDialog',
54-
components: {
55-
NcButton,
56-
NcDialog,
57-
NcTextField,
45+
const props = defineProps({
46+
/**
47+
* The name to be used by default
48+
*/
49+
defaultName: {
50+
type: String,
51+
default: t('files', 'New folder'),
5852
},
59-
props: {
60-
/**
61-
* The name to be used by default
62-
*/
63-
defaultName: {
64-
type: String,
65-
default: t('files', 'New folder'),
66-
},
67-
/**
68-
* Other files that are in the current directory
69-
*/
70-
otherNames: {
71-
type: Array as PropType<string[]>,
72-
default: () => [],
73-
},
74-
/**
75-
* Open state of the dialog
76-
*/
77-
open: {
78-
type: Boolean,
79-
default: true,
80-
},
81-
/**
82-
* Dialog name
83-
*/
84-
name: {
85-
type: String,
86-
default: t('files', 'Create new folder'),
87-
},
88-
/**
89-
* Input label
90-
*/
91-
label: {
92-
type: String,
93-
default: t('files', 'Folder name'),
94-
},
53+
/**
54+
* Other files that are in the current directory
55+
*/
56+
otherNames: {
57+
type: Array as PropType<string[]>,
58+
default: () => [],
9559
},
96-
emits: {
97-
close: (name: string|null) => name === null || name,
60+
/**
61+
* Open state of the dialog
62+
*/
63+
open: {
64+
type: Boolean,
65+
default: true,
9866
},
99-
data() {
100-
return {
101-
localDefaultName: this.defaultName || t('files', 'New folder'),
102-
}
67+
/**
68+
* Dialog name
69+
*/
70+
name: {
71+
type: String,
72+
default: t('files', 'Create new folder'),
10373
},
104-
computed: {
105-
errorMessage() {
106-
if (this.isUniqueName) {
107-
return ''
108-
} else {
109-
return t('files', 'A file or folder with that name already exists.')
110-
}
111-
},
112-
uniqueName() {
113-
return getUniqueName(this.localDefaultName, this.otherNames)
114-
},
115-
isUniqueName() {
116-
return this.localDefaultName === this.uniqueName
117-
},
74+
/**
75+
* Input label
76+
*/
77+
label: {
78+
type: String,
79+
default: t('files', 'Folder name'),
11880
},
119-
watch: {
120-
defaultName() {
121-
this.localDefaultName = this.defaultName || t('files', 'New folder')
122-
},
81+
})
12382
124-
/**
125-
* Ensure the input is focussed even if the dialog is already mounted but not open
126-
*/
127-
open() {
128-
this.$nextTick(() => this.focusInput())
129-
},
130-
},
131-
mounted() {
132-
// on mounted lets use the unique name
133-
this.localDefaultName = this.uniqueName
134-
this.$nextTick(() => this.focusInput())
135-
},
136-
methods: {
137-
t,
83+
const emit = defineEmits<{
84+
(event: 'close', name: string | null): void
85+
}>()
86+
87+
const localDefaultName = ref<string>(props.defaultName)
88+
const nameInput = ref<ComponentPublicInstance>()
89+
const formElement = ref<HTMLFormElement>()
90+
const validity = ref('')
91+
92+
/**
93+
* Focus the filename input field
94+
*/
95+
function focusInput() {
96+
nextTick(() => {
97+
// get the input element
98+
const input = nameInput.value?.$el.querySelector('input')
99+
if (!props.open || !input) {
100+
return
101+
}
138102
139-
/**
140-
* Focus the filename input field
141-
*/
142-
focusInput() {
143-
if (this.open) {
144-
this.$nextTick(() => (this.$refs.input as unknown as ICanFocus)?.focus?.())
145-
}
146-
},
103+
// length of the basename
104+
const length = localDefaultName.value.length - extname(localDefaultName.value).length
105+
// focus the input
106+
input.focus()
107+
// and set the selection to the basename (name without extension)
108+
input.setSelectionRange(0, length)
109+
})
110+
}
147111
148-
onCreate() {
149-
this.$emit('close', this.localDefaultName)
150-
},
151-
onClose(state: boolean) {
152-
if (!state) {
153-
this.$emit('close', null)
154-
}
155-
},
112+
/**
113+
* Trigger submit on the form
114+
*/
115+
function submit() {
116+
formElement.value?.requestSubmit()
117+
}
156118
157-
/**
158-
* Check if the file name is valid and update the
159-
* input validity using browser's native validation.
160-
* @param event the keyup event
161-
*/
162-
checkInputValidity(event: KeyboardEvent) {
163-
const input = event.target as HTMLInputElement
164-
const newName = this.localDefaultName.trim?.() || ''
165-
logger.debug('Checking input validity', { newName })
166-
try {
167-
this.isFileNameValid(newName)
168-
input.setCustomValidity('')
169-
input.title = ''
170-
} catch (e) {
171-
if (e instanceof Error) {
172-
input.setCustomValidity(e.message)
173-
input.title = e.message
174-
} else {
175-
input.setCustomValidity(t('files', 'Invalid file name'))
176-
}
177-
} finally {
178-
input.reportValidity()
179-
}
180-
},
119+
// Reset local name on props change
120+
watch(() => props.defaultName, () => {
121+
localDefaultName.value = getUniqueName(props.defaultName, props.otherNames)
122+
})
181123
182-
isFileNameValid(name: string) {
183-
const trimmedName = name.trim()
184-
const char = trimmedName.indexOf('/') !== -1
185-
? '/'
186-
: forbiddenCharacters.find((char) => trimmedName.includes(char))
124+
// Validate the local name
125+
watchEffect(() => {
126+
if (props.otherNames.includes(localDefaultName.value)) {
127+
validity.value = t('files', 'This name is already in use.')
128+
} else {
129+
validity.value = getFilenameValidity(localDefaultName.value)
130+
}
131+
const input = nameInput.value?.$el.querySelector('input')
132+
if (input) {
133+
input.setCustomValidity(validity.value)
134+
input.reportValidity()
135+
}
136+
})
187137
188-
if (trimmedName === '.' || trimmedName === '..') {
189-
throw new Error(t('files', '"{name}" is an invalid file name.', { name }))
190-
} else if (trimmedName.length === 0) {
191-
throw new Error(t('files', 'File name cannot be empty.'))
192-
} else if (char) {
193-
throw new Error(t('files', '"{char}" is not allowed inside a file name.', { char }))
194-
} else if (trimmedName.match(window.OC.config.blacklist_files_regex)) {
195-
throw new Error(t('files', '"{name}" is not an allowed filetype.', { name }))
196-
}
138+
// Ensure the input is focussed even if the dialog is already mounted but not open
139+
watch(() => props.open, () => {
140+
nextTick(() => {
141+
focusInput()
142+
})
143+
})
197144
198-
return true
199-
},
200-
},
145+
onMounted(() => {
146+
// on mounted lets use the unique name
147+
localDefaultName.value = getUniqueName(localDefaultName.value, props.otherNames)
148+
nextTick(() => focusInput())
201149
})
202150
</script>
203151

204-
<style lang="scss" scoped>
205-
.dialog__input {
206-
:deep(input:invalid) {
207-
// Show red border on invalid input
208-
border-color: var(--color-error);
209-
color: red;
210-
}
152+
<style scoped>
153+
.new-node-dialog__form {
154+
/* Ensure the dialog does not jump when there is a validity error */
155+
min-height: calc(3 * var(--default-clickable-area));
211156
}
212157
</style>

0 commit comments

Comments
 (0)