diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2025-07-14 11:51:23 +0200 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2025-07-15 08:22:28 +0200 |
commit | c8dac22eb198df251b3cd9f969fd963699ae1965 (patch) | |
tree | 137aa18c23185019ba31d0d14a4e2f3f639dd249 | |
parent | c5b3768e21662663442cfe6b31c60b2701dc9de1 (diff) | |
download | nextcloud-server-fix/files-actions-subcomponent.tar.gz nextcloud-server-fix/files-actions-subcomponent.zip |
feat(files): show destructive actions as importantfix/files-actions-subcomponent
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r-- | apps/files/src/actions/deleteAction.ts | 1 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry/FileEntryAction.vue | 168 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry/FileEntryActions.vue | 112 | ||||
-rw-r--r-- | apps/files/src/components/FileEntryMixin.ts | 12 |
4 files changed, 222 insertions, 71 deletions
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index 1f0665ced8f..63f7fd442c5 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -112,5 +112,6 @@ export const action = new FileAction({ return Promise.all(promises) }, + destructive: true, order: 100, }) diff --git a/apps/files/src/components/FileEntry/FileEntryAction.vue b/apps/files/src/components/FileEntry/FileEntryAction.vue new file mode 100644 index 00000000000..70ac7a61965 --- /dev/null +++ b/apps/files/src/components/FileEntry/FileEntryAction.vue @@ -0,0 +1,168 @@ +<!-- + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcActionButton :ref="buttonRef" + class="files-list__row-action" + :class="buttonClasses" + :close-after-click="variant !== 'menu'" + :data-cy-files-list-row-action="action.id" + :is-menu="variant === 'menu'" + :aria-label="actionTitle" + :title="actionTitle" + @click="onActionClick"> + <template #icon> + <NcLoadingIcon v-if="isLoading" size="18" /> + <NcIconSvgWrapper v-else + class="files-list__row-action-icon" + :svg="action.iconSvgInline([source], currentView)" /> + </template> + {{ displayName }} + </NcActionButton> +</template> + +<script lang="ts"> +import type { PropType } from 'vue' +import type { FileAction, Node } from '@nextcloud/files' + +import { defineComponent } from 'vue' + +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' + +import { useFileListWidth } from '../../composables/useFileListWidth.ts' +import { useNavigation } from '../../composables/useNavigation.ts' +import logger from '../../logger.ts' + +export default defineComponent({ + name: 'FileEntryAction', + + components: { + NcActionButton, + NcIconSvgWrapper, + NcLoadingIcon, + }, + + props: { + action: { + type: Object as PropType<FileAction>, + required: true, + }, + source: { + type: Object as PropType<Node>, + required: true, + }, + isLoading: { + type: Boolean, + default: false, + }, + isMenu: { + type: Boolean, + default: false, + }, + gridMode: { + type: Boolean, + default: false, + }, + variant: { + type: String as PropType<'inline'|'menu'|'submenu'>, + default: undefined, + }, + }, + + emits: ['click'], + + setup() { + // The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag + const { currentView } = useNavigation(true) + const filesListWidth = useFileListWidth() + + return { + currentView, + filesListWidth, + } + }, + + computed: { + buttonRef() { + return `action-${this.action.id}` + }, + + buttonClasses() { + const classes = { + [`files-list__row-action-${this.action.id}`]: true, + } + + switch (this.variant) { + case 'inline': + classes['files-list__row-action--inline'] = true + break + case 'submenu': + classes['files-list__row-action--submenu'] = true + break + case 'menu': + classes['files-list__row-action--menu'] = true + break + } + + return classes + }, + + actionTitle() { + try { + return this.action.title?.([this.source], this.currentView) + } catch (error) { + logger.error('Error while getting action title', { action: this.action, error }) + return this.action.id + } + }, + + displayName() { + try { + if ((this.gridMode || (this.filesListWidth < 768 && this.action.inline)) && typeof this.action.title === 'function') { + // if an inline action is rendered in the menu for + // lack of space we use the title first if defined + const title = this.action.title([this.source], this.currentView) + if (title) return title + } + return this.action.displayName([this.source], this.currentView) + } catch (error) { + logger.error('Error while getting action display name', { action: this.action, error }) + // Not ideal, but better than nothing + return this.action.id + } + }, + }, + + methods: { + onActionClick() { + this.$emit('click', this.action) + }, + }, +}) +</script> + +<style scoped lang="scss"> +.files-list__row-action { + --max-icon-size: calc(var(--default-clickable-area) - 2 * var(--default-grid-baseline)); + + // inline icons can have clickable area size so they still fit into the row + &.files-list__row-action--inline { + --max-icon-size: var(--default-clickable-area); + } + // destructive actions should be highlighted in red + &.files-list__row-action--destructive { + ::deep(button) { + color: var(--color-error) !important; + } + } + + // Some icons exceed the default size so we need to enforce a max width and height + .files-list__row-action-icon :deep(svg) { + max-height: var(--max-icon-size) !important; + max-width: var(--max-icon-size) !important; + } +} +</style> diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue index ec111a1235d..4e9c7dbd8b0 100644 --- a/apps/files/src/components/FileEntry/FileEntryActions.vue +++ b/apps/files/src/components/FileEntry/FileEntryActions.vue @@ -26,29 +26,29 @@ @close="onMenuClose" @closed="onMenuClosed"> <!-- Default actions list--> - <NcActionButton v-for="action, index in enabledMenuActions" + <FileEntryAction v-for="(action, index) in renderedNonDestructiveActions" :key="action.id" - :ref="`action-${action.id}`" - class="files-list__row-action" - :class="{ - [`files-list__row-action-${action.id}`]: true, - 'files-list__row-action--inline': index < enabledInlineActions.length, - 'files-list__row-action--menu': isValidMenu(action) - }" - :close-after-click="!isValidMenu(action)" - :data-cy-files-list-row-action="action.id" + :action="action" + :grid-mode="gridMode" + :is-loading="isLoadingAction(action)" :is-menu="isValidMenu(action)" - :aria-label="action.title?.([source], currentView)" - :title="action.title?.([source], currentView)" - @click="onActionClick(action)"> - <template #icon> - <NcLoadingIcon v-if="isLoadingAction(action)" /> - <NcIconSvgWrapper v-else - class="files-list__row-action-icon" - :svg="action.iconSvgInline([source], currentView)" /> - </template> - {{ actionDisplayName(action) }} - </NcActionButton> + :source="source" + :variant="(index < enabledInlineActions.length) ? 'inline' : undefined" + @click="onActionClick(action)" /> + + <template v-if="renderedDestructiveActions.length"> + <NcActionSeparator /> + + <!-- Destructive actions list--> + <FileEntryAction v-for="action in renderedDestructiveActions" + :key="action.id" + :action="action" + :grid-mode="gridMode" + :is-loading="isLoadingAction(action)" + :source="source" + :variant="undefined /* inline already rendered above */" + @click="onActionClick(action)" /> + </template> <!-- Submenu actions list--> <template v-if="openedSubmenu && enabledSubmenuActions[openedSubmenu?.id]"> @@ -62,20 +62,14 @@ <NcActionSeparator /> <!-- Submenu actions --> - <NcActionButton v-for="action in enabledSubmenuActions[openedSubmenu?.id]" + <FileEntryAction v-for="action in enabledSubmenuActions[openedSubmenu?.id]" :key="action.id" - :class="`files-list__row-action-${action.id}`" - class="files-list__row-action--submenu" - close-after-click - :data-cy-files-list-row-action="action.id" - :title="action.title?.([source], currentView)" - @click="onActionClick(action)"> - <template #icon> - <NcLoadingIcon v-if="isLoadingAction(action)" :size="18" /> - <NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" /> - </template> - {{ actionDisplayName(action) }} - </NcActionButton> + :action="action" + :grid-mode="gridMode" + :is-loading="isLoadingAction(action)" + :source="source" + variant="submenu" + @click="onActionClick(action)" /> </template> </NcActions> </td> @@ -85,18 +79,17 @@ import type { PropType } from 'vue' import type { FileAction, Node } from '@nextcloud/files' -import { DefaultType, NodeStatus } from '@nextcloud/files' +import { DefaultType } from '@nextcloud/files' import { defineComponent, inject } from 'vue' import { t } from '@nextcloud/l10n' import { useHotKey } from '@nextcloud/vue/composables/useHotKey' import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue' import CustomElementRender from '../CustomElementRender.vue' +import FileEntryAction from './FileEntryAction.vue' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActions from '@nextcloud/vue/components/NcActions' import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator' -import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' -import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import { executeAction } from '../../utils/actionUtils.ts' import { useActiveStore } from '../../store/active.ts' @@ -112,11 +105,10 @@ export default defineComponent({ components: { ArrowLeftIcon, CustomElementRender, + FileEntryAction, NcActionButton, NcActions, NcActionSeparator, - NcIconSvgWrapper, - NcLoadingIcon, }, mixins: [actionsMixins], @@ -151,6 +143,9 @@ export default defineComponent({ enabledFileActions, filesListWidth, t, + // Allow nested components to be detected as valid NcActionButton + // https://github.com/nextcloud-libraries/nextcloud-vue/blob/a498a5df4f8abbcf3e9ef1f581f379098b4ede6d/src/components/NcActions/NcActions.vue#L1260-L1279 + type: { name: NcActionButton }, } }, @@ -159,10 +154,6 @@ export default defineComponent({ return this.activeStore?.activeNode?.source === this.source.source }, - isLoading() { - return this.source.status === NodeStatus.LOADING - }, - // Enabled action that are displayed inline enabledInlineActions() { if (this.filesListWidth < 768 || this.gridMode) { @@ -211,6 +202,14 @@ export default defineComponent({ return actions.filter(action => !(action.parent && topActionsIds.includes(action.parent))) }, + renderedNonDestructiveActions() { + return this.enabledMenuActions.filter(action => !action.destructive) + }, + + renderedDestructiveActions() { + return this.enabledMenuActions.filter(action => action.destructive) + }, + openedMenu: { get() { return this.opened @@ -250,22 +249,6 @@ export default defineComponent({ }, methods: { - actionDisplayName(action: FileAction) { - try { - if ((this.gridMode || (this.filesListWidth < 768 && action.inline)) && typeof action.title === 'function') { - // if an inline action is rendered in the menu for - // lack of space we use the title first if defined - const title = action.title([this.source], this.currentView) - if (title) return title - } - return action.displayName([this.source], this.currentView) - } catch (error) { - logger.error('Error while getting action display name', { action, error }) - // Not ideal, but better than nothing - return action.id - } - }, - isLoadingAction(action: FileAction) { if (!this.isActive) { return false @@ -338,16 +321,5 @@ main.app-content[style*="mouse-pos-x"] .v-popper__popper { <style scoped lang="scss"> .files-list__row-action { --max-icon-size: calc(var(--default-clickable-area) - 2 * var(--default-grid-baseline)); - - // inline icons can have clickable area size so they still fit into the row - &.files-list__row-action--inline { - --max-icon-size: var(--default-clickable-area); - } - - // Some icons exceed the default size so we need to enforce a max width and height - .files-list__row-action-icon :deep(svg) { - max-height: var(--max-icon-size) !important; - max-width: var(--max-icon-size) !important; - } } </style> diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts index 735490c45b3..e8adefc3890 100644 --- a/apps/files/src/components/FileEntryMixin.ts +++ b/apps/files/src/components/FileEntryMixin.ts @@ -250,7 +250,17 @@ export default defineComponent({ return false } }) - .sort((a, b) => (a.order || 0) - (b.order || 0)) + .sort((a, b) => { + // Sort destructive actions to the end + if (a.destructive && !b.destructive) { + return 1 + } else if (!a.destructive && b.destructive) { + return -1 + } + + // Sort by order, if not defined, use 0 + return (a.order || 0) - (b.order || 0) + }) }, defaultFileAction() { |