@@ -28,7 +28,7 @@ import { type EventData, useEventStore } from "@/stores/eventStore";
2828import { orList } from " @/utils/strings" ;
2929
3030import type { DataOption , ExtendedCollectionType } from " ./types" ;
31- import { containsDataOption } from " ./types" ;
31+ import { containsDataOption , isDataOption } from " ./types" ;
3232import { BATCH , SOURCE , VARIANTS } from " ./variants" ;
3333
3434import FormSelection from " ../FormSelection.vue" ;
@@ -38,13 +38,25 @@ import FormDataWorkflowRunTabs from "./FormDataWorkflowRunTabs.vue";
3838import FormSelect from " @/components/Form/Elements/FormSelect.vue" ;
3939import 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+
4155type SelectOption = {
4256 label: string ;
4357 value: DataOption | null ;
4458};
4559
46- type HistoryOrCollectionItem = HistoryItemSummary | DCESummary ;
47-
4860const 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 */
500615function 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
793908function 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