diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2023-04-22 11:49:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-22 11:49:57 +0200 |
commit | 1b119e10d0249fbafb768cceef3c353b34b65d3a (patch) | |
tree | 7f7c62fb1bbfe57df8ef166694a6851abed0ea25 /apps/files | |
parent | 9e1703e76c974228e3534fae28c754e37ce55e2b (diff) | |
parent | 751bc139a1a418af4604c626311aef0b46c47a16 (diff) | |
download | nextcloud-server-1b119e10d0249fbafb768cceef3c353b34b65d3a.tar.gz nextcloud-server-1b119e10d0249fbafb768cceef3c353b34b65d3a.zip |
Merge pull request #37866 from nextcloud/fix/files-vue
Diffstat (limited to 'apps/files')
-rw-r--r-- | apps/files/src/actions/deleteAction.ts | 7 | ||||
-rw-r--r-- | apps/files/src/actions/sidebarAction.ts | 54 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 41 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeader.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeaderActions.vue | 6 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 17 | ||||
-rw-r--r-- | apps/files/src/main.ts | 3 | ||||
-rw-r--r-- | apps/files/src/services/FileAction.ts | 5 | ||||
-rw-r--r-- | apps/files/src/services/Navigation.ts | 7 | ||||
-rw-r--r-- | apps/files/src/store/files.ts | 4 | ||||
-rw-r--r-- | apps/files/src/store/paths.ts | 16 | ||||
-rw-r--r-- | apps/files/src/types.ts | 10 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 8 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.vue | 2 |
14 files changed, 141 insertions, 41 deletions
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index 087884b3362..a633e477b1f 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -27,10 +27,11 @@ import TrashCan from '@mdi/svg/svg/trash-can.svg?raw' import { registerFileAction, FileAction } from '../services/FileAction.ts' import logger from '../logger.js' +import type { Navigation } from '../services/Navigation.ts' registerFileAction(new FileAction({ id: 'delete', - displayName(nodes: Node[], view) { + displayName(nodes: Node[], view: Navigation) { return view.id === 'trashbin' ? t('files_trashbin', 'Delete permanently') : t('files', 'Delete') @@ -57,8 +58,8 @@ registerFileAction(new FileAction({ return false } }, - async execBatch(nodes: Node[], view) { - return Promise.all(nodes.map(node => this.exec(node, view))) + async execBatch(nodes: Node[], view: Navigation, dir: string) { + return Promise.all(nodes.map(node => this.exec(node, view, dir))) }, order: 100, diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts new file mode 100644 index 00000000000..f56d3a9475f --- /dev/null +++ b/apps/files/src/actions/sidebarAction.ts @@ -0,0 +1,54 @@ +/** + * @copyright Copyright (c) 2023 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/>. + * + */ +import { translate as t } from '@nextcloud/l10n' +import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw' +import type { Node } from '@nextcloud/files' + +import { registerFileAction, FileAction } from '../services/FileAction.ts' +import logger from '../logger.js' + +export const ACTION_DETAILS = 'details' + +registerFileAction(new FileAction({ + id: ACTION_DETAILS, + displayName: () => t('files', 'Details'), + iconSvgInline: () => InformationSvg, + + // Sidebar currently supports user folder only, /files/USER + enabled: (files: Node[]) => !!window?.OCA?.Files?.Sidebar + && files.some(node => node.root?.startsWith('/files/')), + + async exec(node: Node) { + try { + // TODO: migrate Sidebar to use a Node instead + window?.OCA?.Files?.Sidebar?.open?.(node.path) + + return null + } catch (error) { + logger.error('Error while opening sidebar', { error }) + return false + } + }, + + default: true, + order: -50, +})) diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 00ff8a3d533..8dc067a407d 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -75,6 +75,7 @@ :container="boundariesElement" :disabled="source._loading" :force-title="true" + :force-menu="true" :inline="enabledInlineActions.length" :open.sync="openedMenu"> <NcActionButton v-for="action in enabledMenuActions" @@ -94,7 +95,7 @@ <td v-if="isSizeAvailable" :style="{ opacity: sizeOpacity }" class="files-list__row-size" - @click="execDefaultAction"> + @click="openDetailsIfAvailable"> <span>{{ size }}</span> </td> @@ -103,7 +104,7 @@ :key="column.id" :class="`files-list__row-${currentView?.id}-${column.id}`" class="files-list__row-column-custom" - @click="execDefaultAction"> + @click="openDetailsIfAvailable"> <CustomElementRender v-if="active" :current-view="currentView" :render="column.render" @@ -129,6 +130,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import StarIcon from 'vue-material-design-icons/Star.vue' import Vue from 'vue' +import { ACTION_DETAILS } from '../actions/sidebarAction.ts' import { getFileActions } from '../services/FileAction.ts' import { hashCode } from '../utils/hashUtils.ts' import { isCachedPreview } from '../services/PreviewService.ts' @@ -260,6 +262,15 @@ export default Vue.extend({ }, linkTo() { + if (this.source.type === 'folder') { + const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } } + return { + is: 'router-link', + title: this.t('files', 'Open folder {name}', { name: this.displayName }), + to, + } + } + if (this.enabledDefaultActions.length > 0) { const action = this.enabledDefaultActions[0] const displayName = action.displayName([this.source], this.currentView) @@ -269,14 +280,6 @@ export default Vue.extend({ } } - if (this.source.type === 'folder') { - const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } } - return { - is: 'router-link', - title: this.t('files', 'Open folder {name}', { name: this.displayName }), - to, - } - } return { href: this.source.source, // TODO: Use first action title ? @@ -501,7 +504,7 @@ export default Vue.extend({ this.loading = action.id Vue.set(this.source, '_loading', true) - const success = await action.exec(this.source, this.currentView) + const success = await action.exec(this.source, this.currentView, this.dir) // If the action returns null, we stay silent if (success === null) { @@ -523,11 +526,25 @@ export default Vue.extend({ } }, execDefaultAction(event) { + // Do not execute the default action on the folder, navigate instead + if (this.source.type === 'folder') { + return + } + if (this.enabledDefaultActions.length > 0) { event.preventDefault() event.stopPropagation() // Execute the first default action if any - this.enabledDefaultActions[0].exec(this.source, this.currentView) + this.enabledDefaultActions[0].exec(this.source, this.currentView, this.dir) + } + }, + + openDetailsIfAvailable(event) { + const detailsAction = this.enabledDefaultActions.find(action => action.id === ACTION_DETAILS) + if (detailsAction) { + event.preventDefault() + event.stopPropagation() + detailsAction.exec(this.source, this.currentView) } }, diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue index 9e3fe0d46de..2d848d0eefe 100644 --- a/apps/files/src/components/FilesListHeader.vue +++ b/apps/files/src/components/FilesListHeader.vue @@ -173,7 +173,7 @@ export default Vue.extend({ onToggleAll(selected) { if (selected) { - const selection = this.nodes.map(node => node.attributes.fileid.toString()) + const selection = this.nodes.map(node => node.fileid.toString()) logger.debug('Added all nodes to selection', { selection }) this.selectionStore.setLastIndex(null) this.selectionStore.set(selection) diff --git a/apps/files/src/components/FilesListHeaderActions.vue b/apps/files/src/components/FilesListHeaderActions.vue index a53a1d041bf..f8c60a5cd1b 100644 --- a/apps/files/src/components/FilesListHeaderActions.vue +++ b/apps/files/src/components/FilesListHeaderActions.vue @@ -103,6 +103,10 @@ export default Vue.extend({ }, computed: { + dir() { + // Remove any trailing slash but leave root slash + return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') + }, enabledActions() { return actions .filter(action => action.execBatch) @@ -165,7 +169,7 @@ export default Vue.extend({ }) // Dispatch action execution - const results = await action.execBatch(this.nodes, this.currentView) + const results = await action.execBatch(this.nodes, this.currentView, this.dir) // Check if all actions returned null if (!results.some(result => result !== null)) { diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index ad0ba2069ff..866fc6da00d 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -139,7 +139,7 @@ export default Vue.extend({ methods: { getFileId(node) { - return node.attributes.fileid + return node.fileid }, t: translate, @@ -233,22 +233,24 @@ export default Vue.extend({ } .files-list__row-icon { + position: relative; display: flex; + overflow: visible; align-items: center; + // No shrinking or growing allowed + flex: 0 0 var(--icon-preview-size); justify-content: center; width: var(--icon-preview-size); height: 100%; // Show same padding as the checkbox right padding for visual balance margin-right: var(--checkbox-padding); color: var(--color-primary-element); - // No shrinking or growing allowed - flex: 0 0 var(--icon-preview-size); & > span { justify-content: flex-start; } - svg { + &> span:not(.files-list__row-icon-favorite) svg { width: var(--icon-preview-size); height: var(--icon-preview-size); } @@ -263,6 +265,13 @@ export default Vue.extend({ background-position: center; background-size: contain; } + + &-favorite { + position: absolute; + top: 4px; + right: -8px; + color: #ffcc00; + } } .files-list__row-name { diff --git a/apps/files/src/main.ts b/apps/files/src/main.ts index 976714a8f1f..195357d0e0a 100644 --- a/apps/files/src/main.ts +++ b/apps/files/src/main.ts @@ -1,6 +1,7 @@ import './templates.js' import './legacy/filelistSearch.js' import './actions/deleteAction' +import './actions/sidebarAction' import Vue from 'vue' import { createPinia, PiniaVuePlugin } from 'pinia' @@ -11,9 +12,9 @@ import NavigationView from './views/Navigation.vue' import processLegacyFilesViews from './legacy/navigationMapper.js' import registerPreviewServiceWorker from './services/ServiceWorker.js' import router from './router/router.js' +import RouterService from './services/RouterService' import SettingsModel from './models/Setting.js' import SettingsService from './services/Settings.js' -import RouterService from './services/RouterService' declare global { interface Window { diff --git a/apps/files/src/services/FileAction.ts b/apps/files/src/services/FileAction.ts index 94fc7e8ce5f..70d6405c804 100644 --- a/apps/files/src/services/FileAction.ts +++ b/apps/files/src/services/FileAction.ts @@ -22,6 +22,7 @@ import type { Node } from '@nextcloud/files' import logger from '../logger' +import type { Navigation } from './Navigation' declare global { interface Window { @@ -48,14 +49,14 @@ interface FileActionData { * @returns true if the action was executed, false otherwise * @throws Error if the action failed */ - exec: (file: Node, view) => Promise<boolean|null>, + exec: (file: Node, view: Navigation, dir: string) => Promise<boolean|null>, /** * Function executed on multiple files action * @returns true if the action was executed successfully, * false otherwise and null if the action is silent/undefined. * @throws Error if the action failed */ - execBatch?: (files: Node[], view) => Promise<(boolean|null)[]> + execBatch?: (files: Node[], view: Navigation, dir: string) => Promise<(boolean|null)[]> /** This action order in the list */ order?: number, /** Make this action the default */ diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts index e86266013d7..b2ae3b0b973 100644 --- a/apps/files/src/services/Navigation.ts +++ b/apps/files/src/services/Navigation.ts @@ -126,6 +126,13 @@ export default class { this._views.push(view) } + remove(id: string) { + const index = this._views.findIndex(view => view.id === id) + if (index !== -1) { + this._views.splice(index, 1) + } + } + get views(): Navigation[] { return this._views } diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts index ea516a886d9..bd7d3202dd9 100644 --- a/apps/files/src/store/files.ts +++ b/apps/files/src/store/files.ts @@ -59,11 +59,11 @@ export const useFilesStore = function() { updateNodes(nodes: Node[]) { // Update the store all at once const files = nodes.reduce((acc, node) => { - if (!node.attributes.fileid) { + if (!node.fileid) { logger.warn('Trying to update/set a node without fileid', node) return acc } - acc[node.attributes.fileid] = node + acc[node.fileid] = node return acc }, {} as FilesStore) diff --git a/apps/files/src/store/paths.ts b/apps/files/src/store/paths.ts index c9335304bce..ecff97bf00c 100644 --- a/apps/files/src/store/paths.ts +++ b/apps/files/src/store/paths.ts @@ -24,20 +24,22 @@ import type { PathOptions, ServicesState } from '../types.ts' import { defineStore } from 'pinia' import { subscribe } from '@nextcloud/event-bus' -import type { FileId } from '../types' +import type { FileId, PathsStore } from '../types' import Vue from 'vue' export const usePathsStore = function() { const store = defineStore('paths', { - state: (): ServicesState => ({}), + state: () => ({ + paths: {} as ServicesState + } as PathsStore), getters: { getPath: (state) => { return (service: string, path: string): FileId|undefined => { - if (!state[service]) { + if (!state.paths[service]) { return undefined } - return state[service][path] + return state.paths[service][path] } }, }, @@ -45,12 +47,12 @@ export const usePathsStore = function() { actions: { addPath(payload: PathOptions) { // If it doesn't exists, init the service state - if (!this[payload.service]) { - Vue.set(this, payload.service, {}) + if (!this.paths[payload.service]) { + Vue.set(this.paths, payload.service, {}) } // Now we can set the provided path - Vue.set(this[payload.service], payload.path, payload.fileid) + Vue.set(this.paths[payload.service], payload.path, payload.fileid) }, } }) diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index cca6fb9111f..c04a9538827 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -49,13 +49,17 @@ export interface RootOptions { // Paths store export type ServicesState = { - [service: Service]: PathsStore + [service: Service]: PathConfig } -export type PathsStore = { +export type PathConfig = { [path: string]: number } +export type PathsStore = { + paths: ServicesState +} + export interface PathOptions { service: Service path: string @@ -91,4 +95,4 @@ export interface ViewConfigs { } export interface ViewConfigStore { viewConfig: ViewConfigs -}
\ No newline at end of file +} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 50f35fef5aa..f2a20c18f28 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -268,16 +268,16 @@ export default Vue.extend({ this.filesStore.updateNodes(contents) // Define current directory children - folder._children = contents.map(node => node.attributes.fileid) + folder._children = contents.map(node => node.fileid) // If we're in the root dir, define the root if (dir === '/') { this.filesStore.setRoot({ service: currentView.id, root: folder }) } else // Otherwise, add the folder to the store - if (folder.attributes.fileid) { + if (folder.fileid) { this.filesStore.updateNodes([folder]) - this.pathsStore.addPath({ service: currentView.id, fileid: folder.attributes.fileid, path: dir }) + this.pathsStore.addPath({ service: currentView.id, fileid: folder.fileid, path: dir }) } else { // If we're here, the view API messed up logger.error('Invalid root folder returned', { dir, folder, currentView }) @@ -286,7 +286,7 @@ export default Vue.extend({ // Update paths store const folders = contents.filter(node => node.type === 'folder') folders.forEach(node => { - this.pathsStore.addPath({ service: currentView.id, fileid: node.attributes.fileid, path: join(dir, node.basename) }) + this.pathsStore.addPath({ service: currentView.id, fileid: node.fileid, path: join(dir, node.basename) }) }) } catch (error) { logger.error('Error while fetching content', { error }) diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index e5556e88958..e164880003a 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -44,7 +44,7 @@ :title="child.name" :to="generateToNavigation(child)"> <!-- Sanitized icon as svg if provided --> - <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" /> + <NcIconSvgWrapper v-if="child.icon" slot="icon" :svg="child.icon" /> </NcAppNavigationItem> </NcAppNavigationItem> </template> |