diff --git a/client/src/components/Form/Elements/FormSelection.vue b/client/src/components/Form/Elements/FormSelection.vue
index 15176a48f063..12d681f4a517 100644
--- a/client/src/components/Form/Elements/FormSelection.vue
+++ b/client/src/components/Form/Elements/FormSelection.vue
@@ -100,7 +100,7 @@ watch(
);
const showSelectPreference = computed(
- () => props.multiple && props.display !== "checkboxes" && props.display !== "radio"
+ () => props.multiple && props.display !== "checkboxes" && props.display !== "radio" && props.display !== "simple"
);
const displayMany = computed(() => showSelectPreference.value && useMany.value);
diff --git a/client/src/components/Workflow/Editor/Forms/FormCollectionType.vue b/client/src/components/Workflow/Editor/Forms/FormCollectionType.vue
new file mode 100644
index 000000000000..a93cb5ded6f1
--- /dev/null
+++ b/client/src/components/Workflow/Editor/Forms/FormCollectionType.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
diff --git a/client/src/components/Workflow/Editor/Forms/FormDatatype.vue b/client/src/components/Workflow/Editor/Forms/FormDatatype.vue
index 466a9fca75e3..cc50d02a612f 100644
--- a/client/src/components/Workflow/Editor/Forms/FormDatatype.vue
+++ b/client/src/components/Workflow/Editor/Forms/FormDatatype.vue
@@ -1,5 +1,5 @@
+ @input="onInput" />
diff --git a/client/src/components/Workflow/Editor/Forms/FormDefault.vue b/client/src/components/Workflow/Editor/Forms/FormDefault.vue
index 3a0f82c0a9d6..a455ba3a0c5b 100644
--- a/client/src/components/Workflow/Editor/Forms/FormDefault.vue
+++ b/client/src/components/Workflow/Editor/Forms/FormDefault.vue
@@ -43,8 +43,15 @@
v-if="isSubworkflow"
:step="step"
@onUpdateStep="(id, step) => emit('onUpdateStep', id, step)" />
+
+
+import { computed, toRef } from "vue";
+
+import type { DatatypesMapperModel } from "@/components/Datatypes/model";
+import type { Step } from "@/stores/workflowStepStore";
+
+import { useToolState } from "../composables/useToolState";
+
+import FormElement from "@/components/Form/FormElement.vue";
+import FormCollectionType from "@/components/Workflow/Editor/Forms/FormCollectionType.vue";
+import FormDatatype from "@/components/Workflow/Editor/Forms/FormDatatype.vue";
+
+interface ToolState {
+ collection_type: string | null;
+ optional: boolean;
+ format: string | null;
+ tag: string | null;
+}
+
+const props = defineProps<{
+ step: Step;
+ datatypes: DatatypesMapperModel["datatypes"];
+}>();
+
+const stepRef = toRef(props, "step");
+const { toolState } = useToolState(stepRef);
+
+function cleanToolState(): ToolState {
+ if (toolState.value) {
+ return { ...toolState.value } as unknown as ToolState;
+ } else {
+ return {
+ collection_type: null,
+ optional: false,
+ tag: null,
+ format: null,
+ };
+ }
+}
+
+const emit = defineEmits(["onChange"]);
+
+function onDatatype(newDatatype: string[]) {
+ const state = cleanToolState();
+ state.format = newDatatype.join(",");
+ emit("onChange", state);
+}
+
+function onTags(newTags: string | null) {
+ const state = cleanToolState();
+ state.tag = newTags;
+ emit("onChange", state);
+}
+
+function onOptional(newOptional: boolean) {
+ const state = cleanToolState();
+ state.optional = newOptional;
+ emit("onChange", state);
+}
+
+function onCollectionType(newCollectionType: string | null) {
+ const state = cleanToolState();
+ state.collection_type = newCollectionType;
+ emit("onChange", state);
+}
+
+const formatsAsList = computed(() => {
+ const formatStr = toolState.value?.format as string | string[] | null;
+ if (formatStr && typeof formatStr === "string") {
+ return formatStr.split(/\s*,\s*/);
+ } else if (formatStr) {
+ return formatStr;
+ } else {
+ return [];
+ }
+});
+
+// Terrible Hack: The parent component (./FormDefault.vue) ignores the first update, so
+// I am sending a dummy update here. Ideally, the parent FormDefault would not expect this.
+emit("onChange", cleanToolState());
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/Workflow/Editor/composables/useStepProps.ts b/client/src/components/Workflow/Editor/composables/useStepProps.ts
index 95e4787bdfd9..fe63f09b3930 100644
--- a/client/src/components/Workflow/Editor/composables/useStepProps.ts
+++ b/client/src/components/Workflow/Editor/composables/useStepProps.ts
@@ -12,6 +12,7 @@ export function useStepProps(step: Ref) {
inputs: stepInputs,
outputs: stepOutputs,
post_job_actions: postJobActions,
+ tool_state: toolState,
} = toRefs(step);
const label = computed(() => step.value.label ?? undefined);
@@ -29,5 +30,6 @@ export function useStepProps(step: Ref) {
stepOutputs,
configForm,
postJobActions,
+ toolState,
};
}
diff --git a/client/src/components/Workflow/Editor/composables/useToolState.ts b/client/src/components/Workflow/Editor/composables/useToolState.ts
new file mode 100644
index 000000000000..6feb13589de9
--- /dev/null
+++ b/client/src/components/Workflow/Editor/composables/useToolState.ts
@@ -0,0 +1,40 @@
+import { toRefs } from "@vueuse/core";
+import { computed, type Ref } from "vue";
+
+import { type Step } from "@/stores/workflowStepStore";
+
+export function useToolState(step: Ref) {
+ const { tool_state: rawToolStateRef } = toRefs(step);
+
+ const toolState = computed(() => {
+ const rawToolState: Record = rawToolStateRef.value;
+ const parsedToolState: Record = {};
+
+ // This is less than ideal in a couple ways. The fact the JSON response
+ // has encoded JSON is gross and it would be great for module types that
+ // do not use the tool form to just return a simple JSON blob without
+ // the extra encoded. As a step two if each of these module types could
+ // also define a schema so we could use typed entities shared between the
+ // client and server that would be ideal.
+ for (const key in rawToolState) {
+ if (Object.prototype.hasOwnProperty.call(rawToolState, key)) {
+ const value = rawToolState[key];
+ if (typeof value === "string") {
+ try {
+ const parsedValue = JSON.parse(value);
+ parsedToolState[key] = parsedValue;
+ } catch (error) {
+ parsedToolState[key] = rawToolState[key];
+ }
+ } else {
+ parsedToolState[key] = rawToolState[key];
+ }
+ }
+ }
+ return parsedToolState;
+ });
+
+ return {
+ toolState,
+ };
+}
diff --git a/client/src/components/Workflow/Editor/modules/collectionTypeDescription.ts b/client/src/components/Workflow/Editor/modules/collectionTypeDescription.ts
index 792a842b0df9..cc48c2a69117 100644
--- a/client/src/components/Workflow/Editor/modules/collectionTypeDescription.ts
+++ b/client/src/components/Workflow/Editor/modules/collectionTypeDescription.ts
@@ -128,3 +128,13 @@ export class CollectionTypeDescription implements CollectionTypeDescriptor {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
}
+
+const collectionTypeRegex = /^(list|paired)(:(list|paired))*$/;
+
+export function isValidCollectionTypeStr(collectionType: string | undefined) {
+ if (collectionType) {
+ return collectionTypeRegex.test(collectionType);
+ } else {
+ return true;
+ }
+}
diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py
index b52d495d40e6..e82dc623497a 100644
--- a/lib/galaxy/webapps/galaxy/api/workflows.py
+++ b/lib/galaxy/webapps/galaxy/api/workflows.py
@@ -528,15 +528,22 @@ def build_module(self, trans: GalaxyWebTransaction, payload=None):
# payload is tool state
if payload is None:
payload = {}
+ module_type = payload.get("type", "tool")
inputs = payload.get("inputs", {})
trans.workflow_building_mode = workflow_building_modes.ENABLED
- module = module_factory.from_dict(trans, payload, from_tool_form=True)
-
+ from_tool_form = True if module_type != "data_collection_input" else False
+ if not from_tool_form and "tool_state" not in payload and "inputs" in payload:
+ # tool state not sent, use the manually constructed inputs
+ payload["tool_state"] = payload["inputs"]
+ module = module_factory.from_dict(trans, payload, from_tool_form=from_tool_form)
module_state: Dict[str, Any] = {}
errors: ParameterValidationErrorsT = {}
- populate_state(trans, module.get_inputs(), inputs, module_state, errors=errors, check=True)
- module.recover_state(module_state, from_tool_form=True)
- module.check_and_update_state()
+ if from_tool_form:
+ populate_state(trans, module.get_inputs(), inputs, module_state, errors=errors, check=True)
+ module.recover_state(module_state, from_tool_form=True)
+ module.check_and_update_state()
+ else:
+ module_state = module.get_export_state()
step_dict = {
"name": module.get_name(),
"tool_state": module_state,
@@ -546,7 +553,7 @@ def build_module(self, trans: GalaxyWebTransaction, payload=None):
"config_form": module.get_config_form(),
"errors": errors or None,
}
- if payload["type"] == "tool":
+ if module_type == "tool":
step_dict["tool_version"] = module.get_version()
return step_dict
diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py
index 835b9ae2a710..3d0f267ff3bb 100644
--- a/lib/galaxy/workflow/modules.py
+++ b/lib/galaxy/workflow/modules.py
@@ -1114,34 +1114,8 @@ class InputDataCollectionModule(InputModule):
collection_type = default_collection_type
def get_inputs(self):
- parameter_def = self._parse_state_into_dict()
- collection_type = parameter_def["collection_type"]
- tag = parameter_def["tag"]
- optional = parameter_def["optional"]
- collection_type_source = dict(
- name="collection_type", label="Collection type", type="text", value=collection_type
- )
- collection_type_source["options"] = [
- {"value": "list", "label": "List of Datasets"},
- {"value": "paired", "label": "Dataset Pair"},
- {"value": "list:paired", "label": "List of Dataset Pairs"},
- ]
- input_collection_type = TextToolParameter(None, collection_type_source)
- tag_source = dict(
- name="tag",
- label="Tag filter",
- type="text",
- optional="true",
- value=tag,
- help="Tags to automatically filter inputs",
- )
- input_tag = TextToolParameter(None, tag_source)
- inputs = {}
- inputs["collection_type"] = input_collection_type
- inputs["optional"] = optional_param(optional)
- inputs["format"] = format_param(self.trans, parameter_def.get("format"))
- inputs["tag"] = input_tag
- return inputs
+ # migrated to frontend
+ return {}
def get_runtime_inputs(self, step, connections: Optional[Iterable[WorkflowStepConnection]] = None):
parameter_def = self._parse_state_into_dict()
diff --git a/test/unit/workflows/test_modules.py b/test/unit/workflows/test_modules.py
index 8052c6b11950..fa1e0177701f 100644
--- a/test/unit/workflows/test_modules.py
+++ b/test/unit/workflows/test_modules.py
@@ -125,17 +125,6 @@ def test_data_collection_input_connections():
assert output["collection_type"] == "list:paired"
-def test_data_collection_input_config_form():
- module = __from_step(
- type="data_collection_input",
- tool_inputs={
- "collection_type": "list:paired",
- },
- )
- result = module.get_config_form()
- assert result["inputs"][0]["value"], "list:paired"
-
-
def test_cannot_create_tool_modules_for_missing_tools():
trans = MockTrans()
module = modules.module_factory.from_dict(trans, {"type": "tool", "tool_id": "cat1"})