import { ColumnDefinition, ColumnType, FilterType } from '../types' import { ColumnTypeConfig, getColumnTypeConfig } from '../columnTypes' import { SelectMenuGroupType } from '../../Select/types' import { MultiSelectMenuGroupType } from '../../MultiSelect/types' import { getUniqueColumnValues } from '../utils' import { FOUNDATION_THEME } from '../../../tokens' const filterComponentByType: Partial< Record > = { [FilterType.SELECT]: 'select', [FilterType.MULTISELECT]: 'multiselect', [FilterType.DATE]: 'dateRange', [FilterType.SLIDER]: 'slider', } export const getColumnTypeConfigForColumn = ( column: ColumnDefinition> ): ColumnTypeConfig => { const columnConfig = getColumnTypeConfig(column.type || ColumnType.TEXT) const explicitFilterType = column.filterType const mapped = explicitFilterType ? filterComponentByType[explicitFilterType] : undefined // Guard against inherited Object properties: JS consumers are not bound by // the FilterType enum, so a value like 'constructor' would otherwise // resolve to a function and advertise a filter that cannot render. const filterComponent = typeof mapped === 'string' ? mapped : undefined // FilterType values without a filter component (TEXT, NUMBER, BOOLEAN) fall // back to the type-derived config rather than removing the filter entirely. if (!explicitFilterType || !filterComponent) { return columnConfig } return { ...columnConfig, filterType: explicitFilterType, supportsFiltering: true, filterComponent, } } export const getFilterOptions = ( column: ColumnDefinition>, data?: Record[] ) => { if (column.filterOptions && column.filterOptions.length > 0) { return column.filterOptions } if (!data || data.length === 0) { return [] } try { const uniqueValues = getUniqueColumnValues(data, column.field) if (uniqueValues.length === 0) { return [] } return uniqueValues.map((val: string, index: number) => { const normalizedVal = val.toLowerCase().replace(/[^a-z0-9]/g, '-') const uniqueId = normalizedVal ? `${normalizedVal}-${index}` : `option-${index}` return { id: uniqueId, label: val, value: val, } }) } catch (error) { console.warn( `Error generating filter options for column "${column.header}":`, error ) return [] } } export const getSelectMenuItems = ( column: ColumnDefinition>, data?: Record[] ): SelectMenuGroupType[] => { const filterOptions = getFilterOptions(column, data) return [ { groupLabel: `${column.header} Options`, items: filterOptions.map((option) => ({ label: option.label, value: option.value, onClick: () => {}, })), showSeparator: false, }, ] } export const getMultiSelectMenuItems = ( column: ColumnDefinition>, data?: Record[] ): MultiSelectMenuGroupType[] => { const filterOptions = getFilterOptions(column, data) return [ { groupLabel: `${column.header} Options`, items: filterOptions.map((option) => ({ label: option.label, value: option.value, })), showSeparator: false, }, ] } /** * Filters dropdown menu items based on search text * This is used to filter the visible options in select/multiselect dropdowns * NOT for applying column filters to data */ export const filterItemsBySearch = ( items: Array<{ label: string; value: string }>, searchValue: string ): Array<{ label: string; value: string }> => { if (!searchValue) return items return items.filter((item) => item.label.toLowerCase().includes(searchValue.toLowerCase()) ) } export const getPopoverAlignment = ( index: number, totalColumns: number ): 'start' | 'center' | 'end' => { if (index === 0) return 'start' if (index === totalColumns - 1) return 'end' return 'center' } export function getFrozenLeftOffset( index: number, _columnFreeze: number, enableRowExpansion: boolean, enableRowSelection: boolean, visibleColumns: ColumnDefinition>[], getColumnWidth: ( column: ColumnDefinition>, i: number ) => React.CSSProperties, measuredFrozenWidths?: number[] ): number { let leftOffset = 0 if (enableRowExpansion) leftOffset += 50 if (enableRowSelection) leftOffset += 60 if (index === 0) return leftOffset if ( measuredFrozenWidths && measuredFrozenWidths.length >= index && measuredFrozenWidths.slice(0, index).every((w) => w > 0) ) { return ( leftOffset + measuredFrozenWidths.slice(0, index).reduce((a, b) => a + b, 0) ) } for (let i = 0; i < index; i++) { const prevColumn = visibleColumns[i] const prevStyles = getColumnWidth(prevColumn, i) let columnWidth = 140 if (prevColumn.maxWidth) { columnWidth = parseInt(prevColumn.maxWidth.replace(/px|%|em|rem/g, '')) || 140 } else if (prevColumn.minWidth) { columnWidth = parseInt(prevColumn.minWidth.replace(/px|%|em|rem/g, '')) || 140 } else if (prevStyles.maxWidth) { columnWidth = parseInt( String(prevStyles.maxWidth).replace(/px|%|em|rem/g, '') ) || 140 } else if (prevStyles.minWidth) { columnWidth = parseInt( String(prevStyles.minWidth).replace(/px|%|em|rem/g, '') ) || 120 } leftOffset += columnWidth } return leftOffset } const parseWidthPx = (value: unknown, fallback: number): number => { const num = parseInt(String(value ?? '').replace(/px|%|em|rem/g, ''), 10) return Number.isFinite(num) ? num : fallback } export function getFrozenRightOffset( index: number, columnFreezeRight: number, visibleColumns: ColumnDefinition>[], getColumnWidth: ( column: ColumnDefinition>, i: number ) => React.CSSProperties, rightStickyOffsetPx: number ): number { if (columnFreezeRight <= 0) return rightStickyOffsetPx const startIndex = Math.max(visibleColumns.length - columnFreezeRight, 0) if (index < startIndex) return rightStickyOffsetPx let rightOffset = rightStickyOffsetPx for (let i = visibleColumns.length - 1; i > index; i--) { const nextColumn = visibleColumns[i] const nextStyles = getColumnWidth(nextColumn, i) const width = nextColumn.maxWidth ? parseWidthPx(nextColumn.maxWidth, 140) : nextColumn.minWidth ? parseWidthPx(nextColumn.minWidth, 140) : nextStyles.maxWidth ? parseWidthPx(nextStyles.maxWidth, 140) : nextStyles.minWidth ? parseWidthPx(nextStyles.minWidth, 120) : 140 rightOffset += width } return rightOffset } export const getFrozenRightColumnStyles = ( index: number, columnFreezeRight: number, visibleColumns: ColumnDefinition>[], getColumnWidth: ( column: ColumnDefinition>, index: number ) => React.CSSProperties, backgroundColor: React.CSSProperties['backgroundColor'], rightStickyOffsetPx: number, frozenBorder?: React.CSSProperties['borderBottom'] ): React.CSSProperties => { if (columnFreezeRight <= 0) return {} const startIndex = Math.max(visibleColumns.length - columnFreezeRight, 0) if (index < startIndex) return {} const currentColumn = visibleColumns[index] const currentColumnStyles = getColumnWidth(currentColumn, index) const rightOffset = getFrozenRightOffset( index, columnFreezeRight, visibleColumns, getColumnWidth, rightStickyOffsetPx ) const isFirstRightFrozen = index === startIndex const minWidth = currentColumn.minWidth || String(currentColumnStyles.minWidth ?? '140px') const maxWidth = currentColumn.maxWidth || String(currentColumnStyles.maxWidth ?? '200px') return { position: 'sticky' as const, right: `${rightOffset}px`, zIndex: 9, backgroundColor, padding: `0 ${FOUNDATION_THEME.unit[16]}`, minWidth, maxWidth, width: 'auto', boxSizing: 'border-box' as const, overflow: 'hidden', ...(isFirstRightFrozen && { borderLeft: frozenBorder ?? `1px solid ${FOUNDATION_THEME.colors.gray[200]}`, }), } } export const getFrozenColumnStyles = ( index: number, columnFreeze: number, enableRowExpansion: boolean, enableRowSelection: boolean, visibleColumns: ColumnDefinition>[], getColumnWidth: ( column: ColumnDefinition>, index: number ) => React.CSSProperties, backgroundColor: React.CSSProperties['backgroundColor'], measuredFrozenWidths?: number[], frozenBorder?: React.CSSProperties['borderBottom'] ): React.CSSProperties => { if (index >= columnFreeze) return { padding: '0 16px' } const currentColumn = visibleColumns[index] const currentColumnStyles = getColumnWidth(currentColumn, index) const leftOffset = getFrozenLeftOffset( index, columnFreeze, enableRowExpansion, enableRowSelection, visibleColumns, getColumnWidth, measuredFrozenWidths ) const isLastFrozenColumn = index === columnFreeze - 1 const minWidth = currentColumn.minWidth || String(currentColumnStyles.minWidth ?? '140px') const maxWidth = currentColumn.maxWidth || String(currentColumnStyles.maxWidth ?? '200px') return { position: 'sticky' as const, left: `${leftOffset}px`, zIndex: 9, backgroundColor, padding: `0 ${FOUNDATION_THEME.unit[16]}`, minWidth, maxWidth, width: 'auto', boxSizing: 'border-box' as const, overflow: 'hidden', ...(isLastFrozenColumn && { borderRight: frozenBorder ?? `1px solid ${FOUNDATION_THEME.colors.gray[200]}`, }), } }