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

pull/37032/head
John Molakvoæ 2023-04-22 11:49:57 +07: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

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

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long