From 6ef37924bf829e13bf4e3cd1378957b802b129cc Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 28 May 2025 16:50:35 +0200 Subject: [PATCH] fix(files): do nothing if `view local` dialog was just closed We try to open a file in the Nextcloud client. If this fails a dialog is shown with 3 options: 1. Retry: If it fails no further dialog is shown. 2. Open online: The viewer is used to open the file. 3. Close the dialog and nothing happens (abort). This correctly implements 3 and also adds some comments + order file in reading order (using `function` instead of arrow functions allows this easily). Signed-off-by: Ferdinand Thiessen --- .../src/actions/editLocallyAction.spec.ts | 3 +- apps/files/src/actions/editLocallyAction.ts | 139 +++++++++--------- webpack.common.js | 4 +- 3 files changed, 78 insertions(+), 68 deletions(-) diff --git a/apps/files/src/actions/editLocallyAction.spec.ts b/apps/files/src/actions/editLocallyAction.spec.ts index 7eaf24dd3a5..0a32415b5c8 100644 --- a/apps/files/src/actions/editLocallyAction.spec.ts +++ b/apps/files/src/actions/editLocallyAction.spec.ts @@ -122,6 +122,7 @@ describe('Edit locally action execute tests', () => { jest.spyOn(axios, 'post').mockImplementation(async () => ({ data: { ocs: { data: { token: 'foobar' } } }, })) + const windowOpenSpy = jest.spyOn(window, 'open') const mockedShowError = jest.mocked(showError) const spyDialogBuilder = jest.spyOn(dialogBuilder, 'build') @@ -142,7 +143,7 @@ describe('Edit locally action execute tests', () => { expect(axios.post).toBeCalledTimes(1) expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files/api/v1/openlocaleditor?format=json', { path: '/foobar.txt' }) expect(mockedShowError).toBeCalledTimes(0) - expect(window.location.href).toBe('nc://open/test@localhost/foobar.txt?token=foobar') + expect(windowOpenSpy).toBeCalledWith('nc://open/test@localhost/foobar.txt?token=foobar', '_self') }) test('Edit locally fails and shows error', async () => { diff --git a/apps/files/src/actions/editLocallyAction.ts b/apps/files/src/actions/editLocallyAction.ts index 97a055c113f..42a9c64c1d9 100644 --- a/apps/files/src/actions/editLocallyAction.ts +++ b/apps/files/src/actions/editLocallyAction.ts @@ -12,71 +12,6 @@ import axios from '@nextcloud/axios' import IconWeb from '@mdi/svg/svg/web.svg?raw' import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw' -const confirmLocalEditDialog = ( - localEditCallback: (openingLocally: boolean) => void = () => {}, -) => { - let callbackCalled = false - - return (new DialogBuilder()) - .setName(t('files', 'Edit file locally')) - .setText(t('files', 'The file should now open on your device. If it doesn\'t, please check that you have the desktop app installed.')) - .setButtons([ - { - label: t('files', 'Retry and close'), - type: 'secondary', - callback: () => { - callbackCalled = true - localEditCallback(true) - }, - }, - { - label: t('files', 'Edit online'), - icon: IconWeb, - type: 'primary', - callback: () => { - callbackCalled = true - localEditCallback(false) - }, - }, - ]) - .build() - .show() - .then(() => { - // Ensure the callback is called even if the dialog is dismissed in other ways - if (!callbackCalled) { - localEditCallback(false) - } - }) -} - -const attemptOpenLocalClient = async (path: string) => { - openLocalClient(path) - confirmLocalEditDialog( - (openLocally: boolean) => { - if (!openLocally) { - window.OCA.Viewer.open({ path }) - return - } - openLocalClient(path) - }, - ) -} - -const openLocalClient = async function(path: string) { - const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json' - - try { - const result = await axios.post(link, { path }) - const uid = getCurrentUser()?.uid - let url = `nc://open/${uid}@` + window.location.host + encodePath(path) - url += '?token=' + result.data.ocs.data.token - - window.location.href = url - } catch (error) { - showError(t('files', 'Failed to redirect to client')) - } -} - export const action = new FileAction({ id: 'edit-locally', displayName: () => t('files', 'Edit locally'), @@ -93,9 +28,81 @@ export const action = new FileAction({ }, async exec(node: Node) { - attemptOpenLocalClient(node.path) + await attemptOpenLocalClient(node.path) return null }, order: 25, }) + +/** + * Try to open the path in the Nextcloud client. + * + * If this fails a dialog is shown with 3 options: + * 1. Retry: If it fails no further dialog is shown. + * 2. Open online: The viewer is used to open the file. + * 3. Close the dialog and nothing happens (abort). + * + * @param path - The path to open + */ +async function attemptOpenLocalClient(path: string) { + await openLocalClient(path) + const result = await confirmLocalEditDialog() + if (result === 'local') { + await openLocalClient(path) + } else if (result === 'online') { + window.OCA.Viewer.open({ path }) + } +} + +/** + * Try to open a file in the Nextcloud client. + * There is no way to get notified if this action was successfull. + * + * @param path - Path to open + */ +async function openLocalClient(path: string): Promise { + const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json' + + try { + const result = await axios.post(link, { path }) + const uid = getCurrentUser()?.uid + let url = `nc://open/${uid}@` + window.location.host + encodePath(path) + url += '?token=' + result.data.ocs.data.token + + window.open(url, '_self') + } catch (error) { + showError(t('files', 'Failed to redirect to client')) + } +} + +/** + * Open the confirmation dialog. + */ +async function confirmLocalEditDialog(): Promise<'online'|'local'|false> { + let result: 'online'|'local'|false = false + const dialog = (new DialogBuilder()) + .setName(t('files', 'Open file locally')) + .setText(t('files', 'The file should now open on your device. If it doesn\'t, please check that you have the desktop app installed.')) + .setButtons([ + { + label: t('files', 'Retry and close'), + type: 'secondary', + callback: () => { + result = 'local' + }, + }, + { + label: t('files', 'Open online'), + icon: IconWeb, + type: 'primary', + callback: () => { + result = 'online' + }, + }, + ]) + .build() + + await dialog.show() + return result +} diff --git a/webpack.common.js b/webpack.common.js index 1df53a986bd..0d285a3dd61 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -169,7 +169,9 @@ const config = { plugins: [ new VueLoaderPlugin(), - new NodePolyfillPlugin(), + new NodePolyfillPlugin({ + additionalAliases: ['process'], + }), new webpack.ProvidePlugin({ // Provide jQuery to jquery plugins as some are loaded before $ is exposed globally. // We need to provide the path to node_moduels as otherwise npm link will fail due