mirror of https://github.com/nextcloud/server.git
enh(files): Add modal to set filename before creating new files in the fileslist
* Reactive `openfile` query Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>pull/42993/head
parent
e62c5d719d
commit
8be4704e11
@ -0,0 +1,149 @@
|
||||
<!--
|
||||
- @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<NcDialog :name="name"
|
||||
:open="open"
|
||||
close-on-click-outside
|
||||
out-transition
|
||||
@update:open="onClose">
|
||||
<template #actions>
|
||||
<NcButton type="primary"
|
||||
:disabled="!isUniqueName"
|
||||
@click="onCreate">
|
||||
{{ t('files', 'Create') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
<form @submit.prevent="onCreate">
|
||||
<NcTextField ref="input"
|
||||
:error="!isUniqueName"
|
||||
:helper-text="errorMessage"
|
||||
:label="label"
|
||||
:value.sync="localDefaultName" />
|
||||
</form>
|
||||
</NcDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { getUniqueName } from '../utils/fileUtils'
|
||||
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
|
||||
interface ICanFocus {
|
||||
focus: () => void
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewNodeDialog',
|
||||
components: {
|
||||
NcButton,
|
||||
NcDialog,
|
||||
NcTextField,
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 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'),
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
close: (name: string|null) => name === null || name,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localDefaultName: this.defaultName || t('files', 'New folder'),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
errorMessage() {
|
||||
if (this.isUniqueName) {
|
||||
return ''
|
||||
} else {
|
||||
return t('files', 'A file or folder with that name already exists.')
|
||||
}
|
||||
},
|
||||
uniqueName() {
|
||||
return getUniqueName(this.localDefaultName, this.otherNames)
|
||||
},
|
||||
isUniqueName() {
|
||||
return this.localDefaultName === this.uniqueName
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
defaultName() {
|
||||
this.localDefaultName = this.defaultName || t('files', 'New folder')
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// on mounted lets use the unique name
|
||||
this.localDefaultName = this.uniqueName
|
||||
this.$nextTick(() => (this.$refs.input as unknown as ICanFocus)?.focus?.())
|
||||
},
|
||||
methods: {
|
||||
t,
|
||||
onCreate() {
|
||||
this.$emit('close', this.localDefaultName)
|
||||
},
|
||||
onClose(state: boolean) {
|
||||
if (!state) {
|
||||
this.$emit('close', null)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
@ -1,149 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import type { Entry } from '@nextcloud/files'
|
||||
import type { TemplateFile } from './types'
|
||||
|
||||
import { Folder, Node, Permission, addNewFileMenuEntry, removeNewFileMenuEntry } from '@nextcloud/files'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
import { join } from 'path'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
import Vue from 'vue'
|
||||
|
||||
import PlusSvg from '@mdi/svg/svg/plus.svg?raw'
|
||||
|
||||
import TemplatePickerView from './views/TemplatePicker.vue'
|
||||
import { getUniqueName } from './utils/fileUtils.ts'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
// Set up logger
|
||||
const logger = getLoggerBuilder()
|
||||
.setApp('files')
|
||||
.detectUser()
|
||||
.build()
|
||||
|
||||
// Add translates functions
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
t,
|
||||
n,
|
||||
},
|
||||
})
|
||||
|
||||
// Create document root
|
||||
const TemplatePickerRoot = document.createElement('div')
|
||||
TemplatePickerRoot.id = 'template-picker'
|
||||
document.body.appendChild(TemplatePickerRoot)
|
||||
|
||||
// Retrieve and init templates
|
||||
let templates = loadState<TemplateFile[]>('files', 'templates', [])
|
||||
let templatesPath = loadState('files', 'templates_path', false)
|
||||
logger.debug('Templates providers', { templates })
|
||||
logger.debug('Templates folder', { templatesPath })
|
||||
|
||||
// Init vue app
|
||||
const View = Vue.extend(TemplatePickerView)
|
||||
const TemplatePicker = new View({
|
||||
name: 'TemplatePicker',
|
||||
propsData: {
|
||||
logger,
|
||||
},
|
||||
})
|
||||
TemplatePicker.$mount('#template-picker')
|
||||
if (!templatesPath) {
|
||||
logger.debug('Templates folder not initialized')
|
||||
addNewFileMenuEntry({
|
||||
id: 'template-picker',
|
||||
displayName: t('files', 'Create new templates folder'),
|
||||
iconSvgInline: PlusSvg,
|
||||
order: 10,
|
||||
enabled(context: Folder): boolean {
|
||||
// Allow creation on your own folders only
|
||||
if (context.owner !== getCurrentUser()?.uid) {
|
||||
return false
|
||||
}
|
||||
return (context.permissions & Permission.CREATE) !== 0
|
||||
},
|
||||
handler(context: Folder, content: Node[]) {
|
||||
// Check for conflicts
|
||||
const contentNames = content.map((node: Node) => node.basename)
|
||||
const name = getUniqueName(t('files', 'Templates'), contentNames)
|
||||
|
||||
// Create the template folder
|
||||
initTemplatesFolder(context, name)
|
||||
|
||||
// Remove the menu entry
|
||||
removeNewFileMenuEntry('template-picker')
|
||||
},
|
||||
} as Entry)
|
||||
}
|
||||
|
||||
// Init template files menu
|
||||
templates.forEach((provider, index) => {
|
||||
addNewFileMenuEntry({
|
||||
id: `template-new-${provider.app}-${index}`,
|
||||
displayName: provider.label,
|
||||
// TODO: migrate to inline svg
|
||||
iconClass: provider.iconClass || 'icon-file',
|
||||
enabled(context: Folder): boolean {
|
||||
return (context.permissions & Permission.CREATE) !== 0
|
||||
},
|
||||
order: 11,
|
||||
handler(context: Folder, content: Node[]) {
|
||||
// Check for conflicts
|
||||
const contentNames = content.map((node: Node) => node.basename)
|
||||
const name = getUniqueName(provider.label + provider.extension, contentNames)
|
||||
|
||||
// Create the file
|
||||
TemplatePicker.open(name, provider)
|
||||
},
|
||||
} as Entry)
|
||||
})
|
||||
|
||||
// Init template folder
|
||||
const initTemplatesFolder = async function(directory: Folder, name: string) {
|
||||
const templatePath = join(directory.path, name)
|
||||
try {
|
||||
logger.debug('Initializing the templates directory', { templatePath })
|
||||
const response = await axios.post(generateOcsUrl('apps/files/api/v1/templates/path'), {
|
||||
templatePath,
|
||||
copySystemTemplates: true,
|
||||
})
|
||||
|
||||
// Go to template directory
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files', fileid: undefined },
|
||||
{ dir: templatePath },
|
||||
)
|
||||
|
||||
templates = response.data.ocs.data.templates
|
||||
templatesPath = response.data.ocs.data.template_path
|
||||
} catch (error) {
|
||||
logger.error('Unable to initialize the templates directory')
|
||||
showError(t('files', 'Unable to initialize the templates directory'))
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { Entry } from '@nextcloud/files'
|
||||
import type { ComponentInstance } from 'vue'
|
||||
import type { TemplateFile } from '../types.ts'
|
||||
|
||||
import { Folder, Node, Permission, addNewFileMenuEntry } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { newNodeName } from '../utils/newNodeDialog'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import Vue, { defineAsyncComponent } from 'vue'
|
||||
|
||||
// async to reduce bundle size
|
||||
const TemplatePickerVue = defineAsyncComponent(() => import('../views/TemplatePicker.vue'))
|
||||
let TemplatePicker: ComponentInstance & { open: (n: string, t: TemplateFile) => void } | null = null
|
||||
|
||||
const getTemplatePicker = async () => {
|
||||
if (TemplatePicker === null) {
|
||||
// Create document root
|
||||
const mountingPoint = document.createElement('div')
|
||||
mountingPoint.id = 'template-picker'
|
||||
document.body.appendChild(mountingPoint)
|
||||
|
||||
// Init vue app
|
||||
TemplatePicker = new Vue({
|
||||
render: (h) => h(TemplatePickerVue, { ref: 'picker' }),
|
||||
methods: { open(...args) { this.$refs.picker.open(...args) } },
|
||||
el: mountingPoint,
|
||||
})
|
||||
}
|
||||
return TemplatePicker
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all new-file-menu entries for all template providers
|
||||
*/
|
||||
export function registerTemplateEntries() {
|
||||
const templates = loadState<TemplateFile[]>('files', 'templates', [])
|
||||
|
||||
// Init template files menu
|
||||
templates.forEach((provider, index) => {
|
||||
addNewFileMenuEntry({
|
||||
id: `template-new-${provider.app}-${index}`,
|
||||
displayName: provider.label,
|
||||
// TODO: migrate to inline svg
|
||||
iconClass: provider.iconClass || 'icon-file',
|
||||
enabled(context: Folder): boolean {
|
||||
return (context.permissions & Permission.CREATE) !== 0
|
||||
},
|
||||
order: 11,
|
||||
async handler(context: Folder, content: Node[]) {
|
||||
const templatePicker = getTemplatePicker()
|
||||
const name = await newNodeName(`${provider.label}${provider.extension}`, content, {
|
||||
label: t('files', 'Filename'),
|
||||
name: provider.label,
|
||||
})
|
||||
|
||||
if (name !== null) {
|
||||
// Create the file
|
||||
const picker = await templatePicker
|
||||
picker.open(name, provider)
|
||||
}
|
||||
},
|
||||
} as Entry)
|
||||
})
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import type { Entry, Folder, Node } from '@nextcloud/files'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { Permission, removeNewFileMenuEntry } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { join } from 'path'
|
||||
import { newNodeName } from '../utils/newNodeDialog'
|
||||
|
||||
import PlusSvg from '@mdi/svg/svg/plus.svg?raw'
|
||||
import axios from '@nextcloud/axios'
|
||||
import logger from '../logger.js'
|
||||
|
||||
let templatesPath = loadState<string|false>('files', 'templates_path', false)
|
||||
logger.debug('Initial templates folder', { templatesPath })
|
||||
|
||||
/**
|
||||
* Init template folder
|
||||
* @param directory Folder where to create the templates folder
|
||||
* @param name Name to use or the templates folder
|
||||
*/
|
||||
const initTemplatesFolder = async function(directory: Folder, name: string) {
|
||||
const templatePath = join(directory.path, name)
|
||||
try {
|
||||
logger.debug('Initializing the templates directory', { templatePath })
|
||||
const { data } = await axios.post(generateOcsUrl('apps/files/api/v1/templates/path'), {
|
||||
templatePath,
|
||||
copySystemTemplates: true,
|
||||
})
|
||||
|
||||
// Go to template directory
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files', fileid: undefined },
|
||||
{ dir: templatePath },
|
||||
)
|
||||
|
||||
logger.info('Created new templates folder', {
|
||||
...data.ocs.data,
|
||||
})
|
||||
templatesPath = data.ocs.data.templates_path as string
|
||||
} catch (error) {
|
||||
logger.error('Unable to initialize the templates directory')
|
||||
showError(t('files', 'Unable to initialize the templates directory'))
|
||||
}
|
||||
}
|
||||
|
||||
export const entry = {
|
||||
id: 'template-picker',
|
||||
displayName: t('files', 'Create new templates folder'),
|
||||
iconSvgInline: PlusSvg,
|
||||
order: 10,
|
||||
enabled(context: Folder): boolean {
|
||||
// Templates folder already initialized
|
||||
if (templatesPath) {
|
||||
return false
|
||||
}
|
||||
// Allow creation on your own folders only
|
||||
if (context.owner !== getCurrentUser()?.uid) {
|
||||
return false
|
||||
}
|
||||
return (context.permissions & Permission.CREATE) !== 0
|
||||
},
|
||||
async handler(context: Folder, content: Node[]) {
|
||||
const name = await newNodeName(t('files', 'Templates'), content, { name: t('files', 'New template folder') })
|
||||
|
||||
if (name !== null) {
|
||||
// Create the template folder
|
||||
initTemplatesFolder(context, name)
|
||||
|
||||
// Remove the menu entry
|
||||
removeNewFileMenuEntry('template-picker')
|
||||
}
|
||||
},
|
||||
} as Entry
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import { spawnDialog } from '@nextcloud/dialogs'
|
||||
import NewNodeDialog from '../components/NewNodeDialog.vue'
|
||||
|
||||
interface ILabels {
|
||||
/**
|
||||
* Dialog heading, defaults to "New folder name"
|
||||
*/
|
||||
name?: string
|
||||
/**
|
||||
* Label for input box, defaults to "New folder"
|
||||
*/
|
||||
label?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user for file or folder name
|
||||
* @param defaultName Default name to use
|
||||
* @param folderContent Nodes with in the current folder to check for unique name
|
||||
* @param labels Labels to set on the dialog
|
||||
* @return string if successfull otherwise null if aborted
|
||||
*/
|
||||
export function newNodeName(defaultName: string, folderContent: Node[], labels: ILabels = {}) {
|
||||
const contentNames = folderContent.map((node: Node) => node.basename)
|
||||
|
||||
return new Promise<string|null>((resolve) => {
|
||||
spawnDialog(NewNodeDialog, {
|
||||
...labels,
|
||||
defaultName,
|
||||
otherNames: contentNames,
|
||||
}, (folderName) => {
|
||||
resolve(folderName as string|null)
|
||||
})
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue