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 <opensource@fthiessen.de>
pull/53176/head
Ferdinand Thiessen 2025-05-28 16:50:35 +07:00
parent 44dd42870e
commit 6ef37924bf
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
3 changed files with 78 additions and 68 deletions

@ -122,6 +122,7 @@ describe('Edit locally action execute tests', () => {
jest.spyOn(axios, 'post').mockImplementation(async () => ({ jest.spyOn(axios, 'post').mockImplementation(async () => ({
data: { ocs: { data: { token: 'foobar' } } }, data: { ocs: { data: { token: 'foobar' } } },
})) }))
const windowOpenSpy = jest.spyOn(window, 'open')
const mockedShowError = jest.mocked(showError) const mockedShowError = jest.mocked(showError)
const spyDialogBuilder = jest.spyOn(dialogBuilder, 'build') const spyDialogBuilder = jest.spyOn(dialogBuilder, 'build')
@ -142,7 +143,7 @@ describe('Edit locally action execute tests', () => {
expect(axios.post).toBeCalledTimes(1) 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(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files/api/v1/openlocaleditor?format=json', { path: '/foobar.txt' })
expect(mockedShowError).toBeCalledTimes(0) 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 () => { test('Edit locally fails and shows error', async () => {

@ -12,57 +12,56 @@ import axios from '@nextcloud/axios'
import IconWeb from '@mdi/svg/svg/web.svg?raw' import IconWeb from '@mdi/svg/svg/web.svg?raw'
import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw' import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw'
const confirmLocalEditDialog = ( export const action = new FileAction({
localEditCallback: (openingLocally: boolean) => void = () => {}, id: 'edit-locally',
) => { displayName: () => t('files', 'Edit locally'),
let callbackCalled = false iconSvgInline: () => LaptopSvg,
return (new DialogBuilder()) // Only works on single files
.setName(t('files', 'Edit file locally')) enabled(nodes: Node[]) {
.setText(t('files', 'The file should now open on your device. If it doesn\'t, please check that you have the desktop app installed.')) // Only works on single node
.setButtons([ if (nodes.length !== 1) {
{ return false
label: t('files', 'Retry and close'), }
type: 'secondary',
callback: () => { return (nodes[0].permissions & Permission.UPDATE) !== 0
callbackCalled = true
localEditCallback(true)
},
},
{
label: t('files', 'Edit online'),
icon: IconWeb,
type: 'primary',
callback: () => {
callbackCalled = true
localEditCallback(false)
}, },
async exec(node: Node) {
await attemptOpenLocalClient(node.path)
return null
}, },
])
.build() order: 25,
.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) * Try to open the path in the Nextcloud client.
confirmLocalEditDialog( *
(openLocally: boolean) => { * If this fails a dialog is shown with 3 options:
if (!openLocally) { * 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 }) window.OCA.Viewer.open({ path })
return
} }
openLocalClient(path)
},
)
} }
const openLocalClient = async function(path: string) { /**
* 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<void> {
const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json' const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json'
try { try {
@ -71,31 +70,39 @@ const openLocalClient = async function(path: string) {
let url = `nc://open/${uid}@` + window.location.host + encodePath(path) let url = `nc://open/${uid}@` + window.location.host + encodePath(path)
url += '?token=' + result.data.ocs.data.token url += '?token=' + result.data.ocs.data.token
window.location.href = url window.open(url, '_self')
} catch (error) { } catch (error) {
showError(t('files', 'Failed to redirect to client')) showError(t('files', 'Failed to redirect to client'))
} }
} }
export const action = new FileAction({ /**
id: 'edit-locally', * Open the confirmation dialog.
displayName: () => t('files', 'Edit locally'), */
iconSvgInline: () => LaptopSvg, async function confirmLocalEditDialog(): Promise<'online'|'local'|false> {
let result: 'online'|'local'|false = false
// Only works on single files const dialog = (new DialogBuilder())
enabled(nodes: Node[]) { .setName(t('files', 'Open file locally'))
// Only works on single node .setText(t('files', 'The file should now open on your device. If it doesn\'t, please check that you have the desktop app installed.'))
if (nodes.length !== 1) { .setButtons([
return false {
} label: t('files', 'Retry and close'),
type: 'secondary',
return (nodes[0].permissions & Permission.UPDATE) !== 0 callback: () => {
result = 'local'
}, },
async exec(node: Node) {
attemptOpenLocalClient(node.path)
return null
}, },
{
label: t('files', 'Open online'),
icon: IconWeb,
type: 'primary',
callback: () => {
result = 'online'
},
},
])
.build()
order: 25, await dialog.show()
}) return result
}

@ -169,7 +169,9 @@ const config = {
plugins: [ plugins: [
new VueLoaderPlugin(), new VueLoaderPlugin(),
new NodePolyfillPlugin(), new NodePolyfillPlugin({
additionalAliases: ['process'],
}),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
// Provide jQuery to jquery plugins as some are loaded before $ is exposed globally. // 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 // We need to provide the path to node_moduels as otherwise npm link will fail due