aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-07-26 01:42:31 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-08-21 01:05:52 +0200
commit4ebea3db3a6e3ec72502b283f1dfbe842d75dbf0 (patch)
tree7d66730fa947a6bc24beda9b3bc071600c4742d4 /apps/files
parent39780506f0b4b171ba67eeb967a0b80f0dc62d0f (diff)
downloadnextcloud-server-4ebea3db3a6e3ec72502b283f1dfbe842d75dbf0.tar.gz
nextcloud-server-4ebea3db3a6e3ec72502b283f1dfbe842d75dbf0.zip
fix(files): Provide file actions from list entry to make it reactive
This fixes non reactive default action text of the name component. Also use download action as default action so that only one place is needed to define how to download a file. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/files')
-rw-r--r--apps/files/src/actions/downloadAction.spec.ts4
-rw-r--r--apps/files/src/actions/downloadAction.ts4
-rw-r--r--apps/files/src/components/FileEntry/FileEntryActions.vue41
-rw-r--r--apps/files/src/components/FileEntry/FileEntryName.vue58
-rw-r--r--apps/files/src/components/FileEntryMixin.ts42
-rw-r--r--apps/files/src/components/FilesListVirtual.vue18
6 files changed, 90 insertions, 77 deletions
diff --git a/apps/files/src/actions/downloadAction.spec.ts b/apps/files/src/actions/downloadAction.spec.ts
index bc9c87c0718..56ad3882d21 100644
--- a/apps/files/src/actions/downloadAction.spec.ts
+++ b/apps/files/src/actions/downloadAction.spec.ts
@@ -21,7 +21,7 @@
*/
import { action } from './downloadAction'
import { expect } from '@jest/globals'
-import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
+import { File, Folder, Permission, View, FileAction, DefaultType } from '@nextcloud/files'
const view = {
id: 'files',
@@ -34,7 +34,7 @@ describe('Download action conditions tests', () => {
expect(action.id).toBe('download')
expect(action.displayName([], view)).toBe('Download')
expect(action.iconSvgInline([], view)).toBe('<svg>SvgMock</svg>')
- expect(action.default).toBeUndefined()
+ expect(action.default).toBe(DefaultType.DEFAULT)
expect(action.order).toBe(30)
})
})
diff --git a/apps/files/src/actions/downloadAction.ts b/apps/files/src/actions/downloadAction.ts
index de2fa081166..03686cd4243 100644
--- a/apps/files/src/actions/downloadAction.ts
+++ b/apps/files/src/actions/downloadAction.ts
@@ -20,7 +20,7 @@
*
*/
import { generateUrl } from '@nextcloud/router'
-import { FileAction, Permission, Node, FileType, View } from '@nextcloud/files'
+import { FileAction, Permission, Node, FileType, View, DefaultType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
@@ -60,6 +60,8 @@ const isDownloadable = function(node: Node) {
export const action = new FileAction({
id: 'download',
+ default: DefaultType.DEFAULT,
+
displayName: () => t('files', 'Download'),
iconSvgInline: () => ArrowDownSvg,
diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue
index c6ee7b9aac7..597fbc5a082 100644
--- a/apps/files/src/components/FileEntry/FileEntryActions.vue
+++ b/apps/files/src/components/FileEntry/FileEntryActions.vue
@@ -97,9 +97,9 @@ import type { PropType, ShallowRef } from 'vue'
import type { FileAction, Node, View } from '@nextcloud/files'
import { showError, showSuccess } from '@nextcloud/dialogs'
-import { DefaultType, NodeStatus, getFileActions } from '@nextcloud/files'
+import { DefaultType, NodeStatus } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
-import { defineComponent } from 'vue'
+import { defineComponent, inject } from 'vue'
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
@@ -112,9 +112,6 @@ import { useNavigation } from '../../composables/useNavigation'
import CustomElementRender from '../CustomElementRender.vue'
import logger from '../../logger.js'
-// The registered actions list
-const actions = getFileActions()
-
export default defineComponent({
name: 'FileEntryActions',
@@ -153,10 +150,12 @@ export default defineComponent({
setup() {
const { currentView } = useNavigation()
+ const enabledFileActions = inject<FileAction[]>('enabledFileActions', [])
return {
// The file list is guaranteed to be only shown with active view
currentView: currentView as ShallowRef<View>,
+ enabledFileActions,
}
},
@@ -175,23 +174,12 @@ export default defineComponent({
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 || this.gridMode) {
return []
}
- return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView))
+ return this.enabledFileActions.filter(action => action?.inline?.(this.source, this.currentView))
},
// Enabled action that are displayed inline with a custom render function
@@ -199,12 +187,7 @@ export default defineComponent({
if (this.gridMode) {
return []
}
- return this.enabledActions.filter(action => typeof action.renderInline === 'function')
- },
-
- // Default actions
- enabledDefaultActions() {
- return this.enabledActions.filter(action => !!action?.default)
+ return this.enabledFileActions.filter(action => typeof action.renderInline === 'function')
},
// Actions shown in the menu
@@ -219,7 +202,7 @@ export default defineComponent({
// 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'),
+ ...this.enabledFileActions.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)
@@ -233,7 +216,7 @@ export default defineComponent({
},
enabledSubmenuActions() {
- return this.enabledActions
+ return this.enabledFileActions
.filter(action => action.parent)
.reduce((arr, action) => {
if (!arr[action.parent!]) {
@@ -322,14 +305,6 @@ export default defineComponent({
}
}
},
- 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)
- }
- },
isMenu(id: string) {
return this.enabledSubmenuActions[id]?.length > 0
diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue
index 5543b05436a..1a66b0a8e39 100644
--- a/apps/files/src/components/FileEntry/FileEntryName.vue
+++ b/apps/files/src/components/FileEntry/FileEntryName.vue
@@ -54,16 +54,17 @@
</template>
<script lang="ts">
-import type { Node } from '@nextcloud/files'
+import type { FileAction, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
+import axios from '@nextcloud/axios'
+import { showError, showSuccess } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
-import { FileType, NodeStatus, Permission } from '@nextcloud/files'
+import { FileType, NodeStatus } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
-import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
-import axios from '@nextcloud/axios'
-import Vue from 'vue'
+import { isAxiosError} from 'axios'
+import Vue, { inject } from 'vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
@@ -117,8 +118,11 @@ export default Vue.extend({
const { currentView } = useNavigation()
const renamingStore = useRenamingStore()
+ const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
+
return {
currentView,
+ defaultFileAction,
renamingStore,
}
@@ -158,32 +162,20 @@ export default Vue.extend({
}
}
- const enabledDefaultActions = this.$parent?.$refs?.actions?.enabledDefaultActions
- if (enabledDefaultActions?.length > 0) {
- const action = enabledDefaultActions[0]
- const displayName = action.displayName([this.source], this.currentView)
+ if (this.defaultFileAction && this.currentView) {
+ const displayName = this.defaultFileAction.displayName([this.source], this.currentView)
return {
- is: 'a',
+ is: 'button',
params: {
+ 'aria-label': displayName,
title: displayName,
- role: 'button',
- tabindex: '0',
- },
- }
- }
-
- if (this.source?.permissions & Permission.READ) {
- return {
- is: 'a',
- params: {
- download: this.source.basename,
- href: this.source.source,
- title: t('files', 'Download file {name}', { name: `${this.basename}${this.extension}` }),
tabindex: '0',
},
}
}
+ // nothing interactive here, there is no default action
+ // so if not even the download action works we only can show the list entry
return {
is: 'span',
}
@@ -324,12 +316,15 @@ export default Vue.extend({
// Reset the renaming store
this.stopRenaming()
this.$nextTick(() => {
- this.$refs.basename.focus()
+ const nameContainter = this.$refs.basename as HTMLElement | undefined
+ nameContainter?.focus()
})
} catch (error) {
logger.error('Error while renaming file', { error })
+ // Rename back as it failed
this.source.rename(oldName)
- this.$refs.renameInput.focus()
+ // And ensure we reset to the renaming state
+ this.startRenaming()
// TODO: 409 means current folder does not exist, redirect ?
if (error?.response?.status === 404) {
@@ -352,3 +347,16 @@ export default Vue.extend({
},
})
</script>
+
+<style scoped lang="scss">
+button.files-list__row-name-link {
+ background-color: unset;
+ border: none;
+ font-weight: normal;
+
+ &:active {
+ // No active styles - handled by the row entry
+ background-color: unset !important;
+ }
+}
+</style>
diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts
index 95740efe185..42a29455680 100644
--- a/apps/files/src/components/FileEntryMixin.ts
+++ b/apps/files/src/components/FileEntryMixin.ts
@@ -20,11 +20,11 @@
*
*/
-import type { ComponentPublicInstance, PropType } from 'vue'
+import type { PropType } from 'vue'
import type { FileSource } from '../types.ts'
import { showError } from '@nextcloud/dialogs'
-import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files'
+import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, getFileActions } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { vOnClickOutside } from '@vueuse/components'
@@ -36,10 +36,11 @@ import { getDragAndDropPreview } from '../utils/dragUtils.ts'
import { hashCode } from '../utils/hashUtils.ts'
import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts'
import logger from '../logger.js'
-import FileEntryActions from '../components/FileEntry/FileEntryActions.vue'
Vue.directive('onClickOutside', vOnClickOutside)
+const actions = getFileActions()
+
export default defineComponent({
props: {
source: {
@@ -56,6 +57,13 @@ export default defineComponent({
},
},
+ provide() {
+ return {
+ defaultFileAction: this.defaultFileAction,
+ enabledFileActions: this.enabledFileActions,
+ }
+ },
+
data() {
return {
loading: '',
@@ -173,6 +181,23 @@ export default defineComponent({
isRenaming() {
return this.renamingStore.renamingNode === this.source
},
+
+ /**
+ * Sorted actions that are enabled for this node
+ */
+ enabledFileActions() {
+ if (this.source.status === NodeStatus.FAILED) {
+ return []
+ }
+
+ return actions
+ .filter(action => !action.enabled || action.enabled([this.source], this.currentView))
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
+ },
+
+ defaultFileAction() {
+ return this.enabledFileActions.find((action) => action.default !== undefined)
+ },
},
watch: {
@@ -254,8 +279,15 @@ export default defineComponent({
return false
}
- const actions = this.$refs.actions as ComponentPublicInstance<typeof FileEntryActions>
- actions.execDefaultAction(event)
+ if (this.defaultFileAction) {
+ event.preventDefault()
+ event.stopPropagation()
+ // Execute the first default action if any
+ this.defaultFileAction.exec(this.source, this.currentView, this.currentDir)
+ } else {
+ // fallback to open in current tab
+ window.open(generateUrl('/f/{fileId}', { fileId: this.fileid }), '_self')
+ }
},
openDetailsIfAvailable(event) {
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index c11d33f207a..65c88df2184 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -596,24 +596,26 @@ export default defineComponent({
// Take as much space as possible
flex: 1 1 auto;
- a {
+ button.files-list__row-name-link {
display: flex;
align-items: center;
+ text-align: start;
// Fill cell height and width
width: 100%;
height: 100%;
// Necessary for flex grow to work
min-width: 0;
+ margin: 0;
// Already added to the inner text, see rule below
&:focus-visible {
- outline: none;
+ outline: none !important;
}
// Keyboard indicator a11y
&:focus .files-list__row-name-text {
- outline: 2px solid var(--color-main-text) !important;
- border-radius: 20px;
+ outline: var(--border-width-input-focused) solid var(--color-main-text) !important;
+ border-radius: var(--border-radius-element);
}
&:focus:not(:focus-visible) .files-list__row-name-text {
outline: none !important;
@@ -623,7 +625,7 @@ export default defineComponent({
.files-list__row-name-text {
color: var(--color-main-text);
// Make some space for the outline
- padding: 5px 10px;
+ padding: var(--default-grid-baseline) calc(2 * var(--default-grid-baseline));
margin-left: -10px;
// Align two name and ext
display: inline-flex;
@@ -764,12 +766,6 @@ tbody.files-list__tbody.files-list__tbody--grid {
padding-top: var(--half-clickable-area);
}
- a.files-list__row-name-link {
- // Minus action menu
- width: calc(100% - var(--clickable-area));
- height: var(--clickable-area);
- }
-
.files-list__row-name-text {
margin: 0;
padding-right: 0;