Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(files): Provide default file action for file entry name (on click action) #46768

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
susnux marked this conversation as resolved.
Show resolved Hide resolved

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
Loading