Skip to content

Commit

Permalink
Build dataset collection input definition on the client.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Dec 11, 2024
1 parent ccc63dc commit 1b62076
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 58 deletions.
2 changes: 1 addition & 1 deletion client/src/components/Form/Elements/FormSelection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
54 changes: 54 additions & 0 deletions client/src/components/Workflow/Editor/Forms/FormCollectionType.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { isValidCollectionTypeStr } from "@/components/Workflow/Editor/modules/collectionTypeDescription";
import FormElement from "@/components/Form/FormElement.vue";
interface Props {
value?: string;
}
const props = defineProps<Props>();
const currentValue = ref<string | undefined>(undefined);
const warning = ref<string | null>(null);
const error = ref<string | null>(null);
function onInput(newCollectionType: string | undefined) {
emit("onChange", newCollectionType);
}
const collectionTypeOptions = [
{ value: "list", label: "List of Datasets" },
{ value: "paired", label: "Dataset Pair" },
{ value: "list:paired", label: "List of Dataset Pairs" },
];
function updateValue(newValue: string | undefined) {
currentValue.value = newValue;
warning.value = null;
error.value = null;
if (!newValue) {
warning.value = "Typically, a value for this collection type should be specified.";
} else if (!isValidCollectionTypeStr(newValue)) {
error.value = "Invalid collection type";
}
}
watch(() => props.value, updateValue, { immediate: true });
const emit = defineEmits(["onChange"]);
</script>

<template>
<FormElement
id="collection_type"
:value="currentValue"
:attributes="{ datalist: collectionTypeOptions }"
:warning="warning"
:error="error"
title="Collection type"
:optional="true"
type="text"
@input="onInput" />
</template>
36 changes: 25 additions & 11 deletions client/src/components/Workflow/Editor/Forms/FormDatatype.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, ref, watch } from "vue";
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
Expand All @@ -8,16 +8,27 @@ import FormElement from "@/components/Form/FormElement.vue";
const props = withDefaults(
defineProps<{
id: string;
value?: string;
value?: string | string[];
title: string;
help: string;
multiple?: boolean;
datatypes: DatatypesMapperModel["datatypes"];
}>(),
{
value: null,
multiple: false,
}
);
const currentValue = ref<string | string[] | undefined>(undefined);
watch(
() => props.value,
(newValue) => {
currentValue.value = newValue;
},
{ immediate: true }
);
const emit = defineEmits(["onChange"]);
const datatypeExtensions = computed(() => {
Expand All @@ -34,25 +45,28 @@ const datatypeExtensions = computed(() => {
0: "Roadmaps",
1: "Roadmaps",
});
extensions.unshift({
0: "Leave unchanged",
1: "",
});
if (!props.multiple) {
extensions.unshift({
0: "Leave unchanged",
1: "",
});
}
return extensions;
});
function onChange(newDatatype: unknown) {
emit("onChange", newDatatype);
function onInput(newDatatype: string) {
currentValue.value = newDatatype;
emit("onChange", currentValue.value);
}
</script>

<template>
<FormElement
:id="id"
:value="value"
:attributes="{ options: datatypeExtensions }"
:value="currentValue"
:attributes="{ options: datatypeExtensions, multiple: multiple, display: 'simple', optional: true }"
:title="title"
type="select"
:help="help"
@input="onChange" />
@input="onInput" />
</template>
10 changes: 9 additions & 1 deletion client/src/components/Workflow/Editor/Forms/FormDefault.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,15 @@
v-if="isSubworkflow"
:step="step"
@onUpdateStep="(id, step) => emit('onUpdateStep', id, step)" />
<FormInputCollection
v-if="type == 'data_collection_input'"
:step="step"
:datatypes="datatypes"
:inputs="configForm.inputs"
@onChange="onChange">
</FormInputCollection>
<FormDisplay
v-if="configForm?.inputs"
v-else-if="configForm?.inputs"
:id="formDisplayId"
:key="formKey"
:inputs="configForm.inputs"
Expand Down Expand Up @@ -78,6 +85,7 @@ import FormConditional from "./FormConditional.vue";
import FormCard from "@/components/Form/FormCard.vue";
import FormDisplay from "@/components/Form/FormDisplay.vue";
import FormElement from "@/components/Form/FormElement.vue";
import FormInputCollection from "@/components/Workflow/Editor/Forms/FormInputCollection.vue";
import FormOutputLabel from "@/components/Workflow/Editor/Forms/FormOutputLabel.vue";
const props = defineProps<{
Expand Down
104 changes: 104 additions & 0 deletions client/src/components/Workflow/Editor/Forms/FormInputCollection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<script setup lang="ts">
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());
</script>

<template>
<div>
<FormCollectionType :value="toolState?.collection_type" :optional="true" @onChange="onCollectionType" />
<FormElement id="optional" :value="toolState?.optional" title="Optional" type="boolean" @input="onOptional" />
<FormDatatype
id="format"
:value="formatsAsList"
:datatypes="datatypes"
title="Format(s)"
:multiple="true"
help="Leave empty to auto-generate filtered list at runtime based on connections."
@onChange="onDatatype" />
<FormElement
id="tag"
:value="toolState?.tag"
title="Tag filter"
:optional="true"
type="text"
help="Tags to automatically filter inputs"
@input="onTags" />
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function useStepProps(step: Ref<Step>) {
inputs: stepInputs,
outputs: stepOutputs,
post_job_actions: postJobActions,
tool_state: toolState,
} = toRefs(step);

const label = computed(() => step.value.label ?? undefined);
Expand All @@ -29,5 +30,6 @@ export function useStepProps(step: Ref<Step>) {
stepOutputs,
configForm,
postJobActions,
toolState,
};
}
40 changes: 40 additions & 0 deletions client/src/components/Workflow/Editor/composables/useToolState.ts
Original file line number Diff line number Diff line change
@@ -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<Step>) {
const { tool_state: rawToolStateRef } = toRefs(step);

const toolState = computed(() => {
const rawToolState: Record<string, unknown> = rawToolStateRef.value;
const parsedToolState: Record<string, unknown> = {};

// 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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
19 changes: 13 additions & 6 deletions lib/galaxy/webapps/galaxy/api/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
# tool state not send, 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,
Expand All @@ -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

Expand Down
Loading

0 comments on commit 1b62076

Please sign in to comment.