Skip to content

Commit 9dbac0d

Browse files
committed
feat(files): support nested actions
Signed-off-by: John Molakvoæ <[email protected]>
1 parent 694352e commit 9dbac0d

2 files changed

Lines changed: 132 additions & 18 deletions

File tree

apps/files/src/components/FileEntry/FileEntryActions.vue

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,56 @@
3939
:force-name="true"
4040
:force-menu="enabledInlineActions.length === 0 /* forceMenu only if no inline actions */"
4141
:inline="enabledInlineActions.length"
42-
:open.sync="openedMenu">
43-
<NcActionButton v-for="action in enabledMenuActions"
44-
:key="action.id"
45-
:class="'files-list__row-action-' + action.id"
46-
:close-after-click="true"
47-
:data-cy-files-list-row-action="action.id"
48-
:title="action.title?.([source], currentView)"
49-
@click="onActionClick(action)">
50-
<template #icon>
51-
<NcLoadingIcon v-if="loading === action.id" :size="18" />
52-
<NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" />
53-
</template>
54-
{{ actionDisplayName(action) }}
55-
</NcActionButton>
42+
:open.sync="openedMenu"
43+
@close="openedSubmenu = null">
44+
<!-- Default actions list-->
45+
<template>
46+
<NcActionButton v-for="action in enabledMenuActions"
47+
:key="action.id"
48+
:class="{
49+
[`files-list__row-action-${action.id}`]: true,
50+
[`files-list__row-action--menu`]: isMenu(action.id)
51+
}"
52+
:close-after-click="!isMenu(action.id)"
53+
:data-cy-files-list-row-action="action.id"
54+
:title="action.title?.([source], currentView)"
55+
@click="onActionClick(action)">
56+
<template #icon>
57+
<ChevronRightIcon v-if="isMenu(action.id)" />
58+
<NcLoadingIcon v-else-if="loading === action.id" :size="18" />
59+
<NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" />
60+
</template>
61+
{{ actionDisplayName(action) }}
62+
</NcActionButton>
63+
</template>
64+
65+
<!-- Submenu actions list-->
66+
<template v-if="openedSubmenu && enabledSubmenuActions[openedSubmenu]">
67+
<!-- Back to top-level button -->
68+
<NcActionButton class="files-list__row-action-back" @click="openedSubmenu = ''">
69+
<template #icon>
70+
<ChevronLeftIcon />
71+
</template>
72+
{{ t('files', 'Back') }}
73+
</NcActionButton>
74+
<NcActionSeparator />
75+
76+
<!-- Submenu actions -->
77+
<NcActionButton v-for="action in enabledSubmenuActions[openedSubmenu]"
78+
:key="action.id"
79+
:class="`files-list__row-action-${action.id}`"
80+
class="files-list__row-action--submenu"
81+
:close-after-click="true"
82+
:data-cy-files-list-row-action="action.id"
83+
:title="action.title?.([source], currentView)"
84+
@click="onActionClick(action)">
85+
<template #icon>
86+
<NcLoadingIcon v-if="loading === action.id" :size="18" />
87+
<NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" />
88+
</template>
89+
{{ actionDisplayName(action) }}
90+
</NcActionButton>
91+
</template>
5692
</NcActions>
5793
</td>
5894
</template>
@@ -63,8 +99,11 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
6399
import { translate as t } from '@nextcloud/l10n';
64100
import Vue, { PropType } from 'vue'
65101
102+
import ChevronLeftIcon from 'vue-material-design-icons/ChevronLeft.vue'
103+
import ChevronRightIcon from 'vue-material-design-icons/ChevronRight.vue'
66104
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
67105
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
106+
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
68107
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
69108
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
70109
@@ -78,11 +117,14 @@ export default Vue.extend({
78117
name: 'FileEntryActions',
79118
80119
components: {
120+
ChevronLeftIcon,
121+
ChevronRightIcon,
122+
CustomElementRender,
81123
NcActionButton,
82124
NcActions,
125+
NcActionSeparator,
83126
NcIconSvgWrapper,
84127
NcLoadingIcon,
85-
CustomElementRender,
86128
},
87129
88130
props: {
@@ -108,8 +150,9 @@ export default Vue.extend({
108150
},
109151
},
110152
111-
setup() {
153+
data() {
112154
return {
155+
openedSubmenu: '',
113156
}
114157
},
115158
@@ -159,7 +202,13 @@ export default Vue.extend({
159202
160203
// Actions shown in the menu
161204
enabledMenuActions() {
162-
return [
205+
// If we're in a submenu, only render the inline
206+
// actions before the filtered submenu
207+
if (this.openedSubmenu) {
208+
return this.enabledInlineActions
209+
}
210+
211+
const actions = [
163212
// Showing inline first for the NcActions inline prop
164213
...this.enabledInlineActions,
165214
// Then the rest
@@ -168,6 +217,24 @@ export default Vue.extend({
168217
// Then we filter duplicates to prevent inline actions to be shown twice
169218
return index === self.findIndex(action => action.id === value.id)
170219
})
220+
221+
// Generate list of all top-level actions ids
222+
const topActionsIds = actions.filter(action => !action.parent).map(action => action.id) as string[]
223+
224+
// Filter actions that are not top-level AND have a valid parent
225+
return actions.filter(action => !(action.parent && topActionsIds.includes(action.parent)))
226+
},
227+
228+
enabledSubmenuActions() {
229+
return this.enabledActions
230+
.filter(action => action.parent)
231+
.reduce((arr, action) => {
232+
if (!arr[action.parent]) {
233+
arr[action.parent] = []
234+
}
235+
arr[action.parent].push(action)
236+
return arr
237+
}, {} as Record<string, FileAction>)
171238
},
172239
173240
openedMenu: {
@@ -201,6 +268,12 @@ export default Vue.extend({
201268
},
202269
203270
async onActionClick(action) {
271+
// If the action is a submenu, we open it
272+
if (this.enabledSubmenuActions[action.id]) {
273+
this.openedSubmenu = action.id
274+
return
275+
}
276+
204277
const displayName = action.displayName([this.source], this.currentView)
205278
try {
206279
// Set the loading marker
@@ -237,6 +310,10 @@ export default Vue.extend({
237310
}
238311
},
239312
313+
isMenu(id: string) {
314+
return this.enabledSubmenuActions[id]?.length > 0
315+
},
316+
240317
t,
241318
},
242319
})

apps/files/src/init.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
* along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
*
2121
*/
22-
import { addNewFileMenuEntry, registerFileAction } from '@nextcloud/files'
22+
import MenuIcon from '@mdi/svg/svg/sun-compass.svg?raw'
23+
import { FileAction, addNewFileMenuEntry, registerFileAction } from '@nextcloud/files'
2324

2425
import { action as deleteAction } from './actions/deleteAction'
2526
import { action as downloadAction } from './actions/downloadAction'
@@ -62,3 +63,39 @@ registerRecentView()
6263

6364
// Register preview service worker
6465
registerPreviewServiceWorker()
66+
67+
registerFileAction(new FileAction({
68+
id: 'menu',
69+
displayName: () => 'Menu',
70+
iconSvgInline: () => MenuIcon,
71+
exec: async () => true,
72+
}))
73+
74+
registerFileAction(new FileAction({
75+
id: 'submenu1',
76+
displayName: () => 'Submenu 1',
77+
iconSvgInline: () => MenuIcon,
78+
exec: async () => alert('Hello 1'),
79+
parent: 'menu',
80+
}))
81+
registerFileAction(new FileAction({
82+
id: 'submenu2',
83+
displayName: () => 'Submenu 2',
84+
iconSvgInline: () => MenuIcon,
85+
exec: async () => alert('Hello 2'),
86+
parent: 'menu',
87+
}))
88+
registerFileAction(new FileAction({
89+
id: 'submenu3',
90+
displayName: () => 'Submenu 3',
91+
iconSvgInline: () => MenuIcon,
92+
exec: async () => alert('Hello 3'),
93+
parent: 'menu',
94+
}))
95+
registerFileAction(new FileAction({
96+
id: 'submenu4',
97+
displayName: () => 'Submenu 4',
98+
iconSvgInline: () => MenuIcon,
99+
exec: async () => alert('Hello 4'),
100+
parent: 'menu',
101+
}))

0 commit comments

Comments
 (0)