import React, { lazy, Suspense } from "react"; import log from "loglevel"; import moment, { MomentInput } from "moment"; import _, { isNumber, isString, isNil, xor, without, isBoolean, isArray, xorWith, isEmpty, union, isObject, orderBy, } from "lodash"; import BaseWidget, { WidgetState } from "widgets/BaseWidget"; import { RenderModes, WidgetType, WIDGET_PADDING, } from "constants/WidgetConstants"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import Skeleton from "components/utils/Skeleton"; import { noop, retryPromise } from "utils/AppsmithUtils"; import { ReactTableFilter, AddNewRowActions, DEFAULT_FILTER, } from "../component/Constants"; import { ActionColumnTypes, ColumnTypes, COLUMN_MIN_WIDTH, DateInputFormat, defaultEditableCell, DEFAULT_BUTTON_LABEL, DEFAULT_COLUMN_WIDTH, DEFAULT_MENU_BUTTON_LABEL, DEFAULT_MENU_VARIANT, EditableCell, EditableCellActions, InlineEditingSaveOptions, OnColumnEventArgs, ORIGINAL_INDEX_KEY, TableWidgetProps, TransientDataPayload, } from "../constants"; import derivedProperties from "./parseDerivedProperties"; import { getAllTableColumnKeys, getDefaultColumnProperties, getDerivedColumns, getTableStyles, getSelectRowIndex, getSelectRowIndices, getCellProperties, isColumnTypeEditable, getColumnType, getBooleanPropertyValue, } from "./utilities"; import { ColumnProperties, ReactTableColumnProps, CompactModeTypes, SortOrderTypes, } from "../component/Constants"; import contentConfig from "./propertyConfig/contentConfig"; import styleConfig from "./propertyConfig/styleConfig"; import { BatchPropertyUpdatePayload } from "actions/controlActions"; import { IconName, IconNames } from "@blueprintjs/icons"; import { Colors } from "constants/Colors"; import equal from "fast-deep-equal/es6"; import { 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 { Stylesheet } from "entities/AppTheming"; import { DateCell } from "../component/cellComponents/DateCell"; import { MenuItem, MenuItemsSource } from "widgets/MenuButtonWidget/constants"; import { TimePrecision } from "widgets/DatePickerWidget2/constants"; const ReactTableComponent = lazy(() => retryPromise(() => import("../component")), ); class TableWidgetV2 extends BaseWidget { inlineEditTimer: number | null = null; static getPropertyPaneContentConfig() { return contentConfig; } static getPropertyPaneStyleConfig() { return styleConfig; } 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, }; } 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}}", }, }, }; } /* * Function to get the table columns with appropriate render functions * based on columnType */ getTableColumns = () => { const { columnWidthMap = {}, orderedTableColumns = [] } = this.props; let columns: ReactTableColumnProps[] = []; const hiddenColumns: ReactTableColumnProps[] = []; const { componentWidth } = this.getPaddingAdjustedDimensions(); let totalColumnWidth = 0; if (isArray(orderedTableColumns)) { orderedTableColumns.forEach((column: any) => { const isHidden = !column.isVisible; const columnData = { id: column.id, Header: column.label, alias: column.alias, accessor: (row: any) => row[column.alias], width: columnWidthMap[column.id] || DEFAULT_COLUMN_WIDTH, minWidth: COLUMN_MIN_WIDTH, draggable: true, isHidden: false, isAscOrder: column.isAscOrder, isDerived: column.isDerived, metaProperties: { isHidden: isHidden, type: column.columnType, format: column.outputFormat || "", inputFormat: column.inputFormat || "", }, columnProperties: column, Cell: this.renderCell, }; const isAllCellVisible: boolean | boolean[] = column.isCellVisible; /* * If all cells are not visible or column itself is not visible, * set isHidden and push it to hiddenColumns array else columns array */ if ( (isBoolean(isAllCellVisible) && !isAllCellVisible) || (isArray(isAllCellVisible) && isAllCellVisible.every((visibility) => visibility === false)) || isHidden ) { columnData.isHidden = true; hiddenColumns.push(columnData); } else { totalColumnWidth += columnData.width; columns.push(columnData); } }); } const lastColumnIndex = columns.length - 1; if (totalColumnWidth < componentWidth) { /* This "if" block is responsible for upsizing the last column width if there is space left in the table container towards the right */ if (columns[lastColumnIndex]) { const lastColumnWidth = columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH; const remainingWidth = componentWidth - totalColumnWidth; // Adding the remaining width i.e. space left towards the right, to the last column width columns[lastColumnIndex].width = lastColumnWidth + remainingWidth; } } else if (totalColumnWidth > componentWidth) { /* This "else-if" block is responsible for downsizing the last column width if the last column spills over resulting in horizontal scroll */ const extraWidth = totalColumnWidth - componentWidth; const lastColWidth = columns[lastColumnIndex].width || DEFAULT_COLUMN_WIDTH; /* Below if condition explanation: Condition 1: (lastColWidth > COLUMN_MIN_WIDTH) We will downsize the last column only if its greater than COLUMN_MIN_WIDTH Condition 2: (extraWidth < lastColWidth) This condition checks whether the last column is the only column that is spilling over. If more than one columns are spilling over we won't downsize the last column */ if (lastColWidth > COLUMN_MIN_WIDTH && extraWidth < lastColWidth) { const availableWidthForLastColumn = lastColWidth - extraWidth; /* Below we are making sure last column width doesn't go lower than COLUMN_MIN_WIDTH again as availableWidthForLastColumn might go lower than COLUMN_MIN_WIDTH in some cases */ columns[lastColumnIndex].width = availableWidthForLastColumn < COLUMN_MIN_WIDTH ? COLUMN_MIN_WIDTH : availableWidthForLastColumn; } } if (hiddenColumns.length && this.props.renderMode === RenderModes.CANVAS) { columns = columns.concat(hiddenColumns); } return columns.filter((column: ReactTableColumnProps) => !!column.id); }; transformData = ( tableData: Array>, columns: ReactTableColumnProps[], ) => { if (isArray(tableData)) { return tableData.map((row, rowIndex) => { const newRow: { [key: string]: any } = {}; columns.forEach((column) => { const { alias } = column; let value = row[alias]; if (column.metaProperties) { switch (column.metaProperties.type) { case ColumnTypes.DATE: let isValidDate = true; const outputFormat = _.isArray(column.metaProperties.format) ? column.metaProperties.format[rowIndex] : column.metaProperties.format; let inputFormat; try { const type = _.isArray(column.metaProperties.inputFormat) ? column.metaProperties.inputFormat[rowIndex] : column.metaProperties.inputFormat; if ( type !== DateInputFormat.EPOCH && type !== DateInputFormat.MILLISECONDS ) { inputFormat = type; moment(value as MomentInput, inputFormat); } else if (!isNumber(value)) { isValidDate = false; } } catch (e) { isValidDate = false; } if (isValidDate && value) { try { if ( column.metaProperties.inputFormat === DateInputFormat.MILLISECONDS ) { value = Number(value); } else if ( column.metaProperties.inputFormat === DateInputFormat.EPOCH ) { value = 1000 * Number(value); } newRow[alias] = moment( value as MomentInput, inputFormat, ).format(outputFormat); } catch (e) { log.debug("Unable to parse Date:", { e }); newRow[alias] = ""; } } else if (value) { newRow[alias] = "Invalid Value"; } else { newRow[alias] = ""; } break; default: let data; if ( _.isString(value) || _.isNumber(value) || _.isBoolean(value) ) { data = value; } else if (isNil(value)) { data = ""; } else { data = JSON.stringify(value); } newRow[alias] = data; break; } } }); /* * Inject the edited cell value from the editableCell object */ if (this.props.editableCell?.index === rowIndex) { const { column, inputValue } = this.props.editableCell; newRow[column] = inputValue; } return newRow; }); } else { return []; } }; 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 { tableData = [], primaryColumns = {} } = 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, ) => { 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); propertiesToAdd["columnOrder"] = newColumnOrder; } 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); } } }; componentDidMount() { const { 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); } } } 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; } // Check if tableData is modifed const isTableDataModified = !equal( this.props.tableData, prevProps.tableData, ); // If the user has changed the tableData OR // The binding has returned a new value if (isTableDataModified) { this.updateMetaRowData( prevProps.filteredTableData, this.props.filteredTableData, ); this.props.updateWidgetMetaProperty("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); } this.props.updateWidgetMetaProperty("filters", [DEFAULT_FILTER]); } } /* * Clear transient table data and editablecell when tableData changes */ if (isTableDataModified) { this.props.updateWidgetMetaProperty("transientTableData", {}); // reset updatedRowIndex whenever transientTableData is flushed. this.props.updateWidgetMetaProperty("updatedRowIndex", -1); this.clearEditableCell(true); this.props.updateWidgetMetaProperty("selectColumnFilterText", {}); } if (!pageNo) { this.props.updateWidgetMetaProperty("pageNo", 1); } //check if pageNo does not excede the max Page no, due to change of totalRecordsCount if (serverSidePaginationEnabled && totalRecordsCount) { const maxAllowedPageNumber = Math.ceil(totalRecordsCount / pageSize); if (pageNo > maxAllowedPageNumber) { this.props.updateWidgetMetaProperty("pageNo", maxAllowedPageNumber); } } else if ( serverSidePaginationEnabled !== prevProps.serverSidePaginationEnabled ) { //reset pageNo when serverSidePaginationEnabled is toggled this.props.updateWidgetMetaProperty("pageNo", 1); } /* * When defaultSelectedRowIndex or defaultSelectedRowIndices * is changed from property pane */ if ( !equal(defaultSelectedRowIndex, prevProps.defaultSelectedRowIndex) || !equal(defaultSelectedRowIndices, prevProps.defaultSelectedRowIndices) ) { this.updateSelectedRowIndex(); } this.resetPageNo(prevProps); this.resetRowSelectionProperties(prevProps); } resetPageNo = (prevProps: TableWidgetProps) => { const { onPageSizeChange, pageSize } = this.props; if (pageSize !== prevProps.pageSize) { if (onPageSizeChange) { this.props.updateWidgetMetaProperty("pageNo", 1, { triggerPropertyName: "onPageSizeChange", dynamicString: onPageSizeChange, event: { type: EventType.ON_PAGE_SIZE_CHANGE, }, }); } else { this.props.updateWidgetMetaProperty("pageNo", 1); } } }; resetRowSelectionProperties = (prevProps: TableWidgetProps) => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, } = this.props; // reset selectedRowIndices and selectedRowIndex to defaults if (multiRowSelection !== prevProps.multiRowSelection) { if (multiRowSelection) { if ( defaultSelectedRowIndices && _.isArray(defaultSelectedRowIndices) && defaultSelectedRowIndices.every((i) => _.isFinite(i)) ) { this.props.updateWidgetMetaProperty( "selectedRowIndices", defaultSelectedRowIndices, ); } this.props.updateWidgetMetaProperty("selectedRowIndex", -1); } else { if (!isNil(defaultSelectedRowIndex) && defaultSelectedRowIndex > -1) { this.props.updateWidgetMetaProperty( "selectedRowIndex", defaultSelectedRowIndex, ); } this.props.updateWidgetMetaProperty("selectedRowIndices", []); } } }; /* * Function to update selectedRowIndices & selectedRowIndex from * defaultSelectedRowIndices & defaultSelectedRowIndex respectively */ updateSelectedRowIndex = () => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, } = this.props; if (multiRowSelection) { this.props.updateWidgetMetaProperty( "selectedRowIndices", defaultSelectedRowIndices, ); } else { this.props.updateWidgetMetaProperty( "selectedRowIndex", defaultSelectedRowIndex, ); } }; /* * Function to update selectedRow details when order of tableData changes */ updateMetaRowData = ( oldTableData: Array>, newTableData: Array>, ) => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, primaryColumnId, selectedRowIndex, selectedRowIndices, } = this.props; if (multiRowSelection) { const indices = getSelectRowIndices( oldTableData, newTableData, defaultSelectedRowIndices, selectedRowIndices, primaryColumnId, ); this.props.updateWidgetMetaProperty("selectedRowIndices", indices); } else { const index = getSelectRowIndex( oldTableData, newTableData, defaultSelectedRowIndex, selectedRowIndex, primaryColumnId, ); this.props.updateWidgetMetaProperty("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[]) => { this.resetSelectedRowIndex(); this.props.updateWidgetMetaProperty("filters", filters); // Reset Page only when a filter is added if (!isEmpty(xorWith(filters, [DEFAULT_FILTER], equal))) { this.props.updateWidgetMetaProperty("pageNo", 1); } }; toggleDrag = (disable: boolean) => { this.disableDrag(disable); }; getPaddingAdjustedDimensions = () => { // eslint-disable-next-line prefer-const let { componentHeight, componentWidth } = this.getComponentDimensions(); // (2 * WIDGET_PADDING) gives the total horizontal padding (i.e. paddingLeft + paddingRight) componentWidth = componentWidth - 2 * WIDGET_PADDING; return { componentHeight, componentWidth }; }; getPageView() { const { totalRecordsCount, delimiter, pageSize, filteredTableData = [], isVisibleDownload, isVisibleFilters, isVisiblePagination, isVisibleSearch, } = this.props; const tableColumns = this.getTableColumns() || []; const transformedData = this.transformData(filteredTableData, tableColumns); const isVisibleHeaderOptions = isVisibleDownload || isVisibleFilters || isVisiblePagination || isVisibleSearch; const { componentHeight, componentWidth, } = this.getPaddingAdjustedDimensions(); if (this.props.isAddRowInProgress) { transformedData.unshift(this.props.newRowContent); } return ( }> ); } handleReorderColumn = (columnOrder: string[]) => { columnOrder = columnOrder.map((alias) => this.getColumnIdByAlias(alias)); if (this.props.renderMode === RenderModes.CANVAS) { super.updateWidgetProperty("columnOrder", columnOrder); } else { this.props.updateWidgetMetaProperty("columnOrder", columnOrder); } }; handleColumnSorting = (columnAccessor: string, isAsc: boolean) => { const columnId = this.getColumnIdByAlias(columnAccessor); this.resetSelectedRowIndex(false); let sortOrderProps; if (columnId) { sortOrderProps = { column: columnId, order: isAsc ? SortOrderTypes.asc : SortOrderTypes.desc, }; } else { sortOrderProps = { column: "", order: null, }; } this.props.updateWidgetMetaProperty("sortOrder", sortOrderProps, { triggerPropertyName: "onSort", dynamicString: this.props.onSort, event: { type: EventType.ON_SORT, }, }); }; handleResizeColumn = (columnWidthMap: { [key: string]: number }) => { if (this.props.renderMode === RenderModes.CANVAS) { super.updateWidgetProperty("columnWidthMap", columnWidthMap); } else { this.props.updateWidgetMetaProperty("columnWidthMap", columnWidthMap); } }; handleSearchTable = (searchKey: any) => { const { multiRowSelection, onSearchTextChanged } = this.props; /* * Clear rowSelection to avoid selecting filtered rows * based on stale selection indices */ if (multiRowSelection) { this.props.updateWidgetMetaProperty("selectedRowIndices", []); } else { this.props.updateWidgetMetaProperty("selectedRowIndex", -1); } this.props.updateWidgetMetaProperty("pageNo", 1); this.props.updateWidgetMetaProperty("searchText", searchKey, { triggerPropertyName: "onSearchTextChanged", dynamicString: onSearchTextChanged, event: { type: EventType.ON_SEARCH, }, }); }; /* * Function to handle customColumn button type click interactions */ onColumnEvent = ({ rowIndex, action, onComplete = noop, triggerPropertyName, eventType, row, additionalData = {}, }: OnColumnEventArgs) => { const { filteredTableData = [] } = this.props; try { row = row || filteredTableData[rowIndex]; if (action) { this.props.updateWidgetMetaProperty( "triggeredRowIndex", row?.[ORIGINAL_INDEX_KEY], { triggerPropertyName: triggerPropertyName, dynamicString: action, event: { type: eventType, callback: onComplete, }, globalContext: { currentRow: row, ...additionalData }, }, ); } else { onComplete(); } } catch (error) { log.debug("Error parsing row action", error); } }; 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, ); this.props.updateWidgetMetaProperty( "selectedRowIndices", selectedRowIndices, ); } }; handleRowClick = (row: Record, selectedIndex: number) => { const { multiRowSelection, selectedRowIndex, selectedRowIndices, } = this.props; 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) => { if (event) { this.props.updateWidgetMetaProperty("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { type: event, }, }); } else { this.props.updateWidgetMetaProperty("pageNo", pageNo); } if (this.props.onPageChange) { this.resetSelectedRowIndex(); } }; handleNextPageClick = () => { const pageNo = (this.props.pageNo || 1) + 1; this.props.updateWidgetMetaProperty("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { type: EventType.ON_NEXT_PAGE, }, }); if (this.props.onPageChange) { this.resetSelectedRowIndex(); } }; resetSelectedRowIndex = (skipDefault?: boolean) => { const { defaultSelectedRowIndex, defaultSelectedRowIndices, multiRowSelection, } = this.props; if (multiRowSelection) { this.props.updateWidgetMetaProperty( "selectedRowIndices", skipDefault ? [] : defaultSelectedRowIndices, ); } else { this.props.updateWidgetMetaProperty( "selectedRowIndex", skipDefault ? -1 : defaultSelectedRowIndex, ); } }; unSelectAllRow = () => { this.props.updateWidgetMetaProperty("selectedRowIndices", []); }; handlePrevPageClick = () => { const pageNo = (this.props.pageNo || 1) - 1; if (pageNo >= 1) { this.props.updateWidgetMetaProperty("pageNo", pageNo, { triggerPropertyName: "onPageChange", dynamicString: this.props.onPageChange, event: { type: EventType.ON_PREV_PAGE, }, }); if (this.props.onPageChange) { this.resetSelectedRowIndex(); } } }; static getWidgetType(): WidgetType { return "TABLE_WIDGET_V2"; } 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; }); } updateTransientTableData = (data: TransientDataPayload) => { const { __originalIndex__, ...transientData } = data; this.props.updateWidgetMetaProperty("transientTableData", { ...this.props.transientTableData, [__originalIndex__]: { ...this.props.transientTableData[__originalIndex__], ...transientData, }, }); this.props.updateWidgetMetaProperty("updatedRowIndex", __originalIndex__); }; removeRowFromTransientTableData = (index: number) => { const newTransientTableData = clone(this.props.transientTableData); if (newTransientTableData) { delete newTransientTableData[index]; this.props.updateWidgetMetaProperty( "transientTableData", newTransientTableData, ); } this.props.updateWidgetMetaProperty("updatedRowIndex", -1); }; 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 { filteredTableData = [], multiRowSelection, selectedRowIndex, selectedRowIndices, compactMode = CompactModeTypes.DEFAULT, } = 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; } /* * 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); 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 isNewRow = this.props.isAddRowInProgress && rowIndex === 0; 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, ) => { if (this.props.isAddRowInProgress) { this.updateNewRowValues(alias, inputValue, value); } else { this.props.updateWidgetMetaProperty("editableCell", { ...this.props.editableCell, value: value, inputValue, }); if (this.props.editableCell?.column) { this.props.updateWidgetMetaProperty("columnEditableCellValue", { ...this.props.columnEditableCellValue, [this.props.editableCell?.column]: value, }); } } }; 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); } this.props.updateWidgetMetaProperty("editableCell", { column: alias, index: rowIndex, value: value, // To revert back to previous on discard initialValue: value, inputValue: value, }); this.props.updateWidgetMetaProperty("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) { this.props.updateWidgetMetaProperty("selectedRowIndices", []); } else { this.props.updateWidgetMetaProperty("selectedRowIndex", -1); } } } else { if ( this.isColumnCellValid(alias) && action === EditableCellActions.SAVE && value !== this.props.editableCell?.initialValue ) { this.updateTransientTableData({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [alias]: this.props.editableCell?.value, }); if (onSubmit && this.props.editableCell?.column) { this.onColumnEvent({ rowIndex: rowIndex, action: onSubmit, triggerPropertyName: "onSubmit", eventType: EventType.ON_SUBMIT, row: { ...this.props.filteredTableData[rowIndex], [this.props.editableCell.column]: this.props.editableCell.value, }, }); } 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)) { this.updateTransientTableData({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [alias]: value, }); if (onSubmit && this.props.editableCell?.column) { this.onColumnEvent({ rowIndex: rowIndex, action: onSubmit, triggerPropertyName: "onSubmit", eventType: EventType.ON_SUBMIT, row: { ...this.props.filteredTableData[rowIndex], [this.props.editableCell.column]: value, }, }); } this.clearEditableCell(); } }; clearEditableCell = (skipTimeout?: boolean) => { const clear = () => { this.props.updateWidgetMetaProperty("editableCell", defaultEditableCell); this.props.updateWidgetMetaProperty("columnEditableCellValue", {}); }; if (skipTimeout) { clear(); } else { /* * We need to let the evaulations compute derived property (filteredTableData) * before we clear the editableCell to avoid the text flickering */ // @ts-expect-error: setTimeout return type mismatch 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 { this.updateTransientTableData({ [ORIGINAL_INDEX_KEY]: this.getRowOriginalIndex(rowIndex), [column]: value, }); this.props.updateWidgetMetaProperty("editableCell", defaultEditableCell); if (action && this.props.editableCell?.column) { this.onColumnEvent({ rowIndex, action: action, triggerPropertyName: "onOptionChange", eventType: EventType.ON_OPTION_CHANGE, row: { ...this.props.filteredTableData[rowIndex], [this.props.editableCell.column]: value, }, }); } } }; onSelectFilterChange = ( text: string, rowIndex: number, serverSideFiltering: boolean, alias: string, action?: string, ) => { this.props.updateWidgetMetaProperty("selectColumnFilterText", { ...this.props.selectColumnFilterText, [alias]: text, }); if (action && serverSideFiltering) { this.onColumnEvent({ rowIndex, action: action, triggerPropertyName: "onFilterUpdate", eventType: EventType.ON_FILTER_UPDATE, row: { ...this.props.filteredTableData[rowIndex], }, additionalData: { filterText: text, }, }); } }; onCheckChange = ( column: any, row: Record, value: boolean, alias: string, originalIndex: number, rowIndex: number, ) => { if (this.props.isAddRowInProgress) { this.updateNewRowValues(alias, value, value); } else { this.updateTransientTableData({ [ORIGINAL_INDEX_KEY]: originalIndex, [alias]: value, }); this.onColumnEvent({ rowIndex, action: column.onCheckChange, triggerPropertyName: "onCheckChange", eventType: EventType.ON_CHECK_CHANGE, row: { ...row, [alias]: value, }, }); } }; handleAddNewRowClick = () => { const defaultNewRow = this.props.defaultNewRow || {}; this.props.updateWidgetMetaProperty("isAddRowInProgress", true); this.props.updateWidgetMetaProperty("newRowContent", defaultNewRow); this.props.updateWidgetMetaProperty("newRow", defaultNewRow); // New row gets added at the top of page 1 when client side pagination enabled if (!this.props.serverSidePaginationEnabled) { this.props.updateWidgetMetaProperty("pageNo", 1); } //Since we're adding a newRowContent thats not part of tableData, the index changes // so we're resetting the row selection this.props.updateWidgetMetaProperty("selectedRowIndex", -1); this.props.updateWidgetMetaProperty("selectedRowIndices", []); }; handleAddNewRowAction = ( type: AddNewRowActions, onActionComplete: () => void, ) => { let triggerPropertyName, action, eventType; const onComplete = () => { this.props.updateWidgetMetaProperty("isAddRowInProgress", false); this.props.updateWidgetMetaProperty("newRowContent", undefined); this.props.updateWidgetMetaProperty("newRow", undefined); 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, ) => { /* * newRowContent holds whatever the user types while newRow holds the parsed value * newRowContent is being used to populate the cell while newRow is being used * for validations. */ this.props.updateWidgetMetaProperty("newRowContent", { ...this.props.newRowContent, [alias]: value, }); this.props.updateWidgetMetaProperty("newRow", { ...this.props.newRow, [alias]: parsedValue, }); }; } export default TableWidgetV2;