Skip to content

Commit

Permalink
fix(files): Provide file actions from list entry to make it reactive
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
susnux committed Jul 31, 2024
1 parent bfde053 commit a39f13e
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 79 deletions.
4 changes: 2 additions & 2 deletions apps/files/src/actions/downloadAction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,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',
Expand All @@ -23,7 +23,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)
})
})
Expand Down
6 changes: 4 additions & 2 deletions apps/files/src/actions/downloadAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
*/
import type { ShareAttribute } from '../../../files_sharing/src/sharing'

import { FileAction, Permission, Node, FileType, View } from '@nextcloud/files'
import { FileAction, Permission, Node, FileType, View, DefaultType } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { translate as t } from '@nextcloud/l10n'

import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'

Expand Down Expand Up @@ -46,6 +46,8 @@ const isDownloadable = function(node: Node) {

export const action = new FileAction({
id: 'download',
default: DefaultType.DEFAULT,

displayName: () => t('files', 'Download'),
iconSvgInline: () => ArrowDownSvg,

Expand Down
41 changes: 8 additions & 33 deletions apps/files/src/components/FileEntry/FileEntryActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@
import type { PropType, ShallowRef } from 'vue'
import type { FileAction, Node, View } from '@nextcloud/files'
import { DefaultType, NodeStatus, getFileActions } from '@nextcloud/files'
import { DefaultType, NodeStatus } from '@nextcloud/files'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import { defineComponent, inject } from 'vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
Expand All @@ -95,9 +95,6 @@ import CustomElementRender from '../CustomElementRender.vue'
import { useNavigation } from '../../composables/useNavigation'
import logger from '../../logger.js'
// The registered actions list
const actions = getFileActions()
export default defineComponent({
name: 'FileEntryActions',
Expand Down Expand Up @@ -136,10 +133,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,
}
},
Expand All @@ -158,36 +157,20 @@ export default defineComponent({
return this.source.status === NodeStatus.LOADING
},
// Sorted actions that are enabled for this node
enabledActions() {
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))
},
// 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
enabledRenderActions() {
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
Expand All @@ -202,7 +185,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)
Expand All @@ -216,7 +199,7 @@ export default defineComponent({
},
enabledSubmenuActions() {
return this.enabledActions
return this.enabledFileActions
.filter(action => action.parent)
.reduce((arr, action) => {
if (!arr[action.parent!]) {
Expand Down Expand Up @@ -305,14 +288,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
Expand Down
54 changes: 31 additions & 23 deletions apps/files/src/components/FileEntry/FileEntryName.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,20 @@
</template>

<script lang="ts">
import type { Node } from '@nextcloud/files'
import type { FileAction, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import axios, { isAxiosError } 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 { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import { defineComponent, inject } from 'vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import { useNavigation } from '../../composables/useNavigation'
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
import { useRenamingStore } from '../../store/renaming.ts'
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
import logger from '../../logger.js'
Expand Down Expand Up @@ -98,8 +99,11 @@ export default defineComponent({
const { currentView } = useNavigation()
const renamingStore = useRenamingStore()
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
return {
currentView,
defaultFileAction,
renamingStore,
}
Expand Down Expand Up @@ -139,32 +143,20 @@ export default defineComponent({
}
}
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',
}
Expand Down Expand Up @@ -280,12 +272,15 @@ export default defineComponent({
// 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()
if (isAxiosError(error)) {
// TODO: 409 means current folder does not exist, redirect ?
Expand All @@ -309,3 +304,16 @@ export default defineComponent({
},
})
</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>
42 changes: 37 additions & 5 deletions apps/files/src/components/FileEntryMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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 } 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'
Expand All @@ -19,10 +19,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: {
Expand All @@ -47,6 +48,13 @@ export default defineComponent({
},
},

provide() {
return {
defaultFileAction: this.defaultFileAction,
enabledFileActions: this.enabledFileActions,
}
},

data() {
return {
loading: '',
Expand Down Expand Up @@ -178,6 +186,23 @@ export default defineComponent({
color: `color-mix(in srgb, var(--color-main-text) ${ratio}%, var(--color-text-maxcontrast))`,
}
},

/**
* 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: {
Expand Down Expand Up @@ -261,8 +286,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) {
Expand Down
16 changes: 7 additions & 9 deletions apps/files/src/components/FilesListVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -600,24 +600,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;
Expand All @@ -627,7 +629,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;
Expand Down Expand Up @@ -791,10 +793,6 @@ tbody.files-list__tbody.files-list__tbody--grid {
height: var(--icon-preview-size);
}
a.files-list__row-name-link {
height: var(--name-height);
}
.files-list__row-name-text {
margin: 0;
// Ensure that the outline is not too close to the text.
Expand Down
Loading

0 comments on commit a39f13e

Please sign in to comment.