feat(files): add default action support
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
parent
c85c04e4a8
commit
bb4d7969b9
|
@ -33,7 +33,7 @@
|
|||
|
||||
<!-- Link to file -->
|
||||
<td class="files-list__row-name">
|
||||
<a ref="name" v-bind="linkTo">
|
||||
<a ref="name" v-bind="linkTo" @click="execDefaultAction">
|
||||
<!-- Icon or preview -->
|
||||
<span class="files-list__row-icon">
|
||||
<FolderIcon v-if="source.type === 'folder'" />
|
||||
|
@ -49,6 +49,13 @@
|
|||
:style="{ backgroundImage: mimeIconUrl }" />
|
||||
|
||||
<FileIcon v-else />
|
||||
|
||||
<!-- Favorite icon -->
|
||||
<span v-if="isFavorite"
|
||||
class="files-list__row-icon-favorite"
|
||||
:aria-label="t('files', 'Favorite')">
|
||||
<StarIcon aria-hidden="true" :size="20" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- File name -->
|
||||
|
@ -64,6 +71,8 @@
|
|||
<!-- Menu actions -->
|
||||
<NcActions v-if="active"
|
||||
ref="actionsMenu"
|
||||
:boundaries-element="boundariesElement"
|
||||
:container="boundariesElement"
|
||||
:disabled="source._loading"
|
||||
:force-title="true"
|
||||
:inline="enabledInlineActions.length"
|
||||
|
@ -84,7 +93,8 @@
|
|||
<!-- Size -->
|
||||
<td v-if="isSizeAvailable"
|
||||
:style="{ opacity: sizeOpacity }"
|
||||
class="files-list__row-size">
|
||||
class="files-list__row-size"
|
||||
@click="execDefaultAction">
|
||||
<span>{{ size }}</span>
|
||||
</td>
|
||||
|
||||
|
@ -92,7 +102,8 @@
|
|||
<td v-for="column in columns"
|
||||
:key="column.id"
|
||||
:class="`files-list__row-${currentView?.id}-${column.id}`"
|
||||
class="files-list__row-column-custom">
|
||||
class="files-list__row-column-custom"
|
||||
@click="execDefaultAction">
|
||||
<CustomElementRender v-if="active"
|
||||
:current-view="currentView"
|
||||
:render="column.render"
|
||||
|
@ -115,9 +126,11 @@ import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
|||
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import StarIcon from 'vue-material-design-icons/Star.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { getFileActions } from '../services/FileAction.ts'
|
||||
import { hashCode } from '../utils/hashUtils.ts'
|
||||
import { isCachedPreview } from '../services/PreviewService.ts'
|
||||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
|
@ -144,6 +157,7 @@ export default Vue.extend({
|
|||
NcActions,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcLoadingIcon,
|
||||
StarIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -192,6 +206,7 @@ export default Vue.extend({
|
|||
return {
|
||||
backgroundFailed: false,
|
||||
backgroundImage: '',
|
||||
boundariesElement: document.querySelector('.app-content > .files-list'),
|
||||
loading: '',
|
||||
}
|
||||
},
|
||||
|
@ -204,7 +219,6 @@ export default Vue.extend({
|
|||
currentView() {
|
||||
return this.$navigation.active
|
||||
},
|
||||
|
||||
columns() {
|
||||
// Hide columns if the list is too small
|
||||
if (this.filesListWidth < 512) {
|
||||
|
@ -217,7 +231,6 @@ export default Vue.extend({
|
|||
// Remove any trailing slash but leave root slash
|
||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
|
||||
},
|
||||
|
||||
fileid() {
|
||||
return this.source?.fileid?.toString?.()
|
||||
},
|
||||
|
@ -225,6 +238,7 @@ export default Vue.extend({
|
|||
return this.source.attributes.displayName
|
||||
|| this.source.basename
|
||||
},
|
||||
|
||||
size() {
|
||||
const size = parseInt(this.source.size, 10) || 0
|
||||
if (typeof size !== 'number' || size < 0) {
|
||||
|
@ -232,7 +246,6 @@ export default Vue.extend({
|
|||
}
|
||||
return formatFileSize(size, true)
|
||||
},
|
||||
|
||||
sizeOpacity() {
|
||||
const size = parseInt(this.source.size, 10) || 0
|
||||
if (!size || size < 0) {
|
||||
|
@ -247,6 +260,15 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
linkTo() {
|
||||
if (this.enabledDefaultActions.length > 0) {
|
||||
const action = this.enabledDefaultActions[0]
|
||||
const displayName = action.displayName([this.source], this.currentView)
|
||||
return {
|
||||
title: displayName,
|
||||
role: 'button',
|
||||
}
|
||||
}
|
||||
|
||||
if (this.source.type === 'folder') {
|
||||
const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } }
|
||||
return {
|
||||
|
@ -272,7 +294,6 @@ export default Vue.extend({
|
|||
cropPreviews() {
|
||||
return this.userConfig.crop_image_previews
|
||||
},
|
||||
|
||||
previewUrl() {
|
||||
try {
|
||||
const url = new URL(window.location.origin + this.source.attributes.previewUrl)
|
||||
|
@ -280,13 +301,12 @@ export default Vue.extend({
|
|||
url.searchParams.set('x', '32')
|
||||
url.searchParams.set('y', '32')
|
||||
// Handle cropping
|
||||
url.searchParams.set('a', this.cropPreviews === true ? '1' : '0')
|
||||
url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
|
||||
return url.href
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
mimeIconUrl() {
|
||||
const mimeType = this.source.mime || 'application/octet-stream'
|
||||
const mimeIconUrl = window.OC?.MimeType?.getIconUrl?.(mimeType)
|
||||
|
@ -301,29 +321,38 @@ export default Vue.extend({
|
|||
.filter(action => !action.enabled || action.enabled([this.source], this.currentView))
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
},
|
||||
|
||||
enabledInlineActions() {
|
||||
if (this.filesListWidth < 768) {
|
||||
return []
|
||||
}
|
||||
return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView))
|
||||
},
|
||||
|
||||
enabledMenuActions() {
|
||||
if (this.filesListWidth < 768) {
|
||||
// If we have a default action, do not render the first one
|
||||
if (this.enabledDefaultActions.length > 0) {
|
||||
return this.enabledActions.slice(1)
|
||||
}
|
||||
return this.enabledActions
|
||||
}
|
||||
|
||||
return [
|
||||
const actions = [
|
||||
...this.enabledInlineActions,
|
||||
...this.enabledActions.filter(action => !action.inline),
|
||||
]
|
||||
},
|
||||
|
||||
uniqueId() {
|
||||
return this.hashCode(this.source.source)
|
||||
},
|
||||
// If we have a default action, do not render the first one
|
||||
if (this.enabledDefaultActions.length > 0) {
|
||||
return actions.slice(1)
|
||||
}
|
||||
|
||||
return actions
|
||||
},
|
||||
enabledDefaultActions() {
|
||||
return [
|
||||
...this.enabledActions.filter(action => action.default),
|
||||
]
|
||||
},
|
||||
openedMenu: {
|
||||
get() {
|
||||
return this.actionsMenuStore.opened === this.uniqueId
|
||||
|
@ -332,6 +361,14 @@ export default Vue.extend({
|
|||
this.actionsMenuStore.opened = opened ? this.uniqueId : null
|
||||
},
|
||||
},
|
||||
|
||||
uniqueId() {
|
||||
return hashCode(this.source.source)
|
||||
},
|
||||
|
||||
isFavorite() {
|
||||
return this.source.attributes.favorite === 1
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -457,16 +494,6 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
hashCode(str) {
|
||||
let hash = 0
|
||||
for (let i = 0, len = str.length; i < len; i++) {
|
||||
const chr = str.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + chr
|
||||
hash |= 0 // Convert to 32bit integer
|
||||
}
|
||||
return hash
|
||||
},
|
||||
|
||||
async onActionClick(action) {
|
||||
const displayName = action.displayName([this.source], this.currentView)
|
||||
try {
|
||||
|
@ -475,6 +502,12 @@ export default Vue.extend({
|
|||
Vue.set(this.source, '_loading', true)
|
||||
|
||||
const success = await action.exec(this.source, this.currentView)
|
||||
|
||||
// If the action returns null, we stay silent
|
||||
if (success === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (success) {
|
||||
showSuccess(this.t('files', '"{displayName}" action executed successfully', { displayName }))
|
||||
return
|
||||
|
@ -489,6 +522,14 @@ export default Vue.extend({
|
|||
Vue.set(this.source, '_loading', false)
|
||||
}
|
||||
},
|
||||
execDefaultAction(event) {
|
||||
if (this.enabledDefaultActions.length > 0) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// Execute the first default action if any
|
||||
this.enabledDefaultActions[0].exec(this.source, this.currentView)
|
||||
}
|
||||
},
|
||||
|
||||
onSelectionChange(selection) {
|
||||
const newSelectedIndex = this.index
|
||||
|
|
|
@ -167,11 +167,18 @@ export default Vue.extend({
|
|||
// Dispatch action execution
|
||||
const results = await action.execBatch(this.nodes, this.currentView)
|
||||
|
||||
// Check if all actions returned null
|
||||
if (results.filter(result => result !== null).length === 0) {
|
||||
// If the actions returned null, we stay silent
|
||||
this.selectionStore.reset()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle potential failures
|
||||
if (results.some(result => result !== true)) {
|
||||
if (results.some(result => result === false)) {
|
||||
// Remove the failed ids from the selection
|
||||
const failedIds = selectionIds
|
||||
.filter((fileid, index) => results[index] !== true)
|
||||
.filter((fileid, index) => results[index] === false)
|
||||
this.selectionStore.set(failedIds)
|
||||
|
||||
showError(this.t('files', '"{displayName}" failed on some elements ', { displayName }))
|
||||
|
|
|
@ -22,6 +22,9 @@ import router from './router/router.js'
|
|||
window.OCA.Files = window.OCA.Files ?? {}
|
||||
window.OCP.Files = window.OCP.Files ?? {}
|
||||
|
||||
// Expose router
|
||||
Object.assign(window.OCP.Files, { Router: router })
|
||||
|
||||
// Init Pinia store
|
||||
Vue.use(PiniaVuePlugin)
|
||||
const pinia = createPinia()
|
||||
|
@ -57,7 +60,7 @@ const FilesList = new ListView({
|
|||
})
|
||||
FilesList.$mount('#app-content-vue')
|
||||
|
||||
// Init legacy files views
|
||||
// Init legacy and new files views
|
||||
processLegacyFilesViews()
|
||||
|
||||
// Register preview service worker
|
||||
|
|
|
@ -48,13 +48,14 @@ interface FileActionData {
|
|||
* @returns true if the action was executed, false otherwise
|
||||
* @throws Error if the action failed
|
||||
*/
|
||||
exec: (file: Node, view) => Promise<boolean>,
|
||||
exec: (file: Node, view) => Promise<boolean|null>,
|
||||
/**
|
||||
* Function executed on multiple files action
|
||||
* @returns true if the action was executed, false otherwise
|
||||
* @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[]>
|
||||
execBatch?: (files: Node[], view) => Promise<(boolean|null)[]>
|
||||
/** This action order in the list */
|
||||
order?: number,
|
||||
/** Make this action the default */
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export const hashCode = function(str: string): number {
|
||||
return str.split('').reduce(function(a, b) {
|
||||
a = ((a << 5) - a) + b.charCodeAt(0)
|
||||
return a & a
|
||||
}, 0)
|
||||
}
|
|
@ -166,7 +166,7 @@ export default Vue.extend({
|
|||
return []
|
||||
}
|
||||
|
||||
const customColumn = this.currentView.columns
|
||||
const customColumn = (this.currentView?.columns || [])
|
||||
.find(column => column.id === this.sortingMode)
|
||||
|
||||
// Custom column must provide their own sorting methods
|
||||
|
|
|
@ -175,7 +175,6 @@ export default {
|
|||
this.Navigation.setActive(view)
|
||||
logger.debug('Navigation changed', { id: view.id, view })
|
||||
|
||||
// debugger
|
||||
this.showView(view, oldView)
|
||||
},
|
||||
},
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
declare module '*.svg' {
|
||||
declare module '*.svg?raw' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
@ -28,4 +28,3 @@ declare module '*.vue' {
|
|||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue