import React, { lazy, Suspense } from "react"; import log from "loglevel"; import memoizeOne from "memoize-one"; import _, { filter, isArray, isEmpty, isNil, isNumber, isObject, isString, merge, orderBy, pickBy, union, without, xor, xorWith, } from "lodash"; import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; import { RenderModes, WIDGET_PADDING, WIDGET_TAGS, } from "constants/WidgetConstants"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import Skeleton from "components/utils/Skeleton"; import { noop, retryPromise } from "utils/AppsmithUtils"; import type { ColumnProperties, ReactTableColumnProps, ReactTableFilter, } from "../component/Constants"; import { AddNewRowActions, CompactModeTypes, DEFAULT_FILTER, SORT_ORDER, SortOrderTypes, StickyType, } from "../component/Constants"; import type { EditableCell, OnColumnEventArgs, TableWidgetProps, TransientDataPayload, } from "../constants"; import { ActionColumnTypes, ColumnTypes, DEFAULT_BUTTON_LABEL, DEFAULT_COLUMN_WIDTH, DEFAULT_MENU_BUTTON_LABEL, DEFAULT_MENU_VARIANT, defaultEditableCell, EditableCellActions, InlineEditingSaveOptions, ORIGINAL_INDEX_KEY, PaginationDirection, TABLE_COLUMN_ORDER_KEY, } from "../constants"; import derivedProperties from "./parseDerivedProperties"; import { createEditActionColumn, deleteLocalTableColumnOrderByWidgetId, generateLocalNewColumnOrderFromStickyValue, generateNewColumnOrderFromStickyValue, getAllStickyColumnsCount, getAllTableColumnKeys, getBooleanPropertyValue, getCellProperties, getColumnOrderByWidgetIdFromLS, getColumnType, getDefaultColumnProperties, getDerivedColumns, getSelectRowIndex, getSelectRowIndices, getTableStyles, isColumnTypeEditable, updateAndSyncTableLocalColumnOrders, } from "./utilities"; import contentConfig from "./propertyConfig/contentConfig"; import styleConfig from "./propertyConfig/styleConfig"; import type { BatchPropertyUpdatePayload } from "actions/controlActions"; import type { IconName } from "@blueprintjs/icons"; import { IconNames } from "@blueprintjs/icons"; import { Colors } from "constants/Colors"; import equal from "fast-deep-equal/es6"; import { DefaultAutocompleteDefinitions, sanitizeKey, } from "widgets/WidgetUtils"; import PlainTextCell from "../component/cellComponents/PlainTextCell"; import { ButtonCell } from "../component/cellComponents/ButtonCell"; import { MenuButtonCell } from "../component/cellComponents/MenuButtonCell"; import { ImageCell } from "../component/cellComponents/ImageCell"; import { VideoCell } from "../component/cellComponents/VideoCell"; import { IconButtonCell } from "../component/cellComponents/IconButtonCell"; import { EditActionCell } from "../component/cellComponents/EditActionsCell"; import { klona as clone } from "klona"; import { CheckboxCell } from "../component/cellComponents/CheckboxCell"; import { SwitchCell } from "../component/cellComponents/SwitchCell"; import { SelectCell } from "../component/cellComponents/SelectCell"; import { CellWrapper } from "../component/TableStyledWrappers"; import localStorage from "utils/localStorage"; import type { SetterConfig, Stylesheet } from "entities/AppTheming"; 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"; import type { ExtraDef } from "utils/autocomplete/dataTreeTypeDefCreator"; import { generateTypeDef } from "utils/autocomplete/dataTreeTypeDefCreator"; import type { AutocompletionDefinitions, PropertyUpdates, SnipingModeProperty, } from "WidgetProvider/constants"; import type { WidgetQueryConfig, WidgetQueryGenerationFormConfig, } from "WidgetQueryGenerators/types"; import type { DynamicPath } from "utils/DynamicBindingUtils"; import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants"; import { FlexVerticalAlignment, ResponsiveBehavior, } from "layoutSystems/common/utils/constants"; import IconSVG from "../icon.svg"; const ReactTableComponent = lazy(async () => retryPromise(async () => 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 type = "TABLE_WIDGET_V2"; static preloadConfig = true; static getConfig() { return { name: "Table", iconSVG: IconSVG, tags: [WIDGET_TAGS.SUGGESTED_WIDGETS, WIDGET_TAGS.DISPLAY], needsMeta: true, needsHeightForContent: true, }; } static getDefaults() { return { flexVerticalAlignment: FlexVerticalAlignment.Top, responsiveBehavior: ResponsiveBehavior.Fill, minWidth: FILL_WIDGET_MIN_WIDTH, rows: 28, canFreezeColumn: true, columnUpdatedAt: Date.now(), columns: 34, animateLoading: true, defaultSelectedRowIndex: 0, defaultSelectedRowIndices: [0], label: "Data", widgetName: "Table", searchKey: "", textSize: "0.875rem", horizontalAlignment: "LEFT", verticalAlignment: "CENTER", totalRecordsCount: 0, defaultPageSize: 0, dynamicPropertyPathList: [], borderColor: Colors.GREY_5, borderWidth: "1", dynamicBindingPathList: [], primaryColumns: {}, tableData: "", columnWidthMap: {}, columnOrder: [], enableClientSideSearch: true, isVisibleSearch: true, isVisibleFilters: true, isVisibleDownload: true, isVisiblePagination: true, isSortable: true, delimiter: ",", version: 2, inlineEditingSaveOption: InlineEditingSaveOptions.ROW_LEVEL, enableServerSideFiltering: false, }; } static getMethods() { return { getQueryGenerationConfig: (widget: WidgetProps) => { return { select: { limit: `${widget.widgetName}.pageSize`, where: `${widget.widgetName}.searchText`, offset: `${widget.widgetName}.pageOffset`, orderBy: `${widget.widgetName}.sortOrder.column`, sortOrder: `${widget.widgetName}.sortOrder.order !== "desc"`, }, create: { value: `(${widget.widgetName}.newRow || {})`, }, update: { value: `${widget.widgetName}.updatedRow`, where: `${widget.widgetName}.updatedRow`, }, totalRecord: true, }; }, getPropertyUpdatesForQueryBinding: ( queryConfig: WidgetQueryConfig, _widget: WidgetProps, formConfig: WidgetQueryGenerationFormConfig, ) => { const widget = _widget as TableWidgetProps; let modify = {}; const dynamicPropertyPathList: DynamicPath[] = []; if (queryConfig.select) { modify = merge(modify, { tableData: queryConfig.select.data, onPageChange: queryConfig.select.run, serverSidePaginationEnabled: true, onSearchTextChanged: formConfig.searchableColumn ? queryConfig.select.run : undefined, onSort: queryConfig.select.run, enableClientSideSearch: !formConfig.searchableColumn, primaryColumnId: formConfig.primaryColumn, isVisibleDownload: false, }); } if (queryConfig.create) { modify = merge(modify, { onAddNewRowSave: queryConfig.create.run, allowAddNewRow: true, ...Object.keys(widget.primaryColumns).reduce( (prev: Record, curr) => { if (formConfig.primaryColumn !== curr) { prev[`primaryColumns.${curr}.isEditable`] = true; prev[`primaryColumns.${curr}.isCellEditable`] = true; } prev[`showInlineEditingOptionDropdown`] = true; return prev; }, {}, ), }); } if (queryConfig.update) { let editAction = {}; if ( !Object.values(widget.primaryColumns).some( (column) => column.columnType === ColumnTypes.EDIT_ACTIONS, ) ) { editAction = Object.values(createEditActionColumn(widget)).reduce( ( prev: Record, curr: { propertyPath: string; propertyValue: unknown; isDynamicPropertyPath?: boolean; }, ) => { prev[curr.propertyPath] = curr.propertyValue; if (curr.isDynamicPropertyPath) { dynamicPropertyPathList.push({ key: curr.propertyPath }); } return prev; }, {}, ); } modify = merge(modify, { ...editAction, [`primaryColumns.EditActions1.onSave`]: queryConfig.update.run, }); } if (queryConfig.total_record) { modify = merge(modify, { totalRecordsCount: queryConfig.total_record.data, }); } return { modify, dynamicUpdates: { dynamicPropertyPathList, }, }; }, getSnipingModeUpdates: ( propValueMap: SnipingModeProperty, ): PropertyUpdates[] => { return [ { propertyPath: "tableData", propertyValue: propValueMap.data, isDynamicPropertyPath: !!propValueMap.isDynamicPropertyPath, }, ]; }, getOneClickBindingConnectableWidgetConfig: (widget: WidgetProps) => { return { widgetBindPath: `${widget.widgetName}.selectedRow`, message: `Make sure ${widget.widgetName} is bound to the same data source`, }; }, }; } static getAutoLayoutConfig() { return { widgetSize: [ { viewportMinWidth: 0, configuration: () => { return { minWidth: "280px", minHeight: "300px", }; }, }, ], }; } static getPropertyPaneContentConfig() { return contentConfig; } 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 { pageNo: 1, selectedRowIndex: undefined, selectedRowIndices: undefined, searchText: undefined, triggeredRowIndex: undefined, filters: [], sortOrder: { column: "", order: null, }, transientTableData: {}, updatedRowIndex: -1, editableCell: defaultEditableCell, columnEditableCellValue: {}, selectColumnFilterText: {}, isAddRowInProgress: false, newRowContent: undefined, newRow: undefined, previousPageVisited: false, nextPageVisited: false, }; } static getAutocompleteDefinitions(): AutocompletionDefinitions { return (widget: TableWidgetProps, extraDefsToDefine?: ExtraDef) => { const config: AutocompletionDefinitions = { "!doc": "The Table is the hero widget of Appsmith. You can display data from an API in a table, trigger an action when a user selects a row and even work with large paginated data sets", "!url": "https://docs.appsmith.com/widget-reference/table", selectedRow: generateTypeDef(widget.selectedRow, extraDefsToDefine), selectedRows: generateTypeDef(widget.selectedRows, extraDefsToDefine), selectedRowIndices: generateTypeDef(widget.selectedRowIndices), triggeredRow: generateTypeDef(widget.triggeredRow), updatedRow: generateTypeDef(widget.updatedRow), selectedRowIndex: "number", tableData: generateTypeDef(widget.tableData, extraDefsToDefine), pageNo: "number", pageSize: "number", isVisible: DefaultAutocompleteDefinitions.isVisible, searchText: "string", totalRecordsCount: "number", sortOrder: { column: "string", order: ["asc", "desc"], }, updatedRows: generateTypeDef(widget.updatedRows, extraDefsToDefine), updatedRowIndices: generateTypeDef(widget.updatedRowIndices), triggeredRowIndex: generateTypeDef(widget.triggeredRowIndex), pageOffset: generateTypeDef(widget.pageOffset), tableHeaders: generateTypeDef(widget.tableHeaders), newRow: generateTypeDef(widget.newRow), isAddRowInProgress: "bool", previousPageVisited: generateTypeDef(widget.previousPageVisited), nextPageVisited: generateTypeDef(widget.nextPageButtonClicked), filters: generateTypeDef(widget.filters), }; return config; }; } static getDerivedPropertiesMap() { return { selectedRow: `{{(()=>{${derivedProperties.getSelectedRow}})()}}`, triggeredRow: `{{(()=>{${derivedProperties.getTriggeredRow}})()}}`, selectedRows: `{{(()=>{${derivedProperties.getSelectedRows}})()}}`, pageSize: `{{(()=>{${derivedProperties.getPageSize}})()}}`, triggerRowSelection: "{{!!this.onRowSelected}}", processedTableData: `{{(()=>{${derivedProperties.getProcessedTableData}})()}}`, orderedTableColumns: `{{(()=>{${derivedProperties.getOrderedTableColumns}})()}}`, filteredTableData: `{{(()=>{ ${derivedProperties.getFilteredTableData}})()}}`, updatedRows: `{{(()=>{ ${derivedProperties.getUpdatedRows}})()}}`, updatedRowIndices: `{{(()=>{ ${derivedProperties.getUpdatedRowIndices}})()}}`, updatedRow: `{{(()=>{ ${derivedProperties.getUpdatedRow}})()}}`, pageOffset: `{{(()=>{${derivedProperties.getPageOffset}})()}}`, isEditableCellsValid: `{{(()=>{ ${derivedProperties.getEditableCellValidity}})()}}`, tableHeaders: `{{(()=>{${derivedProperties.getTableHeaders}})()}}`, }; } static getDefaultPropertiesMap(): Record { return { searchText: "defaultSearchText", selectedRowIndex: "defaultSelectedRowIndex", selectedRowIndices: "defaultSelectedRowIndices", }; } static getLoadingProperties(): Array | undefined { return [/\.tableData$/]; } static getStylesheetConfig(): Stylesheet { return { accentColor: "{{appsmith.theme.colors.primaryColor}}", borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", boxShadow: "{{appsmith.theme.boxShadow.appBoxShadow}}", childStylesheet: { button: { buttonColor: "{{appsmith.theme.colors.primaryColor}}", borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", boxShadow: "none", }, menuButton: { menuColor: "{{appsmith.theme.colors.primaryColor}}", borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", boxShadow: "none", }, iconButton: { buttonColor: "{{appsmith.theme.colors.primaryColor}}", borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", boxShadow: "none", }, editActions: { saveButtonColor: "{{appsmith.theme.colors.primaryColor}}", saveBorderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", discardButtonColor: "{{appsmith.theme.colors.primaryColor}}", discardBorderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", }, }, }; } static getSetterConfig(): SetterConfig { return { __setters: { setVisibility: { path: "isVisible", type: "string", }, setSelectedRowIndex: { path: "defaultSelectedRowIndex", type: "number", disabled: "return options.entity.multiRowSelection", }, setSelectedRowIndices: { path: "defaultSelectedRowIndices", type: "array", disabled: "return !options.entity.multiRowSelection", }, setData: { path: "tableData", type: "object", }, }, }; } /* * Function to get the table columns with appropriate render functions * based on columnType */ getTableColumns = () => { const { columnWidthMap, orderedTableColumns, renderMode, widgetId } = this.props; const { componentWidth } = this.getPaddingAdjustedDimensions(); const widgetLocalStorageState = getColumnOrderByWidgetIdFromLS(widgetId); const memoisdGetColumnsWithLocalStorage = this.memoiseGetColumnsWithLocalStorage(widgetLocalStorageState); return memoisdGetColumnsWithLocalStorage( this.renderCell, columnWidthMap, orderedTableColumns, componentWidth, renderMode, ); }; transformData = ( tableData: Array>, columns: ReactTableColumnProps[], ) => { return this.memoiseTransformDataWithEditableCell( this.props.editableCell, tableData, columns, ); }; updateDerivedColumnsIndex = ( derivedColumns: Record, tableColumnCount: number, ) => { if (!derivedColumns) { return []; } //update index property of all columns in new derived columns return Object.values(derivedColumns).map( (column: ColumnProperties, index: number) => { return { ...column, index: index + tableColumnCount, }; }, ); }; /* * Function to create new primary Columns from the tableData * gets called on component mount and on component update */ createTablePrimaryColumns = (): | Record | undefined => { const { primaryColumns = {}, tableData = [] } = this.props; if (!_.isArray(tableData) || tableData.length === 0) { return; } const existingColumnIds = Object.keys(primaryColumns); const newTableColumns: Record = {}; const tableStyles = getTableStyles(this.props); const columnKeys: string[] = getAllTableColumnKeys(tableData); /* * Generate default column properties for all columns * But do not replace existing columns with the same id */ columnKeys.forEach((columnKey, index) => { const existingColumn = this.getColumnByOriginalId(columnKey); if (!!existingColumn) { // Use the existing column properties newTableColumns[existingColumn.id] = existingColumn; } else { const hashedColumnKey = sanitizeKey(columnKey, { existingKeys: union(existingColumnIds, Object.keys(newTableColumns)), }); // Create column properties for the new column const columnType = getColumnType(tableData, columnKey); const columnProperties = getDefaultColumnProperties( columnKey, hashedColumnKey, index, this.props.widgetName, false, columnType, ); newTableColumns[columnProperties.id] = { ...columnProperties, ...tableStyles, }; } }); const derivedColumns: Record = getDerivedColumns(primaryColumns); const updatedDerivedColumns = this.updateDerivedColumnsIndex( derivedColumns, Object.keys(newTableColumns).length, ); //add derived columns to new Table columns updatedDerivedColumns.forEach((derivedColumn: ColumnProperties) => { newTableColumns[derivedColumn.id] = derivedColumn; }); const newColumnIds = Object.keys(newTableColumns); // check if the columns ids differ if (_.xor(existingColumnIds, newColumnIds).length > 0) { return newTableColumns; } else { return; } }; /* * Function to update primaryColumns when the tablData schema changes */ updateColumnProperties = ( tableColumns?: Record, shouldPersistLocalOrderWhenTableDataChanges = false, ) => { const { columnOrder = [], primaryColumns = {} } = this.props; const derivedColumns = getDerivedColumns(primaryColumns); if (tableColumns) { const existingColumnIds = Object.keys(primaryColumns); const existingDerivedColumnIds = Object.keys(derivedColumns); const newColumnIds = Object.keys(tableColumns); //Check if there is any difference in the existing and new columns ids if (_.xor(existingColumnIds, newColumnIds).length > 0) { const newColumnIdsToAdd = _.without(newColumnIds, ...existingColumnIds); const propertiesToAdd: Record = {}; newColumnIdsToAdd.forEach((columnId: string) => { // id could be an empty string if (!!columnId) { Object.entries(tableColumns[columnId]).forEach(([key, value]) => { propertiesToAdd[`primaryColumns.${columnId}.${key}`] = value; }); } }); /* * If new columnOrders have different values from the original columnOrders * Only update when there are new Columns(Derived or Primary) */ if ( !!newColumnIds.length && !!_.xor(newColumnIds, columnOrder).length && !equal(_.sortBy(newColumnIds), _.sortBy(existingDerivedColumnIds)) ) { // Maintain original columnOrder and keep new columns at the end let newColumnOrder = _.intersection(columnOrder, newColumnIds); newColumnOrder = _.union(newColumnOrder, newColumnIds); const compareColumns = (a: string, b: string) => { const aSticky = tableColumns[a].sticky || "none"; const bSticky = tableColumns[b].sticky || "none"; if (aSticky === bSticky) { return 0; } return SORT_ORDER[aSticky] - SORT_ORDER[bSticky]; }; // Sort the column order to retain the position of frozen columns 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 = { modify: propertiesToAdd, }; const pathsToDelete: string[] = []; const columnsIdsToDelete = without(existingColumnIds, ...newColumnIds); if (!!columnsIdsToDelete.length) { columnsIdsToDelete.forEach((id: string) => { if (!primaryColumns[id].isDerived) { pathsToDelete.push(`primaryColumns.${id}`); } }); propertiesToUpdate.remove = pathsToDelete; } super.batchUpdateWidgetProperty(propertiesToUpdate, false); } } }; //no need to batch meta updates hydrateStickyColumns = () => { const localTableColumnOrder = getColumnOrderByWidgetIdFromLS( this.props.widgetId, ); const leftLen: number = Object.keys( pickBy(this.props.primaryColumns, (col) => col.sticky === "left"), ).length; const leftOrder = [...(this.props.columnOrder || [])].slice(0, leftLen); const rightLen: number = Object.keys( pickBy(this.props.primaryColumns, (col) => col.sticky !== "right"), ).length; const rightOrder: string[] = [...(this.props.columnOrder || [])].slice( rightLen, ); if (localTableColumnOrder) { const { columnOrder, columnUpdatedAt, leftOrder: localLeftOrder, rightOrder: localRightOrder, } = localTableColumnOrder; if (this.props.columnUpdatedAt !== columnUpdatedAt) { // Delete and set the column orders defined by the developer deleteLocalTableColumnOrderByWidgetId(this.props.widgetId); this.persistColumnOrder( this.props.columnOrder ?? [], leftOrder, rightOrder, ); } else { 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. this.persistColumnOrder( this.props.columnOrder ?? [], leftOrder, rightOrder, ); } }; componentDidMount() { const { canFreezeColumn, renderMode, tableData } = this.props; if (_.isArray(tableData) && !!tableData.length) { const newPrimaryColumns = this.createTablePrimaryColumns(); // When the Table data schema changes if (newPrimaryColumns && !!Object.keys(newPrimaryColumns).length) { this.updateColumnProperties(newPrimaryColumns); } } if (canFreezeColumn && renderMode === RenderModes.PAGE) { //dont neet to batch this since single action this.hydrateStickyColumns(); } } componentDidUpdate(prevProps: TableWidgetProps) { const { defaultSelectedRowIndex, defaultSelectedRowIndices, pageNo, pageSize, primaryColumns = {}, serverSidePaginationEnabled, totalRecordsCount, } = this.props; // Bail out if tableData is a string. This signifies an error in evaluations if (isString(this.props.tableData)) { return; } if ( this.props.primaryColumns && (!equal(prevProps.columnOrder, this.props.columnOrder) || filter(prevProps.orderedTableColumns, { isVisible: false }).length !== filter(this.props.orderedTableColumns, { isVisible: false }).length || getAllStickyColumnsCount(prevProps.orderedTableColumns) !== getAllStickyColumnsCount(this.props.orderedTableColumns)) ) { if (this.props.renderMode === RenderModes.CANVAS) { super.batchUpdateWidgetProperty( { modify: { columnUpdatedAt: Date.now(), }, }, false, ); } } //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.pushMetaRowDataUpdates( prevProps.filteredTableData, this.props.filteredTableData, ); pushBatchMetaUpdates("triggeredRowIndex", -1); const newColumnIds: string[] = getAllTableColumnKeys( this.props.tableData, ); const primaryColumnIds = Object.keys(primaryColumns).filter( (id: string) => !primaryColumns[id].isDerived, ); if (xor(newColumnIds, primaryColumnIds).length > 0) { const newTableColumns = this.createTablePrimaryColumns(); if (newTableColumns) { this.updateColumnProperties(newTableColumns, isTableDataModified); } pushBatchMetaUpdates("filters", []); } } /* * Clear transient table data and editablecell when tableData changes */ if (isTableDataModified) { pushBatchMetaUpdates("transientTableData", {}); // reset updatedRowIndex whenever transientTableData is flushed. pushBatchMetaUpdates("updatedRowIndex", -1); this.pushClearEditableCellsUpdates(); pushBatchMetaUpdates("selectColumnFilterText", {}); } if (!pageNo) { pushBatchMetaUpdates("pageNo", 1); this.updatePaginationDirectionFlags(PaginationDirection.INITIAL); } //check if pageNo does not excede the max Page no, due to change of totalRecordsCount if (serverSidePaginationEnabled !== prevProps.serverSidePaginationEnabled) { //reset pageNo when serverSidePaginationEnabled is toggled pushBatchMetaUpdates("pageNo", 1); this.updatePaginationDirectionFlags(PaginationDirection.INITIAL); } else { //check if pageNo does not excede the max Page no, due to change of totalRecordsCount or change of pageSize if (serverSidePaginationEnabled && totalRecordsCount) { const maxAllowedPageNumber = Math.ceil(totalRecordsCount / pageSize); if (pageNo > maxAllowedPageNumber) { pushBatchMetaUpdates("pageNo", maxAllowedPageNumber); this.updatePaginationDirectionFlags(PaginationDirection.NEXT_PAGE); } } } /* * When defaultSelectedRowIndex or defaultSelectedRowIndices * is changed from property pane */ if ( !equal(defaultSelectedRowIndex, prevProps.defaultSelectedRowIndex) || !equal(defaultSelectedRowIndices, prevProps.defaultSelectedRowIndices) ) { this.pushUpdateSelectedRowIndexUpdates(); } this.pushResetPageNoUpdates(prevProps); this.pushResetRowSelectionPropertiesUpdates(prevProps); commitBatchMetaUpdates(); } pushResetPageNoUpdates = (prevProps: TableWidgetProps) => { const { onPageSizeChange, pageSize, pushBatchMetaUpdates } = this.props; if (pageSize !== prevProps.pageSize) { if (onPageSizeChange) { this.updatePaginationDirectionFlags(PaginationDirection.INITIAL); pushBatchMetaUpdates("pageNo", 1, { triggerPropertyName: "onPageSizeChange", dynamicString: onPageSizeChange, event: { type: EventType.ON_PAGE_SIZE_CHANGE, }, }); } else { pushBatchMetaUpdates("pageNo", 1); this.updatePaginationDirectionFlags(PaginationDirection.INITIAL); } } }; pushResetRowSelectionPropertiesUpdates = (prevProps: TableWidgetProps) => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, pushBatchMetaUpdates, } = this.props; // reset selectedRowIndices and selectedRowIndex to defaults if (multiRowSelection !== prevProps.multiRowSelection) { if (multiRowSelection) { if ( defaultSelectedRowIndices && _.isArray(defaultSelectedRowIndices) && defaultSelectedRowIndices.every((i) => _.isFinite(i)) ) { pushBatchMetaUpdates("selectedRowIndices", defaultSelectedRowIndices); } pushBatchMetaUpdates("selectedRowIndex", -1); } else { if ( !isNil(defaultSelectedRowIndex) && parseInt(defaultSelectedRowIndex?.toString(), 10) > -1 ) { pushBatchMetaUpdates("selectedRowIndex", defaultSelectedRowIndex); } pushBatchMetaUpdates("selectedRowIndices", []); } } }; /* * Function to update selectedRowIndices & selectedRowIndex from * defaultSelectedRowIndices & defaultSelectedRowIndex respectively */ pushUpdateSelectedRowIndexUpdates = () => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, pushBatchMetaUpdates, } = this.props; if (multiRowSelection) { pushBatchMetaUpdates("selectedRowIndices", defaultSelectedRowIndices); } else { pushBatchMetaUpdates("selectedRowIndex", defaultSelectedRowIndex); } }; /* * Function to update selectedRow details when order of tableData changes */ pushMetaRowDataUpdates = ( oldTableData: Array>, newTableData: Array>, ) => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, primaryColumnId, pushBatchMetaUpdates, selectedRowIndex, selectedRowIndices, } = this.props; if (multiRowSelection) { const indices = getSelectRowIndices( oldTableData, newTableData, defaultSelectedRowIndices, selectedRowIndices, primaryColumnId, ); pushBatchMetaUpdates("selectedRowIndices", indices); } else { const index = getSelectRowIndex( oldTableData, newTableData, defaultSelectedRowIndex, selectedRowIndex, primaryColumnId, ); pushBatchMetaUpdates("selectedRowIndex", index); } }; getSelectedRowIndices = () => { const { multiRowSelection, selectedRowIndices } = this.props; let indices: number[] | undefined; if (multiRowSelection) { if (_.isArray(selectedRowIndices)) { indices = selectedRowIndices; } else if (_.isNumber(selectedRowIndices)) { indices = [selectedRowIndices]; } else { indices = []; } } else { indices = undefined; } return indices; }; updateFilters = (filters: ReactTableFilter[]) => { const { commitBatchMetaUpdates, enableServerSideFiltering, onTableFilterUpdate, pushBatchMetaUpdates, } = this.props; this.pushResetSelectedRowIndexUpdates(); if (enableServerSideFiltering) { pushBatchMetaUpdates("filters", filters, { triggerPropertyName: "onTableFilterUpdate", dynamicString: onTableFilterUpdate, event: { type: EventType.ON_FILTER_UPDATE, }, }); } else { pushBatchMetaUpdates("filters", filters); } // Reset Page only when a filter is added if (!isEmpty(xorWith(filters, [DEFAULT_FILTER], equal))) { pushBatchMetaUpdates("pageNo", 1); this.updatePaginationDirectionFlags(PaginationDirection.INITIAL); } commitBatchMetaUpdates(); }; toggleDrag = (disable: boolean) => { this.disableDrag(disable); }; getPaddingAdjustedDimensions = () => { // eslint-disable-next-line prefer-const let { componentHeight, componentWidth } = this.props; // (2 * WIDGET_PADDING) gives the total horizontal padding (i.e. paddingLeft + paddingRight) componentWidth = componentWidth - 2 * WIDGET_PADDING; return { componentHeight, componentWidth }; }; getWidgetView() { const { delimiter, filteredTableData = [], isVisibleDownload, isVisibleFilters, isVisiblePagination, isVisibleSearch, pageSize, primaryColumns, totalRecordsCount, } = this.props; const tableColumns = this.getTableColumns() || emptyArr; const transformedData = this.transformData(filteredTableData, tableColumns); const isVisibleHeaderOptions = isVisibleDownload || isVisibleFilters || isVisiblePagination || isVisibleSearch; const { componentHeight, componentWidth } = this.getPaddingAdjustedDimensions(); const finalTableData = this.memoisedAddNewRow( transformedData, this.props.isAddRowInProgress, this.props.newRowContent, ); return ( }> ); } /** * Function to update or add the tableWidgetColumnOrder key in the local storage * tableWidgetColumnOrder = { * : { * columnOrder: [], * leftOrder: [], * rightOrder: [], * } * } */ persistColumnOrder = ( newColumnOrder: string[], leftOrder: string[], rightOrder: string[], ) => { const widgetId = this.props.widgetId; const localTableWidgetColumnOrder = localStorage.getItem( TABLE_COLUMN_ORDER_KEY, ); let newTableColumnOrder; if (localTableWidgetColumnOrder) { try { let parsedTableWidgetColumnOrder = JSON.parse( localTableWidgetColumnOrder, ); let columnOrder; if (newColumnOrder) { columnOrder = newColumnOrder; } else if (parsedTableWidgetColumnOrder[widgetId]) { columnOrder = parsedTableWidgetColumnOrder[widgetId]; } else { columnOrder = this.props.columnOrder; } parsedTableWidgetColumnOrder = { ...parsedTableWidgetColumnOrder, [widgetId]: { columnOrder, columnUpdatedAt: this.props.columnUpdatedAt, leftOrder, rightOrder, }, }; newTableColumnOrder = parsedTableWidgetColumnOrder; } catch (e) { log.debug("Unable to parse local column order:", { e }); } } else { const tableWidgetColumnOrder = { [widgetId]: { columnOrder: newColumnOrder, columnUpdatedAt: this.props.columnUpdatedAt, leftOrder, rightOrder, }, }; newTableColumnOrder = tableWidgetColumnOrder; } localStorage.setItem( TABLE_COLUMN_ORDER_KEY, JSON.stringify(newTableColumnOrder), ); }; handleColumnFreeze = (columnName: string, sticky?: StickyType) => { if (this.props.columnOrder) { let newColumnOrder; const localTableColumnOrder = getColumnOrderByWidgetIdFromLS( this.props.widgetId, ); if (this.props.renderMode === RenderModes.CANVAS) { newColumnOrder = generateNewColumnOrderFromStickyValue( this.props.primaryColumns, this.props.columnOrder, columnName, sticky, ); // Updating these properties in batch so that undo/redo gets executed in a combined way. super.batchUpdateWidgetProperty( { modify: { [`primaryColumns.${columnName}.sticky`]: sticky, columnOrder: newColumnOrder, }, }, true, ); } else if ( localTableColumnOrder && this.props.renderMode === RenderModes.PAGE ) { const { leftOrder, rightOrder } = localTableColumnOrder; newColumnOrder = generateLocalNewColumnOrderFromStickyValue( localTableColumnOrder.columnOrder, columnName, sticky, leftOrder, rightOrder, ); const updatedOrders = updateAndSyncTableLocalColumnOrders( columnName, leftOrder, rightOrder, sticky, ); this.persistColumnOrder( newColumnOrder, updatedOrders.leftOrder, updatedOrders.rightOrder, ); super.batchUpdateWidgetProperty( { modify: { [`primaryColumns.${columnName}.sticky`]: sticky, columnOrder: newColumnOrder, }, }, true, ); } } }; handleReorderColumn = (columnOrder: string[]) => { columnOrder = columnOrder.map((alias) => this.getColumnIdByAlias(alias)); 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, [], []); } } super.updateWidgetProperty("columnOrder", columnOrder); }; handleColumnSorting = (columnAccessor: string, isAsc: boolean) => { const columnId = this.getColumnIdByAlias(columnAccessor); const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; this.pushResetSelectedRowIndexUpdates(false); let sortOrderProps; if (columnId) { sortOrderProps = { column: columnId, order: isAsc ? SortOrderTypes.asc : SortOrderTypes.desc, }; } else { sortOrderProps = { column: "", order: null, }; } 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 { commitBatchMetaUpdates, multiRowSelection, onSearchTextChanged, pushBatchMetaUpdates, } = this.props; /* * Clear rowSelection to avoid selecting filtered rows * based on stale selection indices */ if (multiRowSelection) { pushBatchMetaUpdates("selectedRowIndices", []); } else { pushBatchMetaUpdates("selectedRowIndex", -1); } pushBatchMetaUpdates("pageNo", 1); this.updatePaginationDirectionFlags(PaginationDirection.INITIAL); pushBatchMetaUpdates("searchText", searchKey, { triggerPropertyName: "onSearchTextChanged", dynamicString: onSearchTextChanged, event: { type: EventType.ON_SEARCH, }, }); commitBatchMetaUpdates(); }; /** * This function just pushes the meta update */ pushOnColumnEvent = ({ action, additionalData = {}, eventType, onComplete = noop, row, rowIndex, triggerPropertyName, }: 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 */ onColumnEvent = ({ action, additionalData = {}, eventType, onComplete = noop, row, rowIndex, triggerPropertyName, }: OnColumnEventArgs) => { if (action) { const { commitBatchMetaUpdates } = this.props; this.pushOnColumnEvent({ rowIndex, action, onComplete, triggerPropertyName, eventType, row, additionalData, }); commitBatchMetaUpdates(); } else { onComplete(); } }; onDropdownOptionSelect = (action: string) => { super.executeAction({ dynamicString: action, event: { type: EventType.ON_OPTION_CHANGE, }, }); }; handleAllRowSelect = (pageData: Record[]) => { if (this.props.multiRowSelection) { const selectedRowIndices = pageData.map( (row: Record) => row.index, ); //single action no need to batch this.props.updateWidgetMetaProperty( "selectedRowIndices", selectedRowIndices, ); } }; 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; if (_.isArray(selectedRowIndices)) { indices = [...selectedRowIndices]; } else { indices = []; } /* * Deselect if the index is already present */ if (indices.includes(selectedIndex)) { indices.splice(indices.indexOf(selectedIndex), 1); this.props.updateWidgetMetaProperty("selectedRowIndices", indices); } else { /* * select if the index is not present already */ indices.push(selectedIndex); this.props.updateWidgetMetaProperty("selectedRowIndices", indices, { triggerPropertyName: "onRowSelected", dynamicString: this.props.onRowSelected, event: { type: EventType.ON_ROW_SELECTED, }, }); } } else { let index; if (isNumber(selectedRowIndex)) { index = selectedRowIndex; } else { index = -1; } if (index !== selectedIndex) { this.props.updateWidgetMetaProperty("selectedRowIndex", selectedIndex, { triggerPropertyName: "onRowSelected", dynamicString: this.props.onRowSelected, event: { type: EventType.ON_ROW_SELECTED, }, }); } else { this.props.updateWidgetMetaProperty("selectedRowIndex", -1); } } }; updatePageNumber = (pageNo: number, event?: EventType) => { const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; const paginationDirection = event == EventType.ON_NEXT_PAGE ? PaginationDirection.NEXT_PAGE : PaginationDirection.PREVIOUS_PAGE; this.updatePaginationDirectionFlags(paginationDirection); if (event) { pushBatchMetaUpdates("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { type: event, }, }); } else { pushBatchMetaUpdates("pageNo", pageNo); } if (this.props.onPageChange) { this.pushResetSelectedRowIndexUpdates(); } commitBatchMetaUpdates(); }; updatePaginationDirectionFlags = (direction?: PaginationDirection) => { const { pushBatchMetaUpdates } = this.props; let previousButtonFlag = false; let nextButtonFlag = false; if (direction) { switch (direction) { case PaginationDirection.INITIAL: { previousButtonFlag = false; nextButtonFlag = false; break; } case PaginationDirection.NEXT_PAGE: { nextButtonFlag = true; break; } case PaginationDirection.PREVIOUS_PAGE: { previousButtonFlag = true; break; } } } pushBatchMetaUpdates("previousPageVisited", previousButtonFlag); pushBatchMetaUpdates("nextPageVisited", nextButtonFlag); }; handleNextPageClick = () => { const pageNo = (this.props.pageNo || 1) + 1; const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; this.updatePaginationDirectionFlags(PaginationDirection.NEXT_PAGE); pushBatchMetaUpdates("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { type: EventType.ON_NEXT_PAGE, }, }); if (this.props.onPageChange) { this.pushResetSelectedRowIndexUpdates(); } commitBatchMetaUpdates(); }; pushResetSelectedRowIndexUpdates = (skipDefault?: boolean) => { const { pushBatchMetaUpdates } = this.props; const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, } = this.props; if (multiRowSelection) { pushBatchMetaUpdates( "selectedRowIndices", skipDefault ? [] : defaultSelectedRowIndices, ); } else { pushBatchMetaUpdates( "selectedRowIndex", skipDefault ? -1 : defaultSelectedRowIndex, ); } }; unSelectAllRow = () => { this.props.updateWidgetMetaProperty("selectedRowIndices", []); }; handlePrevPageClick = () => { const pageNo = (this.props.pageNo || 1) - 1; const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; if (pageNo >= 1) { this.updatePaginationDirectionFlags(PaginationDirection.PREVIOUS_PAGE); pushBatchMetaUpdates("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { type: EventType.ON_PREV_PAGE, }, }); if (this.props.onPageChange) { this.pushResetSelectedRowIndexUpdates(); } } commitBatchMetaUpdates(); }; getColumnIdByAlias(alias: string) { const { primaryColumns } = this.props; if (primaryColumns) { const column = Object.values(primaryColumns).find( (column) => column.alias === alias, ); if (column) { return column.id; } } return alias; } getColumnByOriginalId(originalId: string) { return Object.values(this.props.primaryColumns).find((column) => { return column.originalId === originalId; }); } pushTransientTableDataActionsUpdates = (data: TransientDataPayload) => { const { __originalIndex__, ...transientData } = data; const { pushBatchMetaUpdates } = this.props; pushBatchMetaUpdates("transientTableData", { ...this.props.transientTableData, [__originalIndex__]: { ...this.props.transientTableData[__originalIndex__], ...transientData, }, }); pushBatchMetaUpdates("updatedRowIndex", __originalIndex__); }; removeRowFromTransientTableData = (index: number) => { const newTransientTableData = clone(this.props.transientTableData); const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; if (newTransientTableData) { delete newTransientTableData[index]; pushBatchMetaUpdates("transientTableData", newTransientTableData); } pushBatchMetaUpdates("updatedRowIndex", -1); commitBatchMetaUpdates(); }; getRowOriginalIndex = (index: number) => { const { filteredTableData } = this.props; if (filteredTableData) { const row = filteredTableData[index]; if (row) { return row[ORIGINAL_INDEX_KEY]; } } return -1; }; onBulkEditSave = () => { this.props.updateWidgetMetaProperty( "transientTableData", this.props.transientTableData, { triggerPropertyName: "onBulkSave", dynamicString: this.props.onBulkSave, event: { type: EventType.ON_BULK_SAVE, }, }, ); }; onBulkEditDiscard = () => { this.props.updateWidgetMetaProperty( "transientTableData", {}, { triggerPropertyName: "onBulkDiscard", dynamicString: this.props.onBulkDiscard, event: { type: EventType.ON_BULK_DISCARD, }, }, ); }; renderCell = (props: any) => { const column = this.getColumnByOriginalId( props.cell.column.columnProperties.originalId, ) || props.cell.column.columnProperties; const rowIndex = props.cell.row.index; /* * We don't need to render cells that don't display data (button, iconButton, etc) */ if ( this.props.isAddRowInProgress && rowIndex === 0 && ActionColumnTypes.includes(column.columnType) ) { return ; } const isHidden = !column.isVisible; const { compactMode = CompactModeTypes.DEFAULT, filteredTableData = [], multiRowSelection, selectedRowIndex, selectedRowIndices, } = this.props; let row; let originalIndex: number; /* * In add new row flow, a temporary row is injected at the top of the tableData, which doesn't * have original row index value. so we are using -1 as the value */ if (this.props.isAddRowInProgress) { row = filteredTableData[rowIndex - 1]; originalIndex = rowIndex === 0 ? -1 : row[ORIGINAL_INDEX_KEY] ?? rowIndex; } else { row = filteredTableData[rowIndex]; originalIndex = row[ORIGINAL_INDEX_KEY] ?? rowIndex; } const isNewRow = this.props.isAddRowInProgress && rowIndex === 0; /* * cellProperties order or size does not change when filter/sorting/grouping is applied * on the data thus original index is needed to identify the column's cell property. */ const cellProperties = getCellProperties(column, originalIndex, isNewRow); let isSelected = false; if (this.props.transientTableData) { cellProperties.hasUnsavedChanges = this.props.transientTableData.hasOwnProperty(originalIndex) && this.props.transientTableData[originalIndex].hasOwnProperty( props.cell.column.columnProperties.alias, ); } if (multiRowSelection) { isSelected = _.isArray(selectedRowIndices) && selectedRowIndices.includes(rowIndex); } else { isSelected = selectedRowIndex === rowIndex; } const isColumnEditable = column.isEditable && isColumnTypeEditable(column.columnType); const alias = props.cell.column.columnProperties.alias; const isCellEditable = isColumnEditable && cellProperties.isCellEditable; const isCellEditMode = (props.cell.column.alias === this.props.editableCell?.column && rowIndex === this.props.editableCell?.index) || (isNewRow && isColumnEditable); const shouldDisableEdit = (this.props.inlineEditingSaveOption === InlineEditingSaveOptions.ROW_LEVEL && this.props.updatedRowIndices.length && this.props.updatedRowIndices.indexOf(originalIndex) === -1) || (this.hasInvalidColumnCell() && !isNewRow); const disabledEditMessage = `Save or discard the ${ this.props.isAddRowInProgress ? "newly added" : "unsaved" } row to start editing here`; if (this.props.isAddRowInProgress) { cellProperties.isCellDisabled = rowIndex !== 0; if (rowIndex === 0) { cellProperties.cellBackground = ""; } } switch (column.columnType) { case ColumnTypes.BUTTON: return ( void) => this.onColumnEvent({ rowIndex, action, onComplete, triggerPropertyName: "onClick", eventType: EventType.ON_CLICK, }) } textColor={cellProperties.textColor} textSize={cellProperties.textSize} verticalAlignment={cellProperties.verticalAlignment} /> ); case ColumnTypes.EDIT_ACTIONS: return ( void, eventType: EventType, ) => this.onColumnEvent({ rowIndex, action, onComplete, triggerPropertyName: "onClick", eventType: eventType, }) } onDiscard={() => this.removeRowFromTransientTableData(originalIndex) } textColor={cellProperties.textColor} textSize={cellProperties.textSize} verticalAlignment={cellProperties.verticalAlignment} /> ); case ColumnTypes.SELECT: return ( ); case ColumnTypes.IMAGE: const onClick = column.onClick ? () => this.onColumnEvent({ rowIndex, action: column.onClick, triggerPropertyName: "onClick", eventType: EventType.ON_CLICK, }) : noop; return ( ); case ColumnTypes.MENU_BUTTON: const getVisibleItems = (rowIndex: number) => { const { configureMenuItems, menuItems, menuItemsSource, sourceData } = cellProperties; if (menuItemsSource === MenuItemsSource.STATIC && menuItems) { const visibleItems = Object.values(menuItems)?.filter((item) => getBooleanPropertyValue(item.isVisible, rowIndex), ); return visibleItems?.length ? orderBy(visibleItems, ["index"], ["asc"]) : []; } else if ( menuItemsSource === MenuItemsSource.DYNAMIC && isArray(sourceData) && sourceData?.length && configureMenuItems?.config ) { const { config } = configureMenuItems; const getValue = ( propertyName: keyof MenuItem, index: number, rowIndex: number, ) => { const value = config[propertyName]; if (isArray(value) && isArray(value[rowIndex])) { return value[rowIndex][index]; } else if (isArray(value)) { return value[index]; } return value ?? null; }; const visibleItems = sourceData .map((item, index) => ({ ...item, id: index.toString(), isVisible: getValue("isVisible", index, rowIndex), isDisabled: getValue("isDisabled", index, rowIndex), index: index, widgetId: "", label: getValue("label", index, rowIndex), onClick: config?.onClick, textColor: getValue("textColor", index, rowIndex), backgroundColor: getValue("backgroundColor", index, rowIndex), iconAlign: getValue("iconAlign", index, rowIndex), iconColor: getValue("iconColor", index, rowIndex), iconName: getValue("iconName", index, rowIndex), })) .filter((item) => item.isVisible === true); return visibleItems; } return []; }; return ( void, ) => { const additionalData: Record< string, string | number | Record > = {}; if (cellProperties?.sourceData && _.isNumber(index)) { additionalData.currentItem = cellProperties.sourceData[index]; additionalData.currentIndex = index; } return this.onColumnEvent({ rowIndex, action, onComplete, triggerPropertyName: "onClick", eventType: EventType.ON_CLICK, additionalData, }); }} rowIndex={originalIndex} sourceData={cellProperties.sourceData} textColor={cellProperties.textColor} textSize={cellProperties.textSize} verticalAlignment={cellProperties.verticalAlignment} /> ); case ColumnTypes.ICON_BUTTON: return ( void) => this.onColumnEvent({ rowIndex, action, onComplete, triggerPropertyName: "onClick", eventType: EventType.ON_CLICK, }) } textColor={cellProperties.textColor} textSize={cellProperties.textSize} verticalAlignment={cellProperties.verticalAlignment} /> ); case ColumnTypes.VIDEO: return ( ); case ColumnTypes.CHECKBOX: return ( this.onCheckChange( column, props.cell.row.values, !props.cell.value, alias, originalIndex, rowIndex, ) } value={props.cell.value} verticalAlignment={cellProperties.verticalAlignment} /> ); case ColumnTypes.SWITCH: return ( this.onCheckChange( column, props.cell.row.values, !props.cell.value, alias, originalIndex, rowIndex, ) } value={props.cell.value} verticalAlignment={cellProperties.verticalAlignment} /> ); case ColumnTypes.DATE: return ( ); default: let validationErrorMessage; if (isCellEditMode) { validationErrorMessage = column.validation.isColumnEditableCellRequired && (isNil(props.cell.value) || props.cell.value === "") ? "This field is required" : column.validation?.errorMessage; } return ( ); } }; onCellTextChange = ( value: EditableCell["value"], inputValue: string, alias: string, ) => { const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; if (this.props.isAddRowInProgress) { this.updateNewRowValues(alias, inputValue, value); } else { pushBatchMetaUpdates("editableCell", { ...this.props.editableCell, value: value, inputValue, }); if (this.props.editableCell?.column) { pushBatchMetaUpdates("columnEditableCellValue", { ...this.props.columnEditableCellValue, [this.props.editableCell?.column]: value, }); } commitBatchMetaUpdates(); } }; toggleCellEditMode = ( enable: boolean, rowIndex: number, alias: string, value: string | number, onSubmit?: string, action?: EditableCellActions, ) => { if (this.props.isAddRowInProgress) { return; } if (enable) { if (this.inlineEditTimer) { clearTimeout(this.inlineEditTimer); } const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; pushBatchMetaUpdates("editableCell", { column: alias, index: rowIndex, value: value, // To revert back to previous on discard initialValue: value, inputValue: value, }); pushBatchMetaUpdates("columnEditableCellValue", { ...this.props.columnEditableCellValue, [alias]: value, }); /* * We need to clear the selectedRowIndex and selectedRowIndices * if the rows are sorted, to avoid selectedRow jumping to * different page. */ if (this.props.sortOrder.column) { if (this.props.multiRowSelection) { pushBatchMetaUpdates("selectedRowIndices", []); } else { pushBatchMetaUpdates("selectedRowIndex", -1); } } commitBatchMetaUpdates(); } else { if ( this.isColumnCellValid(alias) && action === EditableCellActions.SAVE && value !== this.props.editableCell?.initialValue ) { const { commitBatchMetaUpdates } = this.props; this.pushTransientTableDataActionsUpdates({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [alias]: this.props.editableCell?.value, }); if (onSubmit && this.props.editableCell?.column) { //since onSubmit is truthy that makes action truthy as well, so we can push this event this.pushOnColumnEvent({ rowIndex: rowIndex, action: onSubmit, triggerPropertyName: "onSubmit", eventType: EventType.ON_SUBMIT, row: { ...this.props.filteredTableData[rowIndex], [this.props.editableCell.column]: this.props.editableCell.value, }, }); } commitBatchMetaUpdates(); this.clearEditableCell(); } else if ( action === EditableCellActions.DISCARD || value === this.props.editableCell?.initialValue ) { this.clearEditableCell(); } } }; onDateSave = ( rowIndex: number, alias: string, value: string, onSubmit?: string, ) => { if (this.isColumnCellValid(alias)) { const { commitBatchMetaUpdates } = this.props; this.pushTransientTableDataActionsUpdates({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [alias]: value, }); if (onSubmit && this.props.editableCell?.column) { //since onSubmit is truthy this makes action truthy as well, so we can push this event this.pushOnColumnEvent({ rowIndex: rowIndex, action: onSubmit, triggerPropertyName: "onSubmit", eventType: EventType.ON_SUBMIT, row: { ...this.props.filteredTableData[rowIndex], [this.props.editableCell.column]: value, }, }); } commitBatchMetaUpdates(); this.clearEditableCell(); } }; pushClearEditableCellsUpdates = () => { const { pushBatchMetaUpdates } = this.props; pushBatchMetaUpdates("editableCell", defaultEditableCell); pushBatchMetaUpdates("columnEditableCellValue", {}); }; clearEditableCell = (skipTimeout?: boolean) => { const clear = () => { const { commitBatchMetaUpdates } = this.props; this.pushClearEditableCellsUpdates(); commitBatchMetaUpdates(); }; if (skipTimeout) { clear(); } else { /* * We need to let the evaulations compute derived property (filteredTableData) * before we clear the editableCell to avoid the text flickering */ this.inlineEditTimer = setTimeout(clear, 100); } }; isColumnCellEditable = (column: ColumnProperties, rowIndex: number) => { return ( column.alias === this.props.editableCell?.column && rowIndex === this.props.editableCell?.index ); }; onOptionSelect = ( value: string | number, rowIndex: number, column: string, action?: string, ) => { if (this.props.isAddRowInProgress) { this.updateNewRowValues(column, value, value); } else { const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; this.pushTransientTableDataActionsUpdates({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [column]: value, }); pushBatchMetaUpdates("editableCell", defaultEditableCell); if (action && this.props.editableCell?.column) { //since action is truthy we can push this event this.pushOnColumnEvent({ rowIndex, action, triggerPropertyName: "onOptionChange", eventType: EventType.ON_OPTION_CHANGE, row: { ...this.props.filteredTableData[rowIndex], [this.props.editableCell.column]: value, }, }); } commitBatchMetaUpdates(); } }; onSelectFilterChange = ( text: string, rowIndex: number, serverSideFiltering: boolean, alias: string, action?: string, ) => { const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; pushBatchMetaUpdates("selectColumnFilterText", { ...this.props.selectColumnFilterText, [alias]: text, }); if (action && serverSideFiltering) { //since action is truthy we can push this event this.pushOnColumnEvent({ rowIndex, action, triggerPropertyName: "onFilterUpdate", eventType: EventType.ON_FILTER_UPDATE, row: { ...this.props.filteredTableData[rowIndex], }, additionalData: { filterText: text, }, }); } commitBatchMetaUpdates(); }; onCheckChange = ( column: any, row: Record, value: boolean, alias: string, originalIndex: number, rowIndex: number, ) => { if (this.props.isAddRowInProgress) { this.updateNewRowValues(alias, value, value); } else { 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, triggerPropertyName: "onCheckChange", eventType: EventType.ON_CHECK_CHANGE, row: { ...row, [alias]: value, }, }); } }; handleAddNewRowClick = () => { const defaultNewRow = this.props.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.updatePaginationDirectionFlags(PaginationDirection.INITIAL); } //Since we're adding a newRowContent thats not part of tableData, the index changes // so we're resetting the row selection pushBatchMetaUpdates("selectedRowIndex", -1); pushBatchMetaUpdates("selectedRowIndices", []); commitBatchMetaUpdates(); }; handleAddNewRowAction = ( type: AddNewRowActions, onActionComplete: () => void, ) => { let triggerPropertyName, action, eventType; const onComplete = () => { const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; pushBatchMetaUpdates("isAddRowInProgress", false); pushBatchMetaUpdates("newRowContent", undefined); pushBatchMetaUpdates("newRow", undefined); commitBatchMetaUpdates(); onActionComplete(); }; if (type === AddNewRowActions.SAVE) { triggerPropertyName = "onAddNewRowSave"; action = this.props.onAddNewRowSave; eventType = EventType.ON_ADD_NEW_ROW_SAVE; } else { triggerPropertyName = "onAddNewRowDiscard"; action = this.props.onAddNewRowDiscard; eventType = EventType.ON_ADD_NEW_ROW_DISCARD; } if (action) { super.executeAction({ triggerPropertyName: triggerPropertyName, dynamicString: action, event: { type: eventType, callback: onComplete, }, }); } else { onComplete(); } }; isColumnCellValid = (columnsAlias: string) => { if (this.props.isEditableCellsValid?.hasOwnProperty(columnsAlias)) { return this.props.isEditableCellsValid[columnsAlias]; } return true; }; hasInvalidColumnCell = () => { if (isObject(this.props.isEditableCellsValid)) { return Object.values(this.props.isEditableCellsValid).some((d) => !d); } else { return false; } }; updateNewRowValues = ( alias: string, 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. */ pushBatchMetaUpdates("newRowContent", { ...this.props.newRowContent, [alias]: value, }); pushBatchMetaUpdates("newRow", { ...this.props.newRow, [alias]: parsedValue, }); commitBatchMetaUpdates(); }; onConnectData = () => { if (this.props.renderMode === RenderModes.CANVAS) { super.updateOneClickBindingOptionsVisibility(true); } }; } export default TableWidgetV2;