diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 7db22482220..00ff8a3d533 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -33,7 +33,7 @@ - + @@ -49,6 +49,13 @@ :style="{ backgroundImage: mimeIconUrl }" /> + + + + @@ -64,6 +71,8 @@ + class="files-list__row-size" + @click="execDefaultAction"> {{ size }} @@ -92,7 +102,8 @@ + class="files-list__row-column-custom" + @click="execDefaultAction"> .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 diff --git a/apps/files/src/components/FilesListHeaderActions.vue b/apps/files/src/components/FilesListHeaderActions.vue index c9f0c66be03..b86d1f1d80b 100644 --- a/apps/files/src/components/FilesListHeaderActions.vue +++ b/apps/files/src/components/FilesListHeaderActions.vue @@ -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 })) diff --git a/apps/files/src/main.js b/apps/files/src/main.js index a8464f0ee0d..db1726e3376 100644 --- a/apps/files/src/main.js +++ b/apps/files/src/main.js @@ -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 diff --git a/apps/files/src/services/FileAction.ts b/apps/files/src/services/FileAction.ts index 8c1d325e645..453dbe535ee 100644 --- a/apps/files/src/services/FileAction.ts +++ b/apps/files/src/services/FileAction.ts @@ -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, + exec: (file: Node, view) => Promise, /** * 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 + execBatch?: (files: Node[], view) => Promise<(boolean|null)[]> /** This action order in the list */ order?: number, /** Make this action the default */ diff --git a/apps/files/src/utils/hashUtils.ts b/apps/files/src/utils/hashUtils.ts new file mode 100644 index 00000000000..55cf8b9f51a --- /dev/null +++ b/apps/files/src/utils/hashUtils.ts @@ -0,0 +1,28 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +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) +} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index c11b5820308..50f35fef5aa 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -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 diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index cc714964c9b..e5556e88958 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -175,7 +175,6 @@ export default { this.Navigation.setActive(view) logger.debug('Navigation changed', { id: view.id, view }) - // debugger this.showView(view, oldView) }, }, diff --git a/custom.d.ts b/custom.d.ts index 80fc7ccf9e1..7f6487fb835 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -19,7 +19,7 @@ * along with this program. If not, see . * */ -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 } -