From 72ffd4999a82c242a7f82afd750372a8d49e517e Mon Sep 17 00:00:00 2001
From: John Molakvoæ <skjnldsv@protonmail.com>
Date: Fri, 20 Oct 2023 12:37:10 +0200
Subject: feat(files): support nested actions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
---
 .../src/components/FileEntry/FileEntryActions.vue  | 111 +++++++++++++++++----
 1 file changed, 94 insertions(+), 17 deletions(-)

(limited to 'apps/files/src/components/FileEntry/FileEntryActions.vue')

diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue
index 688524962f5..284806f7c94 100644
--- a/apps/files/src/components/FileEntry/FileEntryActions.vue
+++ b/apps/files/src/components/FileEntry/FileEntryActions.vue
@@ -39,20 +39,56 @@
 			:force-name="true"
 			:force-menu="enabledInlineActions.length === 0 /* forceMenu only if no inline actions */"
 			:inline="enabledInlineActions.length"
-			:open.sync="openedMenu">
-			<NcActionButton v-for="action in enabledMenuActions"
-				:key="action.id"
-				:class="'files-list__row-action-' + action.id"
-				:close-after-click="true"
-				:data-cy-files-list-row-action="action.id"
-				:title="action.title?.([source], currentView)"
-				@click="onActionClick(action)">
-				<template #icon>
-					<NcLoadingIcon v-if="loading === action.id" :size="18" />
-					<NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" />
-				</template>
-				{{ actionDisplayName(action) }}
-			</NcActionButton>
+			:open.sync="openedMenu"
+			@close="openedSubmenu = null">
+			<!-- Default actions list-->
+			<template>
+				<NcActionButton v-for="action in enabledMenuActions"
+					:key="action.id"
+					:class="{
+						[`files-list__row-action-${action.id}`]: true,
+						[`files-list__row-action--menu`]: isMenu(action.id)
+					}"
+					:close-after-click="!isMenu(action.id)"
+					:data-cy-files-list-row-action="action.id"
+					:title="action.title?.([source], currentView)"
+					@click="onActionClick(action)">
+					<template #icon>
+						<ChevronRightIcon v-if="isMenu(action.id)" />
+						<NcLoadingIcon v-else-if="loading === action.id" :size="18" />
+						<NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" />
+					</template>
+					{{ actionDisplayName(action) }}
+				</NcActionButton>
+			</template>
+
+			<!-- Submenu actions list-->
+			<template v-if="openedSubmenu && enabledSubmenuActions[openedSubmenu]">
+				<!-- Back to top-level button -->
+				<NcActionButton class="files-list__row-action-back" @click="openedSubmenu = ''">
+					<template #icon>
+						<ArrowLeftIcon />
+					</template>
+					{{ t('files', 'Back') }}
+				</NcActionButton>
+				<NcActionSeparator />
+
+				<!-- Submenu actions -->
+				<NcActionButton v-for="action in enabledSubmenuActions[openedSubmenu]"
+					:key="action.id"
+					:class="`files-list__row-action-${action.id}`"
+					class="files-list__row-action--submenu"
+					:close-after-click="true"
+					:data-cy-files-list-row-action="action.id"
+					:title="action.title?.([source], currentView)"
+					@click="onActionClick(action)">
+					<template #icon>
+						<NcLoadingIcon v-if="loading === action.id" :size="18" />
+						<NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" />
+					</template>
+					{{ actionDisplayName(action) }}
+				</NcActionButton>
+			</template>
 		</NcActions>
 	</td>
 </template>
@@ -63,8 +99,11 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
 import { translate as t } from '@nextcloud/l10n';
 import Vue, { PropType } from 'vue'
 
+import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
+import ChevronRightIcon from 'vue-material-design-icons/ChevronRight.vue'
 import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
 import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
+import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
 import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
 import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
 
@@ -78,11 +117,14 @@ export default Vue.extend({
 	name: 'FileEntryActions',
 
 	components: {
+		ArrowLeftIcon,
+		ChevronRightIcon,
+		CustomElementRender,
 		NcActionButton,
 		NcActions,
+		NcActionSeparator,
 		NcIconSvgWrapper,
 		NcLoadingIcon,
-		CustomElementRender,
 	},
 
 	props: {
@@ -108,8 +150,9 @@ export default Vue.extend({
 		},
 	},
 
-	setup() {
+	data() {
 		return {
+			openedSubmenu: '',
 		}
 	},
 
@@ -159,7 +202,13 @@ export default Vue.extend({
 
 		// Actions shown in the menu
 		enabledMenuActions() {
-			return [
+			// If we're in a submenu, only render the inline
+			// actions before the filtered submenu
+			if (this.openedSubmenu) {
+				return this.enabledInlineActions
+			}
+
+			const actions = [
 				// Showing inline first for the NcActions inline prop
 				...this.enabledInlineActions,
 				// Then the rest
@@ -168,6 +217,24 @@ export default Vue.extend({
 				// Then we filter duplicates to prevent inline actions to be shown twice
 				return index === self.findIndex(action => action.id === value.id)
 			})
+
+			// Generate list of all top-level actions ids
+			const topActionsIds = actions.filter(action => !action.parent).map(action => action.id) as string[]
+
+			// Filter actions that are not top-level AND have a valid parent
+			return actions.filter(action => !(action.parent && topActionsIds.includes(action.parent)))
+		},
+
+		enabledSubmenuActions() {
+			return this.enabledActions
+				.filter(action => action.parent)
+				.reduce((arr, action) => {
+					if (!arr[action.parent]) {
+						arr[action.parent] = []
+					}
+					arr[action.parent].push(action)
+					return arr
+				}, {} as Record<string, FileAction>)
 		},
 
 		openedMenu: {
@@ -201,6 +268,12 @@ export default Vue.extend({
 		},
 
 		async onActionClick(action) {
+			// If the action is a submenu, we open it
+			if (this.enabledSubmenuActions[action.id]) {
+				this.openedSubmenu = action.id
+				return
+			}
+
 			const displayName = action.displayName([this.source], this.currentView)
 			try {
 				// Set the loading marker
@@ -237,6 +310,10 @@ export default Vue.extend({
 			}
 		},
 
+		isMenu(id: string) {
+			return this.enabledSubmenuActions[id]?.length > 0
+		},
+
 		t,
 	},
 })
-- 
cgit v1.2.3

