diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2025-02-04 16:03:06 +0100 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2025-02-04 16:38:34 +0100 |
commit | 85ba80b8ceee330069ce47a572b4c0a23017dd91 (patch) | |
tree | e0efb17554b147ae93a089ac1b217e9856549695 /apps | |
parent | 83e35b69915039a6c174be6e471145673995e439 (diff) | |
download | nextcloud-server-85ba80b8ceee330069ce47a572b4c0a23017dd91.tar.gz nextcloud-server-85ba80b8ceee330069ce47a572b4c0a23017dd91.zip |
fix(files): properly update store on files conversions success
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/actions/convertAction.ts | 10 | ||||
-rw-r--r-- | apps/files/src/actions/convertUtils.ts | 97 | ||||
-rw-r--r-- | apps/files/src/services/WebdavClient.ts | 13 | ||||
-rw-r--r-- | apps/files/src/store/files.ts | 4 | ||||
-rw-r--r-- | apps/files/src/views/Sidebar.vue | 2 | ||||
-rw-r--r-- | apps/files_sharing/src/mixins/SharesMixin.js | 4 | ||||
-rw-r--r-- | apps/files_sharing/src/services/WebdavClient.ts | 18 |
7 files changed, 65 insertions, 83 deletions
diff --git a/apps/files/src/actions/convertAction.ts b/apps/files/src/actions/convertAction.ts index a8b4d537eb2..4992dea312b 100644 --- a/apps/files/src/actions/convertAction.ts +++ b/apps/files/src/actions/convertAction.ts @@ -11,7 +11,7 @@ import { t } from '@nextcloud/l10n' import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw' -import { convertFile, convertFiles, getParentFolder } from './convertUtils' +import { convertFile, convertFiles } from './convertUtils' type ConversionsProvider = { from: string, @@ -33,17 +33,17 @@ export const registerConvertActions = () => { return nodes.every(node => from === node.mime) }, - async exec(node: Node, view: View, dir: string) { + async exec(node: Node) { // If we're here, we know that the node has a fileid - convertFile(node.fileid as number, to, getParentFolder(view, dir)) + convertFile(node.fileid as number, to) // Silently terminate, we'll handle the UI in the background return null }, - async execBatch(nodes: Node[], view: View, dir: string) { + async execBatch(nodes: Node[]) { const fileIds = nodes.map(node => node.fileid).filter(Boolean) as number[] - convertFiles(fileIds, to, getParentFolder(view, dir)) + convertFiles(fileIds, to) // Silently terminate, we'll handle the UI in the background return Array(nodes.length).fill(null) diff --git a/apps/files/src/actions/convertUtils.ts b/apps/files/src/actions/convertUtils.ts index f32dbed0cd1..cee89f9bb6f 100644 --- a/apps/files/src/actions/convertUtils.ts +++ b/apps/files/src/actions/convertUtils.ts @@ -2,23 +2,34 @@ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { AxiosResponse } from '@nextcloud/axios' -import type { Folder, View } from '@nextcloud/files' +import type { AxiosResponse, AxiosError } from '@nextcloud/axios' +import type { OCSResponse } from '@nextcloud/typings/ocs' import { emit } from '@nextcloud/event-bus' import { generateOcsUrl } from '@nextcloud/router' import { showError, showLoading, showSuccess } from '@nextcloud/dialogs' import { t } from '@nextcloud/l10n' -import axios from '@nextcloud/axios' +import axios, { isAxiosError } from '@nextcloud/axios' import PQueue from 'p-queue' +import { fetchNode } from '../services/WebdavClient.ts' import logger from '../logger' -import { useFilesStore } from '../store/files' -import { getPinia } from '../store' -import { usePathsStore } from '../store/paths' -const queue = new PQueue({ concurrency: 5 }) +type ConversionResponse = { + path: string + fileId: number +} + +interface PromiseRejectedResult<T> { + status: 'rejected' + reason: T +} + +type PromiseSettledResult<T, E> = PromiseFulfilledResult<T> | PromiseRejectedResult<E>; +type ConversionSuccess = AxiosResponse<OCSResponse<ConversionResponse>> +type ConversionError = AxiosError<OCSResponse<ConversionResponse>> +const queue = new PQueue({ concurrency: 5 }) const requestConversion = function(fileId: number, targetMimeType: string): Promise<AxiosResponse> { return axios.post(generateOcsUrl('/apps/files/api/v1/convert'), { fileId, @@ -26,7 +37,7 @@ const requestConversion = function(fileId: number, targetMimeType: string): Prom }) } -export const convertFiles = async function(fileIds: number[], targetMimeType: string, parentFolder: Folder | null) { +export const convertFiles = async function(fileIds: number[], targetMimeType: string) { const conversions = fileIds.map(fileId => queue.add(() => requestConversion(fileId, targetMimeType))) // Start conversion @@ -34,14 +45,14 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st // Handle results try { - const results = await Promise.allSettled(conversions) - const failed = results.filter(result => result.status === 'rejected') + const results = await Promise.allSettled(conversions) as PromiseSettledResult<ConversionSuccess, ConversionError>[] + const failed = results.filter(result => result.status === 'rejected') as PromiseRejectedResult<ConversionError>[] if (failed.length > 0) { - const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message) as string[] + const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message) logger.error('Failed to convert files', { fileIds, targetMimeType, messages }) // If all failed files have the same error message, show it - if (new Set(messages).size === 1) { + if (new Set(messages).size === 1 && typeof messages[0] === 'string') { showError(t('files', 'Failed to convert files: {message}', { message: messages[0] })) return } @@ -74,15 +85,27 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st // All files converted showSuccess(t('files', 'Files successfully converted')) - // Trigger a reload of the file list - if (parentFolder) { - emit('files:node:updated', parentFolder) - } + // Extract files that are within the current directory + // in batch mode, you might have files from different directories + // ⚠️, let's get the actual current dir, as the one from the action + // might have changed as the user navigated away + const currentDir = window.OCP.Files.Router.query.dir as string + const newPaths = results + .filter(result => result.status === 'fulfilled') + .map(result => result.value.data.ocs.data.path) + .filter(path => path.startsWith(currentDir)) + + // Fetch the new files + logger.debug('Files to fetch', { newPaths }) + const newFiles = await Promise.all(newPaths.map(path => fetchNode(path))) + + // Inform the file list about the new files + newFiles.forEach(file => emit('files:node:created', file)) // Switch to the new files - const firstSuccess = results[0] as PromiseFulfilledResult<AxiosResponse> + const firstSuccess = results[0] as PromiseFulfilledResult<ConversionSuccess> const newFileId = firstSuccess.value.data.ocs.data.fileId - window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query) + window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query) } catch (error) { // Should not happen as we use allSettled and handle errors above showError(t('files', 'Failed to convert files')) @@ -93,24 +116,23 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st } } -export const convertFile = async function(fileId: number, targetMimeType: string, parentFolder: Folder | null) { +export const convertFile = async function(fileId: number, targetMimeType: string) { const toast = showLoading(t('files', 'Converting file…')) try { - const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse + const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse<OCSResponse<ConversionResponse>> showSuccess(t('files', 'File successfully converted')) - // Trigger a reload of the file list - if (parentFolder) { - emit('files:node:updated', parentFolder) - } + // Inform the file list about the new file + const newFile = await fetchNode(result.data.ocs.data.path) + emit('files:node:created', newFile) // Switch to the new file const newFileId = result.data.ocs.data.fileId - window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query) + window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query) } catch (error) { // If the server returned an error message, show it - if (error.response?.data?.ocs?.meta?.message) { + if (isAxiosError(error) && error.response?.data?.ocs?.meta?.message) { showError(t('files', 'Failed to convert file: {message}', { message: error.response.data.ocs.meta.message })) return } @@ -122,26 +144,3 @@ export const convertFile = async function(fileId: number, targetMimeType: string toast.hideToast() } } - -/** - * Get the parent folder of a path - * - * TODO: replace by the parent node straight away when we - * update the Files actions api accordingly. - * - * @param view The current view - * @param path The path to the file - * @returns The parent folder - */ -export const getParentFolder = function(view: View, path: string): Folder | null { - const filesStore = useFilesStore(getPinia()) - const pathsStore = usePathsStore(getPinia()) - - const parentSource = pathsStore.getPath(view.id, path) - if (!parentSource) { - return null - } - - const parentFolder = filesStore.getNode(parentSource) as Folder | undefined - return parentFolder ?? null -} diff --git a/apps/files/src/services/WebdavClient.ts b/apps/files/src/services/WebdavClient.ts index cd33147b03f..2b92deba9b4 100644 --- a/apps/files/src/services/WebdavClient.ts +++ b/apps/files/src/services/WebdavClient.ts @@ -2,17 +2,18 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' import type { FileStat, ResponseDataDetailed } from 'webdav' import type { Node } from '@nextcloud/files' -export const client = davGetClient() +import { getClient, getDefaultPropfind, getRootPath, resultToNode } from '@nextcloud/files/dav' -export const fetchNode = async (node: Node): Promise<Node> => { - const propfindPayload = davGetDefaultPropfind() - const result = await client.stat(`${davRootPath}${node.path}`, { +export const client = getClient() + +export const fetchNode = async (path: string): Promise<Node> => { + const propfindPayload = getDefaultPropfind() + const result = await client.stat(`${getRootPath()}${path}`, { details: true, data: propfindPayload, }) as ResponseDataDetailed<FileStat> - return davResultToNode(result.data) + return resultToNode(result.data) } diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts index a2413b5a1ba..295704c880b 100644 --- a/apps/files/src/store/files.ts +++ b/apps/files/src/store/files.ts @@ -135,7 +135,7 @@ export const useFilesStore = function(...args) { // If we have multiple nodes with the same file ID, we need to update all of them const nodes = this.getNodesById(node.fileid) if (nodes.length > 1) { - await Promise.all(nodes.map(fetchNode)).then(this.updateNodes) + await Promise.all(nodes.map(node => fetchNode(node.path))).then(this.updateNodes) logger.debug(nodes.length + ' nodes updated in store', { fileid: node.fileid }) return } @@ -147,7 +147,7 @@ export const useFilesStore = function(...args) { } // Otherwise, it means we receive an event for a node that is not in the store - fetchNode(node).then(n => this.updateNodes([n])) + fetchNode(node.path).then(n => this.updateNodes([n])) }, // Handlers for legacy sidebar (no real nodes support) diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index 5418d36297b..1867ea6a591 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -491,7 +491,7 @@ export default { this.loading = true try { - this.node = await fetchNode({ path: this.file }) + this.node = await fetchNode(this.file) this.fileInfo = FileInfo(this.node) // adding this as fallback because other apps expect it this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/') diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js index ab84a6f0e19..fdd8bbd72fd 100644 --- a/apps/files_sharing/src/mixins/SharesMixin.js +++ b/apps/files_sharing/src/mixins/SharesMixin.js @@ -7,7 +7,6 @@ import { getCurrentUser } from '@nextcloud/auth' import { showError, showSuccess } from '@nextcloud/dialogs' import { ShareType } from '@nextcloud/sharing' import { emit } from '@nextcloud/event-bus' -import { fetchNode } from '../services/WebdavClient.ts' import PQueue from 'p-queue' import debounce from 'debounce' @@ -20,6 +19,7 @@ import logger from '../services/logger.ts' import { BUNDLED_PERMISSIONS, } from '../lib/SharePermissionsToolBox.js' +import { fetchNode } from '../../../files/src/services/WebdavClient.ts' export default { mixins: [SharesRequests], @@ -164,7 +164,7 @@ export default { async getNode() { const node = { path: this.path } try { - this.node = await fetchNode(node) + this.node = await fetchNode(node.path) logger.info('Fetched node:', { node: this.node }) } catch (error) { logger.error('Error:', error) diff --git a/apps/files_sharing/src/services/WebdavClient.ts b/apps/files_sharing/src/services/WebdavClient.ts deleted file mode 100644 index cd33147b03f..00000000000 --- a/apps/files_sharing/src/services/WebdavClient.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' -import type { FileStat, ResponseDataDetailed } from 'webdav' -import type { Node } from '@nextcloud/files' - -export const client = davGetClient() - -export const fetchNode = async (node: Node): Promise<Node> => { - const propfindPayload = davGetDefaultPropfind() - const result = await client.stat(`${davRootPath}${node.path}`, { - details: true, - data: propfindPayload, - }) as ResponseDataDetailed<FileStat> - return davResultToNode(result.data) -} |