Skip to content

Commit 4ebea3d

Browse files
committed
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>
1 parent 3978050 commit 4ebea3d

File tree

10 files changed

+249
-83
lines changed

10 files changed

+249
-83
lines changed

apps/files/src/actions/downloadAction.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*/
2222
import { action } from './downloadAction'
2323
import { expect } from '@jest/globals'
24-
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
24+
import { File, Folder, Permission, View, FileAction, DefaultType } from '@nextcloud/files'
2525

2626
const view = {
2727
id: 'files',
@@ -34,7 +34,7 @@ describe('Download action conditions tests', () => {
3434
expect(action.id).toBe('download')
3535
expect(action.displayName([], view)).toBe('Download')
3636
expect(action.iconSvgInline([], view)).toBe('<svg>SvgMock</svg>')
37-
expect(action.default).toBeUndefined()
37+
expect(action.default).toBe(DefaultType.DEFAULT)
3838
expect(action.order).toBe(30)
3939
})
4040
})

apps/files/src/actions/downloadAction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
*/
2222
import { generateUrl } from '@nextcloud/router'
23-
import { FileAction, Permission, Node, FileType, View } from '@nextcloud/files'
23+
import { FileAction, Permission, Node, FileType, View, DefaultType } from '@nextcloud/files'
2424
import { translate as t } from '@nextcloud/l10n'
2525
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
2626

@@ -60,6 +60,8 @@ const isDownloadable = function(node: Node) {
6060

6161
export const action = new FileAction({
6262
id: 'download',
63+
default: DefaultType.DEFAULT,
64+
6365
displayName: () => t('files', 'Download'),
6466
iconSvgInline: () => ArrowDownSvg,
6567

apps/files/src/components/FileEntry/FileEntryActions.vue

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ import type { PropType, ShallowRef } from 'vue'
9797
import type { FileAction, Node, View } from '@nextcloud/files'
9898
9999
import { showError, showSuccess } from '@nextcloud/dialogs'
100-
import { DefaultType, NodeStatus, getFileActions } from '@nextcloud/files'
100+
import { DefaultType, NodeStatus } from '@nextcloud/files'
101101
import { translate as t } from '@nextcloud/l10n'
102-
import { defineComponent } from 'vue'
102+
import { defineComponent, inject } from 'vue'
103103
104104
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
105105
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
@@ -112,9 +112,6 @@ import { useNavigation } from '../../composables/useNavigation'
112112
import CustomElementRender from '../CustomElementRender.vue'
113113
import logger from '../../logger.js'
114114
115-
// The registered actions list
116-
const actions = getFileActions()
117-
118115
export default defineComponent({
119116
name: 'FileEntryActions',
120117
@@ -153,10 +150,12 @@ export default defineComponent({
153150
154151
setup() {
155152
const { currentView } = useNavigation()
153+
const enabledFileActions = inject<FileAction[]>('enabledFileActions', [])
156154
157155
return {
158156
// The file list is guaranteed to be only shown with active view
159157
currentView: currentView as ShallowRef<View>,
158+
enabledFileActions,
160159
}
161160
},
162161
@@ -175,36 +174,20 @@ export default defineComponent({
175174
return this.source.status === NodeStatus.LOADING
176175
},
177176
178-
// Sorted actions that are enabled for this node
179-
enabledActions() {
180-
if (this.source.attributes.failed) {
181-
return []
182-
}
183-
184-
return actions
185-
.filter(action => !action.enabled || action.enabled([this.source], this.currentView))
186-
.sort((a, b) => (a.order || 0) - (b.order || 0))
187-
},
188-
189177
// Enabled action that are displayed inline
190178
enabledInlineActions() {
191179
if (this.filesListWidth < 768 || this.gridMode) {
192180
return []
193181
}
194-
return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView))
182+
return this.enabledFileActions.filter(action => action?.inline?.(this.source, this.currentView))
195183
},
196184
197185
// Enabled action that are displayed inline with a custom render function
198186
enabledRenderActions() {
199187
if (this.gridMode) {
200188
return []
201189
}
202-
return this.enabledActions.filter(action => typeof action.renderInline === 'function')
203-
},
204-
205-
// Default actions
206-
enabledDefaultActions() {
207-
return this.enabledActions.filter(action => !!action?.default)
190+
return this.enabledFileActions.filter(action => typeof action.renderInline === 'function')
208191
},
209192
210193
// Actions shown in the menu
@@ -219,7 +202,7 @@ export default defineComponent({
219202
// Showing inline first for the NcActions inline prop
220203
...this.enabledInlineActions,
221204
// Then the rest
222-
...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'),
205+
...this.enabledFileActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'),
223206
].filter((value, index, self) => {
224207
// Then we filter duplicates to prevent inline actions to be shown twice
225208
return index === self.findIndex(action => action.id === value.id)
@@ -233,7 +216,7 @@ export default defineComponent({
233216
},
234217
235218
enabledSubmenuActions() {
236-
return this.enabledActions
219+
return this.enabledFileActions
237220
.filter(action => action.parent)
238221
.reduce((arr, action) => {
239222
if (!arr[action.parent!]) {
@@ -322,14 +305,6 @@ export default defineComponent({
322305
}
323306
}
324307
},
325-
execDefaultAction(event) {
326-
if (this.enabledDefaultActions.length > 0) {
327-
event.preventDefault()
328-
event.stopPropagation()
329-
// Execute the first default action if any
330-
this.enabledDefaultActions[0].exec(this.source, this.currentView, this.currentDir)
331-
}
332-
},
333308
334309
isMenu(id: string) {
335310
return this.enabledSubmenuActions[id]?.length > 0

apps/files/src/components/FileEntry/FileEntryName.vue

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,17 @@
5454
</template>
5555

5656
<script lang="ts">
57-
import type { Node } from '@nextcloud/files'
57+
import type { FileAction, Node } from '@nextcloud/files'
5858
import type { PropType } from 'vue'
5959
60+
import axios from '@nextcloud/axios'
61+
import { showError, showSuccess } from '@nextcloud/dialogs'
6062
import { emit } from '@nextcloud/event-bus'
61-
import { FileType, NodeStatus, Permission } from '@nextcloud/files'
63+
import { FileType, NodeStatus } from '@nextcloud/files'
6264
import { loadState } from '@nextcloud/initial-state'
63-
import { showError, showSuccess } from '@nextcloud/dialogs'
6465
import { translate as t } from '@nextcloud/l10n'
65-
import axios from '@nextcloud/axios'
66-
import Vue from 'vue'
66+
import { isAxiosError} from 'axios'
67+
import Vue, { inject } from 'vue'
6768
6869
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
6970
@@ -117,8 +118,11 @@ export default Vue.extend({
117118
const { currentView } = useNavigation()
118119
const renamingStore = useRenamingStore()
119120
121+
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
122+
120123
return {
121124
currentView,
125+
defaultFileAction,
122126
123127
renamingStore,
124128
}
@@ -158,32 +162,20 @@ export default Vue.extend({
158162
}
159163
}
160164
161-
const enabledDefaultActions = this.$parent?.$refs?.actions?.enabledDefaultActions
162-
if (enabledDefaultActions?.length > 0) {
163-
const action = enabledDefaultActions[0]
164-
const displayName = action.displayName([this.source], this.currentView)
165+
if (this.defaultFileAction && this.currentView) {
166+
const displayName = this.defaultFileAction.displayName([this.source], this.currentView)
165167
return {
166-
is: 'a',
168+
is: 'button',
167169
params: {
170+
'aria-label': displayName,
168171
title: displayName,
169-
role: 'button',
170-
tabindex: '0',
171-
},
172-
}
173-
}
174-
175-
if (this.source?.permissions & Permission.READ) {
176-
return {
177-
is: 'a',
178-
params: {
179-
download: this.source.basename,
180-
href: this.source.source,
181-
title: t('files', 'Download file {name}', { name: `${this.basename}${this.extension}` }),
182172
tabindex: '0',
183173
},
184174
}
185175
}
186176
177+
// nothing interactive here, there is no default action
178+
// so if not even the download action works we only can show the list entry
187179
return {
188180
is: 'span',
189181
}
@@ -324,12 +316,15 @@ export default Vue.extend({
324316
// Reset the renaming store
325317
this.stopRenaming()
326318
this.$nextTick(() => {
327-
this.$refs.basename.focus()
319+
const nameContainter = this.$refs.basename as HTMLElement | undefined
320+
nameContainter?.focus()
328321
})
329322
} catch (error) {
330323
logger.error('Error while renaming file', { error })
324+
// Rename back as it failed
331325
this.source.rename(oldName)
332-
this.$refs.renameInput.focus()
326+
// And ensure we reset to the renaming state
327+
this.startRenaming()
333328
334329
// TODO: 409 means current folder does not exist, redirect ?
335330
if (error?.response?.status === 404) {
@@ -352,3 +347,16 @@ export default Vue.extend({
352347
},
353348
})
354349
</script>
350+
351+
<style scoped lang="scss">
352+
button.files-list__row-name-link {
353+
background-color: unset;
354+
border: none;
355+
font-weight: normal;
356+
357+
&:active {
358+
// No active styles - handled by the row entry
359+
background-color: unset !important;
360+
}
361+
}
362+
</style>

apps/files/src/components/FileEntryMixin.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
*
2121
*/
2222

23-
import type { ComponentPublicInstance, PropType } from 'vue'
23+
import type { PropType } from 'vue'
2424
import type { FileSource } from '../types.ts'
2525

2626
import { showError } from '@nextcloud/dialogs'
27-
import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files'
27+
import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, getFileActions } from '@nextcloud/files'
2828
import { translate as t } from '@nextcloud/l10n'
2929
import { generateUrl } from '@nextcloud/router'
3030
import { vOnClickOutside } from '@vueuse/components'
@@ -36,10 +36,11 @@ import { getDragAndDropPreview } from '../utils/dragUtils.ts'
3636
import { hashCode } from '../utils/hashUtils.ts'
3737
import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts'
3838
import logger from '../logger.js'
39-
import FileEntryActions from '../components/FileEntry/FileEntryActions.vue'
4039

4140
Vue.directive('onClickOutside', vOnClickOutside)
4241

42+
const actions = getFileActions()
43+
4344
export default defineComponent({
4445
props: {
4546
source: {
@@ -56,6 +57,13 @@ export default defineComponent({
5657
},
5758
},
5859

60+
provide() {
61+
return {
62+
defaultFileAction: this.defaultFileAction,
63+
enabledFileActions: this.enabledFileActions,
64+
}
65+
},
66+
5967
data() {
6068
return {
6169
loading: '',
@@ -173,6 +181,23 @@ export default defineComponent({
173181
isRenaming() {
174182
return this.renamingStore.renamingNode === this.source
175183
},
184+
185+
/**
186+
* Sorted actions that are enabled for this node
187+
*/
188+
enabledFileActions() {
189+
if (this.source.status === NodeStatus.FAILED) {
190+
return []
191+
}
192+
193+
return actions
194+
.filter(action => !action.enabled || action.enabled([this.source], this.currentView))
195+
.sort((a, b) => (a.order || 0) - (b.order || 0))
196+
},
197+
198+
defaultFileAction() {
199+
return this.enabledFileActions.find((action) => action.default !== undefined)
200+
},
176201
},
177202

178203
watch: {
@@ -254,8 +279,15 @@ export default defineComponent({
254279
return false
255280
}
256281

257-
const actions = this.$refs.actions as ComponentPublicInstance<typeof FileEntryActions>
258-
actions.execDefaultAction(event)
282+
if (this.defaultFileAction) {
283+
event.preventDefault()
284+
event.stopPropagation()
285+
// Execute the first default action if any
286+
this.defaultFileAction.exec(this.source, this.currentView, this.currentDir)
287+
} else {
288+
// fallback to open in current tab
289+
window.open(generateUrl('/f/{fileId}', { fileId: this.fileid }), '_self')
290+
}
259291
},
260292

261293
openDetailsIfAvailable(event) {

apps/files/src/components/FilesListVirtual.vue

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -596,24 +596,26 @@ export default defineComponent({
596596
// Take as much space as possible
597597
flex: 1 1 auto;
598598
599-
a {
599+
button.files-list__row-name-link {
600600
display: flex;
601601
align-items: center;
602+
text-align: start;
602603
// Fill cell height and width
603604
width: 100%;
604605
height: 100%;
605606
// Necessary for flex grow to work
606607
min-width: 0;
608+
margin: 0;
607609
608610
// Already added to the inner text, see rule below
609611
&:focus-visible {
610-
outline: none;
612+
outline: none !important;
611613
}
612614
613615
// Keyboard indicator a11y
614616
&:focus .files-list__row-name-text {
615-
outline: 2px solid var(--color-main-text) !important;
616-
border-radius: 20px;
617+
outline: var(--border-width-input-focused) solid var(--color-main-text) !important;
618+
border-radius: var(--border-radius-element);
617619
}
618620
&:focus:not(:focus-visible) .files-list__row-name-text {
619621
outline: none !important;
@@ -623,7 +625,7 @@ export default defineComponent({
623625
.files-list__row-name-text {
624626
color: var(--color-main-text);
625627
// Make some space for the outline
626-
padding: 5px 10px;
628+
padding: var(--default-grid-baseline) calc(2 * var(--default-grid-baseline));
627629
margin-left: -10px;
628630
// Align two name and ext
629631
display: inline-flex;
@@ -764,12 +766,6 @@ tbody.files-list__tbody.files-list__tbody--grid {
764766
padding-top: var(--half-clickable-area);
765767
}
766768
767-
a.files-list__row-name-link {
768-
// Minus action menu
769-
width: calc(100% - var(--clickable-area));
770-
height: var(--clickable-area);
771-
}
772-
773769
.files-list__row-name-text {
774770
margin: 0;
775771
padding-right: 0;

0 commit comments

Comments
 (0)