diff --git a/app/client/cypress/fixtures/basicNumberDataTableDsl.json b/app/client/cypress/fixtures/basicNumberDataTableDsl.json new file mode 100644 index 0000000000..822ea6bb01 --- /dev/null +++ b/app/client/cypress/fixtures/basicNumberDataTableDsl.json @@ -0,0 +1,42 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 9, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "label": "Data", + "widgetName": "Table1", + "searchKey": "", + "tableData": "[{\"1\":\"abc\",\"2\":\"bcd\",\"3\":\"cde\",\"Dec\":\"mon\",\"demo\":\"3\",\"demo_1\":\"1\",\"test one\":\"1\",\"test 3 4 9\":\"4\",\"rowIndex\":\"0\"},{\"1\":\"asd\",\"2\":\"dfg\",\"3\":\"jkl\",\"Dec\":\"mon2\",\"demo\":\"2\",\"demo_1\":\"1\",\"test one\":\"2\",\"test 3 4 9\":\"3\",\"rowIndex\":\"1\"}]", + "type": "TABLE_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 1, + "rightColumn": 9, + "topRow": 7, + "bottomRow": 14, + "parentId": "0", + "widgetId": "7miqot30xy", + "dynamicBindingPathList": [] + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/tableWithNumberColumnDsl.json b/app/client/cypress/fixtures/tableWithNumberColumnDsl.json new file mode 100644 index 0000000000..55ac0d69aa --- /dev/null +++ b/app/client/cypress/fixtures/tableWithNumberColumnDsl.json @@ -0,0 +1,266 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 816, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 5016, + "containerStyle": "none", + "snapRows": 125, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 47, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "Table1", + "defaultPageSize": 0, + "columnOrder": [ + "1", + "2", + "3", + "Dec", + "demo", + "demo_1", + "test_one", + "test_3_4_9", + "rowIndex" + ], + "isVisibleDownload": true, + "dynamicPropertyPathList": [], + "displayName": "Table", + "iconSVG": "/static/media/icon.db8a9cbd.svg", + "topRow": 1, + "bottomRow": 29, + "isSortable": true, + "parentRowSpace": 10, + "type": "TABLE_WIDGET", + "defaultSelectedRow": "0", + "hideCard": false, + "animateLoading": true, + "dynamicTriggerPathList": [], + "dynamicBindingPathList": [ + { + "key": "tableData" + }, + { + "key": "primaryColumns.1.computedValue" + }, + { + "key": "primaryColumns.2.computedValue" + }, + { + "key": "primaryColumns.3.computedValue" + }, + { + "key": "primaryColumns.Dec.computedValue" + }, + { + "key": "primaryColumns.demo.computedValue" + }, + { + "key": "primaryColumns.demo_1.computedValue" + }, + { + "key": "primaryColumns.test_one.computedValue" + }, + { + "key": "primaryColumns.test_3_4_9.computedValue" + }, + { + "key": "primaryColumns.rowIndex.computedValue" + } + ], + "leftColumn": 0, + "primaryColumns": { + "1": { + "index": 0, + "width": 150, + "id": "1", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "1", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.1))}}" + }, + "2": { + "index": 1, + "width": 150, + "id": "2", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "2", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.2))}}" + }, + "3": { + "index": 2, + "width": 150, + "id": "3", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "3", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.3))}}" + }, + "Dec": { + "index": 3, + "width": 150, + "id": "Dec", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "Dec", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.Dec))}}" + }, + "demo": { + "index": 4, + "width": 150, + "id": "demo", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "demo", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.demo))}}" + }, + "demo_1": { + "index": 5, + "width": 150, + "id": "demo_1", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "demo_1", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.demo_1))}}" + }, + "test_one": { + "index": 6, + "width": 150, + "id": "test_one", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "test_one", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.test_one))}}" + }, + "test_3_4_9": { + "index": 7, + "width": 150, + "id": "test_3_4_9", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "test_3_4_9", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.test_3_4_9))}}" + }, + "rowIndex": { + "index": 8, + "width": 150, + "id": "rowIndex", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "rowIndex", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.rowIndex))}}" + } + }, + "delimiter": ",", + "key": "zda7sm6on1", + "derivedColumns": {}, + "rightColumn": 34, + "textSize": "PARAGRAPH", + "widgetId": "rqduejqa7w", + "isVisibleFilters": true, + "tableData": "{{Api1.data}}", + "isVisible": true, + "label": "Data", + "searchKey": "", + "enableClientSideSearch": true, + "version": 3, + "totalRecordsCount": 0, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "horizontalAlignment": "LEFT", + "isVisibleSearch": true, + "isVisiblePagination": true, + "verticalAlignment": "CENTER", + "columnSizeMap": { + "task": 245, + "step": 62, + "status": 75 + } + } + ] + } +} diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Number_column_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Number_column_spec.js new file mode 100644 index 0000000000..3b3c4a5ff0 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Table_Number_column_spec.js @@ -0,0 +1,19 @@ +/* eslint-disable cypress/no-unnecessary-waiting */ +const dsl = require("../../../../fixtures/basicNumberDataTableDsl.json"); + +describe("Validate Table Widget Table Data", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Check number key in table data convert table binding and header properly", function() { + cy.openPropertyPane("tablewidget"); + + cy.contains('[role="columnheader"]', "_1").should("exist"); + cy.contains('[role="columnheader"]', "_2").should("exist"); + }); + + afterEach(() => { + // put your clean up code if any + }); +}); diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index b5e6271acc..bb773077df 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -69,7 +69,7 @@ export const layoutConfigurations: LayoutConfigurations = { FLUID: { minWidth: -1, maxWidth: -1 }, }; -export const LATEST_PAGE_VERSION = 47; +export const LATEST_PAGE_VERSION = 48; export const GridDefaults = { DEFAULT_CELL_SIZE: 1, diff --git a/app/client/src/utils/DSLMigrations.ts b/app/client/src/utils/DSLMigrations.ts index e314c45ee4..3d4c99d45a 100644 --- a/app/client/src/utils/DSLMigrations.ts +++ b/app/client/src/utils/DSLMigrations.ts @@ -25,6 +25,7 @@ import { migrateTableSanitizeColumnKeys, isSortableMigration, migrateTableWidgetIconButtonVariant, + migrateTableWidgetNumericColumnName, } from "./migrations/TableWidget"; import { migrateTextStyleFromTextWidget } from "./migrations/TextWidgetReplaceTextStyle"; import { DATA_BIND_REGEX_GLOBAL } from "constants/BindingsConstants"; @@ -990,6 +991,11 @@ export const transformDSL = ( if (currentDSL.version === 46) { currentDSL = migrateCheckboxGroupWidgetInlineProperty(currentDSL); + currentDSL.version = 47; + } + + if (currentDSL.version === 47) { + currentDSL = migrateTableWidgetNumericColumnName(currentDSL); currentDSL.version = LATEST_PAGE_VERSION; } diff --git a/app/client/src/utils/migrations/TableWidget.ts b/app/client/src/utils/migrations/TableWidget.ts index e63033b30c..6460008bab 100644 --- a/app/client/src/utils/migrations/TableWidget.ts +++ b/app/client/src/utils/migrations/TableWidget.ts @@ -3,7 +3,10 @@ import { TextSizes, GridDefaults, } from "constants/WidgetConstants"; -import { getAllTableColumnKeys } from "widgets/TableWidget/component/TableHelpers"; +import { + generateTableColumnId, + getAllTableColumnKeys, +} from "widgets/TableWidget/component/TableHelpers"; import { ColumnProperties, CellAlignmentTypes, @@ -486,3 +489,57 @@ export const migrateTableWidgetIconButtonVariant = (currentDSL: DSLWidget) => { }); return currentDSL; }; + +export const migrateTableWidgetNumericColumnName = (currentDSL: DSLWidget) => { + currentDSL.children = currentDSL.children?.map((child: WidgetProps) => { + if (child.type === "TABLE_WIDGET") { + child.columnOrder = (child.columnOrder || []).map((col: string) => + generateTableColumnId(col), + ); + + const primaryColumns = { ...child.primaryColumns }; + // clear old primaryColumns + child.primaryColumns = {}; + for (const key in primaryColumns) { + if (Object.prototype.hasOwnProperty.call(primaryColumns, key)) { + const column = primaryColumns[key]; + const columnId = generateTableColumnId(key); + const newComputedValue = `{{${child.widgetName}.sanitizedTableData.map((currentRow) => ( currentRow.${columnId}))}}`; + // added column with old accessor + child.primaryColumns[columnId] = { + ...column, + id: columnId, + label: columnId, + computedValue: newComputedValue, + }; + } + } + + child.dynamicBindingPathList = (child.dynamicBindingPathList || []).map( + (path) => { + const pathChunks = path.key.split("."); + // tableData is a valid dynamicBindingPath and pathChunks would have just one entry + if (pathChunks.length < 2) { + return path; + } + const firstPart = pathChunks[0] + "."; // "primaryColumns." + const lastPart = "." + pathChunks.pop(); // ".computedValue" + const key = getSubstringBetweenTwoWords( + path.key, + firstPart, + lastPart, + ); // primaryColumns.$random.header.computedValue -> $random.header + + const sanitizedPrimaryColumnKey = generateTableColumnId(key); + return { + key: firstPart + sanitizedPrimaryColumnKey + lastPart, + }; + }, + ); + } else if (child.children && child.children.length > 0) { + child = migrateTableWidgetNumericColumnName(child); + } + return child; + }); + return currentDSL; +}; diff --git a/app/client/src/widgets/TableWidget/component/TableHelpers.ts b/app/client/src/widgets/TableWidget/component/TableHelpers.ts index 31242682c4..88edc3689d 100644 --- a/app/client/src/widgets/TableWidget/component/TableHelpers.ts +++ b/app/client/src/widgets/TableWidget/component/TableHelpers.ts @@ -1,4 +1,4 @@ -import { uniq, without } from "lodash"; +import { uniq, without, isNaN } from "lodash"; import { ColumnProperties } from "./Constants"; const removeSpecialChars = (value: string, limit?: number) => { @@ -48,3 +48,8 @@ export const reorderColumns = ( } return newColumnsInOrder; }; + +// check and update column id if it is number +export const generateTableColumnId = (accessor: string) => { + return isNaN(Number(accessor)) ? accessor : `_${accessor}`; +}; diff --git a/app/client/src/widgets/TableWidget/component/TableUtilities.tsx b/app/client/src/widgets/TableWidget/component/TableUtilities.tsx index e3b1677211..0e4c1d6f6b 100644 --- a/app/client/src/widgets/TableWidget/component/TableUtilities.tsx +++ b/app/client/src/widgets/TableWidget/component/TableUtilities.tsx @@ -50,6 +50,7 @@ import { import { StyledButton } from "widgets/IconButtonWidget/component"; import MenuButtonTableComponent from "./components/menuButtonTableComponent"; import { stopClickEventPropagation } from "utils/helpers"; +import { generateTableColumnId } from "./TableHelpers"; export const renderCell = ( value: any, @@ -632,10 +633,11 @@ export function getDefaultColumnProperties( widgetName: string, isDerived?: boolean, ): ColumnProperties { + const id = generateTableColumnId(accessor); const columnProps = { index: index, width: 150, - id: accessor, + id, horizontalAlignment: CellAlignmentTypes.LEFT, verticalAlignment: VerticalAlignmentTypes.CENTER, columnType: ColumnTypes.TEXT, @@ -651,7 +653,7 @@ export function getDefaultColumnProperties( label: accessor, computedValue: isDerived ? "" - : `{{${widgetName}.sanitizedTableData.map((currentRow) => ( currentRow.${accessor}))}}`, + : `{{${widgetName}.sanitizedTableData.map((currentRow) => ( currentRow.${id}))}}`, }; return columnProps; diff --git a/app/client/src/widgets/TableWidget/widget/derived.js b/app/client/src/widgets/TableWidget/widget/derived.js index 02bfae300e..a756cfa78b 100644 --- a/app/client/src/widgets/TableWidget/widget/derived.js +++ b/app/client/src/widgets/TableWidget/widget/derived.js @@ -118,10 +118,13 @@ export default { const sanitizedData = {}; for (const [key, value] of Object.entries(entry)) { - const sanitizedKey = key + let sanitizedKey = key .split(separatorRegex) .join("_") .slice(0, 200); + sanitizedKey = _.isNaN(Number(sanitizedKey)) + ? sanitizedKey + : `_${sanitizedKey}`; sanitizedData[sanitizedKey] = value; } return sanitizedData; diff --git a/app/client/src/widgets/TableWidget/widget/derived.test.js b/app/client/src/widgets/TableWidget/widget/derived.test.js index 369442ba84..0f49303b64 100644 --- a/app/client/src/widgets/TableWidget/widget/derived.test.js +++ b/app/client/src/widgets/TableWidget/widget/derived.test.js @@ -1171,6 +1171,83 @@ describe("Validates Derived Properties", () => { let result = getFilteredTableData(input, moment, _); expect(result).toStrictEqual(expected); }); + + it("validates generated sanitized table data with valid property keys", () => { + const { getSanitizedTableData } = derivedProperty; + + const input = { + tableData: [ + { + "1": "abc", + "2": "bcd", + "3": "cde", + Dec: "mon", + demo: "3", + demo_1: "1", + "test one": "1", + "test 3 4 9": "4", + rowIndex: "0", + "😀smile😀": "smile 1", + "🙁sad🙁": "sad 1", + "@user": "user 1", + "@name": "name 1", + ÜserÑame: "john", + }, + { + "1": "asd", + "2": "dfg", + "3": "jkl", + Dec: "mon2", + demo: "2", + demo_1: "1", + "test one": "2", + "test 3 4 9": "3", + rowIndex: "1", + "😀smile😀": "smile 2", + "🙁sad🙁": "sad 2", + "@user": "user 2", + "@name": "name 2", + ÜserÑame: "mike", + }, + ], + }; + const expected = [ + { + _1: "abc", + _2: "bcd", + _3: "cde", + Dec: "mon", + demo: "3", + demo_1: "1", + test_one: "1", + test_3_4_9: "4", + rowIndex: "0", + _smile_: "smile 1", + _sad_: "sad 1", + _user: "user 1", + _name: "name 1", + _ser_ame: "john", + }, + { + _1: "asd", + _2: "dfg", + _3: "jkl", + Dec: "mon2", + demo: "2", + demo_1: "1", + test_one: "2", + test_3_4_9: "3", + rowIndex: "1", + _smile_: "smile 2", + _sad_: "sad 2", + _user: "user 2", + _name: "name 2", + _ser_ame: "mike", + }, + ]; + let result = getSanitizedTableData(input, moment, _); + expect(result).toStrictEqual(expected); + }); }); describe("Validate getSelectedRow function", () => {