mirror of https://github.com/nextcloud/server.git
Merge pull request #42993 from nextcloud/fix/files-new-menu
fix(files): Allow to set node name before creating + bring back new-file highlingtingpull/43426/head
commit
7ff6cbc1b8
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue