Skip to content

Commit 93afc48

Browse files
Merge pull request galaxyproject#22165 from davelopez/add_new_upload_modal
Add new Upload Modal Dialog
2 parents 56ee233 + 9833762 commit 93afc48

38 files changed

Lines changed: 3229 additions & 1444 deletions

client/src/components/DataDialog/DataDialog.vue

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<script setup lang="ts">
22
import { faUpload } from "@fortawesome/free-solid-svg-icons";
33
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
4+
import { BBadge } from "bootstrap-vue";
45
import { onMounted, type Ref, ref, watch } from "vue";
56
import Vue from "vue";
67
8+
import type { DataOption } from "@/components/Form/Elements/FormData/types";
79
import type { SelectionItem } from "@/components/SelectionDialog/selectionTypes";
810
import { useGlobalUploadModal } from "@/composables/globalUploadModal";
11+
import { useUploadMethodModal } from "@/composables/upload/useUploadMethodModal";
912
import { useUrlTracker } from "@/composables/urlTracker";
1013
import { getAppRoot } from "@/onload/loadConfig";
1114
import { errorMessageAsString } from "@/utils/simple-error";
@@ -20,14 +23,16 @@ type Record = SelectionItem;
2023
2124
interface Props {
2225
allowUpload?: boolean;
23-
callback?: (results: Array<Record>) => void;
26+
callback?: (results: Record[] | DataOption[]) => void;
2427
filterOkState?: boolean;
2528
filterByTypeIds?: string[];
2629
format?: string;
2730
library?: boolean;
2831
multiple?: boolean;
2932
title?: string;
3033
history: string;
34+
/** Optional formats to constrain the upload modal */
35+
uploadModalFormats?: string[];
3136
}
3237
3338
const props = withDefaults(defineProps<Props>(), {
@@ -39,6 +44,7 @@ const props = withDefaults(defineProps<Props>(), {
3944
library: true,
4045
multiple: false,
4146
title: "",
47+
uploadModalFormats: undefined,
4248
});
4349
4450
const emit = defineEmits<{
@@ -48,6 +54,7 @@ const emit = defineEmits<{
4854
}>();
4955
5056
const { openGlobalUploadModal } = useGlobalUploadModal();
57+
const { openUploadModal } = useUploadMethodModal();
5158
5259
const errorMessage = ref("");
5360
const filter = ref("");
@@ -125,7 +132,7 @@ function onClick(record: Record) {
125132
function onOk() {
126133
const results = model.finalize();
127134
modalShow.value = false;
128-
props.callback(results);
135+
props.callback?.(results);
129136
emit("onOk", results);
130137
}
131138
@@ -135,7 +142,7 @@ function onOpen(record: Record) {
135142
}
136143
137144
/** Called when user decides to upload new data */
138-
function onUpload() {
145+
function onLegacyUpload() {
139146
const propsData = {
140147
multiple: props.multiple,
141148
format: props.format,
@@ -148,6 +155,21 @@ function onUpload() {
148155
emit("onUpload");
149156
}
150157
158+
async function onBetaUpload() {
159+
const result = await openUploadModal({
160+
formats: props.uploadModalFormats,
161+
multiple: props.multiple,
162+
hideTips: true,
163+
});
164+
modalShow.value = false;
165+
if (!result.cancelled) {
166+
const uploadedOptions = result.toDataOptions();
167+
props.callback?.(uploadedOptions);
168+
emit("onOk", uploadedOptions);
169+
}
170+
emit("onUpload");
171+
}
172+
151173
/** Performs server request to retrieve data records **/
152174
function load(url?: string) {
153175
if (url) {
@@ -207,10 +229,14 @@ watch(
207229
@onOpen="onOpen"
208230
@onUndo="load()">
209231
<template v-slot:buttons>
210-
<GButton v-if="allowUpload" size="small" @click="onUpload">
232+
<GButton v-if="allowUpload" size="small" class="mr-1" @click="onLegacyUpload">
211233
<FontAwesomeIcon :icon="faUpload" />
212234
Upload
213235
</GButton>
236+
<GButton v-if="allowUpload" size="small" title="Try our new upload experience" @click="onBetaUpload">
237+
<FontAwesomeIcon :icon="faUpload" />
238+
<span v-localize>New upload<BBadge variant="warning" class="ml-1">Beta</BBadge></span>
239+
</GButton>
214240
</template>
215241
</SelectionDialog>
216242
</template>

client/src/components/FilesDialog/utilities.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,10 @@ export function sortFileSources(a: SelectionItem, b: SelectionItem): number {
5757
}
5858
return a.label.localeCompare(b.label);
5959
}
60+
61+
/**
62+
* Normalize SelectionDialog's Model.finalize() return value (single item or array) into an array.
63+
*/
64+
export function selectionToArray(selection: SelectionItem | SelectionItem[]): SelectionItem[] {
65+
return Array.isArray(selection) ? selection : selection ? [selection] : [];
66+
}

client/src/components/Form/Elements/FormData/FormData.vue

Lines changed: 143 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { type EventData, useEventStore } from "@/stores/eventStore";
2828
import { orList } from "@/utils/strings";
2929
3030
import type { DataOption, ExtendedCollectionType } from "./types";
31-
import { containsDataOption } from "./types";
31+
import { containsDataOption, isDataOption } from "./types";
3232
import { BATCH, SOURCE, VARIANTS } from "./variants";
3333
3434
import FormSelection from "../FormSelection.vue";
@@ -38,13 +38,25 @@ import FormDataWorkflowRunTabs from "./FormDataWorkflowRunTabs.vue";
3838
import FormSelect from "@/components/Form/Elements/FormSelect.vue";
3939
import HelpText from "@/components/Help/HelpText.vue";
4040
41+
type HistoryOrCollectionItem = HistoryItemSummary | DCESummary;
42+
43+
/**
44+
* These are raw API items that need to be converted to DataOption format.
45+
*/
46+
type SingleOrMultipleHistoryItems = HistoryOrCollectionItem | HistoryOrCollectionItem[];
47+
48+
/**
49+
* Response types from the data dialog callback.
50+
* DataOption[] is returned by the beta upload path for fresh uploads.
51+
* SingleOrMultipleHistoryItems (HistoryItemSummary and DCESummary) are returned for dataset/collection selection.
52+
*/
53+
type DialogResponse = DataOption[] | SingleOrMultipleHistoryItems;
54+
4155
type SelectOption = {
4256
label: string;
4357
value: DataOption | null;
4458
};
4559
46-
type HistoryOrCollectionItem = HistoryItemSummary | DCESummary;
47-
4860
const props = withDefaults(
4961
defineProps<{
5062
loading?: boolean;
@@ -225,6 +237,10 @@ const formattedOptions = computed(() => {
225237
// check if option (with same id) is already in result, if yes replace it with keepOption
226238
const existingOptionIndex = result.findIndex((v) => v.value?.id === option.value?.id);
227239
if (existingOptionIndex >= 0) {
240+
const existingOption = result[existingOptionIndex];
241+
if (existingOption?.value && shouldPreferCanonicalOption(existingOption.value, option.value)) {
242+
return;
243+
}
228244
result[existingOptionIndex] = option;
229245
} else {
230246
result.unshift(option);
@@ -351,8 +367,16 @@ function getSourceType(val: DataOption) {
351367
}
352368
}
353369
354-
/** Add values from drag/drop or data dialog sources */
355-
function handleIncoming(incoming: Record<string, unknown> | Record<string, unknown>[], partial = true) {
370+
/**
371+
* Handle incoming data from sources that require validation and transformation.
372+
* This includes drag-drop operations and data dialog selections.
373+
* Validates datatype compatibility, source type compatibility, and converts to DataOption format.
374+
*
375+
* @param incoming - The incoming data objects to process
376+
* @param partial - If true, merge with existing selection; if false, replace selection
377+
* @returns true if processing succeeded, false otherwise
378+
*/
379+
function handleIncoming(incoming: SingleOrMultipleHistoryItems, partial = true) {
356380
if (incoming) {
357381
const values = Array.isArray(incoming) ? incoming : [incoming];
358382
@@ -478,6 +502,81 @@ function toDataOption(item: HistoryOrCollectionItem): DataOption | null {
478502
return newValue;
479503
}
480504
505+
/**
506+
* Normalize an uploaded option by finding matching options in existing props.
507+
* Returns the canonical option if found, otherwise returns the uploaded option.
508+
*/
509+
function normalizeOption(option: DataOption): DataOption {
510+
const keepKey = `${option.id}_${option.src}`;
511+
const existingOptions = props.options?.[option.src];
512+
const foundOption = existingOptions?.find((existing) => existing.id === option.id);
513+
514+
if (foundOption) {
515+
return foundOption;
516+
}
517+
518+
// Cache new option in keepOptions if not already present
519+
if (!isInKeepOptions(keepKey, option)) {
520+
keepOptions[keepKey] = {
521+
label: `${option.hid || "Selected"}: ${option.name}`,
522+
value: option,
523+
};
524+
keepOptionsUpdate.value++;
525+
}
526+
527+
return option;
528+
}
529+
530+
/**
531+
* Normalize an array of uploaded options, preferring existing matches.
532+
*/
533+
function normalizeUploadedOptions(options: DataOption[]): DataOption[] {
534+
return options.map(normalizeOption);
535+
}
536+
537+
/**
538+
* Update currentValue based on the current variant configuration.
539+
* For multiple dataset fields, merges new options. Otherwise, selects the first option.
540+
*/
541+
function updateCurrentValue(options: DataOption[]): void {
542+
const config = currentVariant.value;
543+
544+
if (config?.src === SOURCE.DATASET && config.multiple) {
545+
// Merge new options into existing selection, avoiding duplicates
546+
const merged = currentValue.value ? [...currentValue.value] : [];
547+
for (const option of options) {
548+
if (!containsDataOption(merged, option)) {
549+
merged.push(option);
550+
}
551+
}
552+
currentValue.value = merged;
553+
} else {
554+
// Single selection: use first option
555+
currentValue.value = [options[0]!];
556+
}
557+
}
558+
559+
/**
560+
* Handle data options freshly uploaded through the upload dialog.
561+
* Normalizes options against existing props and updates the current selection.
562+
*/
563+
function handleUploadedDataOptions(uploadedOptions: DataOption[]): void {
564+
if (!uploadedOptions?.length) {
565+
return;
566+
}
567+
568+
const normalized = normalizeUploadedOptions(uploadedOptions);
569+
updateCurrentValue(normalized);
570+
}
571+
572+
function isUnavailableName(name: string | undefined): boolean {
573+
return Boolean(name && name.toLowerCase().startsWith("(unavailable)"));
574+
}
575+
576+
function shouldPreferCanonicalOption(canonical: DataOption, keep: DataOption): boolean {
577+
return (isUnavailableName(keep.name) && !isUnavailableName(canonical.name)) || (!keep.hid && !!canonical.hid);
578+
}
579+
481580
/**
482581
* Check if the new value is already in the keepOptions.
483582
* This doesn't only check if the value is already stored by the `keepKey`, but also if the new value
@@ -495,23 +594,35 @@ function isInKeepOptions(keepKey: string, newValue: DataOption): boolean {
495594
}
496595
497596
/**
498-
* Open file dialog
597+
* Callback handler for the data dialog.
598+
* Routes responses to appropriate handlers based on their type.
599+
*
600+
* @param response - The response from the data dialog
601+
*/
602+
function onDataDialogResponse(response: DialogResponse): void {
603+
// The data dialog's beta upload path returns DataOption[] directly
604+
if (isDataOptionArray(response)) {
605+
handleUploadedDataOptions(response);
606+
return;
607+
}
608+
// Handle responses that require validation and transformation
609+
handleIncoming(response, false);
610+
}
611+
612+
/**
613+
* Open file dialog for data selection or upload.
499614
*/
500615
function onBrowse() {
501616
if (currentVariant.value) {
502617
const library = !!currentVariant.value.library;
503618
const multiple = !!currentVariant.value.multiple;
504-
getGalaxyInstance().data.dialog(
505-
(response: Record<string, unknown>) => {
506-
handleIncoming(response, false);
507-
},
508-
{
509-
allowUpload: true,
510-
format: null,
511-
library,
512-
multiple,
513-
},
514-
);
619+
const options = {
620+
allowUpload: true,
621+
format: null,
622+
library,
623+
multiple,
624+
};
625+
getGalaxyInstance().data.dialog(onDataDialogResponse, options);
515626
}
516627
}
517628
@@ -716,6 +827,10 @@ function isHistoryOrCollectionItem(item: EventData): item is HistoryOrCollection
716827
return isHistoryItem(item) || isDCE(item);
717828
}
718829
830+
function isDataOptionArray(value: unknown): value is DataOption[] {
831+
return Array.isArray(value) && value.every((item) => isDataOption(item as object));
832+
}
833+
719834
/**
720835
* Helper function to handle collection type changes safely
721836
*/
@@ -792,7 +907,10 @@ function onDragLeave(evt: DragEvent) {
792907
793908
function onDrop(e: DragEvent) {
794909
if (dragData.value.length) {
795-
if (handleIncoming(dragData.value, dragData.value.length === 1)) {
910+
// Filter to only valid history/collection items
911+
const filteredItems = dragData.value.filter(isHistoryOrCollectionItem) as HistoryOrCollectionItem[];
912+
const partial = filteredItems.length === 1;
913+
if (handleIncoming(filteredItems, partial)) {
796914
currentHighlighting.value = "success";
797915
if (props.workflowRun) {
798916
workflowTab.value = "view";
@@ -895,10 +1013,13 @@ const noOptionsWarningMessage = computed(() => {
8951013
:collection-types="props.collectionTypes"
8961014
:current-source="currentSource || undefined"
8971015
:is-populated="currentValue && currentValue.length > 0"
1016+
:extensions="props.extensions"
1017+
:multiple="Boolean(currentVariant?.multiple)"
8981018
show-field-options
8991019
:show-view-create-options="props.workflowRun && !usingSimpleSelect"
9001020
:workflow-tab.sync="workflowTab"
9011021
@create-collection-type="handleCollectionTypeChange"
1022+
@uploaded-data="handleUploadedDataOptions"
9021023
@on-browse="onBrowse"
9031024
@set-current-field="(value) => (currentField = value)" />
9041025

@@ -946,9 +1067,12 @@ const noOptionsWarningMessage = computed(() => {
9461067
:collection-types="props.collectionTypes"
9471068
:current-source="currentSource || undefined"
9481069
:is-populated="currentValue && currentValue.length > 0"
1070+
:extensions="props.extensions"
1071+
:multiple="Boolean(currentVariant?.multiple)"
9491072
show-view-create-options
9501073
:workflow-tab.sync="workflowTab"
951-
@create-collection-type="handleCollectionTypeChange" />
1074+
@create-collection-type="handleCollectionTypeChange"
1075+
@uploaded-data="handleUploadedDataOptions" />
9521076
</div>
9531077

9541078
<FormDataExtensions

0 commit comments

Comments
 (0)