From a05ec3fcf833992ee0c966888add61eda6f400cf Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 21 Nov 2024 11:19:20 -0500 Subject: [PATCH 001/176] 24.2 release testing - UI tests for new workflow parameters --- .../components/Workflow/Run/WorkflowRun.vue | 7 +- client/src/utils/navigation/navigation.yml | 2 + lib/galaxy/selenium/navigates_galaxy.py | 17 ++- .../selenium/test_workflow_editor.py | 115 +++++++++++++++--- 4 files changed, 120 insertions(+), 21 deletions(-) diff --git a/client/src/components/Workflow/Run/WorkflowRun.vue b/client/src/components/Workflow/Run/WorkflowRun.vue index 18a9389654eb..dea37e56adc0 100644 --- a/client/src/components/Workflow/Run/WorkflowRun.vue +++ b/client/src/components/Workflow/Run/WorkflowRun.vue @@ -195,7 +195,12 @@ defineExpose({ before running this workflow.
- + Workflow submission failed: {{ submissionError }} None: license_selector = self.components.workflow_editor.license_selector license_selector.wait_for_and_click() @@ -1618,7 +1631,9 @@ def workflow_run_ensure_expanded(self): workflow_run.expand_form_link.wait_for_and_click() workflow_run.expanded_form.wait_for_visible() - def workflow_create_new(self, annotation=None, clear_placeholder=False, save_workflow=True): + def workflow_create_new( + self, annotation: Optional[str] = None, clear_placeholder: bool = False, save_workflow: bool = True + ): self.workflow_index_open() self.sleep_for(self.wait_types.UX_RENDER) self.click_button_new_workflow() diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py index af8ba29fbc53..cbd3c5f0f31b 100644 --- a/lib/galaxy_test/selenium/test_workflow_editor.py +++ b/lib/galaxy_test/selenium/test_workflow_editor.py @@ -69,7 +69,7 @@ def test_basics(self): def test_edit_annotation(self): editor = self.components.workflow_editor annotation = "new_annotation_test" - name = self.workflow_create_new(annotation=annotation) + name = self.create_and_wait_for_new_workflow_in_editor(annotation=annotation) edit_annotation = self.components.workflow_editor.edit_annotation self.assert_wf_annotation_is(annotation) @@ -83,9 +83,7 @@ def test_edit_annotation(self): @selenium_test def test_edit_name(self): - editor = self.components.workflow_editor - name = self.workflow_create_new() - editor.canvas_body.wait_for_visible() + name = self.create_and_wait_for_new_workflow_in_editor() new_name = self._get_random_name() edit_name = self.components.workflow_editor.edit_name edit_name.wait_for_and_send_keys(new_name) @@ -97,8 +95,7 @@ def test_edit_name(self): @selenium_test def test_edit_license(self): editor = self.components.workflow_editor - name = self.workflow_create_new() - editor.canvas_body.wait_for_visible() + name = self.create_and_wait_for_new_workflow_in_editor() editor.license_selector.wait_for_visible() assert "Do not specify" in editor.license_current_value.wait_for_text() @@ -109,6 +106,87 @@ def test_edit_license(self): editor.license_selector.wait_for_visible() assert "MIT" in editor.license_current_value.wait_for_text() + @selenium_test + def test_parameter_regex_validation(self): + editor = self.components.workflow_editor + workflow_run = self.components.workflow_run + + parameter_name = "text_param" + name = self.create_and_wait_for_new_workflow_in_editor() + self.workflow_editor_add_parameter_input() + editor.label_input.wait_for_and_send_keys(parameter_name) + # this really should be parameterized with the repeat name + self.components.tool_form.repeat_insert.wait_for_and_click() + self.components.tool_form.parameter_input( + parameter="parameter_definition|validators_0|regex_match" + ).wait_for_and_send_keys("moocow.*") + self.components.tool_form.parameter_input( + parameter="parameter_definition|validators_0|regex_doc" + ).wait_for_and_send_keys("input must start with moocow") + self.save_after_node_form_changes() + + self.workflow_run_with_name(name) + self.sleep_for(self.wait_types.UX_TRANSITION) + input_element = workflow_run.simplified_input(label=parameter_name).wait_for_and_click() + input_element.send_keys("startswrong") + workflow_run.run_workflow_disabled.wait_for_absent() + workflow_run.run_error.assert_absent_or_hidden() + self.workflow_run_submit() + element = workflow_run.run_error.wait_for_present() + assert "input must start with moocow" in element.text + + @selenium_test + def test_int_parameter_minimum_validation(self): + editor = self.components.workflow_editor + workflow_run = self.components.workflow_run + + parameter_name = "int_param" + name = self.create_and_wait_for_new_workflow_in_editor() + self.workflow_editor_add_parameter_input() + editor.label_input.wait_for_and_send_keys(parameter_name) + select_field = self.components.tool_form.parameter_select(parameter="parameter_definition|parameter_type") + self.select_set_value(select_field, "integer") + self.components.tool_form.parameter_input(parameter="parameter_definition|min").wait_for_and_send_keys("4") + self.save_after_node_form_changes() + + self.workflow_run_with_name(name) + self.sleep_for(self.wait_types.UX_TRANSITION) + input_element = workflow_run.simplified_input(label=parameter_name).wait_for_and_click() + input_element.send_keys("3") + workflow_run.run_workflow_disabled.wait_for_absent() + workflow_run.run_error.assert_absent_or_hidden() + self.workflow_run_submit() + element = workflow_run.run_error.wait_for_present() + # follow up with a bigger PR to just make this (4 <= value) right? need to set default message + # in parameter validators + assert "Value ('3') must fulfill (4 <= value <= +infinity)" in element.text, element.text + + @selenium_test + def test_float_parameter_maximum_validation(self): + editor = self.components.workflow_editor + workflow_run = self.components.workflow_run + + parameter_name = "float_param" + name = self.create_and_wait_for_new_workflow_in_editor() + self.workflow_editor_add_parameter_input() + editor.label_input.wait_for_and_send_keys(parameter_name) + select_field = self.components.tool_form.parameter_select(parameter="parameter_definition|parameter_type") + self.select_set_value(select_field, "float") + self.components.tool_form.parameter_input(parameter="parameter_definition|max").wait_for_and_send_keys("3.14") + self.save_after_node_form_changes() + + self.workflow_run_with_name(name) + self.sleep_for(self.wait_types.UX_TRANSITION) + input_element = workflow_run.simplified_input(label=parameter_name).wait_for_and_click() + input_element.send_keys("3.2") + workflow_run.run_workflow_disabled.wait_for_absent() + workflow_run.run_error.assert_absent_or_hidden() + self.workflow_run_submit() + element = workflow_run.run_error.wait_for_present() + # see message in test test_int_parameter_minimum_validation about making this a little more human + # friendly. + assert "Value ('3.2') must fulfill (-infinity <= value <= 3.14)" in element.text, element.text + @selenium_test def test_optional_select_data_field(self): editor = self.components.workflow_editor @@ -120,9 +198,7 @@ def test_optional_select_data_field(self): node.title.wait_for_and_click() self.components.tool_form.parameter_checkbox(parameter="select_single").wait_for_and_click() self.components.tool_form.parameter_input(parameter="select_single").wait_for_and_send_keys("parameter value") - # onSetData does an extra POST to build_modules, so we need to wait for that ... - self.sleep_for(self.wait_types.UX_RENDER) - self.assert_workflow_has_changes_and_save() + self.save_after_node_form_changes() workflow = self.workflow_populator.download_workflow(workflow_id) tool_state = json.loads(workflow["steps"]["0"]["tool_state"]) assert tool_state["select_single"] == "parameter value" @@ -1329,6 +1405,17 @@ def test_editor_selection(self): assert editor.tool_bar.selection_count.wait_for_visible().text.find("1 comment") != -1 + def create_and_wait_for_new_workflow_in_editor(self, annotation: Optional[str] = None) -> str: + editor = self.components.workflow_editor + name = self.workflow_create_new(annotation=annotation) + editor.canvas_body.wait_for_visible() + return name + + def save_after_node_form_changes(self): + # onSetData does an extra POST to build_modules, so we need to wait for that ... + self.sleep_for(self.wait_types.UX_RENDER) + self.assert_workflow_has_changes_and_save() + def get_node_position(self, label: str): node = self.components.workflow_editor.node._(label=label).wait_for_present() @@ -1442,16 +1529,6 @@ def workflow_editor_source_sink_terminal_ids(self, source, sink): return source_id, sink_id - def workflow_editor_add_input(self, item_name="data_input"): - editor = self.components.workflow_editor - - # Make sure we're on the workflow editor and not clicking the main tool panel. - editor.canvas_body.wait_for_visible() - - editor.tool_menu.wait_for_visible() - editor.tool_menu_section_link(section_name="inputs").wait_for_and_click() - editor.tool_menu_item_link(item_name=item_name).wait_for_and_click() - def workflow_editor_destroy_connection(self, sink): editor = self.components.workflow_editor From 433766e9535d2fb78500a58533a272bcdf1a775e Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sat, 7 Sep 2024 18:34:32 +0200 Subject: [PATCH 002/176] Allow setting default value for required parameter --- lib/galaxy/workflow/modules.py | 13 ++++++++----- lib/galaxy/workflow/run.py | 4 ++-- lib/galaxy_test/api/test_workflows.py | 4 +++- lib/galaxy_test/base/workflow_fixtures.py | 1 + 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index bf7fffae9d90..df43aefd2e3d 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -1260,12 +1260,15 @@ def get_inputs(self): when_true = ConditionalWhen() when_true.value = "true" - when_true.inputs = {} - when_true.inputs["default"] = specify_default_cond + when_true.inputs = {"default": specify_default_cond} when_false = ConditionalWhen() when_false.value = "false" - when_false.inputs = {} + # This is only present for backwards compatibility, + # We don't need this conditional since you can set + # a default value for optional and required parameters. + # TODO: version the state and upgrade it to a simpler version + when_false.inputs = {"default": specify_default_cond} optional_cases = [when_true, when_false] optional_cond.cases = optional_cases @@ -1504,6 +1507,7 @@ def get_runtime_inputs(self, step, connections: Optional[Iterable[WorkflowStepCo parameter_def = self._parse_state_into_dict() parameter_type = parameter_def["parameter_type"] optional = parameter_def["optional"] + default_value_set = "default" in parameter_def default_value = parameter_def.get("default", self.default_default_value) if parameter_type not in ["text", "boolean", "integer", "float", "color", "directory_uri"]: raise ValueError("Invalid parameter type for workflow parameters encountered.") @@ -1557,7 +1561,7 @@ def _parameter_def_list_to_options(parameter_value): parameter_class = parameter_types[client_parameter_type] - if optional: + if default_value_set: if client_parameter_type == "select": parameter_kwds["selected"] = default_value else: @@ -1633,7 +1637,6 @@ def step_state_to_tool_state(self, state): if "default" in state: default_set = True default_value = state["default"] - state["optional"] = True multiple = state.get("multiple") source_validators = state.get("validators") restrictions = state.get("restrictions") diff --git a/lib/galaxy/workflow/run.py b/lib/galaxy/workflow/run.py index 5619a4547b8f..ab095631ecef 100644 --- a/lib/galaxy/workflow/run.py +++ b/lib/galaxy/workflow/run.py @@ -574,8 +574,8 @@ def set_outputs_for_input( if self.inputs_by_step_id: step_id = step.id if step_id not in self.inputs_by_step_id and "output" not in outputs: - default_value = step.input_default_value - if default_value or step.input_optional: + default_value = step.get_input_default_value(modules.NO_REPLACEMENT) + if default_value is not modules.NO_REPLACEMENT: outputs["output"] = default_value else: log.error(f"{step.log_str()} not found in inputs_step_id {self.inputs_by_step_id}") diff --git a/lib/galaxy_test/api/test_workflows.py b/lib/galaxy_test/api/test_workflows.py index f18cdd8a760d..6d14cb49e7df 100644 --- a/lib/galaxy_test/api/test_workflows.py +++ b/lib/galaxy_test/api/test_workflows.py @@ -3212,7 +3212,7 @@ def test_export_invocation_ro_crate_adv(self): """, test_data=""" num_lines_param: - type: int + type: raw value: 2 input collection 1: collection_type: list @@ -7034,12 +7034,14 @@ def test_subworkflow_import_order_maintained(self, history_id): outer_input_1: type: int default: 1 + optional: true position: left: 0 top: 0 outer_input_2: type: int default: 2 + optional: true position: left: 100 top: 0 diff --git a/lib/galaxy_test/base/workflow_fixtures.py b/lib/galaxy_test/base/workflow_fixtures.py index b736196dd63a..7b4eb6186b99 100644 --- a/lib/galaxy_test/base/workflow_fixtures.py +++ b/lib/galaxy_test/base/workflow_fixtures.py @@ -617,6 +617,7 @@ int_input: type: integer default: 3 + optional: true steps: random: tool_id: random_lines1 From 0f5a89ab6b17a89ff07a231af31662d82b14cb0c Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Thu, 28 Nov 2024 18:44:39 +0100 Subject: [PATCH 003/176] Remove now unused input_default_value property It always returned None ... --- lib/galaxy/model/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index f741da9b1c82..4a5be7c35257 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -8174,10 +8174,6 @@ def input_type(self): assert self.is_input_type, "step.input_type can only be called on input step types" return self.STEP_TYPE_TO_INPUT_TYPE[self.type] - @property - def input_default_value(self): - self.get_input_default_value(None) - def get_input_default_value(self, default_default): # parameter_input and the data parameters handle this slightly differently # unfortunately. From 70a74c21868b01b8939eb4578d6b58cf83919c01 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 29 Nov 2024 11:54:45 +0100 Subject: [PATCH 004/176] Fix and add test for required workflow parameters with a default value The SimpleTextParameter change might have larger implications (https://github.com/galaxyproject/galaxy/pull/13523), but it allows us to detect if we need to use the provided default value. --- lib/galaxy/tools/parameters/basic.py | 6 +----- lib/galaxy_test/base/populators.py | 2 -- .../workflow/default_values.gxwf-tests.yml | 10 ++++++++++ .../workflow/default_values.gxwf.yml | 17 +++++++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 lib/galaxy_test/workflow/default_values.gxwf-tests.yml create mode 100644 lib/galaxy_test/workflow/default_values.gxwf.yml diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 6b35473959e4..34b325a0d123 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -368,11 +368,7 @@ def __init__(self, tool, input_source): def to_json(self, value, app, use_security): """Convert a value to a string representation suitable for persisting""" - if value is None: - rval = "" if not self.optional else None - else: - rval = unicodify(value) - return rval + return unicodify(value) def get_initial_value(self, trans, other_values): return self.value diff --git a/lib/galaxy_test/base/populators.py b/lib/galaxy_test/base/populators.py index 684689de2f6f..4f6ecfcff025 100644 --- a/lib/galaxy_test/base/populators.py +++ b/lib/galaxy_test/base/populators.py @@ -3362,8 +3362,6 @@ def read_test_data(test_dict): hda = dataset_populator.new_dataset(history_id, content=value) label_map[key] = dataset_populator.ds_entry(hda) inputs[key] = hda - else: - raise ValueError(f"Invalid test_data def {test_data}") return inputs, label_map, has_uploads diff --git a/lib/galaxy_test/workflow/default_values.gxwf-tests.yml b/lib/galaxy_test/workflow/default_values.gxwf-tests.yml new file mode 100644 index 000000000000..62bd9c2b8d6a --- /dev/null +++ b/lib/galaxy_test/workflow/default_values.gxwf-tests.yml @@ -0,0 +1,10 @@ +- doc: | + Test that default value doesn't need to be supplied + job: + input: {} + outputs: + out: + class: File + asserts: + - that: has_text + text: "1" diff --git a/lib/galaxy_test/workflow/default_values.gxwf.yml b/lib/galaxy_test/workflow/default_values.gxwf.yml new file mode 100644 index 000000000000..db89c2bedd7d --- /dev/null +++ b/lib/galaxy_test/workflow/default_values.gxwf.yml @@ -0,0 +1,17 @@ +class: GalaxyWorkflow +inputs: + required_int_with_default: + type: int + default: 1 +outputs: + out: + outputSource: integer_default/out_file1 +steps: + integer_default: + tool_id: integer_default + tool_state: + input1: 0 + input2: 0 + in: + input3: + source: required_int_with_default From 6474ed13f13b59dc3f1ee1f11fa465b2884fa88b Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 29 Nov 2024 12:39:04 +0100 Subject: [PATCH 005/176] Adjust EmptyValueFilter --- lib/galaxy/tool_util/parser/parameter_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/tool_util/parser/parameter_validators.py b/lib/galaxy/tool_util/parser/parameter_validators.py index ca539f74ee1d..29af9494b0bd 100644 --- a/lib/galaxy/tool_util/parser/parameter_validators.py +++ b/lib/galaxy/tool_util/parser/parameter_validators.py @@ -313,7 +313,7 @@ class EmptyFieldParameterValidatorModel(StaticValidatorModel): @staticmethod def empty_validate(value: Any, validator: "ValidatorDescription"): - raise_error_if_valiation_fails((value != ""), validator) + raise_error_if_valiation_fails((value not in ("", None)), validator) def statically_validate(self, value: Any) -> None: EmptyFieldParameterValidatorModel.empty_validate(value, self) From 8aa986f60bfd7d8c86dbbcc999a3ee148fea361a Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 29 Nov 2024 15:32:26 +0100 Subject: [PATCH 006/176] Extend tests --- .../workflow/default_values.gxwf-tests.yml | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/galaxy_test/workflow/default_values.gxwf-tests.yml b/lib/galaxy_test/workflow/default_values.gxwf-tests.yml index 62bd9c2b8d6a..71654a948edc 100644 --- a/lib/galaxy_test/workflow/default_values.gxwf-tests.yml +++ b/lib/galaxy_test/workflow/default_values.gxwf-tests.yml @@ -1,7 +1,30 @@ - doc: | Test that default value doesn't need to be supplied + job: {} + outputs: + out: + class: File + asserts: + - that: has_text + text: "1" +- doc: | + Test that null is replaced with default value + job: + required_int_with_default: + type: raw + value: null + outputs: + out: + class: File + asserts: + - that: has_text + text: "1" +- doc: | + Test that empty string is not replaced and fails job: - input: {} + required_int_with_default: + type: raw + value: "" outputs: out: class: File From 5d4e34134ea5a8921ab6f05cf6bb4ad748b39c04 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 29 Nov 2024 15:45:59 +0100 Subject: [PATCH 007/176] Replace explicit null with default value --- lib/galaxy/tool_util/models.py | 2 ++ lib/galaxy/workflow/run.py | 3 ++- .../workflow/default_values.gxwf-tests.yml | 3 ++- .../workflow/test_framework_workflows.py | 25 +++++++++++++------ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/tool_util/models.py b/lib/galaxy/tool_util/models.py index 3f9946e30404..858b50938942 100644 --- a/lib/galaxy/tool_util/models.py +++ b/lib/galaxy/tool_util/models.py @@ -172,6 +172,7 @@ class TestJob(StrictModel): doc: Optional[str] job: JobDict outputs: Dict[str, TestOutputAssertions] + expect_failure: Optional[bool] = False Tests = RootModel[List[TestJob]] @@ -185,6 +186,7 @@ class TestJob(StrictModel): class TestJobDict(TypedDict): doc: NotRequired[str] job: NotRequired[JobDict] + expect_failure: NotRequired[bool] outputs: OutputsDict diff --git a/lib/galaxy/workflow/run.py b/lib/galaxy/workflow/run.py index ab095631ecef..d634f678999a 100644 --- a/lib/galaxy/workflow/run.py +++ b/lib/galaxy/workflow/run.py @@ -588,7 +588,8 @@ def set_outputs_for_input( ) ) elif step_id in self.inputs_by_step_id: - outputs["output"] = self.inputs_by_step_id[step_id] + if self.inputs_by_step_id[step_id] is not None or "output" not in outputs: + outputs["output"] = self.inputs_by_step_id[step_id] if step.label and step.type == "parameter_input" and "output" in outputs: self.runtime_replacements[step.label] = str(outputs["output"]) diff --git a/lib/galaxy_test/workflow/default_values.gxwf-tests.yml b/lib/galaxy_test/workflow/default_values.gxwf-tests.yml index 71654a948edc..92d0ad6e03c5 100644 --- a/lib/galaxy_test/workflow/default_values.gxwf-tests.yml +++ b/lib/galaxy_test/workflow/default_values.gxwf-tests.yml @@ -8,7 +8,7 @@ - that: has_text text: "1" - doc: | - Test that null is replaced with default value + Test that null is replaced with default value (follows https://www.commonwl.org/v1.2/Workflow.html#WorkflowInputParameter) job: required_int_with_default: type: raw @@ -21,6 +21,7 @@ text: "1" - doc: | Test that empty string is not replaced and fails + expect_failure: true job: required_int_with_default: type: raw diff --git a/lib/galaxy_test/workflow/test_framework_workflows.py b/lib/galaxy_test/workflow/test_framework_workflows.py index 264f039d2751..db1d9471fc7f 100644 --- a/lib/galaxy_test/workflow/test_framework_workflows.py +++ b/lib/galaxy_test/workflow/test_framework_workflows.py @@ -64,14 +64,23 @@ def test_workflow(self, workflow_path: Path, test_job: TestJobDict): with workflow_path.open() as f: yaml_content = ordered_load(f) with self.dataset_populator.test_history() as history_id: - run_summary = self.workflow_populator.run_workflow( - yaml_content, - test_data=test_job["job"], - history_id=history_id, - ) - if TEST_WORKFLOW_AFTER_RERUN: - run_summary = self.workflow_populator.rerun(run_summary) - self._verify(run_summary, test_job["outputs"]) + exc = None + try: + run_summary = self.workflow_populator.run_workflow( + yaml_content, + test_data=test_job["job"], + history_id=history_id, + ) + if TEST_WORKFLOW_AFTER_RERUN: + run_summary = self.workflow_populator.rerun(run_summary) + self._verify(run_summary, test_job["outputs"]) + except Exception as e: + exc = e + if test_job.get("expect_failure"): + if not exc: + raise Exception("Expected workflow test to fail but it passed") + elif exc: + raise exc def _verify(self, run_summary: RunJobsSummary, output_definitions: OutputsDict): for output_name, output_definition in output_definitions.items(): From 59dc2a4b571f22353754df294a18f712581b35b0 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 29 Nov 2024 16:20:50 +0100 Subject: [PATCH 008/176] Add test for default value with optional input --- .../default_values_optional.gxwf-tests.yml | 34 +++++++++++++++++++ .../workflow/default_values_optional.gxwf.yml | 18 ++++++++++ 2 files changed, 52 insertions(+) create mode 100644 lib/galaxy_test/workflow/default_values_optional.gxwf-tests.yml create mode 100644 lib/galaxy_test/workflow/default_values_optional.gxwf.yml diff --git a/lib/galaxy_test/workflow/default_values_optional.gxwf-tests.yml b/lib/galaxy_test/workflow/default_values_optional.gxwf-tests.yml new file mode 100644 index 000000000000..7af1432006cb --- /dev/null +++ b/lib/galaxy_test/workflow/default_values_optional.gxwf-tests.yml @@ -0,0 +1,34 @@ +- doc: | + Test that default value doesn't need to be supplied + job: {} + outputs: + out: + class: File + asserts: + - that: has_text + text: "1" +- doc: | + Test that null is replaced with default value (follows https://www.commonwl.org/v1.2/Workflow.html#WorkflowInputParameter) + job: + optional_int_with_default: + type: raw + value: null + outputs: + out: + class: File + asserts: + - that: has_text + text: "1" +- doc: | + Test that empty string is not replaced and fails + expect_failure: true + job: + optional_int_with_default: + type: raw + value: "" + outputs: + out: + class: File + asserts: + - that: has_text + text: "1" diff --git a/lib/galaxy_test/workflow/default_values_optional.gxwf.yml b/lib/galaxy_test/workflow/default_values_optional.gxwf.yml new file mode 100644 index 000000000000..4df2e2528cc5 --- /dev/null +++ b/lib/galaxy_test/workflow/default_values_optional.gxwf.yml @@ -0,0 +1,18 @@ +class: GalaxyWorkflow +inputs: + optional_int_with_default: + type: int + default: 1 + optional: true +outputs: + out: + outputSource: integer_default/out_file1 +steps: + integer_default: + tool_id: integer_default + tool_state: + input1: 0 + input2: 0 + in: + input3: + source: optional_int_with_default From 7cd214725fd9681d85e8bd6990c0c11af49eadb7 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 19 Nov 2024 13:09:47 -0500 Subject: [PATCH 009/176] This really stablizes these tests - they sort of runaway without this. --- client/src/components/History/HistoryView.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/components/History/HistoryView.test.js b/client/src/components/History/HistoryView.test.js index 85dc95106ede..b00e2647f7a9 100644 --- a/client/src/components/History/HistoryView.test.js +++ b/client/src/components/History/HistoryView.test.js @@ -17,6 +17,11 @@ jest.mock("stores/services/history.services"); const { server, http } = useServerMock(); +jest.mock("vue-router/composables", () => ({ + useRoute: jest.fn(() => ({})), + useRouter: jest.fn(() => ({})), +})); + function create_history(historyId, userId, purged = false, archived = false) { const historyName = `${userId}'s History ${historyId}`; return { From abe326a4b0997f99e39ec003282cd7485fd643e6 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:52:28 +0100 Subject: [PATCH 010/176] fix pesky warning --- ...PersistentTaskProgressMonitorAlert.test.ts | 2 +- client/src/composables/genericTaskMonitor.ts | 18 +++++------ .../persistentProgressMonitor.test.ts | 6 ++-- .../composables/persistentProgressMonitor.ts | 14 ++++---- .../shortTermStorageMonitor.test.ts | 28 ++++++++-------- client/src/composables/taskMonitor.test.ts | 32 +++++++++---------- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts b/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts index bc49c53c547d..266d2128a1b7 100644 --- a/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts +++ b/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts @@ -32,7 +32,7 @@ const FAKE_MONITOR: TaskMonitor = { isCompleted: ref(false), hasFailed: ref(false), requestHasFailed: ref(false), - status: ref(), + taskStatus: ref(""), expirationTime: FAKE_EXPIRATION_TIME, isFinalState: jest.fn(), loadStatus: jest.fn(), diff --git a/client/src/composables/genericTaskMonitor.ts b/client/src/composables/genericTaskMonitor.ts index cb0c1f7c7137..e13876dda9b1 100644 --- a/client/src/composables/genericTaskMonitor.ts +++ b/client/src/composables/genericTaskMonitor.ts @@ -42,7 +42,7 @@ export interface TaskMonitor { * The meaning of the status string is up to the monitor implementation. * In case of an error, this will be the error message. */ - status: Readonly>; + taskStatus: Readonly>; /** * Loads the status of the task from a stored value. @@ -96,19 +96,19 @@ export function useGenericMonitor(options: { let pollDelay = options.defaultPollDelay ?? DEFAULT_POLL_DELAY; const isRunning = ref(false); - const status = ref(); + const taskStatus = ref(); const requestId = ref(); const requestHasFailed = ref(false); - const isCompleted = computed(() => options.completedCondition(status.value)); - const hasFailed = computed(() => options.failedCondition(status.value)); + const isCompleted = computed(() => options.completedCondition(taskStatus.value)); + const hasFailed = computed(() => options.failedCondition(taskStatus.value)); function isFinalState(status?: string) { return options.completedCondition(status) || options.failedCondition(status); } function loadStatus(storedStatus: string) { - status.value = storedStatus; + taskStatus.value = storedStatus; } async function waitForTask(taskId: string, pollDelayInMs?: number) { @@ -122,7 +122,7 @@ export function useGenericMonitor(options: { async function fetchTaskStatus(taskId: string) { try { const result = await options.fetchStatus(taskId); - status.value = result; + taskStatus.value = result; if (isCompleted.value || hasFailed.value) { isRunning.value = false; } else { @@ -141,7 +141,7 @@ export function useGenericMonitor(options: { } function handleError(err: string) { - status.value = err.toString(); + taskStatus.value = err.toString(); requestHasFailed.value = true; isRunning.value = false; resetTimeout(); @@ -156,7 +156,7 @@ export function useGenericMonitor(options: { function resetState() { resetTimeout(); - status.value = undefined; + taskStatus.value = undefined; requestHasFailed.value = false; isRunning.value = false; } @@ -169,7 +169,7 @@ export function useGenericMonitor(options: { isCompleted: readonly(isCompleted), hasFailed: readonly(hasFailed), requestHasFailed: readonly(requestHasFailed), - status: readonly(status), + taskStatus: readonly(taskStatus), expirationTime: options.expirationTime, }; } diff --git a/client/src/composables/persistentProgressMonitor.test.ts b/client/src/composables/persistentProgressMonitor.test.ts index 2a5f9c547dd0..88e21b79e7a0 100644 --- a/client/src/composables/persistentProgressMonitor.test.ts +++ b/client/src/composables/persistentProgressMonitor.test.ts @@ -20,7 +20,7 @@ jest.mock("@vueuse/core", () => ({ function useMonitorMock(): TaskMonitor { const isRunning = ref(false); - const status = ref(); + const taskStatus = ref(); return { waitForTask: jest.fn().mockImplementation(() => { @@ -30,11 +30,11 @@ function useMonitorMock(): TaskMonitor { isCompleted: ref(false), hasFailed: ref(false), requestHasFailed: ref(false), - status, + taskStatus, expirationTime: 1000, isFinalState: jest.fn(), loadStatus(storedStatus) { - status.value = storedStatus; + taskStatus.value = storedStatus; }, }; } diff --git a/client/src/composables/persistentProgressMonitor.ts b/client/src/composables/persistentProgressMonitor.ts index fc2c9067771a..a54773d718d2 100644 --- a/client/src/composables/persistentProgressMonitor.ts +++ b/client/src/composables/persistentProgressMonitor.ts @@ -98,7 +98,7 @@ export interface MonitoringData { * The meaning of the status string is up to the monitor implementation. * In case of an error, this will be the error message. */ - status?: string; + taskStatus?: string; } /** @@ -120,7 +120,7 @@ export function usePersistentProgressTaskMonitor( isCompleted, hasFailed, requestHasFailed, - status, + taskStatus, expirationTime, } = useMonitor; @@ -152,12 +152,12 @@ export function usePersistentProgressTaskMonitor( }); watch( - status, + () => taskStatus.value, (newStatus) => { if (newStatus && currentMonitoringData.value) { currentMonitoringData.value = { ...currentMonitoringData.value, - status: newStatus, + taskStatus: newStatus, }; } }, @@ -173,10 +173,10 @@ export function usePersistentProgressTaskMonitor( throw new Error("No monitoring data provided or stored. Cannot start monitoring progress."); } - if (isFinalState(currentMonitoringData.value.status)) { + if (isFinalState(currentMonitoringData.value.taskStatus)) { // The task has already finished no need to start monitoring again. // Instead, reload the stored status to update the UI. - return loadStatus(currentMonitoringData.value.status!); + return loadStatus(currentMonitoringData.value.taskStatus!); } if (hasExpired.value) { @@ -240,7 +240,7 @@ export function usePersistentProgressTaskMonitor( * The meaning of the status string is up to the monitor implementation. * In case of an error, this will be the error message. */ - status, + status: taskStatus, /** * True if the monitoring data can expire. diff --git a/client/src/composables/shortTermStorageMonitor.test.ts b/client/src/composables/shortTermStorageMonitor.test.ts index f76e4c1eabcc..2dc3974c0668 100644 --- a/client/src/composables/shortTermStorageMonitor.test.ts +++ b/client/src/composables/shortTermStorageMonitor.test.ts @@ -31,28 +31,28 @@ describe("useShortTermStorageMonitor", () => { }); it("should indicate the task is running when it is still not ready", async () => { - const { waitForTask, isRunning, status } = useShortTermStorageMonitor(); + const { waitForTask, isRunning, taskStatus } = useShortTermStorageMonitor(); expect(isRunning.value).toBe(false); waitForTask(PENDING_TASK_ID); await flushPromises(); expect(isRunning.value).toBe(true); - expect(status.value).toBe("PENDING"); + expect(taskStatus.value).toBe("PENDING"); }); it("should indicate the task is successfully completed when the state is ready", async () => { - const { waitForTask, isRunning, isCompleted, status } = useShortTermStorageMonitor(); + const { waitForTask, isRunning, isCompleted, taskStatus } = useShortTermStorageMonitor(); expect(isCompleted.value).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); expect(isCompleted.value).toBe(true); expect(isRunning.value).toBe(false); - expect(status.value).toBe("READY"); + expect(taskStatus.value).toBe("READY"); }); it("should indicate the task status request failed when the request failed", async () => { - const { waitForTask, requestHasFailed, isRunning, isCompleted, status } = useShortTermStorageMonitor(); + const { waitForTask, requestHasFailed, isRunning, isCompleted, taskStatus } = useShortTermStorageMonitor(); expect(requestHasFailed.value).toBe(false); waitForTask(REQUEST_FAILED_TASK_ID); @@ -60,16 +60,16 @@ describe("useShortTermStorageMonitor", () => { expect(requestHasFailed.value).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); - expect(status.value).toBe("Request failed"); + expect(taskStatus.value).toBe("Request failed"); }); it("should load the status from the stored monitoring data", async () => { - const { loadStatus, isRunning, isCompleted, hasFailed, status } = useShortTermStorageMonitor(); + const { loadStatus, isRunning, isCompleted, hasFailed, taskStatus } = useShortTermStorageMonitor(); const storedStatus = "READY"; loadStatus(storedStatus); - expect(status.value).toBe(storedStatus); + expect(taskStatus.value).toBe(storedStatus); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); @@ -77,26 +77,26 @@ describe("useShortTermStorageMonitor", () => { describe("isFinalState", () => { it("should indicate is final state when the task is completed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useShortTermStorageMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); }); it("should indicate is final state when the task has failed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useShortTermStorageMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(REQUEST_FAILED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); expect(hasFailed.value).toBe(true); diff --git a/client/src/composables/taskMonitor.test.ts b/client/src/composables/taskMonitor.test.ts index b62dd22bf93e..60702963e983 100644 --- a/client/src/composables/taskMonitor.test.ts +++ b/client/src/composables/taskMonitor.test.ts @@ -35,39 +35,39 @@ describe("useTaskMonitor", () => { }); it("should indicate the task is running when it is still pending", async () => { - const { waitForTask, isRunning, status } = useTaskMonitor(); + const { waitForTask, isRunning, taskStatus } = useTaskMonitor(); expect(isRunning.value).toBe(false); waitForTask(PENDING_TASK_ID); await flushPromises(); expect(isRunning.value).toBe(true); - expect(status.value).toBe("PENDING"); + expect(taskStatus.value).toBe("PENDING"); }); it("should indicate the task is successfully completed when the state is SUCCESS", async () => { - const { waitForTask, isRunning, isCompleted, status } = useTaskMonitor(); + const { waitForTask, isRunning, isCompleted, taskStatus } = useTaskMonitor(); expect(isCompleted.value).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); expect(isCompleted.value).toBe(true); expect(isRunning.value).toBe(false); - expect(status.value).toBe("SUCCESS"); + expect(taskStatus.value).toBe("SUCCESS"); }); it("should indicate the task has failed when the state is FAILED", async () => { - const { waitForTask, isRunning, hasFailed, status } = useTaskMonitor(); + const { waitForTask, isRunning, hasFailed, taskStatus } = useTaskMonitor(); expect(hasFailed.value).toBe(false); waitForTask(FAILED_TASK_ID); await flushPromises(); expect(hasFailed.value).toBe(true); expect(isRunning.value).toBe(false); - expect(status.value).toBe("FAILURE"); + expect(taskStatus.value).toBe("FAILURE"); }); it("should indicate the task status request failed when the request failed", async () => { - const { waitForTask, requestHasFailed, isRunning, isCompleted, status } = useTaskMonitor(); + const { waitForTask, requestHasFailed, isRunning, isCompleted, taskStatus } = useTaskMonitor(); expect(requestHasFailed.value).toBe(false); waitForTask(REQUEST_FAILED_TASK_ID); @@ -75,16 +75,16 @@ describe("useTaskMonitor", () => { expect(requestHasFailed.value).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); - expect(status.value).toBe("Request failed"); + expect(taskStatus.value).toBe("Request failed"); }); it("should load the status from the stored monitoring data", async () => { - const { loadStatus, isRunning, isCompleted, hasFailed, status } = useTaskMonitor(); + const { loadStatus, isRunning, isCompleted, hasFailed, taskStatus } = useTaskMonitor(); const storedStatus = "SUCCESS"; loadStatus(storedStatus); - expect(status.value).toBe(storedStatus); + expect(taskStatus.value).toBe(storedStatus); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); @@ -92,24 +92,24 @@ describe("useTaskMonitor", () => { describe("isFinalState", () => { it("should indicate is final state when the task is completed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = useTaskMonitor(); + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useTaskMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); }); it("should indicate is final state when the task has failed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = useTaskMonitor(); + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useTaskMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(FAILED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); expect(hasFailed.value).toBe(true); From d44f03758471a3eca0a6227c3719d1a4ada9942f Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 19 Sep 2024 15:48:33 -0500 Subject: [PATCH 011/176] add button for creating a list from run form field This is an initial/draft implementation. Some of the next steps are: - By default, instead of the modals treating the items as selections from the current history, automatically filter items valid for the list (e.g.: for a list with csv elements, filter out csvs from the history in this list). - In case nothing can be auto paried for `list:paired`, do not attempt to auto pair by default and simply show all items. - In case the current history is empty and to make it clearer in general, allow history to be switched from within the modal? - Allow files to be uploaded (and dropped) directly to either the form field or within the list builder once it is opened. One thing I have not planned for yet is the rule builder. I can see that for `list` and `list:paired`, we get that from the `props.collectionTypes` in `FormData`. But when would we use the rule builder instead? Fixes https://github.com/galaxyproject/galaxy/issues/18704 --- .../Form/Elements/FormData/FormData.vue | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index 6a97be9c2ec5..d42c024dd651 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -8,9 +8,12 @@ import { computed, onMounted, type Ref, ref, watch } from "vue"; import { isDatasetElement, isDCE } from "@/api"; import { getGalaxyInstance } from "@/app"; +import { buildCollectionModal } from "@/components/History/adapters/buildCollectionModal"; import { useDatatypesMapper } from "@/composables/datatypesMapper"; import { useUid } from "@/composables/utils/uid"; import { type EventData, useEventStore } from "@/stores/eventStore"; +import { useHistoryItemsStore } from "@/stores/historyItemsStore"; +import { useHistoryStore } from "@/stores/historyStore"; import { orList } from "@/utils/strings"; import type { DataOption } from "./types"; @@ -471,6 +474,21 @@ function canAcceptSrc(historyContentType: "dataset" | "dataset_collection", coll } } +const historyStore = useHistoryStore(); +const historyItemsStore = useHistoryItemsStore(); +// Build a new collection +async function buildNewCollection(collectionType: string) { + if (!historyStore.currentHistoryId) { + return; + } + const modalResult = await buildCollectionModal( + collectionType, + historyItemsStore.getHistoryItems(historyStore.currentHistoryId, ""), + historyStore.currentHistoryId + ); + // TODO: Implement handling `modalResult` as input for the field +} + // Drag/Drop event handlers function onDragEnter(evt: MouseEvent) { const eventData = eventStore.getDragData(); @@ -632,7 +650,20 @@ const noOptionsWarningMessage = computed(() => { :placeholder="`Select a ${placeholder}`"> From 6d903746d123b1bb911be53ab3bbf56397a377ad Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 26 Sep 2024 18:13:53 -0500 Subject: [PATCH 012/176] fully implement `list` collection creator in `FormData` This allows a collection type `list` to be created via the collection creater from the workflow/tool form directly. It tracks the current history changes via the new `useHistoryItemsForType` composable. It utilises the `FormSelectMany` component to easily move items between selected and unselected for list columns. The items in the list creator can be filtered for extension, parent datatype or all items in the history, based on whether the form field required a certain extension(s) as input for the list. --- client/src/api/histories.ts | 3 + .../Collections/ListCollectionCreator.vue | 338 ++++++++++++++---- .../Collections/ListCollectionCreatorModal.js | 16 +- .../ListDatasetCollectionElementView.vue | 43 ++- .../Collections/PairCollectionCreator.vue | 1 + .../PairedListCollectionCreator.vue | 1 + .../Collections/common/ClickToEdit.vue | 52 +-- .../Collections/common/CollectionCreator.vue | 163 +++++---- .../src/components/Common/ButtonSpinner.vue | 17 +- client/src/components/Datatypes/model.ts | 10 + .../Form/Elements/FormData/FormData.vue | 93 +++-- .../components/Form/Elements/FormSelect.vue | 3 +- .../FormSelectMany/FormSelectMany.vue | 8 +- client/src/components/Help/terms.yml | 11 + .../HistoryOperations/SelectionOperations.vue | 4 +- .../History/adapters/HistoryPanelProxy.js | 2 +- ...ectionModal.js => buildCollectionModal.ts} | 43 ++- .../src/composables/useHistoryItemsForType.ts | 90 +++++ 18 files changed, 665 insertions(+), 233 deletions(-) create mode 100644 client/src/api/histories.ts rename client/src/components/History/adapters/{buildCollectionModal.js => buildCollectionModal.ts} (55%) create mode 100644 client/src/composables/useHistoryItemsForType.ts diff --git a/client/src/api/histories.ts b/client/src/api/histories.ts new file mode 100644 index 000000000000..ed10bb8efb1f --- /dev/null +++ b/client/src/api/histories.ts @@ -0,0 +1,3 @@ +import { type components } from "@/api"; + +export type HistoryContentsResult = components["schemas"]["HistoryContentsResult"]; diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index 5798187943f2..e5590be15fd1 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -1,21 +1,29 @@ diff --git a/client/src/components/Collections/PairCollectionCreator.vue b/client/src/components/Collections/PairCollectionCreator.vue index 2520a3a022f8..e0641fe50f34 100644 --- a/client/src/components/Collections/PairCollectionCreator.vue +++ b/client/src/components/Collections/PairCollectionCreator.vue @@ -298,6 +298,7 @@ onMounted(() => { :oncancel="oncancel" :hide-source-items="hideSourceItems" :suggested-name="initialSuggestedName" + :extensions-toggle="removeExtensions" @onUpdateHideSourceItems="onUpdateHideSourceItems" @clicked-create="clickedCreate" @remove-extensions-toggle="removeExtensionsToggle"> diff --git a/client/src/components/Collections/PairedListCollectionCreator.vue b/client/src/components/Collections/PairedListCollectionCreator.vue index c699c0de7077..d18767c32572 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.vue +++ b/client/src/components/Collections/PairedListCollectionCreator.vue @@ -102,6 +102,7 @@ :oncancel="oncancel" :hide-source-items="hideSourceItems" :render-extensions-toggle="true" + :extensions-toggle="removeExtensions" @onUpdateHideSourceItems="onUpdateHideSourceItems" @clicked-create="clickedCreate" @remove-extensions-toggle="removeExtensionsToggle"> diff --git a/client/src/components/Collections/common/ClickToEdit.vue b/client/src/components/Collections/common/ClickToEdit.vue index da60f01880d9..84c413d7f1f1 100644 --- a/client/src/components/Collections/common/ClickToEdit.vue +++ b/client/src/components/Collections/common/ClickToEdit.vue @@ -1,4 +1,7 @@ diff --git a/client/src/components/Collections/common/CollectionCreator.vue b/client/src/components/Collections/common/CollectionCreator.vue index 8ef14660d999..e257df302654 100644 --- a/client/src/components/Collections/common/CollectionCreator.vue +++ b/client/src/components/Collections/common/CollectionCreator.vue @@ -1,28 +1,38 @@ diff --git a/client/src/components/Datatypes/model.ts b/client/src/components/Datatypes/model.ts index 77826b73fc13..591f8f2814b0 100644 --- a/client/src/components/Datatypes/model.ts +++ b/client/src/components/Datatypes/model.ts @@ -39,4 +39,14 @@ export class DatatypesMapperModel { isSubTypeOfAny(child: string, parents: DatatypesCombinedMap["datatypes"]): boolean { return parents.some((parent) => this.isSubType(child, parent)); } + + /** For classes like `galaxy.datatypes.{parent}.{extension}`, get the extension's parent */ + getParentDatatype(extension: string) { + const fullClassName = this.datatypesMapping.ext_to_class_name[extension]; + return fullClassName?.split(".")[2]; + } + + isSubClassOfAny(child: string, parents: DatatypesCombinedMap["datatypes"]): boolean { + return parents.every((parent) => this.getParentDatatype(parent) === this.getParentDatatype(child)); + } } diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index d42c024dd651..e87a104c7eb2 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -1,7 +1,7 @@ diff --git a/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue b/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue index 95581fbc3972..58ecf975cf72 100644 --- a/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue +++ b/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue @@ -364,7 +364,9 @@ const selectedCount = computed(() => { :class="{ highlighted: highlightUnselected.highlightedIndexes.includes(i) }" @click="(e) => selectOption(e, i)" @keydown="(e) => optionOnKey('unselected', e, i)"> - {{ option.label }} + + {{ option.label }} + @@ -396,7 +398,9 @@ const selectedCount = computed(() => { :class="{ highlighted: highlightSelected.highlightedIndexes.includes(i) }" @click="(e) => deselectOption(e, i)" @keydown="(e) => optionOnKey('selected', e, i)"> - {{ option.label }} + + {{ option.label }} + diff --git a/client/src/components/Help/terms.yml b/client/src/components/Help/terms.yml index 536d08f3dee1..bc1caa1ccb22 100644 --- a/client/src/components/Help/terms.yml +++ b/client/src/components/Help/terms.yml @@ -59,6 +59,17 @@ galaxy: These lists will be gathered together in a nested list structured (collection type ``list:list``) where the outer element count and structure matches that of the input and the inner list for each of those is just the outputs of the tool for the corresponding element of the input. + collectionBuilder: + hideOriginalElements: | + Toggling this on means that the original history items that will become a part of the collection + will be hidden from the history panel (they will still be searchable via the 'visible: false' filter). + filterForDatatypes: | + This option allows you to filter items shown here by datatype because this input requires specific + extension(s). By default, the toggle is at "Extension" and the list is filtered for the explicit extension(s) + required by the input; *if datasets with that extension(s) are available*. If you toggle to "Datatype", + the list will be filtered for the "parent" datatype of the required extension (for implicit conversion). + If you toggle to "All", the list will show all items regardless of datatype. + jobs: states: # upload, waiting, failed, paused, deleting, deleted, stop, stopped, skipped. diff --git a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue index 313846e57546..e9f6591723ae 100644 --- a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue +++ b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue @@ -392,7 +392,9 @@ export default { if (contents === undefined) { contents = this.contentSelection; } - const modalResult = await buildCollectionModal(collectionType, contents, this.history.id); + const modalResult = await buildCollectionModal(collectionType, contents, this.history.id, { + fromSelection: true, + }); await createDatasetCollection(this.history, modalResult); // have to hide the source items if that was requested diff --git a/client/src/components/History/adapters/HistoryPanelProxy.js b/client/src/components/History/adapters/HistoryPanelProxy.js index 28b95b38627f..e79466d45052 100644 --- a/client/src/components/History/adapters/HistoryPanelProxy.js +++ b/client/src/components/History/adapters/HistoryPanelProxy.js @@ -56,7 +56,7 @@ export class HistoryPanelProxy { selectionContent.set(obj.id, obj); }); } - const modalResult = await buildCollectionModal(collectionType, selectionContent, historyId, fromRulesInput); + const modalResult = await buildCollectionModal(collectionType, selectionContent, historyId, { fromRulesInput }); if (modalResult) { console.debug("Submitting collection build request.", modalResult); await createDatasetCollection({ id: historyId }, modalResult); diff --git a/client/src/components/History/adapters/buildCollectionModal.js b/client/src/components/History/adapters/buildCollectionModal.ts similarity index 55% rename from client/src/components/History/adapters/buildCollectionModal.js rename to client/src/components/History/adapters/buildCollectionModal.ts index f5b5c030ea27..06be87275012 100644 --- a/client/src/components/History/adapters/buildCollectionModal.js +++ b/client/src/components/History/adapters/buildCollectionModal.ts @@ -8,15 +8,33 @@ * deprecated jquery Deferred object. */ -import LIST_COLLECTION_CREATOR from "components/Collections/ListCollectionCreatorModal"; -import PAIR_COLLECTION_CREATOR from "components/Collections/PairCollectionCreatorModal"; -import LIST_OF_PAIRS_COLLECTION_CREATOR from "components/Collections/PairedListCollectionCreatorModal"; -import RULE_BASED_COLLECTION_CREATOR from "components/Collections/RuleBasedCollectionCreatorModal"; import jQuery from "jquery"; +import type { HistoryItemSummary } from "@/api"; +import LIST_COLLECTION_CREATOR from "@/components/Collections/ListCollectionCreatorModal"; +import PAIR_COLLECTION_CREATOR from "@/components/Collections/PairCollectionCreatorModal"; +import LIST_OF_PAIRS_COLLECTION_CREATOR from "@/components/Collections/PairedListCollectionCreatorModal"; +import RULE_BASED_COLLECTION_CREATOR from "@/components/Collections/RuleBasedCollectionCreatorModal"; + +export type CollectionType = "list" | "paired" | "list:paired" | "rules"; +export interface BuildCollectionOptions { + fromRulesInput?: boolean; + fromSelection?: boolean; + extensions?: string[]; + title?: string; + defaultHideSourceItems?: boolean; + historyId?: string; +} + // stand-in for buildCollection from history-view-edit.js -export async function buildCollectionModal(collectionType, selectedContent, historyId, fromRulesInput = false) { +export async function buildCollectionModal( + collectionType: CollectionType, + selectedContent: HistoryItemSummary[], + historyId: string, + options: BuildCollectionOptions = {} +) { // select legacy function + const { fromRulesInput = false } = options; let createFunc; if (collectionType == "list") { createFunc = LIST_COLLECTION_CREATOR.createListCollection; @@ -33,19 +51,25 @@ export async function buildCollectionModal(collectionType, selectedContent, hist if (fromRulesInput) { return await createFunc(selectedContent); } else { - const fakeBackboneContent = createBackboneContent(historyId, selectedContent); + const fakeBackboneContent = createBackboneContent(historyId, selectedContent, options); return await createFunc(fakeBackboneContent); } } -const createBackboneContent = (historyId, selection) => { +const createBackboneContent = (historyId: string, selection: HistoryItemSummary[], options: BuildCollectionOptions) => { const selectionJson = Array.from(selection.values()); return { historyId, toJSON: () => selectionJson, // result must be a $.Deferred object instead of a promise because // that's the kind of deprecated data format that backbone likes to use. - createHDCA(element_identifiers, collection_type, name, hide_source_items, options = {}) { + createHDCA( + element_identifiers: any, + collection_type: CollectionType, + name: string, + hide_source_items: boolean, + options = {} + ) { const def = jQuery.Deferred(); return def.resolve(null, { collection_type, @@ -55,5 +79,8 @@ const createBackboneContent = (historyId, selection) => { options, }); }, + fromSelection: options.fromSelection, + extensions: options.extensions, + defaultHideSourceItems: options.defaultHideSourceItems === undefined ? true : options.defaultHideSourceItems, }; }; diff --git a/client/src/composables/useHistoryItemsForType.ts b/client/src/composables/useHistoryItemsForType.ts new file mode 100644 index 000000000000..765e411637da --- /dev/null +++ b/client/src/composables/useHistoryItemsForType.ts @@ -0,0 +1,90 @@ +import { computed, type Ref, ref, watch } from "vue"; + +import { GalaxyApi, type HistoryItemSummary } from "@/api"; +import { filtersToQueryValues } from "@/components/History/model/queries"; +import { useHistoryStore } from "@/stores/historyStore"; +import { errorMessageAsString } from "@/utils/simple-error"; + +const DEFAULT_FILTERS = { visible: true, deleted: false }; + +let singletonInstance: { + isFetchingItems: Ref; + errorMessage: Ref; + historyItems: Ref; +} | null = null; + +/** + * Creates a composable that fetches the given type of items from a history reactively. + * @param historyId The history ID to fetch items for. (TODO: make this a required parameter; only `string` allowed) + * @param type The type of items to fetch. Default is "dataset". + * @param filters Filters to apply to the items. + * @returns An object containing reactive properties for the fetch status and the fetched items. + */ +export function useHistoryItemsForType( + historyId: Ref, + type: "dataset" | "dataset_collection" = "dataset", + filters = DEFAULT_FILTERS +) { + if (singletonInstance) { + return singletonInstance; + } + const isFetchingItems = ref(false); + const errorMessage = ref(null); + const historyItems = ref([]); + const counter = ref(0); + + const historyStore = useHistoryStore(); + + const historyUpdateTime = computed( + () => historyId.value && historyStore.getHistoryById(historyId.value)?.update_time + ); + + // Fetch items when history ID or update time changes + watch( + () => ({ + time: historyUpdateTime.value, + id: historyId.value, + }), + async (newValues, oldValues) => { + if (newValues.time !== oldValues?.time || newValues.id !== oldValues?.id) { + await fetchItems(); + counter.value++; + } + }, + { immediate: true } + ); + + async function fetchItems() { + if (!historyId.value) { + errorMessage.value = "No history ID provided"; + return; + } + if (isFetchingItems.value) { + return; + } + const filterQuery = filtersToQueryValues(filters); + isFetchingItems.value = true; + const { data, error } = await GalaxyApi().GET("/api/histories/{history_id}/contents/{type}s", { + params: { + path: { history_id: historyId.value, type: type }, + query: { ...filterQuery, v: "dev" }, + }, + }); + isFetchingItems.value = false; + if (error) { + errorMessage.value = errorMessageAsString(error); + console.error("Error fetching history items", errorMessage.value); + } else { + historyItems.value = data as HistoryItemSummary[]; + errorMessage.value = null; + } + } + + singletonInstance = { + isFetchingItems, + errorMessage, + historyItems, + }; + + return singletonInstance; +} From 4b2027667eb39e3e8099b7f0e84a67c7cb82988e Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 26 Sep 2024 18:17:50 -0500 Subject: [PATCH 013/176] add a `maintain-selection-order` prop to `FormSelectMany` This keeps the order in which the user adds items to the selection evident in the selected column. --- .../components/Collections/ListCollectionCreator.vue | 1 + .../Form/Elements/FormSelectMany/FormSelectMany.vue | 6 ++++++ .../Elements/FormSelectMany/worker/selectMany.d.ts | 1 + .../Form/Elements/FormSelectMany/worker/selectMany.js | 2 ++ .../FormSelectMany/worker/selectMany.worker.js | 2 ++ .../Elements/FormSelectMany/worker/selectManyMain.ts | 10 ++++++++++ 6 files changed, 22 insertions(+) diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index e5590be15fd1..f26282248a2f 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -648,6 +648,7 @@ function renameElement(element: any, name: string) { From f53170f430078096cad67668c2c1beb8065e4bed Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 1 Oct 2024 11:48:02 -0500 Subject: [PATCH 015/176] `ListCollectionCreator`: add more types --- .../Collections/ListCollectionCreator.vue | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index f26282248a2f..b3d5e398091c 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -26,10 +26,10 @@ const DEFAULT_DATATYPE_FILTER_OPTIONS = [ type DatatypeToggle = "all" | "datatype" | "ext" | undefined; interface Props { - initialElements: Array; + initialElements: HistoryItemSummary[]; oncancel: () => void; oncreate: () => void; - creationFn: (workingElements: any, collectionName: string, hideSourceItems: boolean) => any; + creationFn: (workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean) => any; defaultHideSourceItems?: boolean; fromSelection?: boolean; extensions?: string[]; @@ -38,7 +38,7 @@ interface Props { const props = defineProps(); const emit = defineEmits<{ - (e: "clicked-create", workingElements: any, collectionName: string, hideSourceItems: boolean): void; + (e: "clicked-create", workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean): void; }>(); const state = ref("build"); @@ -75,7 +75,7 @@ const allElementsAreInvalid = computed(() => { }); /** If not `fromSelection`, the list of elements that will become the collection */ -const inListElements = ref([]); +const inListElements = ref([]); // variables for datatype mapping and then filtering const datatypesMapperStore = useDatatypesMapperStore(); @@ -121,21 +121,21 @@ function _elementsSetUp() { // reverse the order of the elements to emulate what we have in the history panel workingElements.value.reverse(); - _ensureElementIds(); + // _ensureElementIds(); _validateElements(); _mangleDuplicateNames(); } -/** add ids to dataset objs in initial list if none */ -function _ensureElementIds() { - workingElements.value.forEach((element) => { - if (!Object.prototype.hasOwnProperty.call(element, "id")) { - console.warn("Element missing id", element); - } - }); +// TODO: not sure if this is needed +// function _ensureElementIds() { +// workingElements.value.forEach((element) => { +// if (!Object.prototype.hasOwnProperty.call(element, "id")) { +// console.warn("Element missing id", element); +// } +// }); - return workingElements.value; -} +// return workingElements.value; +// } // /** separate working list into valid and invalid elements for this collection */ function _validateElements() { From 35ff19da970a9e2fb7a33116e121c708ae01a3f4 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 1 Oct 2024 11:49:13 -0500 Subject: [PATCH 016/176] modernize/refactor `PairedListCollectionCreator` for input forms - Converted the file(s) to composition API and typescript - Improved styling of the modal and its components Still need to add `extensions` handling for cases where a certain extension is required for the collection. --- .../Collections/PairedElementView.vue | 49 +- .../PairedListCollectionCreator.vue | 1786 +++++++++-------- .../PairedListCollectionCreatorModal.js | 16 +- .../UnpairedDatasetElementView.vue | 63 +- client/src/utils/natural-sort.js | 38 - client/src/utils/naturalSort.ts | 45 + 6 files changed, 1127 insertions(+), 870 deletions(-) delete mode 100644 client/src/utils/natural-sort.js create mode 100644 client/src/utils/naturalSort.ts diff --git a/client/src/components/Collections/PairedElementView.vue b/client/src/components/Collections/PairedElementView.vue index 364832a22db2..42cd1d9e8af0 100644 --- a/client/src/components/Collections/PairedElementView.vue +++ b/client/src/components/Collections/PairedElementView.vue @@ -1,69 +1,58 @@ diff --git a/client/src/components/Collections/PairedListCollectionCreator.vue b/client/src/components/Collections/PairedListCollectionCreator.vue index d18767c32572..6e9ada15396d 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.vue +++ b/client/src/components/Collections/PairedListCollectionCreator.vue @@ -1,115 +1,898 @@ + + - diff --git a/client/src/utils/natural-sort.js b/client/src/utils/natural-sort.js deleted file mode 100644 index c620f169163f..000000000000 --- a/client/src/utils/natural-sort.js +++ /dev/null @@ -1,38 +0,0 @@ -// Alphanumeric/natural sort fn -function naturalSort(a, b) { - // setup temp-scope variables for comparison evauluation - var re = /(-?[0-9.]+)/g; - - var x = a.toString().toLowerCase() || ""; - var y = b.toString().toLowerCase() || ""; - var nC = String.fromCharCode(0); - var xN = x.replace(re, `${nC}$1${nC}`).split(nC); - var yN = y.replace(re, `${nC}$1${nC}`).split(nC); - var xD = new Date(x).getTime(); - var yD = xD ? new Date(y).getTime() : null; - // natural sorting of dates - if (yD) { - if (xD < yD) { - return -1; - } else if (xD > yD) { - return 1; - } - } - - // natural sorting through split numeric strings and default strings - var oFxNcL; - - var oFyNcL; - for (var cLoc = 0, numS = Math.max(xN.length, yN.length); cLoc < numS; cLoc++) { - oFxNcL = parseFloat(xN[cLoc]) || xN[cLoc]; - oFyNcL = parseFloat(yN[cLoc]) || yN[cLoc]; - if (oFxNcL < oFyNcL) { - return -1; - } else if (oFxNcL > oFyNcL) { - return 1; - } - } - return 0; -} - -export default naturalSort; diff --git a/client/src/utils/naturalSort.ts b/client/src/utils/naturalSort.ts new file mode 100644 index 000000000000..91a047b90dd0 --- /dev/null +++ b/client/src/utils/naturalSort.ts @@ -0,0 +1,45 @@ +// Alphanumeric/natural sort fn +export function naturalSort(a = "", b = ""): number { + // setup temp-scope variables for comparison evauluation + const re = /(-?[0-9.]+)/g; + + const x = a.toString().toLowerCase() || ""; + const y = b.toString().toLowerCase() || ""; + const nC = String.fromCharCode(0); + const xN = x.replace(re, `${nC}$1${nC}`).split(nC); + const yN = y.replace(re, `${nC}$1${nC}`).split(nC); + const xD = new Date(x).getTime(); + const yD = xD ? new Date(y).getTime() : null; + // natural sorting of dates + if (yD) { + if (xD < yD) { + return -1; + } else if (xD > yD) { + return 1; + } + } + + // natural sorting through split numeric strings and default strings + let oFxNcL; + + let oFyNcL; + for (let cLoc = 0, numS = Math.max(xN.length, yN.length); cLoc < numS; cLoc++) { + oFxNcL = parseFloat(xN[cLoc] || "") || xN[cLoc]; + oFyNcL = parseFloat(yN[cLoc] || "") || yN[cLoc]; + + // Check if either part is undefined + if (oFxNcL === undefined) { + return -1; + } + if (oFyNcL === undefined) { + return 1; + } + + if (oFxNcL < oFyNcL) { + return -1; + } else if (oFxNcL > oFyNcL) { + return 1; + } + } + return 0; +} From 6a504c0afadc27ccee8603fded2886bf606df91d Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 1 Oct 2024 12:25:09 -0500 Subject: [PATCH 017/176] remove the extensions toggle, only include `isSubTypeOfAny` items The `isSubClassOfAny` was incorrect logic for implicit conversions. `isSubTypeOfAny` gives us what we want as far as filtering items that would be valid for implicit conversions goes. Also, we concluded that the `All` option was also not acceptable as only valid extensions must be enforced in the collection creator. --- .../Collections/ListCollectionCreator.vue | 65 +++---------------- .../Collections/common/CollectionCreator.vue | 49 +++----------- client/src/components/Datatypes/model.ts | 4 -- .../Form/Elements/FormData/FormData.vue | 17 ++--- client/src/components/Help/terms.yml | 9 +-- 5 files changed, 30 insertions(+), 114 deletions(-) diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index b3d5e398091c..a9750da933f4 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -17,14 +17,6 @@ import FormSelectMany from "../Form/Elements/FormSelectMany/FormSelectMany.vue"; import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue"; import DatasetCollectionElementView from "@/components/Collections/ListDatasetCollectionElementView.vue"; -const DEFAULT_DATATYPE_FILTER_OPTIONS = [ - { text: "All", value: "all" }, - { text: "Datatype", value: "datatype" }, - { text: "Extension", value: "ext" }, -]; - -type DatatypeToggle = "all" | "datatype" | "ext" | undefined; - interface Props { initialElements: HistoryItemSummary[]; oncancel: () => void; @@ -81,21 +73,7 @@ const inListElements = ref([]); const datatypesMapperStore = useDatatypesMapperStore(); const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper); -const datatypeToggleOptions = ref<{ text: string; value: string }[] | undefined>( - props.extensions?.length ? DEFAULT_DATATYPE_FILTER_OPTIONS : undefined -); -const baseDatatypeToggle = computed(() => { - const options = datatypeToggleOptions.value || []; - return options.find((option) => option.value === "ext") - ? "ext" - : options.find((option) => option.value === "datatype") - ? "datatype" - : "all"; -}); - -const datatypeToggle = ref(props.extensions?.length ? "ext" : undefined); - -/** Are we filtering by extension or datatype? */ +/** Are we filtering by datatype? */ const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length); /** set up instance vars function */ @@ -149,28 +127,11 @@ function _validateElements() { return !problem; }); - // if all elements are invalid, try again by switching to the next datatypeToggle value - // until we've tried both `ext` and `datatype` and then we revert to showing everything - if (props.initialElements.length && allElementsAreInvalid.value && !props.fromSelection && filterExtensions.value) { - if (datatypeToggle.value === "ext") { - datatypeToggleOptions.value = datatypeToggleOptions.value?.filter((option) => option.value !== "ext"); - datatypeToggle.value = "datatype"; - } else if (datatypeToggle.value === "datatype") { - // we've tried both `ext` and `datatype`, so we remove the toggle options - datatypeToggleOptions.value = undefined; - datatypeToggle.value = "all"; - } else { - return; - } - // re-run `_elementsSetUp` to filter out invalid elements for the new datatypeToggle value - _elementsSetUp(); - } - return workingElements.value; } /** describe what is wrong with a particular element if anything */ -function _isElementInvalid(element: HistoryItemSummary) { +function _isElementInvalid(element: HistoryItemSummary): string | null { if (element.history_content_type === "dataset_collection") { return localize("is a collection, this is not allowed"); } @@ -185,17 +146,13 @@ function _isElementInvalid(element: HistoryItemSummary) { return localize("has been deleted or purged"); } - if (filterExtensions.value && datatypeToggle.value !== "all" && element.extension) { - if ( - // is the element's extension not a subtype of any of the required extensions? - (datatypeToggle.value === "ext" && - !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!)) || - // else, does the element's extension have the same parent class as any of the required extensions? - (datatypeToggle.value === "datatype" && - !datatypesMapper.value?.isSubClassOfAny(element.extension, props.extensions!)) - ) { - return localize("has an invalid extension"); - } + // is the element's extension not a subtype of any of the required extensions? + if ( + filterExtensions.value && + element.extension && + !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!) + ) { + return localize("has an invalid extension"); } return null; } @@ -219,7 +176,6 @@ function _mangleDuplicateNames() { } function changeDatatypeFilter(newFilter: "all" | "datatype" | "ext") { - datatypeToggle.value = newFilter; _elementsSetUp(); } @@ -305,7 +261,6 @@ function checkForDuplicates() { /** reset all data to the initial state */ function reset() { - datatypeToggle.value = baseDatatypeToggle.value; _instanceSetUp(); getOriginalNames(); } @@ -462,8 +417,6 @@ function renameElement(element: any, name: string) { :oncancel="oncancel" :hide-source-items="hideSourceItems" :extensions="extensions" - :datatype-toggle="filterExtensions && datatypeToggleOptions ? datatypeToggle : undefined" - :datatype-toggle-options="datatypeToggleOptions" @on-update-datatype-toggle="changeDatatypeFilter" @onUpdateHideSourceItems="onUpdateHideSourceItems" @clicked-create="clickedCreate"> diff --git a/client/src/components/Collections/common/CollectionCreator.vue b/client/src/components/Collections/common/CollectionCreator.vue index e257df302654..02eea2bee594 100644 --- a/client/src/components/Collections/common/CollectionCreator.vue +++ b/client/src/components/Collections/common/CollectionCreator.vue @@ -1,7 +1,7 @@ diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index 2173b62bec91..49e31cfc6782 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -656,6 +656,18 @@ const noOptionsWarningMessage = computed(() => { ... + + + +
@@ -689,25 +701,6 @@ const noOptionsWarningMessage = computed(() => { {{ noOptionsWarningMessage }} - - { From 6862f64692400b27e941de53c153bc8654864360 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 3 Oct 2024 15:25:44 -0500 Subject: [PATCH 019/176] change create new collection `ButtonSpinner` variant --- client/src/components/Form/Elements/FormData/FormData.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index 49e31cfc6782..a5f6db0a1258 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -661,7 +661,7 @@ const noOptionsWarningMessage = computed(() => { v-for="collectionType in effectiveCollectionTypes" :key="collectionType" :tooltip="collectionType" - variant="secondary" + :variant="formattedOptions.length === 0 ? 'warning' : 'secondary'" :disabled="isFetchingItems" :icon="faPlus" :wait="isFetchingItems" From 297a529dde53420cef855e9feb67365c2c5ddc7a Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 3 Oct 2024 15:34:35 -0500 Subject: [PATCH 020/176] change create new collection `ButtonSpinner` title; fix icon imports --- .../Form/Elements/FormData/FormData.vue | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index a5f6db0a1258..faffa91cb75e 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -1,7 +1,13 @@ + + + + diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index a9750da933f4..8d1e5be51705 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -19,9 +19,6 @@ import DatasetCollectionElementView from "@/components/Collections/ListDatasetCo interface Props { initialElements: HistoryItemSummary[]; - oncancel: () => void; - oncreate: () => void; - creationFn: (workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean) => any; defaultHideSourceItems?: boolean; fromSelection?: boolean; extensions?: string[]; @@ -31,6 +28,7 @@ const props = defineProps(); const emit = defineEmits<{ (e: "clicked-create", workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean): void; + (e: "on-cancel"): void; }>(); const state = ref("build"); @@ -152,7 +150,7 @@ function _isElementInvalid(element: HistoryItemSummary): string | null { element.extension && !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!) ) { - return localize("has an invalid extension"); + return localize(`has an invalid extension: ${element.extension}`); } return null; } @@ -231,13 +229,6 @@ function clickedCreate(collectionName: string) { if (state.value !== "error") { emit("clicked-create", returnedElements, collectionName, hideSourceItems.value); - - return props - .creationFn(returnedElements, collectionName, hideSourceItems.value) - .done(props.oncreate) - .fail(() => { - state.value = "error"; - }); } } @@ -339,14 +330,14 @@ function renameElement(element: any, name: string) { {{ localize("No datasets were selected") }} {{ localize("At least one element is needed for the collection. You may need to") }} - + {{ localize("cancel") }} {{ localize("and reselect new elements.") }}
-
@@ -375,14 +366,14 @@ function renameElement(element: any, name: string) { {{ localize("At least one element is needed for the collection. You may need to") }} - + {{ localize("cancel") }} {{ localize("and reselect new elements.") }}
-
@@ -414,7 +405,7 @@ function renameElement(element: any, name: string) {
(); @@ -21,7 +23,7 @@ const emit = defineEmits<{ (event: "element-is-discarded", element: any): void; }>(); -const elementName = ref(props.element.name); +const elementName = ref(props.element.name || "..."); watch(elementName, () => { emit("onRename", elementName.value); @@ -43,13 +45,16 @@ function clickDiscard() { {{ element.hid }}: - + + {{ elementName }} - ({{ element.extension }}) + ({{ element.extension }})
- Added to list + + Added to collection + Selected
diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue index 86c9ff8eeb73..7b4aaf0cc61f 100644 --- a/client/src/components/Workflow/List/WorkflowCardList.vue +++ b/client/src/components/Workflow/List/WorkflowCardList.vue @@ -23,6 +23,8 @@ const emit = defineEmits<{ (e: "tagClick", tag: string): void; (e: "refreshList", overlayLoading?: boolean, silent?: boolean): void; (e: "updateFilter", key: string, value: any): void; + (e: "insertWorkflow", id: string, name: string): void; + (e: "insertWorkflowSteps", id: string, stepCount: number): void; }>(); const modalOptions = reactive({ @@ -54,6 +56,15 @@ function onPreview(id: string) { modalOptions.preview.id = id; showPreview.value = true; } + +// TODO: clean-up types, as soon as better Workflow type is available +function onInsert(workflow: Workflow) { + emit("insertWorkflow", workflow.id as any, workflow.name as any); +} + +function onInsertSteps(workflow: Workflow) { + emit("insertWorkflowSteps", workflow.id as any, workflow.number_of_steps as any); +} diff --git a/client/src/components/Workflow/List/WorkflowCardList.vue b/client/src/components/Workflow/List/WorkflowCardList.vue index 7b4aaf0cc61f..91a3b9f91151 100644 --- a/client/src/components/Workflow/List/WorkflowCardList.vue +++ b/client/src/components/Workflow/List/WorkflowCardList.vue @@ -117,7 +117,7 @@ function onInsertSteps(workflow: Workflow) { diff --git a/client/src/components/Panels/FlexPanel.vue b/client/src/components/Panels/FlexPanel.vue index 9d0031af395f..a01a8612b097 100644 --- a/client/src/components/Panels/FlexPanel.vue +++ b/client/src/components/Panels/FlexPanel.vue @@ -2,14 +2,9 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -import { useDebounce, useDraggable } from "@vueuse/core"; import { computed, ref, watch } from "vue"; -import { useTimeoutThrottle } from "@/composables/throttle"; - -import { determineWidth } from "./utilities"; - -const { throttle } = useTimeoutThrottle(10); +import DraggableSeparator from "../Common/DraggableSeparator.vue"; library.add(faChevronLeft, faChevronRight); @@ -28,26 +23,18 @@ const props = withDefaults(defineProps(), { defaultWidth: 300, }); -const draggable = ref(null); -const root = ref(null); - const panelWidth = ref(props.defaultWidth); -const show = ref(true); - -const { position, isDragging } = useDraggable(draggable, { - preventDefault: true, - exact: true, -}); -const hoverDraggable = ref(false); -const hoverDraggableDebounced = useDebounce(hoverDraggable, 100); -const showHover = computed(() => (hoverDraggable.value && hoverDraggableDebounced.value) || isDragging.value); +const root = ref(null); +const show = ref(true); const showToggle = ref(false); const hoverToggle = ref(false); -const hoverDraggableOrToggle = computed( - () => (hoverDraggableDebounced.value || hoverToggle.value) && !isDragging.value -); + +const isHoveringDragHandle = ref(false); +const isDragging = ref(false); + +const hoverDraggableOrToggle = computed(() => (isHoveringDragHandle.value || hoverToggle.value) && !isDragging.value); const toggleLinger = 500; const toggleShowDelay = 600; @@ -70,72 +57,6 @@ watch( } ); -/** Watch position changes and adjust width accordingly */ -watch(position, () => { - throttle(() => { - if (!root.value || !draggable.value) { - return; - } - - const rectRoot = root.value.getBoundingClientRect(); - const rectDraggable = draggable.value.getBoundingClientRect(); - panelWidth.value = determineWidth( - rectRoot, - rectDraggable, - props.minWidth, - props.maxWidth, - props.side, - position.value.x - ); - }); -}); - -/** If the `maxWidth` changes, prevent the panel from exceeding it */ -watch( - () => props.maxWidth, - (newVal) => { - if (newVal && panelWidth.value > newVal) { - panelWidth.value = props.maxWidth; - } - }, - { immediate: true } -); - -/** If the `minWidth` changes, ensure the panel width is at least the `minWidth` */ -watch( - () => props.minWidth, - (newVal) => { - if (newVal && panelWidth.value < newVal) { - panelWidth.value = newVal; - } - }, - { immediate: true } -); - -function onKeyLeft() { - if (props.side === "left") { - decreaseWidth(); - } else { - increaseWidth(); - } -} - -function onKeyRight() { - if (props.side === "left") { - increaseWidth(); - } else { - decreaseWidth(); - } -} - -function increaseWidth(by = 50) { - panelWidth.value = Math.min(panelWidth.value + by, props.maxWidth); -} - -function decreaseWidth(by = 50) { - panelWidth.value = Math.max(panelWidth.value - by, props.minWidth); -} - const sideClasses = computed(() => ({ left: props.side === "left", right: props.side === "right", @@ -148,19 +69,9 @@ const sideClasses = computed(() => ({ :id="side" ref="root" class="flex-panel" - :class="{ ...sideClasses, 'show-hover': showHover }" + :class="{ ...sideClasses }" :style="`--width: ${panelWidth}px`"> - +
- + @@ -216,6 +217,7 @@ import reportDefault from "./reportDefault"; import WorkflowLint from "./Lint.vue"; import MessagesModal from "./MessagesModal.vue"; +import NodeInspector from "./NodeInspector.vue"; import RefactorConfirmationModal from "./RefactorConfirmationModal.vue"; import SaveChangesModal from "./SaveChangesModal.vue"; import StateUpgradeModal from "./StateUpgradeModal.vue"; @@ -224,12 +226,9 @@ import WorkflowGraph from "./WorkflowGraph.vue"; import ActivityBar from "@/components/ActivityBar/ActivityBar.vue"; import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue"; import MarkdownToolBox from "@/components/Markdown/MarkdownToolBox.vue"; -import FlexPanel from "@/components/Panels/FlexPanel.vue"; import ToolPanel from "@/components/Panels/ToolPanel.vue"; import WorkflowPanel from "@/components/Panels/WorkflowPanel.vue"; import UndoRedoStack from "@/components/UndoRedo/UndoRedoStack.vue"; -import FormDefault from "@/components/Workflow/Editor/Forms/FormDefault.vue"; -import FormTool from "@/components/Workflow/Editor/Forms/FormTool.vue"; library.add(faArrowLeft, faArrowRight, faHistory); @@ -237,12 +236,9 @@ export default { components: { ActivityBar, MarkdownEditor, - FlexPanel, SaveChangesModal, StateUpgradeModal, ToolPanel, - FormDefault, - FormTool, WorkflowAttributes, WorkflowLint, RefactorConfirmationModal, @@ -252,6 +248,7 @@ export default { UndoRedoStack, WorkflowPanel, MarkdownToolBox, + NodeInspector, }, props: { workflowId: { diff --git a/client/src/components/Workflow/Editor/NodeInspector.vue b/client/src/components/Workflow/Editor/NodeInspector.vue new file mode 100644 index 000000000000..4b506626242f --- /dev/null +++ b/client/src/components/Workflow/Editor/NodeInspector.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/client/src/components/Workflow/Editor/WorkflowGraph.vue b/client/src/components/Workflow/Editor/WorkflowGraph.vue index 14c83d2a8cbf..b0a27f1b6235 100644 --- a/client/src/components/Workflow/Editor/WorkflowGraph.vue +++ b/client/src/components/Workflow/Editor/WorkflowGraph.vue @@ -64,6 +64,7 @@ :viewport-bounding-box="viewportBoundingBox" @panBy="panBy" @moveTo="moveTo" /> + | string) { const undoRedoStore = useUndoRedoStore(id); return { + workflowId: id, connectionStore, stateStore, stepStore, From a3813103028a368912141c0531fbbd98c56fd1af Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Tue, 12 Nov 2024 01:16:25 +0100 Subject: [PATCH 113/176] pass step position for more accurate comment positioning --- client/src/components/Workflow/Editor/modules/layout.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/components/Workflow/Editor/modules/layout.ts b/client/src/components/Workflow/Editor/modules/layout.ts index 3c6de54844cd..843867c7def1 100644 --- a/client/src/components/Workflow/Editor/modules/layout.ts +++ b/client/src/components/Workflow/Editor/modules/layout.ts @@ -235,6 +235,8 @@ function stepToElkStep( id: `${step.id}`, height: roundingFunction(position.height), width: roundingFunction(position.width), + x: step.position?.left, + y: step.position?.top, layoutOptions: { "elk.portConstraints": "FIXED_POS", }, From 63aaaada6d787ae6fd6ec9e3ffa83cec07a7eddb Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:42:21 +0100 Subject: [PATCH 114/176] disable run button on new workflow --- .../components/ActivityBar/ActivityBar.vue | 18 ++++++--- .../components/ActivityBar/ActivityItem.vue | 10 ++++- .../src/components/Workflow/Editor/Index.vue | 11 ++++-- .../Workflow/Editor/modules/activities.ts | 21 ++++++++-- client/src/stores/activityStore.ts | 39 ++++++++++++++++++- 5 files changed, 84 insertions(+), 15 deletions(-) diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue index d026d531a1f8..cf9096bd9fbb 100644 --- a/client/src/components/ActivityBar/ActivityBar.vue +++ b/client/src/components/ActivityBar/ActivityBar.vue @@ -237,8 +237,9 @@ defineExpose({ @click="toggleSidebar()" /> import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import type { Placement } from "@popperjs/core"; +import { computed } from "vue"; import { useRouter } from "vue-router/composables"; -import type { ActivityVariant } from "@/stores/activityStore"; +import { type ActivityVariant, useActivityStore } from "@/stores/activityStore"; import localize from "@/utils/localization"; import TextShort from "@/components/Common/TextShort.vue"; @@ -18,6 +19,7 @@ interface Option { export interface Props { id: string; + activityBarId: string; title?: string; icon?: string | object; indicator?: number; @@ -55,17 +57,21 @@ function onClick(evt: MouseEvent): void { router.push(props.to); } } + +const store = useActivityStore(props.activityBarId); +const meta = computed(() => store.metaForId(props.id));