Skip to content

Commit

Permalink
Merge pull request #46768 from nextcloud/fix/files-actions
Browse files Browse the repository at this point in the history
fix(files): Provide default file action for file entry name (on click action)
  • Loading branch information
susnux authored Aug 1, 2024
2 parents bfde053 + 948a64e commit 356860c
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 86 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
58 changes: 34 additions & 24 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 @@ -96,10 +97,15 @@ export default defineComponent({
setup() {
const { currentView } = useNavigation()
const { directory } = useRouteParameters()
const renamingStore = useRenamingStore()
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
return {
currentView,
defaultFileAction,
directory,
renamingStore,
}
Expand Down Expand Up @@ -139,32 +145,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,20 +274,23 @@ 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 ?
if (error?.response?.status === 404) {
showError(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
return
} else if (error?.response?.status === 412) {
showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir }))
showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.directory }))
return
}
}
Expand All @@ -309,3 +306,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
Loading

0 comments on commit 356860c

Please sign in to comment.