diff --git a/static/css/sass/cdr_ui_styles.scss b/static/css/sass/cdr_ui_styles.scss index c3cbf4ff1a..5c81ff61f6 100644 --- a/static/css/sass/cdr_ui_styles.scss +++ b/static/css/sass/cdr_ui_styles.scss @@ -704,6 +704,14 @@ table.dataTable { border-radius: 5px; } +.is-narrow.item-actions { + text-align: right; + + .actionlink { + margin-bottom: 0; + } +} + .item-actions { .actionlink { a.action { @@ -1038,6 +1046,14 @@ table.dataTable { } } + .is-narrow.item-actions { + text-align: left; + + .actionlink { + margin-bottom: 3px; + } + } + .record-metadata { justify-content: center; display: grid; diff --git a/static/js/vue-cdr-access/package-lock.json b/static/js/vue-cdr-access/package-lock.json index d05061ff38..10a2853fa7 100644 --- a/static/js/vue-cdr-access/package-lock.json +++ b/static/js/vue-cdr-access/package-lock.json @@ -51,9 +51,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", - "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "node_modules/@antfu/utils": { @@ -10261,9 +10261,9 @@ }, "dependencies": { "@adobe/css-tools": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", - "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "@antfu/utils": { diff --git a/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue b/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue index e8406e1c9f..007e1fe83c 100644 --- a/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue +++ b/static/js/vue-cdr-access/src/components/full_record/restrictedContent.vue @@ -20,6 +20,9 @@ View +
{{ $t('full_record.available_date', { available_date: formatDate(recordData.briefObject.embargoDate) }) }} @@ -28,12 +31,15 @@ + + \ No newline at end of file diff --git a/static/js/vue-cdr-access/src/translations.js b/static/js/vue-cdr-access/src/translations.js index 5a4c299428..cb9af99745 100644 --- a/static/js/vue-cdr-access/src/translations.js +++ b/static/js/vue-cdr-access/src/translations.js @@ -42,12 +42,17 @@ export default { available_date: "Available after {available_date}", collection_id: "Archival Collection ID", contains: "Contains", + copied_link: "Download URL, {text}, copied to clipboard", + copied_link_failed: "Unable to copy download URL, {text}, to clipboard", + created_link: "Created link {link} expires in {expire_time}", + created_link_failed: "Unable to create single use link for {uuid}", creator: "Creator", date_added: "Date Added", date_created: "Date Created", detailed_metadata: "Detailed Metadata", download: "Download", download_file: "Download file", + download_single_use: "Generate Single-Use Link", download_title: "Download {title}", download_unavailable: "Download Unavailable", edit: "Edit", diff --git a/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js b/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js index eddd18b8d9..20debbd2d1 100644 --- a/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js +++ b/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js @@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils' import { createRouter, createWebHistory } from 'vue-router'; import restrictedContent from '@/components/full_record/restrictedContent.vue'; import displayWrapper from '@/components/displayWrapper.vue'; +import singleUseLink from '@/components/full_record/singleUseLink.vue'; import {createI18n} from 'vue-i18n'; import translations from '@/translations'; import cloneDeep from 'lodash.clonedeep'; @@ -484,6 +485,26 @@ describe('restrictedContent.vue', () => { expect(wrapper.find('.download').exists()).toBe(false); }); + it('displays a single use link button for files with the proper permissions', async () => { + const updated_data = cloneDeep(record); + updated_data.briefObject.permissions = ['viewAccessCopies', 'viewHidden', 'viewOriginal']; + await wrapper.setProps({ + recordData: updated_data + }); + expect(wrapper.findComponent(singleUseLink).exists()).toBe(true); + }); + + it('does not display a single use link button for files without the proper permissions', async () => { + const updated_data = cloneDeep(record); + updated_data.dataFileUrl = 'content/4db695c0-5fd5-4abf-9248-2e115d43f57d'; + updated_data.resourceType = 'File'; + updated_data.briefObject.permissions = ['viewAccessCopies']; + await wrapper.setProps({ + recordData: updated_data + }); + expect(wrapper.findComponent(singleUseLink).exists()).toBe(false); + }); + it('displays embargo information for files', async () => { const updated_data = cloneDeep(record); updated_data.briefObject.embargoDate = '2199-12-31T20:34:01.799Z'; diff --git a/static/js/vue-cdr-access/tests/unit/singleUseLink.spec.js b/static/js/vue-cdr-access/tests/unit/singleUseLink.spec.js new file mode 100644 index 0000000000..fb7e4841c8 --- /dev/null +++ b/static/js/vue-cdr-access/tests/unit/singleUseLink.spec.js @@ -0,0 +1,80 @@ +import { shallowMount } from '@vue/test-utils' +import singleUseLink from '@/components/full_record/singleUseLink.vue'; +import {createI18n} from 'vue-i18n'; +import translations from '@/translations'; +import moxios from 'moxios'; + +const uuid = '9f7f3746-0237-4261-96a2-4b4765d4ae03'; +const response_date = { link: `https://test.edu`, expires: '24hrs', id: uuid }; +let wrapper; + +describe('singleUseLink.vue', () => { + const i18n = createI18n({ + locale: 'en', + fallbackLocale: 'en', + messages: translations + }); + + beforeEach(() => { + wrapper = shallowMount(singleUseLink, { + global: { + plugins: [i18n] + }, + props: { + uuid: '9f7f3746-0237-4261-96a2-4b4765d4ae03' + } + }); + + moxios.install(); + }); + + afterEach(function () { + moxios.uninstall(); + }); + + it("creates single use links", (done) => { + expect(wrapper.find('.download-link-wrapper').exists()).toBe(false); + + moxios.stubRequest(`/services/api/single_use_link/create/${uuid}`, { + status: 200, + response: JSON.stringify(response_date) + }); + + moxios.wait(async () => { + await wrapper.find('#single-use-link').trigger('click'); + expect(wrapper.find('.download-link-wrapper').exists()).toBe(true); + expect(wrapper.find('.download-link-wrapper div').text()) + .toEqual(`Created link ${response_date.link} expires in ${response_date.expires}`); + expect(wrapper.find('.download-link-wrapper a').exists()).toBe(true); // Copy button + done(); + }); + }); + + it("does not create single use links on response errors", (done) => { + expect(wrapper.find('.download-link-wrapper').exists()).toBe(false); + + moxios.stubRequest(`/services/api/single_use_link/create/${uuid}`, { + status: 404, + response: JSON.stringify('No record here') + }); + + moxios.wait(async () => { + await wrapper.find('#single-use-link').trigger('click'); + expect(wrapper.find('.download-link-wrapper').exists()).toBe(false); + done(); + }); + }); + + it("copies single use links", async () => { + Object.assign(window.navigator, { + clipboard: { + writeText: jest.fn().mockImplementation(() => Promise.resolve()), + }, + }); + + await wrapper.setData({ single_use_links: [response_date] }); + await wrapper.find('.download-link-wrapper a').trigger('click'); + expect(window.navigator.clipboard.writeText) + .toHaveBeenCalledWith(response_date.link); + }); +});