]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat: add edit locally action with tests
authorJohn Molakvoæ <skjnldsv@protonmail.com>
Wed, 21 Jun 2023 15:10:06 +0000 (17:10 +0200)
committerJohn Molakvoæ <skjnldsv@users.noreply.github.com>
Thu, 22 Jun 2023 08:47:08 +0000 (10:47 +0200)
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
__mocks__/@nextcloud/axios.ts
__tests__/jest-setup.ts
apps/files/src/actions/editLocallyAction.spec.ts [new file with mode: 0644]
apps/files/src/actions/editLocallyAction.ts [new file with mode: 0644]
package-lock.json
package.json

index c78420c98b08572f4c304fc57aad3f9d970972ec..02564ab6c6ded5f6f63b4d994d4b1184277829e4 100644 (file)
@@ -20,5 +20,6 @@
  *
  */
 export default {
-         delete: async () => ({ status: 200, data: {} }),
+       delete: async () => ({ status: 200, data: {} }),
+       post: async () => ({ status: 200, data: {} }),
 }
index c0813ff003f2a47f496fa9eaa03aa58681eb9a1f..1bcd6bf767dd53518b128f463dc19ee5e26ec577 100644 (file)
@@ -21,3 +21,6 @@
  */
 
 import '@testing-library/jest-dom'
+
+// Mock `window.location` with Jest spies and extend expect
+import 'jest-location-mock'
diff --git a/apps/files/src/actions/editLocallyAction.spec.ts b/apps/files/src/actions/editLocallyAction.spec.ts
new file mode 100644 (file)
index 0000000..93315c5
--- /dev/null
@@ -0,0 +1,163 @@
+/**
+ * @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/')
+       })
+})
diff --git a/apps/files/src/actions/editLocallyAction.ts b/apps/files/src/actions/editLocallyAction.ts
new file mode 100644 (file)
index 0000000..ad7e805
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * @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)
+}
index 4631d185b967bb8e91bc17d0e3ca4680cd33e8c0..785fd9951ef52efec9e2cd09f0c9667444916b0b 100644 (file)
         "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,
index 632ee14b39ee280fb81be4a7eba2d896fe1c5697..13538c5a2ac5d7bda036e13130a96c58222b87bc 100644 (file)
     "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",