-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add actions into the embed mode
- Loading branch information
Showing
4 changed files
with
353 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<iframe src="<web-url>?mode=embed"></iframe> | ||
``` | ||
|
||
## 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 | |
142 changes: 142 additions & 0 deletions
142
packages/web-app-files/src/components/EmbedActions/EmbedActions.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
<template> | ||
<section class="files-embed-actions"> | ||
<oc-button data-testid="button-cancel" appearance="raw-inverse" @click="emitCancel">{{ | ||
$gettext('Cancel') | ||
}}</oc-button> | ||
<oc-button | ||
data-testid="button-share" | ||
variation="inverse" | ||
appearance="filled" | ||
:disabled="areSelectActionsDisabled || !canCreatePublicLinks" | ||
@click="sharePublicLinks" | ||
>{{ $gettext('Share links') }}</oc-button | ||
> | ||
<oc-button | ||
data-testid="button-select" | ||
variation="inverse" | ||
appearance="filled" | ||
:disabled="areSelectActionsDisabled" | ||
@click="emitSelect" | ||
>{{ $gettext('Attach as copy') }}</oc-button | ||
> | ||
</section> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { computed } from 'vue' | ||
import { | ||
createQuicklink, | ||
showQuickLinkPasswordModal, | ||
useAbility, | ||
useClientService, | ||
usePasswordPolicyService, | ||
useStore | ||
} from '@ownclouders/web-pkg' | ||
import { Resource } from '@ownclouders/web-client' | ||
import { useGettext } from 'vue3-gettext' | ||
const store = useStore() | ||
const ability = useAbility() | ||
const clientService = useClientService() | ||
const passwordPolicyService = usePasswordPolicyService() | ||
const language = useGettext() | ||
const selectedFiles = computed<Resource[]>(() => { | ||
return store.getters['Files/selectedFiles'] | ||
}) | ||
const areSelectActionsDisabled = computed<boolean>(() => selectedFiles.value.length < 1) | ||
const canCreatePublicLinks = computed<boolean>(() => ability.can('create-all', 'PublicLink')) | ||
const emitSelect = (): void => { | ||
const event: CustomEvent<Resource[]> = new CustomEvent('owncloud-embed:select', { | ||
detail: selectedFiles.value | ||
}) | ||
window.parent.dispatchEvent(event) | ||
} | ||
const emitCancel = (): void => { | ||
const event: CustomEvent<void> = new CustomEvent('owncloud-embed:cancel') | ||
window.parent.dispatchEvent(event) | ||
} | ||
const emitShare = (links: string[]): void => { | ||
if (!canCreatePublicLinks.value) return | ||
const event: CustomEvent<string[]> = new CustomEvent('owncloud-embed:share', { | ||
detail: links | ||
}) | ||
window.parent.dispatchEvent(event) | ||
} | ||
const sharePublicLinks = async (): Promise<string[]> => { | ||
if (!canCreatePublicLinks.value) return | ||
try { | ||
const passwordEnforced: boolean = | ||
store.getters.capabilities?.files_sharing?.public?.password?.enforced_for?.read_only === true | ||
if (passwordEnforced) { | ||
showQuickLinkPasswordModal( | ||
{ store, $gettext: language.$gettext, passwordPolicyService }, | ||
async (password) => { | ||
const links: string[] = await Promise.all( | ||
selectedFiles.value.map( | ||
async (resource) => | ||
( | ||
await createQuicklink({ | ||
ability, | ||
resource, | ||
clientService, | ||
language, | ||
store, | ||
password | ||
}) | ||
).url | ||
) | ||
) | ||
emitShare(links) | ||
} | ||
) | ||
return | ||
} | ||
const links: string[] = await Promise.all( | ||
selectedFiles.value.map( | ||
async (resource) => | ||
( | ||
await createQuicklink({ ability, resource, clientService, language, store }) | ||
).url | ||
) | ||
) | ||
emitShare(links) | ||
} catch (error) { | ||
console.error(error) | ||
store.dispatch('showErrorMessage', { | ||
title: language.$gettext('Sharing links failed...'), | ||
error | ||
}) | ||
} | ||
} | ||
</script> | ||
|
||
<style scoped> | ||
.files-embed-actions { | ||
align-items: center; | ||
box-sizing: border-box; | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: var(--oc-space-medium); | ||
justify-content: flex-end; | ||
padding: var(--oc-space-medium) 0; | ||
padding-right: var(--oc-space-small); | ||
width: 100%; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
packages/web-app-files/tests/unit/components/EmbedActions/EmbedActions.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 } | ||
} |