aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/FilesListTableHeaderActions.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/components/FilesListTableHeaderActions.vue')
-rw-r--r--apps/files/src/components/FilesListTableHeaderActions.vue149
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
}