Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Zenodo integration #18022

Merged
merged 11 commits into from
Apr 22, 2024
1 change: 1 addition & 0 deletions client/src/api/remoteFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fetcher } from "@/api/schema/fetcher";
* - `source` - allows to select a source plugin root and doesn't list its contents
*/
export type FileSourceBrowsingMode = "file" | "directory" | "source";
export type FilesSourcePlugin = components["schemas"]["FilesSourcePlugin"];
export type BrowsableFilesSourcePlugin = components["schemas"]["BrowsableFilesSourcePlugin"];
export type RemoteFile = components["schemas"]["RemoteFile"];
export type RemoteDirectory = components["schemas"]["RemoteDirectory"];
Expand Down
59 changes: 53 additions & 6 deletions client/src/components/Common/ExportRDMForm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getLocalVue } from "@tests/jest/helpers";
import { mount, Wrapper } from "@vue/test-utils";
import flushPromises from "flush-promises";

import { CreatedEntry } from "@/api/remoteFiles";
import { type BrowsableFilesSourcePlugin, CreatedEntry } from "@/api/remoteFiles";
import { mockFetcher } from "@/api/schema/__mocks__";

import ExportRDMForm from "./ExportRDMForm.vue";
Expand All @@ -25,11 +25,13 @@ const FAKE_ENTRY: CreatedEntry = {
external_link: "http://example.com",
};

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

const wrapper = mount(ExportRDMForm, {
propsData: {},
const wrapper = mount(ExportRDMForm as object, {
propsData: {
fileSource,
},
localVue,
});
await flushPromises();
Expand Down Expand Up @@ -100,8 +102,53 @@ describe("ExportRDMForm", () => {
});
});

async function selectExportChoice(choice: string) {
const exportChoice = wrapper.find(`#radio-${choice}`);
describe("Pre-select specific File Source", () => {
beforeEach(async () => {
const specificFileSource: BrowsableFilesSourcePlugin = {
id: "test-file-source",
label: "Test File Source",
doc: "Test File Source Description",
uri_root: "gxfiles://test-file-source",
writable: true,
browsable: true,
type: "rdm",
};
wrapper = await initWrapper(specificFileSource);
});

it("enables the create new record button only by setting the record name", async () => {
await selectExportChoice("new", "test-file-source");
expect(wrapper.find(CREATE_RECORD_BTN).attributes("disabled")).toBeTruthy();

await setRecordNameInput(FAKE_RECORD_NAME);

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 () => {
await selectExportChoice("new", "test-file-source");
expect(wrapper.find(EXPORT_TO_NEW_RECORD_BTN).exists()).toBeFalsy();

await setRecordNameInput(FAKE_RECORD_NAME);
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 selectExportChoice("new", "test-file-source");
await setFileNameInput("test file name");
await setRecordNameInput(FAKE_RECORD_NAME);
await clickCreateNewRecordButton();

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

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

Expand Down
37 changes: 31 additions & 6 deletions client/src/components/Common/ExportRDMForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
import { BButton, BCard, BFormGroup, BFormInput, BFormRadio, BFormRadioGroup } from "bootstrap-vue";
import { computed, ref } from "vue";

import { CreatedEntry, createRemoteEntry, FilterFileSourcesOptions } from "@/api/remoteFiles";
import {
BrowsableFilesSourcePlugin,
CreatedEntry,
createRemoteEntry,
FilterFileSourcesOptions,
} from "@/api/remoteFiles";
import { useToast } from "@/composables/toast";
import localize from "@/utils/localization";
import { errorMessageAsString } from "@/utils/simple-error";

import { fileSourcePluginToItem } from "../FilesDialog/utilities";

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

Expand All @@ -17,13 +24,19 @@ interface Props {
clearInputAfterExport?: boolean;
defaultRecordName?: string;
defaultFilename?: string;
/**
* If undefined, the user will need to select a repository to export to,
* otherwise this file source will be pre-selected.
*/
fileSource?: BrowsableFilesSourcePlugin;
}

const props = withDefaults(defineProps<Props>(), {
what: "archive",
clearInputAfterExport: false,
defaultRecordName: "",
defaultFilename: "",
fileSource: undefined,
});

const emit = defineEmits<{
Expand All @@ -35,7 +48,7 @@ type ExportChoice = "existing" | "new";
const includeOnlyRDMCompatible: FilterFileSourcesOptions = { include: ["rdm"] };

const recordUri = ref<string>("");
const sourceUri = ref<string>("");
const sourceUri = ref<string>(props.fileSource?.uri_root ?? "");
const fileName = ref<string>(props.defaultFilename);
const exportChoice = ref<ExportChoice>("new");
const recordName = ref<string>(props.defaultRecordName);
Expand All @@ -53,6 +66,9 @@ const recordNameDescription = computed(() => localize("Give the new record a nam
const namePlaceholder = computed(() => localize("File name"));
const recordNamePlaceholder = computed(() => localize("Record name"));

const uniqueSourceId = computed(() => props.fileSource?.id ?? "any");
const fileSourceAsItem = computed(() => (props.fileSource ? fileSourcePluginToItem(props.fileSource) : undefined));

function doExport() {
emit("export", recordUri.value, fileName.value);

Expand All @@ -72,7 +88,7 @@ async function doCreateRecord() {

function clearInputs() {
recordUri.value = "";
sourceUri.value = "";
sourceUri.value = props.fileSource?.uri_root ?? "";
fileName.value = "";
newEntry.value = undefined;
}
Expand All @@ -84,10 +100,17 @@ function clearInputs() {
<BFormInput id="file-name-input" v-model="fileName" :placeholder="namePlaceholder" required />
</BFormGroup>

<p>
Your {{ what }} needs to be uploaded to an existing <i>draft</i> record. You will need to create a
<b>new record</b> or select an existing <b>draft record</b> and then export your {{ what }} to it.
</p>

<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-new-${uniqueSourceId}`" v-localize name="exportChoice" value="new">
Export to new record
</BFormRadio>

<BFormRadio id="radio-existing" v-localize name="exportChoice" value="existing">
<BFormRadio :id="`radio-existing-${uniqueSourceId}`" v-localize name="exportChoice" value="existing">
Export to existing draft record
</BFormRadio>
</BFormRadioGroup>
Expand Down Expand Up @@ -123,6 +146,7 @@ function clearInputs() {
</div>
<div v-else>
<BFormGroup
v-if="!props.fileSource"
id="fieldset-record-new"
label-for="source-selector"
:description="repositoryRecordDescription"
Expand Down Expand Up @@ -172,7 +196,8 @@ function clearInputs() {
v-model="recordUri"
mode="directory"
:require-writable="true"
:filter-options="includeOnlyRDMCompatible" />
:filter-options="fileSource ? undefined : includeOnlyRDMCompatible"
:selected-item="fileSourceAsItem" />
</BFormGroup>

<BButton
Expand Down
21 changes: 6 additions & 15 deletions client/src/components/FilesDialog/FilesDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import { BAlert } from "bootstrap-vue";
import Vue, { computed, onMounted, ref } from "vue";

import {
BrowsableFilesSourcePlugin,
browseRemoteFiles,
FileSourceBrowsingMode,
FilterFileSourcesOptions,
getFileSources,
RemoteEntry,
} from "@/api/remoteFiles";
import { UrlTracker } from "@/components/DataDialog/utilities";
import { isSubPath } from "@/components/FilesDialog/utilities";
import { fileSourcePluginToItem, isSubPath } from "@/components/FilesDialog/utilities";
import { SELECTION_STATES, type SelectionItem } from "@/components/SelectionDialog/selectionTypes";
import { useConfig } from "@/composables/config";
import { errorMessageAsString } from "@/utils/simple-error";
Expand All @@ -33,6 +32,8 @@ interface FilesDialogProps {
multiple?: boolean;
/** Whether to show only writable sources */
requireWritable?: boolean;
/** Optional selected item to start browsing from */
selectedItem?: SelectionItem;
}

const props = withDefaults(defineProps<FilesDialogProps>(), {
Expand All @@ -42,6 +43,7 @@ const props = withDefaults(defineProps<FilesDialogProps>(), {
mode: "file",
multiple: false,
requireWritable: false,
selectedItem: undefined,
});

const { config, isConfigLoaded } = useConfig();
Expand Down Expand Up @@ -233,7 +235,7 @@ function load(record?: SelectionItem) {
.then((results) => {
const convertedItems = results
.filter((item) => !props.requireWritable || item.writable)
.map(fileSourcePluginToRecord);
.map(fileSourcePluginToItem);
items.value = convertedItems;
formatRows();
optionsShow.value = true;
Expand Down Expand Up @@ -290,17 +292,6 @@ function entryToRecord(entry: RemoteEntry): SelectionItem {
return result;
}

function fileSourcePluginToRecord(plugin: BrowsableFilesSourcePlugin): SelectionItem {
const result = {
id: plugin.id,
label: plugin.label,
details: plugin.doc,
isLeaf: false,
url: plugin.uri_root,
};
return result;
}

/** select all files in current folder**/
function onSelectAll() {
if (!currentDirectory.value) {
Expand Down Expand Up @@ -343,7 +334,7 @@ function onOk() {
}

onMounted(() => {
load();
load(props.selectedItem);
});
</script>

Expand Down
5 changes: 5 additions & 0 deletions client/src/components/FilesDialog/FilesInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { computed } from "vue";
import { FileSourceBrowsingMode, FilterFileSourcesOptions } from "@/api/remoteFiles";
import { filesDialog } from "@/utils/data";

import { SelectionItem } from "../SelectionDialog/selectionTypes";

interface Props {
value: string;
mode?: FileSourceBrowsingMode;
requireWritable?: boolean;
filterOptions?: FilterFileSourcesOptions;
selectedItem?: SelectionItem;
}

interface SelectableFile {
Expand All @@ -20,6 +23,7 @@ const props = withDefaults(defineProps<Props>(), {
mode: "file",
requireWritable: false,
filterOptions: undefined,
selectedItem: undefined,
});

const emit = defineEmits<{
Expand All @@ -40,6 +44,7 @@ const selectFile = () => {
mode: props.mode,
requireWritable: props.requireWritable,
filterOptions: props.filterOptions,
selectedItem: props.selectedItem,
};
filesDialog((selected: SelectableFile) => {
currentValue.value = selected?.url;
Expand Down
15 changes: 15 additions & 0 deletions client/src/components/FilesDialog/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { BrowsableFilesSourcePlugin } from "@/api/remoteFiles";

import type { SelectionItem } from "../SelectionDialog/selectionTypes";

export const isSubPath = (originPath: string, destinationPath: string) => {
return subPathCondition(ensureTrailingSlash(originPath), ensureTrailingSlash(destinationPath));
};
Expand All @@ -9,3 +13,14 @@ function ensureTrailingSlash(path: string) {
function subPathCondition(originPath: string, destinationPath: string) {
return originPath !== destinationPath && destinationPath.startsWith(originPath);
}

export function fileSourcePluginToItem(plugin: BrowsableFilesSourcePlugin): SelectionItem {
const result = {
id: plugin.id,
label: plugin.label,
details: plugin.doc,
isLeaf: false,
url: plugin.uri_root,
};
return result;
}
25 changes: 21 additions & 4 deletions client/src/components/History/Export/HistoryExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { setActivePinia } from "pinia";

import type { HistorySummary } from "@/api";
import { fetchHistoryExportRecords } from "@/api/histories.export";
import type { components } from "@/api/schema";
import type { FilesSourcePlugin } from "@/api/remoteFiles";
import { mockFetcher } from "@/api/schema/__mocks__";
import {
EXPIRED_STS_DOWNLOAD_RECORD,
Expand All @@ -32,8 +32,7 @@ const FAKE_HISTORY = {

const REMOTE_FILES_API_ENDPOINT = new RegExp("/api/remote_files/plugins");

type FilesSourcePluginList = components["schemas"]["FilesSourcePlugin"][];
const REMOTE_FILES_API_RESPONSE: FilesSourcePluginList = [
const REMOTE_FILES_API_RESPONSE: FilesSourcePlugin[] = [
{
id: "test-posix-source",
type: "posix",
Expand All @@ -57,7 +56,7 @@ async function mountHistoryExport() {
(_history_id: string) => FAKE_HISTORY as HistorySummary
);

const wrapper = shallowMount(HistoryExport, {
const wrapper = shallowMount(HistoryExport as object, {
propsData: { historyId: FAKE_HISTORY_ID },
localVue,
pinia,
Expand Down Expand Up @@ -121,4 +120,22 @@ describe("HistoryExport.vue", () => {
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
expect(wrapper.find("#file-source-tab").exists()).toBe(false);
});

it("should display the ZENODO tab if the Zenodo plugin is available", async () => {
const zenodoPlugin: FilesSourcePlugin = {
id: "zenodo",
type: "rdm",
label: "Zenodo",
doc: "For testing",
writable: true,
browsable: true,
};
mockFetcher
.path(REMOTE_FILES_API_ENDPOINT)
.method("get")
.mock({ data: [zenodoPlugin] });
const wrapper = await mountHistoryExport();

expect(wrapper.find("#zenodo-file-source-tab").exists()).toBe(true);
});
});
Loading
Loading