diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/AddNewRow1_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/AddNewRow1_spec.js index ac1fb9c585..2430a981bc 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/AddNewRow1_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/AddNewRow1_spec.js @@ -1,5 +1,7 @@ import { agHelper, + entityExplorer, + locators, propPane, table, } from "../../../../../support/Objects/ObjectsCore"; @@ -185,5 +187,56 @@ describe( propPane.TogglePropertyState("Allow adding a row", "Off"); cy.get(".t--add-new-row").should("not.exist"); }); + + it("1.8. should show the selectOptions data of a new row select cell when no data in the table", () => { + entityExplorer.DragDropWidgetNVerify("tablewidgetv2", 600, 750); + EditorNavigation.SelectEntityByName("Table2", EntityType.Widget); + + // add base data + propPane.ToggleJSMode("Table data", "On"); + propPane.UpdatePropertyFieldValue( + "Table data", + `{{[ + { + role: "", + } + ]}}`, + ); + + // remove all table data + propPane.UpdatePropertyFieldValue("Table data", `[]`); + + // allow adding a row + propPane.TogglePropertyState("Allow adding a row", "On"); + + // Edit role column to select type + table.ChangeColumnType("role", "Select", "v2"); + table.EditColumn("role", "v2"); + propPane.TogglePropertyState("Editable", "On"); + + // Add data to select options + agHelper.UpdateCodeInput( + locators._controlOption, + ` + {{ + [ + {"label": "Software Engineer", + "value": 1,}, + {"label": "Product Manager", + "value": 2,}, + {"label": "UX Designer", + "value": 3,} + ] + }} + `, + ); + + table.AddNewRow(); + agHelper.GetNClick(commonlocators.singleSelectWidgetButtonControl); + agHelper + .GetElement(commonlocators.singleSelectWidgetMenuItem) + .contains("Software Engineer") + .click(); + }); }, ); diff --git a/app/client/packages/dsl/src/migrate/index.ts b/app/client/packages/dsl/src/migrate/index.ts index 75ce6f7b6d..9f54258ed4 100644 --- a/app/client/packages/dsl/src/migrate/index.ts +++ b/app/client/packages/dsl/src/migrate/index.ts @@ -91,9 +91,10 @@ import { migrateTableServerSideFiltering } from "./migrations/086-migrate-table- import { migrateChartwidgetCustomEchartConfig } from "./migrations/087-migrate-chart-widget-customechartdata"; import { migrateCustomWidgetDynamicHeight } from "./migrations/088-migrate-custom-widget-dynamic-height"; import { migrateTableWidgetV2CurrentRowInValidationsBinding } from "./migrations/089-migrage-table-widget-v2-currentRow-binding"; +import { migrateTableComputeValueBinding } from "./migrations/090-migrate-table-compute-value-binding"; import type { DSLWidget } from "./types"; -export const LATEST_DSL_VERSION = 91; +export const LATEST_DSL_VERSION = 92; export const calculateDynamicHeight = () => { const DEFAULT_GRID_ROW_HEIGHT = 10; @@ -617,6 +618,11 @@ const migrateVersionedDSL = async (currentDSL: DSLWidget, newPage = false) => { } if (currentDSL.version === 90) { + currentDSL = migrateTableComputeValueBinding(currentDSL); + currentDSL.version = 91; + } + + if (currentDSL.version === 91) { /** * This is just a version bump without any migration * History: With this PR: https://github.com/appsmithorg/appsmith/pull/38391 diff --git a/app/client/packages/dsl/src/migrate/migrations/090-migrate-table-compute-value-binding.ts b/app/client/packages/dsl/src/migrate/migrations/090-migrate-table-compute-value-binding.ts new file mode 100644 index 0000000000..0472d1e7a4 --- /dev/null +++ b/app/client/packages/dsl/src/migrate/migrations/090-migrate-table-compute-value-binding.ts @@ -0,0 +1,43 @@ +import type { DSLWidget } from "../types"; +import { isDynamicValue } from "../utils"; + +/** + * This migration updates the table compute value bindings to use the new robust fallback mechanism + * Old format: {{table.processedTableData.map((currentRow, currentIndex) => ( value ))}} + * New format: {{(() => { const tableData = table.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (value)) : value })()}} + */ +export const migrateTableComputeValueBinding = (currentDSL: DSLWidget) => { + if (currentDSL.type === "TABLE_WIDGET_V2") { + // Migrate primary columns compute values + if (currentDSL.primaryColumns) { + Object.keys(currentDSL.primaryColumns).forEach((columnKey) => { + const column = currentDSL.primaryColumns[columnKey]; + + if (column.computedValue && isDynamicValue(column.computedValue)) { + const oldBindingPrefix = `{{${currentDSL.widgetName}.processedTableData.map((currentRow, currentIndex) => (`; + const oldBindingSuffix = `))}}`; + + if (column.computedValue.includes(oldBindingPrefix)) { + // Extract the value expression from between the old binding + const valueExpression = column.computedValue.substring( + oldBindingPrefix.length, + column.computedValue.length - oldBindingSuffix.length, + ); + + // Create new binding with fallback mechanism + column.computedValue = `{{(() => { const tableData = ${currentDSL.widgetName}.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (${valueExpression})) : ${valueExpression} })()}}`; + } + } + }); + } + } + + // Recursively migrate children + if (currentDSL.children && currentDSL.children.length > 0) { + currentDSL.children = currentDSL.children.map((child: DSLWidget) => + migrateTableComputeValueBinding(child), + ); + } + + return currentDSL; +}; diff --git a/app/client/packages/dsl/src/migrate/tests/DSLMigration.test.ts b/app/client/packages/dsl/src/migrate/tests/DSLMigration.test.ts index fd03b573d5..90a2d19cb5 100644 --- a/app/client/packages/dsl/src/migrate/tests/DSLMigration.test.ts +++ b/app/client/packages/dsl/src/migrate/tests/DSLMigration.test.ts @@ -90,6 +90,7 @@ import * as m86 from "../migrations/086-migrate-table-server-side-filtering"; import * as m87 from "../migrations/087-migrate-chart-widget-customechartdata"; import * as m88 from "../migrations/088-migrate-custom-widget-dynamic-height"; import * as m89 from "../migrations/089-migrage-table-widget-v2-currentRow-binding"; +import * as m90 from "../migrations/090-migrate-table-compute-value-binding"; interface Migration { functionLookup: { @@ -931,9 +932,18 @@ const migrations: Migration[] = [ version: 89, }, { - functionLookup: [], + functionLookup: [ + { + moduleObj: m90, + functionName: "migrateTableComputeValueBinding", + }, + ], version: 90, }, + { + functionLookup: [], + version: 91, + }, ]; const mockFnObj: Record = {}; diff --git a/app/client/packages/dsl/src/migrate/tests/TableWidgetV2/DSLs/TableComputeBindingDSLs.ts b/app/client/packages/dsl/src/migrate/tests/TableWidgetV2/DSLs/TableComputeBindingDSLs.ts new file mode 100644 index 0000000000..8dd7a0ee2c --- /dev/null +++ b/app/client/packages/dsl/src/migrate/tests/TableWidgetV2/DSLs/TableComputeBindingDSLs.ts @@ -0,0 +1,28 @@ +export const tableComputeBindingInputDsl = { + widgetName: "Table1", + type: "TABLE_WIDGET_V2", + primaryColumns: { + column1: { + computedValue: + "{{Table1.processedTableData.map((currentRow, currentIndex) => (currentRow.value))}}", + }, + column2: { + computedValue: "static value", + }, + }, + version: 1, + children: [], +}; + +export const tableComputeBindingOutputDsl = { + ...tableComputeBindingInputDsl, + primaryColumns: { + column1: { + computedValue: + "{{(() => { const tableData = Table1.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (currentRow.value)) : currentRow.value })()}}", + }, + column2: { + computedValue: "static value", + }, + }, +}; diff --git a/app/client/packages/dsl/src/migrate/tests/TableWidgetV2/TableWidget.test.ts b/app/client/packages/dsl/src/migrate/tests/TableWidgetV2/TableWidget.test.ts index 481c6a00ff..2938cdab8b 100644 --- a/app/client/packages/dsl/src/migrate/tests/TableWidgetV2/TableWidget.test.ts +++ b/app/client/packages/dsl/src/migrate/tests/TableWidgetV2/TableWidget.test.ts @@ -48,6 +48,11 @@ import { inputDsl as updateHeaderOptionsInputDsl, outputDsl as updateHeaderOptionsOutputDsl, } from "./DSLs/UpdateHeaderOptionsDSLs"; +import { migrateTableComputeValueBinding } from "../../migrations/090-migrate-table-compute-value-binding"; +import { + tableComputeBindingInputDsl, + tableComputeBindingOutputDsl, +} from "./DSLs/TableComputeBindingDSLs"; describe("Table Widget Property Pane Upgrade", () => { it("To test primaryColumns are created for a simple table", () => { @@ -136,3 +141,11 @@ describe("migrateTableWidgetV2CurrentRowInValidationsBinding", () => { ).toEqual(currentRownInValidationsBindingOutput); }); }); + +describe("migrateTableComputeValueBinding", () => { + it("should migrate table compute value bindings to use new fallback mechanism", () => { + expect( + migrateTableComputeValueBinding(tableComputeBindingInputDsl), + ).toEqual(tableComputeBindingOutputDsl); + }); +}); diff --git a/app/client/src/components/propertyControls/TableComputeValue.test.tsx b/app/client/src/components/propertyControls/TableComputeValue.test.tsx new file mode 100644 index 0000000000..d0ea214611 --- /dev/null +++ b/app/client/src/components/propertyControls/TableComputeValue.test.tsx @@ -0,0 +1,61 @@ +import ComputeTablePropertyControlV2 from "./TableComputeValue"; + +describe("ComputeTablePropertyControlV2.getInputComputedValue", () => { + const tableName = "Table1"; + const inputVariations = [ + "currentRow.price", + ` + [ + { + "value": "male", + "label": "male" + }, + { + "value": "female", + "label": "female" + } + ] + `, + `["123", "-456", "0.123", "-0.456"]`, + `["true", "false"]`, + `["null", "undefined"]`, + `{ + "name": "John Doe", + "age": 30, + "isActive": true, + "address": { + "street": "123 Main St", + "city": "Boston" + }, + "hobbies": ["reading", "gaming"] + }`, + "() => { return true; }", + "(x) => x * 2", + "currentRow.price * 2", + "currentRow.isValid && true", + "!currentRow.isDeleted", + ]; + + it("1. should return the correct computed value", () => { + inputVariations.forEach((input) => { + const computedValue = + ComputeTablePropertyControlV2.getTableComputeBinding(tableName, input); + + expect( + ComputeTablePropertyControlV2.getInputComputedValue(computedValue), + ).toBe(`{{${input}}}`); + }); + }); + + it("2. should handle addition values", () => { + const input = "currentRow.quantity + 5"; + const computedValue = ComputeTablePropertyControlV2.getTableComputeBinding( + tableName, + input, + ); + + expect( + ComputeTablePropertyControlV2.getInputComputedValue(computedValue), + ).toBe(`{{currentRow.quantity}}{{5}}`); + }); +}); diff --git a/app/client/src/components/propertyControls/TableComputeValue.tsx b/app/client/src/components/propertyControls/TableComputeValue.tsx index 1eba59f89e..197bf2ceb8 100644 --- a/app/client/src/components/propertyControls/TableComputeValue.tsx +++ b/app/client/src/components/propertyControls/TableComputeValue.tsx @@ -99,11 +99,12 @@ function InputText(props: InputTextProp) { } class ComputeTablePropertyControlV2 extends BaseControl { - static getBindingPrefix(tableName: string) { - return `{{${tableName}.processedTableData.map((currentRow, currentIndex) => ( `; - } - - static bindingSuffix = `))}}`; + static getTableComputeBinding = ( + tableName: string, + stringToEvaluate: string, + ) => { + return `{{(() => { const tableData = ${tableName}.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (${stringToEvaluate})) : ${stringToEvaluate} })()}}`; + }; render() { const { @@ -114,13 +115,9 @@ class ComputeTablePropertyControlV2 extends BaseControl { - const bindingPrefix = - ComputeTablePropertyControlV2.getBindingPrefix(tableName); + static getInputComputedValue = (propertyValue: string) => { + const MAP_FUNCTION_SIGNATURE = "map((currentRow, currentIndex) => ("; - if (propertyValue.includes(bindingPrefix)) { - const value = `${propertyValue.substring( - bindingPrefix.length, - propertyValue.length - - ComputeTablePropertyControlV2.bindingSuffix.length, - )}`; + const isComputedValue = propertyValue.includes(MAP_FUNCTION_SIGNATURE); - return JSToString(value); - } else { - return propertyValue; - } + if (!isComputedValue) return propertyValue; + + // Extract the computation logic from the full binding string + // Input example: "{{(() => { const tableData = Table1.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (currentRow.price * 2)) : currentRow.price * 2 })()}}" + const mapSignatureIndex = propertyValue.indexOf(MAP_FUNCTION_SIGNATURE); + + // Find the actual computation expression between the map parentheses + const computationStart = mapSignatureIndex + MAP_FUNCTION_SIGNATURE.length; + const computationEnd = propertyValue.indexOf("))", computationStart); + + // Extract the computation expression between the map parentheses + // Note: At this point, we're just extracting the raw expression like "currentRow.price * 2" + // The actual removal of "currentRow." prefix happens later in JSToString() + const computationExpression = propertyValue.substring( + computationStart, + computationEnd, + ); + + return JSToString(computationExpression); }; getComputedValue = (value: string, tableName: string) => { + // Return raw value if: + // 1. The value is not a dynamic binding (not wrapped in {{...}}) + // 2. AND this control is not configured to handle array values via additionalControlData + // This allows single values to be returned without table binding computation if ( !isDynamicValue(value) && !this.props.additionalControlData?.isArrayValue @@ -188,9 +198,10 @@ class ComputeTablePropertyControlV2 extends BaseControl | string) => {