From 42e49954f37693310f24676162b7373cdc744c38 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 29 Dec 2024 22:12:45 -0500 Subject: [PATCH] Implement paired_or_unpaired collections... --- client/src/api/datasetCollections.ts | 13 +- .../Collections/CollectionCreatorModal.vue | 88 +-- .../Collections/ListCollectionCreator.vue | 38 +- .../src/components/Collections/ListWizard.vue | 201 +++++- .../Collections/ListWizard/WhichBuilder.vue | 141 ++-- .../Collections/ListWizard/types.ts | 8 +- .../Collections/PairCollectionCreator.vue | 36 +- .../PairedOrUnpairedListCollectionCreator.vue | 600 ++++++++++++++++++ .../Collections/common/AutoPairing.vue | 157 +++++ .../common/CellDiscardComponent.vue | 35 + .../common/CellStatusComponent.vue | 69 ++ .../Collections/common/CollectionCreator.vue | 29 +- .../common/CollectionNameInput.vue | 5 +- .../common/PairedDatasetCellComponent.vue | 151 +++++ .../common/PairingFilterInputGroup.vue | 65 ++ .../common/useCollectionCreation.ts | 33 + .../common/useCollectionCreator.ts | 64 +- .../Collections/common/usePairingSummary.ts | 33 + .../components/Collections/pairing.test.ts | 23 +- client/src/components/Collections/pairing.ts | 16 +- .../Collection/CollectionDescription.vue | 25 +- .../HistoryOperations/SelectionOperations.vue | 42 +- .../src/components/History/model/queries.ts | 4 +- .../src/components/RuleCollectionBuilder.vue | 44 +- .../modules/collectionTypeDescription.ts | 37 +- .../Workflow/Editor/modules/terminals.test.ts | 66 ++ .../Editor/test-data/parameter_steps.json | 123 ++++ client/src/entry/analysis/router.js | 3 + .../src/stores/collectionBuilderItemsStore.ts | 24 + .../model/dataset_collections/adapters.py | 257 ++++++++ .../model/dataset_collections/registry.py | 2 + .../model/dataset_collections/structure.py | 11 +- .../dataset_collections/subcollections.py | 17 +- .../dataset_collections/type_description.py | 38 +- .../dataset_collections/types/__init__.py | 8 + .../types/collection_semantics.yml | 60 ++ .../model/dataset_collections/types/paired.py | 11 +- .../types/paired_or_unpaired.py | 43 ++ lib/galaxy/schema/schema.py | 2 + lib/galaxy/tool_util/parameters/models.py | 74 +++ lib/galaxy/tools/__init__.py | 57 ++ lib/galaxy/tools/actions/__init__.py | 24 +- lib/galaxy/tools/parameters/basic.py | 59 +- .../tools/parameters/dataset_matcher.py | 1 + lib/galaxy/tools/parameters/history_query.py | 3 +- .../tools/split_paired_and_unpaired.xml | 132 ++++ lib/galaxy/tools/wrappers.py | 21 +- .../api/test_dataset_collections.py | 19 + lib/galaxy_test/api/test_tool_execute.py | 119 ++++ lib/galaxy_test/base/populators.py | 47 ++ .../collection_list_paired_or_unpaired.xml | 88 +++ .../tools/collection_paired_or_unpaired.xml | 68 ++ test/functional/tools/sample_tool_conf.xml | 4 +- .../data/dataset_collections/test_matching.py | 44 ++ .../dataset_collections/test_structure.py | 31 + .../test_type_descriptions.py | 33 +- 56 files changed, 3229 insertions(+), 217 deletions(-) create mode 100644 client/src/components/Collections/PairedOrUnpairedListCollectionCreator.vue create mode 100644 client/src/components/Collections/common/AutoPairing.vue create mode 100644 client/src/components/Collections/common/CellDiscardComponent.vue create mode 100644 client/src/components/Collections/common/CellStatusComponent.vue create mode 100644 client/src/components/Collections/common/PairedDatasetCellComponent.vue create mode 100644 client/src/components/Collections/common/PairingFilterInputGroup.vue create mode 100644 client/src/components/Collections/common/useCollectionCreation.ts create mode 100644 client/src/components/Collections/common/usePairingSummary.ts create mode 100644 lib/galaxy/model/dataset_collections/adapters.py create mode 100644 lib/galaxy/model/dataset_collections/types/paired_or_unpaired.py create mode 100644 lib/galaxy/tools/split_paired_and_unpaired.xml create mode 100644 test/functional/tools/collection_list_paired_or_unpaired.xml create mode 100644 test/functional/tools/collection_paired_or_unpaired.xml diff --git a/client/src/api/datasetCollections.ts b/client/src/api/datasetCollections.ts index f5955dfb1daa..08c53058deff 100644 --- a/client/src/api/datasetCollections.ts +++ b/client/src/api/datasetCollections.ts @@ -85,10 +85,10 @@ export async function fetchElementsFromCollection(params: { }); } -type CollectionElementIdentifiers = components["schemas"]["CollectionElementIdentifier"][]; -type CreateNewCollectionPayload = components["schemas"]["CreateNewCollectionPayload"]; +export type CollectionElementIdentifiers = components["schemas"]["CollectionElementIdentifier"][]; +export type CreateNewCollectionPayload = components["schemas"]["CreateNewCollectionPayload"]; -type NewCollectionOptions = { +export type NewCollectionOptions = { name: string; element_identifiers: CollectionElementIdentifiers; collection_type: string; @@ -104,13 +104,18 @@ export function createCollectionPayload(options: NewCollectionOptions): CreateNe element_identifiers: options.element_identifiers, collection_type: options.collection_type, instance_type: "history", + fields: "auto", copy_elements: options.copy_elements || true, hide_source_items: options.hide_source_items || true, }; } -export async function createHistoryDatasetCollectionInstance(options: NewCollectionOptions) { +export async function createHistoryDatasetCollectionInstanceSimple(options: NewCollectionOptions) { const payload = createCollectionPayload(options); + return createHistoryDatasetCollectionInstanceFull(payload); +} + +export async function createHistoryDatasetCollectionInstanceFull(payload: CreateNewCollectionPayload) { const { data, error } = await GalaxyApi().POST("/api/dataset_collections", { body: payload, }); diff --git a/client/src/components/Collections/CollectionCreatorModal.vue b/client/src/components/Collections/CollectionCreatorModal.vue index e376a50410ea..a196a777d560 100644 --- a/client/src/components/Collections/CollectionCreatorModal.vue +++ b/client/src/components/Collections/CollectionCreatorModal.vue @@ -4,18 +4,18 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { BAlert, BLink, BModal } from "bootstrap-vue"; import { computed, ref, watch } from "vue"; -import type { HDASummary, HistoryItemSummary, HistorySummary } from "@/api"; -import { createDatasetCollection } from "@/components/History/model/queries"; +import type { HistoryItemSummary } from "@/api"; +import { createHistoryDatasetCollectionInstanceFull, type CreateNewCollectionPayload } from "@/api/datasetCollections"; import { useCollectionBuilderItemsStore } from "@/stores/collectionBuilderItemsStore"; import { useHistoryStore } from "@/stores/historyStore"; import localize from "@/utils/localization"; import { orList } from "@/utils/strings"; -import type { CollectionType, DatasetPair } from "../History/adapters/buildCollectionModal"; +import type { CollectionType } from "../History/adapters/buildCollectionModal"; import ListCollectionCreator from "./ListCollectionCreator.vue"; import PairCollectionCreator from "./PairCollectionCreator.vue"; -import PairedListCollectionCreator from "./PairedListCollectionCreator.vue"; +import PairedOrUnpairedListCollectionCreator from "./PairedOrUnpairedListCollectionCreator.vue"; import Heading from "@/components/Common/Heading.vue"; import GenericItem from "@/components/History/Content/GenericItem.vue"; import LoadingSpan from "@/components/LoadingSpan.vue"; @@ -30,6 +30,7 @@ interface Props { fromRulesInput?: boolean; hideModalOnCreate?: boolean; filterText?: string; + useBetaComponents?: boolean; } const props = defineProps(); @@ -141,63 +142,11 @@ const modalTitle = computed(() => { }); // Methods -function createListCollection(elements: HDASummary[], name: string, hideSourceItems: boolean) { - const returnedElems = elements.map((element) => ({ - id: element.id, - name: element.name, - //TODO: this allows for list:list even if the implementation does not - reconcile - src: "src" in element ? element.src : element.history_content_type == "dataset" ? "hda" : "hdca", - })); - return createHDCA(returnedElems, "list", name, hideSourceItems); -} - -function createListPairedCollection(elements: DatasetPair[], name: string, hideSourceItems: boolean) { - const returnedElems = elements.map((pair) => ({ - collection_type: "paired", - src: "new_collection", - name: pair.name, - element_identifiers: [ - { - name: "forward", - id: pair.forward.id, - src: "src" in pair.forward ? pair.forward.src : "hda", - }, - { - name: "reverse", - id: pair.reverse.id, - src: "src" in pair.reverse ? pair.reverse.src : "hda", - }, - ], - })); - return createHDCA(returnedElems, "list:paired", name, hideSourceItems); -} -function createPairedCollection(elements: DatasetPair, name: string, hideSourceItems: boolean) { - const { forward, reverse } = elements; - const returnedElems = [ - { name: "forward", src: "src" in forward ? forward.src : "hda", id: forward.id }, - { name: "reverse", src: "src" in reverse ? reverse.src : "hda", id: reverse.id }, - ]; - return createHDCA(returnedElems, "paired", name, hideSourceItems); -} - -async function createHDCA( - element_identifiers: any[], - collection_type: CollectionType, - name: string, - hide_source_items: boolean, - options = {} -) { +async function createHDCA(payload: CreateNewCollectionPayload) { try { creatingCollection.value = true; - const collection = await createDatasetCollection(history.value as HistorySummary, { - collection_type, - name, - hide_source_items, - element_identifiers, - options, - }); - + const collection = await createHistoryDatasetCollectionInstanceFull(payload); emit("created-collection", collection); createdCollection.value = collection; @@ -293,16 +242,30 @@ function resetModal() { :default-hide-source-items="props.defaultHideSourceItems" :from-selection="fromSelection" :extensions="props.extensions" - @on-create="createListCollection" + mode="modal" + @on-create="createHDCA" @on-cancel="hideModal" /> - + diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index df63e0f82e44..b9aacbc65230 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -9,12 +9,13 @@ import { computed, ref, watch } from "vue"; import draggable from "vuedraggable"; import type { HDASummary, HistoryItemSummary } from "@/api"; +import { type CollectionElementIdentifiers, type CreateNewCollectionPayload } from "@/api/datasetCollections"; import { useConfirmDialog } from "@/composables/confirmDialog"; import { Toast } from "@/composables/toast"; import localize from "@/utils/localization"; import { stripExtension } from "./common/stripExtension"; -import { useCollectionCreator } from "./common/useCollectionCreator"; +import { type Mode, useCollectionCreator } from "./common/useCollectionCreator"; import FormSelectMany from "../Form/Elements/FormSelectMany/FormSelectMany.vue"; import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue"; @@ -28,14 +29,16 @@ interface Props { defaultHideSourceItems?: boolean; fromSelection?: boolean; extensions?: string[]; - showButtons?: boolean; + mode: Mode; } const props = defineProps(); const emit = defineEmits<{ - (e: "on-create", workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean): void; + (e: "on-create", options: CreateNewCollectionPayload): void; (e: "on-cancel"): void; + (e: "name", value: string): void; + (e: "input-valid", value: boolean): void; }>(); const state = ref("build"); @@ -81,7 +84,16 @@ const allElementsAreInvalid = computed(() => { /** If not `fromSelection`, the list of elements that will become the collection */ const inListElements = ref([]); -const { removeExtensions, hideSourceItems, onUpdateHideSourceItems, isElementInvalid } = useCollectionCreator(props); +const { + removeExtensions, + hideSourceItems, + onUpdateHideSourceItems, + isElementInvalid, + collectionName, + onUpdateCollectionName, + onCollectionCreate, + showButtonsForModal, +} = useCollectionCreator(props, emit); // ----------------------------------------------------------------------- process raw list /** set up main data */ @@ -245,7 +257,7 @@ function clickSelectAll() { } const { confirm } = useConfirmDialog(); -async function clickedCreate(collectionName: string) { +async function attemptCreate() { checkForDuplicates(); const returnedElements = props.fromSelection ? workingElements.value : inListElements.value; @@ -261,10 +273,18 @@ async function clickedCreate(collectionName: string) { } if (state.value !== "error" && (atLeastOneElement.value || confirmed)) { - emit("on-create", returnedElements, collectionName, hideSourceItems.value); + const identifiers = returnedElements.map((element) => ({ + id: element.id, + name: element.name, + //TODO: this allows for list:list even if the implementation does not - reconcile + src: "src" in element ? element.src : element.history_content_type == "dataset" ? "hda" : "hdca", + })) as CollectionElementIdentifiers; + onCollectionCreate("list", identifiers); } } +defineExpose({ attemptCreate }); + function checkForDuplicates() { var valid = true; var existingNames: { [key: string]: boolean } = {}; @@ -399,12 +419,14 @@ function renameElement(element: any, name: string) { collection-type="list" :no-items="props.initialElements.length == 0 && !props.fromSelection" :show-upload="!fromSelection" - :show-buttons="showButtons" + :show-buttons="showButtonsForModal" + :collection-name="collectionName" + @on-update-collection-name="onUpdateCollectionName" @add-uploaded-files="addUploadedFiles" @on-update-datatype-toggle="changeDatatypeFilter" @onUpdateHideSourceItems="onUpdateHideSourceItems" @remove-extensions-toggle="removeExtensionsToggle" - @clicked-create="clickedCreate"> + @clicked-create="attemptCreate">