import type React from 'react' import { useEffect, useState } from 'react' import type { groupAccordionType, GroupColorType, RowStylingProps, } from './StandardTable.models' import { type TooltipProps } from '../Tooltip/Tooltip' // Helper function to safely get row styling properties using keyof export const getRowStylingProperty = ( data: T, property: keyof RowStylingProps, ): string | React.CSSProperties | undefined => { return (data as T & Partial)[property] } // Helper function to safely get row style object for spreading export const getRowStyle = (data: T): React.CSSProperties => { const style = getRowStylingProperty(data, 'rowStyle') return style && typeof style === 'object' ? style : {} } // Helper function to safely handle cell styles from ConfigItemType export const getCellStyle = ( data: T, cellStyle?: StyleGeneric, ): React.CSSProperties => { if (!cellStyle) return {} if ( typeof cellStyle === 'object' && !Array.isArray(cellStyle) && cellStyle !== null ) { return cellStyle as React.CSSProperties } if (typeof cellStyle === 'function') { return (cellStyle as (data: T) => React.CSSProperties)(data) } return {} } // Helper function to merge row-level styles with cell-level styles export const getMergedCellStyle = ( data: T, cellStyle?: StyleGeneric, ): React.CSSProperties => { const rowStyle = getRowStyle(data) const cellStyleResult = getCellStyle(data, cellStyle) return { ...rowStyle, // Row styles first (base styles) ...cellStyleResult, // Cell styles second (overrides row styles) } } type Groups = Array<{ /** The string value or react node to display in the group Header row */ groupHeader: string | ((count: number) => string) | React.ReactNode /** Custom JSX to display info icon in the group Header row (groupHeader) */ tooltipContent?: TooltipProps['tooltipContent'] /** Determines the background color of the row (Default: grey) */ type?: GroupColorType /** Method to determine if a data set belongs to the group */ check: (dataItem: DataItem, checkedBoxes?: DataItem[]) => boolean /** boolean to add Clear button in the selection */ includeClearButton?: boolean /** Function to perform the required action after clearing the selections */ clearCallout?: () => void /** Custom JSX at the last column of the group header to replace clearButton */ customClearButton?: React.ReactNode /** Adds an expand/collapse functionality to entire groups */ groupAccordion?: groupAccordionType /** Override: Always show this group regardless of checkbox state */ alwaysShowGroups?: boolean }> /** * Determines whether a group should be shown based on checkbox state and group configuration */ function shouldShowGroup( hasCheckboxes: boolean | undefined, checkedBoxes: DataItem[] | undefined, group: Groups[0], ): boolean { return ( shouldShowGroupByCheckboxState(hasCheckboxes, checkedBoxes) || !!group.alwaysShowGroups ) } /** * Determines whether a group should be shown based on checkbox state only (without alwaysShowGroups) */ function shouldShowGroupByCheckboxState( hasCheckboxes: boolean | undefined, checkedBoxes: DataItem[] | undefined, ): boolean { return ( !hasCheckboxes || (Boolean(hasCheckboxes) && Boolean(checkedBoxes) && (checkedBoxes?.length ?? 0) > 0) ) } export const getDataGroups = ( data: DataItem[], groups: Groups, checkBoxes: { hasCheckboxes?: boolean; checkedBoxes?: Array }, totalRowKey?: keyof DataItem, ): DataItem[] => { const { hasCheckboxes, checkedBoxes } = checkBoxes /** SET DATA GROUPS */ if (data?.length > 0) { const organizedGroupData: DataItem[] = [] const mutableData = [...data] as (DataItem & { isGroupHeader?: boolean groupNum?: number groupDataKey?: string })[] groups?.forEach((group, groupIndex) => { let hasData = false let headerIndex: number mutableData.forEach((dataItem, dataIndex) => { /** INSERT A GENERIC GROUP HEADER DATA PLACEHOLDER */ if (dataIndex === 0) { if (shouldShowGroup(hasCheckboxes, checkedBoxes, group)) { organizedGroupData.push({ ...dataItem, // just need all of the keys of the dataItem object in new header object to match table config; values don't matter isGroupHeader: true, groupHeader: group.groupHeader, type: group.type, includeClearButton: group.includeClearButton, clearCallout: group.clearCallout, customClearButton: group.customClearButton, displayHeaderData: false, // Explicitly override groupAccordion so group headers never inherit a stale // value from ...dataItem. The render loop mutates dataItem.groupAccordion // directly; without this override, groups without an accordion config // would spread that mutation and show a phantom accordion icon. ...(group.groupAccordion ? { groupAccordion: group.groupAccordion } : { groupAccordion: undefined }), tooltipContent: group.tooltipContent, }) headerIndex = organizedGroupData.length - 1 } } // IF DATA ITEM HAS HEADER DETAILS TO DISPLAY if (dataItem?.isGroupHeader && dataItem?.groupNum === groupIndex + 1) { organizedGroupData[headerIndex] = { ...organizedGroupData[headerIndex], ...dataItem, name: group.groupHeader, ...(group.groupAccordion ? { groupDataKey: group.groupAccordion?.groupKey } : {}), } } // if totalRow is present, insert row into data array as the first item if (groupIndex === 0 && dataIndex === 0 && totalRowKey) { organizedGroupData.unshift(dataItem) } // if dataItem is a regular date row and matches the check criteria add it to the data array if ( group.check( dataItem, totalRowKey ? checkedBoxes?.filter((item) => !item[totalRowKey]) : checkedBoxes, ) ) { hasData = true if ( (!totalRowKey || !dataItem[totalRowKey]) && !dataItem?.isGroupHeader ) { dataItem.groupDataKey = group.groupAccordion?.groupKey organizedGroupData.push(dataItem) } } }) // if the group has no data, remove the group header row from the display if ( !hasData && shouldShowGroupByCheckboxState(hasCheckboxes, checkedBoxes) ) { organizedGroupData.pop() } }) return organizedGroupData } else { return data } } export const useShowStickyClasses = ({ innerTableWidth, tableContainerWidth, }: { innerTableWidth: number tableContainerWidth: number }) => { const [lastValues, setLastValues] = useState([]) useEffect(() => { setLastValues((prevValues) => { const newValues = [...prevValues, innerTableWidth] return newValues.length > 7 ? newValues.slice(1) : newValues }) }, [innerTableWidth]) const isStuckInLoop = lastValues.filter((value) => value === innerTableWidth).length > 2 // If more than 2 times we know it's getting stuck in the render bug loop if (isStuckInLoop) return true // Default to true if stuck in loop return tableContainerWidth < innerTableWidth } type FlattenDataProps = { data?: DataItem nestedDataKey?: keyof DataItem level?: number nestedRowProps?: { nestedData: { [key: string]: DataItem[] } subRowDataKey?: keyof DataItem } } export const useFlattenNestedData = ({ nestedRowProps, nestedDataKey, data, }: FlattenDataProps) => { function processData( dataObject: { [key: string]: DataItem[] } | undefined, // all of the nested data objects, referenced by the dataKey nestedDataKey?: keyof DataItem, maxLevel = 3, // max number of levels to flatten (UX design decision) ): { [key: string]: DataItem[] } | undefined { if (!dataObject || !nestedDataKey) { return dataObject } const flatDataObject: { [key: string]: DataItem[] } = {} const getDataRows = (allDataRows: DataItem[], level = 0): DataItem[] => { const rows: DataItem[] = [] allDataRows?.forEach((row) => { rows.push({ ...row, level }) const nextLevelRows = level < maxLevel && typeof row[nestedDataKey] === 'object' && (row[nestedDataKey] as DataItem[]) if (nextLevelRows) { rows.push(...getDataRows(nextLevelRows, level + 1)) } }) return rows } for (const dataSet in dataObject) { flatDataObject[dataSet] = getDataRows(dataObject[dataSet]) } return flatDataObject } const [nestedData, setNestedData] = useState< { [key: string]: DataItem[] } | undefined >() useEffect(() => { const flatData = processData( nestedRowProps?.nestedData, nestedRowProps?.subRowDataKey, ) setNestedData(flatData) }, [ data, nestedDataKey, nestedRowProps?.nestedData, nestedRowProps?.subRowDataKey, ]) return nestedData }