diff --git a/canvas_modules/common-canvas/__tests__/_utils_/table-utilsRTL.js b/canvas_modules/common-canvas/__tests__/_utils_/table-utilsRTL.js index edda34ec89..e8dbfe636f 100644 --- a/canvas_modules/common-canvas/__tests__/_utils_/table-utilsRTL.js +++ b/canvas_modules/common-canvas/__tests__/_utils_/table-utilsRTL.js @@ -97,7 +97,15 @@ function getTableHeaderRows(container) { } function getTableRows(container) { - const rows = within(container).getAllByRole("row"); + if (!container) { + return []; + } + let rows; + try { + rows = within(container).getAllByRole("row"); + } catch (error) { + rows = Array.from(container.querySelectorAll(".properties-data-row")); + } const res = []; for (let i = 0; i < rows.length; i++) { if (rows[i].outerHTML.includes("properties-data-row")) { diff --git a/canvas_modules/common-canvas/__tests__/common-properties/panels/columnselection-test.js b/canvas_modules/common-canvas/__tests__/common-properties/panels/columnselection-test.js index b4ef8e32ab..9a56521650 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/panels/columnselection-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/panels/columnselection-test.js @@ -15,73 +15,96 @@ */ import { expect } from "chai"; - -import propertyUtils from "./../../_utils_/property-utils"; -import tableUtils from "./../../_utils_/table-utils"; +import { within } from "@testing-library/react"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL"; +import tableUtilsRTL from "../../_utils_/table-utilsRTL"; import columnSelectionPanel from "./../../test_resources/paramDefs/columnSelectionPanel_multiInput_paramDef.json"; import panelParamDef from "./../../test_resources/paramDefs/panel_paramDef.json"; import panelConditionsParamDef from "./../../test_resources/paramDefs/panelConditions_paramDef.json"; +import { cleanup, fireEvent } from "@testing-library/react"; describe("selectcolumn and selectcolumns controls work in columnSelection panel", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should show correct values from selectcolumn controls", () => { - const panel1 = wrapper.find("div[data-id='properties-field1_panel']"); - expect(panel1.find("span.cds--list-box__label").text()).to.equal("age"); + const { container } = wrapper; + const panel1 = container.querySelector("div[data-id='properties-field1_panel']"); + const label1 = panel1.querySelector("span.cds--list-box__label"); + expect(label1.textContent).to.equal("age"); + const dropdownButton = panel1.querySelector("button.cds--list-box__field"); + expect(dropdownButton).to.not.be.null; + fireEvent.click(dropdownButton); + const dropdown1 = panel1.querySelectorAll("ul.cds--list-box__menu li"); + expect(dropdown1).to.not.be.null; + const actualOptions = Array.from(dropdown1).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); const expectedOptions = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "age", value: "age" }, { label: "Na", value: "Na" }, { label: "drug", value: "drug" } ]; - const actualOptions = panel1.find("Dropdown").prop("items"); expect(actualOptions).to.eql(expectedOptions); - - const panel2 = wrapper.find("div[data-id='properties-field2_panel']"); - expect(panel2.find("span.cds--list-box__label").text()).to.equal("BP"); + const panel2 = container.querySelector("div[data-id='properties-field2_panel']"); + const label2 = panel2.querySelector("span.cds--list-box__label"); + expect(label2.textContent).to.equal("BP"); const expectedOptions2 = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "BP", value: "BP" }, { label: "Na", value: "Na" }, { label: "drug", value: "drug" } ]; - const actualOptions2 = panel2.find("Dropdown").prop("items"); + const dropdownButton2 = panel2.querySelector("button.cds--list-box__field"); + expect(dropdownButton2).to.not.be.null; + fireEvent.click(dropdownButton2); + const dropdown2 = panel2.querySelectorAll("ul.cds--list-box__menu li"); + const actualOptions2 = Array.from(dropdown2).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); expect(actualOptions2).to.eql(expectedOptions2); }); it("should show correct values from selectcolumn and selectcolumns controls", () => { - let panel1 = wrapper.find("div[data-id='properties-selectcolumn']"); - expect(panel1.find("span.cds--list-box__label").text()).to.equal("..."); + const { container } = wrapper; + let panel1 = container.querySelector("div[data-id='properties-selectcolumn']"); + const label1 = panel1.querySelector("span.cds--list-box__label"); + expect(label1.textContent).to.equal("..."); const expectedOptions = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "age", value: "age" }, { label: "BP", value: "BP" }, { label: "Na", value: "Na" }, { label: "drug", value: "drug" } ]; - const actualOptions = panel1.find("Dropdown").prop("items"); + const dropdownButton = panel1.querySelector("button.cds--list-box__field"); + expect(dropdownButton).to.not.be.null; + fireEvent.click(dropdownButton); + const dropdown = panel1.querySelectorAll("ul.cds--list-box__menu li"); + const actualOptions = Array.from(dropdown).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); expect(actualOptions).to.eql(expectedOptions); // open the dropdown - const dropdownButton = panel1.find("button"); - dropdownButton.simulate("click"); - panel1 = wrapper.find("div[data-id='properties-selectcolumn']"); - const dropdownList = panel1.find("li.cds--list-box__menu-item"); - expect(dropdownList).to.be.length(5); + expect(dropdown.length).to.equal(5); // select age - dropdownList.at(1).simulate("click"); - panel1 = wrapper.find("div[data-id='properties-selectcolumn']"); - const fieldPicker = tableUtils.openFieldPickerForEmptyTable(wrapper, "properties-ctrl-selectcolumns"); - tableUtils.fieldPicker(fieldPicker, ["BP"], ["BP", "Na", "drug"]); - const panel2 = wrapper.find("div[data-id='properties-selectcolumns']"); - const rows = tableUtils.getTableRows(panel2); + fireEvent.click(dropdown[1]); + panel1 = container.querySelector("div[data-id='properties-selectcolumn']"); + const fieldPicker = tableUtilsRTL.openFieldPickerForEmptyTable(container, "properties-ctrl-selectcolumns"); + tableUtilsRTL.fieldPicker(fieldPicker, ["BP"], ["BP", "Na", "drug"]); + const panel2 = container.querySelector("div[data-id='properties-selectcolumns']"); + const rows = tableUtilsRTL.getTableRows(panel2); expect(rows).to.have.length(1); }); }); @@ -90,21 +113,22 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel let wrapper; let controller; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(columnSelectionPanel); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(columnSelectionPanel); wrapper = renderedObject.wrapper; controller = renderedObject.controller; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should show correct values from selectcolumn controls with multi schema input", () => { - let panel1 = wrapper.find("div[data-id='properties-field1_panel']"); - expect(panel1.find("span.cds--list-box__label").text()).to.equal("0.Age"); - + const { container } = wrapper; + let panel1 = container.querySelector("div[data-id='properties-field1_panel']"); + const label1 = panel1.querySelector("span.cds--list-box__label"); + expect(label1.textContent).to.equal("0.Age"); let expectedSelectColumn1Options = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "0.Age", value: "0.Age" }, { label: "0.Sex", value: "0.Sex" }, { label: "0.Drug", value: "0.Drug" }, @@ -118,14 +142,21 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel { label: "2.drug2", value: "2.drug2" }, { label: "2.drug3", value: "2.drug3" } ]; - let actualOptions = panel1.find("Dropdown").prop("items"); - expect(actualOptions).to.eql(expectedSelectColumn1Options); - - let panel2 = wrapper.find("div[data-id='properties-field2_panel']"); - expect(panel2.find("span.cds--list-box__label").text()).to.equal("0.BP"); + let dropdownButton = panel1.querySelector("button.cds--list-box__field"); + expect(dropdownButton).to.not.be.null; + fireEvent.click(dropdownButton); + let actualOptions1 = Array.from(panel1.querySelectorAll("ul.cds--list-box__menu li")).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); + expect(actualOptions1).to.eql(expectedSelectColumn1Options); + + let panel2 = container.querySelector("div[data-id='properties-field2_panel']"); + const label2 = panel2.querySelector("span.cds--list-box__label"); + expect(label2.textContent).to.equal("0.BP"); let expectedSelectColumn2Options = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "0.Sex", value: "0.Sex" }, { label: "0.BP", value: "0.BP" }, { label: "0.Drug", value: "0.Drug" }, @@ -139,18 +170,22 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel { label: "2.drug2", value: "2.drug2" }, { label: "2.drug3", value: "2.drug3" } ]; - let actualOptions2 = panel2.find("Dropdown").prop("items"); + const dropdownButton2 = panel2.querySelector("button.cds--list-box__field"); + expect(dropdownButton2).to.not.be.null; + fireEvent.click(dropdownButton2); + let actualOptions2 = Array.from(panel2.querySelectorAll("ul.cds--list-box__menu li")).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); expect(actualOptions2).to.eql(expectedSelectColumn2Options); // open the dropdown - const dropdownButton = panel1.find("button"); - dropdownButton.simulate("click"); - panel1 = wrapper.find("div[data-id='properties-field1_panel']"); - const dropdownList = panel1.find("li.cds--list-box__menu-item"); + panel1 = container.querySelector("div[data-id='properties-field1_panel']"); + const dropdownList = panel1.querySelectorAll("li.cds--list-box__menu-item"); expect(dropdownList).to.be.length(13); // select data.drug2 - dropdownList.at(8).simulate("click"); + fireEvent.click(dropdownList[8]); expectedSelectColumn1Options = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "0.Age", value: "0.Age" }, { label: "0.Sex", value: "0.Sex" }, { label: "0.Drug", value: "0.Drug" }, @@ -164,12 +199,18 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel { label: "2.drug2", value: "2.drug2" }, { label: "2.drug3", value: "2.drug3" } ]; - panel1 = wrapper.find("div[data-id='properties-field1_panel']"); - actualOptions = panel1.find("Dropdown").prop("items"); - expect(actualOptions).to.eql(expectedSelectColumn1Options); + dropdownButton = panel1.querySelector("button.cds--list-box__field"); + expect(dropdownButton).to.not.be.null; + fireEvent.click(dropdownButton); + panel1 = container.querySelector("div[data-id='properties-field1_panel']"); + actualOptions1 = Array.from(panel1.querySelectorAll("ul.cds--list-box__menu li")).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); + expect(actualOptions1).to.eql(expectedSelectColumn1Options); expectedSelectColumn2Options = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "0.Age", value: "0.Age" }, { label: "0.Sex", value: "0.Sex" }, { label: "0.BP", value: "0.BP" }, @@ -183,14 +224,19 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel { label: "2.drug2", value: "2.drug2" }, { label: "2.drug3", value: "2.drug3" } ]; - panel2 = wrapper.find("div[data-id='properties-field2_panel']"); - actualOptions2 = panel2.find("Dropdown").prop("items"); + panel2 = container.querySelector("div[data-id='properties-field2_panel']"); + actualOptions2 = Array.from(panel2.querySelectorAll("ul.cds--list-box__menu li")).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); expect(actualOptions2).to.eql(expectedSelectColumn2Options); }); it("should show correct values from selectcolumn and selectcolumns controls with multi schema input", () => { - let panel1 = wrapper.find("div[data-id='properties-selectcolumn']"); - expect(panel1.find("span.cds--list-box__label").text()).to.equal("..."); + const { container } = wrapper; + let panel1 = container.querySelector("div[data-id='properties-selectcolumn']"); + const label1 = panel1.querySelector("span.cds--list-box__label"); + expect(label1.textContent).to.equal("..."); const fieldTable = [ { "name": "Age", "schema": "0" }, @@ -207,26 +253,36 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel { "name": "drug2", "schema": "2" }, { "name": "drug3", "schema": "2" } ]; - let actualOptions = panel1.find("Dropdown").prop("items"); + let dropdownButton = panel1.querySelector("button.cds--list-box__field"); + expect(dropdownButton).to.not.be.null; + fireEvent.click(dropdownButton); + let actualOptions = Array.from(panel1.querySelectorAll("ul.cds--list-box__menu li")).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); expect(actualOptions.length).to.equal(fieldTable.length + 1); // +1 for "..." - const fieldPicker = tableUtils.openFieldPickerForEmptyTable(wrapper, "properties-ctrl-selectcolumns"); - tableUtils.fieldPicker(fieldPicker, ["0.drug2", "2.drug2"], fieldTable); + const fieldPicker = tableUtilsRTL.openFieldPickerForEmptyTable(container, "properties-ctrl-selectcolumns"); + tableUtilsRTL.fieldPicker(fieldPicker, ["0.drug2", "2.drug2"], fieldTable); // open the dropdown - const dropdownButton = panel1.find("button"); - dropdownButton.simulate("click"); - panel1 = wrapper.find("div[data-id='properties-selectcolumn']"); - const dropdownList = panel1.find("li.cds--list-box__menu-item"); + panel1 = container.querySelector("div[data-id='properties-selectcolumn']"); + const dropdownList = panel1.querySelectorAll("li.cds--list-box__menu-item"); expect(dropdownList).to.be.length(12); // select data.Age - dropdownList.at(5).simulate("click"); - panel1 = wrapper.find("div[data-id='properties-selectcolumn']"); - actualOptions = panel1.find("Dropdown").prop("items"); + fireEvent.click(dropdownList[5]); + panel1 = container.querySelector("div[data-id='properties-selectcolumn']"); + dropdownButton = panel1.querySelector("button.cds--list-box__field"); + expect(dropdownButton).to.not.be.null; + fireEvent.click(dropdownButton); + actualOptions = Array.from(panel1.querySelectorAll("ul.cds--list-box__menu li")).map((item) => ({ + label: item.textContent.trim(), + value: item.getAttribute("data-value") || item.textContent.trim() + })); expect(actualOptions.length).to.equal(fieldTable.length - 1); const expectedOptions = [ - { label: "...", value: "" }, + { label: "...", value: "..." }, { label: "0.Age", value: "0.Age" }, { label: "0.Sex", value: "0.Sex" }, { label: "0.BP", value: "0.BP" }, @@ -239,24 +295,24 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel { label: "2.drug", value: "2.drug" }, { label: "2.drug3", value: "2.drug3" } ]; - expect(actualOptions).to.have.deep.members(expectedOptions); + expect(actualOptions).to.deep.equal(expectedOptions); }); it("should show correct values from selectcolumns controls with multi schema input", () => { - const selectColumnsTable2 = wrapper.find("div[data-id='properties-ft-selectcolumns2']"); - expect(selectColumnsTable2).to.have.length(1); + const { container } = wrapper; + const selectColumnsTable2 = container.querySelector("div[data-id='properties-ft-selectcolumns2']"); + expect(selectColumnsTable2).to.exist; // selectColumnsTable3 is an empty table. It shows empty table text and Add columns button - const selectColumnsTable3 = wrapper.find("div[data-id='properties-ctrl-selectcolumns3']"); - expect(selectColumnsTable3).to.have.length(1); + const selectColumnsTable3 = container.querySelectorAll("div[data-id='properties-ctrl-selectcolumns3']"); + expect(selectColumnsTable3).to.exist; - const table2Rows = tableUtils.getTableRows(selectColumnsTable2); + const table2Rows = tableUtilsRTL.getTableRows(selectColumnsTable2); expect(table2Rows).to.have.length(3); const table2Initial = ["0.Age", "0.Drug", "2.Age"]; - for (let idx = 0; idx < table2Rows.length; idx++) { - expect(table2Rows.at(idx).find(".properties-field-type") - .text()).to.equal(table2Initial[idx]); - } + table2Rows.forEach((row, idx) => { + expect(row.querySelector(".properties-field-type").textContent).to.equal(table2Initial[idx]); + }); const fieldTable = [ { "name": "Sex", "schema": "0" }, @@ -294,9 +350,9 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel { "link_ref": "2", "field_name": "drug2" }, { "link_ref": "2", "field_name": "drug3" } ]; - const fieldPicker = tableUtils.openFieldPickerForEmptyTable(wrapper, "properties-ctrl-selectcolumns3"); - tableUtils.fieldPicker(fieldPicker, selectcolumns3, fieldTable); - expect(controller.getPropertyValue({ name: "selectcolumns3" })).to.have.deep.members(selectcolumns3A); + const fieldPicker = tableUtilsRTL.openFieldPickerForEmptyTable(container, "properties-ctrl-selectcolumns3"); + tableUtilsRTL.fieldPicker(fieldPicker, selectcolumns3, fieldTable); + expect(controller.getPropertyValue({ name: "selectcolumns3" })).to.deep.equal(selectcolumns3A); // Verify field picker from selectcolumns2 gets the correct fields const fieldTable2Input = [ @@ -334,12 +390,14 @@ describe("selectcolumn and selectcolumns controls work in columnSelection panel "origName": "Age" } ]; - expect(controller.getFilteredDatasetMetadata({ name: "selectcolumns2" })).to.have.deep.members(fieldTable2Input); + expect(controller.getFilteredDatasetMetadata({ name: "selectcolumns2" })).to.deep.equal(fieldTable2Input); }); it("should handle improply defined string fields as strings", () => { - const panel1 = wrapper.find("div[data-id='properties-BADVAR1']"); - expect(panel1.find("span.cds--list-box__label").text()).to.equal("0.Age"); + const { container } = wrapper; + const panel1 = container.querySelector("div[data-id='properties-BADVAR1']"); + const label1 = panel1.querySelector("span.cds--list-box__label"); + expect(label1.textContent).to.equal("0.Age"); }); }); @@ -347,58 +405,59 @@ describe("column selection panel visible and enabled conditions work correctly", let wrapper; let controller; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; controller = renderedObject.controller; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("controls in column selection panel should be disabled", () => { // by default it is enabled - const checkboxWrapper = wrapper.find("div[data-id='properties-disableColumnSelectionPanel']"); - const disabledCheckbox = checkboxWrapper.find("input"); - expect(disabledCheckbox.getDOMNode().checked).to.equal(false); + const { container } = wrapper; + const checkboxWrapper = container.querySelector("div[data-id='properties-disableColumnSelectionPanel']"); + const disabledCheckbox = checkboxWrapper.querySelector("input"); + expect(disabledCheckbox).to.have.property("checked", false); expect(controller.getPanelState({ name: "selectcolumn-values" })).to.equal("enabled"); expect(controller.getControlState({ name: "field1_panel" })).to.equal("enabled"); expect(controller.getControlState({ name: "field2_panel" })).to.equal("enabled"); - // disable the controls - disabledCheckbox.getDOMNode().checked = true; - disabledCheckbox.simulate("change"); + fireEvent.click(disabledCheckbox); expect(controller.getPanelState({ name: "selectcolumn-values" })).to.equal("disabled"); expect(controller.getControlState({ name: "field1_panel" })).to.equal("disabled"); expect(controller.getControlState({ name: "field2_panel" })).to.equal("disabled"); - // check that the controls are disabled. - const panel = wrapper.find("div[data-id='properties-disable-selectcolumn-values']"); - const disabledPanel = panel.find("div.properties-control-panel").at(1); - const disabledItems = disabledPanel.find("div.properties-control-item"); + const panel = container.querySelector("div[data-id='properties-disable-selectcolumn-values']"); + const disabledPanel = panel.querySelector("div.properties-control-panel"); + const disabledItems = disabledPanel.querySelectorAll("div.properties-control-item"); expect(disabledItems).to.have.length(2); - expect(disabledItems.at(0).prop("disabled")).to.be.true; - expect(disabledItems.at(1).prop("disabled")).to.be.true; + expect(disabledItems[0].hasAttribute("disabled")).to.be.true; + expect(disabledItems[1].hasAttribute("disabled")).to.be.true; }); it("controls in column selection panel should be hidden", () => { - const checkboxWrapper = wrapper.find("div[data-id='properties-hideColumnSelectionPanel']"); - const hiddenCheckbox = checkboxWrapper.find("input"); - expect(hiddenCheckbox.getDOMNode().checked).to.equal(false); - + const { container } = wrapper; + const checkboxWrapper = container.querySelector("div[data-id='properties-hideColumnSelectionPanel']"); + const hiddenCheckbox = checkboxWrapper.querySelector("input"); + expect(hiddenCheckbox.checked).to.equal(false); expect(controller.getPanelState({ name: "column-selection-panel" })).to.equal("visible"); // hide the controls - hiddenCheckbox.getDOMNode().checked = true; - hiddenCheckbox.simulate("change"); + fireEvent.click(hiddenCheckbox); expect(controller.getPanelState({ name: "column-selection-panel" })).to.equal("hidden"); expect(controller.getControlState({ name: "selectcolumn" })).to.equal("hidden"); expect(controller.getControlState({ name: "selectcolumns" })).to.equal("hidden"); // check that the controls are hidden. - const panel = wrapper.find("div[data-id='properties-hide-column-selection-panel']"); - const hiddenPanel = panel.find("div.properties-control-panel").at(1); - const hiddenItems = hiddenPanel.find("div.properties-control-item"); + const panel = container.querySelector("div[data-id='properties-hide-column-selection-panel']"); + expect(panel).to.exist; + const hiddenPanel = panel.querySelectorAll("div.properties-control-panel"); + expect(hiddenPanel.length).to.be.greaterThan(0); + const hiddenPanel1 = hiddenPanel[0]; + expect(hiddenPanel1).to.exist; + const hiddenItems = within(hiddenPanel1).queryAllByRole("div", { name: /properties-control-item/i }); expect(hiddenItems).to.have.length(0); }); }); @@ -406,22 +465,25 @@ describe("column selection panel visible and enabled conditions work correctly", describe("column selection panel classNames applied correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("column selection panel should have custom classname defined", () => { - const columnSelectionWrapper = wrapper.find("div[data-id='properties-column-selections']"); - expect(columnSelectionWrapper.find(".selectcolumn-values-group-columnselection-class")).to.have.length(1); - expect(columnSelectionWrapper.find(".column-selection-panel-group-columnselection-class")).to.have.length(1); + const { container } = wrapper; + const columnSelectionWrapper = container.querySelector("div[data-id='properties-column-selections']"); + expect(columnSelectionWrapper.querySelectorAll(".selectcolumn-values-group-columnselection-class")).to.have.length(1); + expect(columnSelectionWrapper.querySelectorAll(".column-selection-panel-group-columnselection-class")).to.have.length(1); }); it("column selection panel in a structuretable should have custom classname defined", () => { - propertyUtils.openSummaryPanel(wrapper, "structuretable-summary-panel1"); - expect(wrapper.find(".column-selection-panel-group-columnselection-class")).to.have.length(1); + const { container } = wrapper; + propertyUtilsRTL.openSummaryPanel(wrapper, "structuretable-summary-panel1"); + expect(container.querySelectorAll(".column-selection-panel-group-columnselection-class")).to.have.length(1); }); }); + diff --git a/canvas_modules/common-canvas/__tests__/common-properties/panels/panel-test.js b/canvas_modules/common-canvas/__tests__/common-properties/panels/panel-test.js index 4a4159025d..43e76d6abd 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/panels/panel-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/panels/panel-test.js @@ -18,7 +18,8 @@ import React from "react"; import { expect } from "chai"; -import propertyUtils from "./../../_utils_/property-utils"; +import { cleanup } from "@testing-library/react"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL"; import panelParamDef from "./../../test_resources/paramDefs/panel_paramDef.json"; import customPanelParamDef from "./../../test_resources/paramDefs/CustomPanel_paramDef.json"; import panelConditionsParamDef from "./../../test_resources/paramDefs/panelConditions_paramDef.json"; @@ -28,13 +29,14 @@ import AddtlCmptsTest from "./../../_utils_/custom-components/AddtlCmptsTest.jsx // possibly move these under each panel describe("empty panels render correctly", () => { - const renderedObject = propertyUtils.flyoutEditorForm(panelParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelParamDef); const wrapper = renderedObject.wrapper; - it("should render each panel", () => { - const panelContainer = wrapper.find("div[data-id='properties-empty-panels-container']"); - expect(panelContainer).to.have.length(1); - expect(panelContainer.children()).to.have.length(10); + const { container } = wrapper; + const panelContainer = container.querySelector("div[data-id='properties-empty-panels-container']"); + expect(panelContainer).to.exist; + const children = panelContainer.children; + expect(children.length).to.equal(7); // Receiving 7 childrens as per RTL children method }); }); @@ -42,107 +44,120 @@ describe("additional components are rendered correctly", () => { it("when additional components are added to a tab group, it should be rendered at the same level as the other controls", () => { const propertiesInfo = { additionalComponents: { "toggle-panel": } }; const propertiesConfig = { rightFlyout: true }; - const renderedObject = propertyUtils.flyoutEditorForm(customPanelParamDef, propertiesConfig, null, propertiesInfo); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(customPanelParamDef, propertiesConfig, null, propertiesInfo); const wrapper = renderedObject.wrapper; - const customPanel = wrapper.find(".properties-custom-container"); - expect(customPanel).to.have.length(1); - const togglePanelContainer = customPanel.find(".properties-category-container").at(0); - const togglePanelContent = togglePanelContainer.find(".cds--accordion__content"); - expect(togglePanelContent.children()).to.have.length(2); // Default Component & Additional Component - + const { container } = wrapper; + const customPanel = container.querySelector(".properties-custom-container"); + expect(customPanel).to.exist; + const togglePanelContainer = customPanel.querySelector(".properties-category-container"); + expect(togglePanelContainer).to.exist; + const togglePanelContent = togglePanelContainer.querySelector(".cds--accordion__content"); + expect(togglePanelContent).to.exist; + const children = togglePanelContent.children; + expect(children).to.have.length(2); // Default Component & Additional Component*/ }); }); describe("group panel classNames applied correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("group panels should have custom classname defined", () => { + const { container } = wrapper; // top level group panels - expect(wrapper.find(".text-panels-group-panels-class")).to.have.length(1); + expect(container.querySelectorAll(".text-panels-group-panels-class")).to.have.length(1); // double nested panels - expect(wrapper.find(".level1-group-panels-class")).to.have.length(1); + expect(container.querySelectorAll(".level1-group-panels-class")).to.have.length(1); // deeply nested group panels - expect(wrapper.find(".level3-group-panels-class")).to.have.length(1); + expect(container.querySelectorAll(".level3-group-panels-class")).to.have.length(1); }); }); describe("nested panels render correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(nestedPanelParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(nestedPanelParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("Text panels should be nested when nested_panel is set to true", () => { + const { container } = wrapper; // Default text panel - const defaultTextPanel = wrapper.find(".default-textpanel-class"); - expect(defaultTextPanel.hasClass("properties-control-nested-panel")).to.equal(false); + const defaultTextPanel = container.querySelector(".default-textpanel-class"); + expect(defaultTextPanel.classList.contains("properties-control-nested-panel")).to.equal(false); // Nested text panel - const nestedTextPanel = wrapper.find(".nested-textpanel-class"); - expect(nestedTextPanel.hasClass("properties-control-nested-panel")).to.equal(true); + const nestedTextPanel = container.querySelector(".nested-textpanel-class"); + expect(nestedTextPanel.classList.contains("properties-control-nested-panel")).to.equal(true); }); it("Column selection panels should be nested when nested_panel is set to true", () => { + const { container } = wrapper; // Default columnSelection panel - const defaultColumnSelectionPanel = wrapper.find(".default-columnselection-class"); - expect(defaultColumnSelectionPanel.hasClass("properties-control-nested-panel")).to.equal(false); + const defaultColumnSelectionPanel = container.querySelector(".default-columnselection-class"); + expect(defaultColumnSelectionPanel.classList.contains("properties-control-nested-panel")).to.equal(false); // Nested columnSelection panel - const nestedColumnSelectionPanel = wrapper.find(".nested-columnselection-class"); - expect(nestedColumnSelectionPanel.hasClass("properties-control-nested-panel")).to.equal(true); + const nestedColumnSelectionPanel = container.querySelector(".nested-columnselection-class"); + expect(nestedColumnSelectionPanel.classList.contains("properties-control-nested-panel")).to.equal(true); + }); it("Summary panels should be nested when nested_panel is set to true", () => { + const { container } = wrapper; // Default summary panel - const defaultSummaryPanel = wrapper.find(".default-summarypanel-class"); - expect(defaultSummaryPanel.hasClass("properties-control-nested-panel")).to.equal(false); + const defaultSummaryPanel = container.querySelector(".default-summarypanel-class"); + expect(defaultSummaryPanel.classList.contains("properties-control-nested-panel")).to.equal(false); // Nested summary panel - const nestedSummaryPanel = wrapper.find(".nested-summarypanel-class"); - expect(nestedSummaryPanel.hasClass("properties-control-nested-panel")).to.equal(true); + const nestedSummaryPanel = container.querySelector(".nested-summarypanel-class"); + expect(nestedSummaryPanel.classList.contains("properties-control-nested-panel")).to.equal(true); + }); it("Twisty panels should be nested when nested_panel is set to true", () => { + const { container } = wrapper; // Default twisty panel - const defaultTwistyPanel = wrapper.find(".default-twistypanel-class"); - expect(defaultTwistyPanel.hasClass("properties-control-nested-panel")).to.equal(false); + const defaultTwistyPanel = container.querySelector(".default-twistypanel-class"); + expect(defaultTwistyPanel.classList.contains("properties-control-nested-panel")).to.equal(false); // Nested twisty panel - const nestedTwistyPanel = wrapper.find(".nested-twistypanel-class"); - expect(nestedTwistyPanel.hasClass("properties-control-nested-panel")).to.equal(true); + const nestedTwistyPanel = container.querySelector(".nested-twistypanel-class"); + expect(nestedTwistyPanel.classList.contains("properties-control-nested-panel")).to.equal(true); }); it("Action panels should be nested when nested_panel is set to true", () => { + const { container } = wrapper; // Default action panel - const defaultActionPanel = wrapper.find(".default-actionpanel-class"); - expect(defaultActionPanel.hasClass("properties-control-nested-panel")).to.equal(false); + const defaultActionPanel = container.querySelector(".default-actionpanel-class"); + expect(defaultActionPanel.classList.contains("properties-control-nested-panel")).to.be.equal(false); // Nested action panel - const nestedActionPanel = wrapper.find(".nested-actionpanel-class"); - expect(nestedActionPanel.hasClass("properties-control-nested-panel")).to.equal(true); + const nestedAction = container.querySelector(".nested-actionpanel-class"); + expect(nestedAction.classList.contains("properties-control-nested-panel")).to.equal(true); }); it("Column panels should be nested when nested_panel is set to true", () => { // Default column panel - const defaultColumnPanel = wrapper.find(".default-columnpanel-class"); - expect(defaultColumnPanel.hasClass("properties-control-nested-panel")).to.equal(false); + const { container } = wrapper; + const defaultColumnPanel = container.querySelector(".default-columnpanel-class"); + expect(defaultColumnPanel.classList.contains("properties-control-nested-panel")).to.equal(false); // Nested column panel - const nestedColumnPanel = wrapper.find(".nested-columnpanel-class"); - expect(nestedColumnPanel.hasClass("properties-control-nested-panel")).to.equal(true); + const nestedColumnPanel = container.querySelector(".nested-columnpanel-class"); + expect(nestedColumnPanel.classList.contains("properties-control-nested-panel")).to.equal(true); + }); }); diff --git a/canvas_modules/common-canvas/__tests__/common-properties/panels/selector-test.js b/canvas_modules/common-canvas/__tests__/common-properties/panels/selector-test.js index 08199f1126..2ad946c7b5 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/panels/selector-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/panels/selector-test.js @@ -14,68 +14,76 @@ * limitations under the License. */ -import propertyUtils from "./../../_utils_/property-utils"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL"; import { expect } from "chai"; import PANEL_SELECTOR_PARAM_DEF from "./../../test_resources/paramDefs/panelSelector_paramDef.json"; import panelConditionsParamDef from "./../../test_resources/paramDefs/panelConditions_paramDef.json"; +import { cleanup, fireEvent, waitFor } from "@testing-library/react"; describe("'panel selector insert' renders correctly", () => { let wrapper; let controller; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(PANEL_SELECTOR_PARAM_DEF); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(PANEL_SELECTOR_PARAM_DEF); wrapper = renderedObject.wrapper; controller = renderedObject.controller; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); - it("it should have 3 text panels", () => { + it("it should have 3 text panels", async() => { + // Properties should have 3 text panels description - const descriptions = wrapper.find("div[data-id='properties-fruit-color2'] .panel-description"); + const { container } = wrapper; + const descriptions = container.querySelectorAll("div[data-id='properties-fruit-color2'] .panel-description"); expect(descriptions).to.have.length(3); // Check the descriptions are as expected. - expect(descriptions.at(0).text()).to.equal("Apples ripen six to 10 times faster at room temperature than if they are refrigerated."); - expect(descriptions.at(1).text()).to.equal("Blueberries freeze in just 4 minutes."); - expect(descriptions.at(2).text()).to.equal("Lemons are a hybrid between a sour orange and a citron."); + expect(descriptions[0].textContent).to.equal("Apples ripen six to 10 times faster at room temperature than if they are refrigerated."); + expect(descriptions[1].textContent).to.equal("Blueberries freeze in just 4 minutes."); + expect(descriptions[2].textContent).to.equal("Lemons are a hybrid between a sour orange and a citron."); // Check that the red(0) text panel is enabled and blue (1) and yellow (2) // text panels are disabled. - var redState = controller.getPanelState({ "name": "red2" }); + let redState = controller.getPanelState({ "name": "red2" }); expect(redState).to.equal("enabled"); - var blueState = controller.getPanelState({ "name": "blue2" }); + let blueState = controller.getPanelState({ "name": "blue2" }); expect(blueState).to.equal("disabled"); - var yellowState = controller.getPanelState({ "name": "yellow2" }); + let yellowState = controller.getPanelState({ "name": "yellow2" }); expect(yellowState).to.equal("disabled"); // Simulate a click on blue (1) radio button - const input = wrapper.find("div[data-id='properties-fruit-color2']"); - expect(input).to.have.length(1); - const radios = input.find("input[type='radio']"); - expect(radios).to.have.length(3); + const input = container.querySelector("div[data-id='properties-fruit-color2']"); + expect(input).to.exist; + const radios = input.querySelectorAll("input[type='radio']"); + expect(radios.length).to.equal(3); - const radioBlue = radios.find("input[value='blue2']"); - radioBlue.simulate("change", { target: { checked: true, value: "blue2" } }); + const radioBlue = radios[1]; + fireEvent.click(radioBlue); // Check that the blue (1) text panel is enabled and red (0) and yellow (2) // text panels are disabled. - redState = controller.getPanelState({ "name": "red2" }); - expect(redState).to.equal("disabled"); - blueState = controller.getPanelState({ "name": "blue2" }); - expect(blueState).to.equal("enabled"); - yellowState = controller.getPanelState({ "name": "yellow2" }); - expect(yellowState).to.equal("disabled"); + await waitFor(() => { + redState = controller.getPanelState({ "name": "red2" }); + expect(redState).to.equal("disabled"); + blueState = controller.getPanelState({ "name": "blue2" }); + expect(blueState).to.equal("enabled"); + yellowState = controller.getPanelState({ "name": "yellow2" }); + expect(yellowState).to.equal("disabled"); + }); + + }); it("panel selector and controls should be disabled", () => { // disabled - const checkboxWrapper = wrapper.find("div[data-id='properties-disablePanelSelector']"); - const disabledCheckbox = checkboxWrapper.find("input"); - expect(disabledCheckbox.props().checked).to.equal(true); + const { container } = wrapper; + const checkboxWrapper = container.querySelector("div[data-id='properties-disablePanelSelector']"); + const disabledCheckbox = checkboxWrapper.querySelector("input"); + expect(disabledCheckbox.checked).to.equal(true); expect(controller.getControlState({ name: "fruit-color11" })).to.equal("disabled"); expect(controller.getControlState({ name: "number" })).to.equal("disabled"); @@ -84,37 +92,34 @@ describe("'panel selector insert' renders correctly", () => { expect(controller.getPanelState({ name: "dynamicTextSum" })).to.equal("disabled"); // enable - const node = disabledCheckbox.getDOMNode(); - node.checked = false; - disabledCheckbox.simulate("change"); + fireEvent.click(disabledCheckbox); expect(controller.getControlState({ name: "fruit-color11" })).to.equal("enabled"); expect(controller.getControlState({ name: "number" })).to.equal("enabled"); expect(controller.getPanelState({ name: "panel-selector-fields11" })).to.equal("enabled"); expect(controller.getPanelState({ name: "dynamicTextPercent" })).to.equal("enabled"); expect(controller.getPanelState({ name: "dynamicTextSum" })).to.equal("enabled"); + + }); it("panel selector and controls should be hidden", () => { // hidden - const checkboxWrapper = wrapper.find("div[data-id='properties-hidePanelSelector']"); - const hiddenCheckbox = checkboxWrapper.find("input"); - expect(hiddenCheckbox.props().checked).to.equal(true); - - expect(controller.getControlState({ name: "fruit-color21" })).to.equal("hidden"); - expect(controller.getPanelState({ name: "panel-selector-fields21" })).to.equal("hidden"); + const { container } = wrapper; + const checkboxWrapper = container.querySelector("div[data-id='properties-hidePanelSelector']"); + const hiddenCheckbox = checkboxWrapper.querySelector("input"); + expect(hiddenCheckbox.checked).to.equal(true); // visible - hiddenCheckbox.getDOMNode().checked = false; - hiddenCheckbox.simulate("change"); + fireEvent.click(hiddenCheckbox); expect(controller.getControlState({ name: "fruit-color21" })).to.equal("visible"); expect(controller.getPanelState({ name: "panel-selector-fields21" })).to.equal("visible"); - }); - it("all panels for selected type should be enabled", () => { + }); + it("all panels for selected type should be enabled", async() => { expect(controller.getPropertyValue({ name: "fruit-color3" })).to.equal("red3"); expect(controller.getPanelState({ name: "red3" })).to.equal("enabled"); expect(controller.getPanelState({ name: "blue3" })).to.equal("disabled"); @@ -127,29 +132,30 @@ describe("'panel selector insert' renders correctly", () => { expect(controller.getControlState({ name: "blueberry-size" })).to.equal("disabled"); // change selection - const input = wrapper.find("div[data-id='properties-fruit-color3']"); - expect(input).to.have.length(1); - const radios = input.find("input[type='radio']"); + const { container } = wrapper; + const input = container.querySelector("div[data-id='properties-fruit-color3']"); + expect(input).to.be.exist; + const radios = input.querySelectorAll("input[type='radio']"); expect(radios).to.have.length(6); - const radioBlue = radios.find("input[value='blue3']"); - radioBlue.simulate("change", { target: { checked: true, value: "blue3" } }); - - expect(controller.getPropertyValue({ name: "fruit-color3" })).to.equal("blue3"); - expect(controller.getPanelState({ name: "red3" })).to.equal("disabled"); - expect(controller.getPanelState({ name: "blue3" })).to.equal("enabled"); - expect(controller.getPanelState({ name: "yellow3" })).to.equal("disabled"); - expect(controller.getPanelState({ name: "apple-text" })).to.equal("disabled"); - expect(controller.getPanelState({ name: "apple-types-ctl" })).to.equal("disabled"); - expect(controller.getControlState({ name: "apple-types" })).to.equal("disabled"); - expect(controller.getPanelState({ name: "blue-text" })).to.equal("enabled"); - expect(controller.getPanelState({ name: "blueberry-size-ctl" })).to.equal("enabled"); - expect(controller.getControlState({ name: "blueberry-size" })).to.equal("enabled"); - + const radioBlue = container.querySelector("input[value='blue3']"); + expect(radioBlue).to.be.exist; + fireEvent.click(radioBlue); + await waitFor(() => { + expect(controller.getPropertyValue({ name: "fruit-color3" })).to.equal("blue3"); + expect(controller.getPanelState({ name: "red3" })).to.equal("disabled"); + expect(controller.getPanelState({ name: "blue3" })).to.equal("enabled"); + expect(controller.getPanelState({ name: "yellow3" })).to.equal("disabled"); + expect(controller.getPanelState({ name: "apple-text" })).to.equal("disabled"); + expect(controller.getPanelState({ name: "apple-types-ctl" })).to.equal("disabled"); + expect(controller.getControlState({ name: "apple-types" })).to.equal("disabled"); + expect(controller.getPanelState({ name: "blue-text" })).to.equal("enabled"); + expect(controller.getPanelState({ name: "blueberry-size-ctl" })).to.equal("enabled"); + expect(controller.getControlState({ name: "blueberry-size" })).to.equal("enabled"); + }); // change selection - const radioYellow = radios.find("input[value='yellow3']"); - radioYellow.simulate("change", { target: { checked: true, value: "yellow3" } }); - + const radioYellow = container.querySelector("input[value='yellow3']"); + fireEvent.click(radioYellow); expect(controller.getPropertyValue({ name: "fruit-color3" })).to.equal("yellow3"); expect(controller.getPanelState({ name: "red3" })).to.equal("disabled"); expect(controller.getPanelState({ name: "blue3" })).to.equal("disabled"); @@ -160,23 +166,23 @@ describe("'panel selector insert' renders correctly", () => { expect(controller.getPanelState({ name: "blue-text" })).to.equal("disabled"); expect(controller.getPanelState({ name: "blueberry-size-ctl" })).to.equal("disabled"); expect(controller.getControlState({ name: "blueberry-size" })).to.equal("disabled"); - }); }); describe("text panel classNames applied correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("selector panel should have custom classname defined", () => { - const panelSelectorWrapper = wrapper.find("div[data-id='properties-panel-selector2']"); - expect(panelSelectorWrapper.find(".panel-selector2-group-panelselector-class")).to.have.length(1); + const { container } = wrapper; + const panelSelectorWrapper = container.querySelector("div[data-id='properties-panel-selector2']"); + expect(panelSelectorWrapper.querySelectorAll(".panel-selector2-group-panelselector-class")).to.have.length(1); }); }); diff --git a/canvas_modules/common-canvas/__tests__/common-properties/panels/subtabs-test.js b/canvas_modules/common-canvas/__tests__/common-properties/panels/subtabs-test.js index bd28e6b29c..2be71c9919 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/panels/subtabs-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/panels/subtabs-test.js @@ -14,26 +14,27 @@ * limitations under the License. */ -import propertyUtils from "./../../_utils_/property-utils"; +import { cleanup, waitFor } from "@testing-library/react"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL"; import tabParamDef from "./../../test_resources/paramDefs/tab_paramDef.json"; - import { expect } from "chai"; describe("subtabs renders correctly", () => { var wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(tabParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(tabParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should have displayed the 4 tabs created with 6 nested subtabs", () => { - const tabContainer = wrapper.find("div[data-id='properties-Primary'] div.properties-sub-tab-container"); + const { container } = wrapper; + const tabContainer = container.querySelector("div[data-id='properties-Primary'] div.properties-sub-tab-container"); // should render 1 control panel - expect(tabContainer.find("button.properties-subtab")).to.have.length(10); + expect(tabContainer.querySelectorAll("button.properties-subtab")).to.have.length(10); }); }); @@ -41,40 +42,46 @@ describe("subtabs visible and enabled conditions work correctly", () => { let wrapper; let controller; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(tabParamDef, { categoryView: "tabs" }); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(tabParamDef, { categoryView: "tabs" }); wrapper = renderedObject.wrapper; controller = renderedObject.controller; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); - it("subtabs and controls should be disabled", () => { - let subTab = wrapper.find("button[data-id='properties-fruit-subtab']"); + it("subtabs and controls should be disabled", async() => { + const { container } = wrapper; + let subTab = container.querySelector("button[data-id='properties-fruit-subtab']"); // check initial state of enabled - expect(subTab.prop("aria-disabled")).to.equal(false); + expect(subTab.getAttribute("aria-disabled")).to.equal("false"); controller.updatePropertyValue({ name: "disable" }, true); - wrapper.update(); - subTab = wrapper.find("button[data-id='properties-fruit-subtab']"); - expect(subTab.prop("aria-disabled")).to.equal(true); + await waitFor(() => { + subTab = container.querySelector("button[data-id='properties-fruit-subtab']"); + expect(subTab.getAttribute("aria-disabled")).to.equal("true"); + }); }); - it("subtabs and controls should be hidden", () => { - let subTab = wrapper.find("button[data-id='properties-table-subtab']"); + it("subtabs and controls should be hidden", async() => { + const { container } = wrapper; + let subTab = container.querySelectorAll("button[data-id='properties-table-subtab']"); expect(subTab).to.have.length(1); controller.updatePropertyValue({ name: "hide" }, true); - wrapper.update(); - subTab = wrapper.find("button[data-id='properties-table-subtab']"); - expect(subTab).to.have.length(0); + await waitFor(() => { + subTab = container.querySelectorAll("button[data-id='properties-table-subtab']"); + expect(subTab).to.have.length(0); + }); + }); - it("hidden and non hidden tabs display correctly", () => { - let primaryTabs = wrapper.find(".properties-primaryTabs"); - let tab1 = primaryTabs.find("button[title='Tab Test']"); - let tab2 = primaryTabs.find("button[title='Tab Test2']"); - let tab3 = primaryTabs.find("button[title='Tab Test3']"); - let tab4 = primaryTabs.find("button[title='Tab Test4']"); + it("hidden and non hidden tabs display correctly", async() => { + const { container } = wrapper; + let primaryTabs = container.querySelector(".properties-primaryTabs"); + let tab1 = primaryTabs.querySelectorAll("button[title='Tab Test']"); + let tab2 = primaryTabs.querySelectorAll("button[title='Tab Test2']"); + let tab3 = primaryTabs.querySelectorAll("button[title='Tab Test3']"); + let tab4 = primaryTabs.querySelectorAll("button[title='Tab Test4']"); expect(tab1).to.have.length(1); expect(tab2).to.have.length(1); expect(tab3).to.have.length(1); @@ -82,65 +89,69 @@ describe("subtabs visible and enabled conditions work correctly", () => { controller.updatePropertyValue({ name: "hideTab1" }, true); controller.updatePropertyValue({ name: "hideTab4" }, true); - wrapper.update(); - primaryTabs = wrapper.find(".properties-primaryTabs"); - tab1 = primaryTabs.find("button[title='Tab Test']"); - tab2 = primaryTabs.find("button[title='Tab Test2']"); - tab3 = primaryTabs.find("button[title='Tab Test3']"); - tab4 = primaryTabs.find("button[title='Tab Test4']"); + await waitFor(() => { + primaryTabs = container.querySelector(".properties-primaryTabs"); + tab1 = primaryTabs.querySelectorAll("button[title='Tab Test']"); + tab2 = primaryTabs.querySelectorAll("button[title='Tab Test2']"); + tab3 = primaryTabs.querySelectorAll("button[title='Tab Test3']"); + tab4 = primaryTabs.querySelectorAll("button[title='Tab Test4']"); + + expect(tab1).to.have.length(0); + expect(tab2).to.have.length(1); + expect(tab3).to.have.length(1); + expect(tab4).to.have.length(0); + }); - expect(tab1).to.have.length(0); - expect(tab2).to.have.length(1); - expect(tab3).to.have.length(1); - expect(tab4).to.have.length(0); }); }); describe("subtabs classNames applied correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(tabParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(tabParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("subtab container should have custom classname defined", () => { - const mainTab = wrapper.find("div.maintab-panel-class"); - expect(mainTab.find("div.subtab-panel-class")).to.have.length(1); + const { container } = wrapper; + const mainTab = container.querySelector("div.maintab-panel-class"); + expect(mainTab.querySelectorAll("div.subtab-panel-class")).to.have.length(1); }); it("subtabs should have custom classname defined", () => { - const subTabs = wrapper.find("div.properties-sub-tab-container").at(0); - expect(subTabs.find(".range-fields-subtab-control-class")).to.have.length(1); - expect(subTabs.find(".table-subtab-control-class")).to.have.length(1); - expect(subTabs.find(".fruit-subtab-control-class")).to.have.length(1); + const { container } = wrapper; + const subTabs = container.querySelector("div.properties-sub-tab-container"); + expect(subTabs.querySelectorAll(".range-fields-subtab-control-class")).to.have.length(1); + expect(subTabs.querySelectorAll(".table-subtab-control-class")).to.have.length(1); + expect(subTabs.querySelectorAll(".fruit-subtab-control-class")).to.have.length(1); }); }); describe("subtabs renders correctly in a Tearsheet container", () => { - let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(tabParamDef, { rightFlyout: false, containerType: "Tearsheet" }); - wrapper = renderedObject.wrapper; + propertyUtilsRTL.flyoutEditorForm(tabParamDef, { rightFlyout: false, containerType: "Tearsheet" }); }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should have rendered subtabs with leftnav classnames", () => { - const primaryTabs = wrapper.find("div.properties-primary-tab-panel.tearsheet-container"); + const primaryTabs = document.querySelectorAll("div.properties-primary-tab-panel.tearsheet-container"); expect(primaryTabs).to.have.length(5); - const primaryTab = primaryTabs.at(2); // Tab Test2 - expect(primaryTab.find("div.properties-sub-tab-container.vertical.properties-leftnav-container")).to.have.length(1); + const primaryTab = primaryTabs[2]; // Tab Test2 + expect(primaryTab.querySelectorAll("div.properties-sub-tab-container.vertical.properties-leftnav-container")).to.have.length(1); - const leftNav = primaryTab.find("div.properties-subtabs.properties-leftnav-subtabs"); - expect(leftNav).to.have.length(1); - expect(leftNav.find("button.properties-leftnav-subtab-item")).to.have.length(3); + const leftNav = primaryTab.querySelector("div.properties-subtabs.properties-leftnav-subtabs"); + expect(leftNav).to.not.be.null; + expect(leftNav.querySelectorAll("button.properties-leftnav-subtab-item")).to.have.length(3); }); }); + + diff --git a/canvas_modules/common-canvas/__tests__/common-properties/panels/summary-test.js b/canvas_modules/common-canvas/__tests__/common-properties/panels/summary-test.js index 2e0d1bfcab..2c96464d6d 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/panels/summary-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/panels/summary-test.js @@ -14,76 +14,76 @@ * limitations under the License. */ -import propertyUtils from "./../../_utils_/property-utils"; -import tableUtils from "./../../_utils_/table-utils"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL"; +import tableUtilsRTL from "../../_utils_/table-utilsRTL"; import summarypanelParamDef from "./../../test_resources/paramDefs/summarypanel_paramDef.json"; import panelConditionsParamDef from "./../../test_resources/paramDefs/panelConditions_paramDef.json"; import { expect } from "chai"; +import { cleanup, waitFor, fireEvent } from "@testing-library/react"; describe("summary renders correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(summarypanelParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(summarypanelParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should have displayed the initial values in the summary", () => { - const summaries = wrapper.find("div.properties-summary-values"); + const { container } = wrapper; + const summaries = container.querySelectorAll("div.properties-summary-values"); expect(summaries).to.have.length(3); // all summary tables including table in wideflyout - const sortSummary = wrapper.find("div[data-id='properties-structuretableSortOrder-summary-panel']"); - const sortSummaryRows = sortSummary.find("tr.properties-summary-row"); + const sortSummary = container.querySelector("div[data-id='properties-structuretableSortOrder-summary-panel']"); + const sortSummaryRows = sortSummary.querySelectorAll("tr.properties-summary-row"); expect(sortSummaryRows).to.have.length(1); - const sortRow1 = sortSummaryRows.at(0).find("td.properties-summary-row-data") - .at(0); - - expect(sortRow1.find("span").at(0) - .text() - .trim()).to.equal("Cholesterol"); + const sortRow1 = sortSummaryRows[0].querySelector("td.properties-summary-row-data"); + const sortRow1Span = sortRow1.querySelector("span"); + expect(sortRow1Span.textContent.trim()).to.equal("Cholesterol"); // validate tooltip content is correct - expect(sortRow1.find("div.properties-tooltips div") - .at(0) - .text() - .trim()).to.equal("Cholesterol"); + const tooltip = sortRow1.querySelector("div.properties-truncated-tooltip"); + expect(tooltip.textContent.trim()).to.equal("Cholesterol"); + }); it("should open fieldpicker when type unknown", () => { - const sortSummary = wrapper.find("div[data-id='properties-structuretableSortOrder-summary-panel']"); - const summaryButton = sortSummary.find("button.properties-summary-link-button"); - summaryButton.simulate("click"); - const fieldPickerWrapper = tableUtils.openFieldPicker(wrapper, "properties-structuretableSortOrder"); - tableUtils.fieldPicker(fieldPickerWrapper, ["Age"], ["Age", "Sex", "BP", "Cholesterol", "Na", "K", "Drug"]); + const { container } = wrapper; + const sortSummary = container.querySelector("div[data-id='properties-structuretableSortOrder-summary-panel']"); + const summaryButton = sortSummary.querySelector("button.properties-summary-link-button"); + fireEvent.click(summaryButton); + const fieldPickerWrapper = tableUtilsRTL.openFieldPicker(container, "properties-structuretableSortOrder"); + tableUtilsRTL.fieldPicker(fieldPickerWrapper, ["Age"], ["Age", "Sex", "BP", "Cholesterol", "Na", "K", "Drug"]); }); }); describe("summary panel renders correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(summarypanelParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(summarypanelParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should have displayed placeholder in summary panel for more then 10 fields", () => { - const summaries = wrapper.find("div[data-id='properties-Derive-Node'] .properties-summary-values"); - const summaryRows = summaries.at(1).find("tr.properties-summary-row"); // Table Input + const { container } = wrapper; + const summaries = container.querySelectorAll("div[data-id='properties-Derive-Node'] .properties-summary-values"); + const summaryRows = summaries[1].querySelectorAll("tr.properties-summary-row"); // Table Input expect(summaryRows).to.have.length(0); - const summaryPlaceholder = summaries.at(1).find("div.properties-summary-table span"); - expect(summaryPlaceholder).to.have.length(1); - expect(summaryPlaceholder.text()).to.equal("More than ten fields..."); + const summaryPlaceholder = summaries[1].querySelector("div.properties-summary-table span"); + expect(summaryPlaceholder).to.exist; + expect(summaryPlaceholder.textContent).to.equal("More than ten fields..."); }); it("should have a summary panel in a summary panel", () => { - const wideflyout = propertyUtils.openSummaryPanel(wrapper, "structuretableSortOrder-summary-panel"); - const summaryButton = wideflyout.find("button.properties-summary-link-button"); - expect(summaryButton).to.have.length(1); - const summaryData = wideflyout.find("tr.properties-summary-row"); + const wideflyout = propertyUtilsRTL.openSummaryPanel(wrapper, "structuretableSortOrder-summary-panel"); + const summaryButton = wideflyout.querySelectorAll("button.properties-summary-link-button"); + expect(summaryButton).to.have.lengthOf(1); + const summaryData = wideflyout.querySelectorAll("tr.properties-summary-row"); expect(summaryData).to.have.length(1); }); }); @@ -91,150 +91,158 @@ describe("summary panel renders correctly", () => { describe("summary panel renders error/warning status correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(summarypanelParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(summarypanelParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should show warning message in summary when removing rows", () => { - let wideflyout = propertyUtils.openSummaryPanel(wrapper, "Derive-Node"); - tableUtils.clickTableRows(wideflyout, [0]); + let wideflyout = propertyUtilsRTL.openSummaryPanel(wrapper, "Derive-Node"); + tableUtilsRTL.clickTableRows(wideflyout, [0]); // ensure table toolbar has Delete button and click it - wideflyout = wrapper.find("div.properties-wf-content.show"); - let tableWrapper = wideflyout.find("div[data-id='properties-expressionCellTable']"); - let deleteButtons = tableWrapper.find("button.delete-button"); + const { container } = wrapper; + wideflyout = container.querySelector("div.properties-wf-content.show"); + let tableWrapper = wideflyout.querySelector("div[data-id='properties-expressionCellTable']"); + let deleteButtons = tableWrapper.querySelectorAll("button.delete-button"); expect(deleteButtons).to.have.length(2); - deleteButtons.at(0).simulate("click"); + fireEvent.click(deleteButtons[0]); // remove second row - tableUtils.clickTableRows(wideflyout, [0]); - wideflyout = wrapper.find("div.properties-wf-content.show"); - tableWrapper = wideflyout.find("div[data-id='properties-expressionCellTable']"); - deleteButtons = tableWrapper.find("button.delete-button"); + tableUtilsRTL.clickTableRows(wideflyout, [0]); + wideflyout = container.querySelector("div.properties-wf-content.show"); + tableWrapper = wideflyout.querySelector("div[data-id='properties-expressionCellTable']"); + deleteButtons = tableWrapper.querySelectorAll("button.delete-button"); expect(deleteButtons).to.have.length(1); - deleteButtons.at(0).simulate("click"); - + fireEvent.click(deleteButtons[0]); // close fly-out - wideflyout.find("button.properties-apply-button").simulate("click"); + const propertyButton = wideflyout.querySelector("button.properties-apply-button"); + fireEvent.click(propertyButton); // check that Alerts tab is added - const alertCategory = wrapper.find("div.properties-category-container").at(0); // alert category - const alertButton = alertCategory.find("button.cds--accordion__heading"); - expect(alertButton.text()).to.equal("Alerts (1)"); - alertButton.simulate("click"); - const alertList = alertCategory.find("div.properties-link-text-container.warning"); + const alertCategory = container.querySelectorAll("div.properties-category-container")[0]; // alert category + const alertButton = alertCategory.querySelector("button.cds--accordion__heading"); + expect(alertButton.textContent).to.equal("Alerts (1)"); + fireEvent.click(alertButton); + const alertList = alertCategory.querySelectorAll("div.properties-link-text-container.warning"); expect(alertList).to.have.length(1); - const warningMsg = alertList.at(0).find("a.properties-link-text"); - expect(warningMsg.text()).to.equal("Expression cell table cannot be empty"); + const warningMsg = alertList[0].querySelector("a.properties-link-text"); + expect(warningMsg.textContent).to.equal("Expression cell table cannot be empty"); // click on the link should open up structure list table category - warningMsg.simulate("click"); - expect(wrapper.find("li.properties-category-content.show")).to.have.length(1); + fireEvent.click(warningMsg); + expect(container.querySelectorAll("li.properties-category-content.show")).to.have.length(1); // check that warning icon is shown in summary - let tableCategory = wrapper.find("div[data-id='properties-Derive-Node']"); - let summary = tableCategory.find("div.properties-summary-link-container"); - expect(summary.find("svg.warning")).to.have.length(1); + let tableCategory = container.querySelector("div[data-id='properties-Derive-Node']"); + let summary = tableCategory.querySelector("div.properties-summary-link-container"); + expect(summary.querySelectorAll("svg.warning")).to.have.length(1); // add row back in tables - tableCategory.find("button.properties-summary-link-button").simulate("click"); - wideflyout = wrapper.find("div.properties-wf-content.show"); - wideflyout.find("button.properties-empty-table-button").simulate("click"); + const summaryLinkButton = tableCategory.querySelector("button.properties-summary-link-button"); + fireEvent.click(summaryLinkButton); + wideflyout = container.querySelector("div.properties-wf-content.show"); + const emptyTableButton = wideflyout.querySelector("button.properties-empty-table-button"); + fireEvent.click(emptyTableButton); // close fly-out - wideflyout.find("button.properties-apply-button").simulate("click"); - + const applyButton = wideflyout.querySelector("button.properties-apply-button"); + fireEvent.click(applyButton); // ensure warning message and alerts tab are gone - tableCategory = wrapper.find("div[data-id='properties-Derive-Node']"); - summary = tableCategory.find("div.properties-summary-link-container"); - expect(summary.find("svg.warning")).to.have.length(0); + tableCategory = container.querySelector("div[data-id='properties-Derive-Node']"); + summary = tableCategory.querySelector("div.properties-summary-link-container"); + expect(summary.querySelectorAll("svg.warning")).to.have.length(0); }); - it("should show error icon in summary when both error and warning messages exist", () => { - let wideflyout = propertyUtils.openSummaryPanel(wrapper, "Derive-Node"); - tableUtils.clickTableRows(wideflyout, [0]); + it("should show error icon in summary when both error and warning messages exist", async() => { + let wideflyout = propertyUtilsRTL.openSummaryPanel(wrapper, "Derive-Node"); + tableUtilsRTL.clickTableRows(wideflyout, [0]); - wideflyout = wrapper.find("div.properties-wf-content.show"); + const { container } = wrapper; + wideflyout = container.querySelector("div.properties-wf-content.show"); // ensure table toolbar has Delete button and click it - let tableWrapper = wideflyout.find("div[data-id='properties-expressionCellTable']"); - let deleteButtons = tableWrapper.find("button.delete-button"); - deleteButtons.at(0).simulate("click"); + let tableWrapper = wideflyout.querySelector("div[data-id='properties-expressionCellTable']"); + let deleteButtons = tableWrapper.querySelectorAll("button.delete-button"); + fireEvent.click(deleteButtons[0]); // remove second row - tableUtils.clickTableRows(wideflyout, [0]); - wideflyout = wrapper.find("div.properties-wf-content.show"); - tableWrapper = wideflyout.find("div[data-id='properties-expressionCellTable']"); - deleteButtons = tableWrapper.find("button.delete-button"); - deleteButtons.at(0).simulate("click"); - + tableUtilsRTL.clickTableRows(wideflyout, [0]); + wideflyout = container.querySelector("div.properties-wf-content.show"); + tableWrapper = wideflyout.querySelector("div[data-id='properties-expressionCellTable']"); + deleteButtons = tableWrapper.querySelectorAll("button.delete-button"); + fireEvent.click(deleteButtons[0]); // check that all rows were removed - wideflyout = wrapper.find("div.properties-wf-content.show"); - expect(tableUtils.getTableRows(wideflyout.find("div[data-id='properties-expressionCellTable']"))).to.have.length(0); + wideflyout = container.querySelector("div.properties-wf-content.show"); + expect(tableUtilsRTL.getTableRows(wideflyout.querySelector("div[data-id='properties-expressionCellTable']"))).to.have.length(0); + + wideflyout = container.querySelector("div.properties-wf-content.show"); + expect(tableUtilsRTL.getTableRows(wideflyout.querySelector("div[data-id='properties-ft-structurelisteditorTableInput']"))).to.have.length(11); - wideflyout = wrapper.find("div.properties-wf-content.show"); - expect(tableUtils.getTableRows(wideflyout.find("div[data-id='properties-ft-structurelisteditorTableInput']"))).to.have.length(11); // remove all rows from Table Input table - const tableInputBodyData = wideflyout.find("div[data-id='properties-ft-structurelisteditorTableInput']"); + const tableInputBodyData = wideflyout.querySelector("div[data-id='properties-ft-structurelisteditorTableInput']"); summarypanelParamDef.current_parameters.structurelisteditorTableInput.forEach((value) => { - tableUtils.selectCheckboxes(tableInputBodyData, [0]); - const tableInputRemoveButton = wrapper.find("div[data-id='properties-ft-structurelisteditorTableInput']") - .find("div.properties-table-toolbar") - .find("button.properties-action-delete"); + tableUtilsRTL.selectCheckboxes(tableInputBodyData, [0]); + const tableToolbar = wideflyout.querySelector("div.properties-table-toolbar"); + const tableInputRemoveButton = tableToolbar.querySelectorAll("button.properties-action-delete"); expect(tableInputRemoveButton).to.have.length(1); - - tableInputRemoveButton.simulate("click"); + fireEvent.click(tableInputRemoveButton[0]); }); // check that all rows were removed - wideflyout = wrapper.find("div.properties-wf-content.show"); - expect(tableUtils.getTableRows(wideflyout.find("div[data-id='properties-ft-structurelisteditorTableInput']"))).to.have.length(0); + wideflyout = container.querySelector("div.properties-wf-content.show"); + expect(tableUtilsRTL.getTableRows(wideflyout.querySelector("div[data-id='properties-ft-structurelisteditorTableInput']"))).to.have.length(0); // close fly-out - wideflyout.find("button.properties-apply-button").simulate("click"); + const PropApplyButton = wideflyout.querySelector("button.properties-apply-button"); + fireEvent.click(PropApplyButton); // check that Alerts tab is added and that is shows error message before warning message - let alertCategory = wrapper.find("div.properties-category-container").at(0); // alert category - expect(alertCategory.find("button.cds--accordion__heading").text()).to.equal("Alerts (2)"); - let alertList = alertCategory.find("div.properties-link-text-container"); + let alertCategory = container.querySelectorAll("div.properties-category-container")[0]; // alert category + expect(alertCategory.querySelector("button.cds--accordion__heading").textContent).to.equal("Alerts (2)"); + let alertList = alertCategory.querySelectorAll("div.properties-link-text-container"); expect(alertList).to.have.length(2); - const errorWrapper = alertCategory.find("div.properties-link-text-container.error"); - expect(errorWrapper).to.have.length(1); - expect(errorWrapper.find("a.properties-link-text").text()).to.equal("Structure list editor table cannot be empty"); - let warningWrapper = alertCategory.find("div.properties-link-text-container.warning"); - expect(warningWrapper).to.have.length(1); - expect(warningWrapper.find("a.properties-link-text").text()).to.equal("Expression cell table cannot be empty"); + const errorWrapper = alertCategory.querySelector("div.properties-link-text-container.error"); + expect(errorWrapper).to.not.be.null; + expect(errorWrapper.querySelector("a.properties-link-text").textContent).to.equal("Structure list editor table cannot be empty"); + let warningWrapper = alertCategory.querySelector("div.properties-link-text-container.warning"); + expect(warningWrapper).to.not.be.null; + expect(warningWrapper.querySelector("a.properties-link-text").textContent).to.equal("Expression cell table cannot be empty"); // check that summary icon is an error icon - let tableCategory = wrapper.find("div.properties-category-container").at(1); // Structure list table category - expect(tableCategory.find("button.cds--accordion__heading").text()).to.equal("Structure List Table (2)"); - let summary = tableCategory.find("div.properties-summary-link-container"); - expect(summary.find("svg.error")).to.have.length(1); + let tableCategory = container.querySelectorAll("div.properties-category-container")[1]; // Structure list table category + expect(tableCategory.querySelector("button.cds--accordion__heading").textContent).to.equal("Structure List Table (2)"); + let summary = tableCategory.querySelector("div.properties-summary-link-container"); + expect(summary.querySelectorAll("svg.error")).to.have.length(1); // add row back into Table Input table - tableCategory.find("button.properties-summary-link-button").simulate("click"); - wideflyout = wrapper.find("div.properties-wf-content.show"); + const summaryLinkButton = tableCategory.querySelector("button.properties-summary-link-button"); + fireEvent.click(summaryLinkButton); + wideflyout = container.querySelector("div.properties-wf-content.show"); + + const emptyTabButton = wideflyout.querySelectorAll("button.properties-empty-table-button")[1]; + fireEvent.click(emptyTabButton); - wideflyout.find("button.properties-empty-table-button").at(1) - .simulate("click"); // close fly-out - wideflyout.find("button.properties-apply-button").simulate("click"); + const propApplyButton = wideflyout.querySelector("button.properties-apply-button"); + fireEvent.click(propApplyButton); // check that Alerts tab is added and that is shows error message before warning message - alertCategory = wrapper.find("div.properties-category-container").at(0); // alert category - expect(alertCategory.find("button.cds--accordion__heading").text()).to.equal("Alerts (1)"); - alertList = alertCategory.find("div.properties-link-text-container"); + alertCategory = container.querySelectorAll("div.properties-category-container")[0]; // alert category + expect(alertCategory.querySelector("button.cds--accordion__heading").textContent).to.equal("Alerts (1)"); + alertList = alertCategory.querySelectorAll("div.properties-link-text-container"); expect(alertList).to.have.length(1); - warningWrapper = alertCategory.find("div.properties-link-text-container.warning"); - expect(warningWrapper).to.have.length(1); - expect(warningWrapper.find("a.properties-link-text").text()).to.equal("Expression cell table cannot be empty"); + warningWrapper = alertCategory.querySelector("div.properties-link-text-container.warning"); + expect(warningWrapper).to.not.be.null; + expect(warningWrapper.querySelector("a.properties-link-text").textContent).to.equal("Expression cell table cannot be empty"); + // check that summary icon is an error icon - tableCategory = wrapper.find("div.properties-category-container").at(1); // Structure list table category - expect(tableCategory.find("button.cds--accordion__heading").text()).to.equal("Structure List Table (1)"); - summary = tableCategory.find("div.properties-summary-link-container"); - expect(summary.find("svg.warning")).to.have.length(1); + tableCategory = container.querySelectorAll("div.properties-category-container")[1]; // Structure list table category + expect(tableCategory.querySelector("button.cds--accordion__heading").textContent).to.equal("Structure List Table (1)"); + summary = tableCategory.querySelector("div.properties-summary-link-container"); + expect(summary.querySelectorAll("svg.warning")).to.have.length(1); + }); }); @@ -242,63 +250,71 @@ describe("summary panel visible and enabled conditions work correctly", () => { let wrapper; let controller; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; controller = renderedObject.controller; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); - it("summary panel link should be disabled and table should be gone", () => { - let firstSummary = wrapper.find("div[data-id='properties-structuretable-summary-panel1']"); - expect(firstSummary.props().disabled).to.be.false; - expect(firstSummary.find("div.properties-summary-values")).to.have.length(2); + it("summary panel link should be disabled and table should be gone", async() => { + const { container } = wrapper; + let firstSummary = container.querySelector("div[data-id='properties-structuretable-summary-panel1']"); + expect(firstSummary.hasAttribute("disabled")).to.equal(false); + const summaryValue = firstSummary.querySelectorAll("div.properties-summary-values"); + expect(summaryValue.length).to.equal(2); expect(controller.getPanelState({ name: "structuretable-summary-panel1" })).to.equal("enabled"); expect(controller.getControlState({ name: "structuretable_summary1" })).to.equal("enabled"); expect(controller.getControlState({ name: "structuretable_summary2" })).to.equal("enabled"); controller.updatePropertyValue({ name: "enableSummary" }, false); - wrapper.update(); - firstSummary = wrapper.find("div[data-id='properties-structuretable-summary-panel1']"); - expect(firstSummary.props().disabled).to.be.true; - expect(controller.getPanelState({ name: "structuretable-summary-panel1" })).to.equal("disabled"); - expect(controller.getControlState({ name: "structuretable_summary1" })).to.equal("disabled"); - expect(controller.getControlState({ name: "structuretable_summary2" })).to.equal("disabled"); - expect(firstSummary.find("div.properties-summary-values")).to.have.length(0); + await waitFor(() => { + firstSummary = container.querySelector("div[data-id='properties-structuretable-summary-panel1']"); + expect(firstSummary.hasAttribute("disabled")).to.be.true; + expect(controller.getPanelState({ name: "structuretable-summary-panel1" })).to.equal("disabled"); + expect(controller.getControlState({ name: "structuretable_summary1" })).to.equal("disabled"); + expect(controller.getControlState({ name: "structuretable_summary2" })).to.equal("disabled"); + const propSummaryValue = firstSummary.querySelectorAll("div.properties-summary-values"); + expect(propSummaryValue.length).to.equal(0); + }); }); - it("summary panel link should be hidden", () => { - let secondSummary = wrapper.find("div[data-id='properties-structuretable-summary-panel2']"); - const link = secondSummary.find("button.properties-summary-link-button"); + it("summary panel link should be hidden", async() => { + const { container } = wrapper; + let secondSummary = container.querySelector("div[data-id='properties-structuretable-summary-panel2']"); + const link = secondSummary.querySelectorAll("button.properties-summary-link-button"); expect(link).to.have.length(1); expect(controller.getPanelState({ name: "structuretable-summary-panel2" })).to.equal("visible"); - expect(secondSummary.find("div.properties-summary-values")).to.have.length(1); + expect(secondSummary.querySelectorAll("div.properties-summary-values")).to.have.length(1); controller.updatePropertyValue({ name: "hideSummary" }, true); - wrapper.update(); - expect(controller.getPanelState({ name: "structuretable-summary-panel2" })).to.equal("hidden"); - expect(controller.getControlState({ name: "structuretable_summary3" })).to.equal("hidden"); - secondSummary = wrapper.find("div[data-id='properties-structuretable-summary-panel2']"); - expect(secondSummary.find("div.properties-summary-values")).to.have.length(0); + await waitFor(() => { + expect(controller.getPanelState({ name: "structuretable-summary-panel2" })).to.equal("hidden"); + expect(controller.getControlState({ name: "structuretable_summary3" })).to.equal("hidden"); + secondSummary = container.querySelector("div[data-id='properties-structuretable-summary-panel2']"); + expect(secondSummary.querySelectorAll("div.properties-summary-values")).to.have.length(0); + }); }); }); describe("summary panel classNames applied correctly", () => { let wrapper; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("summary panel should have custom classname defined", () => { - const summaryContainer = wrapper.find("div[data-id='properties-summary_panel_category']"); - expect(summaryContainer.find(".structuretable-summary-panel1-category-group-summarypanel-class")).to.have.length(1); + const { container } = wrapper; + const summaryContainer = container.querySelector("div[data-id='properties-summary_panel_category']"); + expect(summaryContainer.querySelectorAll(".structuretable-summary-panel1-category-group-summarypanel-class")).to.have.lengthOf(1); }); }); + diff --git a/canvas_modules/common-canvas/__tests__/common-properties/panels/tearsheet-test.js b/canvas_modules/common-canvas/__tests__/common-properties/panels/tearsheet-test.js index 76a40f15ea..ee69169548 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/panels/tearsheet-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/panels/tearsheet-test.js @@ -16,66 +16,77 @@ // Test suite for generic tearsheet testing import React from "react"; -import propertyUtils from "./../../_utils_/property-utils"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL"; import { expect } from "chai"; -import { mountWithIntl } from "../../_utils_/intl-utils"; +import { renderWithIntl } from "../../_utils_/intl-utils"; import TearSheet from "./../../../src/common-properties/panels/tearsheet"; import codeParamDef from "./../../test_resources/paramDefs/code_paramDef.json"; import Sinon from "sinon"; +import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react"; describe("tearsheet tests", () => { - let wrapper; let controller; + let renderedObject; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(codeParamDef); - wrapper = renderedObject.wrapper; + renderedObject = propertyUtilsRTL.flyoutEditorForm(codeParamDef); controller = renderedObject.controller; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("should be have only one tearsheet rendered", () => { - expect(wrapper.find("div.properties-tearsheet-panel")).to.have.length(1); + expect(document.querySelectorAll("div.properties-tearsheet-panel")).to.have.length(1); }); - it("should be visible from the controller method", () => { + it("should be visible from the controller method", async() => { controller.setActiveTearsheet("tearsheet1"); - wrapper.update(); - expect(wrapper.find("div.properties-tearsheet-panel.is-visible")).to.have.length(1); + await waitFor(() => { + const tearsheetpanel = document.querySelector("div.properties-tearsheet-panel.is-visible"); + expect(tearsheetpanel).to.not.be.null; + }); }); - it("should have title and description set", () => { + it("should have title and description set", async() => { controller.setActiveTearsheet("tearsheet1"); - wrapper.update(); - expect(wrapper.find("div.properties-tearsheet-panel .properties-tearsheet-header h2").text()).to.equal("Python"); - expect(wrapper.find("div.properties-tearsheet-panel .properties-tearsheet-header p").text()).to.equal("Your change is automatically saved."); + await waitFor(() => { + expect(document.querySelector("div.properties-tearsheet-panel .properties-tearsheet-header h2").textContent).to.equal("Python"); + expect(document.querySelector("div.properties-tearsheet-panel .properties-tearsheet-header p").textContent).to.equal("Your change is automatically saved."); + }); }); - it("should be hidden but not removed from DOM on the tearsheet close button", () => { + it("should be hidden but not removed from DOM on the tearsheet close button", async() => { controller.setActiveTearsheet("tearsheet1"); - wrapper.update(); - wrapper.find("div.properties-tearsheet-panel button.cds--modal-close").simulate("click"); - wrapper.update(); - expect(wrapper.find("div.properties-tearsheet-panel.is-visible")).to.have.length(0); - expect(wrapper.find("div.properties-tearsheet-panel")).to.have.length(1); - expect(controller.getActiveTearsheet()).to.equal(null); + await waitFor(() => { + const buttonModalClose = document.querySelector("div.properties-tearsheet-panel"); + expect(buttonModalClose).to.not.be.null; + expect(buttonModalClose.classList.contains("is-visible")).to.be.false; + + const closeButton = document.querySelector("button.cds--modal-close"); + expect(closeButton).to.not.be.null; + fireEvent.click(closeButton); + expect(document.querySelectorAll("div.properties-tearsheet-panel.is-visible")).to.have.length(0); + expect(document.querySelectorAll("div.properties-tearsheet-panel")).to.have.length(1); + expect(controller.getActiveTearsheet()).to.equal(null); + }); + }); - it("should show tearsheet content which is previously hidden", () => { - expect(wrapper.find("div.properties-tearsheet-panel")).to.have.length(1); - expect(wrapper.find("div.properties-tearsheet-panel.is-visible")).to.have.length(0); - expect(wrapper.find("div.properties-tearsheet-panel div[data-id='properties-ctrl-code_rows']")).to.have.length(0); - wrapper.update(); + it("should show tearsheet content which is previously hidden", async() => { + expect(document.querySelectorAll("div.properties-tearsheet-panel")).to.have.length(1); + expect(document.querySelectorAll("div.properties-tearsheet-panel.is-visible")).to.have.length(0); + expect(document.querySelectorAll("div.properties-tearsheet-panel div[data-id='properties-ctrl-code_rows']")).to.have.length(0); controller.updatePropertyValue({ name: "hide" }, false); - wrapper.update(); - wrapper.find("div[data-id='properties-ctrl-code_rows'] button.maximize").simulate("click"); - wrapper.update(); - expect(wrapper.find("div.properties-tearsheet-panel.is-visible")).to.have.length(1); - expect(wrapper.find("div.properties-tearsheet-panel .properties-tearsheet-header h2").text()).to.equal("Python 2"); - expect(wrapper.find("div.properties-tearsheet-panel div[data-id='properties-ctrl-code_rows']")).to.have.length(1); + await waitFor(() => { + const maximizeButton = document.querySelector("div[data-id='properties-ctrl-code_rows']"); + const btnClick = maximizeButton.querySelector("button.maximize"); + fireEvent.click(btnClick); + expect(document.querySelector("div.properties-tearsheet-panel.is-visible")).to.not.be.null; + expect(document.querySelector("div.properties-tearsheet-panel .properties-tearsheet-header h2").textContent).to.equal("Python 2"); + expect(document.querySelector("div.properties-tearsheet-panel div[data-id='properties-ctrl-code_rows']")).to.not.be.null; + }); }); }); describe("Tearsheet renders correctly", () => { it("should not display buttons in tearsheet if showPropertiesButtons is false", () => { - const wrapper = mountWithIntl( { showPropertiesButtons={false} applyOnBlur />); - const tearsheet = wrapper.find("div.properties-tearsheet-panel"); - expect(tearsheet).to.have.length(1); - expect(tearsheet.find("div.properties-tearsheet-header")).to.have.length(1); - expect(tearsheet.find("div.properties-tearsheet-header > h2").text()).to.equal("test title"); - expect(tearsheet.find("div.properties-tearsheet-body")).to.have.length(1); - expect(tearsheet.find("div.properties-tearsheet-body").text()).to.equal("test content"); - expect(tearsheet.find("div.properties-tearsheet-body.with-buttons")).to.have.length(0); - expect(tearsheet.find("div.properties-modal-buttons")).to.have.length(0); + const { container } = wrapper; + const tearsheet = container.getElementsByClassName("properties-tearsheet-panel"); + expect(tearsheet).to.not.be.null; + const header = screen.getByText("test title", { selector: "h2" }); + expect(header).to.exist; + expect(header.tagName).to.equal("H2"); - // Verify close button is visible - expect(tearsheet.find("div.properties-tearsheet-header.hide-close-button")).to.have.length(0); + const body = screen.getByText("test content"); + expect(body).to.exist; + expect(container.querySelectorAll("div.properties-tearsheet-body.with-buttons")).to.have.length(0); + expect(container.querySelectorAll("div.properties-modal-buttons")).to.have.length(0); }); it("should display buttons in tearsheet if showPropertiesButtons is true and applyOnBlur is false", () => { - const wrapper = mountWithIntl( { cancelHandler={Sinon.spy()} showPropertiesButtons />); - const tearsheet = wrapper.find("div.properties-tearsheet-panel"); - expect(tearsheet).to.have.length(1); - expect(tearsheet.find("div.properties-tearsheet-body.with-buttons")).to.have.length(1); - expect(tearsheet.find("div.properties-modal-buttons")).to.have.length(1); + const { container } = wrapper; + const tearsheet = container.querySelectorAll("properties-tearsheet-panel"); + expect(tearsheet).to.not.be.null; + expect(container.querySelectorAll("div.properties-tearsheet-body.with-buttons")).to.not.be.null; + expect(container.querySelectorAll("div.properties-modal-buttons")).to.not.be.null; + // Verify close button is not visible - expect(tearsheet.find("div.properties-tearsheet-header.hide-close-button")).to.have.length(1); + expect(container.querySelectorAll("div.properties-tearsheet-header.hide-close-button")).to.not.be.null; }); }); diff --git a/canvas_modules/common-canvas/__tests__/common-properties/panels/text-test.js b/canvas_modules/common-canvas/__tests__/common-properties/panels/text-test.js index aeeeeaebf2..7c8599328e 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/panels/text-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/panels/text-test.js @@ -15,61 +15,83 @@ */ import { expect } from "chai"; -import propertyUtils from "./../../_utils_/property-utils"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL"; import panelParamDef from "./../../test_resources/paramDefs/panel_paramDef.json"; import panelConditionsParamDef from "./../../test_resources/paramDefs/panelConditions_paramDef.json"; +import { cleanup, fireEvent, waitFor } from "@testing-library/react"; describe("textPanel render correctly", () => { - const renderedObject = propertyUtils.flyoutEditorForm(panelParamDef); - const wrapper = renderedObject.wrapper; + let renderedObject; + let wrapper; + + beforeEach(() => { + renderedObject = propertyUtilsRTL.flyoutEditorForm(panelParamDef); + wrapper = renderedObject.wrapper; + }); + + afterEach(() => { + cleanup(); + }); it("should have displayed correct number of textPanel elements", () => { - const panel = wrapper.find("div[data-id='properties-text-panels']"); - const staticText = panel.find("div.properties-text-panel"); + const { container } = wrapper; + const panel = container.querySelector("div[data-id='properties-text-panels']"); + const staticText = panel.querySelectorAll("div.properties-text-panel"); expect(staticText).to.have.length(5); - const labels = panel.find("div.panel-label"); + const labels = panel.querySelectorAll("div.panel-label"); expect(labels).to.have.length(5); // 3 on_panel descriptions - const descriptions = panel.find("div.panel-description"); + const descriptions = panel.querySelectorAll("div.panel-description"); expect(descriptions).to.have.length(3); // 1 tooltip description - const tooltipDescription = panel.find("div.properties-text-panel").find("div.properties-label-container"); + const tooltipDescription = panel.querySelectorAll("div.properties-text-panel div.properties-label-container"); expect(tooltipDescription).to.have.length(1); }); it("should have displayed correct text in textPanel elements", () => { - let panel = wrapper.find("div[data-id='properties-text-panels']"); - const labels = panel.find("div.panel-label"); - expect(labels.at(0).text()).to.equal("Oranges"); - let descriptions = panel.find("div.panel-description"); - expect(descriptions.at(0).text()).to.equal("An orange tree can grow to reach 30 feet and live for over a hundred years."); - expect(descriptions.at(1).text()).to.equal("Percent: 9.090909 with 6 decimals. Percent: 9.09 with 2 decimals"); - expect(descriptions.at(2).text()).to.equal("Sum: 22 with (number, number). Sum: 24 with (number, 2, number)"); - const input = panel.find("[type='number']"); - input.simulate("change", { target: { value: 0.52, validity: { badInput: false } } }); - panel = wrapper.find("div[data-id='properties-text-panels']"); - descriptions = panel.find("div.panel-description"); - expect(descriptions.at(1).text()).to.equal("Percent: 192.307692 with 6 decimals. Percent: 192.31 with 2 decimals"); - expect(descriptions.at(2).text()).to.equal("Sum: 1.04 with (number, number). Sum: 3.04 with (number, 2, number)"); + const { container } = wrapper; + let panel = container.querySelector("div[data-id='properties-text-panels']"); + const labels = panel.querySelectorAll("div.panel-label"); + expect(labels[0].textContent).to.equal("Oranges"); + let descriptions = panel.querySelectorAll("div.panel-description"); + expect(descriptions[0].textContent).to.equal("An orange tree can grow to reach 30 feet and live for over a hundred years."); + expect(descriptions[1].textContent).to.equal("Percent: 9.090909 with 6 decimals. Percent: 9.09 with 2 decimals"); + expect(descriptions[2].textContent).to.equal("Sum: 22 with (number, number). Sum: 24 with (number, 2, number)"); + const input = panel.querySelector("[type='number']"); + expect(input.tagName.toLowerCase()).to.equal("input"); + fireEvent.change(input, { target: { value: 0.52, } }); // validity has only getter function + panel = container.querySelector("div[data-id='properties-text-panels']"); + descriptions = panel.querySelectorAll("div.panel-description"); + expect(descriptions[1].textContent).to.equal("Percent: 192.307692 with 6 decimals. Percent: 192.31 with 2 decimals"); + expect(descriptions[2].textContent).to.equal("Sum: 1.04 with (number, number). Sum: 3.04 with (number, 2, number)"); }); it("should not show a description when one isn't provided", () => { - const panel = wrapper.find("div[data-id='properties-text-panels']"); - const textPanel = panel.find("div.properties-text-panel").at(4); // panel without description - expect(textPanel.find("div.panel-label").text()).to.equal("This panel shouldn't have a description"); - expect(textPanel.find("div.panel-description")).to.have.length(0); + const { container } = wrapper; + const panel = container.querySelector("div[data-id='properties-text-panels']"); + const textPanel = panel.querySelectorAll("div.properties-text-panel")[4]; // panel without description + expect(textPanel.querySelector("div.panel-label").textContent).to.equal("This panel shouldn't have a description"); + const panelDescription = textPanel.querySelectorAll("div.panel-description"); + expect(panelDescription).to.have.length(0); }); - it("should have displayed textPanel description in tooltip", () => { - const panel = wrapper.find("div[data-id='properties-text-panels']"); - const textPanel = panel.find("div.properties-text-panel").at(1); // panel with description in tooltip - expect(textPanel.find("div.panel-label").text()).to.equal("Avocados"); - expect(textPanel.find("div.properties-label-container")).to.have.length(1); + it("should have displayed textPanel description in tooltip", async() => { + const { container } = wrapper; + const textPanel = container.querySelectorAll(".properties-text-panel")[1]; // panel with description in tooltip + const textLabel = textPanel.querySelector(".panel-label"); + expect(textLabel.textContent).to.equal("Avocados"); + const labelContainer = textPanel.querySelectorAll(".properties-label-container"); + expect(labelContainer).to.have.length(1); // Show description in tooltip - const tooltipTrigger = textPanel.find(".tooltip-trigger"); - const tooltipId = tooltipTrigger.props()["aria-labelledby"]; - tooltipTrigger.simulate("click"); - const textPanelTooltip = wrapper.find(`div[data-id='${tooltipId}']`); - expect(textPanelTooltip.props()).to.have.property("aria-hidden", false); - expect(textPanelTooltip.text()).to.equal("An avocado tree can range from 15 to 30 feet tall."); - + const tooltipTrigger = textPanel.querySelector(".tooltip-trigger"); + expect(tooltipTrigger).to.not.be.null; + const tooltipId = tooltipTrigger.getAttribute("aria-labelledby"); + expect(tooltipId).to.not.be.null; + fireEvent.click(tooltipTrigger); + await waitFor(() => { + const textPanelTooltip = document.querySelector(`div[data-id='${tooltipId}']`); + expect(textPanelTooltip).to.not.be.null; + const ariaHidden = textPanelTooltip.getAttribute("aria-hidden"); + expect(ariaHidden).to.equal("false"); + expect(textPanelTooltip.textContent).to.equal("An avocado tree can range from 15 to 30 feet tall."); + }); }); }); @@ -78,15 +100,16 @@ describe("text panel visible and enabled conditions work correctly", () => { let panels; let controller; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; - const textPanelcategory = wrapper.find("div.properties-category-container").at(2); // TEXT PANEL category - panels = textPanelcategory.find("div.properties-text-panel"); + const { container } = wrapper; + const textPanelcategory = container.querySelectorAll("div.properties-category-container")[2]; // TEXT PANEL category + panels = textPanelcategory.querySelectorAll("div.properties-text-panel"); controller = renderedObject.controller; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("text panel should be disabled", () => { @@ -108,20 +131,22 @@ describe("text panel classNames applied correctly", () => { let wrapper; let panels; beforeEach(() => { - const renderedObject = propertyUtils.flyoutEditorForm(panelConditionsParamDef); + const renderedObject = propertyUtilsRTL.flyoutEditorForm(panelConditionsParamDef); wrapper = renderedObject.wrapper; }); afterEach(() => { - wrapper.unmount(); + cleanup(); }); it("text panel should have custom classname defined", () => { - panels = wrapper.find("div.properties-text-panel"); - expect(panels.find(".orange-panel-group-textpanel-class")).to.have.length(1); + const { container } = wrapper; + panels = container.querySelectorAll("div.properties-text-panel"); + expect(panels[0].className.includes("orange-panel-group-textpanel-class")).to.equal(true); // textPanel within a panelSelector - expect(panels.find(".panel-selector-fields1-red1-group-textpanel-class")).to.have.length(1); + expect(panels[4].className.includes("panel-selector-fields1-red1-group-textpanel-class")).to.equal(true); // deeply nested textpanel group - expect(panels.find(".level3-group-textpanel-class")).to.have.length(1); + expect(panels[9].className.includes("level3-group-textpanel-class")).to.equal(true); }); }); + diff --git a/canvas_modules/common-canvas/package.json b/canvas_modules/common-canvas/package.json index dcda6038f0..f8255d57a5 100644 --- a/canvas_modules/common-canvas/package.json +++ b/canvas_modules/common-canvas/package.json @@ -88,8 +88,9 @@ "grunt-jsonlint": "2.1.3", "grunt-postcss": "0.9.0", "grunt-yamllint": "0.3.0", - "jest": "26.4.2", + "jest": "29.7.0", "jest-fetch-mock": "3.0.3", + "jest-fixed-jsdom": "0.0.9", "jest-localstorage-mock": "2.4.3", "react": "18.2.0", "react-dom": "18.2.0", @@ -113,8 +114,8 @@ "react-intl": "^5.0.0 || ^6.0.0" }, "jest": { + "testEnvironment": "jest-fixed-jsdom", "transformIgnorePatterns": [ - "node_modules/(?!(@codemirror/legacy-modes|d3-*))" ], "moduleFileExtensions": [ "js", diff --git a/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js b/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js index f352eb30e8..8d54d40236 100644 --- a/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js +++ b/canvas_modules/common-canvas/src/common-canvas/canvas-controller.js @@ -144,10 +144,12 @@ export default class CanvasController { setCanvasConfig(config) { this.logger.log("Setting Canvas Config"); if (config) { - // TODO - Remove these next three lines in next major release. + // TODO - Remove these next four lines in next major release. const correctConfig = this.correctTypo(config); correctConfig.enableNodeLayout = CanvasUtils.convertPortPosInfo(correctConfig.enableNodeLayout); + correctConfig.enableNodeLayout = + CanvasUtils.convertPortDisplayInfo(correctConfig.enableNodeLayout); this.objectModel.openPaletteIfNecessary(config); this.objectModel.setCanvasConfig(correctConfig); } diff --git a/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js b/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js index 48fa28b053..30d393d5a8 100644 --- a/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js +++ b/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js @@ -22,8 +22,9 @@ import { get, has, isNumber, set } from "lodash"; import { ASSOCIATION_LINK, ASSOC_STRAIGHT, COMMENT_LINK, NODE_LINK, - LINK_TYPE_STRAIGHT, SUPER_NODE, NORTH, SOUTH, EAST, WEST } - from "../common-canvas/constants/canvas-constants.js"; + LINK_TYPE_STRAIGHT, SUPER_NODE, NORTH, SOUTH, EAST, WEST, + PORT_DISPLAY_IMAGE, PORT_WIDTH_DEFAULT, PORT_HEIGHT_DEFAULT, +} from "../common-canvas/constants/canvas-constants.js"; export default class CanvasUtils { @@ -505,7 +506,13 @@ export default class CanvasUtils { return null; } - // Returns the distance from the start point to finsih point of the link line. + // Returns true if point 1 is inside a circle of the specified radius whose + // center is point 2. + static isInside(point1, point2, radius) { + return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)) < radius; + } + + // Returns the distance from the start point to finish point of the link line. static getLinkDistance(link) { const x = link.x2 - link.x1; const y = link.y2 - link.y1; @@ -855,7 +862,7 @@ export default class CanvasUtils { // Returns true if the portId passed in specifies the first port in the // port array. static isFirstPort(portArray, portId) { - const index = portArray.findIndex((port) => port.id === portId); + const index = this.getPortIndex(portArray, portId); if (index === 0) { return true; @@ -863,6 +870,12 @@ export default class CanvasUtils { return false; } + // Returns the index of the port ID passed in, in the + // port array passed in. + static getPortIndex(portArray, portId) { + return portArray.findIndex((port) => port.id === portId); + } + // Returns a source port Id if one exists in the link, otherwise defaults // to the first available port on the source node. static getSourcePortId(link, srcNode) { @@ -1684,4 +1697,60 @@ export default class CanvasUtils { } return newLayout; } + + // Convert now deprecated layout fields to the port objects arrays. + // TODO - Remove this in a future major release. + static convertPortDisplayInfo(layout) { + const newLayout = layout; + + if (!layout) { + return newLayout; + } + + // If custom fields exist for input object info, write the values into the + // inputPortDisplayObjects array. + if (newLayout.inputPortObject === PORT_DISPLAY_IMAGE) { + newLayout.inputPortDisplayObjects = [ + { type: PORT_DISPLAY_IMAGE, + src: newLayout.inputPortImage, + height: newLayout.inputPortHeight || PORT_HEIGHT_DEFAULT, + width: newLayout.inputPortWidth || PORT_WIDTH_DEFAULT + } + ]; + } + + if (newLayout.inputPortGuideObject === PORT_DISPLAY_IMAGE) { + newLayout.inputPortGuideObjects = [ + { type: PORT_DISPLAY_IMAGE, + src: newLayout.inputPortGuideImage, + height: newLayout.inputPortHeight || PORT_HEIGHT_DEFAULT, + width: newLayout.inputPortWidth || PORT_WIDTH_DEFAULT + } + ]; + } + + // If custom fields exist for output object info, write the values into the + // outputPortDisplayObjects array. + if (newLayout.outputPortObject === PORT_DISPLAY_IMAGE) { + newLayout.outputPortDisplayObjects = [ + { type: PORT_DISPLAY_IMAGE, + src: newLayout.outputPortImage, + height: newLayout.outputPortHeight || PORT_HEIGHT_DEFAULT, + width: newLayout.outputPortWidth || PORT_WIDTH_DEFAULT, + } + ]; + } + + if (newLayout.outputPortGuideObject === PORT_DISPLAY_IMAGE) { + newLayout.outputPortGuideObjects = [ + { type: PORT_DISPLAY_IMAGE, + src: newLayout.outputPortGuideImage, + height: newLayout.outputPortHeight || PORT_HEIGHT_DEFAULT, + width: newLayout.outputPortWidth || PORT_WIDTH_DEFAULT + } + ]; + } + + return newLayout; + } } diff --git a/canvas_modules/common-canvas/src/common-canvas/constants/canvas-constants.js b/canvas_modules/common-canvas/src/common-canvas/constants/canvas-constants.js index 1ef96650d0..13a2de07c9 100644 --- a/canvas_modules/common-canvas/src/common-canvas/constants/canvas-constants.js +++ b/canvas_modules/common-canvas/src/common-canvas/constants/canvas-constants.js @@ -108,8 +108,17 @@ export const SUCCESS = "success"; export const HORIZONTAL = "horizonal"; export const VERTICAL = "vertical"; -export const PORT_OBJECT_IMAGE = "image"; -export const PORT_OBJECT_CIRCLE = "circle"; +export const PORT_DISPLAY_CIRCLE = "circle"; +export const PORT_DISPLAY_CIRCLE_WITH_ARROW = "circleWithArrow"; +export const PORT_DISPLAY_IMAGE = "image"; +export const PORT_DISPLAY_JSX = "jsx"; + +export const FLOW_IN = "in"; +export const FLOW_OUT = "out"; + +export const SINGLE_CLICK = "SINGLE_CLICK"; +export const SINGLE_CLICK_CONTEXTMENU = "SINGLE_CLICK_CONTEXTMENU"; +export const DOUBLE_CLICK = "DOUBLE_CLICK"; // Variations of association links - when enableAssocLinkType === ASSOC_RIGHT_SIDE_CURVE export const ASSOC_VAR_CURVE_RIGHT = "curveRight"; @@ -179,6 +188,11 @@ export const WEST = "w"; export const CAUSE_MOUSE = "M"; export const CAUSE_KEYBOARD = "K"; +// Defaults for port size incase width and height are not provided in +// the inputPortDisplayObjects and outputPortDisplayObjects arrays. +export const PORT_WIDTH_DEFAULT = 12; +export const PORT_HEIGHT_DEFAULT = 12; + // Context Menu button value export const CONTEXT_MENU_BUTTON = 2; diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss index 32ec4e0588..4910f66327 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss @@ -55,7 +55,7 @@ $node-port-output-stroke-color: $background-inverse; $node-port-output-fill-color: $node-body-fill; $node-port-output-connected-stroke-color: $background-inverse; -$node-port-output-connected-fill-color: $background-inverse; +$node-port-output-connected-fill-color: $node-body-fill; $node-port-output-hover-stroke: $background-inverse; $node-port-output-hover-fill: $background-inverse; @@ -65,7 +65,7 @@ $node-port-output-hover-fill: $background-inverse; $node-port-input-stroke-color: $background-inverse; $node-port-input-fill-color: $node-body-fill; -$node-port-input-connected-stroke-color: $node-body-fill; +$node-port-input-connected-stroke-color: $background-inverse; $node-port-input-connected-fill-color: $node-body-fill; $node-port-input-connected-super-binding-stroke-color: $background-inverse; @@ -545,18 +545,47 @@ $link-highlight-color: $support-info; display: none; } -/* Styles for ports */ +/* Styles for output ports */ .d3-node-port-output { stroke: $node-port-output-stroke-color; - fill: $node-port-output-fill-color; + fill: $node-body-fill; stroke-width: 1.25; + & foreignobject { + outline: none; + } +} + +.d3-node-shape-port-arcs { + .d3-node-port-output { + stroke: $background-inverse; + fill: $node-body-fill; + stroke-width: 1; + } + + .d3-node-port-output[connected="yes"] { + stroke: $background-inverse; + fill: $background-inverse; + stroke-width: 1; + } } .d3-node-port-output[connected="yes"] { stroke: $node-port-output-connected-stroke-color; fill: $node-port-output-connected-fill-color; - stroke-width: 2; + stroke-width: 1; + + .d3-node-port-output-arrow { + stroke: $background-inverse; + stroke-width: 1; + fill: transparent; + pointer-events: none; + } +} + +.d3-node-port-output-arrow { + stroke: transparent; + fill: transparent; } .d3-node-port-output:hover { @@ -564,23 +593,23 @@ $link-highlight-color: $support-info; fill: $node-port-output-hover-fill; } -.d3-node-port-input { - stroke: $node-port-input-stroke-color; - fill: $node-port-input-fill-color; - stroke-width: 1.25; -} +/* Styles for input ports */ -.d3-node-port-input-assoc, -.d3-node-port-output-assoc { +.d3-node-port-input { stroke: $node-port-input-stroke-color; fill: $node-port-input-fill-color; stroke-width: 1.25; + & foreignobject { + outline: none; + } } -.d3-node-port-input-assoc:hover, -.d3-node-port-output-assoc:hover { - stroke: $node-port-output-hover-stroke; - fill: $node-port-output-hover-fill; +.d3-node-shape-port-arcs { + .d3-node-port-input[connected="yes"] { + stroke: transparent; + fill: $node-body-fill; + stroke-width: 1; + } } .d3-node-port-input[connected="yes"] { @@ -596,15 +625,30 @@ $link-highlight-color: $support-info; } } +.d3-node-port-input-arrow { + stroke: transparent; + fill: transparent; +} + .d3-node-port-input[connected="yes"][isSupernodeBinding="yes"] { stroke: $node-port-input-connected-super-binding-stroke-color; fill: $node-port-input-connected-super-binding-fill-color; stroke-width: 1; } -.d3-node-port-input-arrow { - stroke: transparent; - fill: transparent; +/* Styles for ports when creating association links */ + +.d3-node-port-input-assoc, +.d3-node-port-output-assoc { + stroke: $node-port-input-stroke-color; + fill: $node-port-input-fill-color; + stroke-width: 1.25; +} + +.d3-node-port-input-assoc:hover, +.d3-node-port-output-assoc:hover { + stroke: $node-port-output-hover-stroke; + fill: $node-port-output-hover-fill; } /* New connection dynamic line styles. */ diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js index b70ac91665..05e364105b 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js @@ -32,19 +32,23 @@ const markdownIt = require("markdown-it")({ import { escape as escapeText, forOwn, get } from "lodash"; import { ASSOC_RIGHT_SIDE_CURVE, ASSOCIATION_LINK, NODE_LINK, COMMENT_LINK, ASSOC_VAR_CURVE_LEFT, ASSOC_VAR_CURVE_RIGHT, ASSOC_VAR_DOUBLE_BACK_RIGHT, - LINK_TYPE_ELBOW, LINK_TYPE_STRAIGHT, + LINK_TYPE_ELBOW, LINK_TYPE_STRAIGHT, LINK_TYPE_CURVE, LINK_DIR_LEFT_RIGHT, LINK_DIR_RIGHT_LEFT, LINK_DIR_TOP_BOTTOM, LINK_DIR_BOTTOM_TOP, LINK_METHOD_FREEFORM, LINK_METHOD_PORTS, LINK_SELECTION_NONE, LINK_SELECTION_HANDLES, LINK_SELECTION_DETACHABLE, CONTEXT_MENU_BUTTON, DEC_LINK, DEC_NODE, EDIT_ICON, - NODE_MENU_ICON, SUPER_NODE_EXPAND_ICON, PORT_OBJECT_IMAGE, + NODE_MENU_ICON, SUPER_NODE_EXPAND_ICON, + PORT_DISPLAY_CIRCLE, PORT_DISPLAY_CIRCLE_WITH_ARROW, PORT_DISPLAY_IMAGE, PORT_DISPLAY_JSX, TIP_TYPE_NODE, TIP_TYPE_PORT, TIP_TYPE_DEC, TIP_TYPE_LINK, USE_DEFAULT_ICON, USE_DEFAULT_EXT_ICON, SUPER_NODE, SNAP_TO_GRID_AFTER, SNAP_TO_GRID_DURING, NORTH, SOUTH, EAST, WEST, WYSIWYG, CAUSE_KEYBOARD, CAUSE_MOUSE, - CANVAS_FOCUS } - from "./constants/canvas-constants"; + FLOW_IN, FLOW_OUT, + PORT_WIDTH_DEFAULT, PORT_HEIGHT_DEFAULT, + SINGLE_CLICK, SINGLE_CLICK_CONTEXTMENU, DOUBLE_CLICK, + CANVAS_FOCUS +} from "./constants/canvas-constants"; import SUPERNODE_ICON from "../../assets/images/supernode.svg"; import SUPERNODE_EXT_ICON from "../../assets/images/supernode_ext.svg"; import Logger from "../logging/canvas-logger.js"; @@ -1344,7 +1348,7 @@ export default class SVGCanvasRenderer { this.logger.log("Canvas - click-zoom"); this.canvasController.clickActionHandler({ - clickType: d3Event.type === "contextmenu" ? "SINGLE_CLICK_CONTEXTMENU" : "SINGLE_CLICK", + clickType: d3Event.type === "contextmenu" ? SINGLE_CLICK_CONTEXTMENU : SINGLE_CLICK, objectType: "canvas", selectedObjectIds: this.activePipeline.getSelectedObjectIds() }); @@ -1352,7 +1356,7 @@ export default class SVGCanvasRenderer { .on("dblclick.zoom", () => { this.logger.log("Zoom - double click"); this.canvasController.clickActionHandler({ - clickType: "DOUBLE_CLICK", + clickType: DOUBLE_CLICK, objectType: "canvas", selectedObjectIds: this.activePipeline.getSelectedObjectIds() }); }) @@ -1572,10 +1576,11 @@ export default class SVGCanvasRenderer { .each((d, i, nodeGrps) => { if (d.isSupernodeInputBinding) { this.updatePortRadiusAndPos(nodeGrps[i], d, "d3-node-port-output-main"); + this.updateOutputPortArrowPath(nodeGrps[i], "d3-node-port-output-arrow"); } if (d.isSupernodeOutputBinding) { this.updatePortRadiusAndPos(nodeGrps[i], d, "d3-node-port-input-main"); - this.updatePortArrowPath(nodeGrps[i], "d3-node-port-input-arrow"); + this.updateInputPortArrowPath(nodeGrps[i], "d3-node-port-input-arrow"); } }); } @@ -1815,6 +1820,13 @@ export default class SVGCanvasRenderer { .each((d, idx, exts) => this.externalUtils.removeExternalObject(d, idx, exts)); + // Remove any JSX ports for nodes being removed to + // unmount their React objects. + removeSel + .selectChildren(".d3-foreign-object-port-jsx") + .each((d, idx, exts) => + this.externalUtils.removeExternalObject(d, idx, exts)); + // Remove any foreign objects for React nodes to // unmount their React objects. removeSel @@ -1872,7 +1884,9 @@ export default class SVGCanvasRenderer { nodeGrp.selectChildren(inSelector) .data(inputs, (p) => p.id) .join( - (enter) => this.createInputPorts(enter, node) + (enter) => this.createInputPorts(enter, node), + null, + (remove) => this.removePorts(remove) ) .attr("connected", (port) => (port.isConnected ? "yes" : "no")) .attr("class", (port) => this.getNodeInputPortClassName() + (port.class_name ? " " + port.class_name : "")) @@ -1884,23 +1898,27 @@ export default class SVGCanvasRenderer { .append("g") .attr("data-port-id", (port) => port.id) .attr("isSupernodeBinding", CanvasUtils.isSuperBindingNode(node) ? "yes" : "no") + .each((port, i, inputPorts) => { + const portIdx = CanvasUtils.getPortIndex(node.inputs, port.id); + const portDisplayInfo = this.getPortDisplayInfo(node.layout.inputPortDisplayObjects, portIdx); + const obj = d3.select(inputPorts[i]); + obj + .append(portDisplayInfo.tag) + .attr("class", "d3-node-port-input-main" + + (portDisplayInfo.tag === "foreignObject" ? " d3-foreign-object-port-jsx" : "")); + + // Show a port arrow inside the port circle if: + // We are not supporting association link creation, + // and we are drawing a circleWithArrow and this is not a super binding node. + obj + .filter(() => (!this.config.enableAssocLinkCreation && + portDisplayInfo.type === PORT_DISPLAY_CIRCLE_WITH_ARROW && + !CanvasUtils.isSuperBindingNode(node))) + .append("path") + .attr("class", "d3-node-port-input-arrow"); + }) .call(this.attachInputPortListeners.bind(this), node); - inputPortGroups - .append(node.layout.inputPortObject) - .attr("class", "d3-node-port-input-main"); - - // Don't show the port arrow when we are supporting association - // link creation - if (!this.config.enableAssocLinkCreation && - node.layout.inputPortObject === "circle" && - !CanvasUtils.isSuperBindingNode(node)) { - // Input port arrow in circle for nodes which are not supernode binding nodes. - inputPortGroups - .append("path") - .attr("class", "d3-node-port-input-arrow"); - } - return inputPortGroups; } @@ -1909,28 +1927,21 @@ export default class SVGCanvasRenderer { .datum((port) => node.inputs.find((i) => port.id === i.id)) .each((port, i, inputPorts) => { const obj = d3.select(inputPorts[i]); - if (node.layout.inputPortObject === PORT_OBJECT_IMAGE) { - obj - .attr("xlink:href", node.layout.inputPortImage) - .attr("x", port.cx - (node.layout.inputPortWidth / 2)) - .attr("y", port.cy - (node.layout.inputPortHeight / 2)) - .attr("width", node.layout.inputPortWidth) - .attr("height", node.layout.inputPortHeight); - } else { - obj - .attr("r", this.getPortRadius(node)) - .attr("cx", port.cx) - .attr("cy", port.cy); - } + const portIdx = CanvasUtils.getPortIndex(node.inputs, port.id); + const portDisplayInfo = this.getPortDisplayInfo(node.layout.inputPortDisplayObjects, portIdx); + const transform = this.getPortImageTransform(port, FLOW_IN); + this.updatePort(obj, portDisplayInfo, node, port.cx, port.cy, transform); }); joinedInputPortGrps.selectChildren(".d3-node-port-input-arrow") .datum((port) => node.inputs.find((i) => port.id === i.id)) .each((port, i, inputPorts) => { const obj = d3.select(inputPorts[i]); - if (node.layout.inputPortObject !== PORT_OBJECT_IMAGE) { + const portIdx = CanvasUtils.getPortIndex(node.inputs, port.id); + const portDisplayInfo = this.getPortDisplayInfo(node.layout.inputPortDisplayObjects, portIdx); + if (portDisplayInfo.type === PORT_DISPLAY_CIRCLE_WITH_ARROW) { obj - .attr("d", this.getPortArrowPath(port)) + .attr("d", this.getPortArrowPath()) .attr("transform", this.getInputPortArrowPathTransform(port)); } }); @@ -1943,6 +1954,18 @@ export default class SVGCanvasRenderer { } } + removePorts(removeSel) { + // Remove any JSX ports for nodes being removed to + // unmount their React objects. + removeSel + .selectChildren(".d3-foreign-object-port-jsx") + .each((d, idx, exts) => + this.externalUtils.removeExternalObject(d, idx, exts)); + + // Remove all ports in the selection. + removeSel.remove(); + } + displayOutputPorts(nodeGrp, node) { const outSelector = "." + this.getNodeOutputPortClassName(); @@ -1962,7 +1985,9 @@ export default class SVGCanvasRenderer { nodeGrp.selectChildren(outSelector) .data(outputs, (p) => p.id) .join( - (enter) => this.createOutputPorts(enter, node) + (enter) => this.createOutputPorts(enter, node), + null, + (remove) => this.removePorts(remove) ) .attr("connected", (port) => (port.isConnected ? "yes" : "no")) .attr("class", (port) => this.getNodeOutputPortClassName() + (port.class_name ? " " + port.class_name : "")) @@ -1974,11 +1999,27 @@ export default class SVGCanvasRenderer { .append("g") .attr("data-port-id", (port) => port.id) .attr("isSupernodeBinding", CanvasUtils.isSuperBindingNode(node) ? "yes" : "no") - .call(this.attachOutputPortListeners.bind(this), node); + .each((port, i, outputPorts) => { + const portIdx = CanvasUtils.getPortIndex(node.outputs, port.id); + const portDisplayInfo = this.getPortDisplayInfo(node.layout.outputPortDisplayObjects, portIdx); + const obj = d3.select(outputPorts[i]); + obj + .append(portDisplayInfo.tag) + .attr("class", "d3-node-port-output-main" + + (portDisplayInfo.tag === "foreignObject" ? " d3-foreign-object-port-jsx" : "")); + + // Show a port arrow inside the port circle if: + // We are not supporting association link creation, + // and we are drawing a circleWithArrow and this is not a super binding node. + obj + .filter(() => (!this.config.enableAssocLinkCreation && + portDisplayInfo.type === PORT_DISPLAY_CIRCLE_WITH_ARROW && + !CanvasUtils.isSuperBindingNode(node))) + .append("path") + .attr("class", "d3-node-port-output-arrow"); - outputPortGroups - .append(node.layout.outputPortObject) - .attr("class", "d3-node-port-output-main"); + }) + .call(this.attachOutputPortListeners.bind(this), node); return outputPortGroups; } @@ -1988,19 +2029,22 @@ export default class SVGCanvasRenderer { .datum((port) => node.outputs.find((o) => port.id === o.id)) .each((port, i, outputPorts) => { const obj = d3.select(outputPorts[i]); - if (node.layout.outputPortObject === PORT_OBJECT_IMAGE) { - obj - .attr("xlink:href", node.layout.outputPortImage) - .attr("x", port.cx - (node.layout.outputPortWidth / 2)) - .attr("y", port.cy - (node.layout.outputPortHeight / 2)) - .attr("width", node.layout.outputPortWidth) - .attr("height", node.layout.outputPortHeight) - .attr("transform", this.getPortImageTransform(port)); - } else { + const portIdx = CanvasUtils.getPortIndex(node.outputs, port.id); + const portInfo = this.getPortDisplayInfo(node.layout.outputPortDisplayObjects, portIdx); + const transform = this.getPortImageTransform(port, FLOW_OUT); + this.updatePort(obj, portInfo, node, port.cx, port.cy, transform); + }); + + joinedOutputPortGrps.selectChildren(".d3-node-port-output-arrow") + .datum((port) => node.outputs.find((i) => port.id === i.id)) + .each((port, i, outputPorts) => { + const obj = d3.select(outputPorts[i]); + const portIdx = CanvasUtils.getPortIndex(node.outputs, port.id); + const portDisplayInfo = this.getPortDisplayInfo(node.layout.outputPortDisplayObjects, portIdx); + if (portDisplayInfo.type === PORT_DISPLAY_CIRCLE_WITH_ARROW) { obj - .attr("r", this.getPortRadius(node)) - .attr("cx", port.cx) - .attr("cy", port.cy); + .attr("d", this.getPortArrowPath()) + .attr("transform", this.getOutputPortArrowPathTransform(port)); } }); @@ -2012,6 +2056,40 @@ export default class SVGCanvasRenderer { } } + updatePort(obj, portInfo, node, cx, cy, transform) { + if (portInfo.type === PORT_DISPLAY_JSX || portInfo.type === PORT_DISPLAY_IMAGE) { + if (portInfo.type === PORT_DISPLAY_JSX) { + obj + .each((portData, idx, exts) => this.externalUtils.addJsxExternalObject(portInfo.src, idx, exts)); + } else { + obj.attr("xlink:href", portInfo.src); + } + obj + .attr("x", cx - (portInfo.width / 2)) + .attr("y", cy - (portInfo.height / 2)) + .attr("width", portInfo.width) + .attr("height", portInfo.height) + .attr("transform", transform); + + } else { + obj + .attr("r", this.getPortRadius(node)) + .attr("cx", cx) + .attr("cy", cy); + } + } + + getPortDisplayInfo(displayObjects, i) { + const idx = (i < displayObjects.length) ? i : displayObjects.length - 1; + const portObj = displayObjects[idx]; + const obj = { ...portObj }; + obj.tag = obj.type === "jsx" ? "foreignObject" : obj.type; + obj.tag = obj.type === "circleWithArrow" ? "circle" : obj.tag; + obj.width = obj.width || PORT_WIDTH_DEFAULT; + obj.width = obj.height || PORT_HEIGHT_DEFAULT; + return obj; + } + // Attaches the appropriate listeners to the node groups. attachNodeGroupListeners(nodeGrps) { nodeGrps @@ -2199,7 +2277,7 @@ export default class SVGCanvasRenderer { this.logger.log("Node Group - double click"); CanvasUtils.stopPropagationAndPreventDefault(d3Event); this.canvasController.clickActionHandler({ - clickType: "DOUBLE_CLICK", + clickType: DOUBLE_CLICK, objectType: "node", id: d.id, selectedObjectIds: this.activePipeline.getSelectedObjectIds(), @@ -2216,7 +2294,9 @@ export default class SVGCanvasRenderer { this.selectObjectD3Event(d3Event, d, "node"); } this.setFocusObject(d, d3Event); - this.openContextMenu(d3Event, "node", d); + if (!this.config.enableContextToolbar) { + this.openContextMenu(d3Event, "node", d); + } } }); } @@ -2502,7 +2582,7 @@ export default class SVGCanvasRenderer { // TODO - Issue 2465 - Find out why this problem occurs. if (objectType === "node" || objectType === "link") { this.canvasController.clickActionHandler({ - clickType: d3EventType === "contextmenu" || this.ellipsisClicked ? "SINGLE_CLICK_CONTEXTMENU" : "SINGLE_CLICK", + clickType: d3EventType === "contextmenu" || this.ellipsisClicked ? SINGLE_CLICK_CONTEXTMENU : SINGLE_CLICK, objectType: objectType, id: d.id, selectedObjectIds: this.activePipeline.getSelectedObjectIds(), @@ -3123,13 +3203,20 @@ export default class SVGCanvasRenderer { .attr("cy", (port) => port.cy); // Port position may change for binding nodes with multiple-ports. } - updatePortArrowPath(nodeObj, portArrowClassName) { + updateInputPortArrowPath(nodeObj, portArrowClassName) { const nodeGrp = d3.select(nodeObj); nodeGrp.selectAll("." + portArrowClassName) - .attr("d", (port) => this.getPortArrowPath(port)) + .attr("d", this.getPortArrowPath()) .attr("transform", (port) => this.getInputPortArrowPathTransform(port)); } + updateOutputPortArrowPath(nodeObj, portArrowClassName) { + const nodeGrp = d3.select(nodeObj); + nodeGrp.selectAll("." + portArrowClassName) + .attr("d", this.getPortArrowPath()) + .attr("transform", (port) => this.getOutputPortArrowPathTransform(port)); + } + // Returns true if the port (from a node template) passed in has a max // cardinaility of zero. If cardinality or cardinality.max is missing the // max is considered to be non-zero. @@ -3233,28 +3320,6 @@ export default class SVGCanvasRenderer { } } - - getLinkImageTransform(d) { - let angle = 0; - if (this.canvasLayout.linkType === LINK_TYPE_STRAIGHT) { - const adjacent = d.x2 - (d.originX || d.x1); - const opposite = d.y2 - (d.originY || d.y1); - if (adjacent === 0 && opposite === 0) { - angle = 0; - } else { - angle = Math.atan(opposite / adjacent) * (180 / Math.PI); - angle = adjacent >= 0 ? angle : angle + 180; - if (this.canvasLayout.linkDirection === LINK_DIR_TOP_BOTTOM) { - angle -= 90; - } else if (this.canvasLayout.linkDirection === LINK_DIR_BOTTOM_TOP) { - angle += 90; - } - } - return `rotate(${angle},${d.x2},${d.y2})`; - } - return null; - } - // Returns a link, if one can be found, at the position indicated by x and y // coordinates. getLinkAtMousePos(x, y) { @@ -3382,13 +3447,8 @@ export default class SVGCanvasRenderer { const prox = nodeProximity || 0; this.getAllNodeGroupsSelection() .each((d) => { - let portRadius = d.layout.portRadius; - if (CanvasUtils.isSuperBindingNode(d)) { - portRadius = this.canvasLayout.supernodeBindingPortRadius / this.zoomUtils.getZoomScale(); - } - - if (pos.x >= d.x_pos - portRadius - prox && // Target port sticks out by its radius so need to allow for it. - pos.x <= d.x_pos + d.width + portRadius + prox && + if (pos.x >= d.x_pos - prox && + pos.x <= d.x_pos + d.width + prox && pos.y >= d.y_pos - prox && pos.y <= d.y_pos + d.height + prox) { node = d; @@ -4132,7 +4192,7 @@ export default class SVGCanvasRenderer { this.displayCommentTextArea(d, d3Event.currentTarget); this.canvasController.clickActionHandler({ - clickType: "DOUBLE_CLICK", + clickType: DOUBLE_CLICK, objectType: "comment", id: d.id, selectedObjectIds: this.activePipeline.getSelectedObjectIds(), @@ -4141,13 +4201,16 @@ export default class SVGCanvasRenderer { }) .on("contextmenu", (d3Event, d) => { this.logger.log("Comment Group - context menu"); + CanvasUtils.stopPropagationAndPreventDefault(d3Event); // With enableDragWithoutSelect set to true, the object for which the // context menu is being requested needs to be implicitely selected. if (this.config.enableDragWithoutSelect) { this.selectObjectD3Event(d3Event, d, "comment"); } this.setFocusObject(d, d3Event); - this.openContextMenu(d3Event, "comment", d); + if (!this.config.enableContextToolbar) { + this.openContextMenu(d3Event, "comment", d); + } }); } @@ -4203,7 +4266,7 @@ export default class SVGCanvasRenderer { const commentGrp = d3.select(commentObj); const commentPort = commentGrp - .append("circle") + .append(PORT_DISPLAY_CIRCLE) .attr("cx", (com) => com.width / 2) .attr("cy", (com) => com.height + this.canvasLayout.commentHighlightGap) .attr("r", this.canvasLayout.commentPortRadius) @@ -4639,11 +4702,14 @@ export default class SVGCanvasRenderer { }) .on("contextmenu", (d3Event, d) => { this.logger.log("Link Group - context menu"); + CanvasUtils.stopPropagationAndPreventDefault(d3Event); if (this.config.enableLinkSelection !== LINK_SELECTION_NONE) { this.selectObjectD3Event(d3Event, d, "link"); } this.setFocusObject(d, d3Event); - this.openContextMenu(d3Event, "link", d); + if (!this.config.enableContextToolbar) { + this.openContextMenu(d3Event, "link", d); + } }); } @@ -4675,7 +4741,7 @@ export default class SVGCanvasRenderer { .datum((d) => this.activePipeline.getLink(d.id)) .each((datum, index, linkHandles) => { const obj = d3.select(linkHandles[index]); - if (this.canvasLayout.linkStartHandleObject === "image") { + if (this.canvasLayout.linkStartHandleObject === PORT_DISPLAY_IMAGE) { obj .attr("xlink:href", this.canvasLayout.linkStartHandleImage) .attr("x", (d) => d.x1 - (this.canvasLayout.linkStartHandleWidth / 2)) @@ -4683,7 +4749,7 @@ export default class SVGCanvasRenderer { .attr("width", this.canvasLayout.linkStartHandleWidth) .attr("height", this.canvasLayout.linkStartHandleHeight); - } else if (this.canvasLayout.linkStartHandleObject === "circle") { + } else if (this.canvasLayout.linkStartHandleObject === PORT_DISPLAY_CIRCLE) { obj .attr("r", this.canvasLayout.linkStartHandleRadius) .attr("cx", (d) => d.x1) @@ -4704,7 +4770,7 @@ export default class SVGCanvasRenderer { .datum((d) => this.activePipeline.getLink(d.id)) .each((datum, index, linkHandles) => { const obj = d3.select(linkHandles[index]); - if (this.canvasLayout.linkEndHandleObject === "image") { + if (this.canvasLayout.linkEndHandleObject === PORT_DISPLAY_IMAGE) { obj .attr("xlink:href", this.canvasLayout.linkEndHandleImage) .attr("x", (d) => d.x2 - (this.canvasLayout.linkEndHandleWidth / 2)) @@ -4713,7 +4779,7 @@ export default class SVGCanvasRenderer { .attr("height", this.canvasLayout.linkEndHandleHeight) .attr("transform", (d) => this.getLinkImageTransform(d)); - } else if (this.canvasLayout.linkEndHandleObject === "circle") { + } else if (this.canvasLayout.linkEndHandleObject === PORT_DISPLAY_CIRCLE) { obj .attr("r", this.canvasLayout.linkEndHandleRadius) .attr("cx", (d) => d.x2) @@ -4863,9 +4929,14 @@ export default class SVGCanvasRenderer { ? " d3-resized" : ""; + const shapeClass = d.layout.nodeShape === "port-arcs" + ? " d3-node-shape-port-arcs" + : ""; + const branchHighlightClass = d.branchHighlight ? " d3-branch-highlight" : ""; - return "d3-node-group" + supernodeClass + resizeClass + draggableClass + branchHighlightClass + customClass; + return "d3-node-group" + supernodeClass + resizeClass + draggableClass + + branchHighlightClass + shapeClass + customClass; } // Pushes the links to be below nodes within the nodesLinksGrp group. @@ -5421,27 +5492,27 @@ export default class SVGCanvasRenderer { // Self-referencing link if (node.id === link.srcObj?.id && link.srcObj?.id === link.trgNode?.id) { - linksInfo[NORTH].push({ type: "in", startNode: link.srcObj, endNode: link.trgNode, link }); - linksInfo[EAST].push({ type: "out", startNode: link.srcObj, endNode: link.trgNode, link }); + linksInfo[NORTH].push({ flow: FLOW_IN, startNode: link.srcObj, endNode: link.trgNode, link }); + linksInfo[EAST].push({ flow: FLOW_OUT, startNode: link.srcObj, endNode: link.trgNode, link }); } else if (link.trgNode && link.trgNode.id === node.id) { if (link.srcObj) { const dir = this.getDirToNode(link.trgNode, link.srcObj); - linksInfo[dir].push({ type: "in", startNode: link.trgNode, endNode: link.srcObj, link }); + linksInfo[dir].push({ flow: FLOW_IN, startNode: link.trgNode, endNode: link.srcObj, link }); } else if (link.srcPos) { const dir = this.getDirToEndPos(link.trgNode, link.srcPos.x_pos, link.srcPos.y_pos); - linksInfo[dir].push({ type: "in", x: link.srcPos.x_pos, y: link.srcPos.y_pos, link }); + linksInfo[dir].push({ flow: FLOW_IN, x: link.srcPos.x_pos, y: link.srcPos.y_pos, link }); } } else if (link.srcObj && link.srcObj.id === node.id) { if (link.trgNode) { const dir = this.getDirToNode(link.srcObj, link.trgNode); - linksInfo[dir].push({ type: "out", startNode: link.srcObj, endNode: link.trgNode, link }); + linksInfo[dir].push({ flow: FLOW_OUT, startNode: link.srcObj, endNode: link.trgNode, link }); } else if (link.trgPos) { const dir = this.getDirToEndPos(link.srcObj, link.trgPos.x_pos, link.trgPos.y_pos); - linksInfo[dir].push({ type: "out", x: link.trgPos.x_pos, y: link.trgPos.y_pos, link }); + linksInfo[dir].push({ flow: FLOW_OUT, x: link.trgPos.x_pos, y: link.trgPos.y_pos, link }); } } } @@ -5595,7 +5666,7 @@ export default class SVGCanvasRenderer { // drawing straight lines to/from nodes to spread out the lines. updateLinksInfo(linksDirArray, dir) { linksDirArray.forEach((li, i) => { - if (li.type === "out") { + if (li.flow === FLOW_OUT) { li.link.srcFreeformInfo = { dir: dir, idx: i, @@ -5614,11 +5685,11 @@ export default class SVGCanvasRenderer { // Returns a variation of association link to draw when a new link is being // drawn outwards from a port. startX is the beginning point of the line // at the port. endX is the position where the mouse is currently positioned. - getNewLinkAssocVariation(startX, endX, portType) { - if (portType === "input" && startX > endX) { + getNewLinkAssocVariation(startX, endX, flow) { + if (flow === FLOW_IN && startX > endX) { return ASSOC_VAR_CURVE_LEFT; - } else if (portType === "output" && startX < endX) { + } else if (flow === FLOW_OUT && startX < endX) { return ASSOC_VAR_CURVE_RIGHT; } return ASSOC_VAR_DOUBLE_BACK_RIGHT; @@ -5642,20 +5713,27 @@ export default class SVGCanvasRenderer { return ASSOC_VAR_DOUBLE_BACK_RIGHT; } - // Returns path for arrow head displayed within an input port circle. It is + // Returns path for arrow head displayed within an port circle. It is // draw so the tip is 2px in front of the origin 0,0 so it appears nicely in // the port circle. - getPortArrowPath(port) { + getPortArrowPath() { return "M -2 3 L 2 0 -2 -3"; } - // Returns the transform to position and, if ncessecary, rotate the port - // circle arrow. + // Returns the transform to position and, if necessary, rotate the port + // circle arrow for input ports. getInputPortArrowPathTransform(port) { const angle = this.getAngleBasedForInputPorts(port.dir); return `translate(${port.cx}, ${port.cy}) rotate(${angle})`; } + // Returns the transform to position and, if necessary, rotate the port + // circle arrow for output ports. + getOutputPortArrowPathTransform(port) { + const angle = this.getAngleBasedForOutputPorts(port.dir); + return `translate(${port.cx}, ${port.cy}) rotate(${angle})`; + } + // Returns an SVG path to draw the arrow head. getArrowHead(d) { if (d.type === COMMENT_LINK) { @@ -5693,10 +5771,12 @@ export default class SVGCanvasRenderer { return `translate(${link.x2}, ${link.y2}) rotate(${angle})`; } - // Returns a rotation transform for an image displayed for an - // output port. - getPortImageTransform(port) { - const angle = this.getAngleBasedForOutputPorts(port.dir); + // Returns a rotation transform for an image displayed for a + // port of the type defined by the flow parameter (in or out). + getPortImageTransform(port, flow) { + const angle = flow === FLOW_OUT + ? this.getAngleBasedForOutputPorts(port.dir) + : this.getAngleBasedForInputPorts(port.dir); return `rotate(${angle},${port.cx},${port.cy})`; } @@ -5713,6 +5793,27 @@ export default class SVGCanvasRenderer { return this.getAngleBasedForInputPorts(d.trgDir); } + getLinkImageTransform(d) { + let angle = 0; + + // For "Freeform" Straight and Curve links, we calculate the angle + // based on the rotation of the link line direction. + if (this.canvasLayout.linkMethod === LINK_METHOD_FREEFORM && + (this.canvasLayout.linkType === LINK_TYPE_STRAIGHT || + this.canvasLayout.linkType === LINK_TYPE_CURVE)) { + angle = CanvasUtils.calculateAngle((d.originX || d.x1), (d.originY || d.y1), d.x2, d.y2); + + // For "Freeform" Elbow and Parallax links AND all links using the + // "Ports" method, we snap the link direction to the target + // direction stored in the link. + } else { + angle = this.getAngleBasedForInputPorts(d.trgDir); + } + + return `rotate(${angle},${d.x2},${d.y2})`; + + } + // Returns the angle for the output port of a source node when // connections to ports are being made. getAngleBasedForOutputPorts(dir) { diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-det-link.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-det-link.js index 8f92b7aee4..0812556d1f 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-det-link.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-det-link.js @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2024 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,11 +24,8 @@ import { cloneDeep } from "lodash"; import Logger from "../logging/canvas-logger.js"; import CanvasUtils from "./common-canvas-utils.js"; -import { LINK_SELECTION_DETACHABLE, PORT_OBJECT_IMAGE } - from "./constants/canvas-constants.js"; +import { LINK_SELECTION_DETACHABLE } from "./constants/canvas-constants.js"; -const INPUT_TYPE = "input_type"; -const OUTPUT_TYPE = "output_type"; // This utility files provides a drag handler which manages drag operations on // the start and end points of detached links. @@ -303,9 +300,7 @@ export default class SVGCanvasUtilsDragDetLink { if (srcNode) { newLink.srcNodeId = srcNode.id; newLink.srcObj = this.ren.activePipeline.getNode(srcNode.id); - newLink.srcNodePortId = nodeProximity - ? this.getNodePortIdNearMousePos(d3Event, OUTPUT_TYPE, srcNode) - : this.ren.getOutputNodePortId(d3Event, srcNode); + newLink.srcNodePortId = this.ren.getOutputNodePortId(d3Event, srcNode); } else { newLink.srcPos = this.draggingLinkData.link.srcPos; } @@ -323,9 +318,7 @@ export default class SVGCanvasUtilsDragDetLink { if (trgNode) { newLink.trgNodeId = trgNode.id; newLink.trgNode = this.ren.activePipeline.getNode(trgNode.id); - newLink.trgNodePortId = nodeProximity - ? this.getNodePortIdNearMousePos(d3Event, INPUT_TYPE, trgNode) - : this.ren.getInputNodePortId(d3Event, trgNode); + newLink.trgNodePortId = this.ren.getInputNodePortId(d3Event, trgNode); } else { newLink.trgPos = this.draggingLinkData.link.trgPos; } @@ -339,107 +332,36 @@ export default class SVGCanvasUtilsDragDetLink { return null; } - if (this.canExecuteUpdateLinkCommand(newLink, oldLink)) { + if (this.canUpdateLink(newLink, oldLink)) { return newLink; } return null; } - // Returns the ID of the port, of the type specified, near to the - // mouse cursor position. - getNodePortIdNearMousePos(d3Event, portType, node) { - const pos = this.ren.getTransformedMousePos(d3Event); - let portId = null; - let defaultPortId = null; - - if (node) { - if (portType === OUTPUT_TYPE) { - const portObjs = this.ren.getAllNodeGroupsSelection() - .selectChildren("." + this.ren.getNodeOutputPortClassName()) - .selectChildren(".d3-node-port-output-main"); - - portId = this.searchForPortNearMouse( - node, pos, portObjs, - node.layout.outputPortObject, - node.width); - defaultPortId = CanvasUtils.getDefaultOutputPortId(node); - - } else { - const portObjs = this.ren.getAllNodeGroupsSelection() - .selectChildren("." + this.ren.getNodeInputPortClassName()) - .selectChildren(".d3-node-port-input-main"); - - portId = this.searchForPortNearMouse( - node, pos, portObjs, - node.layout.inputPortObject, - 0); - defaultPortId = CanvasUtils.getDefaultInputPortId(node); - } - } - - if (!portId) { - portId = defaultPortId; - } - return portId; - } - - // Returns a port ID for the port identified by the position (pos) on the - // node (node) further specified by the other parameters. - searchForPortNearMouse(node, pos, portObjs, portObjectType, nodeWidthOffset) { - let portId = null; - portObjs - .each((p, i, portGrps) => { - const portSel = d3.select(portGrps[i]); - if (portObjectType === PORT_OBJECT_IMAGE) { - const xx = node.x_pos + Number(portSel.attr("x")); - const yy = node.y_pos + Number(portSel.attr("y")); - const wd = Number(portSel.attr("width")); - const ht = Number(portSel.attr("height")); - if (pos.x >= xx && - pos.x <= xx + nodeWidthOffset + wd && - pos.y >= yy && - pos.y <= yy + ht) { - portId = portGrps[i].getAttribute("data-port-id"); - } - } else { // Port must be a circle - const cx = node.x_pos + Number(portSel.attr("cx")); - const cy = node.y_pos + Number(portSel.attr("cy")); - if (pos.x >= cx - node.layout.portRadius && // Target port sticks out by its radius so need to allow for it. - pos.x <= cx + node.layout.portRadius && - pos.y >= cy - node.layout.portRadius && - pos.y <= cy + node.layout.portRadius) { - portId = portGrps[i].getAttribute("data-port-id"); - } - } - }); - - return portId; - } - - // Returns true if the update command for a dragged link can be executed. - // It might be prevented from executing if either the course - canExecuteUpdateLinkCommand(newLink, oldLink) { + // Returns true if the old link passed in can be updated with the attributes + // of the new link. + canUpdateLink(newLink, oldLink) { const srcNode = this.ren.activePipeline.getNode(newLink.srcNodeId); const trgNode = this.ren.activePipeline.getNode(newLink.trgNodeId); const linkSrcChanged = this.hasLinkSrcChanged(newLink, oldLink); const linkTrgChanged = this.hasLinkTrgChanged(newLink, oldLink); const links = this.ren.activePipeline.links; - let executeCommand = true; + let allowed = true; if (linkSrcChanged && srcNode && !CanvasUtils.isSrcConnectionAllowedWithDetachedLinks(newLink.srcNodePortId, srcNode, links)) { - executeCommand = false; + allowed = false; } if (linkTrgChanged && trgNode && !CanvasUtils.isTrgConnectionAllowedWithDetachedLinks(newLink.trgNodePortId, trgNode, links)) { - executeCommand = false; + allowed = false; } if (srcNode && trgNode && !CanvasUtils.isConnectionAllowedWithDetachedLinks(newLink.srcNodePortId, newLink.trgNodePortId, srcNode, trgNode, links, this.ren.config.enableSelfRefLinks)) { - executeCommand = false; + allowed = false; } - return executeCommand; + return allowed; } // Returns true if the source information has changed between diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js index e36881d1b0..fcbde4c97e 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2024 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,11 @@ import Logger from "../logging/canvas-logger.js"; import CanvasUtils from "./common-canvas-utils.js"; import { ASSOCIATION_LINK, COMMENT_LINK, NODE_LINK, LINK_TYPE_CURVE, LINK_TYPE_STRAIGHT, LINK_SELECTION_DETACHABLE, - PORT_OBJECT_CIRCLE, PORT_OBJECT_IMAGE } - from "./constants/canvas-constants.js"; + FLOW_IN, FLOW_OUT, + PORT_DISPLAY_CIRCLE, + LINK_METHOD_PORTS, + SINGLE_CLICK +} from "./constants/canvas-constants.js"; // This utility files provides a drag handler which manages drag operations to // create new links either between nodes or from a comment to a node. @@ -63,14 +66,14 @@ export default class SVGCanvasUtilsDragNewLink { dragStartNewLink(d3Event, d) { if (this.isEventForOutputPort(d3Event)) { const node = this.getNodeForPort(d3Event); - this.startOutputPortNewLink(d, node); + this.startOutputPortNewLink(d3Event, d, node); } else if (this.isEventForInputPort(d3Event)) { const node = this.getNodeForPort(d3Event); - this.startInputPortNewLink(d, node); + this.startInputPortNewLink(d3Event, d, node); } else if (this.ren.activePipeline.getObjectTypeName(d) === "comment") { - this.startCommentNewLink(d); + this.startCommentNewLink(d3Event, d); } } @@ -108,62 +111,58 @@ export default class SVGCanvasUtilsDragNewLink { } // Initialize this.drawingNewLinkData when dragging a comment port. - startCommentNewLink(comment) { + startCommentNewLink(d3Event, comment) { + const srcObj = this.ren.activePipeline.getComment(comment.id); this.drawingNewLinkData = { - srcObjId: comment.id, + srcObj: srcObj, action: COMMENT_LINK, startPos: { x: comment.x_pos - this.ren.canvasLayout.commentHighlightGap, y: comment.y_pos - this.ren.canvasLayout.commentHighlightGap }, + mousePos: { x: d3Event.x, y: d3Event.y }, linkArray: [] }; } // Initialize this.drawingNewLinkData when dragging an input port. This gesture // is only supported for association link creation. - startInputPortNewLink(port, node) { + startInputPortNewLink(d3Event, port, node) { if (this.ren.config.enableAssocLinkCreation) { const srcNode = this.ren.activePipeline.getNode(node.id); + const portIndex = CanvasUtils.getPortIndex(node.inputs, port.id); this.drawingNewLinkData = { - srcObjId: node.id, - srcPortId: port.id, + srcObj: srcNode, + srcPort: port, action: this.ren.config.enableAssocLinkCreation ? ASSOCIATION_LINK : NODE_LINK, - srcNode: srcNode, + mousePos: { x: d3Event.x, y: d3Event.y }, startPos: { x: srcNode.x_pos + port.cx, y: srcNode.y_pos + port.cy }, - portType: "input", - portObject: node.layout.inputPortObject, - portImage: node.layout.inputPortImage, - portWidth: node.layout.inputPortWidth, - portHeight: node.layout.inputPortHeight, + portFlow: FLOW_IN, + portDisplayInfo: this.ren.getPortDisplayInfo(srcNode.layout.inputPortDisplayObjects, portIndex), + portGuideInfo: this.ren.getPortDisplayInfo(srcNode.layout.inputPortGuideObjects, portIndex), portRadius: this.ren.getPortRadius(srcNode), minInitialLine: srcNode.layout.minInitialLine, - guideObject: node.layout.inputPortGuideObject, - guideImage: node.layout.inputPortGuideImage, linkArray: [] }; } } // Initialize this.drawingNewLinkData when dragging an output port. - startOutputPortNewLink(port, node) { + startOutputPortNewLink(d3Event, port, node) { const srcNode = this.ren.activePipeline.getNode(node.id); if (!CanvasUtils.isSrcCardinalityAtMax(port.id, srcNode, this.ren.activePipeline.links)) { + const portIndex = CanvasUtils.getPortIndex(node.outputs, port.id); this.drawingNewLinkData = { - srcObjId: node.id, - srcPortId: port.id, + srcObj: srcNode, + srcPort: port, action: this.ren.config.enableAssocLinkCreation ? ASSOCIATION_LINK : NODE_LINK, - srcNode: srcNode, + mousePos: { x: d3Event.x, y: d3Event.y }, startPos: { x: srcNode.x_pos + port.cx, y: srcNode.y_pos + port.cy }, - portType: "output", - portObject: node.layout.outputPortObject, - portImage: node.layout.outputPortImage, - portWidth: node.layout.outputPortWidth, - portHeight: node.layout.outputPortHeight, + portFlow: FLOW_OUT, + portDisplayInfo: this.ren.getPortDisplayInfo(srcNode.layout.outputPortDisplayObjects, portIndex), + portGuideInfo: this.ren.getPortDisplayInfo(srcNode.layout.outputPortGuideObjects, portIndex), portRadius: this.ren.getPortRadius(srcNode), minInitialLine: srcNode.layout.minInitialLine, - guideObject: node.layout.outputPortGuideObject, - guideImage: node.layout.outputPortGuideImage, linkArray: [] }; if (this.ren.config.enableHighlightUnavailableNodes) { @@ -194,7 +193,7 @@ export default class SVGCanvasUtilsDragNewLink { } drawNewCommentLink(transPos) { - const srcComment = this.ren.activePipeline.getComment(this.drawingNewLinkData.srcObjId); + const srcComment = this.ren.activePipeline.getComment(this.drawingNewLinkData.srcObj.id); const startPos = this.ren.linkUtils.getNewStraightCommentLinkStartPos(srcComment, transPos); const linkType = COMMENT_LINK; @@ -246,29 +245,17 @@ export default class SVGCanvasUtilsDragNewLink { drawNewNodeLink(transPos) { const linkCategory = this.ren.config.enableAssocLinkCreation ? ASSOCIATION_LINK : NODE_LINK; - // Create a temporary link to represent the new link being created. If we are - // creating an association link from an input port we pass in the reverse values. - const inLink = (linkCategory === ASSOCIATION_LINK && this.drawingNewLinkData.portType === "input") - ? { - type: linkCategory, - trgNode: this.drawingNewLinkData.srcNode, - trgNodeId: this.drawingNewLinkData.srcObjId, - trgNodePortId: this.drawingNewLinkData.srcPortId, - srcPos: { - x_pos: transPos.x, - y_pos: transPos.y - } + // Create a temporary link to represent the new link being created. + const inLink = { + type: linkCategory, + srcObj: this.drawingNewLinkData.srcObj, + srcNodeId: this.drawingNewLinkData.srcObj.id, + srcNodePortId: this.drawingNewLinkData.srcPort.id, + trgPos: { + x_pos: transPos.x, + y_pos: transPos.y } - : { - type: linkCategory, - srcObj: this.drawingNewLinkData.srcNode, - srcNodeId: this.drawingNewLinkData.srcObjId, - srcNodePortId: this.drawingNewLinkData.srcPortId, - trgPos: { - x_pos: transPos.x, - y_pos: transPos.y - } - }; + }; const link = this.ren.getDetachedLinkObj(inLink); this.drawingNewLinkData.linkArray = this.ren.linkUtils.addConnectionPaths([link]); @@ -278,7 +265,7 @@ export default class SVGCanvasUtilsDragNewLink { this.ren.getNewLinkAssocVariation( this.drawingNewLinkData.linkArray[0].x1, this.drawingNewLinkData.linkArray[0].x2, - this.drawingNewLinkData.portType); + this.drawingNewLinkData.portFlow); } const pathInfo = this.ren.linkUtils.getConnectorPathInfo( @@ -293,7 +280,7 @@ export default class SVGCanvasUtilsDragNewLink { // to draw straight lines over the node. if (linkCategory === NODE_LINK && this.ren.canvasLayout.linkType === LINK_TYPE_STRAIGHT && - this.ren.nodeUtils.isPointInNodeBoundary(transPos, this.drawingNewLinkData.srcNode)) { + this.ren.nodeUtils.isPointInNodeBoundary(transPos, this.drawingNewLinkData.srcObj)) { this.removeNewLinkLine(); } else { @@ -308,47 +295,44 @@ export default class SVGCanvasUtilsDragNewLink { .attr("transform", pathInfo.transform); } - if (this.ren.canvasLayout.linkType !== LINK_TYPE_STRAIGHT) { + // If we are drawing a link from a port that is displayed as a circle, + // we draw a circle at the start of the link to cover over the actual + // link line that is drawn from the port's center. + if (this.ren.canvasLayout.linkMethod === LINK_METHOD_PORTS && + this.drawingNewLinkData.portDisplayInfo.tag === PORT_DISPLAY_CIRCLE) { connectionStartSel .data(this.drawingNewLinkData.linkArray) .enter() - .append(this.drawingNewLinkData.portObject) + .append("circle") .attr("class", "d3-new-connection-start") .attr("linkType", linkCategory) .merge(connectionStartSel) .each((d, i, startSel) => { - // No need to draw the starting object of the new line if it is an image. - if (this.drawingNewLinkData.portObject === PORT_OBJECT_CIRCLE) { - d3.select(startSel[i]) - .attr("cx", d.x1) - .attr("cy", d.y1) - .attr("r", this.drawingNewLinkData.portRadius); - } + d3.select(startSel[i]) + .attr("cx", d.x1) + .attr("cy", d.y1) + .attr("r", this.drawingNewLinkData.portRadius); }); } + // Draw the guide object (either a circle, an image or JSX) which is + // at the end of the new link being dragged out from the source object. connectionGuideSel .data(this.drawingNewLinkData.linkArray) .enter() - .append(this.drawingNewLinkData.guideObject) + .append(this.drawingNewLinkData.portGuideInfo.tag) .attr("class", "d3-new-connection-guide") .attr("linkType", linkCategory) .merge(connectionGuideSel) .each((d, i, guideSel) => { - if (this.drawingNewLinkData.guideObject === PORT_OBJECT_IMAGE) { - d3.select(guideSel[i]) - .attr("xlink:href", this.drawingNewLinkData.guideImage) - .attr("x", d.x2 - (this.drawingNewLinkData.portWidth / 2)) - .attr("y", d.y2 - (this.drawingNewLinkData.portHeight / 2)) - .attr("width", this.drawingNewLinkData.portWidth) - .attr("height", this.drawingNewLinkData.portHeight) - .attr("transform", this.ren.getLinkImageTransform(d)); - } else { - d3.select(guideSel[i]) - .attr("cx", d.x2) - .attr("cy", d.y2) - .attr("r", this.drawingNewLinkData.portRadius); - } + const obj = d3.select(guideSel[i]); + const transform = this.ren.getLinkImageTransform(d); + this.ren.updatePort(obj, + this.drawingNewLinkData.portGuideInfo, + this.drawingNewLinkData.srcObj, + d.x2, + d.y2, + transform); }); } @@ -368,6 +352,21 @@ export default class SVGCanvasUtilsDragNewLink { const drawingNewLinkData = this.drawingNewLinkData; this.drawingNewLinkData = null; + // If the user has not dragged the mouse far enough to create a new link, we + // treat it as a click on the port. + if (this.isClicked(drawingNewLinkData.mousePos, d3Event)) { + this.removeNewLink(); + this.ren.canvasController.clickActionHandler({ + clickType: SINGLE_CLICK, + objectType: "port", + id: drawingNewLinkData.srcPort.id, + nodeId: drawingNewLinkData.srcObj.id, + selectedObjectIds: this.ren.activePipeline.getSelectedObjectIds(), + pipelineId: this.ren.activePipeline.id + }); + return; + } + if (this.ren.config.enableHighlightUnavailableNodes) { this.ren.unsetUnavailableNodesHighlighting(); } @@ -387,6 +386,12 @@ export default class SVGCanvasUtilsDragNewLink { } } + // Returns true if the mouse position is inside a circle with a radius of + // 3px centred at the d3Event x and y. + isClicked(mousePos, d3Event) { + return CanvasUtils.isInside(mousePos, { x: d3Event.x, y: d3Event.y }, 3); + } + // Handles the creation of a link when the end of a new link // being drawn from a source node is dropped on a target node. createNewLinkFromDragData(d3Event, trgNode, drawingNewLinkData) { @@ -400,21 +405,20 @@ export default class SVGCanvasUtilsDragNewLink { // Create the link. const type = drawingNewLinkData.action; - const srcObjId = drawingNewLinkData.srcObjId; if (trgNode !== null) { if (type === NODE_LINK) { - const srcNode = this.ren.activePipeline.getNode(srcObjId); - const srcPortId = drawingNewLinkData.srcPortId; + const srcNode = drawingNewLinkData.srcObj; + const srcPortId = drawingNewLinkData.srcPort.id; const trgPortId = this.ren.getInputNodePortId(d3Event, trgNode); this.createNewNodeLink(srcNode, srcPortId, trgNode, trgPortId); } else if (type === ASSOCIATION_LINK) { - const srcObj = this.ren.activePipeline.getNode(srcObjId); + const srcObj = drawingNewLinkData.srcObj; this.createNewAssocLink(srcObj, trgNode); } else if (type === COMMENT_LINK) { - const srcObj = this.ren.activePipeline.getComment(srcObjId); + const srcObj = drawingNewLinkData.srcObj; this.createNewCommentLink(srcObj, trgNode); } } @@ -521,8 +525,8 @@ export default class SVGCanvasUtilsDragNewLink { this.ren.canvasController.editActionHandler({ editType: "createDetachedLink", editSource: "canvas", - srcNodeId: drawingNewLinkData.srcObjId, - srcNodePortId: drawingNewLinkData.srcPortId, + srcNodeId: drawingNewLinkData.srcObj.id, + srcNodePortId: drawingNewLinkData.srcPort.id, trgPos: endPoint, type: NODE_LINK, pipelineId: this.ren.activePipeline.id }); @@ -543,21 +547,10 @@ export default class SVGCanvasUtilsDragNewLink { if (drawingNewLinkData.linkArray?.length === 0) { return; } - let saveX1 = drawingNewLinkData.linkArray[0].x1; - let saveY1 = drawingNewLinkData.linkArray[0].y1; - let saveX2 = drawingNewLinkData.linkArray[0].x2; - let saveY2 = drawingNewLinkData.linkArray[0].y2; - - // If we were creating an association link from an input port of - // the node, we reverse the way the snap-back link is drawn by - // switching the coordinates. - if (drawingNewLinkData.action === ASSOCIATION_LINK && - drawingNewLinkData.portType === "input") { - saveX1 = drawingNewLinkData.linkArray[0].x2; - saveY1 = drawingNewLinkData.linkArray[0].y2; - saveX2 = drawingNewLinkData.linkArray[0].x1; - saveY2 = drawingNewLinkData.linkArray[0].y1; - } + const saveX1 = drawingNewLinkData.linkArray[0].x1; + const saveY1 = drawingNewLinkData.linkArray[0].y1; + const saveX2 = drawingNewLinkData.linkArray[0].x2; + const saveY2 = drawingNewLinkData.linkArray[0].y2; const saveNewLinkData = Object.assign({}, drawingNewLinkData); @@ -609,8 +602,8 @@ export default class SVGCanvasUtilsDragNewLink { // though some attributes will not be relevant. This is done // because I could not get the .each() method to work here (which // would be necessary to have an if statement based on guide object) - .attr("x", saveX1 - (saveNewLinkData.portWidth / 2)) - .attr("y", saveY1 - (saveNewLinkData.portHeight / 2)) + .attr("x", saveX1 - (saveNewLinkData.portGuideInfo?.width / 2)) + .attr("y", saveY1 - (saveNewLinkData.portGuideInfo?.height / 2)) .attr("cx", saveX1) .attr("cy", saveY1) .attr("transform", null); @@ -625,45 +618,51 @@ export default class SVGCanvasUtilsDragNewLink { } removeNewLink() { + // If a guide object is drawn using JSX in a foreignObject, we need to + // make sure it's internal React object is removed successfully. + this.ren.nodesLinksGrp.selectAll("foreignObject.d3-new-connection-guide") + .each((x, i, foreignObjects) => + this.ren.externalUtils.removeExternalObject(x, i, foreignObjects)); + + // Remove all the constituent parts of the new link. this.ren.nodesLinksGrp.selectAll(".d3-new-connection-line").remove(); this.ren.nodesLinksGrp.selectAll(".d3-new-connection-start").remove(); this.ren.nodesLinksGrp.selectAll(".d3-new-connection-guide").remove(); this.ren.nodesLinksGrp.selectAll(".d3-new-connection-arrow").remove(); + } // Switches on or off node highlighting depending on whether a node is // close to the new link being dragged. setNewLinkOverNode(d3Event) { const nodeNearMouse = this.ren.getNodeNearMousePos(d3Event, this.ren.canvasLayout.nodeProximity); - const highlightState = nodeNearMouse && this.isNewLinkAllowedToNode(nodeNearMouse); + const highlightState = nodeNearMouse && this.isNewLinkAllowedToNode(d3Event, nodeNearMouse); this.ren.setHighlightingOverNode(highlightState, nodeNearMouse); } // Returns true if a connection is allowed to the node passed in based on the // this.drawingNewLinkData object which describes a new link being dragged. - isNewLinkAllowedToNode(node) { + isNewLinkAllowedToNode(d3Event, node) { if (this.drawingNewLinkData) { if (this.drawingNewLinkData.action === NODE_LINK) { - const srcNode = this.drawingNewLinkData.srcNode; + const srcNode = this.drawingNewLinkData.srcObj; const trgNode = node; - const srcNodePortId = this.drawingNewLinkData.srcPortId; - const trgNodePortId = CanvasUtils.getDefaultInputPortId(trgNode); // TODO - make specific to nodes. + const srcNodePortId = this.drawingNewLinkData.srcPort.id; + const trgNodePortId = this.ren.getInputNodePortId(d3Event, trgNode); return CanvasUtils.isDataConnectionAllowed(srcNodePortId, trgNodePortId, srcNode, trgNode, this.ren.activePipeline.links, this.ren.config.enableSelfRefLinks); } else if (this.drawingNewLinkData.action === ASSOCIATION_LINK) { - const srcNode = this.drawingNewLinkData.srcNode; + const srcNode = this.drawingNewLinkData.srcObj; const trgNode = node; return CanvasUtils.isAssocConnectionAllowed(srcNode, trgNode, this.ren.activePipeline.links); } else if (this.drawingNewLinkData.action === COMMENT_LINK) { - const srcObjId = this.drawingNewLinkData.srcObjId; + const srcObjId = this.drawingNewLinkData.srcObj.id; const trgNodeId = node.id; return CanvasUtils.isCommentLinkConnectionAllowed(srcObjId, trgNodeId, this.ren.activePipeline.links); } } return false; } - - } diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js index fce2b964c2..14a82a7b8e 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js @@ -53,6 +53,10 @@ export default class SvgCanvasExternal { this.renderExternalObject(dec.jsx, foreignObjects[i]); } + addJsxExternalObject(jsx, i, foreignObjects) { + this.renderExternalObject(jsx, foreignObjects[i]); + } + renderExternalObject(jsx, container) { // createRoot only available in React v18 if (this.isReact18OrHigher()) { diff --git a/canvas_modules/common-canvas/src/object-model/layout-dimensions.js b/canvas_modules/common-canvas/src/object-model/layout-dimensions.js index 84085de7b5..0df20988ee 100644 --- a/canvas_modules/common-canvas/src/object-model/layout-dimensions.js +++ b/canvas_modules/common-canvas/src/object-model/layout-dimensions.js @@ -26,7 +26,7 @@ import { const portsHorizontalDefaultLayout = { nodeLayout: { // Default node sizes. These dimensions might be overridden for nodes that have - // more ports than will fit in the default size if inputPortAutoPosition is. + // more ports than will fit in the default size if inputPortAutoPosition is // set to true and outputPortAutoPosition is set to true. (See below). defaultNodeWidth: 160, defaultNodeHeight: 40, @@ -38,7 +38,7 @@ const portsHorizontalDefaultLayout = { // Displays the node outline shape underneath the image and label. nodeShapeDisplay: true, - // Default node shape. Can be "rectangle" or "port-arcs". Used when nodeOutlineDisplay is true. + // Default node shape. Can be "rectangle" or "port-arcs". Used when nodeShapeDisplay is true. nodeShape: "port-arcs", // An SVG path or a function that returns an SVG path. The paths define the node @@ -157,15 +157,21 @@ const portsHorizontalDefaultLayout = { // Display input ports. inputPortDisplay: true, - // Object for input port can be "circle" or "image". - inputPortObject: "circle", - - // If input port object is "image" use this image. - inputPortImage: "", - - // If input port dimensions for "image". - inputPortWidth: 12, - inputPortHeight: 12, + // An array of elements to control display of input ports. Each element + // can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. + inputPortDisplayObjects: [ + { type: "circleWithArrow" } + ], // Indicates whether multiple input ports should be automatically // positioned (true) or positioned based on the contents of @@ -176,31 +182,47 @@ const portsHorizontalDefaultLayout = { // this: { x_pos: 5, y_pos: 10, pos: "topLeft" }. x_pos and y_pos are // offsets from the pos point on the node. // The order of the elements corresponds to the order of ports in the - // inputs array for the node. + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. inputPortPositions: [ { x_pos: 0, y_pos: 20, pos: "topLeft" } ], - // The 'guide' is the object drawn at the mouse position as a new line - // is being dragged outwards. - // Object for input port guide can be "circle" or "image". - inputPortGuideObject: "circle", - - // If input port guide object is "image" use this image. - inputPortGuideImage: "", + // An array of elements to control display of input port guide objects. + // That is the object drawn at the end of a new link as it is being dragged. + // Each element can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. + inputPortGuideObjects: [ + { type: "circle" } + ], // Display output ports. outputPortDisplay: true, - // Object for output port can be "circle" or "image". - outputPortObject: "circle", - - // If output port object is "image" use this image. - outputPortImage: "", - - // Output port dimensions for "image". - outputPortWidth: 12, - outputPortHeight: 12, + // An array of elements to control display of output ports. Each element + // can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. + outputPortDisplayObjects: [ + { type: "circle" } + ], // Indicates whether multiple output ports should be automatically // positioned (true) or positioned based on the contents of @@ -211,18 +233,28 @@ const portsHorizontalDefaultLayout = { // this: { x_pos: 5, y_pos: 10, pos: "topRight" }. x_pos and y_pos are // offsets from the pos point on the node. // The order of the elements corresponds to the order of ports in the - // outputs array for the node. + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. outputPortPositions: [ { x_pos: 0, y_pos: 20, pos: "topRight" } ], - // The 'guide' is the object drawn at the mouse position as a new line - // is being dragged outwards. - // Object for output port guide can be "circle" or "image". - outputPortGuideObject: "circle", - - // If output port guide object is "image" use this image. - outputPortGuideImage: "", + // An array of elements to control display of output port guide objects. + // That is the object drawn at the end of a new link as it is being dragged. + // Each element can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. + outputPortGuideObjects: [ + { type: "circle" } + ], // Automatically increases the node size to accommodate its ports so both // input and output ports can be shown within the dimensions of @@ -243,7 +275,7 @@ const portsHorizontalDefaultLayout = { // Spacing between the port arcs around the ports. portArcSpacing: 3, - // Position of the context toolbar realtive to the node. Some adjustment + // Position of the context toolbar relative to the node. Some adjustment // will be made to account for the width of the toolbar. contextToolbarPosition: "topRight", @@ -452,7 +484,7 @@ const portsHorizontalDefaultLayout = { const portsVerticalDefaultLayout = { nodeLayout: { // Default node sizes. These dimensions might be overridden for nodes that have - // more ports than will fit in the default size if inputPortAutoPosition is. + // more ports than will fit in the default size if inputPortAutoPosition is // set to true and outputPortAutoPosition is set to true. (See below). defaultNodeWidth: 70, defaultNodeHeight: 75, @@ -464,7 +496,7 @@ const portsVerticalDefaultLayout = { // Displays the node outline shape underneath the image and label. nodeShapeDisplay: true, - // Default node shape. Can be "rectangle" or "port-arcs". Used when nodeOutlineDisplay is true. + // Default node shape. Can be "rectangle" or "port-arcs". Used when nodeShapeDisplay is true. nodeShape: "rectangle", // An SVG path or a function that returns an SVG path. The paths define the node @@ -583,15 +615,21 @@ const portsVerticalDefaultLayout = { // Display input ports. inputPortDisplay: true, - // Object for input port can be "circle" or "image". - inputPortObject: "circle", - - // If input port object is "image" use this image. - inputPortImage: "", - - // If input port dimensions for "image". - inputPortWidth: 12, - inputPortHeight: 12, + // An array of elements to control display of input ports. Each element + // can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. + inputPortDisplayObjects: [ + { type: "circleWithArrow" } + ], // Indicates whether multiple input ports should be automatically // positioned (true) or positioned based on the contents of @@ -602,31 +640,47 @@ const portsVerticalDefaultLayout = { // this: { x_pos: 5, y_pos: 10, pos: "topLeft" }. x_pos and y_pos are // offsets from the pos point on the node. // The order of the elements corresponds to the order of ports in the - // inputs array for the node. + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. inputPortPositions: [ { x_pos: 0, y_pos: 29, pos: "topLeft" } ], - // The 'guide' is the object drawn at the mouse position as a new line - // is being dragged outwards. - // Object for input port guide can be "circle" or "image". - inputPortGuideObject: "circle", - - // If input port guide object is "image" use this image. - inputPortGuideImage: "", + // An array of elements to control display of input port guide objects. + // That is the object drawn at the end of a new link as it is being dragged. + // Each element can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. + inputPortGuideObjects: [ + { type: "circle" } + ], // Display output ports. outputPortDisplay: true, - // Object for output port can be "circle" or "image". - outputPortObject: "circle", - - // If output port object is "image" use this image. - outputPortImage: "", - - // Output port dimensions for "image". - outputPortWidth: 12, - outputPortHeight: 12, + // An array of elements to control display of output ports. Each element + // can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. + outputPortDisplayObjects: [ + { type: "circle" } + ], // Indicates whether multiple output ports should be automatically // positioned (true) or positioned based on the contents of @@ -637,18 +691,28 @@ const portsVerticalDefaultLayout = { // this: { x_pos: 5, y_pos: 10, pos: "topRight" }. x_pos and y_pos are // offsets from the pos point on the node. // The order of the elements corresponds to the order of ports in the - // outputs array for the node. + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. outputPortPositions: [ { x_pos: 0, y_pos: 29, pos: "topRight" } ], - // The 'guide' is the object drawn at the mouse position as a new line - // is being dragged outwards. - // Object for output port guide can be "circle" or "image". - outputPortGuideObject: "circle", - - // If output port guide object is "image" use this image. - outputPortGuideImage: "", + // An array of elements to control display of output port guide objects. + // That is the object drawn at the end of a new link as it is being dragged. + // Each element can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. + outputPortGuideObjects: [ + { type: "circle" } + ], // Automatically increases the node size to accommodate its ports so both // input and output ports can be shown within the dimensions of @@ -669,7 +733,7 @@ const portsVerticalDefaultLayout = { // Spacing between the port arcs around the ports. portArcSpacing: 0, - // Position of the context toolbar realtive to the node. Some adjustment + // Position of the context toolbar relative to the node. Some adjustment // will be made to account for the width of the toolbar. contextToolbarPosition: "topCenter", diff --git a/canvas_modules/common-canvas/src/object-model/object-model-utils.js b/canvas_modules/common-canvas/src/object-model/object-model-utils.js index 461e9118b8..42cd042609 100644 --- a/canvas_modules/common-canvas/src/object-model/object-model-utils.js +++ b/canvas_modules/common-canvas/src/object-model/object-model-utils.js @@ -144,6 +144,11 @@ function setNodeLayoutAttributes(node, nodeLayout, layoutHandler) { // to the new port positions arrays for input and output ports. customLayout = CanvasUtils.convertPortPosInfo(customLayout); + // TODO - This should be removed in a future major release. + // This method converts now deprecated port object variables from customLayout + // to the new port object info arrays for input and output ports. + customLayout = CanvasUtils.convertPortDisplayInfo(customLayout); + const decs = CanvasUtils.getCombinedDecorations(node.layout.decorations, customLayout.decorations); node.layout = Object.assign({}, node.layout, customLayout, { decorations: decs }); } diff --git a/canvas_modules/common-canvas/src/palette/palette-flyout-content.jsx b/canvas_modules/common-canvas/src/palette/palette-flyout-content.jsx index c7c7be4b7a..d461cb541d 100644 --- a/canvas_modules/common-canvas/src/palette/palette-flyout-content.jsx +++ b/canvas_modules/common-canvas/src/palette/palette-flyout-content.jsx @@ -145,8 +145,11 @@ class PaletteFlyoutContent extends React.Component { ? this.props.paletteHeader : null; + let className = "palette-flyout-content"; + className += paletteHeader ? " with-palette-header" : ""; + return ( -
+
{contentSearch} {paletteHeader} {contentCategories} diff --git a/canvas_modules/common-canvas/src/palette/palette.scss b/canvas_modules/common-canvas/src/palette/palette.scss index 185c67cd3a..9d55bac2f1 100644 --- a/canvas_modules/common-canvas/src/palette/palette.scss +++ b/canvas_modules/common-canvas/src/palette/palette.scss @@ -57,11 +57,17 @@ $palette-dialog-list-item-height: 46px; height: 100%; } +// Overrides the grid-template-rows for when a palette header object is +// included. (This fixes a problem specifically on Safari.) +.palette-flyout-content.with-palette-header { + grid-template-rows: $palette-search-container-height auto 1fr; +} + .palette-flyout-content { position: absolute; // Needed to allow the scroll of categories/nodes to work. height: 100%; display: grid; - grid-template-rows: $palette-search-container-height auto 1fr; + grid-template-rows: $palette-search-container-height 1fr; // grid-template-columns is set based on narrow or open palette .palette-scroll { diff --git a/canvas_modules/harness/assets/images/custom-canvases/logic/decorations/dragStateArrow.svg b/canvas_modules/harness/assets/images/custom-canvases/logic/decorations/dragStateArrow.svg new file mode 100644 index 0000000000..ddda03fd55 --- /dev/null +++ b/canvas_modules/harness/assets/images/custom-canvases/logic/decorations/dragStateArrow.svg @@ -0,0 +1 @@ + diff --git a/canvas_modules/harness/assets/images/custom-canvases/logic/decorations/dragStateArrowDown.svg b/canvas_modules/harness/assets/images/custom-canvases/logic/decorations/dragStateArrowDown.svg deleted file mode 100644 index 2205e0ea20..0000000000 --- a/canvas_modules/harness/assets/images/custom-canvases/logic/decorations/dragStateArrowDown.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/canvas_modules/harness/assets/images/custom-canvases/prompt/number_1.svg b/canvas_modules/harness/assets/images/custom-canvases/prompt/number_1.svg new file mode 100644 index 0000000000..3d9ac2a1ce --- /dev/null +++ b/canvas_modules/harness/assets/images/custom-canvases/prompt/number_1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/canvas_modules/harness/assets/images/custom-canvases/prompt/number_2.svg b/canvas_modules/harness/assets/images/custom-canvases/prompt/number_2.svg new file mode 100644 index 0000000000..011ad25e8c --- /dev/null +++ b/canvas_modules/harness/assets/images/custom-canvases/prompt/number_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/canvas_modules/harness/assets/images/custom-canvases/prompt/number_3.svg b/canvas_modules/harness/assets/images/custom-canvases/prompt/number_3.svg new file mode 100644 index 0000000000..296a79441a --- /dev/null +++ b/canvas_modules/harness/assets/images/custom-canvases/prompt/number_3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/canvas_modules/harness/src/client/components/custom-canvases/logic/logic-canvas.jsx b/canvas_modules/harness/src/client/components/custom-canvases/logic/logic-canvas.jsx index ba93d3ee77..8819040463 100644 --- a/canvas_modules/harness/src/client/components/custom-canvases/logic/logic-canvas.jsx +++ b/canvas_modules/harness/src/client/components/custom-canvases/logic/logic-canvas.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2024 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,22 +84,17 @@ export default class LogicCanvas extends React.Component { labelSingleLine: true, labelOutline: false, - inputPortTopPosX: 140, - inputPortTopPosY: -20, - inputPortWidth: 20, - inputPortHeight: 20, - inputPortObject: "image", - inputPortImage: "/images/custom-canvases/logic/decorations/dragStateArrowDown.svg", + inputPortDisplay: false, outputPortBottomPosX: 140, outputPortBottomPosY: 20, outputPortWidth: 20, outputPortHeight: 20, outputPortObject: "image", - outputPortImage: "/images/custom-canvases/logic/decorations/dragStateArrowDown.svg", + outputPortImage: "/images/custom-canvases/logic/decorations/dragStateArrow.svg", outputPortGuideObject: "image", - outputPortGuideImage: "/images/custom-canvases/logic/decorations/dragStateArrowDown.svg", + outputPortGuideImage: "/images/custom-canvases/logic/decorations/dragStateArrow.svg", outputPortGuideImageRotate: true }, enableCanvasLayout: { diff --git a/canvas_modules/harness/src/client/components/custom-canvases/network/network-canvas.jsx b/canvas_modules/harness/src/client/components/custom-canvases/network/network-canvas.jsx index f9a2c56dbc..56b481ddae 100644 --- a/canvas_modules/harness/src/client/components/custom-canvases/network/network-canvas.jsx +++ b/canvas_modules/harness/src/client/components/custom-canvases/network/network-canvas.jsx @@ -62,8 +62,7 @@ export default class NetworkCanvas extends React.Component { outputPortDisplay: true, outputPortGuideObject: "image", - outputPortGuideImage: "/images/custom-canvases/flows/decorations/dragStateArrow.svg", - outputPortGuideImageRotate: true + outputPortGuideImage: "/images/custom-canvases/flows/decorations/dragStateArrow.svg" }, enableCanvasLayout: { dataLinkArrowHead: "M 0 0 L -5 -2 -5 2 Z" diff --git a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-canvas.jsx b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-canvas.jsx index c5fa8440d5..5c712b2d89 100644 --- a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-canvas.jsx +++ b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-canvas.jsx @@ -42,8 +42,8 @@ export default class PromptCanvas extends React.Component { const config = Object.assign({}, this.props.config, { enableParentClass: "prompt", enableNodeFormatType: "Vertical", - enableLinkType: "Straight", - enableLinkMethod: "Freeform", + enableLinkType: "Curve", + enableLinkMethod: "Ports", enableLinkDirection: "LeftRight", enableSnapToGridType: "After", enableLinkSelection: "None", @@ -60,29 +60,33 @@ export default class PromptCanvas extends React.Component { drawCommentLinkLineTo: "image_center", defaultNodeWidth: 72, defaultNodeHeight: 72, - selectionPath: "M 8 0 L 64 0 64 56 8 56 8 0", imageWidth: 48, imageHeight: 48, imagePosX: 12, imagePosY: 4, - labelEditable: true, labelPosX: 36, - labelPosY: 56, + labelPosY: 54, labelWidth: 120, labelHeight: 18, - portRadius: 10, - inputPortDisplay: false, - outputPortRightPosX: 5, - outputPortRightPosY: 30, - outputPortObject: "image", - outputPortImage: "/images/custom-canvases/flows/decorations/dragStateArrow.svg", - outputPortWidth: 20, - outputPortHeight: 20, - outputPortGuideObject: "image", - outputPortGuideImage: "/images/custom-canvases/flows/decorations/dragStateArrow.svg" + + inputPortDisplayObjects: [ + { type: "circleWithArrow" } + ], + + outputPortDisplayObjects: [ + { type: "image", src: "/images/custom-canvases/prompt/number_1.svg", width: 16, height: 16 }, + { type: "image", src: "/images/custom-canvases/prompt/number_2.svg", width: 16, height: 16 }, + { type: "image", src: "/images/custom-canvases/prompt/number_3.svg", width: 16, height: 16 } + ], + outputPortGuideObjects: [ + { type: "image", src: "/images/custom-canvases/prompt/number_1.svg", width: 16, height: 16 }, + { type: "image", src: "/images/custom-canvases/prompt/number_2.svg", width: 16, height: 16 }, + { type: "image", src: "/images/custom-canvases/prompt/number_3.svg", width: 16, height: 16 } + ], }, enableCanvasLayout: { - dataLinkArrowHead: true, + // dataLinkArrowHead: "M -15 0 l 0 -5 10 5 -10 5 Z", + dataLinkArrowHead: false, linkGap: 4, displayLinkOnOverlap: false } @@ -91,7 +95,10 @@ export default class PromptCanvas extends React.Component { } clickActionHandler(source) { - // this.addPromptNode(); + if (source.objectType === "port" && + source.clickType === "SINGLE_CLICK") { + this.addPromptNode(source.nodeId, source.id); + } } layoutHandler(node) { @@ -111,7 +118,7 @@ export default class PromptCanvas extends React.Component { editActionHandler(data) { if (data.editType === "app_addPropmpt") { - this.addPromptNode(data.targetObject); + this.addPromptNode(data.targetObject.id); } } @@ -131,10 +138,10 @@ export default class PromptCanvas extends React.Component { return defaultMenu; } - addNodeHandler(nodeTemplate) { - const promptNode = this.canvasController.getNode(this.promptNodeId); - this.canvasController.deleteNode(this.promptNodeId); - this.canvasController.deleteLink("link_to_prompt"); + addNodeHandler(srcNodeId, srcPortId, nodeTemplate, promptNodeId) { + const promptNode = this.canvasController.getNode(promptNodeId); + this.canvasController.deleteNode(promptNodeId); + this.canvasController.deleteLink(this.genPromptLinkId(srcNodeId, srcPortId)); const newNode = this.canvasController.createNode({ nodeTemplate: nodeTemplate, @@ -145,51 +152,56 @@ export default class PromptCanvas extends React.Component { const linksToAdd = this.canvasController.createNodeLinks({ type: "nodeLink", - nodes: [{ id: this.sourceNodeId }], + nodes: [{ id: srcNodeId, portId: srcPortId }], targetNodes: [{ id: newNode.id }] }); this.canvasController.addLinks(linksToAdd); - } - addPromptNode(sourceNode) { - this.sourceNodeId = sourceNode.id; + addPromptNode(srcNodeId, srcPortId) { + const srcNode = this.canvasController.getNode(srcNodeId); const template = Template; template.app_data.prompt_data = { - addNodeCallback: this.addNodeHandler.bind(this) + addNodeCallback: this.addNodeHandler.bind(this, srcNodeId, srcPortId) }; - const newNode = this.canvasController.createNode({ + const promptNode = this.canvasController.createNode({ nodeTemplate: template, - offsetX: sourceNode.x_pos + 200, // Position prompt 200px to right of source node - offsetY: sourceNode.y_pos + offsetX: srcNode.x_pos + 200, // Position prompt 200px to right of source node + offsetY: srcNode.y_pos }); // Make sure prompt doesn't overlap other nodes. - this.adjustNodePosition(newNode, 100); - - // Save the ID of the prompt node for removal, later - this.promptNodeId = newNode.id; + this.adjustNodePosition(promptNode); // Add the prompt node to the canvas with a link - this.canvasController.addNode(newNode); + this.canvasController.addNode(promptNode); const linksToAdd = this.canvasController.createNodeLinks({ - id: "link_to_prompt", + id: this.genPromptLinkId(srcNodeId, srcPortId), type: "nodeLink", - nodes: [{ id: sourceNode.id }], - targetNodes: [{ id: this.promptNodeId }] + nodes: [{ id: srcNodeId, portId: srcPortId }], + targetNodes: [{ id: promptNode.id }] }); this.canvasController.addLinks(linksToAdd); } - adjustNodePosition(node, yInc) { + genPromptLinkId(srcNodeId, srcPortId) { + return "link_to_prompt_" + srcNodeId + "_" + srcPortId; + } + + adjustNodePosition(node) { let overlapNode = true; while (overlapNode) { - overlapNode = this.canvasController.getNodes().find((n) => n.x_pos === node.x_pos && n.y_pos === node.y_pos); + overlapNode = this.canvasController.getNodes().find((n) => + node.x_pos >= n.x_pos && + node.x_pos <= n.x_pos + n.height && + node.y_pos >= n.y_pos && + node.y_pos <= n.y_pos + n.width + ); if (overlapNode) { - node.y_pos += yInc; + node.y_pos += overlapNode.height + 20; } } } diff --git a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-flow.json b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-flow.json index 04e06b428e..6e62500cca 100644 --- a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-flow.json +++ b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-flow.json @@ -17,7 +17,7 @@ "label": "Type", "image": "images/custom-canvases/flows/palette/icons/type.svg", "x_pos": 54, - "y_pos": 250, + "y_pos": 331.2, "description": "Type node." } }, @@ -37,14 +37,38 @@ ], "outputs": [ { - "id": "outPort", + "id": "outPort_1", "app_data": { "ui_data": { "cardinality": { "min": 0, "max": -1 }, - "label": "Output Port" + "label": "Output Port 1" + } + } + }, + { + "id": "outPort_2", + "app_data": { + "ui_data": { + "cardinality": { + "min": 0, + "max": -1 + }, + "label": "Output Port 2" + } + } + }, + { + "id": "outPort_3", + "app_data": { + "ui_data": { + "cardinality": { + "min": 0, + "max": -1 + }, + "label": "Output Port 3" } } } @@ -56,12 +80,12 @@ "comments": [ { "id": "0b123469-7d21-43a5-ae84-cbc999990033", - "x_pos": 54, - "y_pos": 43.2, - "width": 288, - "height": 158.4, + "x_pos": 36, + "y_pos": 28.8, + "width": 342, + "height": 231, "class_name": "d3-comment-rect bkg-col-green-20", - "content": "### Prompt Canvas\n\nThis canvas provides a method to add new nodes to the flow with a prompt. To do this:\n1. Hover over the Type node\n2. In the context toolbar, click the \"Add node with prompt\" button\n3. Select a node type from the prompt. ", + "content": "### Prompt Canvas\n\nThis canvas provides a method to add new nodes to the flow with a prompt. To do this either:\n1. Click one of the ports of the Type node\n2. Select a node type from the prompt. \n\nor:\n1. Hover over the Type node \n2. Click the \"Add node with prompt\" button in the context toolbar", "associated_id_refs": [] } ] diff --git a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-react.jsx b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-react.jsx index 55929c3571..59fc07d57d 100644 --- a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-react.jsx +++ b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt-react.jsx @@ -26,7 +26,8 @@ export default class PromptReactNode extends React.Component { } onClick(nodeTemplate, evt) { - this.props.nodeData.app_data.prompt_data.addNodeCallback(nodeTemplate); + this.props.nodeData.app_data.prompt_data + .addNodeCallback(nodeTemplate, this.props.nodeData.id); } onScroll(evt) { @@ -46,7 +47,7 @@ export default class PromptReactNode extends React.Component { } return ( -
{ nodeDivs } diff --git a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt.scss b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt.scss index ece31c826d..7ee595fae6 100644 --- a/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt.scss +++ b/canvas_modules/harness/src/client/components/custom-canvases/prompt/prompt.scss @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2024 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,6 @@ .prompt { .d3-node-group { - .d3-node-body-outline { - fill: transparent; - stroke: transparent; - } - .d3-node-selection-highlight[data-selected="yes"] { stroke: $gray-50; stroke-dasharray: 5, 5; @@ -31,69 +26,6 @@ fill: transparent; pointer-events: none; } - - .d3-node-port-output { - opacity: 0; - transform: translateX(-8px); - transition: opacity 0.1s cubic-bezier(0.4, 0.14, 0.3, 1), transform 0.1s cubic-bezier(0.175, 0.885, 0.32, 1.275); - transition-delay: 0.125s; - } - - .d3-node-ellipsis-group { - .d3-node-ellipsis { - fill: $icon-primary; - } - - &:hover { - .d3-node-ellipsis-background { - fill: $layer-accent-01; - } - } - } - - // Set the outline/background for decorations. This will only affect the - // zoom-in decorations on supernode since that is th eonly one with an - // outline. - .d3-node-dec-group { - .d3-node-dec-outline { - fill: transparent; - stroke-width: 0; - } - - .d3-node-dec-image[data-id*="supernode-zoom"] { - display: none; - fill: transparent; - stroke-width: 0; - } - } - - /* Hover over d3-node-group */ - &:hover { - .d3-node-port-output { - opacity: 1; - transform: translateX(0); - transition: opacity 0.1s cubic-bezier(0.175, 0.885, 0.32, 1.275), transform 0.1s cubic-bezier(0.175, 0.885, 0.32, 1.275); - transition-delay: 0.125s; - } - - // Set the outline/background for decorations. This will only affect the - // zoom-in decorations on supernode since that is the only one with an - // outline. - .d3-node-dec-group { - .d3-node-dec-image[data-id*="supernode-zoom"] { - display: block; - fill: $icon-primary; - stroke-width: 0; - } - - &:hover { - .d3-node-dec-outline { - fill: $layer-accent-01; - stroke-width: 0; - } - } - } - } } .d3-data-link .d3-link-line, @@ -139,64 +71,4 @@ fill: transparent; pointer-events: none; } - - /* Decoration Styles */ - - .node-cache-empty { - fill: $layer-01; - } - - .node-cache-full { - fill: $layer-01; - } - - .node-sql-pushback { - fill: $layer-01; - } - - /* Override styles in common canvas to fade out nodes and comments - when they are cut to the clipboard.*/ - .node-image[data-is-cut] { - zoom: 1; - filter: "alpha(opacity=50)"; - opacity: 0.5; - } - - .canvas-comment[data-is-cut] { - zoom: 1; - filter: "alpha(opacity=50)"; - opacity: 0.5; - } - - .canvas-ui-empty-placeholder { - height: 150px; - width: 320px; - display: flex; - flex-direction: column; - justify-content: center; - } - - .canvas-ui-empty-image-placeholder { - height: 150px; - width: 250px; - float: left; - margin-left: -48px; - } - - .canvas-ui-empty-text-placeholder { - @include type-style("productive-heading-03"); - color: $text-primary; - } - - .canvas-ui-empty-subtext-placeholder { - @include type-style("body-long-02"); - color: $text-secondary; - margin-top: 8px; - } - - .canvas-ui-empty-node-text { - @include type-style("productive-heading-02"); - color: $text-secondary; - } - } diff --git a/canvas_modules/harness/src/styles/canvas-customization.scss b/canvas_modules/harness/src/styles/canvas-customization.scss index e45910f476..0fb8c2c56c 100644 --- a/canvas_modules/harness/src/styles/canvas-customization.scss +++ b/canvas_modules/harness/src/styles/canvas-customization.scss @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2024 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,10 +26,11 @@ .d3-node-port-output { fill: $layer-01; stroke-width: 1; - } - .d3-node-port-input { - stroke: $background-inverse; + &:hover { + fill: $link-inverse; + stroke: $link-inverse; + } } .d3-node-selection-highlight[data-selected="yes"], diff --git a/docs/pages/03.02.01-canvas-config.md b/docs/pages/03.02.01-canvas-config.md index a8fa1be5bc..2bf8fa9e72 100644 --- a/docs/pages/03.02.01-canvas-config.md +++ b/docs/pages/03.02.01-canvas-config.md @@ -216,6 +216,9 @@ This is a boolean. The default is false. When set to true, Common Canvas will ov ## Canvas Content +#### **enableFocusOnMount** +This is a boolean. The default is true. When set to true, the keyboard focus will automatically be set on the flow editor when Common Canvas first appears. This means, keyboard shortcut operations on the flow editor, and it contents, will be executed without the user having to click on the flow editor to get focus to be moved there. If set to false, Common Canvas makes no change to the focus so the application can set the focus wherever it wants. + #### **emptyCanvasContent** This is a JSX or HTML snippet that contains some text or any elements (such as an image) that you want to display when the canvas is empty, that is, when it doesn't have any nodes or comments. The default behavior if this config parameter is not provided is that Common Canvas will display an image and message saying: "Your flow is empty!". diff --git a/docs/pages/03.05-keyboard-support.md b/docs/pages/03.05-keyboard-support.md index ed472ec20e..ac5c1a8c68 100644 --- a/docs/pages/03.05-keyboard-support.md +++ b/docs/pages/03.05-keyboard-support.md @@ -5,6 +5,7 @@ Common Canvas supports a number of keyboard interactions as described below. Som When any of the shortcut keys are pressed, if the shortcut has an action (listed below), Common Canvas will follow the same procedure as if the action was initiated from a context menu or from the canvas toolbar or by direct manipulation on the canvas. That is, it will: call the [beforeEditActionHandler](03.03.02-before-edit-action-handler.md) and the [editActionHandler](03.03.03-edit-action-handler.md) callbacks, with the `data.editType` parameter set to the action name and the `data.editSource` parameter set to "keyboard"; it will then update the object model with the change and refresh the flow editor display. Note: In the tables below: + * "Meta" means either the Command key (⌘) on the Mac or, on Windows, the Windows key (⊞) or Control key (Ctrl). * "Alt" means either the Option key (⌥) on the Mac or, on Windows, the Alternative key (Alt). diff --git a/docs/pages/03.06.01-node-customization.md b/docs/pages/03.06.01-node-customization.md index cece7cfb1b..8e8d6bbdd6 100644 --- a/docs/pages/03.06.01-node-customization.md +++ b/docs/pages/03.06.01-node-customization.md @@ -22,7 +22,7 @@ The possible node layout properties are shown below with the values they have wh ``` // Default node sizes. These dimensions might be overridden for nodes that have - // more ports than will fit in the default size if inputPortAutoPosition is. + // more ports than will fit in the default size if inputPortAutoPosition is // set to true and outputPortAutoPosition is set to true. (See below). defaultNodeWidth: 160, defaultNodeHeight: 40, @@ -34,7 +34,7 @@ The possible node layout properties are shown below with the values they have wh // Displays the node outline shape underneath the image and label. nodeShapeDisplay: true, - // Default node shape. Can be "rectangle" or "port-arcs". Used when nodeOutlineDisplay is true. + // Default node shape. Can be "rectangle" or "port-arcs". Used when nodeShapeDisplay is true. nodeShape: "port-arcs", // An SVG path or a function that returns an SVG path. The paths define the node @@ -153,15 +153,21 @@ The possible node layout properties are shown below with the values they have wh // Display input ports. inputPortDisplay: true, - // Object for input port can be "circle" or "image". - inputPortObject: "circle", - - // If input port object is "image" use this image. - inputPortImage: "", - - // If input port dimensions for "image". - inputPortWidth: 12, - inputPortHeight: 12, + // An array of elements to control display of input ports. Each element + // can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. + inputPortDisplayObjects: [ + { type: "circleWithArrow" } + ], // Indicates whether multiple input ports should be automatically // positioned (true) or positioned based on the contents of @@ -172,31 +178,47 @@ The possible node layout properties are shown below with the values they have wh // this: { x_pos: 5, y_pos: 10, pos: "topLeft" }. x_pos and y_pos are // offsets from the pos point on the node. // The order of the elements corresponds to the order of ports in the - // inputs array for the node. + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. inputPortPositions: [ { x_pos: 0, y_pos: 20, pos: "topLeft" } ], - // The 'guide' is the object drawn at the mouse position as a new line - // is being dragged outwards. - // Object for input port guide can be "circle" or "image". - inputPortGuideObject: "circle", - - // If input port guide object is "image" use this image. - inputPortGuideImage: "", + // An array of elements to control display of input port guide objects. + // That is the object drawn at the end of a new link as it is being dragged. + // Each element can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // inputs array for the node. If there are more input ports than elements + // in the array, the last element will be used for all remaining ports. + inputPortGuideObjects: [ + { type: "circle" } + ], // Display output ports. outputPortDisplay: true, - // Object for output port can be "circle" or "image". - outputPortObject: "circle", - - // If output port object is "image" use this image. - outputPortImage: "", - - // Output port dimensions for "image". - outputPortWidth: 12, - outputPortHeight: 12, + // An array of elements to control display of output ports. Each element + // can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. + outputPortDisplayObjects: [ + { type: "circle" } + ], // Indicates whether multiple output ports should be automatically // positioned (true) or positioned based on the contents of @@ -207,18 +229,28 @@ The possible node layout properties are shown below with the values they have wh // this: { x_pos: 5, y_pos: 10, pos: "topRight" }. x_pos and y_pos are // offsets from the pos point on the node. // The order of the elements corresponds to the order of ports in the - // outputs array for the node. + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. outputPortPositions: [ { x_pos: 0, y_pos: 20, pos: "topRight" } ], - // The 'guide' is the object drawn at the mouse position as a new line - // is being dragged outwards. - // Object for output port guide can be "circle" or "image". - outputPortGuideObject: "circle", - - // If output port guide object is "image" use this image. - outputPortGuideImage: "", + // An array of elements to control display of output port guide objects. + // That is the object drawn at the end of a new link as it is being dragged. + // Each element can have a number of different structures like this: + // Either + // { type: "circle" } // Can also be "circleWithArrow" + // Or + // { type: "image", src: "path/picture.svg", width: 10, height: 10 } + // Or + // { type: "jsx", src: (), width: 16, height: 16 } + // + // The order of the elements corresponds to the order of ports in the + // outputs array for the node. If there are more output ports than elements + // in the array, the last element will be used for all remaining ports. + outputPortGuideObjects: [ + { type: "circle" } + ], // Automatically increases the node size to accommodate its ports so both // input and output ports can be shown within the dimensions of @@ -239,7 +271,7 @@ The possible node layout properties are shown below with the values they have wh // Spacing between the port arcs around the ports. portArcSpacing: 3, - // Position of the context toolbar realtive to the node. Some adjustment + // Position of the context toolbar relative to the node. Some adjustment // will be made to account for the width of the toolbar. contextToolbarPosition: "topRight", @@ -251,7 +283,6 @@ The possible node layout properties are shown below with the values they have wh ellipsisPosX: 145, ellipsisPosY: 9, ellipsisHoverAreaPadding: 2 - ``` ### Node Element positioning