diff options
author | John Molakvoæ <skjnldsv@protonmail.com> | 2023-10-12 15:15:20 +0200 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2023-10-17 11:19:02 +0200 |
commit | 64c32f714894d2c884c82922a5349bbe64a55be8 (patch) | |
tree | 0653f9c4a287f7fd5bb1d5faba04bed22d238dc2 /apps/files/src | |
parent | 0f1f73478a59f59d92c4542aa42dc61973c600de (diff) | |
download | nextcloud-server-64c32f714894d2c884c82922a5349bbe64a55be8.tar.gz nextcloud-server-64c32f714894d2c884c82922a5349bbe64a55be8.zip |
fix(files): split FileEntry Actions
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 243 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry/FileEntryActions.vue | 238 |
2 files changed, 292 insertions, 189 deletions
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 87c36356210..7ff6186a6e3 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -86,44 +86,14 @@ </td> <!-- Actions --> - <td v-show="!isRenamingSmallScreen" + <FileEntryActions v-show="!isRenamingSmallScreen" + ref="actions" :class="`files-list__row-actions-${uniqueId}`" - class="files-list__row-actions" - data-cy-files-list-row-actions> - <!-- Render actions --> - <CustomElementRender v-for="action in enabledRenderActions" - :key="action.id" - :class="'files-list__row-action-' + action.id" - :current-view="currentView" - :render="action.renderInline" - :source="source" - class="files-list__row-action--inline" /> - - <!-- Menu actions --> - <NcActions v-if="visible" - ref="actionsMenu" - :boundaries-element="getBoundariesElement()" - :container="getBoundariesElement()" - :disabled="isLoading" - :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> - </NcActions> - </td> + :files-list-width="filesListWidth" + :loading.sync="loading" + :opened.sync="openedMenu" + :source="source" + :visible="visible" /> <!-- Size --> <td v-if="isSizeAvailable" @@ -163,7 +133,7 @@ import type { PropType } from 'vue' import { emit } from '@nextcloud/event-bus' import { extname, join } from 'path' -import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File as NcFile, FileAction, NodeStatus, Node } from '@nextcloud/files' +import { FileType, formatFileSize, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files' import { getUploader } from '@nextcloud/upload' import { loadState } from '@nextcloud/initial-state' import { showError, showSuccess } from '@nextcloud/dialogs' @@ -173,30 +143,24 @@ import axios from '@nextcloud/axios' import moment from '@nextcloud/moment' import Vue 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 NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import { action as sidebarAction } from '../actions/sidebarAction.ts' import { getDragAndDropPreview } from '../utils/dragUtils.ts' import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction.ts' -import { MoveCopyAction } from '../actions/moveOrCopyActionUtils.ts' import { hashCode } from '../utils/hashUtils.ts' +import { MoveCopyAction } from '../actions/moveOrCopyActionUtils.ts' import { useActionsMenuStore } from '../store/actionsmenu.ts' import { useDragAndDropStore } from '../store/dragging.ts' import { useFilesStore } from '../store/files.ts' import { useRenamingStore } from '../store/renaming.ts' import { useSelectionStore } from '../store/selection.ts' import CustomElementRender from './CustomElementRender.vue' +import FileEntryActions from './FileEntry/FileEntryActions.vue' import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue' import FileEntryPreview from './FileEntry/FileEntryPreview.vue' import logger from '../logger.js' -// The registered actions list -const actions = getFileActions() - Vue.directive('onClickOutside', vOnClickOutside) const forbiddenCharacters = loadState('files', 'forbiddenCharacters', '') as string @@ -206,11 +170,8 @@ export default Vue.extend({ components: { CustomElementRender, + FileEntryActions, FileEntryPreview, - NcActionButton, - NcActions, - NcIconSvgWrapper, - NcLoadingIcon, NcTextField, FileEntryCheckbox, }, @@ -267,8 +228,8 @@ export default Vue.extend({ }, computed: { - currentView() { - return this.$navigation.active + currentView(): View { + return this.$navigation.active as View }, columns() { // Hide columns if the list is too small @@ -288,6 +249,12 @@ export default Vue.extend({ fileid() { return this.source?.fileid?.toString?.() }, + uniqueId() { + return hashCode(this.source.source) + }, + isLoading() { + return this.source.status === NodeStatus.LOADING + }, extension() { if (this.source.attributes?.displayName) { @@ -363,8 +330,9 @@ export default Vue.extend({ } } - if (this.enabledDefaultActions.length > 0) { - const action = this.enabledDefaultActions[0] + const enabledDefaultActions = this.$refs?.actions?.enabledDefaultActions + if (enabledDefaultActions?.length > 0) { + const action = enabledDefaultActions[0] const displayName = action.displayName([this.source], this.currentView) return { title: displayName, @@ -395,66 +363,6 @@ export default Vue.extend({ return this.selectedFiles.includes(this.fileid) }, - // Sorted actions that are enabled for this node - enabledActions() { - if (this.source.attributes.failed) { - return [] - } - - return actions - .filter(action => !action.enabled || action.enabled([this.source], this.currentView)) - .sort((a, b) => (a.order || 0) - (b.order || 0)) - }, - - // Enabled action that are displayed inline - enabledInlineActions() { - if (this.filesListWidth < 768) { - return [] - } - return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView)) - }, - - // Enabled action that are displayed inline with a custom render function - enabledRenderActions() { - if (!this.visible) { - return [] - } - return this.enabledActions.filter(action => typeof action.renderInline === 'function') - }, - - // Default actions - enabledDefaultActions() { - return this.enabledActions.filter(action => !!action?.default) - }, - - // Actions shown in the menu - enabledMenuActions() { - return [ - // Showing inline first for the NcActions inline prop - ...this.enabledInlineActions, - // Then the rest - ...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'), - ].filter((value, index, self) => { - // Then we filter duplicates to prevent inline actions to be shown twice - return index === self.findIndex(action => action.id === value.id) - }) - }, - openedMenu: { - get() { - return this.actionsMenuStore.opened === this.uniqueId - }, - set(opened) { - this.actionsMenuStore.opened = opened ? this.uniqueId : null - }, - }, - - uniqueId() { - return hashCode(this.source.source) - }, - isLoading() { - return this.source.status === NodeStatus.LOADING - }, - renameLabel() { const matchLabel: Record<FileType, string> = { [FileType.File]: t('files', 'File name'), @@ -507,6 +415,15 @@ export default Vue.extend({ return (this.source.permissions & Permission.CREATE) !== 0 }, + + openedMenu: { + get() { + return this.actionsMenuStore.opened === this.uniqueId + }, + set(opened) { + this.actionsMenuStore.opened = opened ? this.uniqueId : null + }, + }, }, watch: { @@ -545,67 +462,6 @@ export default Vue.extend({ this.openedMenu = false }, - async onActionClick(action) { - const displayName = action.displayName([this.source], this.currentView) - try { - // Set the loading marker - this.loading = action.id - Vue.set(this.source, 'status', NodeStatus.LOADING) - - const success = await action.exec(this.source, this.currentView, this.currentDir) - - // If the action returns null, we stay silent - if (success === null) { - return - } - - if (success) { - showSuccess(t('files', '"{displayName}" action executed successfully', { displayName })) - return - } - showError(t('files', '"{displayName}" action failed', { displayName })) - } catch (e) { - logger.error('Error while executing action', { action, e }) - showError(t('files', '"{displayName}" action failed', { displayName })) - } finally { - // Reset the loading marker - this.loading = '' - Vue.set(this.source, 'status', undefined) - } - }, - execDefaultAction(event) { - if (this.enabledDefaultActions.length > 0) { - event.preventDefault() - event.stopPropagation() - // Execute the first default action if any - this.enabledDefaultActions[0].exec(this.source, this.currentView, this.currentDir) - } - }, - - openDetailsIfAvailable(event) { - event.preventDefault() - event.stopPropagation() - if (sidebarAction?.enabled?.([this.source], this.currentView)) { - sidebarAction.exec(this.source, this.currentView, this.currentDir) - } - }, - - // Open the actions menu on right click - onRightClick(event) { - // If already opened, fallback to default browser - if (this.openedMenu) { - return - } - - // If the clicked row is in the selection, open global menu - const isMoreThanOneSelected = this.selectedFiles.length > 1 - this.actionsMenuStore.opened = this.isSelected && isMoreThanOneSelected ? 'global' : this.uniqueId - - // Prevent any browser defaults - event.preventDefault() - event.stopPropagation() - }, - /** * Check if the file name is valid and update the * input validity using browser's native validation. @@ -749,23 +605,32 @@ export default Vue.extend({ } }, - /** - * Making this a function in case the files-list - * reference changes in the future. That way we're - * sure there is one at the time we call it. - */ - getBoundariesElement() { - return document.querySelector('.app-content > .files-list') + // Open the actions menu on right click + onRightClick(event) { + // If already opened, fallback to default browser + if (this.openedMenu) { + return + } + + // If the clicked row is in the selection, open global menu + const isMoreThanOneSelected = this.selectedFiles.length > 1 + this.actionsMenuStore.opened = this.isSelected && isMoreThanOneSelected ? 'global' : this.uniqueId + + // Prevent any browser defaults + event.preventDefault() + event.stopPropagation() }, - actionDisplayName(action: FileAction) { - if (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 + execDefaultAction() { + this.$refs.actions.execDefaultAction() + }, + + openDetailsIfAvailable(event) { + event.preventDefault() + event.stopPropagation() + if (sidebarAction?.enabled?.([this.source], this.currentView)) { + sidebarAction.exec(this.source, this.currentView, this.currentDir) } - return action.displayName([this.source], this.currentView) }, onDragOver(event: DragEvent) { diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue new file mode 100644 index 00000000000..84d8f4a40f9 --- /dev/null +++ b/apps/files/src/components/FileEntry/FileEntryActions.vue @@ -0,0 +1,238 @@ +<!-- + - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.com> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> +<template> + <td class="files-list__row-actions" + data-cy-files-list-row-actions> + <!-- Render actions --> + <CustomElementRender v-for="action in enabledRenderActions" + :key="action.id" + :class="'files-list__row-action-' + action.id" + :current-view="currentView" + :render="action.renderInline" + :source="source" + class="files-list__row-action--inline" /> + + <!-- Menu actions --> + <NcActions v-if="visible" + ref="actionsMenu" + :boundaries-element="getBoundariesElement()" + :container="getBoundariesElement()" + :disabled="isLoading" + :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> + </NcActions> + </td> +</template> + +<script lang="ts"> +import { DefaultType, FileAction, Folder, Node, NodeStatus, View, getFileActions } from '@nextcloud/files' +import { showError, showSuccess } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n'; +import Vue, { PropType } 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 logger from '../../logger.js' + +// The registered actions list +const actions = getFileActions() + +export default Vue.extend({ + name: 'FileEntryActions', + + components: { + NcActionButton, + NcActions, + NcIconSvgWrapper, + NcLoadingIcon, + }, + + props: { + filesListWidth: { + type: Number, + required: true, + }, + loading: { + type: String, + required: true, + }, + opened: { + type: Boolean, + default: false, + }, + source: { + type: Object as PropType<Node>, + required: true, + }, + visible: { + type: Boolean, + default: false, + }, + }, + + setup() { + return { + } + }, + + computed: { + currentView(): View { + return this.$navigation.active as View + }, + isLoading() { + return this.source.status === NodeStatus.LOADING + }, + + // Sorted actions that are enabled for this node + enabledActions() { + if (this.source.attributes.failed) { + return [] + } + + return actions + .filter(action => !action.enabled || action.enabled([this.source], this.currentView)) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }, + + // Enabled action that are displayed inline + enabledInlineActions() { + if (this.filesListWidth < 768) { + return [] + } + return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView)) + }, + + // Enabled action that are displayed inline with a custom render function + enabledRenderActions() { + if (!this.visible) { + return [] + } + return this.enabledActions.filter(action => typeof action.renderInline === 'function') + }, + + // Default actions + enabledDefaultActions() { + return this.enabledActions.filter(action => !!action?.default) + }, + + // Actions shown in the menu + enabledMenuActions() { + return [ + // Showing inline first for the NcActions inline prop + ...this.enabledInlineActions, + // Then the rest + ...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'), + ].filter((value, index, self) => { + // Then we filter duplicates to prevent inline actions to be shown twice + return index === self.findIndex(action => action.id === value.id) + }) + }, + + openedMenu: { + get() { + return this.opened + }, + set(value) { + this.$emit('update:opened', value) + }, + }, + }, + + methods: { + /** + * Making this a function in case the files-list + * reference changes in the future. That way we're + * sure there is one at the time we call it. + */ + getBoundariesElement() { + return document.querySelector('.app-content > table.files-list') + }, + + actionDisplayName(action: FileAction) { + if (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) + }, + + async onActionClick(action) { + const displayName = action.displayName([this.source], this.currentView) + try { + // Set the loading marker + this.$emit('update:loading', action.id) + Vue.set(this.source, 'status', NodeStatus.LOADING) + + const success = await action.exec(this.source, this.currentView, this.currentDir) + + // If the action returns null, we stay silent + if (success === null) { + return + } + + if (success) { + showSuccess(t('files', '"{displayName}" action executed successfully', { displayName })) + return + } + showError(t('files', '"{displayName}" action failed', { displayName })) + } catch (e) { + logger.error('Error while executing action', { action, e }) + showError(t('files', '"{displayName}" action failed', { displayName })) + } finally { + // Reset the loading marker + this.$emit('update:loading', '') + Vue.set(this.source, 'status', undefined) + } + }, + execDefaultAction(event) { + if (this.enabledDefaultActions.length > 0) { + event.preventDefault() + event.stopPropagation() + // Execute the first default action if any + this.enabledDefaultActions[0].exec(this.source, this.currentView, this.currentDir) + } + }, + + t, + }, +}) +</script> |