From 0e141738af56976128fa38320a60d97e376ba79e 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 | 89 +- .../Collections/ListCollectionCreator.vue | 75 +- .../src/components/Collections/ListWizard.vue | 215 ++++- .../Collections/ListWizard/WhichBuilder.vue | 141 +++- .../Collections/ListWizard/types.ts | 8 +- .../Collections/PairCollectionCreator.vue | 36 +- .../PairedListCollectionCreator.test.js | 147 ---- .../Collections/PairedOrUnpairedComponents.ts | 13 + .../PairedOrUnpairedListCollectionCreator.vue | 684 ++++++++++++++++ .../PairedOrUnpairedListCreatorHelp.vue | 115 +++ .../Collections/common/AutoPairing.vue | 157 ++++ .../common/CellDiscardComponent.vue | 35 + .../common/CellStatusComponent.vue | 77 ++ .../Collections/common/CollectionCreator.vue | 41 +- .../common/CollectionCreatorHelpHeader.vue | 18 +- .../common/CollectionNameInput.vue | 5 +- .../common/PairedDatasetCellComponent.vue | 151 ++++ .../common/PairingFilterInputGroup.vue | 65 ++ .../Collections/common/stripExtension.ts | 60 ++ .../common/useCollectionCreation.ts | 33 + .../common/useCollectionCreator.ts | 64 +- .../Collections/common/usePairingSummary.ts | 41 + .../components/Collections/pairing.test.ts | 23 +- client/src/components/Collections/pairing.ts | 16 +- .../Form/Elements/FormData/FormData.vue | 14 +- .../Collection/CollectionDescription.vue | 23 +- .../HistoryOperations/SelectionOperations.vue | 42 +- .../History/adapters/buildCollectionModal.ts | 9 +- .../src/components/History/model/queries.ts | 4 +- .../src/components/RuleCollectionBuilder.vue | 44 +- .../Editor/Forms/FormCollectionType.vue | 1 + .../modules/collectionTypeDescription.ts | 88 +- .../Workflow/Editor/modules/terminals.test.ts | 223 ++++- .../Workflow/Editor/modules/terminals.ts | 12 +- .../Editor/test-data/parameter_steps.json | 352 ++++++++ client/src/entry/analysis/router.js | 3 + .../src/stores/collectionBuilderItemsStore.ts | 24 + doc/source/conf.py | 3 +- doc/source/dev/collection_semantics.md | 762 ++++++++++++++++++ doc/source/dev/index.rst | 1 + lib/galaxy/model/__init__.py | 26 +- .../model/dataset_collections/adapters.py | 262 ++++++ .../model/dataset_collections/registry.py | 2 + .../model/dataset_collections/structure.py | 11 +- .../dataset_collections/subcollections.py | 17 +- .../dataset_collections/type_description.py | 36 +- .../dataset_collections/types/__init__.py | 8 + .../types/collection_semantics.yml | 511 ++++++++++-- .../model/dataset_collections/types/paired.py | 11 +- .../types/paired_or_unpaired.py | 43 + .../dataset_collections/types/semantics.py | 208 +++++ 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 | 30 +- 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 | 130 +++ lib/galaxy_test/base/populators.py | 57 ++ .../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 | 51 ++ .../dataset_collections/test_structure.py | 31 + .../test_type_descriptions.py | 41 +- 70 files changed, 5457 insertions(+), 473 deletions(-) delete mode 100644 client/src/components/Collections/PairedListCollectionCreator.test.js create mode 100644 client/src/components/Collections/PairedOrUnpairedComponents.ts create mode 100644 client/src/components/Collections/PairedOrUnpairedListCollectionCreator.vue create mode 100644 client/src/components/Collections/PairedOrUnpairedListCreatorHelp.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 doc/source/dev/collection_semantics.md 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/model/dataset_collections/types/semantics.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..18689f2e317b 100644 --- a/client/src/components/Collections/CollectionCreatorModal.vue +++ b/client/src/components/Collections/CollectionCreatorModal.vue @@ -4,18 +4,19 @@ 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 { type SupportedPairedOrPairedBuilderCollectionTypes } from "./common/useCollectionCreator"; 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 +31,7 @@ interface Props { fromRulesInput?: boolean; hideModalOnCreate?: boolean; filterText?: string; + useBetaComponents?: boolean; } const props = defineProps(); @@ -67,6 +69,15 @@ const historyDatasets = computed(() => { return []; } }); +const pairedOrUnpairedSupportedCollectionType = computed(() => { + if ( + ["list:paired", "list:list", "list:paired_or_unpaired", "list:list:paired"].indexOf(props.collectionType) !== -1 + ) { + return props.collectionType as SupportedPairedOrPairedBuilderCollectionTypes; + } else { + return null; + } +}); /** Flag for the initial fetch of history items */ const initialFetch = ref(false); @@ -141,63 +152,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 +252,19 @@ 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..cb8377eda4fd 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 { stripExtension, useUpdateIdentifiersForRemoveExtensions } from "./common/stripExtension"; +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"); @@ -45,13 +48,7 @@ const workingElements = ref([]); const selectedDatasetElements = ref([]); const atLeastOneElement = ref(true); -const initialElementsById = computed(() => { - const byId = {} as Record; - for (const initialElement of props.initialElements) { - byId[initialElement.id] = initialElement; - } - return byId; -}); +const { updateIdentifierIfUnchanged } = useUpdateIdentifiersForRemoveExtensions(props); const atLeastOneDatasetIsSelected = computed(() => { return selectedDatasetElements.value.length > 0; @@ -81,7 +78,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 */ @@ -161,29 +167,11 @@ function _validateElements() { } function removeExtensionsToggle() { - const byId = initialElementsById.value; - removeExtensions.value = !removeExtensions.value; - if (removeExtensions.value) { - workingElements.value.forEach((el) => { - const oName = byId[el.id]?.name; - if (oName && el.name == oName) { - el.name = stripExtension(oName); - } - }); - } else { - workingElements.value.forEach((el) => { - const originalName = byId[el.id]?.name; - console.log(originalName); - if (originalName) { - const strippedOriginalName = stripExtension(originalName); - if (strippedOriginalName && el.name == strippedOriginalName) { - console.log("restoring... to" + originalName); - el.name = originalName; - } - } - }); - } + const removeExtensionsValue = removeExtensions.value; + workingElements.value.forEach((el) => { + updateIdentifierIfUnchanged(el, removeExtensionsValue); + }); _mangleDuplicateNames(); } @@ -245,7 +233,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 +249,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 +395,15 @@ 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" + :mode="mode" + @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">