From f5f775d24f3ecd5e85480f406e82f87eb45f3566 Mon Sep 17 00:00:00 2001 From: Lukas Hirt Date: Mon, 23 Oct 2023 11:02:14 +0200 Subject: [PATCH 1/5] feat: add actions into the embed mode --- .../unreleased/enhancement-embed-mode-actions | 6 + docs/embed-mode/_index.md | 31 ++++ .../components/EmbedActions/EmbedActions.vue | 142 +++++++++++++++ .../src/components/FilesViewWrapper.vue | 14 +- .../EmbedActions/EmbedActions.spec.ts | 167 ++++++++++++++++++ 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/enhancement-embed-mode-actions create mode 100644 docs/embed-mode/_index.md create mode 100644 packages/web-app-files/src/components/EmbedActions/EmbedActions.vue create mode 100644 packages/web-app-files/tests/unit/components/EmbedActions/EmbedActions.spec.ts diff --git a/changelog/unreleased/enhancement-embed-mode-actions b/changelog/unreleased/enhancement-embed-mode-actions new file mode 100644 index 00000000000..a620b9cf9ee --- /dev/null +++ b/changelog/unreleased/enhancement-embed-mode-actions @@ -0,0 +1,6 @@ +Enhancement: Add embed mode actions + +We've added three new actions available in the embed mode. These actions are "Share", "Select" and "Share". They are emitting events with an optional payload. For more information, check the documentation. + +https://github.com/owncloud/web/pull/9841 +https://github.com/owncloud/web/issues/9768 diff --git a/docs/embed-mode/_index.md b/docs/embed-mode/_index.md new file mode 100644 index 00000000000..b443b085854 --- /dev/null +++ b/docs/embed-mode/_index.md @@ -0,0 +1,31 @@ +--- +title: 'Embed Mode' +date: 2023-10-23T00:00:00+00:00 +weight: 60 +geekdocRepo: https://github.com/owncloud/web +geekdocEditPath: edit/master/docs/embed-mode +geekdocFilePath: _index.md +geekdocCollapseSection: true +--- + +{{< toc >}} + +The ownCloud Web can be consumed by another application in a stripped down version called "Embed mode". This mode is supposed to be used in the context of selecting or sharing resources. If you're looking for even more minimalistic approach, you can take a look at the [File picker](https://owncloud.dev/integration/file_picker/). + +## Getting started + +To integrate ownCloud Web into your application, add an iframe element pointing to your ownCloud Web deployed instance with additional query parameter `mode=embed`. + +```html + +``` + +## Events + +The app is emitting various events depending on the goal of the user. All events are prefixed with `owncloud-embed:` to prevent any naming conflicts with other events. + +| Event name | Payload | Description | +| --- | --- | --- | +| **owncloud-embed:select** | Resource[] | Gets emitted when user selects resources via the "Attach as copy" action | +| **owncloud-embed:share** | string[] | Gets emitted when user selects resources and shares them via the "Share links" action | +| **owncloud-embed:cancel** | void | Gets emitted when user attempts to close the embedded instance via "Cancel" action | diff --git a/packages/web-app-files/src/components/EmbedActions/EmbedActions.vue b/packages/web-app-files/src/components/EmbedActions/EmbedActions.vue new file mode 100644 index 00000000000..502e69b43d5 --- /dev/null +++ b/packages/web-app-files/src/components/EmbedActions/EmbedActions.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/packages/web-app-files/src/components/FilesViewWrapper.vue b/packages/web-app-files/src/components/FilesViewWrapper.vue index ffb6b7ce7c7..18b6ecbc7eb 100644 --- a/packages/web-app-files/src/components/FilesViewWrapper.vue +++ b/packages/web-app-files/src/components/FilesViewWrapper.vue @@ -4,13 +4,25 @@ + + + + diff --git a/packages/web-app-files/tests/unit/components/EmbedActions/EmbedActions.spec.ts b/packages/web-app-files/tests/unit/components/EmbedActions/EmbedActions.spec.ts new file mode 100644 index 00000000000..006dffe4270 --- /dev/null +++ b/packages/web-app-files/tests/unit/components/EmbedActions/EmbedActions.spec.ts @@ -0,0 +1,167 @@ +import { + createStore, + defaultPlugins, + defaultStoreMockOptions, + shallowMount +} from 'web-test-helpers' +import EmbedActions from 'web-app-files/src/components/EmbedActions/EmbedActions.vue' + +jest.mock('@ownclouders/web-pkg', () => ({ + ...jest.requireActual('@ownclouders/web-pkg'), + createQuicklink: jest.fn().mockImplementation(({ resource, password }) => ({ + url: (password ? password + '-' : '') + 'link-' + resource.id + })), + showQuickLinkPasswordModal: jest.fn().mockImplementation((_options, cb) => cb('password')) +})) + +const selectors = Object.freeze({ + btnSelect: '[data-testid="button-select"]', + btnCancel: '[data-testid="button-cancel"]', + btnShare: '[data-testid="button-share"]' +}) + +describe('EmbedActions', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('select action', () => { + it('should disable select action when no resources are selected', () => { + const { wrapper } = getWrapper() + + expect(wrapper.find(selectors.btnSelect).attributes()).toHaveProperty('disabled') + }) + + it('should enable select action when at least one resource is selected', () => { + const { wrapper } = getWrapper({ selectedFiles: [{ id: 1 }] }) + + expect(wrapper.find(selectors.btnSelect).attributes()).not.toHaveProperty('disabled') + }) + + it('should emit select event when the select action is triggered', async () => { + window.parent.dispatchEvent = jest.fn() + global.CustomEvent = jest.fn().mockImplementation(mockCustomEvent) + + const { wrapper } = getWrapper({ selectedFiles: [{ id: 1 }] }) + + await wrapper.find(selectors.btnSelect).trigger('click') + + expect(window.parent.dispatchEvent).toHaveBeenCalledWith({ + name: 'owncloud-embed:select', + payload: { detail: [{ id: 1 }] } + }) + }) + }) + + describe('cancel action', () => { + it('should emit cancel event when the cancel action is triggered', async () => { + window.parent.dispatchEvent = jest.fn() + global.CustomEvent = jest.fn().mockImplementation(mockCustomEvent) + + const { wrapper } = getWrapper({ selectedFiles: [{ id: 1 }] }) + + await wrapper.find(selectors.btnCancel).trigger('click') + + expect(window.parent.dispatchEvent).toHaveBeenCalledWith({ + name: 'owncloud-embed:cancel', + payload: undefined + }) + }) + }) + + describe('share action', () => { + it('should disable share action when link creation is disabled', () => { + const { wrapper } = getWrapper({ selectedFiles: [{ id: 1 }] }) + + expect(wrapper.find(selectors.btnShare).attributes()).toHaveProperty('disabled') + }) + + it('should disable share action when no resources are selected', () => { + const { wrapper } = getWrapper() + + expect(wrapper.find(selectors.btnShare).attributes()).toHaveProperty('disabled') + }) + + it('should enable share action when at least one resource is selected and link creation is enabled', () => { + const { wrapper } = getWrapper({ + selectedFiles: [{ id: 1 }], + abilities: [{ action: 'create-all', subject: 'PublicLink' }] + }) + + expect(wrapper.find(selectors.btnShare).attributes()).not.toHaveProperty('disabled') + }) + + it('should emit share event when share action is triggered', async () => { + window.parent.dispatchEvent = jest.fn() + global.CustomEvent = jest.fn().mockImplementation(mockCustomEvent) + + const { wrapper } = getWrapper({ + selectedFiles: [{ id: 1 }], + abilities: [{ action: 'create-all', subject: 'PublicLink' }] + }) + + await wrapper.find(selectors.btnShare).trigger('click') + + expect(window.parent.dispatchEvent).toHaveBeenCalledWith({ + name: 'owncloud-embed:share', + payload: { detail: ['link-1'] } + }) + }) + + it('should ask for password first when required when share action is triggered', async () => { + window.parent.dispatchEvent = jest.fn() + global.CustomEvent = jest.fn().mockImplementation(mockCustomEvent) + + const { wrapper } = getWrapper({ + selectedFiles: [{ id: 1 }], + abilities: [{ action: 'create-all', subject: 'PublicLink' }], + capabilities: jest.fn().mockReturnValue({ + files_sharing: { public: { password: { enforced_for: { read_only: true } } } } + }) + }) + + await wrapper.find(selectors.btnShare).trigger('click') + + expect(window.parent.dispatchEvent).toHaveBeenCalledWith({ + name: 'owncloud-embed:share', + payload: { detail: ['password-link-1'] } + }) + }) + }) +}) + +function getWrapper( + { selectedFiles = [], abilities = [], capabilities = jest.fn().mockReturnValue({}) } = { + selectedFiles: [], + abilities: [], + capabilities: jest.fn().mockReturnValue({}) + } +) { + const storeOptions = { + ...defaultStoreMockOptions, + getters: { ...defaultStoreMockOptions.getters, capabilities }, + modules: { + ...defaultStoreMockOptions.modules, + Files: { + ...defaultStoreMockOptions.modules.Files, + getters: { + ...defaultStoreMockOptions.modules.Files.getters, + selectedFiles: jest.fn().mockReturnValue(selectedFiles) + } + } + } + } + + return { + wrapper: shallowMount(EmbedActions, { + global: { + stubs: { OcButton: false }, + plugins: [...defaultPlugins({ abilities }), createStore(storeOptions)] + } + }) + } +} + +function mockCustomEvent(name, payload) { + return { name, payload } +} From 759443517989521cc5ce23d58ee2a51f4a501c21 Mon Sep 17 00:00:00 2001 From: Lukas Hirt Date: Mon, 23 Oct 2023 11:43:31 +0200 Subject: [PATCH 2/5] test: update snapshots --- .../tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap | 1 + .../tests/unit/views/trash/__snapshots__/Overview.spec.ts.snap | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap index b31f621654f..16884651ef3 100644 --- a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap +++ b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap @@ -20,6 +20,7 @@ exports[`Projects view different files view states lists all available project s + `; diff --git a/packages/web-app-files/tests/unit/views/trash/__snapshots__/Overview.spec.ts.snap b/packages/web-app-files/tests/unit/views/trash/__snapshots__/Overview.spec.ts.snap index e3aa48b67b2..e509c05723e 100644 --- a/packages/web-app-files/tests/unit/views/trash/__snapshots__/Overview.spec.ts.snap +++ b/packages/web-app-files/tests/unit/views/trash/__snapshots__/Overview.spec.ts.snap @@ -81,5 +81,6 @@ exports[`TrashOverview view states should render spaces list 1`] = ` + `; From 86593848043a0c04f827c031b2e73b9d13884eed Mon Sep 17 00:00:00 2001 From: Lukas Hirt Date: Tue, 24 Oct 2023 10:27:42 +0200 Subject: [PATCH 3/5] test: stub out portal --- .../web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts b/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts index 8b27e6c5c8c..f151778ae41 100644 --- a/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts +++ b/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts @@ -264,7 +264,7 @@ function getMountedWrapper({ plugins: [...defaultPlugins(), store], mocks: defaultMocks, provide: defaultMocks, - stubs: { ...defaultStubs, 'resource-details': true } + stubs: { ...defaultStubs, 'resource-details': true, portal: true } } }) } From 46db7cee85e375fd334f272f0ef0cdc322bbc7c4 Mon Sep 17 00:00:00 2001 From: Lukas Hirt Date: Tue, 24 Oct 2023 10:34:38 +0200 Subject: [PATCH 4/5] refactor: use setup function for consistency --- .../components/EmbedActions/EmbedActions.vue | 157 ++++++++++-------- 1 file changed, 85 insertions(+), 72 deletions(-) diff --git a/packages/web-app-files/src/components/EmbedActions/EmbedActions.vue b/packages/web-app-files/src/components/EmbedActions/EmbedActions.vue index 502e69b43d5..556f925a5c2 100644 --- a/packages/web-app-files/src/components/EmbedActions/EmbedActions.vue +++ b/packages/web-app-files/src/components/EmbedActions/EmbedActions.vue @@ -22,7 +22,7 @@ - From 3580eeb7a7717eb639b5e161003f275666d1cd2a Mon Sep 17 00:00:00 2001 From: Lukas Hirt Date: Tue, 24 Oct 2023 12:54:58 +0200 Subject: [PATCH 5/5] test: stub portal also in generictrash tests --- .../web-app-files/tests/unit/views/spaces/GenericTrash.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-app-files/tests/unit/views/spaces/GenericTrash.spec.ts b/packages/web-app-files/tests/unit/views/spaces/GenericTrash.spec.ts index ff665488e34..d4d0e7be383 100644 --- a/packages/web-app-files/tests/unit/views/spaces/GenericTrash.spec.ts +++ b/packages/web-app-files/tests/unit/views/spaces/GenericTrash.spec.ts @@ -84,7 +84,7 @@ function getMountedWrapper({ mocks = {}, props = {}, files = [], loading = false global: { plugins: [...defaultPlugins(), store], mocks: defaultMocks, - stubs: defaultStubs + stubs: { ...defaultStubs, portal: true } } }) }