diff --git a/client/src/components/Workflow/Editor/Actions/actions.test.ts b/client/src/components/Workflow/Editor/Actions/actions.test.ts index 5ade2336264a..aa8cf71c37ef 100644 --- a/client/src/components/Workflow/Editor/Actions/actions.test.ts +++ b/client/src/components/Workflow/Editor/Actions/actions.test.ts @@ -14,9 +14,10 @@ import { LazyChangeDataAction, LazyChangePositionAction, LazyChangeSizeAction, + RemoveAllFreehandCommentsAction, ToggleCommentSelectedAction, } from "./commentActions"; -import { mockComment, mockToolStep, mockWorkflow } from "./mockData"; +import { mockComment, mockFreehandComment, mockToolStep, mockWorkflow } from "./mockData"; import { CopyStepAction, InsertStepAction, @@ -90,6 +91,12 @@ describe("Workflow Undo Redo Actions", () => { return comment; } + function addFreehandComment() { + const comment = mockFreehandComment(commentStore.highestCommentId + 1); + commentStore.addComments([comment]); + return comment; + } + function addStep() { const step = mockToolStep(stepStore.getStepIndex + 1); stepStore.addStep(step); @@ -141,6 +148,15 @@ describe("Workflow Undo Redo Actions", () => { const action = new ToggleCommentSelectedAction(commentStore, comment); testUndoRedo(action); }); + + it("RemoveAllFreehandCommentsAction", () => { + addFreehandComment(); + addFreehandComment(); + addFreehandComment(); + + const action = new RemoveAllFreehandCommentsAction(commentStore); + testUndoRedo(action); + }); }); describe("Workflow Actions", () => { diff --git a/client/src/components/Workflow/Editor/Actions/commentActions.ts b/client/src/components/Workflow/Editor/Actions/commentActions.ts index 090a19221183..3b497886b323 100644 --- a/client/src/components/Workflow/Editor/Actions/commentActions.ts +++ b/client/src/components/Workflow/Editor/Actions/commentActions.ts @@ -177,3 +177,28 @@ export class ToggleCommentSelectedAction extends UndoRedoAction { this.store.setCommentMultiSelected(this.commentId, !this.toggleTo); } } + +export class RemoveAllFreehandCommentsAction extends UndoRedoAction { + store; + comments; + + constructor(store: WorkflowCommentStore) { + super(); + + this.store = store; + const freehandComments = store.comments.filter((comment) => comment.type === "freehand"); + this.comments = structuredClone(freehandComments); + } + + get name() { + return "remove all freehand comments"; + } + + run() { + this.store.deleteFreehandComments(); + } + + undo() { + this.store.addComments(structuredClone(this.comments)); + } +} diff --git a/client/src/components/Workflow/Editor/Actions/mockData.ts b/client/src/components/Workflow/Editor/Actions/mockData.ts index 2b5faea23161..ab285037c09c 100644 --- a/client/src/components/Workflow/Editor/Actions/mockData.ts +++ b/client/src/components/Workflow/Editor/Actions/mockData.ts @@ -1,4 +1,4 @@ -import { WorkflowComment } from "@/stores/workflowEditorCommentStore"; +import { FreehandWorkflowComment, WorkflowComment } from "@/stores/workflowEditorCommentStore"; import type { Step } from "@/stores/workflowStepStore"; import type { Workflow } from "../modules/model"; @@ -227,3 +227,21 @@ export function mockComment(id: number): WorkflowComment { data: { size: 2, text: "Enter Text" }, }; } + +export function mockFreehandComment(id: number): FreehandWorkflowComment { + return { + id, + position: [0, 0], + size: [100, 200], + type: "freehand", + color: "none", + data: { + thickness: 1, + line: [ + [0, 0], + [10, 20], + [100, 200], + ], + }, + }; +} diff --git a/client/src/components/Workflow/Editor/Tools/ToolBar.vue b/client/src/components/Workflow/Editor/Tools/ToolBar.vue index 2d121abfa977..c902f381bccf 100644 --- a/client/src/components/Workflow/Editor/Tools/ToolBar.vue +++ b/client/src/components/Workflow/Editor/Tools/ToolBar.vue @@ -21,6 +21,7 @@ import { BoxSelect } from "lucide-vue"; import { storeToRefs } from "pinia"; import { computed, toRefs, watch } from "vue"; +import { RemoveAllFreehandCommentsAction } from "@/components/Workflow/Editor/Actions/commentActions"; import { useUid } from "@/composables/utils/uid"; import { useWorkflowStores } from "@/composables/workflowStores"; import { type CommentTool } from "@/stores/workflowEditorToolbarStore"; @@ -45,7 +46,7 @@ library.add( faTrash ); -const { toolbarStore, commentStore } = useWorkflowStores(); +const { toolbarStore, undoRedoStore, commentStore } = useWorkflowStores(); const { snapActive, currentTool } = toRefs(toolbarStore); const { commentOptions } = toolbarStore; @@ -122,7 +123,7 @@ const thicknessId = useUid("thickness-"); const smoothingId = useUid("smoothing-"); function onRemoveAllFreehand() { - commentStore.deleteFreehandComments(); + undoRedoStore.applyAction(new RemoveAllFreehandCommentsAction(commentStore)); } useToolLogic(); diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 304879b934ad..3a23a25e19a1 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -489,12 +489,16 @@ def serialize_dataset_association_roles(self, trans, dataset_assoc): def ensure_dataset_on_disk(self, trans, dataset): # Not a guarantee data is really present, but excludes a lot of expected cases + if not dataset.dataset: + raise exceptions.InternalServerError("Item has no associated dataset.") if dataset.purged or dataset.dataset.purged: raise exceptions.ItemDeletionException("The dataset you are attempting to view has been purged.") elif dataset.deleted and not (trans.user_is_admin or self.is_owner(dataset, trans.get_user())): raise exceptions.ItemDeletionException("The dataset you are attempting to view has been deleted.") elif dataset.state == Dataset.states.UPLOAD: raise exceptions.Conflict("Please wait until this dataset finishes uploading before attempting to view it.") + elif dataset.state == Dataset.states.NEW: + raise exceptions.Conflict("The dataset you are attempting to view is new and has no data.") elif dataset.state == Dataset.states.DISCARDED: raise exceptions.ItemDeletionException("The dataset you are attempting to view has been discarded.") elif dataset.state == Dataset.states.DEFERRED: diff --git a/lib/galaxy/model/unittest_utils/data_app.py b/lib/galaxy/model/unittest_utils/data_app.py index fa92af3190a3..75e5101eca80 100644 --- a/lib/galaxy/model/unittest_utils/data_app.py +++ b/lib/galaxy/model/unittest_utils/data_app.py @@ -61,6 +61,7 @@ def __init__(self, root=None, **kwd): self.object_store = "disk" self.object_store_check_old_style = False self.object_store_cache_path = "/tmp/cache" + self.object_store_cache_size = -1 self.object_store_store_by = "uuid" self.umask = os.umask(0o77) diff --git a/lib/galaxy/objectstore/__init__.py b/lib/galaxy/objectstore/__init__.py index 724fcc3e6267..4a4d639bf927 100644 --- a/lib/galaxy/objectstore/__init__.py +++ b/lib/galaxy/objectstore/__init__.py @@ -1726,6 +1726,7 @@ def config_to_dict(config): "jobs_directory": config.jobs_directory, "new_file_path": config.new_file_path, "object_store_cache_path": config.object_store_cache_path, + "object_store_cache_size": config.object_store_cache_size, "gid": config.gid, } diff --git a/lib/galaxy/tools/parameters/wrapped.py b/lib/galaxy/tools/parameters/wrapped.py index dbcc4d1f0a03..11fa98c0e644 100644 --- a/lib/galaxy/tools/parameters/wrapped.py +++ b/lib/galaxy/tools/parameters/wrapped.py @@ -3,6 +3,8 @@ Any, Dict, List, + Sequence, + Union, ) from galaxy.tools.parameters.basic import ( @@ -187,6 +189,26 @@ def process_key(incoming_key: str, incoming_value: Any, d: Dict[str, Any]): process_key("|".join(key_parts[1:]), incoming_value=incoming_value, d=subdict) +def nested_key_to_path(key: str) -> Sequence[Union[str, int]]: + """ + Convert a tool state key that is separated with '|' and '_n' into path iterable. + E.g. "cond|repeat_0|paramA" -> ["cond", "repeat", 0, "paramA"]. + Return value can be used with `boltons.iterutils.get_path`. + """ + path: List[Union[str, int]] = [] + key_parts = key.split("|") + if len(key_parts) == 1: + return key_parts + for key_part in key_parts: + if "_" in key_part: + input_name, _index = key_part.rsplit("_", 1) + if _index.isdigit(): + path.extend((input_name, int(_index))) + continue + path.append(key_part) + return path + + def flat_to_nested_state(incoming: Dict[str, Any]): nested_state: Dict[str, Any] = {} for key, value in incoming.items(): @@ -194,4 +216,11 @@ def flat_to_nested_state(incoming: Dict[str, Any]): return nested_state -__all__ = ("LegacyUnprefixedDict", "WrappedParameters", "make_dict_copy", "process_key", "flat_to_nested_state") +__all__ = ( + "LegacyUnprefixedDict", + "WrappedParameters", + "make_dict_copy", + "process_key", + "flat_to_nested_state", + "nested_key_to_path", +) diff --git a/lib/galaxy/workflow/run.py b/lib/galaxy/workflow/run.py index 934f973db689..41189ee81217 100644 --- a/lib/galaxy/workflow/run.py +++ b/lib/galaxy/workflow/run.py @@ -10,6 +10,7 @@ Union, ) +from boltons.iterutils import get_path from typing_extensions import Protocol from galaxy import model @@ -35,6 +36,7 @@ WarningReason, ) from galaxy.tools.parameters.basic import raw_to_galaxy +from galaxy.tools.parameters.wrapped import nested_key_to_path from galaxy.util import ExecutionTimer from galaxy.workflow import modules from galaxy.workflow.run_request import ( @@ -444,6 +446,10 @@ def replacement_for_input(self, trans, step: "WorkflowStep", input_dict: Dict[st replacement = temp else: replacement = self.replacement_for_connection(connection[0], is_data=is_data) + elif step.state and (state_input := get_path(step.state.inputs, nested_key_to_path(prefixed_name), None)): + # workflow submitted with step parameters populates state directly + # via populate_module_and_state + replacement = state_input else: for step_input in step.inputs: if step_input.name == prefixed_name and step_input.default_value_set: diff --git a/lib/galaxy_test/api/test_workflows.py b/lib/galaxy_test/api/test_workflows.py index 8331271c38a5..c42da227984e 100644 --- a/lib/galaxy_test/api/test_workflows.py +++ b/lib/galaxy_test/api/test_workflows.py @@ -6982,6 +6982,35 @@ def test_parameter_substitution_validation_value_errors_0(self): # Take a valid stat and make it invalid, assert workflow won't run. self._assert_status_code_is(invocation_response, 400) + @skip_without_tool("collection_paired_test") + def test_run_map_over_with_step_parameter_dict(self): + # Tests what the legacy run form submits + with self.dataset_populator.test_history() as history_id: + hdca = self.dataset_collection_populator.create_list_of_pairs_in_history(history_id).json()["outputs"][0] + workflow_id = self._upload_yaml_workflow( + """ +class: GalaxyWorkflow +steps: + "0": + tool_id: collection_paired_conditional_structured_like + state: + cond: + input1: + __class__: RuntimeValue +""" + ) + workflow_request = { + "history": f"hist_id={history_id}", + "parameters": dumps({"0": {"cond|input1": {"values": [{"id": hdca["id"], "src": "hdca"}]}}}), + "parameters_normalized": True, + } + url = f"workflows/{workflow_id}/invocations" + invocation_response = self._post(url, data=workflow_request, json=True) + invocation_response.raise_for_status() + self.workflow_populator.wait_for_invocation_and_jobs( + history_id=history_id, workflow_id=workflow_id, invocation_id=invocation_response.json()["id"] + ) + @skip_without_tool("validation_default") def test_parameter_substitution_validation_value_errors_1(self): substitions = dict(select_param='" ; echo "moo') diff --git a/test/unit/app/tools/test_parameter_parsing.py b/test/unit/app/tools/test_parameter_parsing.py index 28d7565ff1a8..842e7df91844 100644 --- a/test/unit/app/tools/test_parameter_parsing.py +++ b/test/unit/app/tools/test_parameter_parsing.py @@ -3,10 +3,28 @@ Dict, ) -from galaxy.tools.parameters.wrapped import process_key +from galaxy.tools.parameters.wrapped import ( + nested_key_to_path, + process_key, +) from .util import BaseParameterTestCase +def test_nested_key_to_path(): + assert nested_key_to_path("param") == ["param"] + assert nested_key_to_path("param_x") == ["param_x"] + assert nested_key_to_path("cond|param_x") == ["cond", "param_x"] + assert nested_key_to_path("param_") == ["param_"] + assert nested_key_to_path("cond|param_") == ["cond", "param_"] + assert nested_key_to_path("repeat_1|inner_repeat_1|data_table_column_value") == [ + "repeat", + 1, + "inner_repeat", + 1, + "data_table_column_value", + ] + + class TestProcessKey: def test_process_key(self): nested_dict: Dict[str, Any] = {}