mirror of
https://github.com/nextcloud/server.git
synced 2026-07-11 10:36:08 -04:00
Unit tests are AI assisted. Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
176 lines
4.3 KiB
Vue
176 lines
4.3 KiB
Vue
<!--
|
|
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
-->
|
|
<template>
|
|
<NcDialog data-cy-files-new-node-dialog
|
|
:name="name"
|
|
:open="open"
|
|
close-on-click-outside
|
|
out-transition
|
|
@update:open="emit('close', null)">
|
|
<template #actions>
|
|
<NcButton data-cy-files-new-node-dialog-submit
|
|
type="primary"
|
|
:disabled="validity !== ''"
|
|
@click="submit">
|
|
{{ t('files', 'Create') }}
|
|
</NcButton>
|
|
</template>
|
|
<form ref="formElement"
|
|
class="new-node-dialog__form"
|
|
@submit.prevent="emit('close', localDefaultName)">
|
|
<NcTextField ref="nameInput"
|
|
data-cy-files-new-node-dialog-input
|
|
:error="validity !== ''"
|
|
:helper-text="validity"
|
|
:label="label"
|
|
:value.sync="localDefaultName" />
|
|
|
|
<!-- Hidden file warning -->
|
|
<NcNoteCard v-if="isHiddenFileName"
|
|
type="warning"
|
|
:text="t('files', 'Files starting with a dot are hidden by default')" />
|
|
</form>
|
|
</NcDialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { ComponentPublicInstance, PropType } from 'vue'
|
|
import { getUniqueName } from '@nextcloud/files'
|
|
import { t } from '@nextcloud/l10n'
|
|
import { extname } from 'path'
|
|
import { computed, nextTick, onMounted, ref, watch, watchEffect } from 'vue'
|
|
import { getFilenameValidity } from '../utils/filenameValidity.ts'
|
|
|
|
import NcButton from '@nextcloud/vue/components/NcButton'
|
|
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
|
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
|
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
|
|
|
const props = defineProps({
|
|
/**
|
|
* The name to be used by default
|
|
*/
|
|
defaultName: {
|
|
type: String,
|
|
default: t('files', 'New folder'),
|
|
},
|
|
/**
|
|
* Other files that are in the current directory
|
|
*/
|
|
otherNames: {
|
|
type: Array as PropType<string[]>,
|
|
default: () => [],
|
|
},
|
|
/**
|
|
* Open state of the dialog
|
|
*/
|
|
open: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
/**
|
|
* Dialog name
|
|
*/
|
|
name: {
|
|
type: String,
|
|
default: t('files', 'Create new folder'),
|
|
},
|
|
/**
|
|
* Input label
|
|
*/
|
|
label: {
|
|
type: String,
|
|
default: t('files', 'Folder name'),
|
|
},
|
|
|
|
/**
|
|
* Whether the name is for a folder, which affects the validation of the name. Defaults to false.
|
|
*/
|
|
isFolder: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'close', name: string | null): void
|
|
}>()
|
|
|
|
const localDefaultName = ref<string>(props.defaultName)
|
|
const nameInput = ref<ComponentPublicInstance>()
|
|
const formElement = ref<HTMLFormElement>()
|
|
const validity = ref('')
|
|
|
|
const isHiddenFileName = computed(() => {
|
|
// Check if the name starts with a dot, which indicates a hidden file
|
|
return localDefaultName.value.trim().startsWith('.')
|
|
})
|
|
|
|
/**
|
|
* Focus the filename input field
|
|
*/
|
|
function focusInput() {
|
|
nextTick(() => {
|
|
// get the input element
|
|
const input = nameInput.value?.$el.querySelector('input')
|
|
if (!props.open || !input) {
|
|
return
|
|
}
|
|
|
|
// length of the basename
|
|
const length = localDefaultName.value.length - extname(localDefaultName.value).length
|
|
// focus the input
|
|
input.focus()
|
|
// and set the selection to the basename (name without extension)
|
|
input.setSelectionRange(0, length)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Trigger submit on the form
|
|
*/
|
|
function submit() {
|
|
formElement.value?.requestSubmit()
|
|
}
|
|
|
|
// Reset local name on props change
|
|
watch(() => [props.defaultName, props.otherNames], () => {
|
|
localDefaultName.value = getUniqueName(props.defaultName, props.otherNames).trim()
|
|
})
|
|
|
|
// Validate the local name
|
|
watchEffect(() => {
|
|
if (props.otherNames.includes(localDefaultName.value.trim())) {
|
|
validity.value = t('files', 'This name is already in use.')
|
|
} else {
|
|
validity.value = getFilenameValidity(localDefaultName.value.trim(), false, props.isFolder)
|
|
}
|
|
const input = nameInput.value?.$el.querySelector('input')
|
|
if (input) {
|
|
input.setCustomValidity(validity.value)
|
|
input.reportValidity()
|
|
}
|
|
})
|
|
|
|
// Ensure the input is focussed even if the dialog is already mounted but not open
|
|
watch(() => props.open, () => {
|
|
nextTick(() => {
|
|
focusInput()
|
|
})
|
|
})
|
|
|
|
onMounted(() => {
|
|
// on mounted lets use the unique name
|
|
localDefaultName.value = getUniqueName(localDefaultName.value, props.otherNames).trim()
|
|
nextTick(() => focusInput())
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.new-node-dialog__form {
|
|
/* Ensure the dialog does not jump when there is a validity error */
|
|
min-height: calc(2 * var(--default-clickable-area));
|
|
}
|
|
</style>
|