mirror of
https://github.com/nextcloud/server.git
synced 2026-02-20 00:12:30 -05:00
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>
This commit is contained in:
parent
e62c5d719d
commit
8be4704e11
10 changed files with 469 additions and 180 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
149
apps/files/src/components/NewNodeDialog.vue
Normal file
149
apps/files/src/components/NewNodeDialog.vue
Normal file
|
|
@ -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'))
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
88
apps/files/src/newMenu/newFromTemplate.ts
Normal file
88
apps/files/src/newMenu/newFromTemplate.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
100
apps/files/src/newMenu/newTemplatesFolder.ts
Normal file
100
apps/files/src/newMenu/newTemplatesFolder.ts
Normal file
|
|
@ -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
|
||||
57
apps/files/src/utils/newNodeDialog.ts
Normal file
57
apps/files/src/utils/newNodeDialog.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue