Skip to content

Commit a5e8e6d

Browse files
committed
fix(files): properly update store on files conversions success
Signed-off-by: skjnldsv <[email protected]>
1 parent 83e35b6 commit a5e8e6d

File tree

7 files changed

+65
-81
lines changed

7 files changed

+65
-81
lines changed

apps/files/src/actions/convertAction.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { t } from '@nextcloud/l10n'
1111

1212
import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw'
1313

14-
import { convertFile, convertFiles, getParentFolder } from './convertUtils'
14+
import { convertFile, convertFiles } from './convertUtils'
1515

1616
type ConversionsProvider = {
1717
from: string,
@@ -33,17 +33,17 @@ export const registerConvertActions = () => {
3333
return nodes.every(node => from === node.mime)
3434
},
3535

36-
async exec(node: Node, view: View, dir: string) {
36+
async exec(node: Node) {
3737
// If we're here, we know that the node has a fileid
38-
convertFile(node.fileid as number, to, getParentFolder(view, dir))
38+
convertFile(node.fileid as number, to)
3939

4040
// Silently terminate, we'll handle the UI in the background
4141
return null
4242
},
4343

44-
async execBatch(nodes: Node[], view: View, dir: string) {
44+
async execBatch(nodes: Node[]) {
4545
const fileIds = nodes.map(node => node.fileid).filter(Boolean) as number[]
46-
convertFiles(fileIds, to, getParentFolder(view, dir))
46+
convertFiles(fileIds, to)
4747

4848
// Silently terminate, we'll handle the UI in the background
4949
return Array(nodes.length).fill(null)

apps/files/src/actions/convertUtils.ts

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,58 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55
import type { AxiosResponse } from '@nextcloud/axios'
6-
import type { Folder, View } from '@nextcloud/files'
6+
import type { OCSResponse } from '@nextcloud/typings/ocs'
77

8+
// eslint-disable-next-line n/no-extraneous-import
9+
import { AxiosError } from 'axios'
810
import { emit } from '@nextcloud/event-bus'
911
import { generateOcsUrl } from '@nextcloud/router'
1012
import { showError, showLoading, showSuccess } from '@nextcloud/dialogs'
1113
import { t } from '@nextcloud/l10n'
1214
import axios from '@nextcloud/axios'
1315
import PQueue from 'p-queue'
1416

17+
import { fetchNode } from '../services/WebdavClient.ts'
1518
import logger from '../logger'
16-
import { useFilesStore } from '../store/files'
17-
import { getPinia } from '../store'
18-
import { usePathsStore } from '../store/paths'
1919

20-
const queue = new PQueue({ concurrency: 5 })
20+
type ConversionResponse = {
21+
path: string
22+
fileId: number
23+
}
24+
25+
interface PromiseRejectedResult<T> {
26+
status: 'rejected'
27+
reason: T
28+
}
29+
30+
type PromiseSettledResult<T, E> = PromiseFulfilledResult<T> | PromiseRejectedResult<E>;
31+
type ConversionSuccess = AxiosResponse<OCSResponse<ConversionResponse>>
32+
type ConversionError = AxiosError<OCSResponse<ConversionResponse>>
2133

34+
const queue = new PQueue({ concurrency: 5 })
2235
const requestConversion = function(fileId: number, targetMimeType: string): Promise<AxiosResponse> {
2336
return axios.post(generateOcsUrl('/apps/files/api/v1/convert'), {
2437
fileId,
2538
targetMimeType,
2639
})
2740
}
2841

29-
export const convertFiles = async function(fileIds: number[], targetMimeType: string, parentFolder: Folder | null) {
42+
export const convertFiles = async function(fileIds: number[], targetMimeType: string) {
3043
const conversions = fileIds.map(fileId => queue.add(() => requestConversion(fileId, targetMimeType)))
3144

3245
// Start conversion
3346
const toast = showLoading(t('files', 'Converting files…'))
3447

3548
// Handle results
3649
try {
37-
const results = await Promise.allSettled(conversions)
38-
const failed = results.filter(result => result.status === 'rejected')
50+
const results = await Promise.allSettled(conversions) as PromiseSettledResult<ConversionSuccess, ConversionError>[]
51+
const failed = results.filter(result => result.status === 'rejected') as PromiseRejectedResult<ConversionError>[]
3952
if (failed.length > 0) {
40-
const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message) as string[]
53+
const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message)
4154
logger.error('Failed to convert files', { fileIds, targetMimeType, messages })
4255

4356
// If all failed files have the same error message, show it
44-
if (new Set(messages).size === 1) {
57+
if (new Set(messages).size === 1 && typeof messages[0] === 'string') {
4558
showError(t('files', 'Failed to convert files: {message}', { message: messages[0] }))
4659
return
4760
}
@@ -74,15 +87,27 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st
7487
// All files converted
7588
showSuccess(t('files', 'Files successfully converted'))
7689

77-
// Trigger a reload of the file list
78-
if (parentFolder) {
79-
emit('files:node:updated', parentFolder)
80-
}
90+
// Extract files that are within the current directory
91+
// in batch mode, you might have files from different directories
92+
// ⚠️, let's get the actual current dir, as the one from the action
93+
// might have changed as the user navigated away
94+
const currentDir = window.OCP.Files.Router.query.dir as string
95+
const newPaths = results
96+
.filter(result => result.status === 'fulfilled')
97+
.map(result => result.value.data.ocs.data.path)
98+
.filter(path => path.startsWith(currentDir))
99+
100+
// Fetch the new files
101+
logger.debug('Files to fetch', { newPaths })
102+
const newFiles = await Promise.all(newPaths.map(path => fetchNode(path)))
103+
104+
// Inform the file list about the new files
105+
newFiles.forEach(file => emit('files:node:created', file))
81106

82107
// Switch to the new files
83-
const firstSuccess = results[0] as PromiseFulfilledResult<AxiosResponse>
108+
const firstSuccess = results[0] as PromiseFulfilledResult<ConversionSuccess>
84109
const newFileId = firstSuccess.value.data.ocs.data.fileId
85-
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query)
110+
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query)
86111
} catch (error) {
87112
// Should not happen as we use allSettled and handle errors above
88113
showError(t('files', 'Failed to convert files'))
@@ -93,24 +118,23 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st
93118
}
94119
}
95120

96-
export const convertFile = async function(fileId: number, targetMimeType: string, parentFolder: Folder | null) {
121+
export const convertFile = async function(fileId: number, targetMimeType: string) {
97122
const toast = showLoading(t('files', 'Converting file…'))
98123

99124
try {
100-
const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse
125+
const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse<OCSResponse<ConversionResponse>>
101126
showSuccess(t('files', 'File successfully converted'))
102127

103-
// Trigger a reload of the file list
104-
if (parentFolder) {
105-
emit('files:node:updated', parentFolder)
106-
}
128+
// Inform the file list about the new file
129+
const newFile = await fetchNode(result.data.ocs.data.path)
130+
emit('files:node:created', newFile)
107131

108132
// Switch to the new file
109133
const newFileId = result.data.ocs.data.fileId
110-
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query)
134+
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query)
111135
} catch (error) {
112136
// If the server returned an error message, show it
113-
if (error.response?.data?.ocs?.meta?.message) {
137+
if (error instanceof AxiosError && error?.response?.data?.ocs?.meta?.message) {
114138
showError(t('files', 'Failed to convert file: {message}', { message: error.response.data.ocs.meta.message }))
115139
return
116140
}
@@ -122,26 +146,3 @@ export const convertFile = async function(fileId: number, targetMimeType: string
122146
toast.hideToast()
123147
}
124148
}
125-
126-
/**
127-
* Get the parent folder of a path
128-
*
129-
* TODO: replace by the parent node straight away when we
130-
* update the Files actions api accordingly.
131-
*
132-
* @param view The current view
133-
* @param path The path to the file
134-
* @returns The parent folder
135-
*/
136-
export const getParentFolder = function(view: View, path: string): Folder | null {
137-
const filesStore = useFilesStore(getPinia())
138-
const pathsStore = usePathsStore(getPinia())
139-
140-
const parentSource = pathsStore.getPath(view.id, path)
141-
if (!parentSource) {
142-
return null
143-
}
144-
145-
const parentFolder = filesStore.getNode(parentSource) as Folder | undefined
146-
return parentFolder ?? null
147-
}

apps/files/src/services/WebdavClient.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
65
import type { FileStat, ResponseDataDetailed } from 'webdav'
76
import type { Node } from '@nextcloud/files'
87

9-
export const client = davGetClient()
8+
import { getClient, getDefaultPropfind, getRootPath, resultToNode } from '@nextcloud/files/dav'
109

11-
export const fetchNode = async (node: Node): Promise<Node> => {
12-
const propfindPayload = davGetDefaultPropfind()
13-
const result = await client.stat(`${davRootPath}${node.path}`, {
10+
export const client = getClient()
11+
12+
export const fetchNode = async (path: string): Promise<Node> => {
13+
const propfindPayload = getDefaultPropfind()
14+
const result = await client.stat(`${getRootPath()}${path}`, {
1415
details: true,
1516
data: propfindPayload,
1617
}) as ResponseDataDetailed<FileStat>
17-
return davResultToNode(result.data)
18+
return resultToNode(result.data)
1819
}

apps/files/src/store/files.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export const useFilesStore = function(...args) {
135135
// If we have multiple nodes with the same file ID, we need to update all of them
136136
const nodes = this.getNodesById(node.fileid)
137137
if (nodes.length > 1) {
138-
await Promise.all(nodes.map(fetchNode)).then(this.updateNodes)
138+
await Promise.all(nodes.map(node => fetchNode(node.path))).then(this.updateNodes)
139139
logger.debug(nodes.length + ' nodes updated in store', { fileid: node.fileid })
140140
return
141141
}
@@ -147,7 +147,7 @@ export const useFilesStore = function(...args) {
147147
}
148148

149149
// Otherwise, it means we receive an event for a node that is not in the store
150-
fetchNode(node).then(n => this.updateNodes([n]))
150+
fetchNode(node.path).then(n => this.updateNodes([n]))
151151
},
152152

153153
// Handlers for legacy sidebar (no real nodes support)

apps/files/src/views/Sidebar.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ export default {
491491
this.loading = true
492492
493493
try {
494-
this.node = await fetchNode({ path: this.file })
494+
this.node = await fetchNode(this.file)
495495
this.fileInfo = FileInfo(this.node)
496496
// adding this as fallback because other apps expect it
497497
this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')

apps/files_sharing/src/mixins/SharesMixin.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { getCurrentUser } from '@nextcloud/auth'
77
import { showError, showSuccess } from '@nextcloud/dialogs'
88
import { ShareType } from '@nextcloud/sharing'
99
import { emit } from '@nextcloud/event-bus'
10-
import { fetchNode } from '../services/WebdavClient.ts'
1110

1211
import PQueue from 'p-queue'
1312
import debounce from 'debounce'
@@ -20,6 +19,7 @@ import logger from '../services/logger.ts'
2019
import {
2120
BUNDLED_PERMISSIONS,
2221
} from '../lib/SharePermissionsToolBox.js'
22+
import { fetchNode } from '../../../files/src/services/WebdavClient.ts'
2323

2424
export default {
2525
mixins: [SharesRequests],
@@ -164,7 +164,7 @@ export default {
164164
async getNode() {
165165
const node = { path: this.path }
166166
try {
167-
this.node = await fetchNode(node)
167+
this.node = await fetchNode(node.path)
168168
logger.info('Fetched node:', { node: this.node })
169169
} catch (error) {
170170
logger.error('Error:', error)

apps/files_sharing/src/services/WebdavClient.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)