From 0bf64d88ec5dad548066d0c98b5c4b4fd4f6d801 Mon Sep 17 00:00:00 2001 From: Pascal Wengerter Date: Tue, 27 Dec 2022 01:09:55 +0100 Subject: [PATCH] Feature/8134 - copy to clipboard --- .../unreleased/enhancement-clipboard-copy | 8 +++ packages/web-app-files/package.json | 1 - .../SideBar/Details/FileDetails.vue | 70 ++++++++++--------- .../components/SideBar/PrivateLinkItem.vue | 59 ++++++++-------- .../SideBar/Shares/Links/NameAndCopy.vue | 63 ++++++++--------- .../web-app-files/src/helpers/share/link.ts | 5 +- .../SideBar/PrivateLinkItem.spec.ts | 31 ++++---- .../SideBar/Shares/Links/NameAndCopy.spec.ts | 11 +-- packages/web-runtime/src/defaults/vue.js | 5 -- pnpm-lock.yaml | 12 ---- 10 files changed, 136 insertions(+), 129 deletions(-) create mode 100644 changelog/unreleased/enhancement-clipboard-copy diff --git a/changelog/unreleased/enhancement-clipboard-copy b/changelog/unreleased/enhancement-clipboard-copy new file mode 100644 index 00000000000..a4687ed2752 --- /dev/null +++ b/changelog/unreleased/enhancement-clipboard-copy @@ -0,0 +1,8 @@ +Enhancement: Make clipboard copy available to more browsers + +We have added more functionality for copying (e.g. links) to the user's clipboard. +By switching libraries we now use the standard browser API (if available) with a +fallback and only offer copy-to-clipboard buttons if the browser supports it. + +https://github.com/owncloud/web/pull/8136 +https://github.com/owncloud/web/issues/8134 diff --git a/packages/web-app-files/package.json b/packages/web-app-files/package.json index b52eb8965f5..adab7e78705 100644 --- a/packages/web-app-files/package.json +++ b/packages/web-app-files/package.json @@ -5,7 +5,6 @@ "description": "ownCloud web files", "license": "AGPL-3.0", "dependencies": { - "copy-to-clipboard": "^3.3.1", "mark.js": "^8.11.1" }, "devDependencies": { diff --git a/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue b/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue index 40cc97a04bf..904e1fb6c09 100644 --- a/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue +++ b/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue @@ -95,6 +95,7 @@ v-text="file.path" /> diff --git a/packages/web-app-files/src/components/SideBar/Shares/Links/NameAndCopy.vue b/packages/web-app-files/src/components/SideBar/Shares/Links/NameAndCopy.vue index e937731e7ed..e20ac4494d7 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Links/NameAndCopy.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Links/NameAndCopy.vue @@ -15,6 +15,7 @@ /> - diff --git a/packages/web-app-files/src/helpers/share/link.ts b/packages/web-app-files/src/helpers/share/link.ts index dbfc82c7ee4..74be344658e 100644 --- a/packages/web-app-files/src/helpers/share/link.ts +++ b/packages/web-app-files/src/helpers/share/link.ts @@ -2,7 +2,7 @@ import { DateTime } from 'luxon' import { Share } from 'web-client/src/helpers/share' import { Store } from 'vuex' import { clientService } from 'web-pkg/src/services' -import copyToClipboard from 'copy-to-clipboard' +import { useClipboard } from '@vueuse/core' interface CreateQuicklink { store: Store @@ -45,7 +45,8 @@ export const createQuicklink = async (args: CreateQuicklink): Promise => storageId: resource.fileId || resource.id }) - copyToClipboard(link.url) + const { copy } = useClipboard({ legacy: true }) + copy(link.url) await store.dispatch('showMessage', { title: $gettext('The quicklink has been copied to your clipboard.') diff --git a/packages/web-app-files/tests/unit/components/SideBar/PrivateLinkItem.spec.ts b/packages/web-app-files/tests/unit/components/SideBar/PrivateLinkItem.spec.ts index d62c72a3b23..606d2286f2f 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/PrivateLinkItem.spec.ts +++ b/packages/web-app-files/tests/unit/components/SideBar/PrivateLinkItem.spec.ts @@ -1,23 +1,39 @@ -import PrivateLinkItem from 'web-app-files/src/components/SideBar/PrivateLinkItem.vue' -import { mockDeep } from 'jest-mock-extended' +import { mock } from 'jest-mock-extended' import { Resource } from 'web-client' import { createStore, defaultPlugins, mount, defaultStoreMockOptions } from 'web-test-helpers' +import PrivateLinkItem from 'web-app-files/src/components/SideBar/PrivateLinkItem.vue' jest.useFakeTimers() +const folder = mock({ + type: 'folder', + ownerId: 'marie', + ownerDisplayName: 'Marie', + mdate: 'Wed, 21 Oct 2015 07:28:00 GMT', + size: '740', + name: 'lorem.txt', + privateLink: 'https://example.com/fake-private-link' +}) + describe('PrivateLinkItem', () => { it('should render a button', () => { const { wrapper } = getWrapper() expect(wrapper.html()).toMatchSnapshot() }) it('upon clicking it should copy the private link to the clipboard button, render a success message and change icon for half a second', async () => { - jest.spyOn(window, 'prompt').mockImplementation() + Object.assign(window.navigator, { + clipboard: { + writeText: jest.fn().mockImplementation(() => Promise.resolve()) + } + }) + const { wrapper } = getWrapper() const spyShowMessage = jest.spyOn(wrapper.vm, 'showMessage') expect(spyShowMessage).not.toHaveBeenCalled() await wrapper.trigger('click') expect(wrapper.html()).toMatchSnapshot() + expect(window.navigator.clipboard.writeText).toHaveBeenCalledWith(folder.privateLink) expect(spyShowMessage).toHaveBeenCalledTimes(1) jest.advanceTimersByTime(550) @@ -29,15 +45,6 @@ describe('PrivateLinkItem', () => { }) function getWrapper() { - const folder = mockDeep({ - type: 'folder', - ownerId: 'marie', - ownerDisplayName: 'Marie', - mdate: 'Wed, 21 Oct 2015 07:28:00 GMT', - size: '740', - name: 'lorem.txt', - privateLink: 'https://example.com/fake-private-link' - }) const storeOptions = { ...defaultStoreMockOptions } storeOptions.getters.capabilities.mockImplementation(() => ({ files: { privateLinks: true } })) storeOptions.modules.Files.getters.highlightedFile.mockImplementation(() => folder) diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/NameAndCopy.spec.ts b/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/NameAndCopy.spec.ts index 384f4ef0cfd..2ac38ef16e2 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/NameAndCopy.spec.ts +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/Links/NameAndCopy.spec.ts @@ -16,17 +16,20 @@ describe('NameAndCopy', () => { expect(wrapper.html()).toMatchSnapshot() }) it('upon clicking it should copy the private link to the clipboard button, render a success message and change icon for half a second', async () => { - const windowSpy = jest.spyOn(window, 'prompt').mockImplementation() + Object.assign(window.navigator, { + clipboard: { + writeText: jest.fn().mockImplementation(() => Promise.resolve()) + } + }) + const { wrapper } = getWrapper() const spyShowMessage = jest.spyOn(wrapper.vm, 'showMessage') expect(spyShowMessage).not.toHaveBeenCalled() - expect(windowSpy).not.toHaveBeenCalled() await wrapper.find('.oc-files-public-link-copy-url').trigger('click') + expect(window.navigator.clipboard.writeText).toHaveBeenCalledWith(exampleLink.url) expect(wrapper.html()).toMatchSnapshot() expect(spyShowMessage).toHaveBeenCalledTimes(1) - expect(windowSpy).toHaveBeenCalledTimes(1) - expect(windowSpy).toHaveBeenCalledWith('Copy to clipboard: Ctrl+C, Enter', exampleLink.url) jest.advanceTimersByTime(550) diff --git a/packages/web-runtime/src/defaults/vue.js b/packages/web-runtime/src/defaults/vue.js index 2f12418b766..41aa4c58d49 100644 --- a/packages/web-runtime/src/defaults/vue.js +++ b/packages/web-runtime/src/defaults/vue.js @@ -1,12 +1,9 @@ -import 'vue-resize/dist/vue-resize.css' import Vue from 'vue' import WebPlugin from '../plugins/web' import Avatar from '../components/Avatar.vue' import focusMixin from '../mixins/focusMixin' import lifecycleMixin from '../mixins/lifecycleMixin' -import VueEvents from 'vue-events' import VueScrollTo from 'vue-scrollto' -import VueResize from 'vue-resize' import VueMeta from 'vue-meta' import PortalVue from 'portal-vue' import AsyncComputed from 'vue-async-computed' @@ -15,10 +12,8 @@ import Vuex from 'vuex' Vue.use(Vuex) Vue.use(VueRouter) -Vue.use(VueEvents) Vue.use(VueScrollTo) Vue.use(WebPlugin) -Vue.use(VueResize) Vue.use(VueMeta, { refreshOnceOnNavigation: true }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22866f35661..de5a9e35a80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -424,7 +424,6 @@ importers: specifiers: '@jest/globals': 29.3.1 '@vueuse/core': ^9.8.2 - copy-to-clipboard: ^3.3.1 filesize: ^9.0.11 fuse.js: ^6.5.3 lodash-es: 4.17.21 @@ -449,7 +448,6 @@ importers: web-pkg: npm:@ownclouders/web-pkg web-runtime: workspace:* dependencies: - copy-to-clipboard: 3.3.1 filesize: 9.0.11 fuse.js: 6.5.3 lodash-es: 4.17.21 @@ -9763,12 +9761,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /copy-to-clipboard/3.3.1: - resolution: {integrity: sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==} - dependencies: - toggle-selection: 1.0.6 - dev: false - /copy-webpack-plugin/5.1.2_webpack@4.46.0: resolution: {integrity: sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ==} engines: {node: '>= 6.9.0'} @@ -21504,10 +21496,6 @@ packages: safe-regex: 1.1.0 dev: true - /toggle-selection/1.0.6: - resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} - dev: false - /toidentifier/1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'}