Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion apps/files/src/actions/openLocallyAction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ vi.mock('@nextcloud/dialogs', async (importOriginal) => ({
...await importOriginal<typeof nextcloudDialogs>(),
showError: vi.fn(),
}))
vi.mock('@nextcloud/viewer', () => ({ getViewer: () => ({ open: vi.fn() }) }))

const view = {
id: 'files',
Expand Down Expand Up @@ -155,8 +156,31 @@ describe('Open locally action enabled tests', () => {
})

describe('Open locally action execute tests', () => {
const file = new File({
id: 1,
source: 'http://nextcloud.local/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.UPDATE,
root: '/files/admin',
})

/**
* Answer the confirmation dialog with one of its buttons
*
* @param label - The label of the button to click
*/
function clickDialogButton(label: string) {
const setButtons = nextcloudDialogs.DialogBuilder.prototype.setButtons
vi.spyOn(nextcloudDialogs.DialogBuilder.prototype, 'setButtons').mockImplementation(function(this: nextcloudDialogs.DialogBuilder, buttons) {
buttons.find((button) => button.label === label)!.callback()
return setButtons.call(this, buttons)
})
}

let spyShowDialog
beforeEach(() => {
vi.restoreAllMocks()
vi.resetAllMocks()
spyShowDialog = vi.spyOn(nextcloudDialogs.Dialog.prototype, 'show')
.mockImplementation(() => Promise.resolve())
Expand Down Expand Up @@ -189,12 +213,49 @@ describe('Open locally action execute tests', () => {

// Silent action
expect(exec).toBe(null)
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledTimes(2)
expect(axios.post).toBeCalledWith('http://nextcloud.local/ocs/v2.php/apps/files/api/v1/openlocaleditor?format=json', { path: '/foobar.txt' })
expect(axios.post).toBeCalledWith('http://nextcloud.local/ocs/v2.php/apps/files/api/v1/opened', { fileId: 1 })
expect(showError).toBeCalledTimes(0)
expect(windowOpenSpy).toBeCalledWith('nc://open/test@nextcloud.local/foobar.txt?token=foobar', '_self')
})

test('Open locally reports the file opened once, even after a retry', async () => {
vi.spyOn(axios, 'post').mockImplementation(async () => ({
data: { ocs: { data: { token: 'foobar' } } },
}))
vi.spyOn(window, 'open').mockImplementation(() => null)
clickDialogButton('Retry and close')

await action.exec({
nodes: [file],
view,
folder: {} as IFolder,
contents: [],
})

expect(axios.post).toBeCalledTimes(3)
expect(vi.mocked(axios.post).mock.calls.filter(([url]) => url.endsWith('/opened'))).toHaveLength(1)
})

test('Open online leaves reporting the file opened to the viewer', async () => {
vi.spyOn(axios, 'post').mockImplementation(async () => ({
data: { ocs: { data: { token: 'foobar' } } },
}))
vi.spyOn(window, 'open').mockImplementation(() => null)
clickDialogButton('Open online')

await action.exec({
nodes: [file],
view,
folder: {} as IFolder,
contents: [],
})

expect(axios.post).toBeCalledTimes(1)
expect(axios.post).not.toBeCalledWith('http://nextcloud.local/ocs/v2.php/apps/files/api/v1/opened', expect.anything())
})

test('Open locally fails and shows error', async () => {
vi.spyOn(axios, 'post').mockImplementation(async () => ({}))
const showError = vi.spyOn(nextcloudDialogs, 'showError')
Expand Down
30 changes: 26 additions & 4 deletions apps/files/src/actions/openLocallyAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,20 @@ export const action: IFileAction = {
* @param contents - The folder the file is in, so the viewer can navigate
*/
async function attemptOpenLocalClient(node: INode, contents: ActionContextSingle['contents']) {
await openLocalClient(node.path)
let redirected = await openLocalClient(node.path)
const result = await confirmLocalEditDialog()
if (result === 'local') {
await openLocalClient(node.path)
redirected = await openLocalClient(node.path) || redirected
} else if (result === 'online') {
// The listing carries the folders too, and the viewer pages through
// files alone
// files alone. The viewer reports the open itself.
const files = contents.filter((entry): entry is IFile => entry.type === FileType.File)
await getViewer().open(files, node as IFile)
return
}

if (redirected) {
await reportOpened(node)
}
}

Expand All @@ -83,8 +88,9 @@ async function attemptOpenLocalClient(node: INode, contents: ActionContextSingle
* There is no way to get notified if this action was successful.
*
* @param path - Path to open
* @return Whether the client was asked to open it
*/
async function openLocalClient(path: string): Promise<void> {
async function openLocalClient(path: string): Promise<boolean> {
const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json'

try {
Expand All @@ -94,9 +100,25 @@ async function openLocalClient(path: string): Promise<void> {
url += '?token=' + result.data.ocs.data.token

window.open(url, '_self')
return true
} catch (error) {
showError(t('files', 'Failed to redirect to client'))
logger.error('Failed to redirect to client', { error })
return false
}
}

/**
* Tell the server the user opened the file, so apps know what they work with.
* We cannot know whether the client did open it, only that it was asked to.
*
* @param node - The file opened
*/
async function reportOpened(node: INode): Promise<void> {
try {
await axios.post(generateOcsUrl('apps/files/api/v1/opened'), { fileId: node.fileid })
} catch (error) {
logger.error('Failed to report the file as opened', { error })
}
}

Expand Down
Loading