diff --git a/app/client/package.json b/app/client/package.json index 6f15edee77..14cef1e4ca 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -110,7 +110,7 @@ "lottie-web": "^5.7.4", "mammoth": "^1.5.1", "marked": "^4.0.18", - "memoize-one": "^5.2.1", + "memoize-one": "^6.0.0", "micro-memoize": "^4.0.10", "moment": "2.29.4", "moment-timezone": "^0.5.35", diff --git a/app/client/src/actions/metaActions.ts b/app/client/src/actions/metaActions.ts index ae79766795..bdb57d715a 100644 --- a/app/client/src/actions/metaActions.ts +++ b/app/client/src/actions/metaActions.ts @@ -15,6 +15,9 @@ export interface UpdateWidgetMetaPropertyPayload { propertyValue: unknown; } +export interface BatchUpdateWidgetMetaPropertyPayload { + batchMetaUpdates: UpdateWidgetMetaPropertyPayload[]; +} export const updateWidgetMetaPropAndEval = ( widgetId: string, propertyName: string, @@ -79,6 +82,15 @@ export const triggerEvalOnMetaUpdate = () => { }); }; +export const syncBatchUpdateWidgetMetaProperties = ( + batchMetaUpdates: UpdateWidgetMetaPropertyPayload[], +): ReduxAction => { + return { + type: ReduxActionTypes.BATCH_UPDATE_META_PROPS, + payload: { batchMetaUpdates }, + }; +}; + export const syncUpdateWidgetMetaProperty = ( widgetId: string, propertyName: string, diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index de65b6252e..2d38dd09db 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -382,6 +382,7 @@ export const ReduxActionTypes = { CREATE_WORKSPACE_SUCCESS: "CREATE_WORKSPACE_SUCCESS", ADD_USER_TO_WORKSPACE_INIT: "ADD_USER_TO_WORKSPACE_INIT", ADD_USER_TO_WORKSPACE_SUCCESS: "ADD_USER_TO_WORKSPACE_ERROR", + BATCH_UPDATE_META_PROPS: "BATCH_UPDATE_META_PROPS", SET_META_PROP: "SET_META_PROP", SET_META_PROP_AND_EVAL: "SET_META_PROP_AND_EVAL", META_UPDATE_DEBOUNCED_EVAL: "META_UPDATE_DEBOUNCED_EVAL", diff --git a/app/client/src/components/editorComponents/EditorContextProvider.test.tsx b/app/client/src/components/editorComponents/EditorContextProvider.test.tsx index d22867fd90..140aabef73 100644 --- a/app/client/src/components/editorComponents/EditorContextProvider.test.tsx +++ b/app/client/src/components/editorComponents/EditorContextProvider.test.tsx @@ -32,6 +32,7 @@ describe("EditorContextProvider", () => { "setWidgetCache", "updateMetaWidgetProperty", "syncUpdateWidgetMetaProperty", + "syncBatchUpdateWidgetMetaProperties", "triggerEvalOnMetaUpdate", "deleteMetaWidgets", "deleteWidgetProperty", @@ -69,6 +70,7 @@ describe("EditorContextProvider", () => { "setWidgetCache", "updateMetaWidgetProperty", "syncUpdateWidgetMetaProperty", + "syncBatchUpdateWidgetMetaProperties", "triggerEvalOnMetaUpdate", "updateWidgetAutoHeight", "checkContainersForAutoHeight", diff --git a/app/client/src/components/editorComponents/EditorContextProvider.tsx b/app/client/src/components/editorComponents/EditorContextProvider.tsx index 59016934db..b3365ce9db 100644 --- a/app/client/src/components/editorComponents/EditorContextProvider.tsx +++ b/app/client/src/components/editorComponents/EditorContextProvider.tsx @@ -17,8 +17,10 @@ import { import type { ExecuteTriggerPayload } from "constants/AppsmithActionConstants/ActionConstants"; import type { OccupiedSpace } from "constants/CanvasEditorConstants"; +import type { UpdateWidgetMetaPropertyPayload } from "actions/metaActions"; import { resetChildrenMetaProperty, + syncBatchUpdateWidgetMetaProperties, syncUpdateWidgetMetaProperty, triggerEvalOnMetaUpdate, } from "actions/metaActions"; @@ -69,6 +71,9 @@ export type EditorContextType = { propertyName: string, propertyValue: any, ) => void; + syncBatchUpdateWidgetMetaProperties?: ( + batchMetaUpdates: UpdateWidgetMetaPropertyPayload[], + ) => void; updateWidgetAutoHeight?: (widgetId: string, height: number) => void; checkContainersForAutoHeight?: () => void; modifyMetaWidgets?: (modifications: ModifyMetaWidgetPayload) => void; @@ -102,6 +107,7 @@ const COMMON_API_METHODS: EditorContextTypeKey[] = [ "setWidgetCache", "updateMetaWidgetProperty", "syncUpdateWidgetMetaProperty", + "syncBatchUpdateWidgetMetaProperties", "triggerEvalOnMetaUpdate", "updateWidgetAutoHeight", "checkContainersForAutoHeight", @@ -191,6 +197,9 @@ const mapDispatchToProps = { propertyName: string, propertyValue: any, ) => syncUpdateWidgetMetaProperty(widgetId, propertyName, propertyValue), + syncBatchUpdateWidgetMetaProperties: ( + batchMetaUpdates: UpdateWidgetMetaPropertyPayload[], + ) => syncBatchUpdateWidgetMetaProperties(batchMetaUpdates), resetChildrenMetaProperty, disableDrag: disableDragAction, deleteWidgetProperty: deletePropertyAction, diff --git a/app/client/src/entities/AppsmithConsole/index.ts b/app/client/src/entities/AppsmithConsole/index.ts index cc7e140a51..aed49305e7 100644 --- a/app/client/src/entities/AppsmithConsole/index.ts +++ b/app/client/src/entities/AppsmithConsole/index.ts @@ -107,6 +107,7 @@ export interface LogActionPayload { analytics?: Record; // plugin error details if any (only for plugin errors). pluginErrorDetails?: any; + meta?: Record; } export interface Message { diff --git a/app/client/src/reducers/entityReducers/metaReducer/index.ts b/app/client/src/reducers/entityReducers/metaReducer/index.ts index 62292aa496..85446968a7 100644 --- a/app/client/src/reducers/entityReducers/metaReducer/index.ts +++ b/app/client/src/reducers/entityReducers/metaReducer/index.ts @@ -3,6 +3,7 @@ import { createReducer } from "utils/ReducerUtils"; import type { UpdateWidgetMetaPropertyPayload, ResetWidgetMetaPayload, + BatchUpdateWidgetMetaPropertyPayload, } from "actions/metaActions"; import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; @@ -53,6 +54,20 @@ export const metaReducer = createReducer(initialState, { return nextState; }, + [ReduxActionTypes.BATCH_UPDATE_META_PROPS]: ( + state: MetaState, + action: ReduxAction, + ) => { + const nextState = produce(state, (draftMetaState) => { + const { batchMetaUpdates } = action.payload; + batchMetaUpdates.forEach(({ propertyName, propertyValue, widgetId }) => { + set(draftMetaState, `${widgetId}.${propertyName}`, propertyValue); + }); + return draftMetaState; + }); + + return nextState; + }, [ReduxActionTypes.SET_META_PROP_AND_EVAL]: ( state: MetaState, action: ReduxAction, diff --git a/app/client/src/widgets/MetaHOC.tsx b/app/client/src/widgets/MetaHOC.tsx index 0b51380e44..d815d93cef 100644 --- a/app/client/src/widgets/MetaHOC.tsx +++ b/app/client/src/widgets/MetaHOC.tsx @@ -1,7 +1,7 @@ import React from "react"; import type { WidgetProps } from "./BaseWidget"; import type BaseWidget from "./BaseWidget"; -import { debounce, fromPairs } from "lodash"; +import { debounce, fromPairs, isEmpty } from "lodash"; import { EditorContext } from "components/editorComponents/EditorContextProvider"; import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; @@ -10,6 +10,12 @@ import type { ExecuteTriggerPayload } from "constants/AppsmithActionConstants/Ac import { connect } from "react-redux"; import { getWidgetMetaProps } from "sagas/selectors"; import type { AppState } from "@appsmith/reducers"; +import { error } from "loglevel"; +export type pushAction = ( + propertyName: string | batchUpdateWidgetMetaPropertyType, + propertyValue?: unknown, + actionExecution?: DebouncedExecuteActionPayload, +) => void; export type DebouncedExecuteActionPayload = Omit< ExecuteTriggerPayload, @@ -17,8 +23,15 @@ export type DebouncedExecuteActionPayload = Omit< > & { dynamicString?: string; }; +export type batchUpdateWidgetMetaPropertyType = { + propertyName: string; + propertyValue: unknown; + actionExecution?: DebouncedExecuteActionPayload; +}[]; export interface WithMeta { + commitBatchMetaUpdates: () => void; + pushBatchMetaUpdates: pushAction; updateWidgetMetaProperty: ( propertyName: string, propertyValue: unknown, @@ -36,6 +49,7 @@ function withMeta(WrappedWidget: typeof BaseWidget) { initialMetaState: Record; actionsToExecute: Record; + batchMetaUpdates: batchUpdateWidgetMetaPropertyType; updatedProperties: Record; constructor(props: metaHOCProps) { super(props); @@ -47,6 +61,7 @@ function withMeta(WrappedWidget: typeof BaseWidget) { ); this.updatedProperties = {}; this.actionsToExecute = {}; + this.batchMetaUpdates = []; } addPropertyForEval = ( @@ -140,6 +155,112 @@ function withMeta(WrappedWidget: typeof BaseWidget) { actionExecution, ); }; + /** + This function pushes meta updates that can be commited later. + If there are multiple updates, use this function to batch those updates together. + */ + pushBatchMetaUpdates: pushAction = (firstArgument, ...restArgs) => { + //if first argument is an array its a batch lets push it + if (Array.isArray(firstArgument)) { + this.batchMetaUpdates.push(...firstArgument); + return; + } + //if first argument is a string its a propertyName arg and we are pushing a single action + if (typeof firstArgument === "string") { + const [propertyValue, actionExecution] = restArgs; + this.batchMetaUpdates.push({ + propertyName: firstArgument, + propertyValue, + actionExecution, + }); + + return; + } + const allArgs = [firstArgument, ...restArgs]; + + error("unknown args ", allArgs); + }; + /** + This function commits all batched updates in one go. + */ + commitBatchMetaUpdates = () => { + //ignore commit if batch array is empty + if (!this.batchMetaUpdates || !this.batchMetaUpdates.length) return; + + const metaUpdates = this.batchMetaUpdates.reduce( + (acc: any, { propertyName, propertyValue }) => { + acc[propertyName] = propertyValue; + return acc; + }, + {}, + ); + + AppsmithConsole.info({ + logType: LOG_TYPE.WIDGET_UPDATE, + text: "Widget property was updated", + source: { + type: ENTITY_TYPE.WIDGET, + id: this.props.widgetId, + name: this.props.widgetName, + }, + meta: metaUpdates, + }); + // extract payload from updates + const payload = [...this.batchMetaUpdates]; + //clear batch updates + this.batchMetaUpdates = []; + + this.handleBatchUpdateWidgetMetaProperties(payload); + }; + getMetaPropPath = (propertyName: string | undefined) => { + // look at this.props.__metaOptions, check for metaPropPath value + // if they exist, then update the propertyName + // Below code of updating metaOptions can be removed once we have ListWidget v2 where we better manage meta values of ListWidget. + const metaOptions = this.props.__metaOptions; + + if (!metaOptions) return; + + return `${metaOptions.metaPropPrefix}.${this.props.widgetName}.${propertyName}[${metaOptions.index}]`; + }; + handleBatchUpdateWidgetMetaProperties = ( + batchMetaUpdates: batchUpdateWidgetMetaPropertyType, + ) => { + //if no updates ignore update call + if (!batchMetaUpdates || isEmpty(batchMetaUpdates)) return; + + const { syncBatchUpdateWidgetMetaProperties } = this.context; + + const widgetId = this.props.metaWidgetId || this.props.widgetId; + + if (syncBatchUpdateWidgetMetaProperties) { + const metaOptions = this.props.__metaOptions; + const consolidatedUpdates = batchMetaUpdates.reduce( + (acc: any, { propertyName, propertyValue }) => { + acc.push({ widgetId, propertyName, propertyValue }); + if (metaOptions) { + acc.push({ + widgetId: metaOptions.widgetId, + propertyName: this.getMetaPropPath(propertyName), + propertyValue, + }); + } + return acc; + }, + [], + ); + syncBatchUpdateWidgetMetaProperties(consolidatedUpdates); + } + + batchMetaUpdates.forEach(({ actionExecution, propertyName }) => + this.addPropertyForEval(propertyName, actionExecution), + ); + + this.setState({}, () => { + // react batches the setState call + // this will result in batching multiple updateWidgetMetaProperty calls. + this.debouncedTriggerEvalOnMetaUpdate(); + }); + }; handleUpdateWidgetMetaProperty = ( propertyName: string, @@ -165,10 +286,11 @@ function withMeta(WrappedWidget: typeof BaseWidget) { // if they exist, then update the propertyName // Below code of updating metaOptions can be removed once we have ListWidget v2 where we better manage meta values of ListWidget. const metaOptions = this.props.__metaOptions; - if (metaOptions) { + const metaPropPath = this.getMetaPropPath(propertyName); + if (metaOptions && metaPropPath) { syncUpdateWidgetMetaProperty( metaOptions.widgetId, - `${metaOptions.metaPropPrefix}.${this.props.widgetName}.${propertyName}[${metaOptions.index}]`, + metaPropPath, propertyValue, ); } @@ -194,6 +316,8 @@ function withMeta(WrappedWidget: typeof BaseWidget) { return ( ); diff --git a/app/client/src/widgets/MetaWidgetContextProvider.tsx b/app/client/src/widgets/MetaWidgetContextProvider.tsx index 418c8148f2..75371c7d44 100644 --- a/app/client/src/widgets/MetaWidgetContextProvider.tsx +++ b/app/client/src/widgets/MetaWidgetContextProvider.tsx @@ -27,6 +27,9 @@ function MetaWidgetContextProvider({ const updateWidgetProperty = metaEditorContextProps.updateWidgetProperty ?? editorContextProps.updateWidgetProperty; + const syncBatchUpdateWidgetMetaProperties = + metaEditorContextProps.syncBatchUpdateWidgetMetaProperties ?? + editorContextProps.syncBatchUpdateWidgetMetaProperties; const syncUpdateWidgetMetaProperty = metaEditorContextProps.syncUpdateWidgetMetaProperty ?? editorContextProps.syncUpdateWidgetMetaProperty; @@ -77,6 +80,7 @@ function MetaWidgetContextProvider({ getWidgetCache, deleteMetaWidgets, updateMetaWidgetProperty, + syncBatchUpdateWidgetMetaProperties, }), [ executeAction, @@ -93,6 +97,7 @@ function MetaWidgetContextProvider({ getWidgetCache, deleteMetaWidgets, updateMetaWidgetProperty, + syncBatchUpdateWidgetMetaProperties, ], ); diff --git a/app/client/src/widgets/TableWidgetV2/component/Table.tsx b/app/client/src/widgets/TableWidgetV2/component/Table.tsx index 24728bc480..ce2d3b48f6 100644 --- a/app/client/src/widgets/TableWidgetV2/component/Table.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/Table.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useMemo, useRef } from "react"; -import { pick, reduce } from "lodash"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { reduce } from "lodash"; import type { Row as ReactTableRowType } from "react-table"; import { useTable, @@ -157,6 +157,7 @@ export type HeaderComponentProps = { widgetId: string; }; +const emptyArr: any = []; export function Table(props: TableProps) { const isResizingColumn = React.useRef(false); const handleResizeColumn = (columnWidths: Record) => { @@ -176,17 +177,14 @@ export function Table(props: TableProps) { } props.handleResizeColumn(columnWidthMap); }; - const data = React.useMemo(() => props.data, [JSON.stringify(props.data)]); - const columnString = JSON.stringify( - pick(props, ["columns", "compactMode", "columnWidthMap"]), - ); - const columns = React.useMemo(() => props.columns, [columnString]); + const { columns, data, multiRowSelection, toggleAllRowSelect } = props; + const tableHeadercolumns = React.useMemo( () => - props.columns.filter((column: ReactTableColumnProps) => { + columns.filter((column: ReactTableColumnProps) => { return column.alias !== "actions"; }), - [columnString], + [columns], ); /* For serverSidePaginationEnabled we are taking props.data.length as the page size. @@ -212,7 +210,8 @@ export function Table(props: TableProps) { totalColumnsWidth, } = useTable( { - columns: columns, + //columns and data needs to be memoised as per useTable specs + columns, data, defaultColumn, initialState: { @@ -234,6 +233,7 @@ export function Table(props: TableProps) { } else { // We are updating column size since the drag is complete when we are changing value of isResizing from true to false if (isResizingColumn.current) { + //clear timeout logic //update isResizingColumn in next event loop so that dragEnd event does not trigger click event. setTimeout(function () { isResizingColumn.current = false; @@ -247,15 +247,18 @@ export function Table(props: TableProps) { startIndex = 0; endIndex = props.data.length; } - const subPage = page.slice(startIndex, endIndex); - const selectedRowIndices = props.selectedRowIndices || []; + const subPage = useMemo( + () => page.slice(startIndex, endIndex), + [page, startIndex, endIndex], + ); + const selectedRowIndices = props.selectedRowIndices || emptyArr; const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT]; const tableWrapperRef = useRef(null); const scrollBarRef = useRef(null); const tableHeaderWrapperRef = React.createRef(); const rowSelectionState = React.useMemo(() => { // return : 0; no row selected | 1; all row selected | 2: some rows selected - if (!props.multiRowSelection) return null; + if (!multiRowSelection) return null; const selectedRowCount = reduce( page, (count, row) => { @@ -266,16 +269,17 @@ export function Table(props: TableProps) { const result = selectedRowCount === 0 ? 0 : selectedRowCount === page.length ? 1 : 2; return result; - }, [selectedRowIndices, page]); - const handleAllRowSelectClick = ( - e: React.MouseEvent, - ) => { - // if all / some rows are selected we remove selection on click - // else select all rows - props.toggleAllRowSelect(!Boolean(rowSelectionState), page); - // loop over subPage rows and toggleRowSelected if required - e.stopPropagation(); - }; + }, [multiRowSelection, page, selectedRowIndices]); + const handleAllRowSelectClick = useCallback( + (e: React.MouseEvent) => { + // if all / some rows are selected we remove selection on click + // else select all rows + toggleAllRowSelect(!Boolean(rowSelectionState), page); + // loop over subPage rows and toggleRowSelected if required + e.stopPropagation(); + }, + [page, rowSelectionState, toggleAllRowSelect], + ); const isHeaderVisible = props.isVisibleSearch || props.isVisibleFilters || diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/ButtonCell.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/ButtonCell.tsx index e324489ece..a4bfc928ea 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/ButtonCell.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/ButtonCell.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { memo } from "react"; import { CellWrapper } from "../TableStyledWrappers"; import type { BaseCellComponentProps } from "../Constants"; @@ -18,7 +18,7 @@ export interface RenderActionProps extends BaseCellComponentProps { onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void; } -export function ButtonCell(props: RenderActionProps) { +function ButtonCellComponent(props: RenderActionProps) { const { allowCellWrapping, cellBackground, @@ -84,3 +84,4 @@ export function ButtonCell(props: RenderActionProps) { ); } +export const ButtonCell = memo(ButtonCellComponent); diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/CheckboxCell.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/CheckboxCell.tsx index 0be750878c..b1200db931 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/CheckboxCell.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/CheckboxCell.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { memo } from "react"; import type { BaseCellComponentProps, CellAlignment } from "../Constants"; import { ALIGN_ITEMS, JUSTIFY_CONTENT } from "../Constants"; import { CellWrapper, TooltipContentWrapper } from "../TableStyledWrappers"; @@ -59,7 +59,7 @@ type CheckboxCellProps = BaseCellComponentProps & { disabledCheckboxMessage: string; }; -export const CheckboxCell = (props: CheckboxCellProps) => { +const CheckboxCellComponent = (props: CheckboxCellProps) => { const { accentColor, borderRadius, @@ -122,3 +122,5 @@ export const CheckboxCell = (props: CheckboxCellProps) => { ); }; + +export const CheckboxCell = memo(CheckboxCellComponent); diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/EditActionsCell.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/EditActionsCell.tsx index 72a6909f9b..f268882926 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/EditActionsCell.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/EditActionsCell.tsx @@ -1,5 +1,4 @@ -import React from "react"; - +import React, { memo } from "react"; import type { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { ButtonColumnActions } from "widgets/TableWidgetV2/constants"; import { EditableCellActions } from "widgets/TableWidgetV2/constants"; @@ -18,7 +17,7 @@ type RenderEditActionsProps = BaseCellComponentProps & { onDiscard: () => void; }; -export function EditActionCell(props: RenderEditActionsProps) { +function EditActionCellComponent(props: RenderEditActionsProps) { const { allowCellWrapping, cellBackground, @@ -91,3 +90,4 @@ export function EditActionCell(props: RenderEditActionsProps) { ); } +export const EditActionCell = memo(EditActionCellComponent); diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/HeaderCell.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/HeaderCell.tsx index 4d70c45972..6395523ae5 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/HeaderCell.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/HeaderCell.tsx @@ -1,4 +1,10 @@ -import React, { createRef, useCallback, useEffect, useState } from "react"; +import React, { + createRef, + useCallback, + useEffect, + useState, + memo, +} from "react"; import { MenuItem, Tooltip, Menu } from "@blueprintjs/core"; import Check from "remixicon-react/CheckFillIcon"; import ArrowDownIcon from "remixicon-react/ArrowDownSLineIcon"; @@ -145,7 +151,7 @@ type HeaderProps = { ) => void; }; -export const HeaderCell = (props: HeaderProps) => { +const HeaderCellComponent = (props: HeaderProps) => { const { column, editMode, isSortable } = props; const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -335,3 +341,4 @@ export const HeaderCell = (props: HeaderProps) => { ); }; +export const HeaderCell = memo(HeaderCellComponent); diff --git a/app/client/src/widgets/TableWidgetV2/component/header/TableColumnHeader.tsx b/app/client/src/widgets/TableWidgetV2/component/header/TableColumnHeader.tsx index fa95758735..bfd35a8366 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/TableColumnHeader.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/TableColumnHeader.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { memo } from "react"; import { getDragHandlers } from "widgets/TableWidgetV2/widget/utilities"; import { HeaderCell } from "../cellComponents/HeaderCell"; import type { ReactTableColumnProps } from "../Constants"; @@ -143,4 +143,4 @@ const TableColumnHeader = (props: TableColumnHeaderProps) => { ); }; -export default TableColumnHeader; +export default memo(TableColumnHeader); diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/ActionItem.tsx b/app/client/src/widgets/TableWidgetV2/component/header/actions/ActionItem.tsx index 906c4645d5..c4d9a9cac1 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/actions/ActionItem.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/ActionItem.tsx @@ -117,4 +117,4 @@ function ActionItem(props: ActionItemProps) { } } -export default ActionItem; +export default React.memo(ActionItem); diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/PageNumberInput.tsx b/app/client/src/widgets/TableWidgetV2/component/header/actions/PageNumberInput.tsx index bbbc447af1..cf5e62b99d 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/actions/PageNumberInput.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/PageNumberInput.tsx @@ -45,7 +45,7 @@ const PageNumberInputWrapper = styled(NumericInput)<{ const MIN_PAGE_COUNT = 1; -export function PageNumberInput(props: { +function PageNumberInputComponent(props: { pageNo: number; pageCount: number; updatePageNo: (pageNo: number, event?: EventType) => void; @@ -109,3 +109,4 @@ export function PageNumberInput(props: { /> ); } +export const PageNumberInput = React.memo(PageNumberInputComponent); diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/Pagination.tsx b/app/client/src/widgets/TableWidgetV2/component/header/actions/Pagination.tsx deleted file mode 100644 index 54f96d5903..0000000000 --- a/app/client/src/widgets/TableWidgetV2/component/header/actions/Pagination.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -// TODO(vikcy): Fix the banned types in this file -import React from "react"; -import type { IconName } from "@blueprintjs/core"; -import { Icon } from "@blueprintjs/core"; -import styled from "styled-components"; - -const PagerContainer = styled.div` - &&& { - height: 49px; - } -`; -function PagerIcon(props: { - icon: IconName; - onClick: Function; - className: string; -}) { - return ( - - ); -} -interface PagerProps { - pageNo: number; - prevPageClick: Function; - nextPageClick: Function; -} - -const PageWrapper = styled.div` - && { - width: 140px; - display: flex; - margin: 0 auto; - } -`; - -export function TablePagination(props: PagerProps) { - return ( - - - -
- -
- -
-
- ); -} diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPaneContent.tsx b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPaneContent.tsx index 177906f203..68b9d4a19e 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPaneContent.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/FilterPaneContent.tsx @@ -113,17 +113,25 @@ interface TableFilterProps { borderRadius: string; } +const defaultFilters = [{ ...DEFAULT_FILTER }]; +const getTableFilters = (filters: ReactTableFilter[] | undefined) => { + if (!filters || filters.length === 0) { + return defaultFilters; + } + return filters; +}; + function TableFilterPaneContent(props: TableFilterProps) { const [filters, updateFilters] = React.useState( - new Array(), + getTableFilters(props.filters), ); useEffect(() => { - const filters: ReactTableFilter[] = props.filters ? [...props.filters] : []; - if (filters.length === 0) { - filters.push({ ...DEFAULT_FILTER }); + const updatedFiltersState = getTableFilters(props.filters); + //if props has been updated update the filters state + if (updatedFiltersState !== filters) { + updateFilters(updatedFiltersState); } - updateFilters(filters); }, [props.filters]); const addFilter = () => { @@ -150,8 +158,8 @@ function TableFilterPaneContent(props: TableFilterProps) { }; const clearFilters = useCallback(() => { - props.applyFilter([{ ...DEFAULT_FILTER }]); - }, []); + props.applyFilter(defaultFilters); + }, [props]); const columns: DropdownOption[] = props.columns .map((column: ReactTableColumnProps) => { diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/index.tsx b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/index.tsx index a39ab2b52d..4d6456b8d9 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/index.tsx @@ -105,5 +105,5 @@ function TableFilters(props: TableFilterProps) { ); } - -export default TableFilters; +const TableFiltersMemoised = React.memo(TableFilters); +export default TableFiltersMemoised; diff --git a/app/client/src/widgets/TableWidgetV2/component/header/banner/AddNewRowBanner.tsx b/app/client/src/widgets/TableWidgetV2/component/header/banner/AddNewRowBanner.tsx index b5743606be..b2dbdc7c65 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/banner/AddNewRowBanner.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/banner/AddNewRowBanner.tsx @@ -37,7 +37,7 @@ export interface AddNewRowBannerType { disabledAddNewRowSave: boolean; } -export function AddNewRowBanner(props: AddNewRowBannerType) { +function AddNewRowBannerComponent(props: AddNewRowBannerType) { const [isDiscardLoading, setIsDiscardLoading] = useState(false); const [isSaveLoading, setIsSaveLoading] = useState(false); @@ -81,3 +81,4 @@ export function AddNewRowBanner(props: AddNewRowBannerType) { ); } +export const AddNewRowBanner = React.memo(AddNewRowBannerComponent); diff --git a/app/client/src/widgets/TableWidgetV2/component/header/banner/index.tsx b/app/client/src/widgets/TableWidgetV2/component/header/banner/index.tsx index 4076603306..5e98f39282 100644 --- a/app/client/src/widgets/TableWidgetV2/component/header/banner/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/header/banner/index.tsx @@ -6,7 +6,7 @@ export interface BannerPropType extends AddNewRowBannerType { isAddRowInProgress: boolean; } -export function Banner(props: BannerPropType) { +function BannerComponent(props: BannerPropType) { return props.isAddRowInProgress ? ( ) : null; } +export const Banner = React.memo(BannerComponent); diff --git a/app/client/src/widgets/TableWidgetV2/component/index.tsx b/app/client/src/widgets/TableWidgetV2/component/index.tsx index 05e13865c2..197956cd01 100644 --- a/app/client/src/widgets/TableWidgetV2/component/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/index.tsx @@ -162,44 +162,47 @@ function ReactTableComponent(props: ReactTableComponentProps) { width, } = props; - const sortTableColumn = (columnIndex: number, asc: boolean) => { - if (allowSorting) { - if (columnIndex === -1) { - _sortTableColumn("", asc); - } else { - const column = columns[columnIndex]; - const columnType = column.metaProperties?.type || ColumnTypes.TEXT; - if ( - columnType !== ColumnTypes.IMAGE && - columnType !== ColumnTypes.VIDEO - ) { - _sortTableColumn(column.alias, asc); + const sortTableColumn = useCallback( + (columnIndex: number, asc: boolean) => { + if (allowSorting) { + if (columnIndex === -1) { + _sortTableColumn("", asc); + } else { + const column = columns[columnIndex]; + const columnType = column.metaProperties?.type || ColumnTypes.TEXT; + if ( + columnType !== ColumnTypes.IMAGE && + columnType !== ColumnTypes.VIDEO + ) { + _sortTableColumn(column.alias, asc); + } } } - } - }; + }, + [_sortTableColumn, allowSorting, columns], + ); - const selectTableRow = (row: { - original: Record; - index: number; - }) => { - if (allowRowSelection) { - onRowClick(row.original, row.index); - } - }; - - const toggleAllRowSelect = ( - isSelect: boolean, - pageData: Row>[], - ) => { - if (allowRowSelection) { - if (isSelect) { - selectAllRow(pageData); - } else { - unSelectAllRow(pageData); + const selectTableRow = useCallback( + (row: { original: Record; index: number }) => { + if (allowRowSelection) { + onRowClick(row.original, row.index); } - } - }; + }, + [allowRowSelection, onRowClick], + ); + + const toggleAllRowSelect = useCallback( + (isSelect: boolean, pageData: Row>[]) => { + if (allowRowSelection) { + if (isSelect) { + selectAllRow(pageData); + } else { + unSelectAllRow(pageData); + } + } + }, + [allowRowSelection, selectAllRow, unSelectAllRow], + ); const memoziedDisableDrag = useCallback( () => disableDrag(true), @@ -310,11 +313,13 @@ export default React.memo(ReactTableComponent, (prev, next) => { prev.borderWidth === next.borderWidth && prev.borderColor === next.borderColor && prev.accentColor === next.accentColor && + //shallow equal possible equal(prev.columnWidthMap, next.columnWidthMap) && - equal(prev.tableData, next.tableData) && + //static reference + prev.tableData === next.tableData && // Using JSON stringify becuase isEqual doesnt work with functions, // and we are not changing the columns manually. - JSON.stringify(prev.columns) === JSON.stringify(next.columns) && + prev.columns === next.columns && equal(prev.editableCell, next.editableCell) && prev.variant === next.variant && prev.primaryColumnId === next.primaryColumnId && diff --git a/app/client/src/widgets/TableWidgetV2/widget/index.tsx b/app/client/src/widgets/TableWidgetV2/widget/index.tsx index 72e69757c4..61be5bc9df 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/widget/index.tsx @@ -1,21 +1,19 @@ import React, { lazy, Suspense } from "react"; import log from "loglevel"; -import type { MomentInput } from "moment"; -import moment from "moment"; +import memoizeOne from "memoize-one"; + import _, { isNumber, isString, isNil, xor, without, - isBoolean, isArray, xorWith, isEmpty, union, isObject, pickBy, - findIndex, orderBy, filter, } from "lodash"; @@ -27,12 +25,8 @@ import { RenderModes, WIDGET_PADDING } from "constants/WidgetConstants"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import Skeleton from "components/utils/Skeleton"; import { noop, retryPromise } from "utils/AppsmithUtils"; -import type { ReactTableFilter } from "../component/Constants"; -import { - AddNewRowActions, - StickyType, - DEFAULT_FILTER, -} from "../component/Constants"; +import type { ReactTableFilter, StickyType } from "../component/Constants"; +import { AddNewRowActions, DEFAULT_FILTER } from "../component/Constants"; import type { EditableCell, OnColumnEventArgs, @@ -42,8 +36,6 @@ import type { import { ActionColumnTypes, ColumnTypes, - COLUMN_MIN_WIDTH, - DateInputFormat, defaultEditableCell, DEFAULT_BUTTON_LABEL, DEFAULT_COLUMN_WIDTH, @@ -53,7 +45,6 @@ import { InlineEditingSaveOptions, ORIGINAL_INDEX_KEY, TABLE_COLUMN_ORDER_KEY, - DEFAULT_COLUMN_NAME, } from "../constants"; import derivedProperties from "./parseDerivedProperties"; import { @@ -68,7 +59,6 @@ import { getColumnType, getBooleanPropertyValue, deleteLocalTableColumnOrderByWidgetId, - fetchSticky, getColumnOrderByWidgetIdFromLS, generateLocalNewColumnOrderFromStickyValue, updateAndSyncTableLocalColumnOrders, @@ -106,13 +96,39 @@ import { DateCell } from "../component/cellComponents/DateCell"; import type { MenuItem } from "widgets/MenuButtonWidget/constants"; import { MenuItemsSource } from "widgets/MenuButtonWidget/constants"; import { TimePrecision } from "widgets/DatePickerWidget2/constants"; +import type { getColumns } from "./reactTableUtils/getColumnsPureFn"; +import { getMemoiseGetColumnsWithLocalStorageFn } from "./reactTableUtils/getColumnsPureFn"; +import type { + tableData, + transformDataWithEditableCell, +} from "./reactTableUtils/transformDataPureFn"; +import { getMemoiseTransformDataWithEditableCell } from "./reactTableUtils/transformDataPureFn"; const ReactTableComponent = lazy(() => retryPromise(() => import("../component")), ); +const emptyArr: any = []; + +type addNewRowToTable = ( + tableData: tableData, + isAddRowInProgress: boolean, + newRowContent: Record, +) => tableData; + +const getMemoisedAddNewRow = (): addNewRowToTable => + memoizeOne((tableData, isAddRowInProgress, newRowContent) => { + if (isAddRowInProgress) { + return [newRowContent, ...tableData]; + } + return tableData; + }); + class TableWidgetV2 extends BaseWidget { inlineEditTimer: number | null = null; + memoisedAddNewRow: addNewRowToTable; + memoiseGetColumnsWithLocalStorage: (localStorage: any) => getColumns; + memoiseTransformDataWithEditableCell: transformDataWithEditableCell; static getPropertyPaneContentConfig() { return contentConfig; @@ -121,6 +137,15 @@ class TableWidgetV2 extends BaseWidget { static getPropertyPaneStyleConfig() { return styleConfig; } + constructor(props: TableWidgetProps) { + super(props); + // generate new cache instances so that each table widget instance has its own respective cache instance + this.memoisedAddNewRow = getMemoisedAddNewRow(); + this.memoiseGetColumnsWithLocalStorage = + getMemoiseGetColumnsWithLocalStorageFn(); + this.memoiseTransformDataWithEditableCell = + getMemoiseTransformDataWithEditableCell(); + } static getMetaPropertiesMap(): Record { return { @@ -213,228 +238,37 @@ class TableWidgetV2 extends BaseWidget { * based on columnType */ getTableColumns = () => { - const { columnWidthMap = {}, orderedTableColumns = [] } = this.props; - let columns: ReactTableColumnProps[] = []; - const hiddenColumns: ReactTableColumnProps[] = []; - + const { + columnWidthMap, + orderedTableColumns, + primaryColumns, + renderMode, + widgetId, + } = this.props; const { componentWidth } = this.getPaddingAdjustedDimensions(); - let totalColumnWidth = 0; - - if (isArray(orderedTableColumns)) { - orderedTableColumns.forEach((column: any) => { - const isHidden = !column.isVisible; - - const columnData = { - id: column.id, - Header: - column.hasOwnProperty("label") && typeof column.label === "string" - ? column.label - : DEFAULT_COLUMN_NAME, - alias: column.alias, - accessor: (row: any) => row[column.alias], - width: columnWidthMap[column.id] || DEFAULT_COLUMN_WIDTH, - minWidth: COLUMN_MIN_WIDTH, - draggable: true, - isHidden: false, - isAscOrder: column.isAscOrder, - isDerived: column.isDerived, - sticky: fetchSticky( - column.id, - this.props.primaryColumns, - this.props.renderMode, - this.props.widgetId, - ), - metaProperties: { - isHidden: isHidden, - type: column.columnType, - format: column.outputFormat || "", - inputFormat: column.inputFormat || "", - }, - columnProperties: column, - Cell: this.renderCell, - }; - - const isAllCellVisible: boolean | boolean[] = column.isCellVisible; - - /* - * If all cells are not visible or column itself is not visible, - * set isHidden and push it to hiddenColumns array else columns array - */ - if ( - (isBoolean(isAllCellVisible) && !isAllCellVisible) || - (isArray(isAllCellVisible) && - isAllCellVisible.every((visibility) => visibility === false)) || - isHidden - ) { - columnData.isHidden = true; - hiddenColumns.push(columnData); - } else { - totalColumnWidth += columnData.width; - columns.push(columnData); - } - }); - } - - const lastColumnIndex = columns.length - 1; - if (totalColumnWidth < componentWidth) { - /* - This "if" block is responsible for upsizing the last column width - if there is space left in the table container towards the right - */ - if (columns[lastColumnIndex]) { - const lastColumnWidth = - columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH; - const remainingWidth = componentWidth - totalColumnWidth; - // Adding the remaining width i.e. space left towards the right, to the last column width - columns[lastColumnIndex].width = lastColumnWidth + remainingWidth; - } - } else if (totalColumnWidth > componentWidth) { - /* - This "else-if" block is responsible for downsizing the last column width - if the last column spills over resulting in horizontal scroll - */ - const extraWidth = totalColumnWidth - componentWidth; - const lastColWidth = - columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH; - /* - Below if condition explanation: - Condition 1: (lastColWidth > COLUMN_MIN_WIDTH) - We will downsize the last column only if its greater than COLUMN_MIN_WIDTH - Condition 2: (extraWidth < lastColWidth) - This condition checks whether the last column is the only column that is spilling over. - If more than one columns are spilling over we won't downsize the last column - */ - if (lastColWidth > COLUMN_MIN_WIDTH && extraWidth < lastColWidth) { - const availableWidthForLastColumn = lastColWidth - extraWidth; - /* - Below we are making sure last column width doesn't go lower than COLUMN_MIN_WIDTH again - as availableWidthForLastColumn might go lower than COLUMN_MIN_WIDTH in some cases - */ - columns[lastColumnIndex].width = - availableWidthForLastColumn < COLUMN_MIN_WIDTH - ? COLUMN_MIN_WIDTH - : availableWidthForLastColumn; - } - } - - if (hiddenColumns.length && this.props.renderMode === RenderModes.CANVAS) { - // Get the index of the first column that is frozen to right - const rightFrozenColumnIdx = findIndex( - columns, - (col) => col.sticky === StickyType.RIGHT, - ); - if (rightFrozenColumnIdx !== -1) { - columns.splice(rightFrozenColumnIdx, 0, ...hiddenColumns); - } else { - columns = columns.concat(hiddenColumns); - } - } - - return columns.filter((column: ReactTableColumnProps) => !!column.id); + const widgetLocalStorageState = getColumnOrderByWidgetIdFromLS(widgetId); + const memoisdGetColumnsWithLocalStorage = + this.memoiseGetColumnsWithLocalStorage(widgetLocalStorageState); + return memoisdGetColumnsWithLocalStorage( + this.renderCell, + columnWidthMap, + orderedTableColumns, + componentWidth, + primaryColumns, + renderMode, + widgetId, + ); }; transformData = ( tableData: Array>, columns: ReactTableColumnProps[], ) => { - if (isArray(tableData)) { - return tableData.map((row, rowIndex) => { - const newRow: { [key: string]: any } = {}; - - columns.forEach((column) => { - const { alias } = column; - let value = row[alias]; - - if (column.metaProperties) { - switch (column.metaProperties.type) { - case ColumnTypes.DATE: - let isValidDate = true; - const outputFormat = _.isArray(column.metaProperties.format) - ? column.metaProperties.format[rowIndex] - : column.metaProperties.format; - let inputFormat; - - try { - const type = _.isArray(column.metaProperties.inputFormat) - ? column.metaProperties.inputFormat[rowIndex] - : column.metaProperties.inputFormat; - - if ( - type !== DateInputFormat.EPOCH && - type !== DateInputFormat.MILLISECONDS - ) { - inputFormat = type; - moment(value as MomentInput, inputFormat); - } else if (!isNumber(value)) { - isValidDate = false; - } - } catch (e) { - isValidDate = false; - } - - if (isValidDate && value) { - try { - if ( - column.metaProperties.inputFormat === - DateInputFormat.MILLISECONDS - ) { - value = Number(value); - } else if ( - column.metaProperties.inputFormat === - DateInputFormat.EPOCH - ) { - value = 1000 * Number(value); - } - - newRow[alias] = moment( - value as MomentInput, - inputFormat, - ).format(outputFormat); - } catch (e) { - log.debug("Unable to parse Date:", { e }); - newRow[alias] = ""; - } - } else if (value) { - newRow[alias] = "Invalid Value"; - } else { - newRow[alias] = ""; - } - break; - default: - let data; - - if ( - _.isString(value) || - _.isNumber(value) || - _.isBoolean(value) - ) { - data = value; - } else if (isNil(value)) { - data = ""; - } else { - data = JSON.stringify(value); - } - - newRow[alias] = data; - break; - } - } - }); - - /* - * Inject the edited cell value from the editableCell object - */ - if (this.props.editableCell?.index === rowIndex) { - const { column, inputValue } = this.props.editableCell; - - newRow[column] = inputValue; - } - - return newRow; - }); - } else { - return []; - } + return this.memoiseTransformDataWithEditableCell( + this.props.editableCell, + tableData, + columns, + ); }; updateDerivedColumnsIndex = ( @@ -595,6 +429,7 @@ class TableWidgetV2 extends BaseWidget { } }; + //no need to batch meta updates hydrateStickyColumns = () => { const localTableColumnOrder = getColumnOrderByWidgetIdFromLS( this.props.widgetId, @@ -642,6 +477,7 @@ class TableWidgetV2 extends BaseWidget { const { canFreezeColumn, renderMode, tableData } = this.props; if (canFreezeColumn && renderMode === RenderModes.PAGE) { + //dont neet to batch this since single action this.hydrateStickyColumns(); } @@ -690,21 +526,23 @@ class TableWidgetV2 extends BaseWidget { ); } } + + //check if necessary we are batching now updates // Check if tableData is modifed const isTableDataModified = !equal( this.props.tableData, prevProps.tableData, ); - + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; // If the user has changed the tableData OR // The binding has returned a new value if (isTableDataModified) { - this.updateMetaRowData( + this.pushMetaRowDataUpdates( prevProps.filteredTableData, this.props.filteredTableData, ); - this.props.updateWidgetMetaProperty("triggeredRowIndex", -1); + pushBatchMetaUpdates("triggeredRowIndex", -1); const newColumnIds: string[] = getAllTableColumnKeys( this.props.tableData, @@ -720,7 +558,7 @@ class TableWidgetV2 extends BaseWidget { this.updateColumnProperties(newTableColumns); } - this.props.updateWidgetMetaProperty("filters", [DEFAULT_FILTER]); + pushBatchMetaUpdates("filters", [DEFAULT_FILTER]); } } @@ -728,16 +566,16 @@ class TableWidgetV2 extends BaseWidget { * Clear transient table data and editablecell when tableData changes */ if (isTableDataModified) { - this.props.updateWidgetMetaProperty("transientTableData", {}); + pushBatchMetaUpdates("transientTableData", {}); // reset updatedRowIndex whenever transientTableData is flushed. - this.props.updateWidgetMetaProperty("updatedRowIndex", -1); + pushBatchMetaUpdates("updatedRowIndex", -1); - this.clearEditableCell(true); - this.props.updateWidgetMetaProperty("selectColumnFilterText", {}); + this.pushClearEditableCellsUpdates(); + pushBatchMetaUpdates("selectColumnFilterText", {}); } if (!pageNo) { - this.props.updateWidgetMetaProperty("pageNo", 1); + pushBatchMetaUpdates("pageNo", 1); } //check if pageNo does not excede the max Page no, due to change of totalRecordsCount @@ -745,13 +583,13 @@ class TableWidgetV2 extends BaseWidget { const maxAllowedPageNumber = Math.ceil(totalRecordsCount / pageSize); if (pageNo > maxAllowedPageNumber) { - this.props.updateWidgetMetaProperty("pageNo", maxAllowedPageNumber); + pushBatchMetaUpdates("pageNo", maxAllowedPageNumber); } } else if ( serverSidePaginationEnabled !== prevProps.serverSidePaginationEnabled ) { //reset pageNo when serverSidePaginationEnabled is toggled - this.props.updateWidgetMetaProperty("pageNo", 1); + pushBatchMetaUpdates("pageNo", 1); } /* @@ -762,20 +600,21 @@ class TableWidgetV2 extends BaseWidget { !equal(defaultSelectedRowIndex, prevProps.defaultSelectedRowIndex) || !equal(defaultSelectedRowIndices, prevProps.defaultSelectedRowIndices) ) { - this.updateSelectedRowIndex(); + this.pushUpdateSelectedRowIndexUpdates(); } - this.resetPageNo(prevProps); + this.pushResetPageNoUpdates(prevProps); - this.resetRowSelectionProperties(prevProps); + this.pushResetRowSelectionPropertiesUpdates(prevProps); + commitBatchMetaUpdates(); } - resetPageNo = (prevProps: TableWidgetProps) => { - const { onPageSizeChange, pageSize } = this.props; + pushResetPageNoUpdates = (prevProps: TableWidgetProps) => { + const { onPageSizeChange, pageSize, pushBatchMetaUpdates } = this.props; if (pageSize !== prevProps.pageSize) { if (onPageSizeChange) { - this.props.updateWidgetMetaProperty("pageNo", 1, { + pushBatchMetaUpdates("pageNo", 1, { triggerPropertyName: "onPageSizeChange", dynamicString: onPageSizeChange, event: { @@ -783,16 +622,17 @@ class TableWidgetV2 extends BaseWidget { }, }); } else { - this.props.updateWidgetMetaProperty("pageNo", 1); + pushBatchMetaUpdates("pageNo", 1); } } }; - resetRowSelectionProperties = (prevProps: TableWidgetProps) => { + pushResetRowSelectionPropertiesUpdates = (prevProps: TableWidgetProps) => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, + pushBatchMetaUpdates, } = this.props; // reset selectedRowIndices and selectedRowIndex to defaults @@ -803,22 +643,16 @@ class TableWidgetV2 extends BaseWidget { _.isArray(defaultSelectedRowIndices) && defaultSelectedRowIndices.every((i) => _.isFinite(i)) ) { - this.props.updateWidgetMetaProperty( - "selectedRowIndices", - defaultSelectedRowIndices, - ); + pushBatchMetaUpdates("selectedRowIndices", defaultSelectedRowIndices); } - this.props.updateWidgetMetaProperty("selectedRowIndex", -1); + pushBatchMetaUpdates("selectedRowIndex", -1); } else { if (!isNil(defaultSelectedRowIndex) && defaultSelectedRowIndex > -1) { - this.props.updateWidgetMetaProperty( - "selectedRowIndex", - defaultSelectedRowIndex, - ); + pushBatchMetaUpdates("selectedRowIndex", defaultSelectedRowIndex); } - this.props.updateWidgetMetaProperty("selectedRowIndices", []); + pushBatchMetaUpdates("selectedRowIndices", []); } } }; @@ -827,30 +661,25 @@ class TableWidgetV2 extends BaseWidget { * Function to update selectedRowIndices & selectedRowIndex from * defaultSelectedRowIndices & defaultSelectedRowIndex respectively */ - updateSelectedRowIndex = () => { + pushUpdateSelectedRowIndexUpdates = () => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, + pushBatchMetaUpdates, } = this.props; if (multiRowSelection) { - this.props.updateWidgetMetaProperty( - "selectedRowIndices", - defaultSelectedRowIndices, - ); + pushBatchMetaUpdates("selectedRowIndices", defaultSelectedRowIndices); } else { - this.props.updateWidgetMetaProperty( - "selectedRowIndex", - defaultSelectedRowIndex, - ); + pushBatchMetaUpdates("selectedRowIndex", defaultSelectedRowIndex); } }; /* * Function to update selectedRow details when order of tableData changes */ - updateMetaRowData = ( + pushMetaRowDataUpdates = ( oldTableData: Array>, newTableData: Array>, ) => { @@ -859,6 +688,7 @@ class TableWidgetV2 extends BaseWidget { defaultSelectedRowIndices, multiRowSelection, primaryColumnId, + pushBatchMetaUpdates, selectedRowIndex, selectedRowIndices, } = this.props; @@ -871,8 +701,7 @@ class TableWidgetV2 extends BaseWidget { selectedRowIndices, primaryColumnId, ); - - this.props.updateWidgetMetaProperty("selectedRowIndices", indices); + pushBatchMetaUpdates("selectedRowIndices", indices); } else { const index = getSelectRowIndex( oldTableData, @@ -881,8 +710,7 @@ class TableWidgetV2 extends BaseWidget { selectedRowIndex, primaryColumnId, ); - - this.props.updateWidgetMetaProperty("selectedRowIndex", index); + pushBatchMetaUpdates("selectedRowIndex", index); } }; @@ -907,13 +735,17 @@ class TableWidgetV2 extends BaseWidget { }; updateFilters = (filters: ReactTableFilter[]) => { - this.resetSelectedRowIndex(); - this.props.updateWidgetMetaProperty("filters", filters); + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + + this.pushResetSelectedRowIndexUpdates(); + + pushBatchMetaUpdates("filters", filters); // Reset Page only when a filter is added if (!isEmpty(xorWith(filters, [DEFAULT_FILTER], equal))) { - this.props.updateWidgetMetaProperty("pageNo", 1); + pushBatchMetaUpdates("pageNo", 1); } + commitBatchMetaUpdates(); }; toggleDrag = (disable: boolean) => { @@ -939,7 +771,7 @@ class TableWidgetV2 extends BaseWidget { isVisiblePagination, isVisibleSearch, } = this.props; - const tableColumns = this.getTableColumns() || []; + const tableColumns = this.getTableColumns() || emptyArr; const transformedData = this.transformData(filteredTableData, tableColumns); const isVisibleHeaderOptions = isVisibleDownload || @@ -949,10 +781,11 @@ class TableWidgetV2 extends BaseWidget { const { componentHeight, componentWidth } = this.getPaddingAdjustedDimensions(); - - if (this.props.isAddRowInProgress) { - transformedData.unshift(this.props.newRowContent); - } + const finalTableData = this.memoisedAddNewRow( + transformedData, + this.props.isAddRowInProgress, + this.props.newRowContent, + ); return ( }> @@ -1014,7 +847,7 @@ class TableWidgetV2 extends BaseWidget { selectedRowIndices={this.getSelectedRowIndices()} serverSidePaginationEnabled={!!this.props.serverSidePaginationEnabled} sortTableColumn={this.handleColumnSorting} - tableData={transformedData} + tableData={finalTableData} totalRecordsCount={totalRecordsCount} triggerRowSelection={this.props.triggerRowSelection} unSelectAllRow={this.unSelectAllRow} @@ -1143,6 +976,7 @@ class TableWidgetV2 extends BaseWidget { updatedOrders.leftOrder, updatedOrders.rightOrder, ); + // only a single meta property update no need to batch this this.props.updateWidgetMetaProperty("columnOrder", newColumnOrder); } } @@ -1165,13 +999,17 @@ class TableWidgetV2 extends BaseWidget { this.persistColumnOrder(columnOrder, [], []); } } + // only a single meta property update no need to batch this + this.props.updateWidgetMetaProperty("columnOrder", columnOrder); } }; handleColumnSorting = (columnAccessor: string, isAsc: boolean) => { const columnId = this.getColumnIdByAlias(columnAccessor); - this.resetSelectedRowIndex(false); + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + + this.pushResetSelectedRowIndexUpdates(false); let sortOrderProps; @@ -1187,46 +1025,84 @@ class TableWidgetV2 extends BaseWidget { }; } - this.props.updateWidgetMetaProperty("sortOrder", sortOrderProps, { + pushBatchMetaUpdates("sortOrder", sortOrderProps, { triggerPropertyName: "onSort", dynamicString: this.props.onSort, event: { type: EventType.ON_SORT, }, }); + commitBatchMetaUpdates(); }; handleResizeColumn = (columnWidthMap: { [key: string]: number }) => { if (this.props.renderMode === RenderModes.CANVAS) { super.updateWidgetProperty("columnWidthMap", columnWidthMap); } else { + //single action no need to batch this.props.updateWidgetMetaProperty("columnWidthMap", columnWidthMap); } }; handleSearchTable = (searchKey: any) => { - const { multiRowSelection, onSearchTextChanged } = this.props; + const { + commitBatchMetaUpdates, + multiRowSelection, + onSearchTextChanged, + pushBatchMetaUpdates, + } = this.props; /* * Clear rowSelection to avoid selecting filtered rows * based on stale selection indices */ if (multiRowSelection) { - this.props.updateWidgetMetaProperty("selectedRowIndices", []); + pushBatchMetaUpdates("selectedRowIndices", []); } else { - this.props.updateWidgetMetaProperty("selectedRowIndex", -1); + pushBatchMetaUpdates("selectedRowIndex", -1); } - this.props.updateWidgetMetaProperty("pageNo", 1); - this.props.updateWidgetMetaProperty("searchText", searchKey, { + pushBatchMetaUpdates("pageNo", 1); + pushBatchMetaUpdates("searchText", searchKey, { triggerPropertyName: "onSearchTextChanged", dynamicString: onSearchTextChanged, event: { type: EventType.ON_SEARCH, }, }); + + commitBatchMetaUpdates(); }; + /** + * This function just pushes the meta update + */ + pushOnColumnEvent = ({ + rowIndex, + action, + onComplete = noop, + triggerPropertyName, + eventType, + row, + additionalData = {}, + }: OnColumnEventArgs) => { + const { filteredTableData = [], pushBatchMetaUpdates } = this.props; + + const currentRow = row || filteredTableData[rowIndex]; + pushBatchMetaUpdates( + "triggeredRowIndex", + currentRow?.[ORIGINAL_INDEX_KEY], + { + triggerPropertyName: triggerPropertyName, + dynamicString: action, + event: { + type: eventType, + callback: onComplete, + }, + globalContext: { currentRow, ...additionalData }, + }, + ); + }; /* * Function to handle customColumn button type click interactions */ @@ -1239,30 +1115,21 @@ class TableWidgetV2 extends BaseWidget { row, additionalData = {}, }: OnColumnEventArgs) => { - const { filteredTableData = [] } = this.props; + if (action) { + const { commitBatchMetaUpdates } = this.props; - try { - row = row || filteredTableData[rowIndex]; - - if (action) { - this.props.updateWidgetMetaProperty( - "triggeredRowIndex", - row?.[ORIGINAL_INDEX_KEY], - { - triggerPropertyName: triggerPropertyName, - dynamicString: action, - event: { - type: eventType, - callback: onComplete, - }, - globalContext: { currentRow: row, ...additionalData }, - }, - ); - } else { - onComplete(); - } - } catch (error) { - log.debug("Error parsing row action", error); + this.pushOnColumnEvent({ + rowIndex, + action, + onComplete, + triggerPropertyName, + eventType, + row, + additionalData, + }); + commitBatchMetaUpdates(); + } else { + onComplete(); } }; @@ -1280,6 +1147,7 @@ class TableWidgetV2 extends BaseWidget { const selectedRowIndices = pageData.map( (row: Record) => row.index, ); + //single action no need to batch this.props.updateWidgetMetaProperty( "selectedRowIndices", selectedRowIndices, @@ -1290,6 +1158,7 @@ class TableWidgetV2 extends BaseWidget { handleRowClick = (row: Record, selectedIndex: number) => { const { multiRowSelection, selectedRowIndex, selectedRowIndices } = this.props; + // no need to batch actions here because it a time only one will execute if (multiRowSelection) { let indices: Array; @@ -1344,8 +1213,10 @@ class TableWidgetV2 extends BaseWidget { }; updatePageNumber = (pageNo: number, event?: EventType) => { + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + if (event) { - this.props.updateWidgetMetaProperty("pageNo", pageNo, { + pushBatchMetaUpdates("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { @@ -1353,18 +1224,20 @@ class TableWidgetV2 extends BaseWidget { }, }); } else { - this.props.updateWidgetMetaProperty("pageNo", pageNo); + pushBatchMetaUpdates("pageNo", pageNo); } if (this.props.onPageChange) { - this.resetSelectedRowIndex(); + this.pushResetSelectedRowIndexUpdates(); } + commitBatchMetaUpdates(); }; handleNextPageClick = () => { const pageNo = (this.props.pageNo || 1) + 1; + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; - this.props.updateWidgetMetaProperty("pageNo", pageNo, { + pushBatchMetaUpdates("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { @@ -1373,11 +1246,14 @@ class TableWidgetV2 extends BaseWidget { }); if (this.props.onPageChange) { - this.resetSelectedRowIndex(); + this.pushResetSelectedRowIndexUpdates(); } + commitBatchMetaUpdates(); }; - resetSelectedRowIndex = (skipDefault?: boolean) => { + pushResetSelectedRowIndexUpdates = (skipDefault?: boolean) => { + const { pushBatchMetaUpdates } = this.props; + const { defaultSelectedRowIndex, defaultSelectedRowIndices, @@ -1385,12 +1261,12 @@ class TableWidgetV2 extends BaseWidget { } = this.props; if (multiRowSelection) { - this.props.updateWidgetMetaProperty( + pushBatchMetaUpdates( "selectedRowIndices", skipDefault ? [] : defaultSelectedRowIndices, ); } else { - this.props.updateWidgetMetaProperty( + pushBatchMetaUpdates( "selectedRowIndex", skipDefault ? -1 : defaultSelectedRowIndex, ); @@ -1403,9 +1279,10 @@ class TableWidgetV2 extends BaseWidget { handlePrevPageClick = () => { const pageNo = (this.props.pageNo || 1) - 1; + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; if (pageNo >= 1) { - this.props.updateWidgetMetaProperty("pageNo", pageNo, { + pushBatchMetaUpdates("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { @@ -1414,9 +1291,10 @@ class TableWidgetV2 extends BaseWidget { }); if (this.props.onPageChange) { - this.resetSelectedRowIndex(); + this.pushResetSelectedRowIndexUpdates(); } } + commitBatchMetaUpdates(); }; static getWidgetType(): WidgetType { @@ -1445,10 +1323,11 @@ class TableWidgetV2 extends BaseWidget { }); } - updateTransientTableData = (data: TransientDataPayload) => { + pushTransientTableDataActionsUpdates = (data: TransientDataPayload) => { const { __originalIndex__, ...transientData } = data; + const { pushBatchMetaUpdates } = this.props; - this.props.updateWidgetMetaProperty("transientTableData", { + pushBatchMetaUpdates("transientTableData", { ...this.props.transientTableData, [__originalIndex__]: { ...this.props.transientTableData[__originalIndex__], @@ -1456,21 +1335,20 @@ class TableWidgetV2 extends BaseWidget { }, }); - this.props.updateWidgetMetaProperty("updatedRowIndex", __originalIndex__); + pushBatchMetaUpdates("updatedRowIndex", __originalIndex__); }; removeRowFromTransientTableData = (index: number) => { const newTransientTableData = clone(this.props.transientTableData); + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; if (newTransientTableData) { delete newTransientTableData[index]; - this.props.updateWidgetMetaProperty( - "transientTableData", - newTransientTableData, - ); + pushBatchMetaUpdates("transientTableData", newTransientTableData); } - this.props.updateWidgetMetaProperty("updatedRowIndex", -1); + pushBatchMetaUpdates("updatedRowIndex", -1); + commitBatchMetaUpdates(); }; getRowOriginalIndex = (index: number) => { @@ -2166,21 +2044,24 @@ class TableWidgetV2 extends BaseWidget { inputValue: string, alias: string, ) => { + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + if (this.props.isAddRowInProgress) { this.updateNewRowValues(alias, inputValue, value); } else { - this.props.updateWidgetMetaProperty("editableCell", { + pushBatchMetaUpdates("editableCell", { ...this.props.editableCell, value: value, inputValue, }); if (this.props.editableCell?.column) { - this.props.updateWidgetMetaProperty("columnEditableCellValue", { + pushBatchMetaUpdates("columnEditableCellValue", { ...this.props.columnEditableCellValue, [this.props.editableCell?.column]: value, }); } + commitBatchMetaUpdates(); } }; @@ -2200,8 +2081,9 @@ class TableWidgetV2 extends BaseWidget { if (this.inlineEditTimer) { clearTimeout(this.inlineEditTimer); } + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; - this.props.updateWidgetMetaProperty("editableCell", { + pushBatchMetaUpdates("editableCell", { column: alias, index: rowIndex, value: value, @@ -2209,7 +2091,7 @@ class TableWidgetV2 extends BaseWidget { initialValue: value, inputValue: value, }); - this.props.updateWidgetMetaProperty("columnEditableCellValue", { + pushBatchMetaUpdates("columnEditableCellValue", { ...this.props.columnEditableCellValue, [alias]: value, }); @@ -2221,24 +2103,28 @@ class TableWidgetV2 extends BaseWidget { */ if (this.props.sortOrder.column) { if (this.props.multiRowSelection) { - this.props.updateWidgetMetaProperty("selectedRowIndices", []); + pushBatchMetaUpdates("selectedRowIndices", []); } else { - this.props.updateWidgetMetaProperty("selectedRowIndex", -1); + pushBatchMetaUpdates("selectedRowIndex", -1); } } + commitBatchMetaUpdates(); } else { if ( this.isColumnCellValid(alias) && action === EditableCellActions.SAVE && value !== this.props.editableCell?.initialValue ) { - this.updateTransientTableData({ + const { commitBatchMetaUpdates } = this.props; + + this.pushTransientTableDataActionsUpdates({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [alias]: this.props.editableCell?.value, }); if (onSubmit && this.props.editableCell?.column) { - this.onColumnEvent({ + //since onSubmit is truthy that makes action truthy as well, so we can push this event + this.pushOnColumnEvent({ rowIndex: rowIndex, action: onSubmit, triggerPropertyName: "onSubmit", @@ -2249,6 +2135,7 @@ class TableWidgetV2 extends BaseWidget { }, }); } + commitBatchMetaUpdates(); this.clearEditableCell(); } else if ( @@ -2267,13 +2154,16 @@ class TableWidgetV2 extends BaseWidget { onSubmit?: string, ) => { if (this.isColumnCellValid(alias)) { - this.updateTransientTableData({ + const { commitBatchMetaUpdates } = this.props; + + this.pushTransientTableDataActionsUpdates({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [alias]: value, }); if (onSubmit && this.props.editableCell?.column) { - this.onColumnEvent({ + //since onSubmit is truthy this makes action truthy as well, so we can push this event + this.pushOnColumnEvent({ rowIndex: rowIndex, action: onSubmit, triggerPropertyName: "onSubmit", @@ -2285,14 +2175,23 @@ class TableWidgetV2 extends BaseWidget { }); } + commitBatchMetaUpdates(); this.clearEditableCell(); } }; + pushClearEditableCellsUpdates = () => { + const { pushBatchMetaUpdates } = this.props; + + pushBatchMetaUpdates("editableCell", defaultEditableCell); + pushBatchMetaUpdates("columnEditableCellValue", {}); + }; clearEditableCell = (skipTimeout?: boolean) => { const clear = () => { - this.props.updateWidgetMetaProperty("editableCell", defaultEditableCell); - this.props.updateWidgetMetaProperty("columnEditableCellValue", {}); + const { commitBatchMetaUpdates } = this.props; + + this.pushClearEditableCellsUpdates(); + commitBatchMetaUpdates(); }; if (skipTimeout) { @@ -2323,17 +2222,19 @@ class TableWidgetV2 extends BaseWidget { if (this.props.isAddRowInProgress) { this.updateNewRowValues(column, value, value); } else { - this.updateTransientTableData({ + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + + this.pushTransientTableDataActionsUpdates({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [column]: value, }); - - this.props.updateWidgetMetaProperty("editableCell", defaultEditableCell); + pushBatchMetaUpdates("editableCell", defaultEditableCell); if (action && this.props.editableCell?.column) { - this.onColumnEvent({ + //since action is truthy we can push this event + this.pushOnColumnEvent({ rowIndex, - action: action, + action, triggerPropertyName: "onOptionChange", eventType: EventType.ON_OPTION_CHANGE, row: { @@ -2342,6 +2243,7 @@ class TableWidgetV2 extends BaseWidget { }, }); } + commitBatchMetaUpdates(); } }; @@ -2352,15 +2254,18 @@ class TableWidgetV2 extends BaseWidget { alias: string, action?: string, ) => { - this.props.updateWidgetMetaProperty("selectColumnFilterText", { + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + + pushBatchMetaUpdates("selectColumnFilterText", { ...this.props.selectColumnFilterText, [alias]: text, }); if (action && serverSideFiltering) { - this.onColumnEvent({ + //since action is truthy we can push this event + this.pushOnColumnEvent({ rowIndex, - action: action, + action, triggerPropertyName: "onFilterUpdate", eventType: EventType.ON_FILTER_UPDATE, row: { @@ -2371,6 +2276,7 @@ class TableWidgetV2 extends BaseWidget { }, }); } + commitBatchMetaUpdates(); }; onCheckChange = ( @@ -2384,11 +2290,14 @@ class TableWidgetV2 extends BaseWidget { if (this.props.isAddRowInProgress) { this.updateNewRowValues(alias, value, value); } else { - this.updateTransientTableData({ + const { commitBatchMetaUpdates } = this.props; + + this.pushTransientTableDataActionsUpdates({ [ORIGINAL_INDEX_KEY]: originalIndex, [alias]: value, }); - + commitBatchMetaUpdates(); + //cannot batch this update because we are not sure if it action is truthy or not this.onColumnEvent({ rowIndex, action: column.onCheckChange, @@ -2404,19 +2313,22 @@ class TableWidgetV2 extends BaseWidget { handleAddNewRowClick = () => { const defaultNewRow = this.props.defaultNewRow || {}; - this.props.updateWidgetMetaProperty("isAddRowInProgress", true); - this.props.updateWidgetMetaProperty("newRowContent", defaultNewRow); - this.props.updateWidgetMetaProperty("newRow", defaultNewRow); + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + + pushBatchMetaUpdates("isAddRowInProgress", true); + pushBatchMetaUpdates("newRowContent", defaultNewRow); + pushBatchMetaUpdates("newRow", defaultNewRow); // New row gets added at the top of page 1 when client side pagination enabled if (!this.props.serverSidePaginationEnabled) { - this.props.updateWidgetMetaProperty("pageNo", 1); + pushBatchMetaUpdates("pageNo", 1); } //Since we're adding a newRowContent thats not part of tableData, the index changes // so we're resetting the row selection - this.props.updateWidgetMetaProperty("selectedRowIndex", -1); - this.props.updateWidgetMetaProperty("selectedRowIndices", []); + pushBatchMetaUpdates("selectedRowIndex", -1); + pushBatchMetaUpdates("selectedRowIndices", []); + commitBatchMetaUpdates(); }; handleAddNewRowAction = ( @@ -2426,9 +2338,13 @@ class TableWidgetV2 extends BaseWidget { let triggerPropertyName, action, eventType; const onComplete = () => { - this.props.updateWidgetMetaProperty("isAddRowInProgress", false); - this.props.updateWidgetMetaProperty("newRowContent", undefined); - this.props.updateWidgetMetaProperty("newRow", undefined); + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + + pushBatchMetaUpdates("isAddRowInProgress", false); + pushBatchMetaUpdates("newRowContent", undefined); + pushBatchMetaUpdates("newRow", undefined); + commitBatchMetaUpdates(); + onActionComplete(); }; @@ -2477,19 +2393,22 @@ class TableWidgetV2 extends BaseWidget { value: unknown, parsedValue: unknown, ) => { + const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + /* * newRowContent holds whatever the user types while newRow holds the parsed value * newRowContent is being used to populate the cell while newRow is being used * for validations. */ - this.props.updateWidgetMetaProperty("newRowContent", { + pushBatchMetaUpdates("newRowContent", { ...this.props.newRowContent, [alias]: value, }); - this.props.updateWidgetMetaProperty("newRow", { + pushBatchMetaUpdates("newRow", { ...this.props.newRow, [alias]: parsedValue, }); + commitBatchMetaUpdates(); }; } diff --git a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/getColumnsPureFn.tsx b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/getColumnsPureFn.tsx new file mode 100644 index 0000000000..11a42d902b --- /dev/null +++ b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/getColumnsPureFn.tsx @@ -0,0 +1,164 @@ +import { isBoolean, isArray, findIndex, isEqual } from "lodash"; +import type { RenderMode } from "constants/WidgetConstants"; +import { RenderModes } from "constants/WidgetConstants"; +import { StickyType } from "../../component/Constants"; +import { + COLUMN_MIN_WIDTH, + DEFAULT_COLUMN_WIDTH, + DEFAULT_COLUMN_NAME, +} from "../../constants"; +import { fetchSticky } from "../utilities"; +import type { + ColumnProperties, + ReactTableColumnProps, +} from "../../component/Constants"; +import memoizeOne from "memoize-one"; + +export type getColumns = ( + renderCell: any, + columnWidthMap: { [key: string]: number } | undefined, + orderedTableColumns: any, + componentWidth: number, + primaryColumns: Record, + renderMode: RenderMode, + widgetId: string, +) => ReactTableColumnProps[]; + +//TODO: (Vamsi) need to unit test this function + +export const getColumnsPureFn: getColumns = ( + renderCell, + columnWidthMap = {}, + orderedTableColumns = [], + componentWidth, + primaryColumns, + renderMode, + widgetId, +) => { + let columns: ReactTableColumnProps[] = []; + const hiddenColumns: ReactTableColumnProps[] = []; + + let totalColumnWidth = 0; + + if (isArray(orderedTableColumns)) { + orderedTableColumns.forEach((column: any) => { + const isHidden = !column.isVisible; + + const columnData = { + id: column.id, + Header: + column.hasOwnProperty("label") && typeof column.label === "string" + ? column.label + : DEFAULT_COLUMN_NAME, + alias: column.alias, + accessor: (row: any) => row[column.alias], + width: columnWidthMap[column.id] || DEFAULT_COLUMN_WIDTH, + minWidth: COLUMN_MIN_WIDTH, + draggable: true, + isHidden: false, + isAscOrder: column.isAscOrder, + isDerived: column.isDerived, + sticky: fetchSticky(column.id, primaryColumns, renderMode, widgetId), + metaProperties: { + isHidden: isHidden, + type: column.columnType, + format: column.outputFormat || "", + inputFormat: column.inputFormat || "", + }, + columnProperties: column, + Cell: renderCell, + }; + + const isAllCellVisible: boolean | boolean[] = column.isCellVisible; + + /* + * If all cells are not visible or column itself is not visible, + * set isHidden and push it to hiddenColumns array else columns array + */ + if ( + (isBoolean(isAllCellVisible) && !isAllCellVisible) || + (isArray(isAllCellVisible) && + isAllCellVisible.every((visibility) => visibility === false)) || + isHidden + ) { + columnData.isHidden = true; + hiddenColumns.push(columnData); + } else { + totalColumnWidth += columnData.width; + columns.push(columnData); + } + }); + } + + const lastColumnIndex = columns.length - 1; + if (totalColumnWidth < componentWidth) { + /* + This "if" block is responsible for upsizing the last column width + if there is space left in the table container towards the right + */ + if (columns[lastColumnIndex]) { + const lastColumnWidth = + columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH; + const remainingWidth = componentWidth - totalColumnWidth; + // Adding the remaining width i.e. space left towards the right, to the last column width + columns[lastColumnIndex].width = lastColumnWidth + remainingWidth; + } + } else if (totalColumnWidth > componentWidth) { + /* + This "else-if" block is responsible for downsizing the last column width + if the last column spills over resulting in horizontal scroll + */ + const extraWidth = totalColumnWidth - componentWidth; + const lastColWidth = columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH; + /* + Below if condition explanation: + Condition 1: (lastColWidth > COLUMN_MIN_WIDTH) + We will downsize the last column only if its greater than COLUMN_MIN_WIDTH + Condition 2: (extraWidth < lastColWidth) + This condition checks whether the last column is the only column that is spilling over. + If more than one columns are spilling over we won't downsize the last column + */ + if (lastColWidth > COLUMN_MIN_WIDTH && extraWidth < lastColWidth) { + const availableWidthForLastColumn = lastColWidth - extraWidth; + /* + Below we are making sure last column width doesn't go lower than COLUMN_MIN_WIDTH again + as availableWidthForLastColumn might go lower than COLUMN_MIN_WIDTH in some cases + */ + columns[lastColumnIndex].width = + availableWidthForLastColumn < COLUMN_MIN_WIDTH + ? COLUMN_MIN_WIDTH + : availableWidthForLastColumn; + } + } + + if (hiddenColumns.length && renderMode === RenderModes.CANVAS) { + // Get the index of the first column that is frozen to right + const rightFrozenColumnIdx = findIndex( + columns, + (col) => col.sticky === StickyType.RIGHT, + ); + if (rightFrozenColumnIdx !== -1) { + columns.splice(rightFrozenColumnIdx, 0, ...hiddenColumns); + } else { + columns = columns.concat(hiddenColumns); + } + } + + return columns.filter((column: ReactTableColumnProps) => !!column.id); +}; + +// the result of this cache function is a prop for the useTable hook, this prop needs to memoised as per their docs +// we have noticed expensive computation from the useTable if columns isnt memoised +export const getMemoiseGetColumnsWithLocalStorageFn = () => { + const memoisedGetColumns = memoizeOne(getColumnsPureFn); + + return memoizeOne( + //we are not using this parameter it is used by the memoisation comparator + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (widgetLocalStorageState) => { + memoisedGetColumns.clear(); + return memoisedGetColumns as getColumns; + }, + isEqual, + ); +}; diff --git a/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/transformDataPureFn.tsx b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/transformDataPureFn.tsx new file mode 100644 index 0000000000..fcce4f43dd --- /dev/null +++ b/app/client/src/widgets/TableWidgetV2/widget/reactTableUtils/transformDataPureFn.tsx @@ -0,0 +1,162 @@ +import log from "loglevel"; +import type { MomentInput } from "moment"; +import moment from "moment"; +import _, { isNumber, isNil, isArray } from "lodash"; +import type { EditableCell } from "../../constants"; +import { ColumnTypes, DateInputFormat } from "../../constants"; +import type { ReactTableColumnProps } from "../../component/Constants"; +import memoizeOne from "memoize-one"; +import shallowEqual from "shallowequal"; + +export type tableData = Array>; + +//TODO: (Vamsi) need to unit test this function +export const transformDataPureFn = ( + tableData: Array>, + columns: ReactTableColumnProps[], +): tableData => { + if (isArray(tableData)) { + return tableData.map((row, rowIndex) => { + const newRow: { [key: string]: any } = {}; + + columns.forEach((column) => { + const { alias } = column; + let value = row[alias]; + + if (column.metaProperties) { + switch (column.metaProperties.type) { + case ColumnTypes.DATE: + let isValidDate = true; + const outputFormat = _.isArray(column.metaProperties.format) + ? column.metaProperties.format[rowIndex] + : column.metaProperties.format; + let inputFormat; + + try { + const type = _.isArray(column.metaProperties.inputFormat) + ? column.metaProperties.inputFormat[rowIndex] + : column.metaProperties.inputFormat; + + if ( + type !== DateInputFormat.EPOCH && + type !== DateInputFormat.MILLISECONDS + ) { + inputFormat = type; + moment(value as MomentInput, inputFormat); + } else if (!isNumber(value)) { + isValidDate = false; + } + } catch (e) { + isValidDate = false; + } + + if (isValidDate && value) { + try { + if ( + column.metaProperties.inputFormat === + DateInputFormat.MILLISECONDS + ) { + value = Number(value); + } else if ( + column.metaProperties.inputFormat === DateInputFormat.EPOCH + ) { + value = 1000 * Number(value); + } + + newRow[alias] = moment( + value as MomentInput, + inputFormat, + ).format(outputFormat); + } catch (e) { + log.debug("Unable to parse Date:", { e }); + newRow[alias] = ""; + } + } else if (value) { + newRow[alias] = "Invalid Value"; + } else { + newRow[alias] = ""; + } + break; + default: + let data; + + if ( + _.isString(value) || + _.isNumber(value) || + _.isBoolean(value) + ) { + data = value; + } else if (isNil(value)) { + data = ""; + } else { + data = JSON.stringify(value); + } + + newRow[alias] = data; + break; + } + } + }); + + return newRow; + }); + } else { + return []; + } +}; + +// lazily generate the cache so that we can create several memoised instances +const getMemoizedTransformData = () => memoizeOne(transformDataPureFn); + +export const injectEditableCellToTableData = ( + tableData: tableData, + editableCell: EditableCell | undefined, +): tableData => { + /* + * Inject the edited cell value from the editableCell object + */ + if (!editableCell || !tableData.length) return tableData; + const { column, index: updatedRowIndex, inputValue } = editableCell; + + const inRangeForUpdate = + updatedRowIndex >= 0 && updatedRowIndex < tableData.length; + if (!inRangeForUpdate) return tableData; + //if same value ignore update + if (tableData[updatedRowIndex][column] === inputValue) return tableData; + //create copies of data + const copy = [...tableData]; + copy[updatedRowIndex] = { ...copy[updatedRowIndex], [column]: inputValue }; + return copy; +}; + +const getMemoiseInjectEditableCellToTableData = () => + memoizeOne(injectEditableCellToTableData, (prev, next) => { + const [prevTableData, prevCellEditable] = prev; + const [nextTableData, nextCellEditable] = next; + //shallow compare the cellEditable properties + if (!shallowEqual(prevCellEditable, nextCellEditable)) return false; + + return shallowEqual(prevTableData, nextTableData); + }); + +export type transformDataWithEditableCell = ( + editableCell: EditableCell | undefined, + tableData: Array>, + columns: ReactTableColumnProps[], +) => tableData; + +// the result of this cache function is a prop for the useTable hook, this prop needs to memoised as per their docs +// we have noticed expensive computation from the useTable if tableData isnt memoised +export const getMemoiseTransformDataWithEditableCell = + (): transformDataWithEditableCell => { + const memoizedTransformData = getMemoizedTransformData(); + const memoiseInjectEditableCellToTableData = + getMemoiseInjectEditableCellToTableData(); + return memoizeOne((editableCell, tableData, columns) => { + const transformedData = memoizedTransformData(tableData, columns); + return memoiseInjectEditableCellToTableData( + transformedData, + editableCell, + ); + }); + }; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index f4c1974f7d..d1ed370257 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -14943,9 +14943,10 @@ memfs@^3.2.2: version "5.1.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz" -memoize-one@^5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== memoizerific@^1.11.3: version "1.11.3"