Skip to content

Commit

Permalink
Merge pull request #46939 from nextcloud/backport/46768/stable28
Browse files Browse the repository at this point in the history
[stable28] fix(files): Provide default file action for file entry name (on click action)
  • Loading branch information
susnux authored Aug 20, 2024
2 parents 44cae23 + 64d2bc5 commit e5d7550
Show file tree
Hide file tree
Showing 43 changed files with 963 additions and 554 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 @@ -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',
Expand All @@ -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)
})
})
Expand Down
4 changes: 3 additions & 1 deletion apps/files/src/actions/downloadAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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,

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 @@ -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'
Expand All @@ -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',
Expand Down Expand Up @@ -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,
}
},
Expand All @@ -175,36 +174,20 @@ 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
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 @@ -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)
Expand All @@ -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!]) {
Expand Down Expand Up @@ -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
Expand Down
79 changes: 46 additions & 33 deletions apps/files/src/components/FileEntry/FileEntryName.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,22 @@
</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'
import { useNavigation } from '../../composables/useNavigation'
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
import { useRenamingStore } from '../../store/renaming.ts'
import logger from '../../logger.js'
Expand Down Expand Up @@ -115,10 +117,15 @@ export default Vue.extend({
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 @@ -158,32 +165,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',
}
Expand Down Expand Up @@ -324,20 +319,25 @@ 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()
// 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 }))
return
// 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.directory }))
return
}
}
// Unknown error
Expand All @@ -352,3 +352,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>
42 changes: 37 additions & 5 deletions apps/files/src/components/FileEntryMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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: {
Expand All @@ -56,6 +57,13 @@ export default defineComponent({
},
},

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

data() {
return {
loading: '',
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit e5d7550

Please sign in to comment.