Skip to content

Commit 656828a

Browse files
authored
Merge pull request #46909 from nextcloud/backport/46452/stable28
[stable28] feat(editLocallyAction): Handle possible no local client scenario
2 parents 24d5c22 + 52ff995 commit 656828a

14 files changed

Lines changed: 110 additions & 30 deletions

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,35 @@
2222
import { action } from './editLocallyAction'
2323
import { expect } from '@jest/globals'
2424
import { File, Permission, View, FileAction } from '@nextcloud/files'
25-
import * as ncDialogs from '@nextcloud/dialogs'
25+
import { DialogBuilder, showError } from '@nextcloud/dialogs'
2626
import axios from '@nextcloud/axios'
2727

28+
const dialogBuilder = {
29+
setName: jest.fn().mockReturnThis(),
30+
setText: jest.fn().mockReturnThis(),
31+
setButtons: jest.fn().mockReturnThis(),
32+
build: jest.fn().mockReturnValue({
33+
show: jest.fn().mockResolvedValue(true),
34+
}),
35+
} as unknown as DialogBuilder
36+
37+
jest.mock('@nextcloud/dialogs', () => ({
38+
DialogBuilder: jest.fn(() => dialogBuilder),
39+
showError: jest.fn(),
40+
}))
41+
2842
const view = {
2943
id: 'files',
3044
name: 'Files',
3145
} as View
3246

47+
// Mock webroot variable
48+
beforeAll(() => {
49+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50+
(window as any)._oc_webroot = '';
51+
(window as any).OCA = { Viewer: { open: jest.fn() } }
52+
})
53+
3354
describe('Edit locally action conditions tests', () => {
3455
test('Default values', () => {
3556
expect(action).toBeInstanceOf(FileAction)
@@ -55,7 +76,7 @@ describe('Edit locally action enabled tests', () => {
5576
expect(action.enabled!([file], view)).toBe(true)
5677
})
5778

58-
test('Disabled for non-dav ressources', () => {
79+
test('Disabled for non-dav resources', () => {
5980
const file = new File({
6081
id: 1,
6182
source: 'https://domain.com/data/foobar.txt',
@@ -115,8 +136,11 @@ describe('Edit locally action enabled tests', () => {
115136

116137
describe('Edit locally action execute tests', () => {
117138
test('Edit locally opens proper URL', async () => {
118-
jest.spyOn(axios, 'post').mockImplementation(async () => ({ data: { ocs: { data: { token: 'foobar' } } } }))
119-
jest.spyOn(ncDialogs, 'showError')
139+
jest.spyOn(axios, 'post').mockImplementation(async () => ({
140+
data: { ocs: { data: { token: 'foobar' } } }
141+
}))
142+
const mockedShowError = jest.mocked(showError)
143+
const spyDialogBuilder = jest.spyOn(dialogBuilder, 'build')
120144

121145
const file = new File({
122146
id: 1,
@@ -128,17 +152,20 @@ describe('Edit locally action execute tests', () => {
128152

129153
const exec = await action.exec(file, view, '/')
130154

155+
expect(spyDialogBuilder).toBeCalled()
156+
131157
// Silent action
132158
expect(exec).toBe(null)
133159
expect(axios.post).toBeCalledTimes(1)
134160
expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files/api/v1/openlocaleditor?format=json', { path: '/foobar.txt' })
135-
expect(ncDialogs.showError).toBeCalledTimes(0)
161+
expect(mockedShowError).toBeCalledTimes(0)
136162
expect(window.location.href).toBe('nc://open/test@localhost/foobar.txt?token=foobar')
137163
})
138164

139-
test('Edit locally fails and show error', async () => {
165+
test('Edit locally fails and shows error', async () => {
140166
jest.spyOn(axios, 'post').mockImplementation(async () => ({}))
141-
jest.spyOn(ncDialogs, 'showError')
167+
const mockedShowError = jest.mocked(showError)
168+
const spyDialogBuilder = jest.spyOn(dialogBuilder, 'build')
142169

143170
const file = new File({
144171
id: 1,
@@ -150,12 +177,14 @@ describe('Edit locally action execute tests', () => {
150177

151178
const exec = await action.exec(file, view, '/')
152179

180+
expect(spyDialogBuilder).toBeCalled()
181+
153182
// Silent action
154183
expect(exec).toBe(null)
155184
expect(axios.post).toBeCalledTimes(1)
156185
expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files/api/v1/openlocaleditor?format=json', { path: '/foobar.txt' })
157-
expect(ncDialogs.showError).toBeCalledTimes(1)
158-
expect(ncDialogs.showError).toBeCalledWith('Failed to redirect to client')
186+
expect(mockedShowError).toBeCalledTimes(1)
187+
expect(mockedShowError).toBeCalledWith('Failed to redirect to client')
159188
expect(window.location.href).toBe('http://localhost/')
160189
})
161190
})

apps/files/src/actions/editLocallyAction.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,62 @@ import { encodePath } from '@nextcloud/paths'
2323
import { generateOcsUrl } from '@nextcloud/router'
2424
import { getCurrentUser } from '@nextcloud/auth'
2525
import { FileAction, Permission, type Node } from '@nextcloud/files'
26-
import { showError } from '@nextcloud/dialogs'
26+
import { showError, DialogBuilder } from '@nextcloud/dialogs'
2727
import { translate as t } from '@nextcloud/l10n'
2828
import axios from '@nextcloud/axios'
29-
3029
import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw'
30+
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
31+
import IconCheck from '@mdi/svg/svg/check.svg?raw'
32+
33+
const confirmLocalEditDialog = (
34+
localEditCallback: (openingLocally: boolean) => void = () => {},
35+
) => {
36+
let callbackCalled = false
37+
38+
return (new DialogBuilder())
39+
.setName(t('files', 'Edit file locally'))
40+
.setText(t('files', 'The file should now open locally. If you don\'t see this happening, make sure that the desktop client is installed on your system.'))
41+
.setButtons([
42+
{
43+
label: t('files', 'Retry local edit'),
44+
icon: IconCancel,
45+
callback: () => {
46+
callbackCalled = true
47+
localEditCallback(false)
48+
},
49+
},
50+
{
51+
label: t('files', 'Edit online'),
52+
icon: IconCheck,
53+
type: 'primary',
54+
callback: () => {
55+
callbackCalled = true
56+
localEditCallback(true)
57+
},
58+
},
59+
])
60+
.build()
61+
.show()
62+
.then(() => {
63+
// Ensure the callback is called even if the dialog is dismissed in other ways
64+
if (!callbackCalled) {
65+
localEditCallback(false)
66+
}
67+
})
68+
}
69+
70+
const attemptOpenLocalClient = async (path: string) => {
71+
openLocalClient(path)
72+
confirmLocalEditDialog(
73+
(openLocally: boolean) => {
74+
if (!openLocally) {
75+
window.OCA.Viewer.open({ path })
76+
return
77+
}
78+
openLocalClient(path)
79+
},
80+
)
81+
}
3182

3283
const openLocalClient = async function(path: string) {
3384
const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json'
@@ -60,7 +111,7 @@ export const action = new FileAction({
60111
},
61112

62113
async exec(node: Node) {
63-
openLocalClient(node.path)
114+
attemptOpenLocalClient(node.path)
64115
return null
65116
},
66117

dist/core-common.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/core-common.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-init.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-init.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files_sharing-init.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files_sharing-init.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-apps-view-4529.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-apps-view-4529.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)