From 3ed87bff0a88c43c5f5018023d8f2b8e2fbd0323 Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Tue, 6 Sep 2022 14:01:14 +0200 Subject: [PATCH 01/43] linter: allow options elements in data params linting of valid childs for params has been added here: https://github.com/galaxyproject/galaxy/pull/12232 options for data params has been forgotten in addition also checks for valid attribs and filter types has been added to linting --- lib/galaxy/tool_util/linters/inputs.py | 19 ++++++++- test/unit/tool_util/test_tool_linters.py | 52 +++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/linters/inputs.py b/lib/galaxy/tool_util/linters/inputs.py index 6a7e602f7c0..3fcf9b5068f 100644 --- a/lib/galaxy/tool_util/linters/inputs.py +++ b/lib/galaxy/tool_util/linters/inputs.py @@ -108,7 +108,7 @@ PARAMETER_VALIDATOR_TYPE_COMPATIBILITY = { } PARAM_TYPE_CHILD_COMBINATIONS = [ - ("./options", ["select", "drill_down"]), + ("./options", ["data", "select", "drill_down"]), ("./options/option", ["drill_down"]), ("./column", ["data_column"]), ] @@ -167,6 +167,23 @@ def lint_inputs(tool_xml, lint_ctx): lint_ctx.warn( f"Param input [{param_name}] with no format specified - 'data' format will be assumed.", node=param ) + options = param.findall("./options") + if len(options) == 1: + if len(options[0].attrib) > 0: + lint_ctx.error( + f"Data parameter [{param_name}] uses invalid attributes: {options[0].attrib}", node=param + ) + elif len(options) > 1: + lint_ctx.error(f"Data parameter [{param_name}] contains multiple options elements.", node=options[1]) + # for data params only filters with key='build' of type='data_meta' are allowed + filters = param.findall("./options/filter") + for f in filters: + if f.get("key") != "dbkey" or f.get("type") != "data_meta": + lint_ctx.error( + f'Data parameter [{param_name}] for filters only type="data_meta" and key="dbkey" are allowed, found type="{f.get("type")}" and key="{f.get("key")}"', + node=f, + ) + elif param_type == "select": # get dynamic/statically defined options dynamic_options = param.get("dynamic_options", None) diff --git a/test/unit/tool_util/test_tool_linters.py b/test/unit/tool_util/test_tool_linters.py index cbc0a2fc88d..2d28f3b8531 100644 --- a/test/unit/tool_util/test_tool_linters.py +++ b/test/unit/tool_util/test_tool_linters.py @@ -190,6 +190,31 @@ INPUTS_DATA_PARAM = """ """ +INPUTS_DATA_PARAM_OPTIONS = """ + + + + + + + + + +""" + +INPUTS_DATA_PARAM_INVALIDOPTIONS = """ + + + + + + + + + + +""" + INPUTS_CONDITIONAL = """ @@ -1012,6 +1037,31 @@ def test_inputs_data_param(lint_ctx): assert not lint_ctx.error_messages +def test_inputs_data_param_options(lint_ctx): + tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_OPTIONS) + run_lint(lint_ctx, inputs.lint_inputs, tool_source) + assert not lint_ctx.valid_messages + assert "Found 1 input parameters." in lint_ctx.info_messages + assert len(lint_ctx.info_messages) == 1 + assert not lint_ctx.warn_messages + assert not lint_ctx.error_messages + + +def test_inputs_data_param_invalidoptions(lint_ctx): + tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_INVALIDOPTIONS) + run_lint(lint_ctx, inputs.lint_inputs, tool_source) + assert not lint_ctx.valid_messages + assert "Found 1 input parameters." in lint_ctx.info_messages + assert len(lint_ctx.info_messages) == 1 + assert not lint_ctx.warn_messages + assert "Data parameter [valid_name] contains multiple options elements." in lint_ctx.error_messages + assert ( + 'Data parameter [valid_name] for filters only type="data_meta" and key="dbkey" are allowed, found type="expression" and key="None"' + in lint_ctx.error_messages + ) + assert len(lint_ctx.error_messages) == 2 + + def test_inputs_conditional(lint_ctx): tool_source = get_xml_tool_source(INPUTS_CONDITIONAL) run_lint(lint_ctx, inputs.lint_inputs, tool_source) @@ -1197,7 +1247,7 @@ def test_inputs_type_child_combinations(lint_ctx): assert not lint_ctx.valid_messages assert not lint_ctx.warn_messages assert ( - "Parameter [text_param] './options' tags are only allowed for parameters of type ['select', 'drill_down']" + "Parameter [text_param] './options' tags are only allowed for parameters of type ['data', 'select', 'drill_down']" in lint_ctx.error_messages ) assert ( From 6ebfa1ca6620203d75d319d912dd7fe002ca8ec1 Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Tue, 6 Sep 2022 14:20:56 +0200 Subject: [PATCH 02/43] also improve docs --- lib/galaxy/tool_util/xsd/galaxy.xsd | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 90cc1ccf6dd..603f40d9b0b 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -3968,14 +3968,18 @@ dataset for the contained input of the type specified using the ``type`` tag. `` tag when the ``type`` attribute value is ``select`` or -``data`` and used to dynamically generated lists of options. This tag set -dynamically creates a list of options whose values can be -obtained from a predefined file stored locally or a dataset selected from the -current history. +``data`` and used to dynamically generated lists of options. + +For data parameters it can only be used to restrict inputs to datasets having +the same ``dbkey`` like another input by using a ``data_meta`` filter. See for +instance here: [/tools/maf/interval2maf.xml](https://github.com/galaxyproject/galaxy/blob/master/tools/maf/interval2maf.xml) + +For select parameters this tag set dynamically creates a list of options whose +values can be obtained from a predefined file stored locally or a dataset +selected from the current history. There are at least five basic ways to use this tag - four of these correspond to a ``from_XXX`` attribute on the ``options`` directive and the other is to From 1cb1ac3dfd4649a45071c10882fd7f2fcb6e8597 Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Mon, 19 Sep 2022 10:24:49 +0200 Subject: [PATCH 03/43] adapt linter to check also for options_filter_attribute --- lib/galaxy/tool_util/linters/inputs.py | 26 +++++++++++++++++------ test/unit/tool_util/test_tool_linters.py | 27 ++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/tool_util/linters/inputs.py b/lib/galaxy/tool_util/linters/inputs.py index 3fcf9b5068f..16455b51a32 100644 --- a/lib/galaxy/tool_util/linters/inputs.py +++ b/lib/galaxy/tool_util/linters/inputs.py @@ -168,21 +168,35 @@ def lint_inputs(tool_xml, lint_ctx): f"Param input [{param_name}] with no format specified - 'data' format will be assumed.", node=param ) options = param.findall("./options") + has_options_filter_attribute = False if len(options) == 1: - if len(options[0].attrib) > 0: - lint_ctx.error( - f"Data parameter [{param_name}] uses invalid attributes: {options[0].attrib}", node=param - ) + for oa in options[0].attrib: + if oa == "options_filter_attribute": + has_options_filter_attribute = True + else: + lint_ctx.error(f"Data parameter [{param_name}] uses invalid attribute: {oa}", node=param) elif len(options) > 1: lint_ctx.error(f"Data parameter [{param_name}] contains multiple options elements.", node=options[1]) # for data params only filters with key='build' of type='data_meta' are allowed filters = param.findall("./options/filter") for f in filters: - if f.get("key") != "dbkey" or f.get("type") != "data_meta": + if not f.get("ref"): lint_ctx.error( - f'Data parameter [{param_name}] for filters only type="data_meta" and key="dbkey" are allowed, found type="{f.get("type")}" and key="{f.get("key")}"', + f"Data parameter [{param_name}] filter needs to define a ref attribute", node=f, ) + if has_options_filter_attribute: + if f.get("type") != "data_meta": + lint_ctx.error( + f'Data parameter [{param_name}] for filters only type="data_meta" is allowed, found type="{f.get("type")}"', + node=f, + ) + else: + if f.get("key") != "dbkey" or f.get("type") != "data_meta": + lint_ctx.error( + f'Data parameter [{param_name}] for filters only type="data_meta" and key="dbkey" are allowed, found type="{f.get("type")}" and key="{f.get("key")}"', + node=f, + ) elif param_type == "select": # get dynamic/statically defined options diff --git a/test/unit/tool_util/test_tool_linters.py b/test/unit/tool_util/test_tool_linters.py index 2d28f3b8531..533dcd31e36 100644 --- a/test/unit/tool_util/test_tool_linters.py +++ b/test/unit/tool_util/test_tool_linters.py @@ -195,7 +195,19 @@ INPUTS_DATA_PARAM_OPTIONS = """ - + + + + + +""" + +INPUTS_DATA_PARAM_OPTIONS_FILTER_ATTRIBUTE = """ + + + + + @@ -1047,6 +1059,16 @@ def test_inputs_data_param_options(lint_ctx): assert not lint_ctx.error_messages +def test_inputs_data_param_options_filter_attribute(lint_ctx): + tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_OPTIONS_FILTER_ATTRIBUTE) + run_lint(lint_ctx, inputs.lint_inputs, tool_source) + assert not lint_ctx.valid_messages + assert "Found 1 input parameters." in lint_ctx.info_messages + assert len(lint_ctx.info_messages) == 1 + assert not lint_ctx.warn_messages + assert not lint_ctx.error_messages + + def test_inputs_data_param_invalidoptions(lint_ctx): tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_INVALIDOPTIONS) run_lint(lint_ctx, inputs.lint_inputs, tool_source) @@ -1055,11 +1077,12 @@ def test_inputs_data_param_invalidoptions(lint_ctx): assert len(lint_ctx.info_messages) == 1 assert not lint_ctx.warn_messages assert "Data parameter [valid_name] contains multiple options elements." in lint_ctx.error_messages + assert "Data parameter [valid_name] filter needs to define a ref attribute" in lint_ctx.error_messages assert ( 'Data parameter [valid_name] for filters only type="data_meta" and key="dbkey" are allowed, found type="expression" and key="None"' in lint_ctx.error_messages ) - assert len(lint_ctx.error_messages) == 2 + assert len(lint_ctx.error_messages) == 3 def test_inputs_conditional(lint_ctx): From e7d738569c3215ece224d567e703f9dcc6b6121d Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Sun, 29 Jan 2023 13:50:13 +0100 Subject: [PATCH 04/43] add unit test for is_a_yaml_with_class as used in planemo --- test/unit/tool_util/test_loader_directory.py | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/unit/tool_util/test_loader_directory.py diff --git a/test/unit/tool_util/test_loader_directory.py b/test/unit/tool_util/test_loader_directory.py new file mode 100644 index 00000000000..8a35da215d8 --- /dev/null +++ b/test/unit/tool_util/test_loader_directory.py @@ -0,0 +1,23 @@ +import os +import tempfile + +from galaxy.tool_util.loader_directory import is_a_yaml_with_class + +def test_is_a_yaml_with_class(): + with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as tf: + fname = tf.name + tf.write("""class: GalaxyWorkflow +name: "Test Workflow" +inputs: + - id: input1 +outputs: + - id: wf_output_1 + outputSource: first_cat/out_file1 +steps: + - tool_id: cat + label: first_cat + in: + input1: input1""") + + assert is_a_yaml_with_class(fname, ["GalaxyWorkflow"]) + os.unlink(fname) \ No newline at end of file From 4c4cc2c75bc998b1f52f6b3d6e0e3c2c34337cec Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Sun, 29 Jan 2023 13:14:30 +0100 Subject: [PATCH 05/43] fix looks_like_yaml_or_cwl_with_class the string `class: ...` might appear on the first line hence the leading `\n` was wrong I guess its better to use `^` and `$` and add the MULTILINE flag --- lib/galaxy/tool_util/loader_directory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/loader_directory.py b/lib/galaxy/tool_util/loader_directory.py index fd0e73e58f5..1491f5ab047 100644 --- a/lib/galaxy/tool_util/loader_directory.py +++ b/lib/galaxy/tool_util/loader_directory.py @@ -190,7 +190,7 @@ def looks_like_a_data_manager_xml(path): def as_dict_if_looks_like_yaml_or_cwl_with_class(path, classes): """ - get a dict from yaml file if it contains `class: CLASS`, where CLASS is + get a dict from yaml file if it contains a line `class: CLASS`, where CLASS is any string given in CLASSES. must appear in the first 5k and also load properly in total. """ @@ -199,7 +199,7 @@ def as_dict_if_looks_like_yaml_or_cwl_with_class(path, classes): start_contents = f.read(5 * 1024) except UnicodeDecodeError: return False, None - if re.search(rf"\nclass:\s+({'|'.join(classes)})\s*\n", start_contents) is None: + if re.search(rf"^class:\s+{'|'.join(classes)}\s*$", start_contents, re.MULTILINE) is None: return False, None with open(path) as f: From 63b0c7e23b2614341646cef07be6c602017547d7 Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Sun, 29 Jan 2023 15:45:21 +0100 Subject: [PATCH 06/43] linter fixes --- test/unit/tool_util/test_loader_directory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/tool_util/test_loader_directory.py b/test/unit/tool_util/test_loader_directory.py index 8a35da215d8..3b808caa1c6 100644 --- a/test/unit/tool_util/test_loader_directory.py +++ b/test/unit/tool_util/test_loader_directory.py @@ -3,6 +3,7 @@ import tempfile from galaxy.tool_util.loader_directory import is_a_yaml_with_class + def test_is_a_yaml_with_class(): with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as tf: fname = tf.name @@ -20,4 +21,4 @@ steps: input1: input1""") assert is_a_yaml_with_class(fname, ["GalaxyWorkflow"]) - os.unlink(fname) \ No newline at end of file + os.unlink(fname) From 92d06c26e7fc13db96e798ae3e64a20923005a20 Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Sun, 29 Jan 2023 16:05:32 +0100 Subject: [PATCH 07/43] black formatting fixes --- test/unit/tool_util/test_loader_directory.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/tool_util/test_loader_directory.py b/test/unit/tool_util/test_loader_directory.py index 3b808caa1c6..6b25364349a 100644 --- a/test/unit/tool_util/test_loader_directory.py +++ b/test/unit/tool_util/test_loader_directory.py @@ -7,7 +7,8 @@ from galaxy.tool_util.loader_directory import is_a_yaml_with_class def test_is_a_yaml_with_class(): with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as tf: fname = tf.name - tf.write("""class: GalaxyWorkflow + tf.write( + """class: GalaxyWorkflow name: "Test Workflow" inputs: - id: input1 @@ -18,7 +19,8 @@ steps: - tool_id: cat label: first_cat in: - input1: input1""") + input1: input1""" + ) assert is_a_yaml_with_class(fname, ["GalaxyWorkflow"]) os.unlink(fname) From 8ec08f6665894fb5ab2de8387b44daa1745e8be9 Mon Sep 17 00:00:00 2001 From: M Bernt Date: Mon, 30 Jan 2023 10:38:29 +0100 Subject: [PATCH 08/43] Test improvements Co-authored-by: Marius van den Beek --- test/unit/tool_util/test_loader_directory.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/unit/tool_util/test_loader_directory.py b/test/unit/tool_util/test_loader_directory.py index 6b25364349a..08ec1f5a51c 100644 --- a/test/unit/tool_util/test_loader_directory.py +++ b/test/unit/tool_util/test_loader_directory.py @@ -1,11 +1,10 @@ -import os import tempfile from galaxy.tool_util.loader_directory import is_a_yaml_with_class def test_is_a_yaml_with_class(): - with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as tf: + with tempfile.NamedTemporaryFile("w", suffix=".yaml") as tf: fname = tf.name tf.write( """class: GalaxyWorkflow @@ -21,6 +20,5 @@ steps: in: input1: input1""" ) - - assert is_a_yaml_with_class(fname, ["GalaxyWorkflow"]) - os.unlink(fname) + tf.flush() + assert is_a_yaml_with_class(fname, ["GalaxyWorkflow"]) From fb60ec257f7e0d62364c8c0d640cb2a5ad290a39 Mon Sep 17 00:00:00 2001 From: M Bernt Date: Mon, 30 Jan 2023 11:26:50 +0100 Subject: [PATCH 09/43] Fix test name typo Co-authored-by: Marius van den Beek --- test/unit/tool_util/test_tool_linters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tool_util/test_tool_linters.py b/test/unit/tool_util/test_tool_linters.py index 533dcd31e36..ff722d2e091 100644 --- a/test/unit/tool_util/test_tool_linters.py +++ b/test/unit/tool_util/test_tool_linters.py @@ -1069,7 +1069,7 @@ def test_inputs_data_param_options_filter_attribute(lint_ctx): assert not lint_ctx.error_messages -def test_inputs_data_param_invalidoptions(lint_ctx): +def test_inputs_data_param_invalid_options(lint_ctx): tool_source = get_xml_tool_source(INPUTS_DATA_PARAM_INVALIDOPTIONS) run_lint(lint_ctx, inputs.lint_inputs, tool_source) assert not lint_ctx.valid_messages From 327e4221895ff67efe4accb5eea98606f594162f Mon Sep 17 00:00:00 2001 From: M Bernt Date: Mon, 30 Jan 2023 12:40:05 +0100 Subject: [PATCH 10/43] Reformulate doc --- lib/galaxy/tool_util/xsd/galaxy.xsd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index 603f40d9b0b..7db7caf5928 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -3973,8 +3973,7 @@ for an example of how to use this tag set. This tag set is optionally contained within the ```` tag when the ``type`` attribute value is ``select`` or ``data`` and used to dynamically generated lists of options. -For data parameters it can only be used to restrict inputs to datasets having -the same ``dbkey`` like another input by using a ``data_meta`` filter. See for +For data parameters this tag can be used to restrict possible input datasets to datasets that match the ``dbkey`` of another data input by including a ``data_meta`` filter. See for instance here: [/tools/maf/interval2maf.xml](https://github.com/galaxyproject/galaxy/blob/master/tools/maf/interval2maf.xml) For select parameters this tag set dynamically creates a list of options whose From f8cf8ed7f56bd04a10078bbd48e45baffc1f8b50 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 23 Jan 2023 19:16:45 +0100 Subject: [PATCH 11/43] Mark disconnected required inputs in editor --- .../components/Workflow/Editor/NodeInput.vue | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/src/components/Workflow/Editor/NodeInput.vue b/client/src/components/Workflow/Editor/NodeInput.vue index c0f89069fd2..08fad856f2c 100644 --- a/client/src/components/Workflow/Editor/NodeInput.vue +++ b/client/src/components/Workflow/Editor/NodeInput.vue @@ -17,6 +17,13 @@ @click="onRemove" @keyup.delete="onRemove" /> {{ label }} + + * + @@ -214,3 +221,16 @@ export default { }, }; + + From c2a25138b454a42a6736e008a8e1454461e5c17c Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 23 Jan 2023 19:17:25 +0100 Subject: [PATCH 12/43] Reset extra step connection for when when removing input --- client/src/stores/workflowStepStore.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts index 20057d984f1..034a98ab1ff 100644 --- a/client/src/stores/workflowStepStore.ts +++ b/client/src/stores/workflowStepStore.ts @@ -252,7 +252,11 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", { }, removeConnection(connection: Connection) { const inputStep = this.getStep(connection.input.stepId); - Vue.delete(inputStep.input_connections, connection.input.name); + if (this.getStepExtraInputs(inputStep.id).find((input) => connection.input.name === input.name)) { + inputStep.input_connections[connection.input.name] = undefined; + } else { + Vue.delete(inputStep.input_connections, connection.input.name); + } this.updateStep(inputStep); }, removeStep(this: State, stepId: number) { From d0752096128a0043d2b15fec5e486560793e479a Mon Sep 17 00:00:00 2001 From: Martin Cech Date: Mon, 30 Jan 2023 10:05:57 -0800 Subject: [PATCH 13/43] correct the url in password reset links --- lib/galaxy/managers/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index a3b31ec8843..622a91da2de 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -537,7 +537,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): reset_user, prt = self.get_reset_token(trans, email) if prt: host = self.__get_host(trans) - reset_url = url_for(controller="root", action="login", token=prt.token) + reset_url = url_for(controller="login", action="start", token=prt.token) body = PASSWORD_RESET_TEMPLATE % ( host, prt.expiration_time.strftime(trans.app.config.pretty_datetime_format), From 035e1cf6f7a9a0e1950cdf492348abe4b497013f Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 31 Jan 2023 12:13:08 +0100 Subject: [PATCH 14/43] Add Selenium test that verifies condiional step handling --- client/src/utils/navigation/navigation.yml | 5 ++ .../selenium/test_workflow_editor.py | 61 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml index 7114c497544..9f614fd7579 100644 --- a/client/src/utils/navigation/navigation.yml +++ b/client/src/utils/navigation/navigation.yml @@ -615,6 +615,11 @@ workflow_editor: type: xpath selector: > //div[@id='form-element-__annotation']//textarea + step_when: + type: xpath + selector: > + //div[@id='form-element-__conditional']//input + param_type_form: '#parameter_definition\|parameter_type' configure_output: type: xpath selector: > diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py index eb40f60ff96..24686289d39 100644 --- a/lib/galaxy_test/selenium/test_workflow_editor.py +++ b/lib/galaxy_test/selenium/test_workflow_editor.py @@ -747,6 +747,67 @@ steps: workflow = self.workflow_populator.download_workflow(workflow_id) assert len(workflow["steps"]) == 3 + @selenium_test + def test_editor_create_conditional_step(self): + editor = self.components.workflow_editor + self.workflow_create_new(annotation="simple when step definition") + # Insert a boolean parameter + self.workflow_editor_add_input(item_name="parameter_input") + param_type_element = editor.param_type_form.wait_for_present() + self.switch_param_type(param_type_element, "Boolean") + editor.label_input.wait_for_and_send_keys("param_input") + editor.tool_menu.wait_for_visible() + # Insert cat tool + self.tool_open("cat") + self.sleep_for(self.wait_types.UX_RENDER) + editor.label_input.wait_for_and_send_keys("downstream_step") + # Insert head tool + self.tool_open("head") + self.workflow_editor_click_option("Auto Layout") + self.sleep_for(self.wait_types.UX_RENDER) + editor.label_input.wait_for_and_send_keys("conditional_step") + # Connect head to cat + self.workflow_editor_connect("conditional_step#out_file1", "downstream_step#input1") + self.assert_connected("conditional_step#out_file1", "downstream_step#input1") + # Make head tool conditional + conditional_node = editor.node._(label="conditional_step") + conditional_node.input_terminal(name="input").wait_for_present() + # Assert no when input before making step conditional + conditional_node.input_terminal(name="when").wait_for_absent() + conditional_toggle = editor.step_when.wait_for_present() + self.action_chains().move_to_element(conditional_toggle).click().perform() + # Toggling conditional should cause when input to appear + conditional_node.input_terminal(name="when").wait_for_present() + self.action_chains().move_to_element(conditional_toggle).click().perform() + # Toggling conditional should cause when input to disappear + conditional_node.input_terminal(name="when").wait_for_absent() + self.action_chains().move_to_element(conditional_toggle).click().perform() + conditional_node.input_terminal(name="when").wait_for_present() + # Output connection should be invalid, as output from conditional step is potentially null + self.assert_connection_invalid("conditional_step#out_file1", "downstream_step#input1") + downstream_step = editor.node._(label="downstream_step") + downstream_step.destroy.wait_for_and_click() + downstream_step.wait_for_absent() + # Connect boolean input to when + self.workflow_editor_connect("param_input#output", "conditional_step#when") + self.assert_connected("param_input#output", "conditional_step#when") + # Change boolean input parameter to invalid parameter type + editor.node._(label="param_input").wait_for_and_click() + param_type_element = editor.param_type_form.wait_for_present() + self.switch_param_type(param_type_element, "Text") + self.assert_connection_invalid("param_input#output", "conditional_step#when") + self.workflow_editor_destroy_connection("conditional_step#when") + # Make sure the when input is still shown + conditional_node.input_terminal(name="when").wait_for_present() + # Assert save button is disabled because of disconnected when + save_button = self.components.workflow_editor.save_button + save_button.wait_for_visible() + # TODO: hook up best practice panel, disable save when "when" not connected + # assert save_button.has_class("disabled") + + def switch_param_type(self, element, param_type): + self.action_chains().move_to_element(element).click().send_keys(param_type).send_keys(Keys.ENTER).perform() + @selenium_test def test_editor_invalid_tool_state(self): workflow_populator = self.workflow_populator From cc725cfc9d246f0e89477fea27f459f5117101ed Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 31 Jan 2023 08:19:39 -0500 Subject: [PATCH 15/43] Prevent tool panel from sorting workflows list -- it was noted that this is, unlike the tool sections, actually a chronologically ordered list that is more useful that way --- client/src/components/Panels/Common/ToolSection.vue | 5 +++++ client/src/components/Panels/ToolBoxWorkflow.vue | 1 + 2 files changed, 6 insertions(+) diff --git a/client/src/components/Panels/Common/ToolSection.vue b/client/src/components/Panels/Common/ToolSection.vue index c57738e49e3..39942c94cff 100644 --- a/client/src/components/Panels/Common/ToolSection.vue +++ b/client/src/components/Panels/Common/ToolSection.vue @@ -97,6 +97,10 @@ export default { type: Boolean, default: false, }, + sortItems: { + type: Boolean, + default: true, + }, }, setup() { const { config, isLoaded } = useConfig(); @@ -135,6 +139,7 @@ export default { if ( this.isLoaded && this.config.toolbox_auto_sort === true && + this.sortItems === true && !this.category.elems.some((el) => el.text !== undefined && el.text !== "") ) { const elements = [...this.category.elems]; diff --git a/client/src/components/Panels/ToolBoxWorkflow.vue b/client/src/components/Panels/ToolBoxWorkflow.vue index bcb26c53f60..6acdd736a5a 100644 --- a/client/src/components/Panels/ToolBoxWorkflow.vue +++ b/client/src/components/Panels/ToolBoxWorkflow.vue @@ -59,6 +59,7 @@ :key="workflowSection.name" :category="workflowSection" section-name="workflows" + sort-items="false" operation-icon="fa fa-files-o" operation-title="Insert individual steps." :query-filter="query" From c0eb66173ea9d2fd0e24d69bd23705a67b2582df Mon Sep 17 00:00:00 2001 From: Matthias Bernt Date: Tue, 31 Jan 2023 15:33:14 +0100 Subject: [PATCH 16/43] restore rst_invalid function used in planemo https://github.com/galaxyproject/planemo/pull/1275 has been removed here https://github.com/galaxyproject/galaxy/pull/14588 --- lib/galaxy/tool_util/linters/help.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/tool_util/linters/help.py b/lib/galaxy/tool_util/linters/help.py index 1d5776a8c56..c47eb848cb7 100644 --- a/lib/galaxy/tool_util/linters/help.py +++ b/lib/galaxy/tool_util/linters/help.py @@ -30,10 +30,22 @@ def lint_help(tool_xml, lint_ctx): if "TODO" in help_text: lint_ctx.warn("Help contains TODO text.", node=helps[0]) - try: - rst_to_html(help_text, error=True) - except Exception as e: - lint_ctx.warn(f"Invalid reStructuredText found in help - [{unicodify(e)}].", node=helps[0]) - return + invalid_rst = rst_invalid(help_text) + if invalid_rst: + lint_ctx.warn(f"Invalid reStructuredText found in help - [{invalid_rst}].", node=helps[0]) + else: + lint_ctx.valid("Help contains valid reStructuredText.", node=helps[0]) - lint_ctx.valid("Help contains valid reStructuredText.", node=helps[0]) + +def rst_invalid(text): + """ + Predicate to determine if text is invalid reStructuredText. + Return False if the supplied text is valid reStructuredText or + a string indicating the problem. + """ + invalid_rst = False + try: + rst_to_html(text, error=True) + except Exception as e: + invalid_rst = unicodify(e) + return invalid_rst From d6dff77d5cce5afc1cb9ede61347a290dc4bb48d Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 31 Jan 2023 09:47:51 -0500 Subject: [PATCH 17/43] Fix some usage of types from non-ts (yet) files. --- client/src/components/Workflow/Editor/Forms/FormOutput.vue | 4 ++-- client/src/components/Workflow/Editor/Forms/FormSection.vue | 4 ++-- client/src/components/Workflow/Editor/NodeInput.vue | 4 ++-- client/src/components/Workflow/Editor/WorkflowMinimap.vue | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/client/src/components/Workflow/Editor/Forms/FormOutput.vue b/client/src/components/Workflow/Editor/Forms/FormOutput.vue index 12e858b61d2..cc1b2a49c8a 100644 --- a/client/src/components/Workflow/Editor/Forms/FormOutput.vue +++ b/client/src/components/Workflow/Editor/Forms/FormOutput.vue @@ -84,7 +84,6 @@ import FormCard from "@/components/Form/FormCard"; import FormElement from "@/components/Form/FormElement"; import FormOutputLabel from "@/components/Workflow/Editor/Forms/FormOutputLabel"; -import { Step } from "@/stores/workflowStepStore"; const actions = [ "RenameDatasetAction__newname", @@ -129,7 +128,8 @@ export default { required: true, }, step: { - type: Step, + // type Step from "@/stores/workflowStepStore"; + type: Object, required: true, }, }, diff --git a/client/src/components/Workflow/Editor/Forms/FormSection.vue b/client/src/components/Workflow/Editor/Forms/FormSection.vue index b924842cd71..d86ab98d969 100644 --- a/client/src/components/Workflow/Editor/Forms/FormSection.vue +++ b/client/src/components/Workflow/Editor/Forms/FormSection.vue @@ -30,7 +30,6 @@ + + + + diff --git a/client/src/components/Workflow/Editor/MinimapNode.vue b/client/src/components/Workflow/Editor/MinimapNode.vue deleted file mode 100644 index 6fec83da375..00000000000 --- a/client/src/components/Workflow/Editor/MinimapNode.vue +++ /dev/null @@ -1,27 +0,0 @@ - - diff --git a/client/src/components/Workflow/Editor/Node.vue b/client/src/components/Workflow/Editor/Node.vue index 0d7abe9abd6..f10d5e629a2 100644 --- a/client/src/components/Workflow/Editor/Node.vue +++ b/client/src/components/Workflow/Editor/Node.vue @@ -176,7 +176,12 @@ const connectionStore = useConnectionStore(); const stateStore = useWorkflowStateStore(); const stepStore = useWorkflowStepStore(); const isLoading = computed(() => Boolean(stateStore.getStepLoadingState(props.id)?.loading)); -useNodePosition(el, props.id, stateStore); +useNodePosition( + el, + props.id, + stateStore, + computed(() => props.scale) +); const title = computed(() => props.step.label || props.step.name); const idString = computed(() => `wf-node-step-${props.id}`); const showRule = computed(() => props.step.inputs?.length > 0 && props.step.outputs?.length > 0); diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue index 3dc2aa26b14..b1909a92d56 100644 --- a/client/src/components/Workflow/Editor/WorkflowGraph.vue +++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue @@ -29,9 +29,9 @@ @@ -40,7 +40,7 @@ import ZoomControl from "@/components/Workflow/Editor/ZoomControl.vue"; import WorkflowNode from "@/components/Workflow/Editor/Node.vue"; import WorkflowEdges from "@/components/Workflow/Editor/WorkflowEdges.vue"; -import WorkflowMinimap from "@/components/Workflow/Editor/WorkflowMinimap.vue"; +import WorkflowMinimap from "@/components/Workflow/Editor/Minimap.vue"; import { computed, provide, reactive, ref, watch, type Ref, type PropType, watchEffect } from "vue"; import { useElementBounding, useScroll } from "@vueuse/core"; import { storeToRefs } from "pinia"; diff --git a/client/src/components/Workflow/Editor/composables/useNodePosition.ts b/client/src/components/Workflow/Editor/composables/useNodePosition.ts index 2ce4e85c31d..ec51e0f08a1 100644 --- a/client/src/components/Workflow/Editor/composables/useNodePosition.ts +++ b/client/src/components/Workflow/Editor/composables/useNodePosition.ts @@ -1,14 +1,33 @@ import { useElementBounding } from "@vueuse/core"; -import { onUnmounted, reactive, type Ref } from "vue"; +import { onUnmounted, unref, watch, type ComputedRef, type Ref } from "vue"; import type { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; export function useNodePosition( nodeRef: Ref, stepId: number, - workflowStateStore: ReturnType + workflowStateStore: ReturnType, + scale: ComputedRef | Ref ) { const position = useElementBounding(nodeRef, { windowResize: false }); - workflowStateStore.setStepPosition(stepId, reactive(position)); + + watch( + Object.values(position), + () => { + workflowStateStore.setStepPosition(stepId, { + height: unref(position.height) / scale.value, + width: unref(position.width) / scale.value, + left: unref(position.left) / scale.value, + right: unref(position.right) / scale.value, + top: unref(position.top) / scale.value, + bottom: unref(position.bottom) / scale.value, + x: unref(position.x) / scale.value, + y: unref(position.y) / scale.value, + update: position.update, + }); + }, + { immediate: true } + ); + onUnmounted(() => { workflowStateStore.deleteStepPosition(stepId); }); diff --git a/client/src/components/Workflow/Editor/modules/geomerty.ts b/client/src/components/Workflow/Editor/modules/geomerty.ts new file mode 100644 index 00000000000..840c7cdd18a --- /dev/null +++ b/client/src/components/Workflow/Editor/modules/geomerty.ts @@ -0,0 +1,83 @@ +export interface Rectangle { + x: number; + y: number; + width: number; + height: number; +} + +export class AxisAlignedBoundingBox implements Rectangle { + /** x coordinate of left edge */ + x = Infinity; + + /** y coordinate of upper edge */ + y = Infinity; + + /** x coordinate of right edge */ + endX = -Infinity; + + /** y coordinate of lower edge */ + endY = -Infinity; + + get width() { + return this.endX - this.x; + } + + set width(value) { + this.endX = this.x + value; + } + + get height() { + return this.endY - this.y; + } + + set height(value) { + this.endY = this.y + value; + } + + reset() { + this.x = Infinity; + this.y = Infinity; + this.endX = -Infinity; + this.endY = -Infinity; + } + + /** expand bounding box to fit a rectangle */ + fitRectangle(rect: Readonly) { + if (this.x > rect.x) { + this.x = rect.x; + } + + if (this.y > rect.y) { + this.y = rect.y; + } + + if (this.endX < rect.x + rect.width) { + this.endX = rect.x + rect.width; + } + + if (this.endY < rect.y + rect.height) { + this.endY = rect.y + rect.height; + } + } + + /** make width and height the same, maintaining the center of the bounding box */ + squareCenter() { + if (this.width > this.height) { + const difference = this.width - this.height; + this.y -= difference * 0.5; + this.endY += difference * 0.5; + } else { + const difference = this.height - this.width; + this.x -= difference * 0.5; + this.endX += difference * 0.5; + } + } + + /** expand bounding box in every direction */ + expand(by: number) { + this.x -= by; + this.y -= by; + this.endX += by; + this.endY += by; + } +} diff --git a/client/src/style/scss/workflow.scss b/client/src/style/scss/workflow.scss index a3ca99cf01d..dbbbbca99bd 100644 --- a/client/src/style/scss/workflow.scss +++ b/client/src/style/scss/workflow.scss @@ -210,10 +210,9 @@ } .workflow-overview { border-top-left-radius: 0.3rem; - cursor: pointer; + cursor: nwse-resize; position: absolute; - width: 150px; - height: 150px; + width: 100%; right: 0px; bottom: 0px; border-top: solid $border-color 1px; @@ -230,6 +229,7 @@ stroke: #25537b; } .workflow-overview-body { + cursor: pointer; position: relative; overflow: hidden; width: 100%; From 813c1758b714ed74bf9ffb7c7deec1b8feacff40 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 10:30:11 +0100 Subject: [PATCH 22/43] add transform class --- .../Workflow/Editor/modules/geomerty.ts | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/client/src/components/Workflow/Editor/modules/geomerty.ts b/client/src/components/Workflow/Editor/modules/geomerty.ts index 840c7cdd18a..344dbca2cb2 100644 --- a/client/src/components/Workflow/Editor/modules/geomerty.ts +++ b/client/src/components/Workflow/Editor/modules/geomerty.ts @@ -19,7 +19,8 @@ export class AxisAlignedBoundingBox implements Rectangle { endY = -Infinity; get width() { - return this.endX - this.x; + const width = this.endX - this.x; + return width > 0 ? width : 0; } set width(value) { @@ -27,7 +28,8 @@ export class AxisAlignedBoundingBox implements Rectangle { } get height() { - return this.endY - this.y; + const height = this.endY - this.y; + return height > 0 ? height : 0; } set height(value) { @@ -81,3 +83,83 @@ export class AxisAlignedBoundingBox implements Rectangle { this.endY += by; } } + +/* Format + [a b + c d + e f] + as used by canvas: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform +*/ +// prettier-ignore +export type Matrix = [ + number, number, + number, number, + number, number, +]; + +export type Vector = [number, number]; + +export function scaleVector(vector: Vector, scale: number): Vector { + return [vector[0] * scale, vector[1] * scale]; +} + +export function addVector(vectorA: Vector, vectorB: Vector): Vector { + return [vectorA[0] + vectorB[0], vectorA[1] + vectorB[1]]; +} + +export class Transform { + matrix: Matrix; + + constructor(matrix: Matrix = [0, 1, 0, 1, 0, 0]) { + this.matrix = matrix; + } + + translate(vector: Vector) { + // prettier-ignore + return new Transform([ + this.matrix[0], this.matrix[1], + this.matrix[2], this.matrix[3], + this.matrix[4] + vector[0], this.matrix[5] + vector[1] + ]); + } + + scale(vector: Vector) { + // prettier-ignore + return new Transform([ + this.matrix[0] * vector[0], this.matrix[1] * vector[1], + this.matrix[2] * vector[0], this.matrix[3] * vector[1], + this.matrix[4], this.matrix[5] + ]); + } + + inverse() { + const m = this.matrix; + // https://www.wolframalpha.com/input?i=Inverse+%5B%7B%7Ba%2Cc%2Ce%7D%2C%7Bb%2Cd%2Cf%7D%2C%7B0%2C0%2C1%7D%7D%5D + const denominator = m[0] * m[3] - m[1] * m[2]; + + const a = m[3] / denominator; + const b = m[1] / -denominator; + const c = m[2] / -denominator; + const d = m[0] / denominator; + const e = (m[3] * m[4] - m[2] * m[5]) / -denominator; + const f = (m[1] * m[4] - m[0] * m[5]) / denominator; + + // prettier-ignore + return new Transform([ + a, b, + c, d, + e, f + ]); + } + + applyToContext(ctx: CanvasRenderingContext2D): void { + ctx.transform(...this.matrix); + } + + apply(vector: Vector): Vector { + return [ + this.matrix[0] * vector[0] + this.matrix[2] * vector[1] + this.matrix[4], + this.matrix[1] * vector[0] + this.matrix[3] * vector[1] + this.matrix[5], + ]; + } +} From 4fe7cdf6a8de470a2b61a0a4806d9e731595cb0c Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 10:54:39 +0100 Subject: [PATCH 23/43] add methods to transform fix normal transform --- .../Workflow/Editor/modules/geomerty.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/src/components/Workflow/Editor/modules/geomerty.ts b/client/src/components/Workflow/Editor/modules/geomerty.ts index 344dbca2cb2..f5a37ef37c6 100644 --- a/client/src/components/Workflow/Editor/modules/geomerty.ts +++ b/client/src/components/Workflow/Editor/modules/geomerty.ts @@ -110,7 +110,7 @@ export function addVector(vectorA: Vector, vectorB: Vector): Vector { export class Transform { matrix: Matrix; - constructor(matrix: Matrix = [0, 1, 0, 1, 0, 0]) { + constructor(matrix: Matrix = [1, 0, 0, 1, 0, 0]) { this.matrix = matrix; } @@ -162,4 +162,21 @@ export class Transform { this.matrix[1] * vector[0] + this.matrix[3] * vector[1] + this.matrix[5], ]; } + + resetTranslation(): Transform { + // prettier-ignore + return new Transform ([ + this.matrix[0], this.matrix[1], + this.matrix[2], this.matrix[3], + 0, 0 + ]); + } + + get scaleX() { + return this.matrix[0]; + } + + get scaleY() { + return this.matrix[3]; + } } From 7af5444cc0a58fb1a0c861f712949baf7684d34b Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 10:55:03 +0100 Subject: [PATCH 24/43] use css var to determine size --- client/src/style/scss/workflow.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/style/scss/workflow.scss b/client/src/style/scss/workflow.scss index dbbbbca99bd..2339da41246 100644 --- a/client/src/style/scss/workflow.scss +++ b/client/src/style/scss/workflow.scss @@ -209,10 +209,13 @@ @extend .mr-1; } .workflow-overview { + --workflow-overview-size: 150px; + border-top-left-radius: 0.3rem; cursor: nwse-resize; position: absolute; - width: 100%; + width: var(--workflow-overview-size); + height: var(--workflow-overview-size); right: 0px; bottom: 0px; border-top: solid $border-color 1px; From 0cf1394c845cec700a2ac60ac2dca1a64ed55ba5 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 12:19:39 +0100 Subject: [PATCH 25/43] add in bounds check function --- client/src/components/Workflow/Editor/modules/geomerty.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/components/Workflow/Editor/modules/geomerty.ts b/client/src/components/Workflow/Editor/modules/geomerty.ts index f5a37ef37c6..a0633a68f09 100644 --- a/client/src/components/Workflow/Editor/modules/geomerty.ts +++ b/client/src/components/Workflow/Editor/modules/geomerty.ts @@ -82,6 +82,14 @@ export class AxisAlignedBoundingBox implements Rectangle { this.endX += by; this.endY += by; } + + isPointInBounds(point: { x: number; y: number }) { + if (point.x > this.x && point.y > this.y && point.x < this.endX && point.y < this.endY) { + return true; + } else { + return false; + } + } } /* Format From 5c74b6c7defbe1d12ce2825e17a35e8043155ace Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 12:20:22 +0100 Subject: [PATCH 26/43] add resize and grad drive with css variables --- .../components/Workflow/Editor/Minimap.vue | 142 +++++++++++++++--- client/src/style/scss/workflow.scss | 21 ++- 2 files changed, 136 insertions(+), 27 deletions(-) diff --git a/client/src/components/Workflow/Editor/Minimap.vue b/client/src/components/Workflow/Editor/Minimap.vue index aa88db7192a..d553b15d8ff 100644 --- a/client/src/components/Workflow/Editor/Minimap.vue +++ b/client/src/components/Workflow/Editor/Minimap.vue @@ -1,11 +1,12 @@ @@ -194,7 +296,7 @@ function renderMinimap() { .workflow-overview-body { --node-color: #{$brand-primary}; --error-color: #{$brand-warning}; - --selected-outline-color: #{$brand-info}; + --selected-outline-color: #{$brand-primary}; --view-color: #{fade-out($brand-dark, 0.8)}; --view-outline-color: #{$brand-info}; } diff --git a/client/src/style/scss/workflow.scss b/client/src/style/scss/workflow.scss index 2339da41246..c39a39c9bee 100644 --- a/client/src/style/scss/workflow.scss +++ b/client/src/style/scss/workflow.scss @@ -210,6 +210,10 @@ } .workflow-overview { --workflow-overview-size: 150px; + --workflow-overview-min-size: 50px; + --workflow-overview-max-size: 300px; + --workflow-overview-padding: 7px; + --workflow-overview-border: 1px; border-top-left-radius: 0.3rem; cursor: nwse-resize; @@ -218,16 +222,19 @@ height: var(--workflow-overview-size); right: 0px; bottom: 0px; - border-top: solid $border-color 1px; - border-left: solid $border-color 1px; - padding: 7px 0 0 7px; + border-top: solid $border-color var(--workflow-overview-border); + border-left: solid $border-color var(--workflow-overview-border); background: $workflow-overview-bg no-repeat url("../../assets/images/resizable.png"); z-index: 20000; overflow: hidden; - max-width: 300px; - max-height: 300px; - min-width: 50px; - min-height: 50px; + padding: var(--workflow-overview-padding) 0 0 var(--workflow-overview-padding); + + // account for padding and border + max-width: calc(var(--workflow-overview-max-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); + max-height: calc(var(--workflow-overview-max-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); + min-width: calc(var(--workflow-overview-min-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); + min-height: calc(var(--workflow-overview-min-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); + .viewport { stroke: #25537b; } From 419b50b5127f56c09b6d18521ca13bf6b00f5719 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 12:20:40 +0100 Subject: [PATCH 27/43] remove old minimap --- .../Workflow/Editor/WorkflowMinimap.vue | 172 ------------------ 1 file changed, 172 deletions(-) delete mode 100644 client/src/components/Workflow/Editor/WorkflowMinimap.vue diff --git a/client/src/components/Workflow/Editor/WorkflowMinimap.vue b/client/src/components/Workflow/Editor/WorkflowMinimap.vue deleted file mode 100644 index 5a4a53bba08..00000000000 --- a/client/src/components/Workflow/Editor/WorkflowMinimap.vue +++ /dev/null @@ -1,172 +0,0 @@ - - From bd795786937b9360d3860248c09a5ac4c463283a Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 12:22:55 +0100 Subject: [PATCH 28/43] remove unused css --- client/src/style/scss/workflow.scss | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/client/src/style/scss/workflow.scss b/client/src/style/scss/workflow.scss index c39a39c9bee..cf316c1ca81 100644 --- a/client/src/style/scss/workflow.scss +++ b/client/src/style/scss/workflow.scss @@ -235,9 +235,6 @@ min-width: calc(var(--workflow-overview-min-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); min-height: calc(var(--workflow-overview-min-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); - .viewport { - stroke: #25537b; - } .workflow-overview-body { cursor: pointer; position: relative; @@ -245,19 +242,6 @@ width: 100%; height: 100%; } - .mini-node { - + .ok { - // this is $primary-brand / #25537b - filter: invert(26%) sepia(75%) saturate(489%) hue-rotate(166deg) brightness(90%) contrast(89%); - } - + .error { - // this is #e31a1e - filter: invert(16%) sepia(86%) saturate(3962%) hue-rotate(350deg) brightness(96%) contrast(97%); - } - + .highlight { - filter: invert(14%) sepia(38%) saturate(1544%) hue-rotate(321deg) brightness(84%) contrast(117%); - } - } } #input-choices-menu { color: black; From dacb91fcc0fbb222493ddd54672cece9d0f9e5ac Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 12:48:06 +0100 Subject: [PATCH 29/43] add annotations unify event casing --- .../components/Workflow/Editor/Minimap.vue | 119 ++++++++++-------- .../Workflow/Editor/WorkflowGraph.vue | 2 +- .../Workflow/Editor/modules/geomerty.ts | 22 ++++ 3 files changed, 89 insertions(+), 54 deletions(-) diff --git a/client/src/components/Workflow/Editor/Minimap.vue b/client/src/components/Workflow/Editor/Minimap.vue index d553b15d8ff..f973007ca4f 100644 --- a/client/src/components/Workflow/Editor/Minimap.vue +++ b/client/src/components/Workflow/Editor/Minimap.vue @@ -28,18 +28,69 @@ const props = defineProps({ }); const emit = defineEmits<{ - (e: "pan-by", offset: { x: number; y: number }): void; + (e: "panBy", offset: { x: number; y: number }): void; (e: "moveTo", position: { x: number; y: number }): void; }>(); +const stateStore = useWorkflowStateStore(); + +/** reference to the main canvas element */ const canvas: Ref = ref(null); let redraw = false; -let aabbChanged = false; -const stateStore = useWorkflowStateStore(); + +/** bounding box encompassing all nodes in the workflow */ const aabb = new AxisAlignedBoundingBox(); +let aabbChanged = false; +/** transform mapping workflow coordinates to minimap coordinates */ +let canvasTransform = new Transform(); + +function recalculateAABB() { + aabb.reset(); + + Object.values(props.steps).forEach((step) => { + const rect = stateStore.stepPosition[step.id]; + aabb.fitRectangle({ + x: step.position!.left, + y: step.position!.top, + width: rect.width, + height: rect.height, + }); + }); + + aabb.squareCenter(); + aabb.expand(120); + + // transform canvas to show entire workflow bounding box + if (canvas.value) { + const scale = canvas.value.width / aabb.width; + canvasTransform = new Transform().translate([-aabb.x * scale, -aabb.y * scale]).scale([scale, scale]); + } +} + +// redraw if any of these props change watch(props.viewportBounds, () => (redraw = true), { deep: true }); +watch( + props.steps, + () => { + redraw = true; + aabbChanged = true; + }, + { deep: true } +); +watch( + () => { + props.viewportScale; + props.viewportPan; + }, + () => { + redraw = true; + }, + { deep: true } +); +// these settings are controlled via css, so they can be defined in one common place +// this ensures future style changes wont break the minimap's behavior const colors = { node: "#000", error: "#000", @@ -57,8 +108,6 @@ const size = { }; onMounted(() => { - // these settings are controlled via css, so they can be defined in one common place - // this ensures future style changes wont break the minimap's behavior const element = canvas.value!; const style = getComputedStyle(element); @@ -78,50 +127,7 @@ onMounted(() => { redraw = true; }); -watch( - props.steps, - () => { - redraw = true; - aabbChanged = true; - }, - { deep: true } -); - -watch( - () => { - props.viewportScale; - props.viewportPan; - }, - () => { - redraw = true; - }, - { deep: true } -); - -let canvasTransform = new Transform(); - -function recalculateAABB() { - aabb.reset(); - - Object.values(props.steps).forEach((step) => { - const rect = stateStore.stepPosition[step.id]; - aabb.fitRectangle({ - x: step.position!.left, - y: step.position!.top, - width: rect.width, - height: rect.height, - }); - }); - - aabb.squareCenter(); - aabb.expand(120); - - if (canvas.value) { - const scale = canvas.value.width / aabb.width; - canvasTransform = new Transform().translate([-aabb.x * scale, -aabb.y * scale]).scale([scale, scale]); - } -} - +// for performance reasons, only draw and calculate on animation frames. useAnimationFrame(() => { if (aabbChanged) { recalculateAABB(); @@ -134,11 +140,13 @@ useAnimationFrame(() => { } }); +/** Renders the entire minimap to the canvas */ function renderMinimap() { const ctx = canvas.value!.getContext("2d") as CanvasRenderingContext2D; ctx.resetTransform(); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + // apply global to local transform canvasTransform.applyToContext(ctx); const allSteps = Object.values(props.steps); @@ -146,6 +154,7 @@ function renderMinimap() { const errorSteps: Step[] = []; let selectedStep: Step | undefined; + // sort steps into different arrays allSteps.forEach((step) => { if (stateStore.activeNodeId === step.id) { selectedStep = step; @@ -175,6 +184,7 @@ function renderMinimap() { }); ctx.fill(); + // draw selected if (selectedStep) { const edge = 2 / canvasTransform.scaleX; @@ -206,7 +216,7 @@ function renderMinimap() { ctx.stroke(); } -// resizing +// -- Resizing -- const minimap: Ref = ref(null); const { position: dragHandlePosition, isDragging: isHandleDragging } = useDraggable(minimap, { preventDefault: true, @@ -231,18 +241,20 @@ watch(isHandleDragging, () => { } }); -// repositioning +// -- Repositioning Viewport -- const scaleFactor = computed(() => size.max / minimapSize.value); let dragViewport = false; useDraggable(canvas, { onStart: (position, event) => { + // convert viewport to bounds const bounds = new AxisAlignedBoundingBox(); bounds.x = -props.viewportPan.x / props.viewportScale; bounds.y = -props.viewportPan.y / props.viewportScale; bounds.width = unref(props.viewportBounds.width) / props.viewportScale; bounds.height = unref(props.viewportBounds.height) / props.viewportScale; + // minimap coordinates to global coordinates const [x, y] = canvasTransform .inverse() .scale([scaleFactor.value, scaleFactor.value]) @@ -257,15 +269,17 @@ useDraggable(canvas, { return; } + // minimap coordinates to global coordinates, without translation const [x, y] = canvasTransform .resetTranslation() .inverse() .scale([scaleFactor.value, scaleFactor.value]) .apply([-event.movementX, -event.movementY]); - emit("pan-by", { x, y }); + emit("panBy", { x, y }); }, onEnd(position, event) { + // minimap coordinates to global coordinates const [x, y] = canvasTransform .inverse() .scale([scaleFactor.value, scaleFactor.value]) @@ -277,7 +291,6 @@ useDraggable(canvas, { dragViewport = false; }, - exact: true, }); diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue index b1909a92d56..a53d9ee6e3b 100644 --- a/client/src/components/Workflow/Editor/WorkflowGraph.vue +++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue @@ -32,7 +32,7 @@ :viewport-bounds="elementBounding" :viewport-scale="scale" :viewport-pan="transform" - @pan-by="panBy" + @panBy="panBy" @moveTo="moveTo" /> diff --git a/client/src/components/Workflow/Editor/modules/geomerty.ts b/client/src/components/Workflow/Editor/modules/geomerty.ts index a0633a68f09..f277f107d07 100644 --- a/client/src/components/Workflow/Editor/modules/geomerty.ts +++ b/client/src/components/Workflow/Editor/modules/geomerty.ts @@ -1,3 +1,4 @@ +/** simple rectangle without rotation */ export interface Rectangle { x: number; y: number; @@ -5,6 +6,12 @@ export interface Rectangle { height: number; } +/** + * Class compatible with rectangle interface. + * Provides additional properties and methods specific to bounding boxes. + * Useful to calculate the bounds of multiple rectangles, + * using the `fitRectangle` method. + */ export class AxisAlignedBoundingBox implements Rectangle { /** x coordinate of left edge */ x = Infinity; @@ -83,6 +90,7 @@ export class AxisAlignedBoundingBox implements Rectangle { this.endY += by; } + /** check if a point is inside the bounding box */ isPointInBounds(point: { x: number; y: number }) { if (point.x > this.x && point.y > this.y && point.x < this.endX && point.y < this.endY) { return true; @@ -105,16 +113,24 @@ export type Matrix = [ number, number, ]; +/** vector as a tuple */ export type Vector = [number, number]; +/** scale a vector by a fixed value */ export function scaleVector(vector: Vector, scale: number): Vector { return [vector[0] * scale, vector[1] * scale]; } +/** add two vectors together */ export function addVector(vectorA: Vector, vectorB: Vector): Vector { return [vectorA[0] + vectorB[0], vectorA[1] + vectorB[1]]; } +/** + * Wraps basic transform operations. + * Each operation returns a new instance, so method calls can be chained + * without mutating the initial transform. + */ export class Transform { matrix: Matrix; @@ -122,6 +138,7 @@ export class Transform { this.matrix = matrix; } + /** returns a new transform with a translation vector added */ translate(vector: Vector) { // prettier-ignore return new Transform([ @@ -131,6 +148,7 @@ export class Transform { ]); } + /** returns a new transform scaled by a given vector */ scale(vector: Vector) { // prettier-ignore return new Transform([ @@ -140,6 +158,7 @@ export class Transform { ]); } + /** Returns the inverse vector. Can be used to un-transform things */ inverse() { const m = this.matrix; // https://www.wolframalpha.com/input?i=Inverse+%5B%7B%7Ba%2Cc%2Ce%7D%2C%7Bb%2Cd%2Cf%7D%2C%7B0%2C0%2C1%7D%7D%5D @@ -160,10 +179,12 @@ export class Transform { ]); } + /** applies this transform to a rendering context */ applyToContext(ctx: CanvasRenderingContext2D): void { ctx.transform(...this.matrix); } + /** returns a vector transformed by this transform */ apply(vector: Vector): Vector { return [ this.matrix[0] * vector[0] + this.matrix[2] * vector[1] + this.matrix[4], @@ -171,6 +192,7 @@ export class Transform { ]; } + /** removes the translation portion of the vector */ resetTranslation(): Transform { // prettier-ignore return new Transform ([ From 23b9fb8e5e134dd05445da94945cbfa38077c11a Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 14:37:30 +0100 Subject: [PATCH 30/43] move viewportBounds to computed --- .../components/Workflow/Editor/Minimap.vue | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/client/src/components/Workflow/Editor/Minimap.vue b/client/src/components/Workflow/Editor/Minimap.vue index f973007ca4f..55ca3db24df 100644 --- a/client/src/components/Workflow/Editor/Minimap.vue +++ b/client/src/components/Workflow/Editor/Minimap.vue @@ -34,6 +34,17 @@ const emit = defineEmits<{ const stateStore = useWorkflowStateStore(); +/** bounding box following the viewport */ +const viewportBounds = computed(() => { + const bounds = new AxisAlignedBoundingBox(); + bounds.x = -props.viewportPan.x / props.viewportScale; + bounds.y = -props.viewportPan.y / props.viewportScale; + bounds.width = unref(props.viewportBounds.width) / props.viewportScale; + bounds.height = unref(props.viewportBounds.height) / props.viewportScale; + + return bounds; +}); + /** reference to the main canvas element */ const canvas: Ref = ref(null); let redraw = false; @@ -69,7 +80,7 @@ function recalculateAABB() { } // redraw if any of these props change -watch(props.viewportBounds, () => (redraw = true), { deep: true }); +watch(viewportBounds, () => (redraw = true)); watch( props.steps, () => { @@ -78,16 +89,6 @@ watch( }, { deep: true } ); -watch( - () => { - props.viewportScale; - props.viewportPan; - }, - () => { - redraw = true; - }, - { deep: true } -); // these settings are controlled via css, so they can be defined in one common place // this ensures future style changes wont break the minimap's behavior @@ -206,12 +207,7 @@ function renderMinimap() { ctx.strokeStyle = colors.viewOutline; ctx.fillStyle = colors.view; ctx.lineWidth = 1 / canvasTransform.scaleX; - ctx.rect( - -props.viewportPan.x / props.viewportScale, - -props.viewportPan.y / props.viewportScale, - unref(props.viewportBounds.width) / props.viewportScale, - unref(props.viewportBounds.height) / props.viewportScale - ); + ctx.rect(viewportBounds.value.x, viewportBounds.value.y, viewportBounds.value.width, viewportBounds.value.height); ctx.fill(); ctx.stroke(); } @@ -247,20 +243,13 @@ let dragViewport = false; useDraggable(canvas, { onStart: (position, event) => { - // convert viewport to bounds - const bounds = new AxisAlignedBoundingBox(); - bounds.x = -props.viewportPan.x / props.viewportScale; - bounds.y = -props.viewportPan.y / props.viewportScale; - bounds.width = unref(props.viewportBounds.width) / props.viewportScale; - bounds.height = unref(props.viewportBounds.height) / props.viewportScale; - // minimap coordinates to global coordinates const [x, y] = canvasTransform .inverse() .scale([scaleFactor.value, scaleFactor.value]) .apply([event.offsetX, event.offsetY]); - if (bounds.isPointInBounds({ x, y })) { + if (viewportBounds.value.isPointInBounds({ x, y })) { dragViewport = true; } }, From da22978da548dda7cfd57e089d4f3ec69fa0557d Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 14:43:04 +0100 Subject: [PATCH 31/43] add annotation --- client/src/components/Workflow/Editor/Minimap.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/components/Workflow/Editor/Minimap.vue b/client/src/components/Workflow/Editor/Minimap.vue index 55ca3db24df..2e7ff14287c 100644 --- a/client/src/components/Workflow/Editor/Minimap.vue +++ b/client/src/components/Workflow/Editor/Minimap.vue @@ -238,6 +238,8 @@ watch(isHandleDragging, () => { }); // -- Repositioning Viewport -- + +/** Scaling factor of the canvas element. Draw size in relation to actual size on screen */ const scaleFactor = computed(() => size.max / minimapSize.value); let dragViewport = false; From 725f9199f374e835ae7ddab89d0103456d1d9236 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 16:08:58 +0100 Subject: [PATCH 32/43] move prop definition to typedef --- .../components/Workflow/Editor/Minimap.vue | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/client/src/components/Workflow/Editor/Minimap.vue b/client/src/components/Workflow/Editor/Minimap.vue index 2e7ff14287c..eb1c7640cd0 100644 --- a/client/src/components/Workflow/Editor/Minimap.vue +++ b/client/src/components/Workflow/Editor/Minimap.vue @@ -6,26 +6,14 @@ import { AxisAlignedBoundingBox, Transform } from "./modules/geomerty"; import { useDraggable, type UseElementBoundingReturn } from "@vueuse/core"; import type { Step, Steps } from "@/stores/workflowStepStore"; -import type { PropType, Ref } from "vue"; +import type { Ref } from "vue"; -const props = defineProps({ - steps: { - type: Object as PropType, - required: true, - }, - viewportBounds: { - type: Object as PropType, - required: true, - }, - viewportPan: { - type: Object as PropType<{ x: number; y: number }>, - required: true, - }, - viewportScale: { - type: Number, - required: true, - }, -}); +const props = defineProps<{ + steps: Steps; + viewportBounds: UseElementBoundingReturn; + viewportPan: { x: number; y: number }; + viewportScale: number; +}>(); const emit = defineEmits<{ (e: "panBy", offset: { x: number; y: number }): void; From 5ad8d3535013d4e7b9bbc302ba80d92379a45c66 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 19:04:51 +0100 Subject: [PATCH 33/43] remove unused geometry functions --- .../src/components/Workflow/Editor/modules/geomerty.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/src/components/Workflow/Editor/modules/geomerty.ts b/client/src/components/Workflow/Editor/modules/geomerty.ts index f277f107d07..bbf06de7173 100644 --- a/client/src/components/Workflow/Editor/modules/geomerty.ts +++ b/client/src/components/Workflow/Editor/modules/geomerty.ts @@ -116,16 +116,6 @@ export type Matrix = [ /** vector as a tuple */ export type Vector = [number, number]; -/** scale a vector by a fixed value */ -export function scaleVector(vector: Vector, scale: number): Vector { - return [vector[0] * scale, vector[1] * scale]; -} - -/** add two vectors together */ -export function addVector(vectorA: Vector, vectorB: Vector): Vector { - return [vectorA[0] + vectorB[0], vectorA[1] + vectorB[1]]; -} - /** * Wraps basic transform operations. * Each operation returns a new instance, so method calls can be chained From a1ceb23e9c54332c544fc638eeebad903108e0f6 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 31 Jan 2023 19:05:25 +0100 Subject: [PATCH 34/43] fix spelling --- client/src/components/Workflow/Editor/Minimap.vue | 2 +- .../Workflow/Editor/modules/{geomerty.ts => geometry.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename client/src/components/Workflow/Editor/modules/{geomerty.ts => geometry.ts} (100%) diff --git a/client/src/components/Workflow/Editor/Minimap.vue b/client/src/components/Workflow/Editor/Minimap.vue index eb1c7640cd0..74e0addb29c 100644 --- a/client/src/components/Workflow/Editor/Minimap.vue +++ b/client/src/components/Workflow/Editor/Minimap.vue @@ -2,7 +2,7 @@ import { computed, onMounted, ref, unref, watch } from "vue"; import { useAnimationFrame } from "@/composables/sensors/animationFrame"; import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; -import { AxisAlignedBoundingBox, Transform } from "./modules/geomerty"; +import { AxisAlignedBoundingBox, Transform } from "./modules/geometry"; import { useDraggable, type UseElementBoundingReturn } from "@vueuse/core"; import type { Step, Steps } from "@/stores/workflowStepStore"; diff --git a/client/src/components/Workflow/Editor/modules/geomerty.ts b/client/src/components/Workflow/Editor/modules/geometry.ts similarity index 100% rename from client/src/components/Workflow/Editor/modules/geomerty.ts rename to client/src/components/Workflow/Editor/modules/geometry.ts From e5324761957d955ccf652c76b62bde7bd53ca719 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 31 Jan 2023 14:29:43 -0500 Subject: [PATCH 35/43] prettier --- client/src/style/scss/workflow.scss | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/client/src/style/scss/workflow.scss b/client/src/style/scss/workflow.scss index cf316c1ca81..c16fc0af9af 100644 --- a/client/src/style/scss/workflow.scss +++ b/client/src/style/scss/workflow.scss @@ -230,10 +230,22 @@ padding: var(--workflow-overview-padding) 0 0 var(--workflow-overview-padding); // account for padding and border - max-width: calc(var(--workflow-overview-max-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); - max-height: calc(var(--workflow-overview-max-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); - min-width: calc(var(--workflow-overview-min-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); - min-height: calc(var(--workflow-overview-min-size) + var(--workflow-overview-padding) + var(--workflow-overview-border)); + max-width: calc( + var(--workflow-overview-max-size) + var(--workflow-overview-padding) + + var(--workflow-overview-border) + ); + max-height: calc( + var(--workflow-overview-max-size) + var(--workflow-overview-padding) + + var(--workflow-overview-border) + ); + min-width: calc( + var(--workflow-overview-min-size) + var(--workflow-overview-padding) + + var(--workflow-overview-border) + ); + min-height: calc( + var(--workflow-overview-min-size) + var(--workflow-overview-padding) + + var(--workflow-overview-border) + ); .workflow-overview-body { cursor: pointer; From 346a94f5df0298a09c3d85938333d8272021c33f Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 31 Jan 2023 14:32:28 -0500 Subject: [PATCH 36/43] Multi-word component name --- client/src/components/Workflow/Editor/WorkflowGraph.vue | 2 +- .../Workflow/Editor/{Minimap.vue => WorkflowMinimap.vue} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename client/src/components/Workflow/Editor/{Minimap.vue => WorkflowMinimap.vue} (100%) diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue index a53d9ee6e3b..c4853f34f83 100644 --- a/client/src/components/Workflow/Editor/WorkflowGraph.vue +++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue @@ -40,7 +40,7 @@ import ZoomControl from "@/components/Workflow/Editor/ZoomControl.vue"; import WorkflowNode from "@/components/Workflow/Editor/Node.vue"; import WorkflowEdges from "@/components/Workflow/Editor/WorkflowEdges.vue"; -import WorkflowMinimap from "@/components/Workflow/Editor/Minimap.vue"; +import WorkflowMinimap from "@/components/Workflow/Editor/WorkflowMinimap.vue"; import { computed, provide, reactive, ref, watch, type Ref, type PropType, watchEffect } from "vue"; import { useElementBounding, useScroll } from "@vueuse/core"; import { storeToRefs } from "pinia"; diff --git a/client/src/components/Workflow/Editor/Minimap.vue b/client/src/components/Workflow/Editor/WorkflowMinimap.vue similarity index 100% rename from client/src/components/Workflow/Editor/Minimap.vue rename to client/src/components/Workflow/Editor/WorkflowMinimap.vue From f09be47dccf16a6b2605ed1fafb5c830fec86e0c Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 31 Jan 2023 14:43:06 -0500 Subject: [PATCH 37/43] Fix runtime type checking for toolbox sorting --- client/src/components/Panels/ToolBoxWorkflow.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Panels/ToolBoxWorkflow.vue b/client/src/components/Panels/ToolBoxWorkflow.vue index 6acdd736a5a..251ad3c8497 100644 --- a/client/src/components/Panels/ToolBoxWorkflow.vue +++ b/client/src/components/Panels/ToolBoxWorkflow.vue @@ -59,7 +59,7 @@ :key="workflowSection.name" :category="workflowSection" section-name="workflows" - sort-items="false" + :sort-items="false" operation-icon="fa fa-files-o" operation-title="Insert individual steps." :query-filter="query" From 9420f3a9e11c2a364593283560c9191958da388d Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 31 Jan 2023 15:17:27 -0500 Subject: [PATCH 38/43] More comfortable initial pan for workflow editor --- client/src/components/Workflow/Editor/WorkflowGraph.vue | 2 +- client/src/components/Workflow/Editor/composables/d3Zoom.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue index 3dc2aa26b14..10444a39e24 100644 --- a/client/src/components/Workflow/Editor/WorkflowGraph.vue +++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue @@ -68,7 +68,7 @@ const canvas: Ref = ref(null); const elementBounding = useElementBounding(canvas, { windowResize: false, windowScroll: false }); const scroll = useScroll(canvas); -const { transform, panBy, setZoom, moveTo } = useD3Zoom(1, minZoom, maxZoom, canvas, scroll); +const { transform, panBy, setZoom, moveTo } = useD3Zoom(1, minZoom, maxZoom, canvas, scroll, { x: 20, y: 20 }); const isDragging = ref(false); provide("isDragging", isDragging); diff --git a/client/src/components/Workflow/Editor/composables/d3Zoom.ts b/client/src/components/Workflow/Editor/composables/d3Zoom.ts index 17df1fc74dd..07ecb321784 100644 --- a/client/src/components/Workflow/Editor/composables/d3Zoom.ts +++ b/client/src/components/Workflow/Editor/composables/d3Zoom.ts @@ -17,9 +17,10 @@ export function useD3Zoom( minZoom: number, maxZoom: number, targetRef: Ref, - scroll: UseScrollReturn + scroll: UseScrollReturn, + initialPan: XYPosition = { x: 0, y: 0 } ) { - const transform = ref({ x: 0, y: 0, k: k }); + const transform = ref({ x: initialPan.x, y: initialPan.y, k: k }); const d3Zoom = zoom().filter(filter).scaleExtent([minZoom, maxZoom]); watch(targetRef, () => { From 86e4511e6c013e3f818d44e849a3c85ce203b7b6 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 31 Jan 2023 15:50:21 -0500 Subject: [PATCH 39/43] Fix minimap node background color to use the same as nodes in error --- client/src/components/Workflow/Editor/WorkflowMinimap.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/Workflow/Editor/WorkflowMinimap.vue b/client/src/components/Workflow/Editor/WorkflowMinimap.vue index 74e0addb29c..4327e843f1b 100644 --- a/client/src/components/Workflow/Editor/WorkflowMinimap.vue +++ b/client/src/components/Workflow/Editor/WorkflowMinimap.vue @@ -283,11 +283,12 @@ useDraggable(canvas, {