diff options
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/actions/openInFilesAction.spec.ts | 4 | ||||
-rw-r--r-- | apps/files/src/actions/openInFilesAction.ts | 2 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/FilesListTableHeaderButton.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 50 | ||||
-rw-r--r-- | apps/files/src/init-templates.ts (renamed from apps/files/src/templates.js) | 116 | ||||
-rw-r--r-- | apps/files/src/init.ts | 5 | ||||
-rw-r--r-- | apps/files/src/main.ts | 3 | ||||
-rw-r--r-- | apps/files/src/newMenu/newFolder.ts | 11 | ||||
-rw-r--r-- | apps/files/src/store/paths.ts | 54 | ||||
-rw-r--r-- | apps/files/src/utils/davUtils.js | 13 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 10 | ||||
-rw-r--r-- | apps/files/src/views/TemplatePicker.vue | 89 |
13 files changed, 210 insertions, 151 deletions
diff --git a/apps/files/src/actions/openInFilesAction.spec.ts b/apps/files/src/actions/openInFilesAction.spec.ts index 866880670a3..c400a701d37 100644 --- a/apps/files/src/actions/openInFilesAction.spec.ts +++ b/apps/files/src/actions/openInFilesAction.spec.ts @@ -76,7 +76,7 @@ describe('Open in files action execute tests', () => { // Silent action expect(exec).toBe(null) expect(goToRouteMock).toBeCalledTimes(1) - expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo' }) + expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { dir: '/Foo' }) }) test('Open in files with folder', async () => { @@ -96,6 +96,6 @@ describe('Open in files action execute tests', () => { // Silent action expect(exec).toBe(null) expect(goToRouteMock).toBeCalledTimes(1) - expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo/Bar' }) + expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { dir: '/Foo/Bar' }) }) }) diff --git a/apps/files/src/actions/openInFilesAction.ts b/apps/files/src/actions/openInFilesAction.ts index 235b59046d7..6cdf2357036 100644 --- a/apps/files/src/actions/openInFilesAction.ts +++ b/apps/files/src/actions/openInFilesAction.ts @@ -42,7 +42,7 @@ export const action = new FileAction({ window.OCP.Files.Router.goToRoute( null, // use default route { view: 'files', fileid: node.fileid }, - { dir, fileid: node.fileid }, + { dir }, ) return null }, diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index ff71eaeff9a..f1606d218c2 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -620,7 +620,7 @@ export default Vue.extend({ canDrag() { const canDrag = (node: Node): boolean => { - return (node.permissions & Permission.UPDATE) !== 0 + return (node?.permissions & Permission.UPDATE) !== 0 } // If we're dragging a selection, we need to check all files diff --git a/apps/files/src/components/FilesListTableHeaderButton.vue b/apps/files/src/components/FilesListTableHeaderButton.vue index 203c5b307a3..659aee8e456 100644 --- a/apps/files/src/components/FilesListTableHeaderButton.vue +++ b/apps/files/src/components/FilesListTableHeaderButton.vue @@ -22,7 +22,7 @@ <template> <NcButton :aria-label="sortAriaLabel(name)" :class="{'files-list__column-sort-button--active': sortingMode === mode}" - :alignment="mode !== 'size' ? 'start-reverse' : 'center'" + :alignment="mode !== 'size' ? 'start-reverse' : undefined" class="files-list__column-sort-button" type="tertiary" @click.stop.prevent="toggleSortBy(mode)"> diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 438a9d04ca7..c5ff9e663a3 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -145,7 +145,7 @@ export default Vue.extend({ }, fileId() { - return parseInt(this.$route.params.fileid || this.$route.query.fileid) || null + return parseInt(this.$route.params.fileid) || null }, summaryFile() { @@ -187,35 +187,47 @@ export default Vue.extend({ }, }, + watch: { + fileId(fileId) { + this.scrollToFile(fileId, false) + }, + }, + mounted() { // Add events on parent to cover both the table and DragAndDrop notice const mainContent = window.document.querySelector('main.app-content') as HTMLElement mainContent.addEventListener('dragover', this.onDragOver) mainContent.addEventListener('dragleave', this.onDragLeave) - // Scroll to the file if it's in the url - if (this.fileId) { - const index = this.nodes.findIndex(node => node.fileid === this.fileId) - if (index === -1 && this.fileId !== this.currentFolder.fileid) { - showError(this.t('files', 'File not found')) - } - this.scrollToIndex = Math.max(0, index) - } + this.scrollToFile(this.fileId) + this.openSidebarForFile(this.fileId) + }, + methods: { // Open the file sidebar if we have the room for it // but don't open the sidebar for the current folder - if (document.documentElement.clientWidth > 1024 && this.currentFolder.fileid !== this.fileId) { - // Open the sidebar for the given URL fileid - // iif we just loaded the app. - const node = this.nodes.find(n => n.fileid === this.fileId) as NcNode - if (node && sidebarAction?.enabled?.([node], this.currentView)) { - logger.debug('Opening sidebar on file ' + node.path, { node }) - sidebarAction.exec(node, this.currentView, this.currentFolder.path) + openSidebarForFile(fileId) { + if (document.documentElement.clientWidth > 1024 && this.currentFolder.fileid !== fileId) { + // Open the sidebar for the given URL fileid + // iif we just loaded the app. + const node = this.nodes.find(n => n.fileid === fileId) as NcNode + if (node && sidebarAction?.enabled?.([node], this.currentView)) { + logger.debug('Opening sidebar on file ' + node.path, { node }) + sidebarAction.exec(node, this.currentView, this.currentFolder.path) + } } - } - }, + }, + + scrollToFile(fileId: number, warn = true) { + if (fileId) { + const index = this.nodes.findIndex(node => node.fileid === fileId) + if (warn && index === -1 && fileId !== this.currentFolder.fileid) { + showError(this.t('files', 'File not found')) + } + this.scrollToIndex = Math.max(0, index) + } + }, - methods: { getFileId(node) { return node.fileid }, diff --git a/apps/files/src/templates.js b/apps/files/src/init-templates.ts index 9be12d8608f..90651826a7c 100644 --- a/apps/files/src/templates.js +++ b/apps/files/src/init-templates.ts @@ -20,17 +20,23 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ +import type { Entry } from '@nextcloud/files' +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 { generateOcsUrl } from '@nextcloud/router' -import { getCurrentDirectory } from './utils/davUtils.js' 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 { showError } from '@nextcloud/dialogs' +import { getUniqueName } from './newMenu/newFolder' +import { getCurrentUser } from '@nextcloud/auth' // Set up logger const logger = getLoggerBuilder() @@ -66,67 +72,59 @@ const TemplatePicker = new View({ }, }) 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) -// Init template engine after load to make sure it's the last injected entry -window.addEventListener('DOMContentLoaded', function() { - if (!templatesPath) { - logger.debug('Templates folder not initialized') - const initTemplatesPlugin = { - attach(menu) { - // register the new menu entry - menu.addMenuEntry({ - id: 'template-init', - displayName: t('files', 'Set up templates folder'), - templateName: t('files', 'Templates'), - iconClass: 'icon-template-add', - fileType: 'file', - actionLabel: t('files', 'Create new templates folder'), - actionHandler(name) { - initTemplatesFolder(name) - menu.removeMenuEntry('template-init') - }, - }) - }, - } - OC.Plugins.register('OCA.Files.NewFileMenu', initTemplatesPlugin) - } -}) + // Create the template folder + initTemplatesFolder(context, name) + + // Remove the menu entry + removeNewFileMenuEntry('template-picker') + }, + } as Entry) +} // Init template files menu templates.forEach((provider, index) => { - const newTemplatePlugin = { - attach(menu) { - const fileList = menu.fileList - - // only attach to main file list, public view is not supported yet - if (fileList.id !== 'files' && fileList.id !== 'files.public') { - return - } + 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) - // register the new menu entry - menu.addMenuEntry({ - id: `template-new-${provider.app}-${index}`, - displayName: provider.label, - templateName: provider.label + provider.extension, - iconClass: provider.iconClass || 'icon-file', - fileType: 'file', - actionLabel: provider.actionLabel, - actionHandler(name) { - TemplatePicker.open(name, provider) - }, - }) + // Create the file + TemplatePicker.open(name, provider) }, - } - OC.Plugins.register('OCA.Files.NewFileMenu', newTemplatePlugin) + } as Entry) }) -/** - * Init the template directory - * - * @param {string} name the templates folder name - */ -const initTemplatesFolder = async function(name) { - const templatePath = (getCurrentDirectory() + `/${name}`).replace('//', '/') +// 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'), { @@ -135,7 +133,11 @@ const initTemplatesFolder = async function(name) { }) // Go to template directory - OCA.Files.App.currentFileList.changeDirectory(templatePath, true, true) + 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 diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts index c3b70641ca1..9cbf3dc2e69 100644 --- a/apps/files/src/init.ts +++ b/apps/files/src/init.ts @@ -19,6 +19,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ +import { addNewFileMenuEntry, registerFileAction } from '@nextcloud/files' + import { action as deleteAction } from './actions/deleteAction' import { action as downloadAction } from './actions/downloadAction' import { action as editLocallyAction } from './actions/editLocallyAction' @@ -35,7 +37,8 @@ import registerFavoritesView from './views/favorites' import registerRecentView from './views/recent' import registerFilesView from './views/files' import registerPreviewServiceWorker from './services/ServiceWorker.js' -import { addNewFileMenuEntry, registerFileAction } from '@nextcloud/files' + +import './init-templates' // Register file actions registerFileAction(deleteAction) diff --git a/apps/files/src/main.ts b/apps/files/src/main.ts index 38bec4ad087..1206b9cc711 100644 --- a/apps/files/src/main.ts +++ b/apps/files/src/main.ts @@ -1,6 +1,3 @@ -import './templates.js' -import './legacy/filelistSearch.js' - import Vue from 'vue' import { createPinia, PiniaVuePlugin } from 'pinia' import { getNavigation } from '@nextcloud/files' diff --git a/apps/files/src/newMenu/newFolder.ts b/apps/files/src/newMenu/newFolder.ts index a7fa38c706c..75a09261912 100644 --- a/apps/files/src/newMenu/newFolder.ts +++ b/apps/files/src/newMenu/newFolder.ts @@ -21,15 +21,14 @@ */ import type { Entry, Node } from '@nextcloud/files' -import { addNewFileMenuEntry, Permission, Folder } from '@nextcloud/files' import { basename, extname } from 'path' import { emit } from '@nextcloud/event-bus' import { getCurrentUser } from '@nextcloud/auth' +import { Permission, Folder } from '@nextcloud/files' import { showSuccess } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import FolderPlusSvg from '@mdi/svg/svg/folder-plus.svg?raw' -import Vue from 'vue' type createFolderResponse = { fileid: number @@ -65,8 +64,9 @@ export const getUniqueName = (name: string, names: string[]): string => { export const entry = { id: 'newFolder', displayName: t('files', 'New folder'), - if: (context: Folder) => (context.permissions & Permission.CREATE) !== 0, + enabled: (context: Folder) => (context.permissions & Permission.CREATE) !== 0, 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) @@ -82,11 +82,6 @@ export const entry = { root: context?.root || '/files/' + getCurrentUser()?.uid, }) - if (!context._children) { - Vue.set(context, '_children', []) - } - context._children.push(folder.fileid) - showSuccess(t('files', 'Created new folder "{name}"', { name: basename(source) })) emit('files:node:created', folder) emit('files:node:rename', folder) diff --git a/apps/files/src/store/paths.ts b/apps/files/src/store/paths.ts index d678b5bc592..49b0ec9495b 100644 --- a/apps/files/src/store/paths.ts +++ b/apps/files/src/store/paths.ts @@ -21,12 +21,16 @@ */ import type { FileId, PathsStore, PathOptions, ServicesState } from '../types' import { defineStore } from 'pinia' -import { Node, getNavigation } from '@nextcloud/files' +import { FileType, Folder, Node, getNavigation } from '@nextcloud/files' import { subscribe } from '@nextcloud/event-bus' import Vue from 'vue' import logger from '../logger' +import { useFilesStore } from './files' + export const usePathsStore = function(...args) { + const files = useFilesStore() + const store = defineStore('paths', { state: () => ({ paths: {} as ServicesState, @@ -55,16 +59,52 @@ export const usePathsStore = function(...args) { }, onCreatedNode(node: Node) { - const currentView = getNavigation().active + const service = getNavigation()?.active?.id || 'files' if (!node.fileid) { logger.error('Node has no fileid', { node }) return } - this.addPath({ - service: currentView?.id || 'files', - path: node.path, - fileid: node.fileid, - }) + + // Only add path if it's a folder + if (node.type === FileType.Folder) { + this.addPath({ + service, + path: node.path, + fileid: node.fileid, + }) + } + + // Update parent folder children if exists + // If the folder is the root, get it and update it + if (node.dirname === '/') { + const root = files.getRoot(service) + if (!root._children) { + Vue.set(root, '_children', []) + } + root._children.push(node.fileid) + return + } + + // If the folder doesn't exists yet, it will be + // fetched later and its children updated anyway. + if (this.paths[service][node.dirname]) { + const parentId = this.paths[service][node.dirname] + const parentFolder = files.getNode(parentId) as Folder + logger.debug('Path already exists, updating children', { parentFolder, node }) + + if (!parentFolder) { + logger.error('Parent folder not found', { parentId }) + return + } + + if (!parentFolder._children) { + Vue.set(parentFolder, '_children', []) + } + parentFolder._children.push(node.fileid) + return + } + + logger.debug('Parent path does not exists, skipping children update', { node }) }, }, }) diff --git a/apps/files/src/utils/davUtils.js b/apps/files/src/utils/davUtils.js index 1bd63347518..22367d09a1a 100644 --- a/apps/files/src/utils/davUtils.js +++ b/apps/files/src/utils/davUtils.js @@ -38,16 +38,3 @@ export const isPublic = function() { export const getToken = function() { return document.getElementById('sharingToken') && document.getElementById('sharingToken').value } - -/** - * Return the current directory, fallback to root - * - * @return {string} - */ -export const getCurrentDirectory = function() { - const currentDirInfo = OCA?.Files?.App?.currentFileList?.dirInfo - || { path: '/', name: '' } - - // Make sure we don't have double slashes - return `${currentDirInfo.path}/${currentDirInfo.name}`.replace(/\/\//gi, '/') -} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 03ddafb7346..a7bb55fb2cd 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -89,13 +89,14 @@ import type { Upload } from '@nextcloud/upload' import type { UserConfig } from '../types.ts' import type { View, ContentsWithRoot } from '@nextcloud/files' +import { emit } from '@nextcloud/event-bus' import { Folder, Node, Permission } from '@nextcloud/files' import { getCapabilities } from '@nextcloud/capabilities' import { join, dirname } from 'path' import { orderBy } from 'natural-orderby' import { translate, translatePlural } from '@nextcloud/l10n' -import { UploadPicker } from '@nextcloud/upload' import { Type } from '@nextcloud/sharing' +import { UploadPicker } from '@nextcloud/upload' import Vue from 'vue' import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' @@ -326,6 +327,11 @@ export default Vue.extend({ this.$refs.filesListVirtual.$el.scrollTop = 0 } }, + + dirContents(contents) { + logger.debug('Directory contents changed', { view: this.currentView, folder: this.currentFolder, contents }) + emit('files:list:updated', { view: this.currentView, folder: this.currentFolder, contents }) + }, }, mounted() { @@ -360,7 +366,7 @@ export default Vue.extend({ // Define current directory children // TODO: make it more official - folder._children = contents.map(node => node.fileid) + Vue.set(folder, '_children', contents.map(node => node.fileid)) // If we're in the root dir, define the root if (dir === '/') { diff --git a/apps/files/src/views/TemplatePicker.vue b/apps/files/src/views/TemplatePicker.vue index a6bb9809b10..5f248602d4d 100644 --- a/apps/files/src/views/TemplatePicker.vue +++ b/apps/files/src/views/TemplatePicker.vue @@ -60,20 +60,24 @@ </NcModal> </template> -<script> -import { normalize } from 'path' +<script lang="ts"> +import { emit, subscribe } from '@nextcloud/event-bus' +import { File } from '@nextcloud/files' +import { generateRemoteUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' +import { normalize, extname, join } from 'path' import { showError } from '@nextcloud/dialogs' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' +import Vue from 'vue' -import { getCurrentDirectory } from '../utils/davUtils.js' import { createFromTemplate, getTemplates } from '../services/Templates.js' import TemplatePreview from '../components/TemplatePreview.vue' const border = 2 const margin = 8 -export default { +export default Vue.extend({ name: 'TemplatePicker', components: { @@ -101,15 +105,14 @@ export default { }, computed: { - /** - * Strip away extension from name - * - * @return {string} - */ + extension() { + return extname(this.name) + }, nameWithoutExt() { - return this.name.indexOf('.') > -1 - ? this.name.split('.').slice(0, -1).join('.') - : this.name + // Strip extension from name if defined + return !this.extension + ? this.name + : this.name.slice(0, 0 - this.extension.length) }, emptyTemplate() { @@ -123,15 +126,23 @@ export default { }, selectedTemplate() { + if (!this.provider) { + return null + } + return this.provider.templates.find(template => template.fileid === this.checked) }, /** - * Style css vars bin,d + * Style css vars bind * * @return {object} */ style() { + if (!this.provider) { + return {} + } + // Fallback to 16:9 landscape ratio const ratio = this.provider.ratio ? this.provider.ratio : 1.77 // Landscape templates should be wider than tall ones @@ -154,8 +165,7 @@ export default { * @param {string} name the file name to create * @param {object} provider the template provider picked */ - async open(name, provider) { - + async open(name: string, provider) { this.checked = this.emptyTemplate.fileid this.name = name this.provider = provider @@ -199,12 +209,11 @@ export default { async onSubmit() { this.loading = true - const currentDirectory = getCurrentDirectory() - const fileList = OCA?.Files?.App?.currentFileList + const currentDirectory = new URL(window.location.href).searchParams.get('dir') || '/' // If the file doesn't have an extension, add the default one if (this.nameWithoutExt === this.name) { - this.logger.debug('Fixed invalid filename', { name: this.name, extension: this.provider?.extension }) + this.logger.warn('Fixed invalid filename', { name: this.name, extension: this.provider?.extension }) this.name = this.name + this.provider?.extension } @@ -216,35 +225,43 @@ export default { ) this.logger.debug('Created new file', fileInfo) - // Fetch FileInfo and model - const data = await fileList?.addAndFetchFileInfo(this.name).then((status, data) => data) - const model = new OCA.Files.FileInfoModel(data, { - filesClient: fileList?.filesClient, + const owner = getCurrentUser()?.uid || null + const node = new File({ + id: fileInfo.fileid, + source: generateRemoteUrl(join('dav/files', owner, fileInfo.filename)), + root: `/files/${owner}`, + mime: fileInfo.mime, + mtime: new Date(fileInfo.lastmod * 1000), + owner, + size: fileInfo.size, + permissions: fileInfo.permissions, + attributes: { + ...fileInfo, + 'has-preview': fileInfo.hasPreview, + }, }) - // Run default action - const fileAction = OCA.Files.fileActions.getDefaultFileAction(fileInfo.mime, 'file', OC.PERMISSION_ALL) - if (fileAction) { - fileAction.action(fileInfo.basename, { - $file: fileList?.findFileEl(this.name), - dir: currentDirectory, - fileList, - fileActions: fileList?.fileActions, - fileInfoModel: model, - }) - } + // Update files list + emit('files:node:created', node) + // Open the new file + window.OCP.Files.Router.goToRoute( + null, // use default route + { view: 'files', fileid: node.fileid }, + { dir: node.dirname, openfile: true }, + ) + + // Close the picker this.close() } catch (error) { - this.logger.error('Error while creating the new file from template') - console.error(error) + this.logger.error('Error while creating the new file from template', { error }) showError(this.t('files', 'Unable to create new file from template')) } finally { this.loading = false } }, }, -} +}) </script> <style lang="scss" scoped> |