diff options
author | John Molakvoæ <skjnldsv@protonmail.com> | 2023-04-18 09:43:29 +0200 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2023-04-20 09:06:57 +0200 |
commit | bb4d7969b93c806e4f578ecc5a6d04bb6bebee73 (patch) | |
tree | e6247c554d7e136042e0913f370f37ff1467ac37 /apps/files | |
parent | c85c04e4a8495eb04419a27a8e162c03acad6282 (diff) | |
download | nextcloud-server-bb4d7969b93c806e4f578ecc5a6d04bb6bebee73.tar.gz nextcloud-server-bb4d7969b93c806e4f578ecc5a6d04bb6bebee73.zip |
feat(files): add default action support
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files')
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 93 | ||||
-rw-r--r-- | apps/files/src/components/FilesListHeaderActions.vue | 11 | ||||
-rw-r--r-- | apps/files/src/main.js | 5 | ||||
-rw-r--r-- | apps/files/src/services/FileAction.ts | 7 | ||||
-rw-r--r-- | apps/files/src/utils/hashUtils.ts | 28 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 2 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.vue | 1 |
7 files changed, 113 insertions, 34 deletions
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 @@ <!-- 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 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<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 */ 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æ <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) +} 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) }, }, |