import log from "loglevel"; import memoizeOne from "memoize-one"; import React, { lazy, Suspense } from "react"; import _, { filter, isArray, isEmpty, isNil, isNumber, isObject, isString, merge, orderBy, pickBy, union, without, xor, xorWith, } from "lodash"; import type { IconName } from "@blueprintjs/icons"; import { IconNames } from "@blueprintjs/icons"; import type { BatchPropertyUpdatePayload } from "actions/controlActions"; import Skeleton from "components/utils/Skeleton"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import { Colors } from "constants/Colors"; import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants"; import { RenderModes, WIDGET_PADDING, WIDGET_TAGS, } from "constants/WidgetConstants"; import type { SetterConfig, Stylesheet } from "entities/AppTheming"; import equal from "fast-deep-equal/es6"; import { FlexVerticalAlignment, ResponsiveBehavior, } from "layoutSystems/common/utils/constants"; import { noop, retryPromise } from "utils/AppsmithUtils"; import type { ExtraDef } from "utils/autocomplete/defCreatorUtils"; import { generateTypeDef } from "utils/autocomplete/defCreatorUtils"; import type { DynamicPath } from "utils/DynamicBindingUtils"; import { klonaRegularWithTelemetry } from "utils/helpers"; import localStorage from "utils/localStorage"; import type { AnvilConfig, AutocompletionDefinitions, PropertyUpdates, SnipingModeProperty, } from "WidgetProvider/constants"; import type { WidgetQueryConfig, WidgetQueryGenerationFormConfig, } from "WidgetQueryGenerators/types"; import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; import { TimePrecision } from "widgets/DatePickerWidget2/constants"; import type { MenuItem } from "widgets/MenuButtonWidget/constants"; import { MenuItemsSource } from "widgets/MenuButtonWidget/constants"; import { DefaultAutocompleteDefinitions, sanitizeKey, } from "widgets/WidgetUtils"; import { ButtonCell } from "../component/cellComponents/ButtonCell"; import { CheckboxCell } from "../component/cellComponents/CheckboxCell"; import { DateCell } from "../component/cellComponents/DateCell"; import { EditActionCell } from "../component/cellComponents/EditActionsCell"; import HTMLCell from "../component/cellComponents/HTMLCell"; import { IconButtonCell } from "../component/cellComponents/IconButtonCell"; import { ImageCell } from "../component/cellComponents/ImageCell"; import { MenuButtonCell } from "../component/cellComponents/MenuButtonCell"; import PlainTextCell from "../component/cellComponents/PlainTextCell"; import { SelectCell } from "../component/cellComponents/SelectCell"; import { SwitchCell } from "../component/cellComponents/SwitchCell"; import { VideoCell } from "../component/cellComponents/VideoCell"; import type { ColumnProperties, ReactTableColumnProps, ReactTableFilter, } from "../component/Constants"; import { AddNewRowActions, CompactModeTypes, DEFAULT_FILTER, SORT_ORDER, SortOrderTypes, StickyType, } from "../component/Constants"; import { CellWrapper } from "../component/TableStyledWrappers"; import type { EditableCell, OnColumnEventArgs, TableWidgetProps, TransientDataPayload, } from "../constants"; import { ActionColumnTypes, ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING, 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 IconSVG from "../icon.svg"; import ThumbnailSVG from "../thumbnail.svg"; import derivedProperties from "./parseDerivedProperties"; import contentConfig from "./propertyConfig/contentConfig"; import styleConfig from "./propertyConfig/styleConfig"; import type { getColumns } from "./reactTableUtils/getColumnsPureFn"; import { getMemoiseGetColumnsWithLocalStorageFn } from "./reactTableUtils/getColumnsPureFn"; import type { tableData, transformDataWithEditableCell, } from "./reactTableUtils/transformDataPureFn"; import { getMemoiseTransformDataWithEditableCell } from "./reactTableUtils/transformDataPureFn"; import { createEditActionColumn, deleteLocalTableColumnOrderByWidgetId, generateLocalNewColumnOrderFromStickyValue, generateNewColumnOrderFromStickyValue, getAllStickyColumnsCount, getAllTableColumnKeys, getBooleanPropertyValue, getCellProperties, getColumnOrderByWidgetIdFromLS, getColumnType, getDefaultColumnProperties, getDerivedColumns, getSelectRowIndex, getSelectRowIndices, getTableStyles, isColumnTypeEditable, updateAndSyncTableLocalColumnOrders, } from "./utilities"; const ReactTableComponent = lazy(async () => retryPromise(async () => import("../component")), ); // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any 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; // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any memoiseGetColumnsWithLocalStorage: (localStorage: any) => getColumns; memoiseTransformDataWithEditableCell: transformDataWithEditableCell; static type = "TABLE_WIDGET_V2"; static getConfig() { return { name: "Table", iconSVG: IconSVG, thumbnailSVG: ThumbnailSVG, 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: false, isVisibleDownload: true, isVisiblePagination: true, isSortable: true, delimiter: ",", version: 2, inlineEditingSaveOption: InlineEditingSaveOptions.ROW_LEVEL, enableServerSideFiltering: TableWidgetV2.getFeatureFlag( ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING, ) ? false : undefined, customIsLoading: false, customIsLoadingValue: "", cachedTableData: {}, endOfData: 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, }); dynamicPropertyPathList.push({ key: "tableData" }); } 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 getAnvilConfig(): AnvilConfig | null { return { isLargeWidget: false, widgetSize: { maxHeight: {}, maxWidth: {}, minHeight: { base: "300px" }, minWidth: { base: "280px" }, }, }; } 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(); } // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any 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), }; if (this.getFeatureFlag(ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING)) { config["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: "array", }, }, }; } /* * Function to get the table columns with appropriate render functions * based on columnType */ getTableColumns = () => { const { columnWidthMap, isPreviewMode, 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, isPreviewMode, ); }; 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(); } // Commit Batch Updates property `true` is passed as commitBatchMetaUpdates is not called on componentDidMount and we need to call it for updating the batch updates this.updateInfiniteScrollProperties(true); } componentDidUpdate(prevProps: TableWidgetProps) { const { commitBatchMetaUpdates, componentHeight, defaultSelectedRowIndex, defaultSelectedRowIndices, infiniteScrollEnabled, pageNo, pageSize, primaryColumns = {}, pushBatchMetaUpdates, 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 = this.props.tableData !== prevProps.tableData; // 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 */ pushBatchMetaUpdates("transientTableData", {}); // reset updatedRowIndex whenever transientTableData is flushed. pushBatchMetaUpdates("updatedRowIndex", -1); /* * Updating the caching layer on table data modification * Commit Batch Updates property `false` is passed as commitBatchMetaUpdates is called on componentDidUpdate * and we need not to explicitly call it for updating the batch updates * */ this.updateInfiniteScrollProperties(); this.pushClearEditableCellsUpdates(); pushBatchMetaUpdates("selectColumnFilterText", {}); } else { // TODO: reset the widget on any property change, like if the toggle of infinite scroll is enabled and previously it was disabled, currently we update cachedTableData property to the current tableData at pageNo. /* * Commit Batch Updates property `false` is passed as commitBatchMetaUpdates is called on componentDidUpdate * and we need not to explicitly call it for updating the batch updates * */ if ( !prevProps.infiniteScrollEnabled && this.props.infiniteScrollEnabled ) { this.updateInfiniteScrollProperties(); } } 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); } } } // Reset widget state when infinite scroll is initially enabled // This should come after all updateInfiniteScrollProperties are done if (!prevProps.infiniteScrollEnabled && infiniteScrollEnabled) { this.resetTableForInfiniteScroll(); } // Reset widget state when height changes while infinite scroll is enabled if ( infiniteScrollEnabled && prevProps.componentHeight !== componentHeight ) { this.resetTableForInfiniteScroll(); } /* * 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 { customIsLoading, customIsLoadingValue, customSortFunction: customSortFunctionData, delimiter, filteredTableData = [], isVisibleDownload, isVisibleFilters, isVisiblePagination, isVisibleSearch, pageSize, primaryColumns, totalRecordsCount, } = this.props; const tableColumns = this.getTableColumns() || emptyArr; let data = filteredTableData; if (customSortFunctionData && Array.isArray(customSortFunctionData)) { data = customSortFunctionData; } const transformedData = this.transformData(data, 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); } }; // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any 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 = klonaRegularWithTelemetry( this.props.transientTableData, "TableWidgetV2.removeRowFromTransientTableData", ); 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, }, }, ); }; // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any 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 ? row[ORIGINAL_INDEX_KEY] ?? rowIndex : 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 ( ); case ColumnTypes.HTML: 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, __originalIndex__: this.getRowOriginalIndex(rowIndex), }); 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, ) => { 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 = ( // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any 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); } }; updateInfiniteScrollProperties(shouldCommitBatchUpdates?: boolean) { const { cachedTableData, commitBatchMetaUpdates, infiniteScrollEnabled, pageNo, pageSize, processedTableData, pushBatchMetaUpdates, tableData, totalRecordsCount, } = this.props; if (infiniteScrollEnabled) { // Update the cache key for a particular page whenever this function is called. The pageNo data is updated with the tableData. const updatedCachedTableData = { ...(cachedTableData || {}), [pageNo]: tableData, }; pushBatchMetaUpdates("cachedTableData", updatedCachedTableData); // The check (!!totalRecordsCount && processedTableData.length === totalRecordsCount) is added if the totalRecordsCount property is set then match the length with the processedTableData which has all flatted data from each page in a single array except the current tableData page i.e. [ ...array of page 1 data, ...array of page 2 data ]. Another 'or' check is if (tableData.length < pageSize) when totalRecordsCount is undefined. Table data has a single page data and if the data comes out to be lesser than the pageSize, it is assumed that the data is finished. if ( (!!totalRecordsCount && processedTableData.length + tableData.length === totalRecordsCount) || (!totalRecordsCount && tableData.length < pageSize) ) { pushBatchMetaUpdates("endOfData", true); } else { pushBatchMetaUpdates("endOfData", false); } if (shouldCommitBatchUpdates) { commitBatchMetaUpdates(); } } } resetTableForInfiniteScroll = () => { const { infiniteScrollEnabled, pushBatchMetaUpdates, updateWidgetMetaProperty, } = this.props; if (infiniteScrollEnabled) { // reset the cachedRows const isAlreadyOnFirstPage = this.props.pageNo === 1; const data = isAlreadyOnFirstPage ? { 1: this.props.tableData } : {}; pushBatchMetaUpdates("cachedTableData", data); pushBatchMetaUpdates("endOfData", false); // Explicitly reset specific meta properties updateWidgetMetaProperty("selectedRowIndex", undefined); updateWidgetMetaProperty("selectedRowIndices", undefined); updateWidgetMetaProperty("searchText", undefined); updateWidgetMetaProperty("triggeredRowIndex", undefined); updateWidgetMetaProperty("filters", []); updateWidgetMetaProperty("sortOrder", { column: "", order: null, }); updateWidgetMetaProperty("transientTableData", {}); updateWidgetMetaProperty("updatedRowIndex", -1); updateWidgetMetaProperty("editableCell", defaultEditableCell); updateWidgetMetaProperty("columnEditableCellValue", {}); updateWidgetMetaProperty("selectColumnFilterText", {}); updateWidgetMetaProperty("isAddRowInProgress", false); updateWidgetMetaProperty("newRowContent", undefined); updateWidgetMetaProperty("newRow", undefined); updateWidgetMetaProperty("previousPageVisited", false); updateWidgetMetaProperty("nextPageVisited", false); // reset and reload page if (!isAlreadyOnFirstPage) { this.updatePageNumber(1, EventType.ON_NEXT_PAGE); } } }; } export default TableWidgetV2;