Skip to content

[25.0] More fixes to FormData drag and drop and typing #19900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: release_25.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions client/src/components/Collections/CollectionCreatorIndex.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BLink, BModal } from "bootstrap-vue";
import { computed, ref, watch } from "vue";

import type { CreateNewCollectionPayload, HistoryItemSummary } from "@/api";
import { type CreateNewCollectionPayload, type HDCASummary, type HistoryItemSummary, isHDCA } from "@/api";
import { createHistoryDatasetCollectionInstanceFull } from "@/api/datasetCollections";
import { useCollectionBuilderItemsStore } from "@/stores/collectionBuilderItemsStore";
import { useHistoryItemsStore } from "@/stores/historyItemsStore";
Expand Down Expand Up @@ -40,7 +40,7 @@ interface Props {
const props = defineProps<Props>();

const emit = defineEmits<{
(e: "created-collection", collection: any): void;
(e: "created-collection", collection: HDCASummary): void;
(e: "update:show", show: boolean): void;
(e: "on-hide"): void;
}>();
Expand Down Expand Up @@ -158,9 +158,13 @@ const modalTitle = computed(() => {

const historyItemsStore = useHistoryItemsStore();
/** The created collection accessed from the history items store */
const createdCollectionInHistory = computed(() => {
const createdCollectionInHistory = computed<HDCASummary | undefined>(() => {
const historyItems = historyItemsStore.getHistoryItems(props.historyId, "");
return historyItems.find((item) => item.id === createdCollection.value?.id);
const createdCollectionItem = historyItems.find((item) => item.id === createdCollection.value?.id);
if (createdCollectionItem && isHDCA(createdCollectionItem)) {
return createdCollectionItem;
}
return undefined;
});
/** If the created collection has achieved a terminal state */
const createdCollectionInReadyState = computed(
Expand Down Expand Up @@ -189,7 +193,7 @@ async function createHDCA(payload: CreateNewCollectionPayload) {
watch(
() => createdCollectionInReadyState.value,
(stateReady) => {
if (stateReady) {
if (stateReady && createdCollectionInHistory.value) {
emit("created-collection", createdCollectionInHistory.value);
}
}
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Form/Elements/FormData/FormData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ describe("FormData", () => {
dispatchEvent(wrapper, "dragenter");
dispatchEvent(wrapper, "drop");
expect(wrapper.emitted().alert[0][0]).toEqual(
"paired dataset collection is not a valid input for list type dataset collection parameter."
"dataset pair dataset collection is not a valid input for list type dataset collection parameter."
);
});

Expand All @@ -423,7 +423,7 @@ describe("FormData", () => {
});
dispatchEvent(wrapper, "dragenter");
dispatchEvent(wrapper, "drop");
expect(wrapper.emitted().alert[0][0]).toEqual(undefined);
expect(wrapper.emitted().alert).toBeUndefined();
});

it("linked and unlinked batch mode handling", async () => {
Expand Down
83 changes: 65 additions & 18 deletions client/src/components/Form/Elements/FormData/FormData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type DCESummary,
type HDAObject,
type HistoryItemSummary,
isCollectionElement,
isDatasetElement,
isDCE,
isHDCA,
Expand All @@ -16,7 +17,10 @@ import {
import type { CollectionType } from "@/api/datasetCollections";
import type { HistoryContentType } from "@/api/datasets";
import { getGalaxyInstance } from "@/app";
import type { CollectionBuilderType } from "@/components/History/adapters/buildCollectionModal";
import {
COLLECTION_TYPE_TO_LABEL,
type CollectionBuilderType,
} from "@/components/History/adapters/buildCollectionModal";
import { useDatatypesMapper } from "@/composables/datatypesMapper";
import { useUid } from "@/composables/utils/uid";
import { type EventData, useEventStore } from "@/stores/eventStore";
Expand Down Expand Up @@ -103,11 +107,21 @@ const restrictsExtensions = computed(() => {
/** Store options which need to be preserved **/
const keepOptions: Record<string, SelectOption> = {};

/** Update counter for keep-options, used to trigger a re-compute of `formattedOptions` */
const keepOptionsUpdate = ref(0);

/**
* Determine whether the file dialog can be used or not
*/
const canBrowse = computed(() => variant.value && !!variant.value.find((v) => v.src === SOURCE.DATASET));

/**
* A list of valid `src`s for the variant
*/
const validSrcs = computed(() => {
return variant.value ? variant.value.map((v) => v.src) : [];
});

/**
* Provides the currently shown source type
*/
Expand Down Expand Up @@ -164,6 +178,9 @@ const currentVariant = computed(() => {
* Converts and populates options for the shown select field
*/
const formattedOptions = computed(() => {
// When the keepOptionsUpdate value changes, this computed property is re-evaluated
keepOptionsUpdate.value;

if (currentSource.value && currentSource.value in props.options) {
// Map incoming values to available options
const options = props.options[currentSource.value] || [];
Expand Down Expand Up @@ -254,6 +271,7 @@ const usingSimpleSelect = computed(
*/
function clearHighlighting(timeout = 1000) {
setTimeout(() => {
$emit("alert", undefined);
currentHighlighting.value = null;
}, timeout);
}
Expand Down Expand Up @@ -354,8 +372,7 @@ function handleIncoming(incoming: Record<string, unknown> | Record<string, unkno
}
if (
values.some((v) => {
const { historyContentType } = getSrcAndContentType(v);
const collectionType = "collection_type" in v && v.collection_type ? v.collection_type : undefined;
const { historyContentType, collectionType } = getElementAttributes(v);
return !canAcceptSrc(historyContentType, collectionType);
})
) {
Expand All @@ -373,11 +390,13 @@ function handleIncoming(incoming: Record<string, unknown> | Record<string, unkno
const keepKey = `${newValue.id}_${newValue.src}`;
const existingOptions = props.options && props.options[newValue.src];
const foundOption = existingOptions && existingOptions.find((option) => option.id === newValue.id);
if (!foundOption && !(keepKey in keepOptions)) {
if (!foundOption && !isInKeepOptions(keepKey, newValue)) {
keepOptions[keepKey] = {
label: `${newValue.hid || "Selected"}: ${newValue.name}`,
value: newValue,
};
// this is used to trigger an update on formattedOptions
keepOptionsUpdate.value++;
}
// Add new value to list
incomingValues.push(newValue);
Expand Down Expand Up @@ -417,7 +436,11 @@ function handleIncoming(incoming: Record<string, unknown> | Record<string, unkno
}

function toDataOption(item: HistoryOrCollectionItem): DataOption | null {
const { newSrc, datasetCollectionDataset } = getSrcAndContentType(item);
const { newSrc, datasetCollectionDataset } = getElementAttributes(item);

// for name, somehow even though schema says otherwise, DCESummary can return a name
const newName = "name" in item && typeof item.name === "string" ? item.name : null;

let v: HistoryOrCollectionItem | HDAObject;
if (datasetCollectionDataset) {
v = datasetCollectionDataset;
Expand All @@ -426,14 +449,13 @@ function toDataOption(item: HistoryOrCollectionItem): DataOption | null {
}
const newHid = isHistoryItem(v) ? v.hid : undefined;
const newId = v.id;
const newName = isHistoryItem(v) && v.name ? v.name : newId;
const newValue: DataOption = {
id: newId,
src: newSrc,
batch: false,
map_over_type: undefined,
hid: newHid,
name: newName,
name: newName || newId,
keep: true,
tags: [],
};
Expand All @@ -453,6 +475,22 @@ function toDataOption(item: HistoryOrCollectionItem): DataOption | null {
return newValue;
}

/**
* Check if the new value is already in the keepOptions.
* This doesn't only check if the value is already stored by the `keepKey`, but also if the new value
* has a `hid` and the existing value doesn't. This is to ensure that values with `hid` are preferred.
*/
function isInKeepOptions(keepKey: string, newValue: DataOption): boolean {
if (keepKey in keepOptions) {
const existingOption = keepOptions[keepKey];
if (!existingOption?.value || (!!newValue.hid && !existingOption.value.hid)) {
return false;
}
return true;
}
return false;
}

/**
* Open file dialog
*/
Expand Down Expand Up @@ -500,14 +538,16 @@ function canAcceptDatatype(itemDatatypes: string | Array<string>) {
* Given an element, determine the source and content type.
* Also returns the collection element dataset object if it exists.
*/
function getSrcAndContentType(element: HistoryOrCollectionItem): {
function getElementAttributes(element: HistoryOrCollectionItem): {
historyContentType: HistoryContentType;
newSrc: string;
datasetCollectionDataset: HDAObject | undefined;
collectionType?: string;
} {
let historyContentType: HistoryContentType;
let newSrc: string;
let datasetCollectionDataset: HDAObject | undefined;
let collectionType: string | undefined;
if (isDCE(element)) {
if (isDatasetElement(element)) {
historyContentType = "dataset";
Expand All @@ -516,17 +556,22 @@ function getSrcAndContentType(element: HistoryOrCollectionItem): {
} else {
historyContentType = "dataset_collection";
newSrc = SOURCE.COLLECTION_ELEMENT;
// we already know it is a collection element by this point
if (isCollectionElement(element)) {
collectionType = element.object.collection_type;
}
}
} else {
historyContentType = element.history_content_type;
collectionType = "collection_type" in element && element.collection_type ? element.collection_type : undefined;
newSrc =
"src" in element && typeof element.src === "string"
? element.src
: historyContentType === "dataset_collection"
? SOURCE.COLLECTION
: SOURCE.DATASET;
}
return { historyContentType, newSrc, datasetCollectionDataset };
return { historyContentType, newSrc, datasetCollectionDataset, collectionType };
}

function canAcceptSrc(historyContentType: "dataset" | "dataset_collection", collectionType?: string) {
Expand All @@ -540,7 +585,12 @@ function canAcceptSrc(historyContentType: "dataset" | "dataset_collection", coll
}
} else if (historyContentType === "dataset_collection") {
if (props.type === "data") {
// collection can always be mapped over a data input ... in theory.
// if this input doesn't accept collections at all, return false
if (!validSrcs.value.includes(SOURCE.COLLECTION)) {
$emit("alert", "dataset collection is not a valid input for this dataset parameter.");
return false;
}
// otherwise, collection can always be mapped over a data input ... in theory.
// One day we should also validate the map over model
return true;
}
Expand All @@ -559,7 +609,7 @@ function canAcceptSrc(historyContentType: "dataset" | "dataset_collection", coll
} else {
$emit(
"alert",
`${collectionType} dataset collection is not a valid input for ${orList(
`${collectionTypeToText(collectionType)} dataset collection is not a valid input for ${orList(
props.collectionTypes
)} type dataset collection parameter.`
);
Expand Down Expand Up @@ -627,16 +677,14 @@ function onDragEnter(evt: DragEvent) {
for (const item of eventData) {
if (isHistoryOrCollectionItem(item)) {
const extensions = getExtensionsForItem(item);
const { historyContentType } = getSrcAndContentType(item);
const collectionType =
"collection_type" in item && item.collection_type ? item.collection_type : undefined;
const { historyContentType, collectionType } = getElementAttributes(item);

if (extensions && !canAcceptDatatype(extensions)) {
highlightingState = "warning";
$emit("alert", `${extensions} is not an acceptable format for this parameter.`);
} else if (!canAcceptSrc(historyContentType, collectionType)) {
highlightingState = "warning";
$emit("alert", `${historyContentType} is not an acceptable input type for this parameter.`);
// `canAcceptSrc` already alerts if false so no need to alert again
}
// Check if the item is already in the current value
const option = toDataOption(item);
Expand Down Expand Up @@ -686,7 +734,6 @@ function onDrop(e: DragEvent) {
} else {
currentHighlighting.value = "warning";
}
$emit("alert", undefined);
dragData.value = [];
clearHighlighting();
} else if (props.workflowRun && e.dataTransfer?.files?.length) {
Expand Down Expand Up @@ -740,8 +787,8 @@ const formatsVisible = ref(false);
const formatsButtonId = useUid("form-data-formats-");

function collectionTypeToText(collectionType: string): string {
if (collectionType == "list:paired") {
return "list of pairs";
if (COLLECTION_TYPE_TO_LABEL[collectionType]) {
return COLLECTION_TYPE_TO_LABEL[collectionType].toLowerCase();
} else {
return collectionType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { storeToRefs } from "pinia";
import { computed, nextTick, ref, watch } from "vue";

import type { HDCASummary } from "@/api";
import type { CollectionBuilderType } from "@/components/History/adapters/buildCollectionModal";
import { useUploadConfigurations } from "@/composables/uploadConfigurations";
import { useHistoryStore } from "@/stores/historyStore";
Expand Down Expand Up @@ -62,7 +63,7 @@ function addUploadedFiles(value: any[], viewUploads = true) {
}
}

function collectionCreated(collection: any) {
function collectionCreated(collection: HDCASummary) {
addUploadedFiles([collection], false);
emit("focus");
}
Expand Down
6 changes: 5 additions & 1 deletion lib/galaxy/managers/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,11 @@ def _create_instance_for_collection(
assert isinstance(dataset_collection_instance, model.HistoryDatasetCollectionAssociation)
if implicit_inputs:
for input_name, input_collection in implicit_inputs:
dataset_collection_instance.add_implicit_input_collection(input_name, input_collection)
if isinstance(input_collection, model.HistoryDatasetCollectionAssociation):
# Can also get dragged DatasetCollectionElement's here.
# We only use this for extracting workflows currently,
# but obviously it would be better to track that somehow.
dataset_collection_instance.add_implicit_input_collection(input_name, input_collection)

if implicit_output_name:
dataset_collection_instance.implicit_output_name = implicit_output_name
Expand Down
9 changes: 8 additions & 1 deletion lib/galaxy/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5459,7 +5459,7 @@ class HistoryDatasetAssociation(DatasetInstance, HasTags, Dictifiable, UsesAnnot
history_id: Mapped[Optional[int]]
dataset_id: Mapped[Optional[int]]
hidden_beneath_collection_instance: Mapped[Optional["HistoryDatasetCollectionAssociation"]]
tags: Mapped[Optional["HistoryDatasetAssociationTagAssociation"]]
tags: Mapped[List["HistoryDatasetAssociationTagAssociation"]]

def __init__(
self,
Expand Down Expand Up @@ -7715,6 +7715,13 @@ def element_object(
else:
raise AttributeError(f"Unknown element type provided: {type(value)}")

@property
def auto_propagated_tags(self):
first_dataset_instance = self.first_dataset_instance()
if first_dataset_instance:
return [t for t in first_dataset_instance.tags if t.user_tname in AUTO_PROPAGATED_TAGS]
return []

@property
def dataset_instance(self):
element_object = self.element_object
Expand Down
9 changes: 7 additions & 2 deletions lib/galaxy/model/dataset_collections/matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ def items(self):
return self.collections.items()


def get_child_collection(item):
# item could be HDCA or DCE
return getattr(item, "child_collection", item.collection)


class MatchingCollections:
"""Structure holding the result of matching a list of collections
together. This class being different than the class above and being
Expand Down Expand Up @@ -85,7 +90,7 @@ def structure(self):
def map_over_action_tuples(self, input_name):
if input_name not in self.action_tuples:
collection_instance = self.collections[input_name]
self.action_tuples[input_name] = collection_instance.collection.dataset_action_tuples
self.action_tuples[input_name] = get_child_collection(collection_instance).dataset_action_tuples
return self.action_tuples[input_name]

def is_mapped_over(self, input_name):
Expand All @@ -100,7 +105,7 @@ def for_collections(collections_to_match, collection_type_descriptions) -> Optio
for input_key, to_match in sorted(collections_to_match.items()):
hdca = to_match.hdca
collection_type_description = collection_type_descriptions.for_collection_type(
hdca.collection.collection_type
get_child_collection(hdca).collection_type
)
subcollection_type = to_match.subcollection_type

Expand Down
Loading
Loading