diff options
Diffstat (limited to 'apps/files/src/components/FileEntry')
5 files changed, 130 insertions, 90 deletions
diff --git a/apps/files/src/components/FileEntry/FavoriteIcon.vue b/apps/files/src/components/FileEntry/FavoriteIcon.vue index 84f9fd828fc..c66cb8fbd7f 100644 --- a/apps/files/src/components/FileEntry/FavoriteIcon.vue +++ b/apps/files/src/components/FileEntry/FavoriteIcon.vue @@ -11,7 +11,7 @@ import { translate as t } from '@nextcloud/l10n' import { defineComponent } from 'vue' import StarSvg from '@mdi/svg/svg/star.svg?raw' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' /** * A favorite icon to be used for overlaying favorite entries like the file preview / icon @@ -56,8 +56,8 @@ export default defineComponent({ :deep() { svg { // We added a stroke for a11y so we must increase the size to include the stroke - width: 26px !important; - height: 26px !important; + width: 20px !important; + height: 20px !important; // Override NcIconSvgWrapper defaults of 20px max-width: unset !important; diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue index 4e7bec88452..5c537d878fe 100644 --- a/apps/files/src/components/FileEntry/FileEntryActions.vue +++ b/apps/files/src/components/FileEntry/FileEntryActions.vue @@ -22,33 +22,68 @@ type="tertiary" :force-menu="enabledInlineActions.length === 0 /* forceMenu only if no inline actions */" :inline="enabledInlineActions.length" - :open.sync="openedMenu" - @close="openedSubmenu = null"> - <!-- Default actions list--> - <NcActionButton v-for="action in enabledMenuActions" + :open="openedMenu" + @close="onMenuClose" + @closed="onMenuClosed"> + <!-- Non-destructive actions list --> + <!-- Please keep this block in sync with the destructive actions block below --> + <NcActionButton 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--menu`]: isMenu(action.id) + 'files-list__row-action--inline': index < enabledInlineActions.length, + 'files-list__row-action--menu': isValidMenu(action), }" - :close-after-click="!isMenu(action.id)" + :close-after-click="!isValidMenu(action)" :data-cy-files-list-row-action="action.id" - :is-menu="isMenu(action.id)" + :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)" :size="18" /> - <NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" /> + <NcLoadingIcon v-if="isLoadingAction(action)" /> + <NcIconSvgWrapper v-else + class="files-list__row-action-icon" + :svg="action.iconSvgInline([source], currentView)" /> </template> - {{ mountType === 'shared' && action.id === 'sharing-status' ? '' : actionDisplayName(action) }} + {{ actionDisplayName(action) }} </NcActionButton> + <!-- Destructive actions list --> + <template v-if="renderedDestructiveActions.length > 0"> + <NcActionSeparator /> + <NcActionButton v-for="action, index in renderedDestructiveActions" + :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), + 'files-list__row-action--destructive': true, + }" + :close-after-click="!isValidMenu(action)" + :data-cy-files-list-row-action="action.id" + :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> + </template> + <!-- Submenu actions list--> <template v-if="openedSubmenu && enabledSubmenuActions[openedSubmenu?.id]"> <!-- Back to top-level button --> - <NcActionButton class="files-list__row-action-back" @click="onBackToMenuClick(openedSubmenu)"> + <NcActionButton class="files-list__row-action-back" data-cy-files-list-row-action="menu-back" @click="onBackToMenuClick(openedSubmenu)"> <template #icon> <ArrowLeftIcon /> </template> @@ -63,10 +98,11 @@ class="files-list__row-action--submenu" close-after-click :data-cy-files-list-row-action="action.id" + :aria-label="action.title?.([source], currentView)" :title="action.title?.([source], currentView)" @click="onActionClick(action)"> <template #icon> - <NcLoadingIcon v-if="isLoadingAction(action)" :size="18" /> + <NcLoadingIcon v-if="isLoadingAction(action)" /> <NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" /> </template> {{ actionDisplayName(action) }} @@ -82,22 +118,23 @@ import type { FileAction, Node } from '@nextcloud/files' import { DefaultType, NodeStatus } from '@nextcloud/files' import { defineComponent, inject } from 'vue' -import { translate as t } from '@nextcloud/l10n' +import { t } from '@nextcloud/l10n' +import { useHotKey } from '@nextcloud/vue/composables/useHotKey' -import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js' import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue' import CustomElementRender from '../CustomElementRender.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' +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' import { useFileListWidth } from '../../composables/useFileListWidth.ts' import { useNavigation } from '../../composables/useNavigation' import { useRouteParameters } from '../../composables/useRouteParameters.ts' +import actionsMixins from '../../mixins/actionsMixin.ts' import logger from '../../logger.ts' export default defineComponent({ @@ -113,6 +150,8 @@ export default defineComponent({ NcLoadingIcon, }, + mixins: [actionsMixins], + props: { opened: { type: Boolean, @@ -146,12 +185,6 @@ export default defineComponent({ } }, - data() { - return { - openedSubmenu: null as FileAction | null, - } - }, - computed: { isActive() { return this.activeStore?.activeNode?.source === this.source.source @@ -209,16 +242,12 @@ export default defineComponent({ return actions.filter(action => !(action.parent && topActionsIds.includes(action.parent))) }, - enabledSubmenuActions() { - return this.enabledFileActions - .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[]>) + renderedNonDestructiveActions() { + return this.enabledMenuActions.filter(action => !action.destructive) + }, + + renderedDestructiveActions() { + return this.enabledMenuActions.filter(action => action.destructive) }, openedMenu: { @@ -238,14 +267,10 @@ export default defineComponent({ getBoundariesElement() { return document.querySelector('.app-content > .files-list') }, - - mountType() { - return this.source.attributes['mount-type'] - }, }, watch: { - // Close any submenu when the menu is closed + // Close any submenu when the menu state changes openedMenu() { this.openedSubmenu = null }, @@ -287,7 +312,7 @@ export default defineComponent({ return this.activeStore?.activeAction?.id === action.id }, - async onActionClick(action, isSubmenu = false) { + async onActionClick(action) { // If the action is a submenu, we open it if (this.enabledSubmenuActions[action.id]) { this.openedSubmenu = action @@ -295,34 +320,10 @@ export default defineComponent({ } // Make sure we set the node as active - this.activeStore.setActiveNode(this.source) + this.activeStore.activeNode = this.source // Execute the action await executeAction(action) - - // If that was a submenu, we just go back after the action - if (isSubmenu) { - this.openedSubmenu = null - } - }, - - isMenu(id: string) { - return this.enabledSubmenuActions[id]?.length > 0 - }, - - async onBackToMenuClick(action: FileAction) { - this.openedSubmenu = null - // Wait for first render - await this.$nextTick() - - // Focus the previous menu action button - this.$nextTick(() => { - // Focus the action button - const menuAction = this.$refs[`action-${action.id}`]?.[0] - if (menuAction) { - menuAction.$el.querySelector('button')?.focus() - } - }) }, onKeyDown(event: KeyboardEvent) { @@ -341,6 +342,16 @@ export default defineComponent({ this.openedMenu = true } }, + + onMenuClose() { + // We reset the submenu state when the menu is closing + this.openedSubmenu = null + }, + + onMenuClosed() { + // We reset the actions menu state when the menu is finally closed + this.openedMenu = false + }, }, }) </script> @@ -363,13 +374,26 @@ main.app-content[style*="mouse-pos-x"] .v-popper__popper { } </style> -<style lang="scss" scoped> -:deep(.button-vue--icon-and-text, .files-list__row-action-sharing-status) { - .button-vue__text { - color: var(--color-primary-element); +<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); } - .button-vue__icon { - color: var(--color-primary-element); + + // 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; + } + + &.files-list__row-action--destructive { + ::deep(button) { + color: var(--color-error) !important; + } } } + </style> diff --git a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue index 1b6112373f4..5b80a971118 100644 --- a/apps/files/src/components/FileEntry/FileEntryCheckbox.vue +++ b/apps/files/src/components/FileEntry/FileEntryCheckbox.vue @@ -21,11 +21,11 @@ import type { FileSource } from '../../types.ts' import { FileType } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' +import { useHotKey } from '@nextcloud/vue/composables/useHotKey' import { defineComponent } from 'vue' -import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import { useActiveStore } from '../../store/active.ts' import { useKeyboardStore } from '../../store/keyboard.ts' diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue index 2a727b55c29..418f9581eb6 100644 --- a/apps/files/src/components/FileEntry/FileEntryName.vue +++ b/apps/files/src/components/FileEntry/FileEntryName.vue @@ -23,7 +23,6 @@ <component :is="linkTo.is" v-else ref="basename" - :aria-hidden="isRenaming" class="files-list__row-name-link" data-cy-files-list-row-name-link v-bind="linkTo.params"> @@ -31,7 +30,7 @@ <span class="files-list__row-name-text" dir="auto"> <!-- Keep the filename stuck to the extension to avoid whitespace rendering issues--> <span class="files-list__row-name-" v-text="basename" /> - <span class="files-list__row-name-ext" v-text="extension" /> + <span v-if="userConfigStore.userConfig.show_files_extensions" class="files-list__row-name-ext" v-text="extension" /> </span> </component> </template> @@ -45,13 +44,14 @@ import { FileType, NodeStatus } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import { defineComponent, inject } from 'vue' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' +import NcTextField from '@nextcloud/vue/components/NcTextField' -import { useNavigation } from '../../composables/useNavigation' +import { getFilenameValidity } from '../../utils/filenameValidity.ts' import { useFileListWidth } from '../../composables/useFileListWidth.ts' -import { useRouteParameters } from '../../composables/useRouteParameters.ts' +import { useNavigation } from '../../composables/useNavigation.ts' import { useRenamingStore } from '../../store/renaming.ts' -import { getFilenameValidity } from '../../utils/filenameValidity.ts' +import { useRouteParameters } from '../../composables/useRouteParameters.ts' +import { useUserConfigStore } from '../../store/userconfig.ts' import logger from '../../logger.ts' export default defineComponent({ @@ -96,6 +96,7 @@ export default defineComponent({ const { directory } = useRouteParameters() const filesListWidth = useFileListWidth() const renamingStore = useRenamingStore() + const userConfigStore = useUserConfigStore() const defaultFileAction = inject<FileAction | undefined>('defaultFileAction') @@ -106,6 +107,7 @@ export default defineComponent({ filesListWidth, renamingStore, + userConfigStore, } }, @@ -117,11 +119,11 @@ export default defineComponent({ return this.isRenaming && this.filesListWidth < 512 }, newName: { - get() { - return this.renamingStore.newName + get(): string { + return this.renamingStore.newNodeName }, - set(newName) { - this.renamingStore.newName = newName + set(newName: string) { + this.renamingStore.newNodeName = newName }, }, @@ -249,7 +251,9 @@ export default defineComponent({ try { const status = await this.renamingStore.rename() if (status) { - showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName })) + showSuccess( + t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName: this.source.basename }), + ) this.$nextTick(() => { const nameContainer = this.$refs.basename as HTMLElement | undefined nameContainer?.focus() diff --git a/apps/files/src/components/FileEntry/FileEntryPreview.vue b/apps/files/src/components/FileEntry/FileEntryPreview.vue index 2d5844f851f..3d0fffe7584 100644 --- a/apps/files/src/components/FileEntry/FileEntryPreview.vue +++ b/apps/files/src/components/FileEntry/FileEntryPreview.vue @@ -21,6 +21,7 @@ class="files-list__row-icon-blurhash" aria-hidden="true" /> <img v-if="backgroundFailed !== true" + :key="source.fileid" ref="previewImg" alt="" class="files-list__row-icon-preview" @@ -63,7 +64,7 @@ import FolderIcon from 'vue-material-design-icons/Folder.vue' import FolderOpenIcon from 'vue-material-design-icons/FolderOpen.vue' import KeyIcon from 'vue-material-design-icons/Key.vue' import LinkIcon from 'vue-material-design-icons/Link.vue' -import NetworkIcon from 'vue-material-design-icons/Network.vue' +import NetworkIcon from 'vue-material-design-icons/NetworkOutline.vue' import TagIcon from 'vue-material-design-icons/Tag.vue' import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue' @@ -147,6 +148,17 @@ export default defineComponent({ return null } + if (this.source.attributes['has-preview'] !== true + && this.source.mime !== undefined + && this.source.mime !== 'application/octet-stream' + ) { + const previewUrl = generateUrl('/core/mimeicon?mime={mime}', { + mime: this.source.mime, + }) + const url = new URL(window.location.origin + previewUrl) + return url.href + } + try { const previewUrl = this.source.attributes.previewUrl || (this.isPublic |