mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 16:26:59 -04:00
Merge pull request #37866 from nextcloud/fix/files-vue
This commit is contained in:
commit
1b119e10d0
18 changed files with 147 additions and 47 deletions
|
|
@ -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,
|
||||
|
|
|
|||
54
apps/files/src/actions/sidebarAction.ts
Normal file
54
apps/files/src/actions/sidebarAction.ts
Normal file
|
|
@ -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,
|
||||
}))
|
||||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -49,11 +49,15 @@ export interface RootOptions {
|
|||
|
||||
// Paths store
|
||||
export type ServicesState = {
|
||||
[service: Service]: PathsStore
|
||||
[service: Service]: PathConfig
|
||||
}
|
||||
|
||||
export type PathConfig = {
|
||||
[path: string]: number
|
||||
}
|
||||
|
||||
export type PathsStore = {
|
||||
[path: string]: number
|
||||
paths: ServicesState
|
||||
}
|
||||
|
||||
export interface PathOptions {
|
||||
|
|
@ -91,4 +95,4 @@ export interface ViewConfigs {
|
|||
}
|
||||
export interface ViewConfigStore {
|
||||
viewConfig: ViewConfigs
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue