diff --git a/canvas_modules/common-canvas/__tests__/common-properties/conditions/nested-conditions-test.js b/canvas_modules/common-canvas/__tests__/common-properties/conditions/nested-conditions-test.js new file mode 100644 index 0000000000..6c6478035c --- /dev/null +++ b/canvas_modules/common-canvas/__tests__/common-properties/conditions/nested-conditions-test.js @@ -0,0 +1,704 @@ +/* + * Copyright 2025 Elyra Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from "chai"; +import { fireEvent } from "@testing-library/react"; + +// import { validateInput } from "../../../src/common-properties/ui-conditions/ui-conditions.js"; +import propertyUtilsRTL from "../../_utils_/property-utilsRTL.js"; +import tableUtilsRTL from "./../../_utils_/table-utilsRTL"; +import nestedConditionsParamDef from "../../test_resources/paramDefs/nestedConditions_paramDef.json"; + +describe("nested conditions display the error in the correct cell and table", () => { + let wrapper; + let controller; + beforeEach(() => { + const renderedObject = propertyUtilsRTL.flyoutEditorForm(nestedConditionsParamDef); + wrapper = renderedObject.wrapper; + controller = renderedObject.controller; + // controller.setErrorMessages({}); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + it("error message does not appear when common properties first opened", () => { + const expectedError = { + "nested_table": { + "0": { + "2": { + "displayError": false, + "propertyId": { + "col": 2, + "name": "nested_table", + "row": 0 + }, + "required": false, + "text": "Cannot select 'orange'", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + const allMessages = controller.getAllErrorMessages(); + expect(allMessages).to.eql(expectedError); + expect(controller.getErrorMessages()).to.eql({}); + + const container = wrapper.container; + const errors = container.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(0); + }); + + it("conditions work correctly for a nested list control", () => { + const container = wrapper.container; + let mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + tableUtilsRTL.selectCheckboxes(mainTable, [0]); // Select first row for onPanel edit + + // verify onPanel edit shows list control + mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + const onPanelList = mainTable.querySelectorAll(".properties-onpanel-container"); + expect(onPanelList).to.have.length(1); + + const listControl = onPanelList[0].querySelectorAll(".properties-list-table"); + expect(listControl.length).to.equal(1); + const inputs = listControl[0].querySelectorAll("input[type='text']"); + expect(inputs.length).to.equal(2); + + const expectedErrors = { + "nested_table": { + "0": { + "0": { + "1": { + "propertyId": { + "col": 0, + "name": "nested_table", + "propertyId": { + "name": "list", + "row": 1 + }, + "row": 0 + }, + "required": false, + "text": "Cannot be 'error'", + "type": "error", + "validation_id": "nested_table" + }, + "propertyId": { + "col": 0, + "name": "nested_table", + "propertyId": { + "name": "list", + "row": 1 + }, + "row": 0 + }, + "required": false, + "text": "Cannot be 'error'", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + + // Modify second input in list to show error + fireEvent.change(inputs[1], { target: { value: "error" } }); + expect(controller.getErrorMessages()).to.eql(expectedErrors); + + // Verify main table also display the error in cell + let errorCells = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errorCells.length).to.equal(2); // parent table and on panel `list` table + + // Remove error from list will also clear from parent table + fireEvent.change(inputs[1], { target: { value: "removed error cell" } }); + expect(controller.getErrorMessages()).to.eql({}); + errorCells = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errorCells.length).to.equal(0); + }); + + it("conditions work correctly for a nested structuretable control", () => { + const container = wrapper.container; + let mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + tableUtilsRTL.selectCheckboxes(mainTable, [0]); // Select first row for onPanel edit + + // verify onPanel edit shows structuretable control + mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + const onPanelTable = mainTable.querySelectorAll(".properties-onpanel-container"); + expect(onPanelTable).to.have.length(1); + + const structureTableControl = onPanelTable[0].querySelectorAll("div[data-id='properties-structuretable']"); + expect(structureTableControl.length).to.equal(1); + const dropdowns = structureTableControl[0].querySelectorAll("select"); + expect(dropdowns.length).to.equal(2); + + const expectedOneError = { + "nested_table": { + "0": { + "3": { + "0": { + "1": { + "propertyId": { + "col": 3, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structuretable", + "row": 0 + }, + "row": 0 + }, + "required": false, + "text": "Cannot select 'green'", + "type": "error", + "validation_id": "nested_table" + }, + }, + "propertyId": { + "col": 3, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structuretable", + "row": 0 + }, + "row": 0 + }, + "required": false, + "text": "Cannot select 'green'", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + + // Modify first dropdown to show error + fireEvent.change(dropdowns[0], { target: { value: "green" } }); + expect(controller.getErrorMessages()).to.eql(expectedOneError); + + // Verify main table also display the error in cell + const errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(2); // parent table and on panel `list` table + + const expectedTwoErrors = { + "nested_table": { + "0": { + "3": { + "0": { + "1": { + "propertyId": { + "col": 3, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structuretable", + "row": 0 + }, + "row": 0 + }, + "required": false, + "text": "Cannot select 'green'", + "type": "error", + "validation_id": "nested_table" + } + }, + "1": { + "1": { + "propertyId": { + "col": 3, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structuretable", + "row": 1 + }, + "row": 0 + }, + "required": false, + "tableText": "There are 2 error cells. ", + "text": "Cannot select 'green'", + "type": "error", + "validation_id": "nested_table" + } + }, + "propertyId": { + "col": 3, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structuretable", + "row": 1 + }, + "row": 0 + }, + "required": false, + "text": "Cannot select 'green'", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + + // Modify second dropdown to show another error + fireEvent.change(dropdowns[1], { target: { value: "green" } }); + expect(controller.getErrorMessages()).to.eql(expectedTwoErrors); + + const validationErrors = structureTableControl[0].querySelectorAll(".properties-validation-message.error"); + expect(validationErrors.length).to.equal(3); // First two are in cells, last one is on table + expect(validationErrors[2].querySelector("span").textContent).to.equal("There are 2 error cells. "); + + // Verify main table also display the error in cell + let errorCells = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errorCells.length).to.equal(3); // parent table and on panel `structuretable` table + + // Remove error from second dropdown will also update the error propertyId of structuretable + fireEvent.change(dropdowns[1], { target: { value: "yellow" } }); + expect(controller.getErrorMessages()).to.eql(expectedOneError); + + // Remove error fom first dropdown will clear all error cells from both structuretable and parent table + fireEvent.change(dropdowns[0], { target: { value: "red" } }); + expect(controller.getErrorMessages()).to.eql({}); + errorCells = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errorCells.length).to.equal(0); + }); + + it("conditions work correctly for a nested selectColumns control", () => { + const container = wrapper.container; + const mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + const editButton = mainTable.querySelectorAll("button.properties-subpanel-button")[0]; + fireEvent.click(editButton); // Click subpanel edit button on first row + + // verify subpanel shows selectColumns control + const subPanel = container.querySelectorAll(".properties-editstyle-sub-panel"); + expect(subPanel).to.have.length(1); + + const selectColumnsControl = subPanel[0].querySelectorAll(".properties-column-select-table"); + expect(selectColumnsControl.length).to.equal(1); + + // Select 'K' to show error + const fieldPicker = tableUtilsRTL.openFieldPicker(container, "properties-ft-select_columns"); + tableUtilsRTL.fieldPicker(fieldPicker, ["K"]); + + const expectedSelectedValues = ["Age", "Na", "K"]; + expect(controller.getPropertyValue({ name: "nested_table", row: 0, col: 1 })).to.eql(expectedSelectedValues); + const expectedOneError = { + "nested_table": { + "0": { + "1": { + "2": { + "propertyId": { + "col": 1, + "name": "nested_table", + "propertyId": { + "row": 2 + }, + "row": 0 + }, + "required": false, + "text": "Cannot contain 'K'", + "type": "error", + "validation_id": "nested_table" + }, + "propertyId": { + "col": 1, + "name": "nested_table", + "propertyId": { + "row": 2 + }, + "row": 0 + }, + "required": false, + "text": "Cannot contain 'K'", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + expect(controller.getErrorMessages()).to.eql(expectedOneError); + + // Verify subpanel selectColumns table display the error in cell + const subPanelErrors = selectColumnsControl[0].querySelectorAll(".properties-validation-message.error.inTable"); + expect(subPanelErrors.length).to.equal(1); + + // Verify main parent table also display the error in cell + const errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(1); + }); + + it("conditions work correctly for a nested someofselect control", () => { + const container = wrapper.container; + const mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + const editButton = mainTable.querySelectorAll("button.properties-subpanel-button")[0]; + fireEvent.click(editButton); // Click subpanel edit button on first row + + // verify subpanel shows someofselect control + const subPanel = container.querySelectorAll(".properties-editstyle-sub-panel"); + expect(subPanel).to.have.length(1); + + // Select 'yellow' to trigger the error to show + const someofselectControl = subPanel[0].querySelectorAll("div[data-id='properties-someofselect']"); + expect(someofselectControl.length).to.equal(1); + tableUtilsRTL.selectCheckboxes(someofselectControl[0], [2]); + + const expectedSelectedValues = ["orange", "yellow", "green"]; + expect(controller.getPropertyValue({ name: "nested_table", row: 0, col: 2 })).to.eql(expectedSelectedValues); + + const expectedOneError = { + "nested_table": { + "0": { + "2": { + "propertyId": { + "col": 2, + "name": "nested_table", + "row": 0 + }, + "required": false, + "text": "Cannot select 'orange'", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + expect(controller.getErrorMessages()).to.eql(expectedOneError); + + // Verify subpanel someofselect table display the error in table + let subPanelErrors = someofselectControl[0].querySelectorAll(".properties-validation-message.error"); + expect(subPanelErrors.length).to.equal(1); + + // Verify main parent table also display the error in cell + let errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(1); + + // Deselect 'orange' will remove error from someofselect table and parent table + tableUtilsRTL.selectCheckboxes(someofselectControl[0], [1]); + const expectedValues = ["yellow", "green"]; + expect(controller.getPropertyValue({ name: "nested_table", row: 0, col: 2 })).to.eql(expectedValues); + + subPanelErrors = someofselectControl[0].querySelectorAll(".properties-validation-message.error"); + expect(subPanelErrors.length).to.equal(0); + errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(0); + }); + + it("conditions work correctly for a nested structurelisteditor control", () => { + const container = wrapper.container; + const mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + const editButton = mainTable.querySelectorAll("button.properties-subpanel-button")[0]; + fireEvent.click(editButton); // Click subpanel edit button on first row + + // verify subpanel shows structurelisteditor control + const subPanel = container.querySelectorAll(".properties-editstyle-sub-panel"); + expect(subPanel).to.have.length(1); + + const structurelisteditorControl = subPanel[0].querySelectorAll("div[data-id='properties-ft-structurelisteditor']"); + expect(structurelisteditorControl.length).to.equal(1); + + const textInputs = structurelisteditorControl[0].querySelectorAll("input[type='text']"); + expect(textInputs.length).to.equal(2); + const numberInputs = structurelisteditorControl[0].querySelectorAll("input[type='number']"); + expect(numberInputs.length).to.equal(2); + + const expectedOneError = { + "nested_table": { + "0": { + "4": { + "0": { + "0": { + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 0, + "name": "structurelisteditor", + "row": 0 + }, + "row": 0 + }, + "required": false, + "text": "Cannot be 'error'", + "type": "error", + "validation_id": "nested_table" + } + }, + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 0, + "name": "structurelisteditor", + "row": 0 + }, + "row": 0 + }, + "required": false, + "text": "Cannot be 'error'", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + + // Modify row 0 textinput to show error + fireEvent.change(textInputs[0], { target: { value: "error" } }); + expect(controller.getErrorMessages()).to.eql(expectedOneError); + + // Verify main parent table also display the error in cell + let errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(1); + + // Verify structurelistedior also display the error in bottom of table + let validationErrors = subPanel[0].querySelectorAll(".properties-validation-message.error"); + expect(validationErrors.length).to.equal(2); // First one is in cell, the other is on table + expect(validationErrors[1].querySelector("span").textContent).to.equal("Cannot be 'error'"); + + const expectedTwoErrors = { + "nested_table": { + "0": { + "4": { + "0": { + "0": { + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 0, + "name": "structurelisteditor", + "row": 0 + }, + "row": 0 + }, + "required": false, + "text": "Cannot be 'error'", + "type": "error", + "validation_id": "nested_table" + }, + "1": { + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structurelisteditor", + "row": 0 + }, + "row": 0 + }, + "required": false, + "tableText": "There are 2 error cells. ", + "text": "Must be greater than 0", + "type": "error", + "validation_id": "nested_table" + } + }, + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structurelisteditor", + "row": 0 + }, + "row": 0 + }, + "required": false, + "text": "Must be greater than 0", + "type": "error", + "validation_id": "nested_table" + } + } + } + }; + + // Modify row 0 numberinput to show error + fireEvent.change(numberInputs[0], { target: { value: 0 } }); + expect(controller.getErrorMessages()).to.eql(expectedTwoErrors); + + // Verify main table also display the error in cell + errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(1); + + // Verify structurelisteditor table error gets updated + validationErrors = subPanel[0].querySelectorAll(".properties-validation-message.error"); + expect(validationErrors.length).to.equal(3); // Two errors in the cell, the other is on table + expect(validationErrors[2].querySelector("span").textContent).to.equal("There are 2 error cells. "); + + // Remove error from numberinput, table error should be set back to first error + fireEvent.change(numberInputs[0], { target: { value: 1 } }); + expect(controller.getErrorMessages()).to.eql(expectedOneError); + validationErrors = subPanel[0].querySelectorAll(".properties-validation-message.error"); + expect(validationErrors.length).to.equal(2); // First one is in cell, the other is on table + expect(validationErrors[1].querySelector("span").textContent).to.equal("Cannot be 'error'"); + }); + + it("default required conditions work correctly for a nested structurelisteditor control", () => { + const container = wrapper.container; + const mainTable = container.querySelector("div[data-id='properties-ci-nested_table']"); + const editButton = mainTable.querySelectorAll("button.properties-subpanel-button")[0]; + fireEvent.click(editButton); // Click subpanel edit button on first row + + // verify subpanel shows structurelisteditor control + const subPanel = container.querySelectorAll(".properties-editstyle-sub-panel"); + expect(subPanel).to.have.length(1); + + const structurelisteditorControl = subPanel[0].querySelectorAll("div[data-id='properties-ft-structurelisteditor']"); + expect(structurelisteditorControl.length).to.equal(1); + + const textInputs = structurelisteditorControl[0].querySelectorAll("input[type='text']"); + expect(textInputs.length).to.equal(2); + const numberInputs = structurelisteditorControl[0].querySelectorAll("input[type='number']"); + expect(numberInputs.length).to.equal(2); + + const expectedOneError = { + "nested_table": { + "0": { + "4": { + "1": { + "0": { + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 0, + "name": "structurelisteditor", + "row": 1 + }, + "row": 0 + }, + "required": true, + "text": "You must enter a value for textfield.", + "type": "error", + "validation_id": "required_nested_table[4][0]_186.07303925125697" + } + }, + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 0, + "name": "structurelisteditor", + "row": 1 + }, + "row": 0 + }, + "required": true, + "text": "You must enter a value for textfield.", + "type": "error", + "validation_id": "required_nested_table[4][0]_186.07303925125697" + } + } + } + }; + + // Modify row 1 textinput to show error + fireEvent.change(textInputs[1], { target: { value: "" } }); + expect(controller.getErrorMessages()).to.eql(expectedOneError); + + // Verify main parent table also display the error in cell + let errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(1); + + // Verify structurelistedior also display the error in bottom of table + let validationErrors = subPanel[0].querySelectorAll(".properties-validation-message.error"); + expect(validationErrors.length).to.equal(2); // First one is in cell, the other is on table + expect(validationErrors[1].querySelector("span").textContent).to.equal("You must enter a value for textfield."); + + const expectedTwoErrors = { + "nested_table": { + "0": { + "4": { + "0": { + "1": { + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structurelisteditor", + "row": 0 + }, + "row": 0 + }, + "required": true, + "text": "You must enter a value for numberfield.", + "type": "error", + "validation_id": "required_nested_table[4][1]_344.3166233602058" + } + }, + "1": { + "0": { + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 0, + "name": "structurelisteditor", + "row": 1 + }, + "row": 0 + }, + "required": true, + "tableText": "There are 2 error cells. ", + "text": "You must enter a value for textfield.", + "type": "error", + "validation_id": "required_nested_table[4][0]_186.07303925125697" + } + }, + "propertyId": { + "col": 4, + "name": "nested_table", + "propertyId": { + "col": 1, + "name": "structurelisteditor", + "row": 0 + }, + "row": 0 + }, + "required": true, + "text": "You must enter a value for numberfield.", + "type": "error", + "validation_id": "required_nested_table[4][1]_344.3166233602058" + } + } + } + }; + + // Modify row 1 numberinput to show error + fireEvent.change(numberInputs[0], { target: { value: null } }); + expect(controller.getErrorMessages()).to.eql(expectedTwoErrors); + + // Verify main table also display the error in cell + errors = mainTable.querySelectorAll(".properties-validation-message.error.inTable"); + expect(errors.length).to.equal(1); + + // Verify structurelisteditor table error gets updated + validationErrors = subPanel[0].querySelectorAll(".properties-validation-message.error"); + expect(validationErrors.length).to.equal(3); // Two errors in the cell, the other is on table + expect(validationErrors[2].querySelector("span").textContent).to.equal("There are 2 error cells. "); + + // Remove error from numberinput, table error should be set back to first error + fireEvent.change(numberInputs[0], { target: { value: 1 } }); + expect(controller.getErrorMessages()).to.eql(expectedOneError); + validationErrors = subPanel[0].querySelectorAll(".properties-validation-message.error"); + expect(validationErrors.length).to.equal(2); // First one is in cell, the other is on table + expect(validationErrors[1].querySelector("span").textContent).to.equal("You must enter a value for textfield."); + }); +}); diff --git a/canvas_modules/common-canvas/__tests__/common-properties/controls/structurelisteditor-test.js b/canvas_modules/common-canvas/__tests__/common-properties/controls/structurelisteditor-test.js index 0b32b4b455..b0d4a2d81d 100644 --- a/canvas_modules/common-canvas/__tests__/common-properties/controls/structurelisteditor-test.js +++ b/canvas_modules/common-canvas/__tests__/common-properties/controls/structurelisteditor-test.js @@ -620,7 +620,8 @@ describe("StructureListEditor render from paramdef", () => { "required": false, "validation_id": "tableerrortest3", "type": "error", - "text": "There are 2 error cells. ", + "tableText": "There are 2 error cells. ", + "text": "checkbox cannot be off" }; actual = renderedController.getErrorMessage({ name: "inlineEditingTableError" }); diff --git a/canvas_modules/common-canvas/__tests__/test_resources/paramDefs/nestedConditions_paramDef.json b/canvas_modules/common-canvas/__tests__/test_resources/paramDefs/nestedConditions_paramDef.json new file mode 100644 index 0000000000..53fa69f24b --- /dev/null +++ b/canvas_modules/common-canvas/__tests__/test_resources/paramDefs/nestedConditions_paramDef.json @@ -0,0 +1,422 @@ +{ + "titleDefinition": { + "title": "Nested cells" + }, + "current_parameters": { + "nested_table": [ + [ + ["list1", "list2"], + ["Age", "Na"], + ["orange", "green"], + [["Sex", "red"], ["BP", "blue"]], + [["Hello", 1], ["world", 2]] + ] + ] + }, + "parameters": [ + { + "id": "nested_table", + "type": "array[table_controls]" + } + ], + "complex_types": [ + { + "id": "table_controls", + "parameters": [ + { + "id": "list", + "type": "array[string]", + "required": true + }, + { + "id": "select_columns", + "type": "array[string]", + "role": "column" + }, + { + "id": "someofselect", + "type": "array[string]", + "enum": [ + "red", + "orange", + "yellow", + "green", + "blue", + "purple" + ] + }, + { + "id": "structuretable", + "type": "map[string, nestedStructureTable]", + "role": "column" + }, + { + "id": "structurelisteditor", + "type": "array[nestedStructureListEditor]" + } + ] + }, + { + "id": "nestedStructureTable", + "parameters": [ + { + "id": "field", + "type": "string", + "role": "column" + }, + { + "id": "oneofselect", + "enum": [ + "red", + "orange", + "yellow", + "green", + "blue", + "purple" + ] + } + ] + }, + { + "id": "nestedStructureListEditor", + "parameters": [ + { + "id": "textfield", + "type": "string", + "required": true + }, + { + "id": "numberfield", + "type": "integer", + "required": true + } + ] + } + ], + "uihints": { + "id": "nested cells test", + "icon": "images/default.svg", + "label": { + "default": "Nested cells test" + }, + "editor_size": "large", + "parameter_info": [ + { + "parameter_ref": "nested_table", + "label": { + "default": "Table within table" + }, + "description": { + "default": "This example displays conditions within a nested table.", + "placement": "on_panel" + } + } + ], + "complex_type_info": [ + { + "complex_type_ref": "table_controls", + "parameters": [ + { + "parameter_ref": "list", + "width": 10, + "label": { + "default": "list" + }, + "description": { + "default": "Enter 'error' in a cell to display an error", + "placement": "on_panel" + }, + "edit_style": "on_panel", + "control": "list" + }, + { + "parameter_ref": "select_columns", + "width": 10, + "label": { + "default": "selectColumns" + }, + "description": { + "default": "Select the column 'K' to display an error", + "placement": "on_panel" + }, + "edit_style": "subpanel" + }, + { + "parameter_ref": "someofselect", + "width": 10, + "label": { + "default": "someofselect" + }, + "description": { + "default": "Select 'orange' below to display an error. It is preselected initially and error will not be shown until the control is modified", + "placement": "on_panel" + }, + "edit_style": "subpanel" + }, + { + "parameter_ref": "structuretable", + "width": 10, + "label": { + "default": "structuretable" + }, + "description": { + "default": "Select 'green' in the second column to display an error", + "placement": "on_panel" + }, + "edit_style": "on_panel" + }, + { + "parameter_ref": "structurelisteditor", + "width": 10, + "label": { + "default": "structurelisteditor" + }, + "description": { + "default": "Enter 'error' in a cell in the first column, or enter a number less than 1 in the second column to display an error. Clearing a cell error will update the table error to show the remaining error present in the table", + "placement": "on_panel" + }, + "edit_style": "subpanel" + } + ] + }, + { + "complex_type_ref": "nestedStructureTable", + "moveable_rows": true, + "parameters": [ + { + "parameter_ref": "field", + "label": { + "default": "field" + } + }, + { + "parameter_ref": "oneofselect", + "label": { + "default": "oneofselect" + } + } + ] + }, + { + "complex_type_ref": "nestedStructureListEditor", + "moveable_rows": true, + "parameters": [ + { + "parameter_ref": "textfield", + "label": { + "default": "textfield" + }, + "control": "textfield" + }, + { + "parameter_ref": "numberfield", + "label": { + "default": "numberfield" + }, + "control": "numberfield" + } + ] + } + ], + "group_info": [ + { + "id": "numberfield-table-panels", + "label": { + "default": "Table" + }, + "type": "controls", + "parameter_refs": [ + "nested_table" + ] + } + ] + }, + "conditions": [ + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[0]", + "message": { + "default": "Cannot be 'error'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[0]", + "op": "notEquals", + "value": "error" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[1]", + "message": { + "default": "Cannot contain 'K'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[1]", + "op": "notContains", + "value": "K" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[2]", + "message": { + "default": "Cannot select 'orange'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[2]", + "op": "notContains", + "value": "orange" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[3]", + "message": { + "default": "Cannot select 'green'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[3][1]", + "op": "notEquals", + "value": "green" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[4][0]", + "message": { + "default": "Cannot be 'error'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[4][0]", + "op": "notEquals", + "value": "error" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[4][1]", + "message": { + "default": "Must be greater than 0" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[4][1]", + "op": "greaterThan", + "value": "0" + } + } + } + } + ], + "dataset_metadata": [ + { + "fields": [ + { + "name": "Age", + "type": "integer", + "metadata": { + "description": "", + "measure": "range", + "modeling_role": "input" + } + }, + { + "name": "Sex", + "type": "string", + "metadata": { + "description": "", + "measure": "ordered_set", + "modeling_role": "input" + } + }, + { + "name": "BP", + "type": "string", + "metadata": { + "description": "", + "measure": "discrete", + "modeling_role": "input" + } + }, + { + "name": "Cholesterol", + "type": "string", + "metadata": { + "description": "", + "measure": "set", + "modeling_role": "input" + } + }, + { + "name": "Na", + "type": "double", + "metadata": { + "description": "", + "measure": "flag", + "modeling_role": "input" + } + }, + { + "name": "K", + "type": "double", + "metadata": { + "description": "", + "measure": "collection", + "modeling_role": "input" + } + }, + { + "name": "Drug", + "type": "string", + "metadata": { + "description": "", + "measure": "geospatial", + "modeling_role": "input" + } + }, + { + "name": "Ag", + "type": "integer", + "metadata": { + "description": "", + "measure": "", + "modeling_role": "input" + } + } + ] + } + ] +} diff --git a/canvas_modules/common-canvas/__tests__/test_resources/paramDefs/numberfield_paramDef.json b/canvas_modules/common-canvas/__tests__/test_resources/paramDefs/numberfield_paramDef.json index 8b32f247dc..134e26a917 100644 --- a/canvas_modules/common-canvas/__tests__/test_resources/paramDefs/numberfield_paramDef.json +++ b/canvas_modules/common-canvas/__tests__/test_resources/paramDefs/numberfield_paramDef.json @@ -4,6 +4,7 @@ }, "current_parameters": { "number_int": 10, + "number_int_readonly": 10, "number_dbl": 11.012, "number_null": null, "number_random": 12345, @@ -28,6 +29,11 @@ "type": "integer", "required": true }, + { + "id": "number_int_readonly", + "type": "integer", + "required": true + }, { "id": "number_dbl", "type": "double", @@ -161,7 +167,24 @@ "description": { "default": "numberfield with parameter value set to '10'" }, - "class_name": "numberfield-control-class" + "class_name": "numberfield-control-class", + "helper_text": { + "default": "numberfield with parameter value set to '10'" + } + }, + { + "parameter_ref": "number_int_readonly", + "label": { + "default": "Integer" + }, + "description": { + "default": "Readonly numberfield with parameter value set to '10'" + }, + "class_name": "numberfield-readonly-control-class", + "helper_text": { + "default": "Readonly numberfield with parameter value set to '10'" + }, + "read_only": true }, { "parameter_ref": "number_dbl", @@ -421,7 +444,8 @@ "number_random", "number_random_resource_key", "number_longControlName", - "number_placeholder" + "number_placeholder", + "number_int_readonly" ] }, { diff --git a/canvas_modules/common-canvas/src/common-properties/components/validation-message/validation-message.jsx b/canvas_modules/common-canvas/src/common-properties/components/validation-message/validation-message.jsx index 2414ab48f4..7b392f767d 100644 --- a/canvas_modules/common-canvas/src/common-properties/components/validation-message/validation-message.jsx +++ b/canvas_modules/common-canvas/src/common-properties/components/validation-message/validation-message.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 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,6 +19,7 @@ import PropTypes from "prop-types"; import Icon from "./../../../icons/icon.jsx"; import Tooltip from "./../../../tooltip/tooltip.jsx"; import { STATES } from "./../../constants/constants.js"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import classNames from "classnames"; export default class ValidationMessage extends React.Component { @@ -28,7 +29,13 @@ export default class ValidationMessage extends React.Component { if (!this.props.messageInfo || (this.props.tableOnly && !this.props.inTable)) { return null; } - const msgText = this.props.inTable ? null : {this.props.messageInfo.text}; + + // Check if this is a nested control, and if the messageInfo applies to that specific cell + if ((this.props.tableOnly || this.props.inTable) && !doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo)) { + return null; + } + + const msgText = this.props.inTable ? null : {this.props.messageInfo.tableText || this.props.messageInfo.text}; const icon = (
{}
); @@ -57,8 +64,11 @@ export default class ValidationMessage extends React.Component { ValidationMessage.propTypes = { messageInfo: PropTypes.shape({ text: PropTypes.string, - type: PropTypes.string + tableText: PropTypes.string, + type: PropTypes.string, + propertyId: PropTypes.object }), + propertyId: PropTypes.object, state: PropTypes.string, inTable: PropTypes.bool, tableOnly: PropTypes.bool diff --git a/canvas_modules/common-canvas/src/common-properties/constants/constants.js b/canvas_modules/common-canvas/src/common-properties/constants/constants.js index bdc08fa10d..1fe217714d 100644 --- a/canvas_modules/common-canvas/src/common-properties/constants/constants.js +++ b/canvas_modules/common-canvas/src/common-properties/constants/constants.js @@ -153,6 +153,16 @@ export const CONDITION_MESSAGE_TYPE = { SUCCESS: "success" }; +export const DEFAULT_ERROR_MESSAGE_KEYS = [ + "propertyId", + "required", + "text", + "type", + "validation_id", + "displayError", + "tableText" +]; + export const SPINNER = "spinner"; diff --git a/canvas_modules/common-canvas/src/common-properties/controls/checkbox/checkbox.jsx b/canvas_modules/common-canvas/src/common-properties/controls/checkbox/checkbox.jsx index 4a05d70e18..33085876a3 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/checkbox/checkbox.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/checkbox/checkbox.jsx @@ -20,6 +20,7 @@ import { connect } from "react-redux"; import { isEmpty } from "lodash"; import { Checkbox } from "@carbon/react"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { STATES, CARBON_ICONS } from "./../../constants/constants.js"; import Tooltip from "./../../../tooltip/tooltip.jsx"; @@ -74,7 +75,9 @@ class CheckboxControl extends React.Component { ); return (
- ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/checkboxset/checkboxset.jsx b/canvas_modules/common-canvas/src/common-properties/controls/checkboxset/checkboxset.jsx index 6cda0412d8..89a06bf483 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/checkboxset/checkboxset.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/checkboxset/checkboxset.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import { Checkbox, CheckboxGroup } from "@carbon/react"; import * as ControlUtils from "./../../util/control-utils"; import classNames from "classnames"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import { v4 as uuid4 } from "uuid"; import { intersection, isEqual } from "lodash"; import { Information } from "@carbon/react/icons"; @@ -132,11 +133,13 @@ class CheckboxsetControl extends React.Component { readOnly={this.props.readOnly} aria-label={this.props.control.labelVisible ? null : this.props.control?.label?.text} > -
+
{checkboxes}
- +
); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/datefield/datefield.jsx b/canvas_modules/common-canvas/src/common-properties/controls/datefield/datefield.jsx index 578da96d33..e4426650c4 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/datefield/datefield.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/datefield/datefield.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 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,6 +19,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import { TextInput } from "@carbon/react"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { parse, format, isValid } from "date-fns"; import { DEFAULT_DATE_FORMAT, STATES } from "./../../constants/constants.js"; @@ -73,7 +74,7 @@ class DatefieldControl extends React.Component { return null; // Do not render hidden controls } const className = classNames("properties-datefield", "properties-input-control", { "hide": hidden }, - this.props.messageInfo ? this.props.messageInfo.type : null); + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); const validationProps = ControlUtils.getValidationProps(this.props.messageInfo, this.props.tableControl); return (
@@ -91,7 +92,7 @@ class DatefieldControl extends React.Component { readOnly={this.props.readOnly} aria-label={this.props.control.labelVisible ? null : this.props.control?.label?.text} /> - +
); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/datepicker-range/datepicker-range.jsx b/canvas_modules/common-canvas/src/common-properties/controls/datepicker-range/datepicker-range.jsx index c384fd0677..ae528aa392 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/datepicker-range/datepicker-range.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/datepicker-range/datepicker-range.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2023 Elyra Authors + * Copyright 2023-2025 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,6 +24,7 @@ import { v4 as uuid4 } from "uuid"; import Tooltip from "./../../../tooltip/tooltip.jsx"; import Icon from "./../../../icons/icon.jsx"; import ValidationMessage from "../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "../../util/control-utils"; import { getFormattedDate, getISODate } from "../../util/date-utils"; import { STATES, DATEPICKER_TYPE, MESSAGE_KEYS, CARBON_ICONS } from "../../constants/constants.js"; @@ -134,7 +135,7 @@ class DatepickerRangeControl extends React.Component { endLabel = this.createInfoDesc(endLabel, endDesc, "end"); const className = classNames("properties-datepicker-range", "properties-input-control", { "hide": hidden }, - this.props.messageInfo ? this.props.messageInfo.type : null); + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); const validationProps = ControlUtils.getValidationProps(this.props.messageInfo, this.props.tableControl); return ( @@ -172,7 +173,7 @@ class DatepickerRangeControl extends React.Component { helperText={!this.props.tableControl && endHelperText} /> - +
); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/datepicker/datepicker.jsx b/canvas_modules/common-canvas/src/common-properties/controls/datepicker/datepicker.jsx index 88f1f9d651..812e98bd7d 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/datepicker/datepicker.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/datepicker/datepicker.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2023 Elyra Authors + * Copyright 2023-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import { DatePicker, DatePickerInput } from "@carbon/react"; import classNames from "classnames"; import ValidationMessage from "../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "../../util/control-utils"; import { getFormattedDate, getISODate } from "../../util/date-utils"; import { STATES, DATEPICKER_TYPE } from "../../constants/constants.js"; @@ -70,7 +71,7 @@ class DatepickerControl extends React.Component { } const helperText = this.props.controller.getResource(`${this.props.control.name}.helper`, null); const className = classNames("properties-datepicker", "properties-input-control", { "hide": hidden }, - this.props.messageInfo ? this.props.messageInfo.type : null); + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); const validationProps = ControlUtils.getValidationProps(this.props.messageInfo, this.props.tableControl); return ( @@ -96,7 +97,7 @@ class DatepickerControl extends React.Component { helperText={(!this.props.tableControl && helperText) || this.props.control.helperText} /> - + ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/dropdown/dropdown.jsx b/canvas_modules/common-canvas/src/common-properties/controls/dropdown/dropdown.jsx index 382d23c7b8..9db5f10210 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/dropdown/dropdown.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/dropdown/dropdown.jsx @@ -21,6 +21,7 @@ import { SelectItem, Select, Dropdown, ComboBox } from "@carbon/react"; import { isEqual, isEmpty } from "lodash"; import * as ControlUtils from "./../../util/control-utils"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import classNames from "classnames"; import * as PropertyUtils from "./../../util/property-utils.js"; import { ControlType } from "./../../constants/form-constants"; @@ -323,10 +324,11 @@ class DropDown extends React.Component { return ( ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/expression/expression.jsx b/canvas_modules/common-canvas/src/common-properties/controls/expression/expression.jsx index 5a93e154d8..1e08591ed1 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/expression/expression.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/expression/expression.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -444,7 +444,7 @@ class ExpressionControl extends React.Component { style={{ height: this.state.expressionEditorHeight, minHeight: minLineHeight }} aria-disabled={this.props.state === STATES.DISABLED || !this.props.readOnly} /> - + diff --git a/canvas_modules/common-canvas/src/common-properties/controls/list/list.jsx b/canvas_modules/common-canvas/src/common-properties/controls/list/list.jsx index 51f620a868..429e45c360 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/list/list.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/list/list.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -191,7 +191,7 @@ class ListControl extends AbstractTable {
{table}
- + ); return ( @@ -219,6 +219,7 @@ ListControl.propTypes = { controller: PropTypes.object.isRequired, controlItem: PropTypes.element, rightFlyout: PropTypes.bool, + tableControl: PropTypes.bool, selectedRows: PropTypes.array, // set by redux state: PropTypes.string, // pass in by redux value: PropTypes.array, // pass in by redux diff --git a/canvas_modules/common-canvas/src/common-properties/controls/multiselect/multiselect.jsx b/canvas_modules/common-canvas/src/common-properties/controls/multiselect/multiselect.jsx index ec0076595a..13fe91b150 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/multiselect/multiselect.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/multiselect/multiselect.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import { connect } from "react-redux"; import { MultiSelect, FilterableMultiSelect } from "@carbon/react"; import * as ControlUtils from "./../../util/control-utils"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import classNames from "classnames"; import * as PropertyUtils from "./../../util/property-utils.js"; import { MESSAGE_KEYS, STATES } from "./../../constants/constants.js"; @@ -193,10 +194,11 @@ class MultiSelectControl extends React.Component { return ( ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/numberfield/numberfield.jsx b/canvas_modules/common-canvas/src/common-properties/controls/numberfield/numberfield.jsx index 00deb19d20..a96290a5a7 100755 --- a/canvas_modules/common-canvas/src/common-properties/controls/numberfield/numberfield.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/numberfield/numberfield.jsx @@ -21,6 +21,7 @@ import { NumberInput, Button } from "@carbon/react"; import ValidationMessage from "./../../components/validation-message"; import * as ControlUtils from "./../../util/control-utils"; import { formatMessage } from "./../../util/property-utils"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import { STATES, MESSAGE_KEYS } from "./../../constants/constants.js"; import classNames from "classnames"; import { ControlType } from "./../../constants/form-constants"; @@ -170,7 +171,7 @@ class NumberfieldControl extends React.Component { "properties-numberfield", { "numberfield-with-number-generator": has(this.props.control, "label.numberGenerator") ? this.props.control.label.numberGenerator : null }, { "hide": hidden }, - this.props.messageInfo ? this.props.messageInfo.type : null + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null ); const validationProps = ControlUtils.getValidationProps(this.props.messageInfo, this.props.tableControl); return ( @@ -195,7 +196,7 @@ class NumberfieldControl extends React.Component { aria-label={this.props.control.labelVisible ? null : this.props.control?.label?.text} /> {numberGenerator} - + ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/passwordfield/passwordfield.jsx b/canvas_modules/common-canvas/src/common-properties/controls/passwordfield/passwordfield.jsx index 4d2f77be4e..f678ef3d27 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/passwordfield/passwordfield.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/passwordfield/passwordfield.jsx @@ -19,6 +19,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import { PasswordInput } from "@carbon/react"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { STATES, MESSAGE_KEYS } from "./../../constants/constants.js"; import classNames from "classnames"; @@ -51,7 +52,7 @@ class PasswordControl extends React.Component { const hidePasswordLabel = this.props.controller.getResource(overrideHidePasswordLabel, defaultHidePasswordLabel); const value = this.props.value ? this.props.value : ""; const className = classNames("properties-pwdfield", "properties-input-control", { "hide": hidden }, - this.props.messageInfo ? this.props.messageInfo.type : null); + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); const validationProps = ControlUtils.getValidationProps(this.props.messageInfo, this.props.tableControl); return (
@@ -71,7 +72,7 @@ class PasswordControl extends React.Component { helperText={this.props.control.helperText} aria-label={this.props.control.labelVisible ? null : this.props.control?.label?.text} /> - +
); } } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/radioset/radioset.jsx b/canvas_modules/common-canvas/src/common-properties/controls/radioset/radioset.jsx index 22c7189004..b623ec1895 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/radioset/radioset.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/radioset/radioset.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import * as ControlUtils from "./../../util/control-utils"; import * as PropertyUtils from "./../../util/property-utils"; import * as ConditionsUtils from "./../../ui-conditions/conditions-utils.js"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import { RadioButton, RadioButtonGroup } from "@carbon/react"; import classNames from "classnames"; import { MESSAGE_KEYS, STATES, UPDATE_TYPE } from "./../../constants/constants.js"; @@ -234,7 +235,9 @@ class RadiosetControl extends React.Component { ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/readonly/readonly.jsx b/canvas_modules/common-canvas/src/common-properties/controls/readonly/readonly.jsx index 9b4502f1e4..fa28bf316e 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/readonly/readonly.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/readonly/readonly.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,7 +144,7 @@ class ReadonlyControl extends React.Component { > {this.props.tableControl ? null : this.props.controlItem} {display} - + ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/readonlytable/readonlytable.jsx b/canvas_modules/common-canvas/src/common-properties/controls/readonlytable/readonlytable.jsx index 73406a93d8..09c99e3d3a 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/readonlytable/readonlytable.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/readonlytable/readonlytable.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ class ReadonlyTableControl extends AbstractTable {
{table}
- + ); return ( diff --git a/canvas_modules/common-canvas/src/common-properties/controls/selectcolumns/selectcolumns.jsx b/canvas_modules/common-canvas/src/common-properties/controls/selectcolumns/selectcolumns.jsx index bcab06849d..fc61e7b23a 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/selectcolumns/selectcolumns.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/selectcolumns/selectcolumns.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -177,7 +177,7 @@ class SelectColumnsControl extends AbstractTable {
{table}
- + ); diff --git a/canvas_modules/common-canvas/src/common-properties/controls/slider/slider.jsx b/canvas_modules/common-canvas/src/common-properties/controls/slider/slider.jsx index e54d1305e0..39d98138c5 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/slider/slider.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/slider/slider.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import { v4 as uuid4 } from "uuid"; import * as ControlUtils from "../../util/control-utils"; import ValidationMessage from "../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import { STATES, MESSAGE_KEYS } from "../../constants/constants"; import { formatMessage } from "./../../util/property-utils"; @@ -51,8 +52,8 @@ class SliderControl extends React.Component { const step = this.props.control.increment ? this.props.control.increment : 1; return ( - ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/someofselect/someofselect.jsx b/canvas_modules/common-canvas/src/common-properties/controls/someofselect/someofselect.jsx index 8be5f93d1c..2348743fce 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/someofselect/someofselect.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/someofselect/someofselect.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import { connect } from "react-redux"; import FlexibleTable from "../../components/flexible-table"; import * as ControlUtils from "./../../util/control-utils"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import classNames from "classnames"; import { isEqual, intersection } from "lodash"; @@ -100,7 +101,7 @@ class SomeofselectControl extends React.Component { return ( ); diff --git a/canvas_modules/common-canvas/src/common-properties/controls/structureeditor/structureeditor.jsx b/canvas_modules/common-canvas/src/common-properties/controls/structureeditor/structureeditor.jsx index 0e662f4070..a6902e01cd 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/structureeditor/structureeditor.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/structureeditor/structureeditor.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import { connect } from "react-redux"; import ControlFactory from "../control-factory"; import * as ControlUtils from "./../../util/control-utils"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import classNames from "classnames"; import { STATES } from "./../../constants/constants.js"; import { cloneDeep } from "lodash"; @@ -149,10 +150,10 @@ class StructureEditorControl extends React.Component { return ( ); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/structurelisteditor/structurelisteditor.jsx b/canvas_modules/common-canvas/src/common-properties/controls/structurelisteditor/structurelisteditor.jsx index 69adfb0df9..307c5569da 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/structurelisteditor/structurelisteditor.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/structurelisteditor/structurelisteditor.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ class StructurelisteditorControl extends AbstractTable {
{table}
- + ); const onPanelContainer = this.getOnPanelContainer(this.props.selectedRows); diff --git a/canvas_modules/common-canvas/src/common-properties/controls/structuretable/structuretable.jsx b/canvas_modules/common-canvas/src/common-properties/controls/structuretable/structuretable.jsx index cbcbc91842..92d2def004 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/structuretable/structuretable.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/structuretable/structuretable.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -173,7 +173,7 @@ class StructureTableControl extends AbstractTable {
{table}
- + ); const onPanelContainer = this.getOnPanelContainer(this.props.selectedRows); diff --git a/canvas_modules/common-canvas/src/common-properties/controls/textarea/textarea.jsx b/canvas_modules/common-canvas/src/common-properties/controls/textarea/textarea.jsx index e3c8c420e4..6439ae72ad 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/textarea/textarea.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/textarea/textarea.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 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,6 +19,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import { TextArea } from "@carbon/react"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { formatMessage } from "./../../util/property-utils"; import { STATES } from "./../../constants/constants.js"; @@ -87,7 +88,7 @@ class TextareaControl extends React.Component { readOnly={this.props.readOnly} aria-label={this.props.control.labelVisible ? null : this.props.control?.label?.text} /> - + ); } else { textArea = ( @@ -129,11 +130,17 @@ class TextareaControl extends React.Component { {textArea} ); } - const className = classNames("properties-textarea", { "hide": hidden }, this.props.messageInfo ? this.props.messageInfo.type : null); + const className = classNames("properties-textarea", { "hide": hidden }, + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); return (
{display} - +
); diff --git a/canvas_modules/common-canvas/src/common-properties/controls/textfield/textfield.jsx b/canvas_modules/common-canvas/src/common-properties/controls/textfield/textfield.jsx index 92ac45812e..5aa4dff105 100755 --- a/canvas_modules/common-canvas/src/common-properties/controls/textfield/textfield.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/textfield/textfield.jsx @@ -20,6 +20,7 @@ import { connect } from "react-redux"; import { TextInput } from "@carbon/react"; import ReadonlyControl from "./../readonly"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { formatMessage } from "./../../util/property-utils"; import { STATES } from "./../../constants/constants.js"; @@ -79,7 +80,7 @@ class TextfieldControl extends React.Component { truncated = value.length !== 0 && value.length !== this.props.value.length; } const className = classNames("properties-textfield", "properties-input-control", { "hide": hidden }, - this.props.messageInfo ? this.props.messageInfo.type : null); + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); let textInput = null; if (truncated) { @@ -97,7 +98,7 @@ class TextfieldControl extends React.Component { tableControl={this.props.tableControl} /> {/* // TODO this could conflict with the below ValidationMessage. */} - + ); } else { const validationProps = ControlUtils.getValidationProps(this.props.messageInfo, this.props.tableControl); @@ -139,7 +140,7 @@ class TextfieldControl extends React.Component { return (
{textInput} - +
); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/timefield/timefield.jsx b/canvas_modules/common-canvas/src/common-properties/controls/timefield/timefield.jsx index 23554364d3..8ea5aa3919 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/timefield/timefield.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/timefield/timefield.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import { parse, format, isValid } from "date-fns"; import classNames from "classnames"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { DEFAULT_TIME_FORMAT, STATES } from "./../../constants/constants"; @@ -71,7 +72,7 @@ class TimefieldControl extends React.Component { return null; // Do not render hidden controls } const className = classNames("properties-timefield", "properties-input-control", { "hide": hidden }, - this.props.messageInfo ? this.props.messageInfo.type : null); + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); const validationProps = ControlUtils.getValidationProps(this.props.messageInfo, this.props.tableControl); return (
@@ -89,7 +90,7 @@ class TimefieldControl extends React.Component { readOnly={this.props.readOnly} aria-label={this.props.control.labelVisible ? null : this.props.control?.label?.text} /> - +
); } diff --git a/canvas_modules/common-canvas/src/common-properties/controls/toggle/toggle.jsx b/canvas_modules/common-canvas/src/common-properties/controls/toggle/toggle.jsx index ee360dea20..37431d5b89 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/toggle/toggle.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/toggle/toggle.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 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,6 +19,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import { Toggle } from "@carbon/react"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { STATES, MESSAGE_KEYS } from "./../../constants/constants.js"; import classNames from "classnames"; @@ -61,7 +62,8 @@ class ToggleControl extends React.Component { aria-labelledby={`${this.props.propertyId?.name}-toggle-label`} readOnly={this.props.readOnly} />); - const className = classNames("properties-toggle", { "hide": hidden }, this.props.messageInfo ? this.props.messageInfo.type : null); + const className = classNames("properties-toggle", { "hide": hidden }, + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); return (
{ @@ -74,7 +76,7 @@ class ToggleControl extends React.Component { ) } {toggleControl} - +
); diff --git a/canvas_modules/common-canvas/src/common-properties/controls/toggletext/toggletext.jsx b/canvas_modules/common-canvas/src/common-properties/controls/toggletext/toggletext.jsx index db93a193de..6a22cc1147 100644 --- a/canvas_modules/common-canvas/src/common-properties/controls/toggletext/toggletext.jsx +++ b/canvas_modules/common-canvas/src/common-properties/controls/toggletext/toggletext.jsx @@ -19,6 +19,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import { Button } from "@carbon/react"; import ValidationMessage from "./../../components/validation-message"; +import { doesErrorMessageApplyToCell } from "../../ui-conditions/validation-utils.js"; import * as ControlUtils from "./../../util/control-utils"; import { formatMessage } from "./../../util/property-utils"; import { STATES, MESSAGE_KEYS } from "./../../constants/constants.js"; @@ -90,7 +91,8 @@ class ToggletextControl extends React.Component { ); } - const className = classNames("properties-toggletext", { "hide": hidden }, this.props.messageInfo ? this.props.messageInfo.type : null); + const className = classNames("properties-toggletext", { "hide": hidden }, + this.props.messageInfo && doesErrorMessageApplyToCell(this.props.propertyId, this.props.messageInfo) ? this.props.messageInfo.type : null); return (
{this.props.tableControl ? null : this.props.controlItem} {button} - +
); } diff --git a/canvas_modules/common-canvas/src/common-properties/properties-controller.js b/canvas_modules/common-canvas/src/common-properties/properties-controller.js index 136e53f7f3..593465412a 100644 --- a/canvas_modules/common-canvas/src/common-properties/properties-controller.js +++ b/canvas_modules/common-canvas/src/common-properties/properties-controller.js @@ -315,7 +315,9 @@ export default class PropertiesController { // with the the name and col set from the definition ref. const baseId = conditionsUtil.getParamRefPropertyId(conditionKey); // baseId.col and propertyId.col can be undefined - if (baseId.name === propertyId.name && baseId.col === propertyId.col) { + if (baseId.name === propertyId.name && baseId.col === propertyId.col && + (!baseId.propertyId || (baseId.propertyId && baseId.propertyId.col === propertyId.propertyId?.col)) + ) { retCond = retCond.concat(conditionDefinitions[conditionKey]); } } diff --git a/canvas_modules/common-canvas/src/common-properties/properties-store.js b/canvas_modules/common-canvas/src/common-properties/properties-store.js index b6766085b5..c88423fd8a 100644 --- a/canvas_modules/common-canvas/src/common-properties/properties-store.js +++ b/canvas_modules/common-canvas/src/common-properties/properties-store.js @@ -15,7 +15,7 @@ */ import { createStore, combineReducers } from "redux"; -import { has, get, isEqual, cloneDeep } from "lodash"; +import { has, get, isEqual, cloneDeep, difference } from "lodash"; import { setTearsheetState } from "./actions"; import { setPropertyValues, updatePropertyValue, removePropertyValue } from "./actions"; import { setControlStates, updateControlState } from "./actions"; @@ -42,7 +42,7 @@ import wideFlyoutPrimaryButtonDisableReducer from "./reducers/wide-flyout-primar import propertiesSettingsReducer from "./reducers/properties-settings"; import tearsheetStatesReducer from "./reducers/tearsheet-states"; import * as PropertyUtils from "./util/property-utils.js"; -import { CONDITION_MESSAGE_TYPE, MESSAGE_KEYS } from "./constants/constants.js"; +import { CONDITION_MESSAGE_TYPE, MESSAGE_KEYS, DEFAULT_ERROR_MESSAGE_KEYS } from "./constants/constants.js"; /* eslint max-depth: ["error", 6] */ @@ -275,9 +275,33 @@ export default class PropertiesStore { if (typeof propertyId.row !== "undefined" && controlMsg) { controlMsg = controlMsg[propertyId.row.toString()]; if (typeof propertyId.col !== "undefined" && controlMsg) { - return controlMsg[propertyId.col.toString()]; // return cell message - } - if (controlMsg && controlMsg.text) { + const cellMessage = controlMsg[propertyId.col.toString()]; // return cell message + const cellErrors = cellMessage ? difference(Object.keys(cellMessage), DEFAULT_ERROR_MESSAGE_KEYS) : []; + if (cellErrors.length > 0) { + delete cellMessage.tableText; + let tableReturnMessage = null; + let errorMsgCount = 0; + let warningMsgCount = 0; + cellErrors.forEach((cellError) => { + const countedCellMessages = this._countTableCellErrors(cellMessage[cellError]); + errorMsgCount += countedCellMessages.errorMsgCount; + warningMsgCount += countedCellMessages.warningMsgCount; + tableReturnMessage = countedCellMessages.returnMessage; + }); + const tableErrorMsg = this._getTableReturnMessage(errorMsgCount, warningMsgCount, tableReturnMessage, intl); + // Only get the updated messages + const messages = {}; + if (tableErrorMsg) { + messages.type = tableErrorMsg.type; + messages.text = tableErrorMsg.text; + if (tableErrorMsg.tableText) { + messages.tableText = tableErrorMsg.tableText; + } + } + return Object.assign({}, cellMessage, messages); + } + return cellMessage; + } else if (controlMsg && controlMsg.text) { return controlMsg; } } @@ -298,8 +322,7 @@ export default class PropertiesStore { return controlMessage; } - // get the table cell level error messages. if more than one cell is in error, return summary message; - _getTableCellErrors(controlMsg, intl) { + _countTableCellErrors(controlMsg) { let returnMessage = null; let errorMsgCount = 0; let warningMsgCount = 0; @@ -334,13 +357,18 @@ export default class PropertiesStore { } } } - if ((errorMsgCount + warningMsgCount) !== 1 && returnMessage) { + return { errorMsgCount, warningMsgCount, returnMessage }; + } + + _getTableReturnMessage(errorMsgCount, warningMsgCount, returnMessage, intl) { + delete returnMessage?.tableText; + if ((errorMsgCount + warningMsgCount) > 1 && returnMessage) { returnMessage.type = (errorMsgCount > 0) ? CONDITION_MESSAGE_TYPE.ERROR : CONDITION_MESSAGE_TYPE.WARNING; - returnMessage.text = (errorMsgCount > 0) + returnMessage.tableText = (errorMsgCount > 0) ? PropertyUtils.formatMessage(intl, MESSAGE_KEYS.TABLE_SUMMARY_ERROR, { errorMsgCount: errorMsgCount }) : ""; - returnMessage.text += (warningMsgCount > 0) + returnMessage.tableText += (warningMsgCount > 0) ? PropertyUtils.formatMessage(intl, MESSAGE_KEYS.TABLE_SUMMARY_WARNING, { warningMsgCount: warningMsgCount }) : ""; @@ -348,6 +376,12 @@ export default class PropertiesStore { return returnMessage; } + // get the table cell level error messages. if more than one cell is in error, return summary message; + _getTableCellErrors(controlMsg, intl) { + const { errorMsgCount, warningMsgCount, returnMessage } = this._countTableCellErrors(controlMsg); + return this._getTableReturnMessage(errorMsgCount, warningMsgCount, returnMessage, intl); + } + getErrorMessages() { const state = this.store.getState(); return PropertyUtils.copy(state.errorMessagesReducer); diff --git a/canvas_modules/common-canvas/src/common-properties/reducers/error-messages.js b/canvas_modules/common-canvas/src/common-properties/reducers/error-messages.js index 06e4d5f272..2d24c60d20 100644 --- a/canvas_modules/common-canvas/src/common-properties/reducers/error-messages.js +++ b/canvas_modules/common-canvas/src/common-properties/reducers/error-messages.js @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,8 @@ */ import { UPDATE_ERROR_MESSAGE, SET_ERROR_MESSAGES, CLEAR_ERROR_MESSAGE } from "../actions"; -import { isEmpty } from "lodash"; +import { DEFAULT_ERROR_MESSAGE_KEYS } from "../constants/constants"; +import { isEmpty, difference } from "lodash"; /* eslint max-depth: ["error", 6] */ /* @@ -31,19 +32,8 @@ function messages(state = {}, action) { newState[propertyId.name] = {}; } if (typeof propertyId.row !== "undefined") { - const strRow = propertyId.row.toString(); - if (typeof newState[propertyId.name][strRow] === "undefined") { - newState[propertyId.name][strRow] = {}; - } - if (typeof propertyId.col !== "undefined") { - const strCol = propertyId.col.toString(); - if (typeof newState[propertyId.name][strRow][strCol] === "undefined") { - newState[propertyId.name][strRow][strCol] = {}; - } - newState[propertyId.name][strRow][strCol] = action.message.value; - } else { - newState[propertyId.name][strRow] = Object.assign({}, action.message.value); - } + const newNameState = newState[propertyId.name]; + updateNestedPropertyValue(propertyId, newNameState, action.message.value); } else { newState[propertyId.name] = Object.assign({}, action.message.value); } @@ -51,22 +41,30 @@ function messages(state = {}, action) { } case CLEAR_ERROR_MESSAGE: { const newState = state; - if (newState[action.message.propertyId.name]) { - if (typeof action.message.propertyId.row !== "undefined") { - if (typeof action.message.propertyId.col !== "undefined") { - delete newState[action.message.propertyId.name][action.message.propertyId.row][action.message.propertyId.col]; + const propertyId = action.message.propertyId; + if (newState[propertyId.name]) { + if (typeof propertyId.row !== "undefined") { + if (typeof propertyId.col !== "undefined") { + if (typeof propertyId.propertyId !== "undefined" && typeof propertyId.propertyId.row !== "undefined") { // clear subcell + clearNestedPropertyMessage(propertyId.propertyId, newState[propertyId.name][propertyId.row][propertyId.col]); + clearCellMessage(propertyId.col, newState[propertyId.name][propertyId.row]); + updateParentErrorMessage(propertyId.col, propertyId.row, newState[propertyId.name]); + } else { + delete newState[propertyId.name][propertyId.row][propertyId.col]; + } } else { - delete newState[action.message.propertyId.name][action.message.propertyId.row]; + delete newState[propertyId.name][propertyId.row]; } } else { - delete newState[action.message.propertyId.name].type; - delete newState[action.message.propertyId.name].text; - delete newState[action.message.propertyId.name].validation_id; - delete newState[action.message.propertyId.name].required; - delete newState[action.message.propertyId.name].propertyId; - delete newState[action.message.propertyId.name].displayError; - if (isEmpty(newState[action.message.propertyId.name])) { - delete newState[action.message.propertyId.name]; + delete newState[propertyId.name].type; + delete newState[propertyId.name].text; + delete newState[propertyId.name].validation_id; + delete newState[propertyId.name].required; + delete newState[propertyId.name].propertyId; + delete newState[propertyId.name].displayError; + delete newState[propertyId.name].tableText; + if (isEmpty(newState[propertyId.name])) { + delete newState[propertyId.name]; } } } @@ -81,4 +79,81 @@ function messages(state = {}, action) { } } +function updateNestedPropertyValue(propertyId, newState, value) { + if (typeof propertyId.row !== "undefined") { + const strRow = propertyId.row.toString(); + + if (typeof newState[strRow] === "undefined") { + newState[strRow] = {}; + } else if (typeof newState[strRow].displayError !== "undefined") { + delete newState[strRow].displayError; + } + delete newState[strRow].tableText; + + if (typeof propertyId.col !== "undefined") { + const strCol = propertyId.col.toString(); + if (typeof newState[strRow][strCol] === "undefined") { + newState[strRow][strCol] = {}; + } else if (typeof newState[strRow][strCol].displayError !== "undefined") { + delete newState[strRow][strCol].displayError; + } + delete newState[strRow][strCol].tableText; + newState[strRow][strCol] = Object.assign({}, newState[strRow][strCol], value); + if (typeof propertyId.propertyId !== "undefined") { + updateNestedPropertyValue(propertyId.propertyId, newState[strRow][strCol], Object.assign({}, value)); + } + } else if (typeof propertyId.propertyId !== "undefined") { // nested structureeditor + updateNestedPropertyValue(propertyId.propertyId, newState[strRow], value); + } else { + newState[strRow] = Object.assign({}, newState[strRow], value); + } + } +} + +function clearNestedPropertyMessage(propertyId, newState) { + if (typeof propertyId.col !== "undefined") { + if (typeof propertyId.propertyId !== "undefined" && typeof propertyId.propertyId.row !== "undefined") { // clear subcell + clearNestedPropertyMessage(propertyId.propertyId, newState[propertyId.row][propertyId.col]); + clearCellMessage(propertyId.col, newState[propertyId.row]); + updateParentErrorMessage(propertyId.col, propertyId.row, newState); + } else { + delete newState[propertyId.row][propertyId.col]; + clearCellMessage(propertyId.row, newState); + } + } else { + delete newState[propertyId.row]; + delete newState.tableText; + } +} + +// Check that no cells have error before deleting the error from the parent row/col +function clearCellMessage(col, newState) { + const cellErrors = difference(Object.keys(newState[col]), DEFAULT_ERROR_MESSAGE_KEYS); + if (cellErrors.length === 0) { + delete newState[col]; + delete newState.tableText; + } +} + +// Once an error is cleared from the nested cell, and there is still another error present, +// ensure that the parent error reflects the error still present, instead of the error that was removed +function updateParentErrorMessage(errIdx, delIdx, newState) { + // delete newState.tableText; + const errorState = newState[delIdx]; + const cellErrors = difference(Object.keys(errorState[errIdx]), DEFAULT_ERROR_MESSAGE_KEYS); + if (typeof errorState[errIdx].type !== "undefined") { + const remainingCellErrorIdx = cellErrors[0]; // Set the parent error to the first cell + const cellError = difference(Object.keys(errorState[errIdx][remainingCellErrorIdx]), DEFAULT_ERROR_MESSAGE_KEYS); + if (cellError.length === 0) { + const remainingError = errorState[errIdx][remainingCellErrorIdx]; + delete remainingError.tableText; + errorState[errIdx] = Object.assign({}, errorState[errIdx], remainingError); + } else { // go deeper to find the nested error + const remainingError = errorState[errIdx][remainingCellErrorIdx][cellError[0]]; + delete remainingError.tableText; + errorState[errIdx] = Object.assign({}, errorState[errIdx], remainingError); + } + } +} + export default messages; diff --git a/canvas_modules/common-canvas/src/common-properties/ui-conditions/conditions-utils.js b/canvas_modules/common-canvas/src/common-properties/ui-conditions/conditions-utils.js index e508ac6b7c..b5b030e864 100644 --- a/canvas_modules/common-canvas/src/common-properties/ui-conditions/conditions-utils.js +++ b/canvas_modules/common-canvas/src/common-properties/ui-conditions/conditions-utils.js @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Elyra Authors + * Copyright 2017-2025 Elyra Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -201,6 +201,13 @@ function validateInput(inPropertyId, controller, showErrors = true) { propertyId.col = 0; _validateInput(propertyId, controller, control, showErrors); } + } else if (typeof propertyId.row !== "undefined" && control.controlType === "selectcolumns") { // validate each row in the array within a table. + for (let rowIndex = 0; rowIndex < controlValue.length; rowIndex++) { + const newNestedPropertyId = {}; + newNestedPropertyId.row = rowIndex; + propertyId.propertyId = newNestedPropertyId; + _validateInput(propertyId, controller, control, showErrors); + } } } else { @@ -549,10 +556,21 @@ function getParamRefPropertyId(paramRef, controlPropertyId) { const offset = paramRef.indexOf("["); if (offset > -1) { baseParam.name = paramRef.substring(0, offset); - baseParam.col = parseInt(paramRef.substring(offset + 1, paramRef.indexOf("]")), 10); + const colOffset = paramRef.substring(offset + 1); + baseParam.col = parseInt(colOffset.substring(0, paramRef.indexOf("]")), 10); + + const nestedOffset = colOffset.indexOf("["); + if (nestedOffset > -1) { + const nestedColOffset = colOffset.substring(nestedOffset + 1); + const nestedCol = parseInt(nestedColOffset.substring(0, paramRef.indexOf("]")), 10); + baseParam.propertyId = { col: nestedCol }; + } } if (controlPropertyId) { baseParam.row = controlPropertyId.row; + if (controlPropertyId.propertyId) { + baseParam.propertyId = controlPropertyId.propertyId; + } } return baseParam; } @@ -710,10 +728,11 @@ function _validateInput(propertyId, controller, control, showErrors) { errorSet = false; } // Before setting an error message for table cell, clear the error message for table (if any) + // only if there are no nested propertyId if (typeof msgPropertyId.row !== "undefined" || typeof msgPropertyId.col !== "undefined") { const tablePropertyId = controller.convertPropertyId(msgPropertyId.name); const tableErrorMessage = controller.getErrorMessage(tablePropertyId); - if (tableErrorMessage !== null) { + if (tableErrorMessage !== null && !msgPropertyId.propertyId) { controller.updateErrorMessage(tablePropertyId, null); } } @@ -723,7 +742,7 @@ function _validateInput(propertyId, controller, control, showErrors) { if (isError) { errorSet = true; } - } else if ((!isError && !errorSet) || (!isError && errorSet)) { + } else if (!isError) { const msg = controller.getErrorMessage(msgPropertyId, false, false, false); if (!isEmpty(msg) && (msg.validation_id === errorMessage.validation_id)) { controller.updateErrorMessage(msgPropertyId, DEFAULT_VALIDATION_MESSAGE); diff --git a/canvas_modules/common-canvas/src/common-properties/ui-conditions/validation-utils.js b/canvas_modules/common-canvas/src/common-properties/ui-conditions/validation-utils.js new file mode 100644 index 0000000000..c73066c137 --- /dev/null +++ b/canvas_modules/common-canvas/src/common-properties/ui-conditions/validation-utils.js @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Elyra Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** +* Check if the given propertyId is a nested control, and if the messageInfo applies to that specific cell +* +* @param {object} propertyId. required +* @param {object} messageInfo. required +*/ +function doesErrorMessageApplyToCell(propertyId, messageInfo) { + if (propertyId?.propertyId && messageInfo.propertyId?.propertyId) { + const currentCellRow = propertyId?.propertyId?.row; + const currentCellCol = propertyId?.propertyId?.col; + const errorCellRow = messageInfo[currentCellRow]; + if (!errorCellRow || (typeof currentCellCol !== "undefined" && typeof errorCellRow[currentCellCol] === "undefined")) { + return false; + } + } else if (typeof propertyId?.index !== "undefined" && messageInfo.propertyId?.propertyId) { // selectColumns + const currentCellRow = propertyId.index; + const errorCellRow = messageInfo[currentCellRow]; + if (!errorCellRow) { + return false; + } + } + return true; +} + +export { + doesErrorMessageApplyToCell +}; diff --git a/canvas_modules/harness/test_resources/parameterDefs/nestedConditions_paramDef.json b/canvas_modules/harness/test_resources/parameterDefs/nestedConditions_paramDef.json new file mode 100644 index 0000000000..53fa69f24b --- /dev/null +++ b/canvas_modules/harness/test_resources/parameterDefs/nestedConditions_paramDef.json @@ -0,0 +1,422 @@ +{ + "titleDefinition": { + "title": "Nested cells" + }, + "current_parameters": { + "nested_table": [ + [ + ["list1", "list2"], + ["Age", "Na"], + ["orange", "green"], + [["Sex", "red"], ["BP", "blue"]], + [["Hello", 1], ["world", 2]] + ] + ] + }, + "parameters": [ + { + "id": "nested_table", + "type": "array[table_controls]" + } + ], + "complex_types": [ + { + "id": "table_controls", + "parameters": [ + { + "id": "list", + "type": "array[string]", + "required": true + }, + { + "id": "select_columns", + "type": "array[string]", + "role": "column" + }, + { + "id": "someofselect", + "type": "array[string]", + "enum": [ + "red", + "orange", + "yellow", + "green", + "blue", + "purple" + ] + }, + { + "id": "structuretable", + "type": "map[string, nestedStructureTable]", + "role": "column" + }, + { + "id": "structurelisteditor", + "type": "array[nestedStructureListEditor]" + } + ] + }, + { + "id": "nestedStructureTable", + "parameters": [ + { + "id": "field", + "type": "string", + "role": "column" + }, + { + "id": "oneofselect", + "enum": [ + "red", + "orange", + "yellow", + "green", + "blue", + "purple" + ] + } + ] + }, + { + "id": "nestedStructureListEditor", + "parameters": [ + { + "id": "textfield", + "type": "string", + "required": true + }, + { + "id": "numberfield", + "type": "integer", + "required": true + } + ] + } + ], + "uihints": { + "id": "nested cells test", + "icon": "images/default.svg", + "label": { + "default": "Nested cells test" + }, + "editor_size": "large", + "parameter_info": [ + { + "parameter_ref": "nested_table", + "label": { + "default": "Table within table" + }, + "description": { + "default": "This example displays conditions within a nested table.", + "placement": "on_panel" + } + } + ], + "complex_type_info": [ + { + "complex_type_ref": "table_controls", + "parameters": [ + { + "parameter_ref": "list", + "width": 10, + "label": { + "default": "list" + }, + "description": { + "default": "Enter 'error' in a cell to display an error", + "placement": "on_panel" + }, + "edit_style": "on_panel", + "control": "list" + }, + { + "parameter_ref": "select_columns", + "width": 10, + "label": { + "default": "selectColumns" + }, + "description": { + "default": "Select the column 'K' to display an error", + "placement": "on_panel" + }, + "edit_style": "subpanel" + }, + { + "parameter_ref": "someofselect", + "width": 10, + "label": { + "default": "someofselect" + }, + "description": { + "default": "Select 'orange' below to display an error. It is preselected initially and error will not be shown until the control is modified", + "placement": "on_panel" + }, + "edit_style": "subpanel" + }, + { + "parameter_ref": "structuretable", + "width": 10, + "label": { + "default": "structuretable" + }, + "description": { + "default": "Select 'green' in the second column to display an error", + "placement": "on_panel" + }, + "edit_style": "on_panel" + }, + { + "parameter_ref": "structurelisteditor", + "width": 10, + "label": { + "default": "structurelisteditor" + }, + "description": { + "default": "Enter 'error' in a cell in the first column, or enter a number less than 1 in the second column to display an error. Clearing a cell error will update the table error to show the remaining error present in the table", + "placement": "on_panel" + }, + "edit_style": "subpanel" + } + ] + }, + { + "complex_type_ref": "nestedStructureTable", + "moveable_rows": true, + "parameters": [ + { + "parameter_ref": "field", + "label": { + "default": "field" + } + }, + { + "parameter_ref": "oneofselect", + "label": { + "default": "oneofselect" + } + } + ] + }, + { + "complex_type_ref": "nestedStructureListEditor", + "moveable_rows": true, + "parameters": [ + { + "parameter_ref": "textfield", + "label": { + "default": "textfield" + }, + "control": "textfield" + }, + { + "parameter_ref": "numberfield", + "label": { + "default": "numberfield" + }, + "control": "numberfield" + } + ] + } + ], + "group_info": [ + { + "id": "numberfield-table-panels", + "label": { + "default": "Table" + }, + "type": "controls", + "parameter_refs": [ + "nested_table" + ] + } + ] + }, + "conditions": [ + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[0]", + "message": { + "default": "Cannot be 'error'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[0]", + "op": "notEquals", + "value": "error" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[1]", + "message": { + "default": "Cannot contain 'K'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[1]", + "op": "notContains", + "value": "K" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[2]", + "message": { + "default": "Cannot select 'orange'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[2]", + "op": "notContains", + "value": "orange" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[3]", + "message": { + "default": "Cannot select 'green'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[3][1]", + "op": "notEquals", + "value": "green" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[4][0]", + "message": { + "default": "Cannot be 'error'" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[4][0]", + "op": "notEquals", + "value": "error" + } + } + } + }, + { + "validation": { + "fail_message": { + "type": "error", + "focus_parameter_ref": "nested_table[4][1]", + "message": { + "default": "Must be greater than 0" + } + }, + "evaluate": { + "condition": { + "parameter_ref": "nested_table[4][1]", + "op": "greaterThan", + "value": "0" + } + } + } + } + ], + "dataset_metadata": [ + { + "fields": [ + { + "name": "Age", + "type": "integer", + "metadata": { + "description": "", + "measure": "range", + "modeling_role": "input" + } + }, + { + "name": "Sex", + "type": "string", + "metadata": { + "description": "", + "measure": "ordered_set", + "modeling_role": "input" + } + }, + { + "name": "BP", + "type": "string", + "metadata": { + "description": "", + "measure": "discrete", + "modeling_role": "input" + } + }, + { + "name": "Cholesterol", + "type": "string", + "metadata": { + "description": "", + "measure": "set", + "modeling_role": "input" + } + }, + { + "name": "Na", + "type": "double", + "metadata": { + "description": "", + "measure": "flag", + "modeling_role": "input" + } + }, + { + "name": "K", + "type": "double", + "metadata": { + "description": "", + "measure": "collection", + "modeling_role": "input" + } + }, + { + "name": "Drug", + "type": "string", + "metadata": { + "description": "", + "measure": "geospatial", + "modeling_role": "input" + } + }, + { + "name": "Ag", + "type": "integer", + "metadata": { + "description": "", + "measure": "", + "modeling_role": "input" + } + } + ] + } + ] +} diff --git a/canvas_modules/harness/test_resources/parameterDefs/numberfield_paramDef.json b/canvas_modules/harness/test_resources/parameterDefs/numberfield_paramDef.json index 13953acf1f..134e26a917 100644 --- a/canvas_modules/harness/test_resources/parameterDefs/numberfield_paramDef.json +++ b/canvas_modules/harness/test_resources/parameterDefs/numberfield_paramDef.json @@ -180,7 +180,7 @@ "description": { "default": "Readonly numberfield with parameter value set to '10'" }, - "class_name": "numberfield-control-class", + "class_name": "numberfield-readonly-control-class", "helper_text": { "default": "Readonly numberfield with parameter value set to '10'" },