*
*/
export default {
- delete: async () => ({ status: 200, data: {} }),
+ delete: async () => ({ status: 200, data: {} }),
+ post: async () => ({ status: 200, data: {} }),
}
*/
import '@testing-library/jest-dom'
+
+// Mock `window.location` with Jest spies and extend expect
+import 'jest-location-mock'
--- /dev/null
+/**
+ * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+import { action } from './editLocallyAction'
+import { expect } from '@jest/globals'
+import { File, Folder, Permission } from '@nextcloud/files'
+import { FileAction } from '../services/FileAction'
+import axios from '@nextcloud/axios'
+import type { Navigation } from '../services/Navigation'
+import ncDialogs from '@nextcloud/dialogs'
+
+const view = {
+ id: 'files',
+ name: 'Files',
+} as Navigation
+
+describe('Edit locally action conditions tests', () => {
+ test('Default values', () => {
+ expect(action).toBeInstanceOf(FileAction)
+ expect(action.id).toBe('edit-locally')
+ expect(action.displayName([], view)).toBe('Edit locally')
+ expect(action.iconSvgInline([], view)).toBe('SvgMock')
+ expect(action.default).toBe(true)
+ expect(action.order).toBe(25)
+ })
+})
+
+describe('Edit locally action enabled tests', () => {
+ test('Enabled for file with UPDATE permission', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ })
+
+ expect(action.enabled).toBeDefined()
+ expect(action.enabled!([file], view)).toBe(true)
+ })
+
+ test('Disabled for non-dav ressources', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://domain.com/data/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ })
+
+ expect(action.enabled).toBeDefined()
+ expect(action.enabled!([file], view)).toBe(false)
+ })
+
+ test('Disabled if more than one node', () => {
+ const file1 = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ })
+ const file2 = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ })
+
+ expect(action.enabled).toBeDefined()
+ expect(action.enabled!([file1, file2], view)).toBe(false)
+ })
+
+ test('Disabled for files', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
+ owner: 'admin',
+ mime: 'text/plain',
+ })
+
+ expect(action.enabled).toBeDefined()
+ expect(action.enabled!([file], view)).toBe(false)
+ })
+
+ test('Disabled without UPDATE permissions', () => {
+ const file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.READ,
+ })
+
+ expect(action.enabled).toBeDefined()
+ expect(action.enabled!([file], view)).toBe(false)
+ })
+})
+
+describe('Edit locally action execute tests', () => {
+ test('Edit locally opens proper URL', async () => {
+ jest.spyOn(axios, 'post').mockImplementation(async () => ({ data: { ocs: { data: { token: 'foobar' } } } }))
+ jest.spyOn(ncDialogs, 'showError')
+
+ const file = new File({
+ id: 1,
+ source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.UPDATE,
+ })
+
+ const exec = await action.exec(file, view, '/')
+
+ // Silent action
+ expect(exec).toBe(null)
+ 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(ncDialogs.showError).toBeCalledTimes(0)
+ expect(window.location.href).toBe('nc://open/test@localhost/foobar.txt?token=foobar')
+ })
+
+ test('Edit locally fails and show error', async () => {
+ jest.spyOn(axios, 'post').mockImplementation(async () => ({}))
+ jest.spyOn(ncDialogs, 'showError')
+
+ const file = new File({
+ id: 1,
+ source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.UPDATE,
+ })
+
+ const exec = await action.exec(file, view, '/')
+
+ // Silent action
+ expect(exec).toBe(null)
+ 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(ncDialogs.showError).toBeCalledTimes(1)
+ expect(ncDialogs.showError).toBeCalledWith('Failed to redirect to client')
+ expect(window.location.href).toBe('http://localhost/')
+ })
+})
--- /dev/null
+/**
+ * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+import { encodePath } from '@nextcloud/paths'
+import { Permission, type Node } from '@nextcloud/files'
+import { translate as t } from '@nextcloud/l10n'
+import axios from '@nextcloud/axios'
+import DevicesSvg from '@mdi/svg/svg/devices.svg?raw'
+
+import { generateOcsUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import { registerFileAction, FileAction } from '../services/FileAction'
+import { showError } from '@nextcloud/dialogs'
+
+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'),
+ iconSvgInline: () => DevicesSvg,
+
+ // Only works on single files
+ enabled(nodes: Node[]) {
+ // Only works on single node
+ if (nodes.length !== 1) {
+ return false
+ }
+
+ return (nodes[0].permissions & Permission.UPDATE) !== 0
+ },
+
+ async exec(node: Node) {
+ openLocalClient(node.path)
+ return null
+ },
+
+ default: true,
+ order: 25,
+})
+
+if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
+ registerFileAction(action)
+}
"jasmine-sinon": "^0.4.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.5.0",
+ "jest-location-mock": "^1.0.9",
"jsdoc": "^4.0.2",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.1.1",
"workbox-webpack-plugin": "^6.5.4"
},
"engines": {
- "node": "^16.0.0",
- "npm": "^7.0.0 || ^8.0.0"
+ "node": "^20.0.0",
+ "npm": "^9.0.0"
}
},
"node_modules/@adobe/css-tools": {
"node": ">=8"
}
},
+ "node_modules/@jedmao/location": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@jedmao/location/-/location-3.0.0.tgz",
+ "integrity": "sha512-p7mzNlgJbCioUYLUEKds3cQG4CHONVFJNYqMe6ocEtENCL/jYmMo1Q3ApwsMmU+L0ZkaDJEyv4HokaByLoPwlQ==",
+ "dev": true
+ },
"node_modules/@jest/console": {
"version": "29.5.0",
"dev": true,
"dev": true,
"license": "MIT"
},
+ "node_modules/jest-location-mock": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/jest-location-mock/-/jest-location-mock-1.0.9.tgz",
+ "integrity": "sha512-DN/v7Zsa3N4uGgWTCrMrPPxhZORr/4N5gi+u7Tk6sLdORYplrC0//wfFN5FOtx4ZdQzDVfY6rLa4d+wfTKzQHw==",
+ "dev": true,
+ "dependencies": {
+ "@jedmao/location": "^3.0.0",
+ "jest-diff": "^27.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-location-mock/node_modules/diff-sequences": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
+ "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==",
+ "dev": true,
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/jest-diff": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
+ "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^27.5.1",
+ "jest-get-type": "^27.5.1",
+ "pretty-format": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/jest-get-type": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
+ "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==",
+ "dev": true,
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-location-mock/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jest-matcher-utils": {
"version": "29.5.0",
"dev": true,
"version": "0.1.3",
"dev": true
},
+ "@jedmao/location": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@jedmao/location/-/location-3.0.0.tgz",
+ "integrity": "sha512-p7mzNlgJbCioUYLUEKds3cQG4CHONVFJNYqMe6ocEtENCL/jYmMo1Q3ApwsMmU+L0ZkaDJEyv4HokaByLoPwlQ==",
+ "dev": true
+ },
"@jest/console": {
"version": "29.5.0",
"dev": true,
}
}
},
+ "jest-location-mock": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/jest-location-mock/-/jest-location-mock-1.0.9.tgz",
+ "integrity": "sha512-DN/v7Zsa3N4uGgWTCrMrPPxhZORr/4N5gi+u7Tk6sLdORYplrC0//wfFN5FOtx4ZdQzDVfY6rLa4d+wfTKzQHw==",
+ "dev": true,
+ "requires": {
+ "@jedmao/location": "^3.0.0",
+ "jest-diff": "^27.0.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "diff-sequences": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
+ "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "jest-diff": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
+ "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^27.5.1",
+ "jest-get-type": "^27.5.1",
+ "pretty-format": "^27.5.1"
+ }
+ },
+ "jest-get-type": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
+ "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
"jest-matcher-utils": {
"version": "29.5.0",
"dev": true,
"jasmine-sinon": "^0.4.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.5.0",
+ "jest-location-mock": "^1.0.9",
"jsdoc": "^4.0.2",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.1.1",