From 1563b71e4fc936135ebfb25c33c32070b9c0bf50 Mon Sep 17 00:00:00 2001 From: Lukas Hirt Date: Wed, 24 Jan 2024 17:09:30 +0100 Subject: [PATCH] fix: handle quick link copy action in resources list on safari --- .../src/components/CreateLinkModal.vue | 20 +++-- .../files/useFileActionsCopyQuicklink.ts | 17 ++-- .../src/composables/clipboard/useClipboard.ts | 79 ++++++++++++++----- 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/packages/web-pkg/src/components/CreateLinkModal.vue b/packages/web-pkg/src/components/CreateLinkModal.vue index 2ee0922e1cd..ad5eae8f15b 100644 --- a/packages/web-pkg/src/components/CreateLinkModal.vue +++ b/packages/web-pkg/src/components/CreateLinkModal.vue @@ -182,7 +182,7 @@ export default defineComponent({ space: { type: Object as PropType, default: undefined }, isQuickLink: { type: Boolean, default: false }, callbackFn: { - type: Function as PropType<(result: PromiseSettledResult[]) => Promise | void>, + type: Function as PropType<(promise: Promise) => Promise | void>, default: undefined } }, @@ -278,7 +278,7 @@ export default defineComponent({ ) } - const onConfirm = async () => { + const handleLinksCreation = async (): Promise => { if (!unref(selectedRoleIsInternal)) { if (unref(passwordEnforced) && !unref(password).value) { password.error = $gettext('Password must not be empty') @@ -292,11 +292,13 @@ export default defineComponent({ const result = await createLinks() - const succeeded = result.filter(({ status }) => status === 'fulfilled') + const succeeded = result.filter( + ({ status }) => status === 'fulfilled' + ) as PromiseFulfilledResult[] if (succeeded.length && unref(isEmbedEnabled)) { postMessage( 'owncloud-embed:share', - (succeeded as PromiseFulfilledResult[]).map(({ value }) => value.url) + succeeded.map(({ value }) => value.url) ) } @@ -319,9 +321,17 @@ export default defineComponent({ return Promise.reject() } + Promise.resolve(succeeded[0]?.value.url ?? null) + } + + const onConfirm = async () => { if (props.callbackFn) { - props.callbackFn(result) + props.callbackFn(handleLinksCreation) + + return } + + await handleLinksCreation() } expose({ onConfirm }) diff --git a/packages/web-pkg/src/composables/actions/files/useFileActionsCopyQuicklink.ts b/packages/web-pkg/src/composables/actions/files/useFileActionsCopyQuicklink.ts index 9e2c48c79fc..dd409839b3d 100644 --- a/packages/web-pkg/src/composables/actions/files/useFileActionsCopyQuicklink.ts +++ b/packages/web-pkg/src/composables/actions/files/useFileActionsCopyQuicklink.ts @@ -43,7 +43,7 @@ export const useFileActionsCopyQuickLink = () => { unref(createLinkActions).find(({ name }) => name === 'create-quick-links') ) - const copyQuickLinkToClipboard = async (url: string) => { + const copyQuickLinkToClipboard = async (url: string | (() => Promise)) => { try { await copyToClipboard(url) showMessage({ title: $gettext('The link has been copied to your clipboard.') }) @@ -63,17 +63,18 @@ export const useFileActionsCopyQuickLink = () => { include_tags: false }) - return linkSharesForResource - .map((share: any) => buildShare(share.shareInfo, null, null)) - .find((share: Share) => share.quicklink === true) + return ( + linkSharesForResource + .map((share: any) => buildShare(share.shareInfo, null, null)) + .find((share: Share) => share.quicklink === true)?.url || null + ) } - const handler = async ({ space, resources }: FileActionOptions) => { + const handler = ({ space, resources }: FileActionOptions) => { const [resource] = resources - const existingQuickLink = await getExistingQuickLink(resource) - if (existingQuickLink) { - return copyQuickLinkToClipboard(existingQuickLink.url) + if (ShareTypes.containsAnyValue(ShareTypes.unauthenticated, resource.shareTypes ?? [])) { + return copyQuickLinkToClipboard(getExistingQuickLink.bind(this, resource)) } return unref(createQuicklinkAction).handler({ space, resources }) diff --git a/packages/web-pkg/src/composables/clipboard/useClipboard.ts b/packages/web-pkg/src/composables/clipboard/useClipboard.ts index c46611902b5..7fc538bd739 100644 --- a/packages/web-pkg/src/composables/clipboard/useClipboard.ts +++ b/packages/web-pkg/src/composables/clipboard/useClipboard.ts @@ -1,27 +1,66 @@ import { useClipboard as _useClipboard } from '@vueuse/core' export const useClipboard = () => { - // doCopy creates the requested link and copies the url to the clipboard, - // the copy action uses the clipboard // clipboardItem api to work around the webkit limitations. - // - // https://developer.apple.com/forums/thread/691873 - // - // if those apis not available (or like in firefox behind dom.events.asyncClipboard.clipboardItem) - // it has a fallback to the vue-use implementation. - // - // https://webkit.org/blog/10855/ - const copyToClipboard = (quickLinkUrl: string) => { - if (typeof ClipboardItem && navigator?.clipboard?.write) { - return navigator.clipboard.write([ - new ClipboardItem({ - 'text/plain': new Blob([quickLinkUrl], { type: 'text/plain' }) - }) - ]) - } else { - const { copy } = _useClipboard({ legacy: true }) - return copy(quickLinkUrl) - } + /** + * Copies given text into clipboard + * + * @remarks the copy action uses the clipboard // clipboardItem api to work around the webkit limitations.(https://developer.apple.com/forums/thread/691873) if those apis not available (or like in firefox behind dom.events.asyncClipboard.clipboardItem). it has a fallback to the vue-use implementation. (https://webkit.org/blog/10855/) + * + * @param quickLinkUrl If the text is supposed to be resolved via a promise, pass the promise directly and resolve it to string + * @returns resolves whether the text has been copied into clipboard (e.g. whether the quickLinkUrl function resolved into a string or not) + */ + const copyToClipboard = ( + quickLinkUrl: string | (() => Promise) + ): Promise => { + return new Promise(async (resolve) => { + try { + if (typeof ClipboardItem && navigator?.clipboard?.write) { + const blob = + typeof quickLinkUrl === 'function' + ? quickLinkUrl().then((text) => { + if (!text) { + throw new EmptyTextError('No text received') + } + + return new Blob([text], { type: 'text/plain' }) + }) + : new Blob([quickLinkUrl], { type: 'text/plain' }) + + await navigator.clipboard.write([ + new ClipboardItem({ + 'text/plain': blob + }) + ]) + + resolve(true) + return + } + + const { copy } = _useClipboard({ legacy: true }) + const text = typeof quickLinkUrl === 'function' ? await quickLinkUrl() : quickLinkUrl + + await copy(text) + resolve(true) + } catch (error) { + if (error instanceof EmptyTextError) { + resolve(false) + + return + } + + // A real error happened + throw error + } + }) } return { copyToClipboard } } + +class EmptyTextError extends Error { + constructor(msg) { + super(msg) + + this.name = 'ClipboardEmptyTextError' + } +}