Skip to content

Commit

Permalink
Merge pull request #16381 from davelopez/explore_doi_repository_integ…
Browse files Browse the repository at this point in the history
…ration

Add Invenio RDM repository integration
  • Loading branch information
jmchilton authored Aug 30, 2023
2 parents d15b2fe + 343f874 commit 8b5be5b
Show file tree
Hide file tree
Showing 26 changed files with 1,587 additions and 92 deletions.
10 changes: 9 additions & 1 deletion client/src/components/Common/ExportForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { BButton, BCol, BFormGroup, BFormInput, BRow } from "bootstrap-vue";
import { computed, ref } from "vue";
import { FilterFileSourcesOptions } from "@/components/FilesDialog/services";
import localize from "@/utils/localization";
import FilesInput from "@/components/FilesDialog/FilesInput.vue";
Expand All @@ -20,6 +21,8 @@ const emit = defineEmits<{
(e: "export", directory: string, name: string): void;
}>();
const defaultExportFilterOptions: FilterFileSourcesOptions = { exclude: ["rdm"] };
const directory = ref<string>("");
const name = ref<string>("");
Expand All @@ -43,7 +46,12 @@ const doExport = () => {
<template>
<div class="export-to-remote-file">
<BFormGroup id="fieldset-directory" label-for="directory" :description="directoryDescription" class="mt-3">
<FilesInput id="directory" v-model="directory" mode="directory" :require-writable="true" />
<FilesInput
id="directory"
v-model="directory"
mode="directory"
:require-writable="true"
:filter-options="defaultExportFilterOptions" />
</BFormGroup>
<BFormGroup id="fieldset-name" label-for="name" :description="nameDescription" class="mt-3">
<BFormInput id="name" v-model="name" :placeholder="namePlaceholder" required />
Expand Down
139 changes: 139 additions & 0 deletions client/src/components/Common/ExportRDMForm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { getLocalVue } from "@tests/jest/helpers";
import { mount, Wrapper } from "@vue/test-utils";
import flushPromises from "flush-promises";

import { mockFetcher } from "@/schema/__mocks__";

import { CreatedEntry } from "../FilesDialog/services";

import ExportRDMForm from "./ExportRDMForm.vue";
import FilesInput from "@/components/FilesDialog/FilesInput.vue";

jest.mock("@/schema");

const localVue = getLocalVue(true);

const CREATE_RECORD_BTN = "#create-record-button";
const EXPORT_TO_NEW_RECORD_BTN = "#export-button-new-record";
const EXPORT_TO_EXISTING_RECORD_BTN = "#export-button-existing-record";

const FAKE_RDM_SOURCE_URI = "gxfiles://test-uri";
const FAKE_RDM_EXISTING_RECORD_URI = "gxfiles://test-uri/test-record";
const FAKE_RECORD_NAME = "test record name";
const FAKE_ENTRY: CreatedEntry = {
uri: FAKE_RDM_SOURCE_URI,
name: FAKE_RECORD_NAME,
external_link: "http://example.com",
};

async function initWrapper() {
mockFetcher.path("/api/remote_files").method("post").mock({ data: FAKE_ENTRY });

const wrapper = mount(ExportRDMForm, {
propsData: {},
localVue,
});
await flushPromises();
return wrapper;
}

describe("ExportRDMForm", () => {
let wrapper: Wrapper<Vue>;

beforeEach(async () => {
wrapper = await initWrapper();
});

describe("Export to new record", () => {
beforeEach(async () => {
await selectExportChoice("new");
});

it("enables the create new record button when the required fields are filled in", async () => {
expect(wrapper.find(CREATE_RECORD_BTN).attributes("disabled")).toBeTruthy();

await setRecordNameInput(FAKE_RECORD_NAME);
await setRDMSourceInput(FAKE_RDM_SOURCE_URI);

expect(wrapper.find(CREATE_RECORD_BTN).attributes("disabled")).toBeFalsy();
});

it("displays the export to this record button when the create new record button is clicked", async () => {
expect(wrapper.find(EXPORT_TO_NEW_RECORD_BTN).exists()).toBeFalsy();

await setRecordNameInput(FAKE_RECORD_NAME);
await setRDMSourceInput(FAKE_RDM_SOURCE_URI);
await clickCreateNewRecordButton();

expect(wrapper.find(EXPORT_TO_NEW_RECORD_BTN).exists()).toBeTruthy();
});

it("emits an export event when the export to new record button is clicked", async () => {
await setFileNameInput("test file name");
await setRecordNameInput(FAKE_RECORD_NAME);
await setRDMSourceInput(FAKE_RDM_SOURCE_URI);
await clickCreateNewRecordButton();

await wrapper.find(EXPORT_TO_NEW_RECORD_BTN).trigger("click");
expect(wrapper.emitted("export")).toBeTruthy();
});
});

describe("Export to existing record", () => {
beforeEach(async () => {
await selectExportChoice("existing");
});

it("enables the export to existing record button when the required fields are filled in", async () => {
expect(wrapper.find(EXPORT_TO_EXISTING_RECORD_BTN).attributes("disabled")).toBeTruthy();

await setFileNameInput("test file name");
await setRDMDirectoryInput(FAKE_RDM_EXISTING_RECORD_URI);

expect(wrapper.find(EXPORT_TO_EXISTING_RECORD_BTN).attributes("disabled")).toBeFalsy();
});

it("emits an export event when the export to existing record button is clicked", async () => {
await setFileNameInput("test file name");
await setRDMDirectoryInput(FAKE_RDM_EXISTING_RECORD_URI);
await wrapper.find(EXPORT_TO_EXISTING_RECORD_BTN).trigger("click");
expect(wrapper.emitted("export")).toBeTruthy();
});
});

async function selectExportChoice(choice: string) {
const exportChoice = wrapper.find(`#radio-${choice}`);
await exportChoice.setChecked(true);
}

async function setRDMSourceInput(newValue: string) {
const component = wrapper.findComponent(FilesInput);
expect(component.attributes("placeholder")).toContain("source");
component.vm.$emit("input", newValue);
await flushPromises();
}

async function setRDMDirectoryInput(newValue: string) {
const component = wrapper.findComponent(FilesInput);
expect(component.attributes("placeholder")).toContain("directory");
component.vm.$emit("input", newValue);
await flushPromises();
}

async function setRecordNameInput(newValue: string) {
const recordNameInput = wrapper.find("#record-name-input");
await recordNameInput.setValue(newValue);
}

async function setFileNameInput(newValue: string) {
const recordNameInput = wrapper.find("#file-name-input");
await recordNameInput.setValue(newValue);
}

async function clickCreateNewRecordButton() {
const createRecordButton = wrapper.find(CREATE_RECORD_BTN);
expect(createRecordButton.attributes("disabled")).toBeFalsy();
await createRecordButton.trigger("click");
await flushPromises();
}
});
181 changes: 181 additions & 0 deletions client/src/components/Common/ExportRDMForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<script setup lang="ts">
import { BButton, BCard, BFormGroup, BFormInput, BFormRadio, BFormRadioGroup } from "bootstrap-vue";
import { computed, ref } from "vue";
import { CreatedEntry, createRemoteEntry, FilterFileSourcesOptions } from "@/components/FilesDialog/services";
import { useToast } from "@/composables/toast";
import localize from "@/utils/localization";
import { errorMessageAsString } from "@/utils/simple-error";
import ExternalLink from "@/components/ExternalLink.vue";
import FilesInput from "@/components/FilesDialog/FilesInput.vue";
const toast = useToast();
interface Props {
what?: string;
clearInputAfterExport?: boolean;
defaultRecordName?: string;
defaultFilename?: string;
}
const props = withDefaults(defineProps<Props>(), {
what: "archive",
clearInputAfterExport: false,
defaultRecordName: "",
defaultFilename: "",
});
const emit = defineEmits<{
(e: "export", recordUri: string, fileName: string, newRecordName?: string): void;
}>();
type ExportChoice = "existing" | "new";
const includeOnlyRDMCompatible: FilterFileSourcesOptions = { include: ["rdm"] };
const recordUri = ref<string>("");
const sourceUri = ref<string>("");
const fileName = ref<string>(props.defaultFilename);
const exportChoice = ref<ExportChoice>("new");
const recordName = ref<string>(props.defaultRecordName);
const newEntry = ref<CreatedEntry>();
const canCreateRecord = computed(() => Boolean(sourceUri.value) && Boolean(recordName.value));
const canExport = computed(() => Boolean(recordUri.value) && Boolean(fileName.value));
const repositoryRecordDescription = computed(() => localize(`Select a repository to export ${props.what} to.`));
const nameDescription = computed(() => localize("Give the exported file a name."));
const recordNameDescription = computed(() => localize("Give the new record a name or title."));
const namePlaceholder = computed(() => localize("File name"));
const recordNamePlaceholder = computed(() => localize("Record name"));
function doExport() {
emit("export", recordUri.value, fileName.value);
if (props.clearInputAfterExport) {
clearInputs();
}
}
async function doCreateRecord() {
try {
newEntry.value = await createRemoteEntry(sourceUri.value, recordName.value);
recordUri.value = newEntry.value.uri;
} catch (e) {
toast.error(errorMessageAsString(e));
}
}
function clearInputs() {
recordUri.value = "";
sourceUri.value = "";
fileName.value = "";
newEntry.value = undefined;
}
</script>

<template>
<div class="export-to-rdm-repository">
<BFormGroup id="fieldset-name" label-for="name" :description="nameDescription" class="mt-3">
<BFormInput id="file-name-input" v-model="fileName" :placeholder="namePlaceholder" required />
</BFormGroup>

<BFormRadioGroup v-model="exportChoice" class="export-radio-group">
<BFormRadio id="radio-new" v-localize name="exportChoice" value="new"> Export to new record </BFormRadio>
<BFormRadio id="radio-existing" v-localize name="exportChoice" value="existing">
Export to existing draft record
</BFormRadio>
</BFormRadioGroup>

<div v-if="exportChoice === 'new'">
<div v-if="newEntry">
<BCard>
<p>
<b>{{ newEntry.name }}</b>
<span v-localize> draft record has been created in the repository.</span>
</p>
<p v-if="newEntry.external_link">
You can preview the record in the repository, further edit its metadata and decide when to
publish it at
<ExternalLink :href="newEntry.external_link">
<b>{{ newEntry.external_link }}</b>
</ExternalLink>
</p>
<p v-localize>Please use the button below to upload the exported {{ props.what }} to the record.</p>
<BButton
id="export-button-new-record"
v-localize
class="export-button"
variant="primary"
:disabled="!canExport"
@click.prevent="doExport">
Export to this record
</BButton>
</BCard>
</div>
<div v-else>
<BFormGroup
id="fieldset-record-new"
label-for="source-selector"
:description="repositoryRecordDescription"
class="mt-3">
<FilesInput
id="source-selector"
v-model="sourceUri"
mode="source"
:require-writable="true"
:filter-options="includeOnlyRDMCompatible" />
</BFormGroup>
<BFormGroup
id="fieldset-record-name"
label-for="record-name"
:description="recordNameDescription"
class="mt-3">
<BFormInput
id="record-name-input"
v-model="recordName"
:placeholder="recordNamePlaceholder"
required />
</BFormGroup>
<p v-localize>
You need to create the new record in a repository before exporting the {{ props.what }} to it.
</p>
<BButton
id="create-record-button"
v-localize
variant="primary"
:disabled="!canCreateRecord"
@click.prevent="doCreateRecord">
Create new record
</BButton>
</div>
</div>
<div v-else>
<BFormGroup
id="fieldset-record-existing"
label-for="existing-record-selector"
:description="repositoryRecordDescription"
class="mt-3">
<FilesInput
id="existing-record-selector"
v-model="recordUri"
mode="directory"
:require-writable="true"
:filter-options="includeOnlyRDMCompatible" />
</BFormGroup>
<BButton
id="export-button-existing-record"
v-localize
class="export-button"
variant="primary"
:disabled="!canExport"
@click.prevent="doExport">
Export to existing record
</BButton>
</div>
</div>
</template>
Loading

0 comments on commit 8b5be5b

Please sign in to comment.