Skip to content
758 changes: 758 additions & 0 deletions dev/embed-demo.html

Large diffs are not rendered by default.

92 changes: 75 additions & 17 deletions packages/web-app-files/src/components/EmbedActions/EmbedActions.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<section class="files-embed-actions oc-width-1-1 oc-flex oc-flex-middle oc-flex-between oc-my-s">
<section v-if="!isInlineAttach" class="files-embed-actions oc-width-1-1 oc-flex oc-flex-middle oc-flex-between oc-my-s">
<oc-text-input
v-if="chooseFileName"
v-model="fileName"
Expand Down Expand Up @@ -36,26 +36,29 @@
data-testid="button-select"
variation="inverse"
appearance="filled"
:disabled="areSelectActionsDisabled"
:disabled="areSelectActionsDisabled || signing"
@click="emitSelect"
>{{ selectLabel }}
>{{ signing ? $gettext('Preparing…') : selectLabel }}
</oc-button>
</div>
</section>
</template>

<script lang="ts">
import { computed, defineComponent, ref, unref } from 'vue'
import { computed, defineComponent, onMounted, onUnmounted, ref, unref } from 'vue'
import {
embedModeLocationPickMessageData,
FileAction,
routeToContextQuery,
useAbility,
useCapabilityStore,
useClientService,
useEmbedMode,
useFileActionsCreateLink,
useResourcesStore,
useRouter,
useSpacesStore
useSpacesStore,
useUserStore
} from '@ownclouders/web-pkg'
import { Resource } from '@ownclouders/web-client'
import { useGettext } from 'vue3-gettext'
Expand All @@ -68,6 +71,8 @@ export default defineComponent({
const {
isLocationPicker,
isFilePicker,
isInlineAttach,
messagesTargetOrigin,
postMessage,
chooseFileName,
chooseFileNameSuggestion
Expand All @@ -84,9 +89,18 @@ export default defineComponent({
return [unref(currentFolder)]
}

return unref(selectedResources)
const resources = unref(selectedResources)
if (isInlineAttach.value) {
return resources.filter((r) => !r.isFolder)
}
return resources
})

const capabilityStore = useCapabilityStore()
const clientService = useClientService()
const userStore = useUserStore()
const signing = ref(false)

const { actions: createLinkActions } = useFileActionsCreateLink({ enforceModal: true })
const createLinkAction = computed<FileAction>(() => unref(createLinkActions)[0])

Expand All @@ -102,26 +116,68 @@ export default defineComponent({
return [0, unref(fileName).split('.')[0].length] as [number, number]
})

const emitSelect = (): void => {
if (unref(chooseFileName)) {
postMessage<embedModeLocationPickMessageData>('owncloud-embed:select', {
resources: JSON.parse(JSON.stringify(selectedFiles.value)),
fileName: unref(fileName),
locationQuery: JSON.parse(JSON.stringify(routeToContextQuery(unref(router.currentRoute))))
})
const withSignedUrls = async (resources: Resource[]): Promise<Resource[]> => {
if (!capabilityStore.supportUrlSigning || !unref(space)) {
return resources
}

// TODO: adjust type to embedModeLocationPickMessageData later (breaking)
postMessage<Resource[]>(
'owncloud-embed:select',
JSON.parse(JSON.stringify(selectedFiles.value))
return Promise.all(
resources.map(async (resource) => {
if (resource.type === 'folder') {
return resource
}
try {
const signedUrl = await clientService.webdav.getFileUrl(unref(space), resource, {
isUrlSigningEnabled: true,
username: userStore.user?.onPremisesSamAccountName
})
return { ...resource, downloadURL: signedUrl }
} catch {
return resource
}
})
)
}

const emitSelect = async (): Promise<void> => {
signing.value = true
try {
const resources = await withSignedUrls(JSON.parse(JSON.stringify(selectedFiles.value)))

if (unref(chooseFileName)) {
postMessage<embedModeLocationPickMessageData>('owncloud-embed:select', {
resources,
fileName: unref(fileName),
locationQuery: JSON.parse(
JSON.stringify(routeToContextQuery(unref(router.currentRoute)))
)
})
} else {
// TODO: adjust type to embedModeLocationPickMessageData later (breaking)
postMessage<Resource[]>('owncloud-embed:select', resources)
}
} finally {
signing.value = false
}
}

const emitCancel = (): void => {
postMessage<null>('owncloud-embed:cancel', null)
}

const handleRequestSelection = (event: MessageEvent): void => {
if (unref(messagesTargetOrigin) && event.origin !== unref(messagesTargetOrigin)) {
return
}
if (event.data?.name !== 'owncloud-embed:request-selection') {
return
}
emitSelect()
}

onMounted(() => window.addEventListener('message', handleRequestSelection))
onUnmounted(() => window.removeEventListener('message', handleRequestSelection))

return {
chooseFileName,
chooseFileNameSuggestion,
Expand All @@ -130,9 +186,11 @@ export default defineComponent({
canCreatePublicLinks,
isLocationPicker,
isFilePicker,
isInlineAttach,
selectLabel,
emitCancel,
emitSelect,
signing,
space,
createLinkAction,
fileName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FileAction, useEmbedMode, useFileActionsCreateLink } from '@ownclouders
import { mock } from 'vitest-mock-extended'
import { ref } from 'vue'
import { Resource } from '@ownclouders/web-client'
import { flushPromises } from '@vue/test-utils'

vi.mock('@ownclouders/web-pkg', async (importOriginal) => ({
...(await importOriginal<any>()),
Expand Down Expand Up @@ -46,8 +47,10 @@ describe('EmbedActions', () => {
const { wrapper, mocks } = getWrapper({ selectedIds: ['1'] })

await wrapper.find(selectors.btnSelect).trigger('click')
await flushPromises()

expect(mocks.postMessageMock).toHaveBeenCalledWith('owncloud-embed:select', [{ id: '1' }])
expect(mocks.postMessageMock).toHaveBeenCalledTimes(1)
})

it('should enable select action when embedTarget is set to location', () => {
Expand All @@ -63,8 +66,10 @@ describe('EmbedActions', () => {
})

await wrapper.find(selectors.btnSelect).trigger('click')
await flushPromises()

expect(mocks.postMessageMock).toHaveBeenCalledWith('owncloud-embed:select', [{ id: '1' }])
expect(mocks.postMessageMock).toHaveBeenCalledTimes(1)
})
it('should display the file name input when chooseFileName is configured', () => {
const { wrapper } = getWrapper({
Expand All @@ -91,6 +96,7 @@ describe('EmbedActions', () => {
})

await wrapper.find(selectors.btnSelect).trigger('click')
await flushPromises()

expect(mocks.postMessageMock).toHaveBeenCalledWith('owncloud-embed:select', {
fileName: 'file.txt',
Expand All @@ -100,6 +106,7 @@ describe('EmbedActions', () => {
contextRouteQuery: {}
}
})
expect(mocks.postMessageMock).toHaveBeenCalledTimes(1)
})
})

Expand Down Expand Up @@ -177,6 +184,7 @@ function getWrapper(
mock<ReturnType<typeof useEmbedMode>>({
isLocationPicker: ref(isLocationPicker),
isFilePicker: ref(isFilePicker),
isInlineAttach: ref(false),
chooseFileName: ref(chooseFileName),
chooseFileNameSuggestion: ref('file.txt'),
postMessage: postMessageMock
Expand Down
19 changes: 18 additions & 1 deletion packages/web-pkg/src/components/CreateLinkModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
isFolder
})
"
>{{ $gettext('Copy link and password') }}
>{{ confirmPasswordButtonText }}
</oc-button>
</li>
</oc-list>
Expand Down Expand Up @@ -193,6 +193,13 @@ export default defineComponent({
return $gettext('Copy link')
})

const confirmPasswordButtonText = computed(() => {
if (unref(isEmbedEnabled)) {
return $gettext('Share link(s) and password(s)')
}
return $gettext('Copy link and password')
})

const passwordInputKey = ref(uuidV4())
const roleRefs = ref<Record<string, RoleRef>>({})

Expand Down Expand Up @@ -265,11 +272,20 @@ export default defineComponent({
const result = await createLinks()

const succeeded = result.filter(({ status }) => status === 'fulfilled')
// **DEPRECATED**: Always emit the share url for backwards compatibility
if (succeeded.length && unref(isEmbedEnabled)) {
postMessage<string[]>(
'owncloud-embed:share',
(succeeded as PromiseFulfilledResult<LinkShare>[]).map(({ value }) => value.webUrl)
)
// Always emit new event with objects, include password only when copyPassword is enabled
postMessage<Array<{ url: string; password?: string }>>(
'owncloud-embed:share-links',
(succeeded as PromiseFulfilledResult<LinkShare>[]).map(({ value }) => ({
url: value.webUrl,
...(options.copyPassword && { password: password.value })
}))
)
}

const userFacingErrors: Error[] = []
Expand Down Expand Up @@ -350,6 +366,7 @@ export default defineComponent({
updatePassword,
getLinkRoleByType,
confirmButtonText,
confirmPasswordButtonText,
isAdvancedMode,
setAdvancedMode,
onExpiryDateChanged,
Expand Down
41 changes: 36 additions & 5 deletions packages/web-pkg/src/components/FilesList/ResourceTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
:label="getResourceCheckboxLabel(item)"
:label-hidden="true"
size="large"
:disabled="isResourceDisabled(item)"
:disabled="isResourceDisabled(item) || (isInlineAttach && item.isFolder)"
:model-value="isResourceSelected(item)"
:outline="isLatestSelectedItem(item)"
@click.stop="toggleSelection(item.id)"
Expand Down Expand Up @@ -222,7 +222,10 @@
</oc-button>
</template>
<template #actions="{ item }">
<div v-if="!isResourceDisabled(item)" class="resource-table-actions">
<div
v-if="!isResourceDisabled(item) && !(isEmbedModeEnabled && isFilePicker)"
class="resource-table-actions"
>
<!-- @slot Add quick actions before the `context-menu / three dot` button in the actions column -->
<slot name="quickActions" :resource="item" />
<context-menu-quick-action
Expand Down Expand Up @@ -276,6 +279,7 @@ import {
useGetMatchingSpace,
useFolderLink,
useEmbedMode,
useEmbedModeDownloadUrl,
useAuthStore,
useCapabilityStore,
useConfigStore,
Expand Down Expand Up @@ -547,10 +551,12 @@ export default defineComponent({
const {
isLocationPicker,
isFilePicker,
isInlineAttach,
postMessage,
isEnabled: isEmbedModeEnabled,
fileTypes: embedModeFileTypes
} = useEmbedMode()
const { withDownloadUrl } = useEmbedModeDownloadUrl()
const { getDefaultAction } = useFileActions()
const configStore = useConfigStore()
const { options: configOptions } = storeToRefs(configStore)
Expand Down Expand Up @@ -682,7 +688,9 @@ export default defineComponent({
),
...folderLinkUtils,
postMessage,
withDownloadUrl,
isFilePicker,
isInlineAttach,
isLocationPicker,
isEmbedModeEnabled,
emitSelect,
Expand Down Expand Up @@ -868,6 +876,13 @@ export default defineComponent({
return false
}

// clicking the shared-with avatars would also trigger the row's own @highlight
// handler (the file-pick postMessage), same reason context menu/rename are
// hidden in this mode - see isEmbedModeEnabled/isFilePicker usages below
if (field.name === 'sharedWith' && this.isEmbedModeEnabled && this.isFilePicker) {
return false
}

let hasField: boolean
if (field.prop) {
hasField = get(firstResource, field.prop) !== undefined
Expand Down Expand Up @@ -960,6 +975,9 @@ export default defineComponent({
return item.id === this.latestSelectedId
},
hasRenameAction(item: Resource) {
if (this.isEmbedModeEnabled && this.isFilePicker) {
return false
}
if (isProjectSpaceResource(item)) {
return this.renameActionsSpace.filter((menuItem) =>
menuItem.isVisible({ resources: [item] })
Expand Down Expand Up @@ -1089,7 +1107,7 @@ export default defineComponent({
*/
this.$emit('rowMounted', resource, component, this.constants.ImageDimension.Thumbnail)
},
fileClicked(data: [Resource, MouseEvent, boolean]) {
async fileClicked(data: [Resource, MouseEvent, boolean]) {
/**
* Triggered when the file row is clicked
* @property {object} resource The resource for which the event is triggered
Expand All @@ -1101,8 +1119,10 @@ export default defineComponent({
}

if (this.isEmbedModeEnabled && this.isFilePicker && !resource.isFolder) {
const clonedResource = JSON.parse(JSON.stringify(resource))
const resourceWithUrl = await this.withDownloadUrl(this.space, clonedResource)
return this.postMessage<embedModeFilePickMessageData>('owncloud-embed:file-pick', {
resource: JSON.parse(JSON.stringify(resource)),
resource: resourceWithUrl,
locationQuery: JSON.parse(
JSON.stringify(routeToContextQuery(unref(this.router.currentRoute)))
)
Expand Down Expand Up @@ -1142,11 +1162,22 @@ export default defineComponent({
}
this.emitSelect(
this.resources
.filter((resource) => !this.disabledResources.includes(resource.id))
.filter(
(resource) =>
!this.disabledResources.includes(resource.id) &&
!(this.isInlineAttach && resource.isFolder)
)
.map((resource) => resource.id)
)
},
emitFileClick(resource: Resource) {
// in single-file embed mode, the row's own @highlight handler (fileClicked) already
// handles the click (file-pick postMessage) - the default-action dispatch this
// triggers (e.g. falling back to download-file) is unwanted and would run alongside it
if (this.isEmbedModeEnabled && this.isFilePicker) {
return
}

const space = this.getMatchingSpace(resource)

/**
Expand Down
Loading