diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/table_data_change_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/table_data_change_spec.ts new file mode 100644 index 0000000000..b97062b503 --- /dev/null +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/TableV2/table_data_change_spec.ts @@ -0,0 +1,212 @@ +import { + entityExplorer, + propPane, + agHelper, + draggableWidgets, + deployMode, + table, + locators, +} from "../../../../../support/Objects/ObjectsCore"; + +const readTableLocalColumnOrder = (columnOrderKey: string) => { + const localColumnOrder = window.localStorage.getItem(columnOrderKey) || ""; + if (localColumnOrder) { + const parsedTableConfig = JSON.parse(localColumnOrder); + if (parsedTableConfig) { + const tableWidgetId = Object.keys(parsedTableConfig)[0]; + return parsedTableConfig[tableWidgetId]; + } + } +}; + +const freezeColumnFromDropdown = (columnName: string, direction: string) => { + agHelper + .GetElement(`[data-header=${columnName}] .header-menu .bp3-popover2-target`) + .click({ force: true }); + agHelper.GetNClickByContains(".bp3-menu", `Freeze column ${direction}`); +}; + +const checkIfColumnIsFrozenViaCSS = ( + columnName: string, + position = "sticky", +) => { + agHelper + .GetElement(table._headerCell(columnName)) + .should("have.css", "position", position); +}; + +const TABLE_DATA_1 = `[ + { + "step": "#1", + "task": "Drop a table", + "status": "✅", + "action": "" + }, + { + "step": "#2", + "task": "Create a query fetch_users with the Mock DB", + "status": "--", + "action": "" + }, + { + "step": "#3", + "task": "Bind the query using => fetch_users.data", + "status": "--", + "action": "" + } + ]`; + +const TABLE_DATA_2 = `[ + { + "id": 1, + "name": "Barty Crouch", + "status": "APPROVED", + "gender": "", + "avatar": "https://robohash.org/sednecessitatibuset.png?size=100x100&set=set1", + "email": "barty.crouch@gmail.com", + "address": "St Petersberg #911 4th main", + "createdAt": "2020-03-16T18:00:05.000Z", + "updatedAt": "2020-08-12T17:29:31.980Z" + }, + { + "id": 2, + "name": "Jenelle Kibbys", + "status": "APPROVED", + "gender": "Female", + "avatar": "https://robohash.org/quiaasperiorespariatur.bmp?size=100x100&set=set1", + "email": "jkibby1@hp.com", + "address": "85 Tennessee Plaza", + "createdAt": "2019-10-04T03:22:23.000Z", + "updatedAt": "2019-09-11T20:18:38.000Z" + }, + { + "id": 3, + "name": "Demetre", + "status": "APPROVED", + "gender": "Male", + "avatar": "https://robohash.org/iustooptiocum.jpg?size=100x100&set=set1", + "email": "aaaa@bbb.com", + "address": "262 Saint Paul Park", + "createdAt": "2020-05-01T17:30:50.000Z", + "updatedAt": "2019-10-08T14:55:53.000Z" + }, + { + "id": 10, + "name": "Tobin Shellibeer", + "status": "APPROVED", + "gender": "Male", + "avatar": "https://robohash.org/odioeumdolores.png?size=100x100&set=set1", + "email": "tshellibeer9@ihg.com", + "address": "4 Ridgeway Lane", + "createdAt": "2019-11-27T06:09:41.000Z", + "updatedAt": "2019-09-07T16:35:48.000Z" + }]`; + +describe("Table widget v2: tableData change test", function () { + before(() => { + agHelper.ClearLocalStorageCache(); + }); + + it("1. should test that the number of columns needs to be same when table data changes in depoyed app", function () { + entityExplorer.DragDropWidgetNVerify(draggableWidgets.TABLE, 300, 100); + propPane.EnterJSContext( + "Table data", + `{{appsmith.store.test === '0' ? ${TABLE_DATA_1} : ${TABLE_DATA_2}}}`, + ); + + entityExplorer.DragDropWidgetNVerify(draggableWidgets.BUTTON, 500, 500); + propPane.UpdatePropertyFieldValue("Label", "Set table data 1"); + propPane.SelectPlatformFunction("onClick", "Store value"); + agHelper.EnterActionValue("Key", "test"); + agHelper.EnterActionValue("Value", "0"); + + // add a success callback + agHelper.GetNClick(propPane._actionCallbacks); + agHelper.GetNClick(propPane._actionAddCallback("success")); + agHelper.GetNClick(locators._dropDownValue("Show alert")); + agHelper.EnterActionValue("Message", "table data 1 set"); + + entityExplorer.DragDropWidgetNVerify(draggableWidgets.BUTTON, 500, 600); + propPane.UpdatePropertyFieldValue("Label", "Set table data 2"); + propPane.SelectPlatformFunction("onClick", "Store value"); + agHelper.EnterActionValue("Key", "test"); + agHelper.EnterActionValue("Value", "1"); + + // add a success callback + agHelper.GetNClick(propPane._actionCallbacks); + agHelper.GetNClick(propPane._actionAddCallback("success")); + agHelper.GetNClick(locators._dropDownValue("Show alert")); + agHelper.EnterActionValue("Message", "table data 2 set"); + + deployMode.DeployApp(); + + agHelper.ClickButton("Set table data 1"); + + agHelper.WaitUntilToastDisappear("table data 1 set"); + table.AssertTableHeaderOrder("statussteptaskaction"); + let tableLocalColumnOrder = readTableLocalColumnOrder( + "tableWidgetColumnOrder", + ); + if (tableLocalColumnOrder) + expect(tableLocalColumnOrder.columnOrder.join("")).equal( + "statussteptaskaction", + ); + + agHelper.ClickButton("Set table data 2"); + + agHelper.WaitUntilToastDisappear("table data 2 set"); + + table.AssertTableHeaderOrder( + "statusidnamegenderavataremailaddresscreatedAtupdatedAt", + ); + tableLocalColumnOrder = readTableLocalColumnOrder("tableWidgetColumnOrder"); + if (tableLocalColumnOrder) + expect(tableLocalColumnOrder.columnOrder.join("")).equal( + "statusidnamegenderavataremailaddresscreatedAtupdatedAt", + ); + + /** + * Flow: Check the above flow for frozen columns + * 1. Freeze columns with table data 1. + * 2. Refresh the page. + * 3. Check if the frozen columns are same. + * 4. Similarly do the same thing for table data 2. + */ + + agHelper.ClickButton("Set table data 1"); + + table.AssertTableHeaderOrder("statussteptaskaction"); + tableLocalColumnOrder = readTableLocalColumnOrder("tableWidgetColumnOrder"); + if (tableLocalColumnOrder) + expect(tableLocalColumnOrder.columnOrder.join("")).equal( + "statussteptaskaction", + ); + + freezeColumnFromDropdown("status", "left"); + freezeColumnFromDropdown("action", "right"); + + agHelper.RefreshPage(true, "viewPage"); + + checkIfColumnIsFrozenViaCSS("status"); + checkIfColumnIsFrozenViaCSS("action"); + + agHelper.ClickButton("Set table data 2"); + + table.AssertTableHeaderOrder( + "statusidnamegenderavataremailaddresscreatedAtupdatedAt", + ); + tableLocalColumnOrder = readTableLocalColumnOrder("tableWidgetColumnOrder"); + if (tableLocalColumnOrder) + expect(tableLocalColumnOrder.columnOrder.join("")).equal( + "statusidnamegenderavataremailaddresscreatedAtupdatedAt", + ); + + freezeColumnFromDropdown("id", "left"); + freezeColumnFromDropdown("updatedAt", "right"); + + agHelper.RefreshPage(true, "viewPage"); + + checkIfColumnIsFrozenViaCSS("id"); + checkIfColumnIsFrozenViaCSS("updatedAt"); + }); +}); diff --git a/app/client/src/widgets/TableWidgetV2/widget/index.tsx b/app/client/src/widgets/TableWidgetV2/widget/index.tsx index 8da52c9916..fd72fc29b3 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/widget/index.tsx @@ -27,7 +27,8 @@ import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import Skeleton from "components/utils/Skeleton"; import { noop, retryPromise } from "utils/AppsmithUtils"; import { SORT_ORDER } from "../component/Constants"; -import type { StickyType, ReactTableFilter } from "../component/Constants"; +import { StickyType } from "../component/Constants"; +import type { ReactTableFilter } from "../component/Constants"; import { AddNewRowActions, DEFAULT_FILTER } from "../component/Constants"; import type { EditableCell, @@ -388,13 +389,8 @@ class TableWidgetV2 extends BaseWidget { * based on columnType */ getTableColumns = () => { - const { - columnWidthMap, - orderedTableColumns, - primaryColumns, - renderMode, - widgetId, - } = this.props; + const { columnWidthMap, orderedTableColumns, renderMode, widgetId } = + this.props; const { componentWidth } = this.getPaddingAdjustedDimensions(); const widgetLocalStorageState = getColumnOrderByWidgetIdFromLS(widgetId); const memoisdGetColumnsWithLocalStorage = @@ -404,9 +400,7 @@ class TableWidgetV2 extends BaseWidget { columnWidthMap, orderedTableColumns, componentWidth, - primaryColumns, renderMode, - widgetId, ); }; @@ -518,6 +512,7 @@ class TableWidgetV2 extends BaseWidget { */ updateColumnProperties = ( tableColumns?: Record, + shouldPersistLocalOrderWhenTableDataChanges = false, ) => { const { columnOrder = [], primaryColumns = {} } = this.props; const derivedColumns = getDerivedColumns(primaryColumns); @@ -571,6 +566,28 @@ class TableWidgetV2 extends BaseWidget { newColumnOrder.sort(compareColumns); propertiesToAdd["columnOrder"] = newColumnOrder; + + /** + * As the table data changes in Deployed app, we also update the local storage. + * + * this.updateColumnProperties gets executed on mount and on update of the component. + * On mount we get new tableColumns that may not have any sticky columns. + * This will lead to loss of sticky column that were frozen by the user. + * To avoid this and to maintain user's sticky columns we use shouldPersistLocalOrderWhenTableDataChanges below + * so as to avoid updating the local storage on mount. + **/ + if ( + this.props.renderMode === RenderModes.PAGE && + shouldPersistLocalOrderWhenTableDataChanges + ) { + const leftOrder = newColumnOrder.filter( + (col: string) => tableColumns[col].sticky === StickyType.LEFT, + ); + const rightOrder = newColumnOrder.filter( + (col: string) => tableColumns[col].sticky === StickyType.RIGHT, + ); + this.persistColumnOrder(newColumnOrder, leftOrder, rightOrder); + } } const propertiesToUpdate: BatchPropertyUpdatePayload = { @@ -614,7 +631,12 @@ class TableWidgetV2 extends BaseWidget { ); if (localTableColumnOrder) { - const { columnOrder, columnUpdatedAt } = localTableColumnOrder; + const { + columnOrder, + columnUpdatedAt, + leftOrder: localLeftOrder, + rightOrder: localRightOrder, + } = localTableColumnOrder; if (this.props.columnUpdatedAt !== columnUpdatedAt) { // Delete and set the column orders defined by the developer @@ -626,7 +648,47 @@ class TableWidgetV2 extends BaseWidget { rightOrder, ); } else { - this.props.updateWidgetMetaProperty("columnOrder", columnOrder); + const propertiesToAdd: Record = {}; + + propertiesToAdd["columnOrder"] = columnOrder; + + /** + * We reset the sticky values of the columns that were frozen by the developer. + */ + if (Object.keys(this.props.primaryColumns).length > 0) { + columnOrder.forEach((colName: string) => { + if ( + this.props.primaryColumns[colName]?.sticky !== StickyType.NONE + ) { + propertiesToAdd[`primaryColumns.${colName}.sticky`] = + StickyType.NONE; + } + }); + } + + /** + * We pickup the left and the right frozen columns from the localstorage + * and update the sticky value of these columns respectively. + */ + + if (localLeftOrder.length > 0) { + localLeftOrder.forEach((colName: string) => { + propertiesToAdd[`primaryColumns.${colName}.sticky`] = + StickyType.LEFT; + }); + } + + if (localRightOrder.length > 0) { + localRightOrder.forEach((colName: string) => { + propertiesToAdd[`primaryColumns.${colName}.sticky`] = + StickyType.RIGHT; + }); + } + + const propertiesToUpdate = { + modify: propertiesToAdd, + }; + super.batchUpdateWidgetProperty(propertiesToUpdate); } } else { // If user deletes local storage or no column orders for the given table widget exists hydrate it with the developer changes. @@ -641,11 +703,6 @@ class TableWidgetV2 extends BaseWidget { componentDidMount() { const { canFreezeColumn, renderMode, tableData } = this.props; - if (canFreezeColumn && renderMode === RenderModes.PAGE) { - //dont neet to batch this since single action - this.hydrateStickyColumns(); - } - if (_.isArray(tableData) && !!tableData.length) { const newPrimaryColumns = this.createTablePrimaryColumns(); @@ -654,6 +711,11 @@ class TableWidgetV2 extends BaseWidget { this.updateColumnProperties(newPrimaryColumns); } } + + if (canFreezeColumn && renderMode === RenderModes.PAGE) { + //dont neet to batch this since single action + this.hydrateStickyColumns(); + } } componentDidUpdate(prevProps: TableWidgetProps) { @@ -721,7 +783,7 @@ class TableWidgetV2 extends BaseWidget { const newTableColumns = this.createTablePrimaryColumns(); if (newTableColumns) { - this.updateColumnProperties(newTableColumns); + this.updateColumnProperties(newTableColumns, isTableDataModified); } pushBatchMetaUpdates("filters", [DEFAULT_FILTER]); @@ -1157,8 +1219,16 @@ class TableWidgetV2 extends BaseWidget { updatedOrders.leftOrder, updatedOrders.rightOrder, ); - // only a single meta property update no need to batch this - this.props.updateWidgetMetaProperty("columnOrder", newColumnOrder); + + super.batchUpdateWidgetProperty( + { + modify: { + [`primaryColumns.${columnName}.sticky`]: sticky, + columnOrder: newColumnOrder, + }, + }, + true, + ); } } }; @@ -1166,24 +1236,22 @@ class TableWidgetV2 extends BaseWidget { handleReorderColumn = (columnOrder: string[]) => { columnOrder = columnOrder.map((alias) => this.getColumnIdByAlias(alias)); - if (this.props.renderMode === RenderModes.CANVAS) { - super.updateWidgetProperty("columnOrder", columnOrder); - } else { - if (this.props.canFreezeColumn) { - const localTableColumnOrder = getColumnOrderByWidgetIdFromLS( - this.props.widgetId, - ); - if (localTableColumnOrder) { - const { leftOrder, rightOrder } = localTableColumnOrder; - this.persistColumnOrder(columnOrder, leftOrder, rightOrder); - } else { - this.persistColumnOrder(columnOrder, [], []); - } + if ( + this.props.canFreezeColumn && + this.props.renderMode === RenderModes.PAGE + ) { + const localTableColumnOrder = getColumnOrderByWidgetIdFromLS( + this.props.widgetId, + ); + if (localTableColumnOrder) { + const { leftOrder, rightOrder } = localTableColumnOrder; + this.persistColumnOrder(columnOrder, leftOrder, rightOrder); + } else { + this.persistColumnOrder(columnOrder, [], []); } - // only a single meta property update no need to batch this - - this.props.updateWidgetMetaProperty("columnOrder", columnOrder); } + + super.updateWidgetProperty("columnOrder", columnOrder); }; handleColumnSorting = (columnAccessor: string, isAsc: boolean) => { diff --git a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/getColumnsPureFn.tsx b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/getColumnsPureFn.tsx index 11a42d902b..1c21beff2d 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/getColumnsPureFn.tsx +++ b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/getColumnsPureFn.tsx @@ -7,11 +7,7 @@ import { DEFAULT_COLUMN_WIDTH, DEFAULT_COLUMN_NAME, } from "../../constants"; -import { fetchSticky } from "../utilities"; -import type { - ColumnProperties, - ReactTableColumnProps, -} from "../../component/Constants"; +import type { ReactTableColumnProps } from "../../component/Constants"; import memoizeOne from "memoize-one"; export type getColumns = ( @@ -19,9 +15,7 @@ export type getColumns = ( columnWidthMap: { [key: string]: number } | undefined, orderedTableColumns: any, componentWidth: number, - primaryColumns: Record, renderMode: RenderMode, - widgetId: string, ) => ReactTableColumnProps[]; //TODO: (Vamsi) need to unit test this function @@ -31,9 +25,7 @@ export const getColumnsPureFn: getColumns = ( columnWidthMap = {}, orderedTableColumns = [], componentWidth, - primaryColumns, renderMode, - widgetId, ) => { let columns: ReactTableColumnProps[] = []; const hiddenColumns: ReactTableColumnProps[] = []; @@ -58,7 +50,7 @@ export const getColumnsPureFn: getColumns = ( isHidden: false, isAscOrder: column.isAscOrder, isDerived: column.isDerived, - sticky: fetchSticky(column.id, primaryColumns, renderMode, widgetId), + sticky: column.sticky, metaProperties: { isHidden: isHidden, type: column.columnType, diff --git a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts index 4a785eacfb..aeb2de5b71 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts @@ -1,6 +1,5 @@ import { Colors } from "constants/Colors"; -import type { RenderMode } from "constants/WidgetConstants"; -import { FontStyleTypes, RenderModes } from "constants/WidgetConstants"; +import { FontStyleTypes } from "constants/WidgetConstants"; import _, { filter, isBoolean, isObject, uniq, without } from "lodash"; import tinycolor from "tinycolor2"; import type { @@ -873,32 +872,6 @@ export const deleteLocalTableColumnOrderByWidgetId = (widgetId: string) => { } }; -export const fetchSticky = ( - columnId: string, - primaryColumns: Record, - renderMode: RenderMode, - widgetId?: string, -): StickyType | undefined => { - if (renderMode === RenderModes.PAGE && widgetId) { - const localTableColumnOrder = getColumnOrderByWidgetIdFromLS(widgetId); - if (localTableColumnOrder) { - const { leftOrder, rightOrder } = localTableColumnOrder; - if (leftOrder.indexOf(columnId) > -1) { - return StickyType.LEFT; - } else if (rightOrder.indexOf(columnId) > -1) { - return StickyType.RIGHT; - } else { - return StickyType.NONE; - } - } else { - return get(primaryColumns, `${columnId}`).sticky; - } - } - if (renderMode === RenderModes.CANVAS) { - return get(primaryColumns, `${columnId}`).sticky; - } -}; - export const updateAndSyncTableLocalColumnOrders = ( columnName: string, leftOrder: string[],