Merge pull request #37866 from nextcloud/fix/files-vue

This commit is contained in:
John Molakvoæ 2023-04-22 11:49:57 +02:00 committed by GitHub
commit 1b119e10d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 147 additions and 47 deletions

View file

@ -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,

View 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,
}))

View file

@ -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)
}
},

View file

@ -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)

View file

@ -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)) {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 */

View file

@ -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
}

View file

@ -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)

View file

@ -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)
},
}
})

View file

@ -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
}
}

View file

@ -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 })

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long