mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Add post job action support to PickValueModule
Backend: PJA round-trip (from_dict, from_workflow_step, save_to_step, get_post_job_actions) and execution via ActionBox.execute_on_mapped_over. Added execute_on_mapped_over to ChangeDatatypeAction and ColumnSetAction in post.py to enable jobless PJA execution. Frontend: FormPickValue renders FormSection for PJA UI (rename, change datatype, tags, columns). Wired through FormDefault and NodeInspector. Framework tests: 4 workflow tests for change_datatype, rename, add_tag, and multi-PJA (all three combined). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
27d200e939
commit
700c991072
@@ -46,7 +46,11 @@
|
||||
<FormPickValue
|
||||
v-if="type == 'pick_value'"
|
||||
:step="step"
|
||||
@onChange="onChange" />
|
||||
:datatypes="datatypes"
|
||||
:node-inputs="stepInputs"
|
||||
:post-job-actions="postJobActions"
|
||||
@onChange="onChange"
|
||||
@onChangePostJobActions="onChangePostJobActions" />
|
||||
<FormInputCollection
|
||||
v-else-if="type == 'data_collection_input'"
|
||||
:step="step"
|
||||
@@ -104,9 +108,11 @@ const emit = defineEmits([
|
||||
"onEditSubworkflow",
|
||||
"onSetData",
|
||||
"onUpdateStep",
|
||||
"onChangePostJobActions",
|
||||
]);
|
||||
const stepRef = toRef(props, "step");
|
||||
const { stepId, contentId, annotation, label, name, type, configForm } = useStepProps(stepRef);
|
||||
const { stepId, contentId, annotation, label, name, type, configForm, stepInputs, postJobActions } =
|
||||
useStepProps(stepRef);
|
||||
const { stepStore } = useWorkflowStores();
|
||||
const uniqueErrorLabel = useUniqueLabelError(stepStore, label.value);
|
||||
const stepTitle = computed(() => {
|
||||
@@ -132,6 +138,9 @@ function onEditSubworkflow() {
|
||||
function onUpgradeSubworkflow() {
|
||||
emit("onAttemptRefactor", [{ action_type: "upgrade_subworkflow", step: { order_index: stepId.value } }]);
|
||||
}
|
||||
function onChangePostJobActions(postJobActions: unknown) {
|
||||
emit("onChangePostJobActions", stepId.value, postJobActions);
|
||||
}
|
||||
|
||||
// keeps the component from emitting the onCreate change event
|
||||
const initialChange = ref(true);
|
||||
|
||||
@@ -35,7 +35,7 @@ function makeStep(overrides: Partial<Step> = {}): Step {
|
||||
}
|
||||
|
||||
function mountPickValue(step?: Step): Wrapper<Vue> {
|
||||
return shallowMount(FormPickValue, {
|
||||
return shallowMount(FormPickValue as any, {
|
||||
propsData: {
|
||||
step: step ?? makeStep(),
|
||||
},
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { toRef, watch } from "vue";
|
||||
|
||||
import type { Step } from "@/stores/workflowStepStore";
|
||||
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import type { InputTerminalSource, PostJobActions, Step } from "@/stores/workflowStepStore";
|
||||
|
||||
import { useToolState } from "../composables/useToolState";
|
||||
|
||||
import FormElement from "@/components/Form/FormElement.vue";
|
||||
import FormSection from "@/components/Workflow/Editor/Forms/FormSection.vue";
|
||||
import Heading from "@/components/Common/Heading.vue";
|
||||
|
||||
interface ToolState {
|
||||
mode: string;
|
||||
num_inputs: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
step: Step;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
step: Step;
|
||||
datatypes?: DatatypesMapperModel["datatypes"];
|
||||
nodeInputs?: InputTerminalSource[];
|
||||
postJobActions?: PostJobActions;
|
||||
}>(),
|
||||
{
|
||||
datatypes: undefined,
|
||||
nodeInputs: () => [],
|
||||
postJobActions: () => ({}),
|
||||
}
|
||||
);
|
||||
|
||||
const stepRef = toRef(props, "step");
|
||||
const { toolState } = useToolState(stepRef);
|
||||
@@ -34,7 +47,7 @@ function cleanToolState(): ToolState {
|
||||
return { mode: "first_non_null", num_inputs: 2 };
|
||||
}
|
||||
|
||||
const emit = defineEmits(["onChange"]);
|
||||
const emit = defineEmits(["onChange", "onChangePostJobActions"]);
|
||||
|
||||
const modeOptions = [
|
||||
["First non-null (error if all null)", "first_non_null"],
|
||||
@@ -49,6 +62,10 @@ function onMode(newMode: string) {
|
||||
emit("onChange", state);
|
||||
}
|
||||
|
||||
function onChangePostJobActions(postJobActions: PostJobActions) {
|
||||
emit("onChangePostJobActions", postJobActions);
|
||||
}
|
||||
|
||||
// Grow-on-connect: watch step connections, add terminal when last empty one gets connected
|
||||
watch(
|
||||
() => props.step.input_connections,
|
||||
@@ -77,5 +94,16 @@ emit("onChange", cleanToolState());
|
||||
:options="modeOptions"
|
||||
help="How to select among the connected inputs."
|
||||
@input="onMode" />
|
||||
<div v-if="datatypes && step.outputs && step.outputs.length > 0" class="mt-2 mb-4">
|
||||
<Heading h2 separator bold size="sm"> Additional Options </Heading>
|
||||
<FormSection
|
||||
:id="step.id"
|
||||
:node-inputs="nodeInputs ?? []"
|
||||
:node-outputs="step.outputs"
|
||||
:step="step"
|
||||
:datatypes="datatypes"
|
||||
:post-job-actions="postJobActions ?? {}"
|
||||
@onChange="onChangePostJobActions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -129,6 +129,7 @@ function updateStored(v: boolean) {
|
||||
:datatypes="datatypes"
|
||||
@onSetData="(id, d) => emit('dataChanged', id, d)"
|
||||
@onUpdateStep="(id, s) => emit('stepUpdated', id, s)"
|
||||
@onChangePostJobActions="(id, a) => emit('postJobActionsChanged', id, a)"
|
||||
@onAnnotation="(id, a) => emit('annotationChanged', id, a)"
|
||||
@onLabel="(id, l) => emit('labelChanged', id, l)"
|
||||
@onEditSubworkflow="(id) => emit('editSubworkflow', id)"
|
||||
|
||||
@@ -107,6 +107,21 @@ class ChangeDatatypeAction(DefaultJobAction):
|
||||
name = "ChangeDatatypeAction"
|
||||
verbose_name = "Change Datatype"
|
||||
|
||||
@classmethod
|
||||
def execute_on_mapped_over(
|
||||
cls, trans, sa_session, action, step_inputs, step_outputs, replacement_dict, final_job_state=None
|
||||
):
|
||||
if action.action_arguments and action.action_arguments.get("newtype"):
|
||||
newtype = action.action_arguments["newtype"]
|
||||
for name, step_output in step_outputs.items():
|
||||
if action.output_name == "" or name == action.output_name:
|
||||
if hasattr(step_output, "dataset_instances"):
|
||||
for element in step_output.dataset_instances:
|
||||
if element:
|
||||
trans.app.datatypes_registry.change_datatype(element, newtype)
|
||||
else:
|
||||
trans.app.datatypes_registry.change_datatype(step_output, newtype)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, app, sa_session, action, job, replacement_dict=None, final_job_state=None):
|
||||
if job.state == job.states.SKIPPED:
|
||||
@@ -329,19 +344,31 @@ class ColumnSetAction(DefaultJobAction):
|
||||
name = "ColumnSetAction"
|
||||
verbose_name = "Assign Columns"
|
||||
|
||||
@classmethod
|
||||
def _apply_column_set(cls, dataset, action_arguments):
|
||||
for k, v in action_arguments.items():
|
||||
if v:
|
||||
if not isinstance(v, int):
|
||||
if v[0] == "c":
|
||||
v = v[1:]
|
||||
v = int(v)
|
||||
if v != 0:
|
||||
setattr(dataset.metadata, k, v)
|
||||
|
||||
@classmethod
|
||||
def execute_on_mapped_over(
|
||||
cls, trans, sa_session, action, step_inputs, step_outputs, replacement_dict, final_job_state=None
|
||||
):
|
||||
if action.action_arguments:
|
||||
for name, step_output in step_outputs.items():
|
||||
if action.output_name == "" or name == action.output_name:
|
||||
cls._apply_column_set(step_output, action.action_arguments)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, app, sa_session, action, job, replacement_dict=None, final_job_state=None):
|
||||
for dataset_assoc in job.output_datasets:
|
||||
if action.output_name == "" or dataset_assoc.name == action.output_name:
|
||||
for k, v in action.action_arguments.items():
|
||||
if v:
|
||||
# Try to use both pure integer and 'cX' format.
|
||||
if not isinstance(v, int):
|
||||
if v[0] == "c":
|
||||
v = v[1:]
|
||||
v = int(v)
|
||||
if v != 0:
|
||||
setattr(dataset_assoc.dataset.metadata, k, v)
|
||||
cls._apply_column_set(dataset_assoc.dataset, action.action_arguments)
|
||||
|
||||
@classmethod
|
||||
def get_short_str(cls, pja):
|
||||
|
||||
@@ -1961,6 +1961,27 @@ class PickValueModule(WorkflowModule):
|
||||
|
||||
MODES = ("first_non_null", "first_or_skip", "the_only_non_null", "all_non_null")
|
||||
|
||||
def __init__(self, trans, content_id=None, **kwds):
|
||||
super().__init__(trans, content_id=content_id, **kwds)
|
||||
self.post_job_actions: dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def from_dict(Class, trans, d, **kwds):
|
||||
module = super().from_dict(trans, d, **kwds)
|
||||
module.post_job_actions = d.get("post_job_actions", {})
|
||||
return module
|
||||
|
||||
@classmethod
|
||||
def from_workflow_step(Class, trans, step, **kwds):
|
||||
module = super().from_workflow_step(trans, step, **kwds)
|
||||
module.post_job_actions = {}
|
||||
for pja in step.post_job_actions:
|
||||
module.post_job_actions[pja.action_type + pja.output_name] = pja
|
||||
return module
|
||||
|
||||
def get_post_job_actions(self, incoming):
|
||||
return self.post_job_actions
|
||||
|
||||
def get_inputs(self):
|
||||
# State managed by frontend Vue component, not backend forms
|
||||
return {}
|
||||
@@ -1981,6 +2002,10 @@ class PickValueModule(WorkflowModule):
|
||||
def save_to_step(self, step, detached=False):
|
||||
step.type = self.type
|
||||
step.tool_inputs = self._get_state_dict()
|
||||
if not detached:
|
||||
for k, v in self.post_job_actions.items():
|
||||
pja = self._to_pja(k, v, step)
|
||||
self.trans.sa_session.add(pja)
|
||||
|
||||
@property
|
||||
def _num_inputs(self):
|
||||
@@ -2093,6 +2118,7 @@ class PickValueModule(WorkflowModule):
|
||||
raise ValueError(f"Unknown pick_value mode: {mode}")
|
||||
|
||||
progress.set_step_outputs(invocation_step, {"output": output})
|
||||
self._apply_post_job_actions(trans, step, output, progress.effective_replacement_dict())
|
||||
return None
|
||||
|
||||
def _create_skipped_output(self, trans, invocation_step):
|
||||
@@ -2133,6 +2159,28 @@ class PickValueModule(WorkflowModule):
|
||||
)
|
||||
return hdca
|
||||
|
||||
def _apply_post_job_actions(self, trans, step, output, replacement_dict):
|
||||
"""Apply post job actions directly to module output via ActionBox.
|
||||
|
||||
Uses execute_on_mapped_over which operates on step_outputs dict
|
||||
rather than requiring a Job object.
|
||||
"""
|
||||
step_outputs = {"output": output}
|
||||
step_inputs = {}
|
||||
for pja in step.post_job_actions:
|
||||
ActionBox.execute_on_mapped_over(
|
||||
trans, trans.sa_session, pja, step_inputs, step_outputs, replacement_dict
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _to_pja(key, value, step):
|
||||
if isinstance(value, PostJobAction):
|
||||
return value
|
||||
output_name = value.get("output_name") if isinstance(value, dict) else None
|
||||
action_arguments = value.get("action_arguments") if isinstance(value, dict) else None
|
||||
action_type = value.get("action_type", key) if isinstance(value, dict) else key
|
||||
return PostJobAction(action_type, step, output_name, action_arguments)
|
||||
|
||||
|
||||
class ToolModule(WorkflowModule):
|
||||
type = "tool"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
- doc: |
|
||||
Test that TagDatasetAction on pick_value adds a tag to the output.
|
||||
job:
|
||||
input_data:
|
||||
type: File
|
||||
value: 1.bed
|
||||
file_type: bed
|
||||
when:
|
||||
type: raw
|
||||
value: true
|
||||
outputs:
|
||||
picked:
|
||||
class: File
|
||||
metadata:
|
||||
tags: "picktag"
|
||||
@@ -0,0 +1,29 @@
|
||||
class: GalaxyWorkflow
|
||||
inputs:
|
||||
input_data:
|
||||
type: data
|
||||
when:
|
||||
type: boolean
|
||||
outputs:
|
||||
picked:
|
||||
outputSource: pick/output
|
||||
steps:
|
||||
branch:
|
||||
tool_id: cat
|
||||
in:
|
||||
input1:
|
||||
source: input_data
|
||||
when:
|
||||
source: when
|
||||
when: $(inputs.when)
|
||||
pick:
|
||||
type: pick_value
|
||||
in:
|
||||
input_0:
|
||||
source: branch/out_file1
|
||||
state:
|
||||
mode: first_or_skip
|
||||
out:
|
||||
output:
|
||||
add_tags:
|
||||
- picktag
|
||||
@@ -0,0 +1,17 @@
|
||||
- doc: |
|
||||
Test that ChangeDatatypeAction on pick_value changes output datatype.
|
||||
job:
|
||||
input_data:
|
||||
type: File
|
||||
value: 1.bed
|
||||
file_type: bed
|
||||
when:
|
||||
type: raw
|
||||
value: true
|
||||
outputs:
|
||||
picked:
|
||||
class: File
|
||||
ftype: txt
|
||||
asserts:
|
||||
- that: has_text
|
||||
text: chr1
|
||||
@@ -0,0 +1,28 @@
|
||||
class: GalaxyWorkflow
|
||||
inputs:
|
||||
input_data:
|
||||
type: data
|
||||
when:
|
||||
type: boolean
|
||||
outputs:
|
||||
picked:
|
||||
outputSource: pick/output
|
||||
steps:
|
||||
branch:
|
||||
tool_id: cat
|
||||
in:
|
||||
input1:
|
||||
source: input_data
|
||||
when:
|
||||
source: when
|
||||
when: $(inputs.when)
|
||||
pick:
|
||||
type: pick_value
|
||||
in:
|
||||
input_0:
|
||||
source: branch/out_file1
|
||||
state:
|
||||
mode: first_or_skip
|
||||
out:
|
||||
output:
|
||||
change_datatype: txt
|
||||
@@ -0,0 +1,17 @@
|
||||
- doc: |
|
||||
Test multiple PJAs on pick_value: change datatype, rename, and add tag.
|
||||
job:
|
||||
input_data:
|
||||
type: File
|
||||
value: 1.bed
|
||||
file_type: bed
|
||||
when:
|
||||
type: raw
|
||||
value: true
|
||||
outputs:
|
||||
picked:
|
||||
class: File
|
||||
ftype: txt
|
||||
metadata:
|
||||
name: "picked_and_typed"
|
||||
tags: "pv_tag"
|
||||
@@ -0,0 +1,31 @@
|
||||
class: GalaxyWorkflow
|
||||
inputs:
|
||||
input_data:
|
||||
type: data
|
||||
when:
|
||||
type: boolean
|
||||
outputs:
|
||||
picked:
|
||||
outputSource: pick/output
|
||||
steps:
|
||||
branch:
|
||||
tool_id: cat
|
||||
in:
|
||||
input1:
|
||||
source: input_data
|
||||
when:
|
||||
source: when
|
||||
when: $(inputs.when)
|
||||
pick:
|
||||
type: pick_value
|
||||
in:
|
||||
input_0:
|
||||
source: branch/out_file1
|
||||
state:
|
||||
mode: first_or_skip
|
||||
out:
|
||||
output:
|
||||
change_datatype: txt
|
||||
rename: "picked_and_typed"
|
||||
add_tags:
|
||||
- pv_tag
|
||||
@@ -0,0 +1,15 @@
|
||||
- doc: |
|
||||
Test that RenameDatasetAction on pick_value renames the output.
|
||||
job:
|
||||
input_data:
|
||||
type: File
|
||||
value: 1.bed
|
||||
file_type: bed
|
||||
when:
|
||||
type: raw
|
||||
value: true
|
||||
outputs:
|
||||
picked:
|
||||
class: File
|
||||
metadata:
|
||||
name: "picked_result"
|
||||
@@ -0,0 +1,28 @@
|
||||
class: GalaxyWorkflow
|
||||
inputs:
|
||||
input_data:
|
||||
type: data
|
||||
when:
|
||||
type: boolean
|
||||
outputs:
|
||||
picked:
|
||||
outputSource: pick/output
|
||||
steps:
|
||||
branch:
|
||||
tool_id: cat
|
||||
in:
|
||||
input1:
|
||||
source: input_data
|
||||
when:
|
||||
source: when
|
||||
when: $(inputs.when)
|
||||
pick:
|
||||
type: pick_value
|
||||
in:
|
||||
input_0:
|
||||
source: branch/out_file1
|
||||
state:
|
||||
mode: first_or_skip
|
||||
out:
|
||||
output:
|
||||
rename: "picked_result"
|
||||
Reference in New Issue
Block a user