Skip to content

Commit

Permalink
Feature/8134 - copy to clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalwengerter committed Dec 27, 2022
1 parent c2ec30b commit d367bd9
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 97 deletions.
8 changes: 8 additions & 0 deletions changelog/unreleased/enhancement-clipboard-copy
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion packages/web-app-files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
59 changes: 30 additions & 29 deletions packages/web-app-files/src/components/SideBar/PrivateLinkItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<oc-button
v-if="isSupported"
v-oc-tooltip="buttonLabel"
appearance="raw"
:aria-label="buttonLabel"
Expand All @@ -18,43 +19,43 @@
</oc-button>
</template>

<script>
import { mapActions } from 'vuex'
import copyToClipboard from 'copy-to-clipboard'
import { unref } from 'vue'
<script lang="ts">
import { computed, defineComponent, inject, unref } from 'vue'
import { useClipboard } from '@vueuse/core'
import { Resource } from 'web-client'
import { useStore, useTranslations } from 'web-pkg/src'
export default {
export default defineComponent({
name: 'PrivateLinkItem',
inject: ['displayedItem'],
data: () => ({
copied: false,
timeout: null
}),
setup() {
const { $gettext } = useTranslations()
const store = useStore<any>()
const displayedItem = inject<Resource>('displayedItem')
const privateLink = computed(() => unref(displayedItem))
const { copy, copied, isSupported } = useClipboard({ legacy: true, copiedDuring: 550 })
const copyLinkToClipboard = () => {
copy(privateLink.value.privateLink)
store.dispatch('showMessage', {
title: $gettext('Private link copied'),
desc: $gettext('The private link has been copied to your clipboard.')
})
}
return {
copied,
copyLinkToClipboard,
isSupported
}
},
computed: {
buttonText() {
return this.$gettext('Private link')
},
buttonLabel() {
return this.$gettext('Copy private link to clipboard')
}
},
methods: {
...mapActions(['showMessage']),
copyPrivateLinkToClipboard() {
copyToClipboard(unref(this.displayedItem).privateLink)
this.clipboardSuccessHandler()
this.showMessage({
title: this.$gettext('Private link copied'),
desc: this.$gettext('The private link has been copied to your clipboard.')
})
},
clipboardSuccessHandler() {
this.copied = true
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.copied = false
}, 550)
}
}
}
})
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
/>
</div>
<oc-button
v-if="isSupported"
v-oc-tooltip="copyBtnHint"
appearance="raw"
:aria-label="copyBtnHint"
Expand All @@ -26,10 +27,10 @@
</div>
</template>

<script>
import { mapActions } from 'vuex'
<script lang="ts">
import { defineComponent } from 'vue'
import copyToClipboard from 'copy-to-clipboard'
import { useStore, useTranslations } from 'web-pkg/src/composables'
import { useClipboard } from '@vueuse/core'
export default defineComponent({
name: 'NameAndCopy',
Expand All @@ -39,10 +40,33 @@ export default defineComponent({
required: true
}
},
data: () => ({
copied: false,
timeout: null
}),
setup(props) {
const { $gettext, $gettextInterpolate } = useTranslations()
const store = useStore<any>()
const { copy, copied, isSupported } = useClipboard({ legacy: true, copiedDuring: 550 })
const copyLinkToClipboard = () => {
copy(props.link.url)
store.dispatch('showMessage', {
title: props.link.quicklink
? $gettext('The quicklink has been copied to your clipboard.')
: $gettextInterpolate(
$gettext('The link "%{linkName}" has been copied to your clipboard.'),
{
linkName: props.link.linkName
},
true
)
})
}
return {
copied,
copyLinkToClipboard,
isSupported
}
},
computed: {
linkName() {
return this.link.name
Expand All @@ -56,31 +80,6 @@ export default defineComponent({
copiedLabel() {
return this.$gettext('Copied')
}
},
methods: {
...mapActions(['showMessage']),
copyLinkToClipboard() {
copyToClipboard(this.link.url)
this.clipboardSuccessHandler()
this.showMessage({
title: this.link.quicklink
? this.$gettext('The quicklink has been copied to your clipboard.')
: this.$gettextInterpolate(
this.$gettext('The link "%{linkName}" has been copied to your clipboard.'),
{
linkName: this.linkName
},
true
)
})
},
clipboardSuccessHandler() {
this.copied = true
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.copied = false
}, 550)
}
}
})
</script>
Expand Down
5 changes: 3 additions & 2 deletions packages/web-app-files/src/helpers/share/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>
Expand Down Expand Up @@ -45,7 +45,8 @@ export const createQuicklink = async (args: CreateQuicklink): Promise<Share> =>
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.')
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Resource>({
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)
Expand All @@ -29,15 +45,6 @@ describe('PrivateLinkItem', () => {
})

function getWrapper() {
const folder = mockDeep<Resource>({
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 0 additions & 5 deletions packages/web-runtime/src/defaults/vue.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
})
Expand Down
12 changes: 0 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d367bd9

Please sign in to comment.