aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@protonmail.com>2023-10-12 15:15:20 +0200
committerJohn Molakvoæ <skjnldsv@protonmail.com>2023-10-17 11:19:02 +0200
commit64c32f714894d2c884c82922a5349bbe64a55be8 (patch)
tree0653f9c4a287f7fd5bb1d5faba04bed22d238dc2 /apps/files/src
parent0f1f73478a59f59d92c4542aa42dc61973c600de (diff)
downloadnextcloud-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.vue243
-rw-r--r--apps/files/src/components/FileEntry/FileEntryActions.vue238
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>