Skip to content

Commit

Permalink
Create UI for single use links (#1638)
Browse files Browse the repository at this point in the history
* Create UI for single use links

* Add tests and pin copy messages to the top of the visible page
  • Loading branch information
lfarrell authored Dec 11, 2023
1 parent 44b2d5a commit 37ed761
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 6 deletions.
16 changes: 16 additions & 0 deletions static/css/sass/cdr_ui_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1038,6 +1046,14 @@ table.dataTable {
}
}

.is-narrow.item-actions {
text-align: left;

.actionlink {
margin-bottom: 3px;
}
}

.record-metadata {
justify-content: center;
display: grid;
Expand Down
12 changes: 6 additions & 6 deletions static/js/vue-cdr-access/package-lock.json

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

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<i class="fa fa-search" aria-hidden="true"></i> View</a>
</div>
</template>
<template v-if="hasPermission(recordData, 'viewHidden')">
<single-use-link :uuid="recordData.briefObject.id"></single-use-link>
</template>
</template>
<div v-if="fieldExists(recordData.briefObject.embargoDate) && !hasPermission(recordData, 'viewOriginal')" class="noaction">
{{ $t('full_record.available_date', { available_date: formatDate(recordData.briefObject.embargoDate) }) }}
Expand All @@ -28,12 +31,15 @@
</template>

<script>
import singleUseLink from '@/components/full_record/singleUseLink.vue';
import fileDownloadUtils from '../../mixins/fileDownloadUtils';
import fullRecordUtils from '../../mixins/fullRecordUtils';
export default {
name: 'restrictedContent',
components: {singleUseLink},
mixins: [fileDownloadUtils, fullRecordUtils],
props: {
Expand Down
137 changes: 137 additions & 0 deletions static/js/vue-cdr-access/src/components/full_record/singleUseLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<div class="header-button-single-download">
<div class="actionlink single-download">
<div class="single-use-msg-text" :class="{'display-msg': this.message !== ''}">
{{ this.message }}
</div>
<a class="button action" id="single-use-link" href="#" @click.prevent="createLink()">{{ $t('full_record.download_single_use') }}</a>
<ul>
<li v-for="single_use_link in single_use_links">
<div class="download-link-wrapper">
<div>{{ $t('full_record.created_link', { link: single_use_link.link, expire_time: single_use_link.expires }) }}</div>
<a @click.prevent="copyUrl(single_use_link.link)" href="#" class="download button action">Copy</a>
</div>
</li>
</ul>
</div>
</div>
</template>

<script>
import axios from 'axios';
export default {
name: 'singleUseLink',
props: {
uuid: String
},
data() {
return {
single_use_links: [],
message: ''
}
},
methods: {
createLink() {
axios({
method: 'post',
url: `/services/api/single_use_link/create/${this.uuid}`
}).then((response) => {
this.single_use_links.push(response.data)
}).catch((error) => {
console.log(error);
this.message = this.$t('full_record.created_link_failed', { uuid: this.uuid});
this.fadeOutMsg();
});
},
async copyUrl(text) {
try {
await navigator.clipboard.writeText(text);
this.message = this.$t('full_record.copied_link', { text: text});
} catch(err) {
this.message = this.$t('full_record.copied_link_failed', { text: text});
}
this.fadeOutMsg();
},
fadeOutMsg() {
setTimeout(() => this.message = '', 3000);
}
}
}
</script>

<style scoped lang="scss">
.header-button-single-download {
text-align: right;
.single-download {
display: block;
a {
float: right;
max-width: 210px;
}
ul {
margin-top: 10px;
li {
.download-link-wrapper {
align-items: center;
display: inline-flex;
margin: 5px auto;
div {
background-color: white;
padding: 15px;
}
}
}
}
}
.single-use-msg-text {
display: none;
}
.display-msg {
background: white;
border: 1px solid;
border-radius: 5px;
display: block;
height: auto;
padding: 5px;
position: fixed;
right: 10px;
text-align: center;
top: 10px;
width: 250px;
z-index: 599;
}
}
@media (max-width: 768px) {
.header-button-single-download {
text-align: left;
.single-download {
display: block;
a {
float: none;
}
ul {
li {
margin-left: 0;
}
}
}
}
}
</style>
5 changes: 5 additions & 0 deletions static/js/vue-cdr-access/src/translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
80 changes: 80 additions & 0 deletions static/js/vue-cdr-access/tests/unit/singleUseLink.spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 37ed761

Please sign in to comment.