diff options
Diffstat (limited to 'apps/files/src/components/FilesListTableHeaderActions.vue')
-rw-r--r-- | apps/files/src/components/FilesListTableHeaderActions.vue | 149 |
1 files changed, 134 insertions, 15 deletions
diff --git a/apps/files/src/components/FilesListTableHeaderActions.vue b/apps/files/src/components/FilesListTableHeaderActions.vue index 16d99f974dd..53b7e7ef21b 100644 --- a/apps/files/src/components/FilesListTableHeaderActions.vue +++ b/apps/files/src/components/FilesListTableHeaderActions.vue @@ -6,16 +6,26 @@ <div class="files-list__column files-list__row-actions-batch" data-cy-files-list-selection-actions> <NcActions ref="actionsMenu" container="#app-content-vue" + :boundaries-element="boundariesElement" :disabled="!!loading || areSomeNodesLoading" :force-name="true" - :inline="inlineActions" - :menu-name="inlineActions <= 1 ? t('files', 'Actions') : null" - :open.sync="openedMenu"> - <NcActionButton v-for="action in enabledActions" + :inline="enabledInlineActions.length" + :menu-name="enabledInlineActions.length <= 1 ? t('files', 'Actions') : null" + :open.sync="openedMenu" + @close="openedSubmenu = null"> + <!-- Default actions list--> + <NcActionButton v-for="action in enabledMenuActions" :key="action.id" - :aria-label="action.displayName(nodes, currentView) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */" - :class="'files-list__row-actions-batch-' + action.id" + :ref="`action-batch-${action.id}`" + :class="{ + [`files-list__row-actions-batch-${action.id}`]: true, + [`files-list__row-actions-batch--menu`]: isValidMenu(action) + }" + :close-after-click="!isValidMenu(action)" :data-cy-files-list-selection-action="action.id" + :is-menu="isValidMenu(action)" + :aria-label="action.displayName(nodes, currentView) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */" + :title="action.title?.(nodes, currentView)" @click="onActionClick(action)"> <template #icon> <NcLoadingIcon v-if="loading === action.id" :size="18" /> @@ -23,30 +33,61 @@ </template> {{ action.displayName(nodes, currentView) }} </NcActionButton> + + <!-- Submenu actions list--> + <template v-if="openedSubmenu && enabledSubmenuActions[openedSubmenu?.id]"> + <!-- Back to top-level button --> + <NcActionButton class="files-list__row-actions-batch-back" data-cy-files-list-selection-action="menu-back" @click="onBackToMenuClick(openedSubmenu)"> + <template #icon> + <ArrowLeftIcon /> + </template> + {{ t('files', 'Back') }} + </NcActionButton> + <NcActionSeparator /> + + <!-- Submenu actions --> + <NcActionButton v-for="action in enabledSubmenuActions[openedSubmenu?.id]" + :key="action.id" + :class="`files-list__row-actions-batch-${action.id}`" + class="files-list__row-actions-batch--submenu" + close-after-click + :data-cy-files-list-selection-action="action.id" + :aria-label="action.displayName(nodes, currentView) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */" + :title="action.title?.(nodes, currentView)" + @click="onActionClick(action)"> + <template #icon> + <NcLoadingIcon v-if="loading === action.id" :size="18" /> + <NcIconSvgWrapper v-else :svg="action.iconSvgInline(nodes, currentView)" /> + </template> + {{ action.displayName(nodes, currentView) }} + </NcActionButton> + </template> </NcActions> </div> </template> <script lang="ts"> -import type { Node, View } from '@nextcloud/files' +import type { FileAction, Node, View } from '@nextcloud/files' import type { PropType } from 'vue' import type { FileSource } from '../types' -import { NodeStatus, getFileActions } from '@nextcloud/files' +import { getFileActions, NodeStatus, DefaultType } from '@nextcloud/files' import { showError, showSuccess } from '@nextcloud/dialogs' import { translate } from '@nextcloud/l10n' import { defineComponent } from 'vue' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import { useRouteParameters } from '../composables/useRouteParameters.ts' import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useActionsMenuStore } from '../store/actionsmenu.ts' import { useFilesStore } from '../store/files.ts' import { useSelectionStore } from '../store/selection.ts' +import actionsMixins from '../mixins/actionsMixin.ts' import logger from '../logger.ts' // The registered actions list @@ -56,12 +97,15 @@ export default defineComponent({ name: 'FilesListTableHeaderActions', components: { + ArrowLeftIcon, NcActions, NcActionButton, NcIconSvgWrapper, NcLoadingIcon, }, + mixins: [actionsMixins], + props: { currentView: { type: Object as PropType<View>, @@ -80,6 +124,8 @@ export default defineComponent({ const fileListWidth = useFileListWidth() const { directory } = useRouteParameters() + const boundariesElement = document.getElementById('app-content-vue') + return { directory, fileListWidth, @@ -87,6 +133,8 @@ export default defineComponent({ actionsMenuStore, filesStore, selectionStore, + + boundariesElement, } }, @@ -97,13 +145,78 @@ export default defineComponent({ }, computed: { - enabledActions() { + enabledFileActions(): FileAction[] { return actions - .filter(action => action.execBatch) + // We don't handle renderInline actions in this component + .filter(action => !action.renderInline) + // We don't handle actions that are not visible + .filter(action => action.default !== DefaultType.HIDDEN) .filter(action => !action.enabled || action.enabled(this.nodes, this.currentView)) .sort((a, b) => (a.order || 0) - (b.order || 0)) }, + /** + * Return the list of enabled actions that are + * allowed to be rendered inlined. + * This means that they are not within a menu, nor + * being the parent of submenu actions. + */ + enabledInlineActions(): FileAction[] { + return this.enabledFileActions + // Remove all actions that are not top-level actions + .filter(action => action.parent === undefined) + // Remove all actions that are not batch actions + .filter(action => action.execBatch !== undefined) + // Remove all top-menu entries + .filter(action => !this.isValidMenu(action)) + // Return a maximum actions to fit the screen + .slice(0, this.inlineActions) + }, + + /** + * Return the rest of enabled actions that are not + * rendered inlined. + */ + enabledMenuActions(): FileAction[] { + // If we're in a submenu, only render the inline + // actions before the filtered submenu + if (this.openedSubmenu) { + return this.enabledInlineActions + } + + // We filter duplicates to prevent inline actions to be shown twice + const actions = this.enabledFileActions.filter((value, index, self) => { + return index === self.findIndex(action => action.id === value.id) + }) + + // Generate list of all top-level actions ids + const childrenActionsIds = actions.filter(action => action.parent).map(action => action.parent) as string[] + + const menuActions = actions + .filter(action => { + // If the action is not a batch action, we need + // to make sure it's a top-level parent entry + // and that we have some children actions bound to it + if (!action.execBatch) { + return childrenActionsIds.includes(action.id) + } + + // Rendering second-level actions is done in the template + // when openedSubmenu is set. + if (action.parent) { + return false + } + + return true + }) + .filter(action => !this.enabledInlineActions.includes(action)) + + // Make sure we render the inline actions first + // and then the rest of the actions. + // We do NOT want nested actions to be rendered inlined + return [...this.enabledInlineActions, ...menuActions] + }, + nodes() { return this.selectedNodes .map(source => this.getNode(source)) @@ -148,6 +261,12 @@ export default defineComponent({ }, async onActionClick(action) { + // If the action is a submenu, we open it + if (this.enabledSubmenuActions[action.id]) { + this.openedSubmenu = action + return + } + let displayName = action.id try { displayName = action.displayName(this.nodes, this.currentView) @@ -186,7 +305,7 @@ export default defineComponent({ return } - showError(this.t('files', '"{displayName}" failed on some elements ', { displayName })) + showError(this.t('files', '"{displayName}" failed on some elements', { displayName })) return } |