Skip to content

Commit

Permalink
Merge pull request galaxyproject#18022 from davelopez/add_zenodo_inte…
Browse files Browse the repository at this point in the history
…gration

Add Zenodo integration
  • Loading branch information
bgruening authored Apr 22, 2024
2 parents f96ca10 + 0ba0a6f commit af49b5d
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 48 deletions.
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

0 comments on commit af49b5d

Please sign in to comment.