diff --git a/client/src/api/datasetCollections.ts b/client/src/api/datasetCollections.ts index ec8a8ee4811..52daf47b5f8 100644 --- a/client/src/api/datasetCollections.ts +++ b/client/src/api/datasetCollections.ts @@ -14,6 +14,14 @@ const DEFAULT_LIMIT = 50; export type CollectionType = string; +export type SampleSheetCollectionType = + | "sample_sheet" + | "sample_sheet:paired" + | "sample_sheet:paired_or_unpaired" + | "sample_sheet:record"; +// mirror the python definition here +export type SampleSheetColumnValueT = string | number | boolean; + /** * Fetches the details of a collection. * @param params.id The ID of the collection (HDCA) to fetch. diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 9702b8d2076..d97edbb4a2c 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -327,6 +327,10 @@ export type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskRe export type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"]; export type ExportObjectResultMetadata = components["schemas"]["ExportObjectResultMetadata"]; +export type SampleSheetColumnDefinition = components["schemas"]["SampleSheetColumnDefinitionModel"]; +export type SampleSheetColumnDefinitionType = SampleSheetColumnDefinition["type"]; +export type SampleSheetColumnDefinitions = SampleSheetColumnDefinition[] | null; + export type AsyncTaskResultSummary = components["schemas"]["AsyncTaskResultSummary"]; export type CollectionElementIdentifiers = components["schemas"]["CollectionElementIdentifier"][]; diff --git a/client/src/components/Collections/CollectionCreatorIndex.vue b/client/src/components/Collections/CollectionCreatorIndex.vue index 0fe7d82d621..eb88736bcec 100644 --- a/client/src/components/Collections/CollectionCreatorIndex.vue +++ b/client/src/components/Collections/CollectionCreatorIndex.vue @@ -5,7 +5,8 @@ import { BAlert, BLink, BModal } from "bootstrap-vue"; import { computed, ref, watch } from "vue"; import { type CreateNewCollectionPayload, type HDCASummary, type HistoryItemSummary, isHDCA } from "@/api"; -import { createHistoryDatasetCollectionInstanceFull } from "@/api/datasetCollections"; +import { createHistoryDatasetCollectionInstanceFull, type SampleSheetCollectionType } from "@/api/datasetCollections"; +import type { ExtendedCollectionType } from "@/components/Form/Elements/FormData/types"; import { useCollectionBuilderItemsStore } from "@/stores/collectionBuilderItemsStore"; import { useHistoryItemsStore } from "@/stores/historyItemsStore"; import { useHistoryStore } from "@/stores/historyStore"; @@ -19,6 +20,7 @@ import type { SupportedPairedOrPairedBuilderCollectionTypes } from "./common/use import ListCollectionCreator from "./ListCollectionCreator.vue"; import PairCollectionCreator from "./PairCollectionCreator.vue"; import PairedOrUnpairedListCollectionCreator from "./PairedOrUnpairedListCollectionCreator.vue"; +import SampleSheetCollectionCreator from "./SampleSheetCollectionCreator.vue"; import Heading from "@/components/Common/Heading.vue"; import GenericItem from "@/components/History/Content/GenericItem.vue"; import LoadingSpan from "@/components/LoadingSpan.vue"; @@ -27,6 +29,7 @@ interface Props { historyId: string; show: boolean; collectionType: CollectionBuilderType; + extendedCollectionType: ExtendedCollectionType; selectedItems?: HistoryItemSummary[]; defaultHideSourceItems?: boolean; extensions?: string[]; @@ -231,6 +234,13 @@ function redrawCreator() { } } +const sampleSheetType = computed(() => { + if (!props.collectionType.startsWith("sample_sheet")) { + return null; + } + return props.collectionType as SampleSheetCollectionType; +}); + defineExpose({ redrawCreator }); @@ -329,6 +339,17 @@ defineExpose({ redrawCreator }); mode="modal" @on-cancel="hideCreator" @on-create="createHDCA" /> + diff --git a/client/src/components/Collections/SampleSheetCollectionCreator.vue b/client/src/components/Collections/SampleSheetCollectionCreator.vue new file mode 100644 index 00000000000..97b54353a78 --- /dev/null +++ b/client/src/components/Collections/SampleSheetCollectionCreator.vue @@ -0,0 +1,28 @@ + + + diff --git a/client/src/components/Collections/SampleSheetWizard.vue b/client/src/components/Collections/SampleSheetWizard.vue new file mode 100644 index 00000000000..05ade878cd5 --- /dev/null +++ b/client/src/components/Collections/SampleSheetWizard.vue @@ -0,0 +1,507 @@ + + + + + diff --git a/client/src/components/Collections/common/AutoPairing.vue b/client/src/components/Collections/common/AutoPairing.vue index 9435997943c..bd59964b85d 100644 --- a/client/src/components/Collections/common/AutoPairing.vue +++ b/client/src/components/Collections/common/AutoPairing.vue @@ -3,6 +3,7 @@ import { BButton } from "bootstrap-vue"; import { computed, ref } from "vue"; import type { HistoryItemSummary } from "@/api"; +import type { HasName } from "@/components/Collections/pairing"; import localize from "@/utils/localization"; import { useExtensionFiltering } from "./useExtensionFilter"; @@ -10,9 +11,16 @@ import { usePairingSummary } from "./usePairingSummary"; import PairingFilterInputGroup from "./PairingFilterInputGroup.vue"; +type ElementType = HistoryItemSummary | HasName; +type ElementsType = HistoryItemSummary[] | HasName[]; + interface Props { - elements: HistoryItemSummary[]; - collectionType: "list:paired" | "list:paired_or_unpaired"; + elements: ElementsType; + collectionType: + | "list:paired" + | "list:paired_or_unpaired" + | "sample_sheet:paired" + | "sample_sheet:paired_or_unpaired"; forwardFilter?: string; reverseFilter?: string; removeExtensions: boolean; @@ -31,7 +39,7 @@ const props = defineProps(); const currentForwardFilter = ref(props.forwardFilter || ""); const currentReverseFilter = ref(props.reverseFilter || ""); -const { currentSummary, summaryText, autoPair } = usePairingSummary(props); +const { currentSummary, summaryText, autoPair } = usePairingSummary(props); const { showElementExtension } = useExtensionFiltering(props); @@ -60,6 +68,14 @@ const whereIsTheBuilder = computed(() => { } }); +function getHid(element: ElementType): string { + if ("hid" in element) { + return element.hid.toString(); + } else { + return ""; + } +} + function onApply() { emit("on-apply", currentForwardFilter.value, currentReverseFilter.value); } @@ -84,9 +100,9 @@ function onApply() {
  • , {{ pair.name }} (FORWARD{{ pair.forward.hid }}: {{ getHid(pair.forward) }}: {{ pair.forward.name }} | REVERSE{{ pair.reverse.hid }}: {{ getHid(pair.reverse) }}: {{ pair.reverse.name }})
  • @@ -96,7 +112,7 @@ function onApply() {
    These datasets were not paired automatically. This builder will allow you to match any of pairs of these manually {{ whereIsTheBuilder }}. - + All unmatched datasets will not be included in the final list of paired datasets. @@ -106,10 +122,14 @@ function onApply() {
    1. , - {{ unpairedDataset.hid }}: + {{ getHid(unpairedDataset) }}: {{ unpairedDataset.name }} ( {{ unpairedDataset.extension }} diff --git a/client/src/components/Collections/common/usePairingSummary.ts b/client/src/components/Collections/common/usePairingSummary.ts index 07333b2633d..6e1ad76dc8b 100644 --- a/client/src/components/Collections/common/usePairingSummary.ts +++ b/client/src/components/Collections/common/usePairingSummary.ts @@ -5,7 +5,10 @@ import { type AutoPairingResult, type HasName, splitIntoPairedAndUnpaired } from import type { SupportedPairedOrPairedBuilderCollectionTypes } from "./useCollectionCreator"; interface PropsWithCollectionType { - collectionType: SupportedPairedOrPairedBuilderCollectionTypes; + collectionType: + | SupportedPairedOrPairedBuilderCollectionTypes + | "sample_sheet:paired" + | "sample_sheet:paired_or_unpaired"; } export function usePairingSummary(props: PropsWithCollectionType) { diff --git a/client/src/components/Collections/sheet/DownloadWorkbookButton.vue b/client/src/components/Collections/sheet/DownloadWorkbookButton.vue new file mode 100644 index 00000000000..aec1084ec9e --- /dev/null +++ b/client/src/components/Collections/sheet/DownloadWorkbookButton.vue @@ -0,0 +1,22 @@ + + + diff --git a/client/src/components/Collections/sheet/SampleSheetGrid.vue b/client/src/components/Collections/sheet/SampleSheetGrid.vue new file mode 100644 index 00000000000..6d320861ae3 --- /dev/null +++ b/client/src/components/Collections/sheet/SampleSheetGrid.vue @@ -0,0 +1,645 @@ + + + + + diff --git a/client/src/components/Collections/sheet/workbooks.ts b/client/src/components/Collections/sheet/workbooks.ts new file mode 100644 index 00000000000..9d8895338ee --- /dev/null +++ b/client/src/components/Collections/sheet/workbooks.ts @@ -0,0 +1,105 @@ +import { GalaxyApi, type SampleSheetColumnDefinition, type SampleSheetColumnDefinitions } from "@/api"; +import type { SampleSheetCollectionType } from "@/api/datasetCollections"; +import { stripExtension } from "@/components/Collections/common/stripExtension"; +import type { PrefixColumnsType } from "@/components/Collections/wizard/types"; +import { withPrefix } from "@/utils/redirect"; + +export function getDownloadWorkbookUrl( + columnDefinitions: SampleSheetColumnDefinitions, + collectionType: SampleSheetCollectionType, + initialRows?: string[][] +) { + const columnDefinitionsJson = JSON.stringify(columnDefinitions); + const columnDefinitionsJsonBase64 = Buffer.from(columnDefinitionsJson).toString("base64"); + let url = withPrefix( + `/api/sample_sheet_workbook/generate?collection_type=${collectionType}&column_definitions=${columnDefinitionsJsonBase64}` + ); + if (initialRows) { + const initialRowsJson = JSON.stringify(initialRows); + const initialRowsJsonBase64 = Buffer.from(initialRowsJson).toString("base64"); + url = `${url}&prefix_values=${initialRowsJsonBase64}`; + } + return url; +} + +export function getDownloadWorkbookUrlForCollection(column_definitions: SampleSheetColumnDefinitions, hdca_id: string) { + const columnDefinitionsJson = JSON.stringify(column_definitions); + const columnDefinitionsJsonBase64 = Buffer.from(columnDefinitionsJson).toString("base64"); + const url = withPrefix( + `/api/dataset_collections/${hdca_id}/sample_sheet_workbook/generate?column_definitions=${columnDefinitionsJsonBase64}` + ); + return url; +} + +export function downloadWorkbook( + columnDefinitions: SampleSheetColumnDefinitions, + collectionType: SampleSheetCollectionType, + initialRows?: string[][] +) { + const url = getDownloadWorkbookUrl(columnDefinitions, collectionType, initialRows); + window.location.assign(url); +} + +export function downloadWorkbookForCollection(columnDefinitions: SampleSheetColumnDefinitions, hdca_id: string) { + const url = getDownloadWorkbookUrlForCollection(columnDefinitions, hdca_id); + window.location.assign(url); +} + +export function initialValue(columnDefinition: SampleSheetColumnDefinition) { + const defaultValue = columnDefinition.default_value; + if (defaultValue === undefined) { + switch (columnDefinition.type) { + case "int": + return columnDefinition.optional ? null : 0; + case "float": + return columnDefinition.optional ? null : 0.0; + case "boolean": + // TODO!!! + return false; + case "string": + default: + return ""; + } + } else { + return defaultValue; + } +} + +export function parseWorkbook( + collectionType: SampleSheetCollectionType, + columnDefinitions: SampleSheetColumnDefinitions | undefined, + prefixColumnTypes: PrefixColumnsType, + base64Content: string +) { + const parseBody = { + collection_type: collectionType, + column_definitions: columnDefinitions || [], + content: base64Content, + prefix_columns_type: prefixColumnTypes, + }; + return GalaxyApi().POST("/api/sample_sheet_workbook/parse", { + body: parseBody, + }); +} + +export function withAutoListIdentifiers(rows: string[][]): string[][] { + const seenBasenames = new Map(); + return rows.map((row) => { + console.log(row); + if (row.length === 1 && row[0]) { + const uri = row[0]; + let basename = uri.split("/").pop() || uri; + + if (seenBasenames.has(basename)) { + const count = seenBasenames.get(basename)! + 1; + seenBasenames.set(basename, count); + basename = `${basename}_${count}`; + } else { + seenBasenames.set(basename, 1); + } + + return [uri, stripExtension(basename)]; + } + return row; + }); +} diff --git a/client/src/components/Collections/usePairing.ts b/client/src/components/Collections/usePairing.ts index 5bf4d27b2bc..25f990711bb 100644 --- a/client/src/components/Collections/usePairing.ts +++ b/client/src/components/Collections/usePairing.ts @@ -1,26 +1,32 @@ import { ref } from "vue"; +import type { GenericPair } from "@/components/History/adapters/buildCollectionModal"; + import { autoPairWithCommonFilters, type HasName } from "./pairing"; import AutoPairing from "./common/AutoPairing.vue"; -export function useAutoPairing() { +export function useAutoPairing() { const currentForwardFilter = ref(""); const currentReverseFilter = ref(""); const countPaired = ref(-1); const countUnpaired = ref(-1); + const pairs = ref[]>(); + const unpaired = ref(); function onFilters(forwardFilter: string, reverseFilter: string) { currentForwardFilter.value = forwardFilter; currentReverseFilter.value = reverseFilter; } - function autoPair(selectedItems: HasName[]) { - const summary = autoPairWithCommonFilters(selectedItems, true); - currentForwardFilter.value = summary.forwardFilter || ""; - currentReverseFilter.value = summary.reverseFilter || ""; - countPaired.value = summary.pairs?.length || 0; - countUnpaired.value = summary.unpaired.length; + function autoPair(selectedItems: T[]) { + const thisSummary = autoPairWithCommonFilters(selectedItems, true); + pairs.value = thisSummary.pairs; + unpaired.value = thisSummary.unpaired; + currentForwardFilter.value = thisSummary.forwardFilter || ""; + currentReverseFilter.value = thisSummary.reverseFilter || ""; + countPaired.value = thisSummary.pairs?.length || 0; + countUnpaired.value = thisSummary.unpaired.length; } return { @@ -31,5 +37,7 @@ export function useAutoPairing() { currentForwardFilter, currentReverseFilter, onFilters, + pairs, + unpaired, }; } diff --git a/client/src/components/Collections/wizard/SelectCollection.vue b/client/src/components/Collections/wizard/SelectCollection.vue new file mode 100644 index 00000000000..7de1c7ef51e --- /dev/null +++ b/client/src/components/Collections/wizard/SelectCollection.vue @@ -0,0 +1,53 @@ + + + diff --git a/client/src/components/Collections/wizard/SourceFromCollection.vue b/client/src/components/Collections/wizard/SourceFromCollection.vue new file mode 100644 index 00000000000..6025089b31a --- /dev/null +++ b/client/src/components/Collections/wizard/SourceFromCollection.vue @@ -0,0 +1,29 @@ + + + diff --git a/client/src/components/Collections/wizard/UploadSampleSheet.vue b/client/src/components/Collections/wizard/UploadSampleSheet.vue new file mode 100644 index 00000000000..42ac715c8cf --- /dev/null +++ b/client/src/components/Collections/wizard/UploadSampleSheet.vue @@ -0,0 +1,39 @@ + + + diff --git a/client/src/components/Collections/wizard/fetchWorkbooks.test.ts b/client/src/components/Collections/wizard/fetchWorkbooks.test.ts index 1cc0946d842..8c615a62978 100644 --- a/client/src/components/Collections/wizard/fetchWorkbooks.test.ts +++ b/client/src/components/Collections/wizard/fetchWorkbooks.test.ts @@ -1,5 +1,6 @@ -import { forBuilder } from "./fetchWorkbooks"; -import type { ParsedFetchWorkbook } from "./types"; +import { columnTitleToTargetType, forBuilder } from "./fetchWorkbooks"; +import SPECIFICATIONS from "./rule_target_column_specification.yml"; +import type { ColumnMappingType, ParsedFetchWorkbook } from "./types"; describe("forBuilder", () => { it("should return the correct ForBuilderResponse for a valid ParsedFetchWorkbook", () => { @@ -31,3 +32,23 @@ describe("forBuilder", () => { ]); }); }); + +interface SpecificationTest { + doc?: string; + column_header: string; + maps_to: ColumnMappingType | null; +} + +describe("column name to rule builder mapping targets", () => { + it("should follow the specifications laid out in rule_target_column_specification.yml", () => { + SPECIFICATIONS.forEach((spec: SpecificationTest) => { + const { column_header, maps_to } = spec; + const columnType = columnTitleToTargetType(column_header); + if (maps_to === null) { + expect(columnType).toBeUndefined(); + } else { + expect(columnType).toBe(maps_to); + } + }); + }); +}); diff --git a/client/src/components/Collections/wizard/fetchWorkbooks.ts b/client/src/components/Collections/wizard/fetchWorkbooks.ts index 64ac80a2350..774e1668f8b 100644 --- a/client/src/components/Collections/wizard/fetchWorkbooks.ts +++ b/client/src/components/Collections/wizard/fetchWorkbooks.ts @@ -77,3 +77,60 @@ function buildInitialMapping(parsedWorkbook: ParsedFetchWorkbook): RuleBuilderMa } return columnMappings; } + +const COLUMN_TITLE_PREFIXES: Record = { + name: "name", + listname: "collection_name", + collectionname: "collection_name", + uri: "url", + url: "url", + urldeferred: "url_deferred", + deferredurl: "url_deferred", + genome: "dbkey", + dbkey: "dbkey", + filetype: "file_type", + extension: "file_type", + info: "info", + tag: "tags", + grouptag: "group_tags", + nametag: "name_tag", + listidentifier: "list_identifiers", + pairedidentifier: "paired_identifier", + hashmd5sum: "hash_md5", + hashmd5: "hash_md5", + md5sum: "hash_md5", + md5: "hash_md5", + sha1hash: "hash_sha1", + hashsha1sum: "hash_sha1", + hashsha1: "hash_sha1", + sha1sum: "hash_sha1", + sha1: "hash_sha1", + sha256hash: "hash_sha256", + hashsha256sum: "hash_sha256", + hashsha256: "hash_sha256", + sha256sum: "hash_sha256", + sha256: "hash_sha256", + sha512hash: "hash_sha512", + hashsha512sum: "hash_sha512", + hashsha512: "hash_sha512", + sha512sum: "hash_sha512", + sha512: "hash_sha512", +}; + +export function columnTitleToTargetType(columnTitle: string): ColumnMappingType | undefined { + let normalizedTitle = columnTitle.toLowerCase().replace(/[\s()\-_]|optional/g, ""); + if (!(normalizedTitle in COLUMN_TITLE_PREFIXES)) { + for (const key of Object.keys(COLUMN_TITLE_PREFIXES)) { + if (normalizedTitle.startsWith(key) || normalizedTitle.endsWith(key)) { + normalizedTitle = key; + break; + } + } + } + + if (!(normalizedTitle in COLUMN_TITLE_PREFIXES)) { + return undefined; + } + + return COLUMN_TITLE_PREFIXES[normalizedTitle] as string; +} diff --git a/client/src/components/Collections/wizard/rule_target_column_specification.yml b/client/src/components/Collections/wizard/rule_target_column_specification.yml new file mode 120000 index 00000000000..31c07343c38 --- /dev/null +++ b/client/src/components/Collections/wizard/rule_target_column_specification.yml @@ -0,0 +1 @@ +../../../../../lib/galaxy/model/dataset_collections/rule_target_column_specification.yml \ No newline at end of file diff --git a/client/src/components/Collections/wizard/types.ts b/client/src/components/Collections/wizard/types.ts index 7ce84043291..1fa99985a6f 100644 --- a/client/src/components/Collections/wizard/types.ts +++ b/client/src/components/Collections/wizard/types.ts @@ -1,3 +1,4 @@ +import type { HDCADetailed } from "@/api"; import type { components } from "@/api/schema"; import type { MAPPING_TARGETS } from "@/components/RuleBuilder/rule-definitions"; @@ -15,6 +16,11 @@ export type ParsedFetchWorkbookForCollectionCollectionType = components["schemas"]["ParsedFetchWorkbookForCollections"]["collection_type"]; export type RawRowData = string[][]; +export type ParsedSampleSheetWorkbook = components["schemas"]["ParsedWorkbook"]; +export type ParsedWorkbookForCollection = components["schemas"]["ParsedWorkbookForCollection"]; +export type AnyParsedSampleSheetWorkbook = ParsedSampleSheetWorkbook | ParsedWorkbookForCollection; +export type InitialElements = RawRowData | HDCADetailed | ParsedSampleSheetWorkbook | ParsedWorkbookForCollection; +export type PrefixColumnsType = "URI" | "ModelObjects"; // types and helpers around initializing the rule builder with data export type RuleSelectionType = "raw" | "remote_files"; diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index b00ad32109e..f280fa660f1 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -26,7 +26,7 @@ import { useUid } from "@/composables/utils/uid"; import { type EventData, useEventStore } from "@/stores/eventStore"; import { orList } from "@/utils/strings"; -import type { DataOption } from "./types"; +import type { DataOption, ExtendedCollectionType } from "./types"; import { containsDataOption } from "./types"; import { BATCH, SOURCE, VARIANTS } from "./variants"; @@ -60,6 +60,7 @@ const props = withDefaults( tag?: string; userDefinedTitle?: string; workflowRun?: boolean; + extendedCollectionType: ExtendedCollectionType; }>(), { loading: false, @@ -72,6 +73,7 @@ const props = withDefaults( flavor: undefined, tag: undefined, userDefinedTitle: undefined, + extendedCollectionType: () => ({} as ExtendedCollectionType), } ); @@ -628,6 +630,10 @@ const collectionTypesWithBuilders: CollectionBuilderType[] = [ "list:list", "list:list:paired", "list:paired_or_unpaired", + "sample_sheet", + "sample_sheet:paired", + "sample_sheet:paired_or_unpaired", + "sample_sheet:record", ]; /** Allowed collection types for collection creation */ @@ -932,6 +938,7 @@ const noOptionsWarningMessage = computed(() => { :can-browse="canBrowse" :extensions="props.extensions" :collection-type="currentCollectionTypeTab" + :extended-collection-type="extendedCollectionType" :step-title="props.userDefinedTitle" :workflow-tab.sync="workflowTab" @focus="$emit('focus')" diff --git a/client/src/components/Form/Elements/FormData/FormDataWorkflowRunTabs.vue b/client/src/components/Form/Elements/FormData/FormDataWorkflowRunTabs.vue index 7edfe198196..e75798bebbe 100644 --- a/client/src/components/Form/Elements/FormData/FormDataWorkflowRunTabs.vue +++ b/client/src/components/Form/Elements/FormData/FormDataWorkflowRunTabs.vue @@ -9,7 +9,7 @@ import type { CollectionBuilderType } from "@/components/History/adapters/buildC import { useUploadConfigurations } from "@/composables/uploadConfigurations"; import { useHistoryStore } from "@/stores/historyStore"; -import type { DataOption } from "./types"; +import type { DataOption, ExtendedCollectionType } from "./types"; import type { VariantInterface } from "./variants"; import CollectionCreatorIndex from "@/components/Collections/CollectionCreatorIndex.vue"; @@ -31,6 +31,7 @@ const props = defineProps<{ collectionType?: CollectionBuilderType; stepTitle?: string; workflowTab: string; + extendedCollectionType: ExtendedCollectionType; }>(); const emit = defineEmits<{ @@ -141,6 +142,7 @@ watch( not-modal :extensions="props.extensions && props.extensions.filter((ext) => ext !== 'data')" :suggested-name="props.stepTitle" + :extended-collection-type="extendedCollectionType" @created-collection="collectionCreated" @on-hide="goToFirstWorkflowTab" />
    diff --git a/client/src/components/Form/Elements/FormData/collections.ts b/client/src/components/Form/Elements/FormData/collections.ts index 1bee57b93fe..a4378fa0cba 100644 --- a/client/src/components/Form/Elements/FormData/collections.ts +++ b/client/src/components/Form/Elements/FormData/collections.ts @@ -14,6 +14,14 @@ export function buildersForCollectionType(collectionType: CollectionType): Colle return ["list:paired"]; } else if (collectionType == "list:paired_or_unpaired") { return ["list", "list:paired", "list:paired_or_unpaired"]; + } else if (collectionType == "sample_sheet") { + return ["sample_sheet"]; + } else if (collectionType == "sample_sheet:paired") { + return ["sample_sheet:paired"]; + } else if (collectionType == "sample_sheet:paired_or_unpaired") { + return ["sample_sheet:paired_or_unpaired"]; + } else if (collectionType == "sample_sheet:record") { + return ["sample_sheet:record"]; } else { return []; } diff --git a/client/src/components/Form/Elements/FormData/types.ts b/client/src/components/Form/Elements/FormData/types.ts index 18789ed40ab..ce098473fb4 100644 --- a/client/src/components/Form/Elements/FormData/types.ts +++ b/client/src/components/Form/Elements/FormData/types.ts @@ -2,6 +2,7 @@ * The Uri types here are based on `DataOrCollectionRequest` defined in * `lib/galaxy/tool_util_models/parameters.py`. */ +import type { FieldDict, SampleSheetColumnDefinition } from "@/api"; interface DatasetHash { hash_function: "MD5" | "SHA-1" | "SHA-256" | "SHA-512"; @@ -101,3 +102,8 @@ export function itemUniqueKey(item: DataOption): string { export function containsDataOption(items: DataOption[], item: DataOption | null): boolean { return item !== null && items.some((i) => itemUniqueKey(i) === itemUniqueKey(item)); } + +export type ExtendedCollectionType = { + columnDefinitions?: SampleSheetColumnDefinition[] | undefined; + fields?: FieldDict[] | undefined; +}; diff --git a/client/src/components/Form/FormElement.vue b/client/src/components/Form/FormElement.vue index 22368634a6c..8af32ecd5f6 100644 --- a/client/src/components/Form/FormElement.vue +++ b/client/src/components/Form/FormElement.vue @@ -9,7 +9,7 @@ import { computed, ref, useAttrs } from "vue"; import { linkify } from "@/utils/utils"; -import { isDataUri } from "./Elements/FormData/types"; +import { type ExtendedCollectionType, isDataUri } from "./Elements/FormData/types"; import type { FormParameterAttributes, FormParameterTypes, FormParameterValue } from "./parameterTypes"; import FormBoolean from "./Elements/FormBoolean.vue"; @@ -267,6 +267,14 @@ function addTempFocus() { function onAlert(value: string | undefined) { formAlert.value = value; } + +const extendedCollectionType = computed(() => { + const attrsValue = attrs.value; + return { + columnDefinitions: attrsValue.column_definitions ?? undefined, + fields: attrsValue.fields ?? undefined, + }; +});