summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/files/src/components/FileEntry.vue15
-rw-r--r--apps/files/src/components/FilesListVirtual.vue35
-rw-r--r--apps/files/src/components/NewNodeDialog.vue149
-rw-r--r--apps/files/src/init-templates.ts149
-rw-r--r--apps/files/src/init.ts7
-rw-r--r--apps/files/src/newMenu/newFolder.ts40
-rw-r--r--apps/files/src/newMenu/newFromTemplate.ts88
-rw-r--r--apps/files/src/newMenu/newTemplatesFolder.ts100
-rw-r--r--apps/files/src/utils/newNodeDialog.ts57
-rw-r--r--apps/files/src/views/FilesList.vue9
10 files changed, 469 insertions, 180 deletions
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index 0ccd5622a5e..973e1de667f 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -21,7 +21,11 @@
-->
<template>
- <tr :class="{'files-list__row--dragover': dragover, 'files-list__row--loading': isLoading}"
+ <tr :class="{
+ 'files-list__row--dragover': dragover,
+ 'files-list__row--loading': isLoading,
+ 'files-list__row--active': isActive,
+ }"
data-cy-files-list-row
:data-cy-files-list-row-fileid="fileid"
:data-cy-files-list-row-name="source.basename"
@@ -97,7 +101,7 @@
<script lang="ts">
import { defineComponent } from 'vue'
-import { formatFileSize } from '@nextcloud/files'
+import { Permission, formatFileSize } from '@nextcloud/files'
import moment from '@nextcloud/moment'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
@@ -232,6 +236,13 @@ export default defineComponent({
}
return ''
},
+
+ /**
+ * This entry is the current active node
+ */
+ isActive() {
+ return this.fileid === this.currentFileId?.toString?.()
+ },
},
methods: {
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index afb2dbd888a..b6a11391dc1 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -139,6 +139,7 @@ export default defineComponent({
FileEntryGrid,
headers: getFileListHeaders(),
scrollToIndex: 0,
+ openFileId: null as number|null,
}
},
@@ -151,6 +152,14 @@ export default defineComponent({
return parseInt(this.$route.params.fileid) || null
},
+ /**
+ * If the current `fileId` should be opened
+ * The state of the `openfile` query param
+ */
+ openFile() {
+ return !!this.$route.query.openfile
+ },
+
summary() {
return getSummaryFor(this.nodes)
},
@@ -199,6 +208,12 @@ export default defineComponent({
fileId(fileId) {
this.scrollToFile(fileId, false)
},
+
+ openFile(open: boolean) {
+ if (open) {
+ this.$nextTick(() => this.handleOpenFile(this.fileId))
+ }
+ },
},
mounted() {
@@ -206,9 +221,11 @@ export default defineComponent({
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
mainContent.addEventListener('dragover', this.onDragOver)
- this.scrollToFile(this.fileId)
- this.openSidebarForFile(this.fileId)
- this.handleOpenFile()
+ // handle initially opening a given file
+ const { id } = loadState<{ id?: number }>('files', 'openFileInfo', {})
+ this.scrollToFile(id ?? this.fileId)
+ this.openSidebarForFile(id ?? this.fileId)
+ this.handleOpenFile(id ?? null)
},
beforeDestroy() {
@@ -241,18 +258,22 @@ export default defineComponent({
}
},
- handleOpenFile() {
- const openFileInfo = loadState('files', 'openFileInfo', {}) as ({ id?: number })
- if (openFileInfo === undefined) {
+ /**
+ * Handle opening a file (e.g. by ?openfile=true)
+ * @param fileId File to open
+ */
+ handleOpenFile(fileId: number|null) {
+ if (fileId === null || this.openFileId === fileId) {
return
}
- const node = this.nodes.find(n => n.fileid === openFileInfo.id) as NcNode
+ const node = this.nodes.find(n => n.fileid === fileId) as NcNode
if (node === undefined || node.type === FileType.Folder) {
return
}
logger.debug('Opening file ' + node.path, { node })
+ this.openFileId = fileId
getFileActions()
.filter(action => !action.enabled || action.enabled([node], this.currentView))
.sort((a, b) => (a.order || 0) - (b.order || 0))
diff --git a/apps/files/src/components/NewNodeDialog.vue b/apps/files/src/components/NewNodeDialog.vue
new file mode 100644
index 00000000000..38337ddf4b8
--- /dev/null
+++ b/apps/files/src/components/NewNodeDialog.vue
@@ -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>
diff --git a/apps/files/src/init-templates.ts b/apps/files/src/init-templates.ts
deleted file mode 100644
index 6803143d4b2..00000000000
--- a/apps/files/src/init-templates.ts
+++ /dev/null
@@ -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'))
- }
-}
diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts
index 9f463244d91..c3b4b570e12 100644
--- a/apps/files/src/init.ts
+++ b/apps/files/src/init.ts
@@ -31,14 +31,15 @@ import { action as openInFilesAction } from './actions/openInFilesAction'
import { action as renameAction } from './actions/renameAction'
import { action as sidebarAction } from './actions/sidebarAction'
import { action as viewInFolderAction } from './actions/viewInFolderAction'
-import { entry as newFolderEntry } from './newMenu/newFolder'
+import { entry as newFolderEntry } from './newMenu/newFolder.ts'
+import { entry as newTemplatesFolder } from './newMenu/newTemplatesFolder.ts'
+import { registerTemplateEntries } from './newMenu/newFromTemplate.ts'
import registerFavoritesView from './views/favorites'
import registerRecentView from './views/recent'
import registerFilesView from './views/files'
import registerPreviewServiceWorker from './services/ServiceWorker.js'
-import './init-templates'
import { initLivePhotos } from './services/LivePhotos'
@@ -56,6 +57,8 @@ registerFileAction(viewInFolderAction)
// Register new menu entry
addNewFileMenuEntry(newFolderEntry)
+addNewFileMenuEntry(newTemplatesFolder)
+registerTemplateEntries()
// Register files views
registerFavoritesView()
diff --git a/apps/files/src/newMenu/newFolder.ts b/apps/files/src/newMenu/newFolder.ts
index 37dcf6d3d89..64ab8004e78 100644
--- a/apps/files/src/newMenu/newFolder.ts
+++ b/apps/files/src/newMenu/newFolder.ts
@@ -31,7 +31,7 @@ import axios from '@nextcloud/axios'
import FolderPlusSvg from '@mdi/svg/svg/folder-plus.svg?raw'
-import { getUniqueName } from '../utils/fileUtils.ts'
+import { newNodeName } from '../utils/newNodeDialog'
import logger from '../logger'
type createFolderResponse = {
@@ -63,23 +63,27 @@ export const entry = {
iconSvgInline: FolderPlusSvg,
order: 0,
async handler(context: Folder, content: Node[]) {
- const contentNames = content.map((node: Node) => node.basename)
- const name = getUniqueName(t('files', 'New folder'), contentNames)
- const { fileid, source } = await createNewFolder(context, name)
+ const name = await newNodeName(t('files', 'New folder'), content)
+ if (name !== null) {
+ const { fileid, source } = await createNewFolder(context, name)
+ // Create the folder in the store
+ const folder = new Folder({
+ source,
+ id: fileid,
+ mtime: new Date(),
+ owner: getCurrentUser()?.uid || null,
+ permissions: Permission.ALL,
+ root: context?.root || '/files/' + getCurrentUser()?.uid,
+ })
- // Create the folder in the store
- const folder = new Folder({
- source,
- id: fileid,
- mtime: new Date(),
- owner: getCurrentUser()?.uid || null,
- permissions: Permission.ALL,
- root: context?.root || '/files/' + getCurrentUser()?.uid,
- })
-
- showSuccess(t('files', 'Created new folder "{name}"', { name: basename(source) }))
- logger.debug('Created new folder', { folder, source })
- emit('files:node:created', folder)
- emit('files:node:rename', folder)
+ showSuccess(t('files', 'Created new folder "{name}"', { name: basename(source) }))
+ logger.debug('Created new folder', { folder, source })
+ emit('files:node:created', folder)
+ window.OCP.Files.Router.goToRoute(
+ null, // use default route
+ { view: 'files', fileid: folder.fileid },
+ { dir: context.path },
+ )
+ }
},
} as Entry
diff --git a/apps/files/src/newMenu/newFromTemplate.ts b/apps/files/src/newMenu/newFromTemplate.ts
new file mode 100644
index 00000000000..5e69181995e
--- /dev/null
+++ b/apps/files/src/newMenu/newFromTemplate.ts
@@ -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)
+ })
+}
diff --git a/apps/files/src/newMenu/newTemplatesFolder.ts b/apps/files/src/newMenu/newTemplatesFolder.ts
new file mode 100644
index 00000000000..fafee553a10
--- /dev/null
+++ b/apps/files/src/newMenu/newTemplatesFolder.ts
@@ -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
diff --git a/apps/files/src/utils/newNodeDialog.ts b/apps/files/src/utils/newNodeDialog.ts
new file mode 100644
index 00000000000..f53694fc68c
--- /dev/null
+++ b/apps/files/src/utils/newNodeDialog.ts
@@ -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)
+ })
+ })
+}
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index 19e9b8e86d2..4e80379f632 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -566,15 +566,20 @@ export default defineComponent({
/**
* Refreshes the current folder on update.
*
- * @param {Node} node is the file/folder being updated.
+ * @param node is the file/folder being updated.
*/
- onUpdatedNode(node) {
+ onUpdatedNode(node?: Node) {
if (node?.fileid === this.currentFolder?.fileid) {
this.fetchContent()
}
},
openSharingSidebar() {
+ if (!this.currentFolder) {
+ logger.debug('No current folder found for opening sharing sidebar')
+ return
+ }
+
if (window?.OCA?.Files?.Sidebar?.setActiveTab) {
window.OCA.Files.Sidebar.setActiveTab('sharing')
}