diff --git a/src/components/Dropdown/DropdownItem.tsx b/src/components/Dropdown/DropdownItem.tsx index 0c4a89c41..6aa7085bd 100644 --- a/src/components/Dropdown/DropdownItem.tsx +++ b/src/components/Dropdown/DropdownItem.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from "react"; +import { ReactNode } from "react"; export interface DropdownItemProps { children: ReactNode; @@ -8,13 +8,13 @@ export interface DropdownItemProps { className?: string; } -const DropdownItem: React.FC = ({ +const DropdownItem = ({ children, onClick, isSelected = false, disabled = false, className = "", -}) => { +}: DropdownItemProps) => { const handleClick = () => { if (!disabled && onClick) { onClick(); diff --git a/src/components/animate/Animate.tsx b/src/components/animate/Animate.tsx index fa9d0cc99..a2fb21252 100644 --- a/src/components/animate/Animate.tsx +++ b/src/components/animate/Animate.tsx @@ -24,14 +24,14 @@ const MIN_DYNAMIC_DISTANCE = 100; // px - minimum distance from viewport edge fo const MAX_DYNAMIC_DISTANCE = 900; // px - maximum distance from viewport edge for animations const DISTANCE_SCALING_FACTOR = 80; // Factor for logarithmic scaling of dynamic distance -interface AnimateProps extends Omit, "id"> { +interface AnimateProps extends Omit, "id"> { children: ReactNode; id: string; parentRef?: MutableRefObject; - tableRow?: TableRow; + tableRow?: TableRow; } -export const Animate = ({ children, id, parentRef, tableRow, ...props }: AnimateProps) => { - const { allowAnimations, isResizing, isScrolling, rowHeight } = useTableContext(); +export const Animate = ({ children, id, parentRef, tableRow, ...props }: AnimateProps) => { + const { allowAnimations, isResizing, isScrolling, rowHeight } = useTableContext(); const elementRef = useRef(null); const fromBoundsRef = useRef(null); const previousScrollingState = usePrevious(isScrolling); diff --git a/src/components/animate/animation-utils.ts b/src/components/animate/animation-utils.ts index 2bfad1556..3bb97a3f2 100644 --- a/src/components/animate/animation-utils.ts +++ b/src/components/animate/animation-utils.ts @@ -1,4 +1,3 @@ -import CellValue from "../../types/CellValue"; import { AnimationConfig, FlipAnimationOptions, CustomAnimationOptions } from "./types"; /** @@ -102,8 +101,7 @@ const cleanupAnimation = (element: HTMLElement) => { const animateToFinalPosition = ( element: HTMLElement, config: AnimationConfig, - options: FlipAnimationOptions = {}, - id?: CellValue + options: FlipAnimationOptions = {} ): Promise => { return new Promise((resolve) => { // Force a reflow to ensure the initial transform is applied diff --git a/src/components/date-picker/DatePicker.tsx b/src/components/date-picker/DatePicker.tsx index f5dbe020b..2002b6604 100644 --- a/src/components/date-picker/DatePicker.tsx +++ b/src/components/date-picker/DatePicker.tsx @@ -1,4 +1,4 @@ -import React, { useState, ReactNode } from "react"; +import { useState, ReactNode } from "react"; import { useTableContext } from "../../context/TableContext"; interface DatePickerProps { diff --git a/src/components/filters/BooleanFilter.tsx b/src/components/filters/BooleanFilter.tsx index 000826c75..e7b957af6 100644 --- a/src/components/filters/BooleanFilter.tsx +++ b/src/components/filters/BooleanFilter.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import HeaderObject from "../../types/HeaderObject"; import { FilterCondition, @@ -13,19 +13,19 @@ import FilterSelect from "./shared/FilterSelect"; import FilterSection from "./shared/FilterSection"; import FilterActions from "./shared/FilterActions"; -interface BooleanFilterProps { - header: HeaderObject; - currentFilter?: FilterCondition; - onApplyFilter: (filter: FilterCondition) => void; +interface BooleanFilterProps { + header: HeaderObject; + currentFilter?: FilterCondition; + onApplyFilter: (filter: FilterCondition) => void; onClearFilter: () => void; } -const BooleanFilter: React.FC = ({ +const BooleanFilter = ({ header, currentFilter, onApplyFilter, onClearFilter, -}) => { +}: BooleanFilterProps) => { const [selectedOperator, setSelectedOperator] = useState( (currentFilter?.operator as BooleanFilterOperator) || "equals" ); @@ -47,7 +47,8 @@ const BooleanFilter: React.FC = ({ }, [currentFilter]); const handleApplyFilter = () => { - const filter: FilterCondition = { + if (!header.accessor) return; + const filter: FilterCondition = { accessor: header.accessor, operator: selectedOperator, }; diff --git a/src/components/filters/DateFilter.tsx b/src/components/filters/DateFilter.tsx index b5ae3b76b..e59bd6393 100644 --- a/src/components/filters/DateFilter.tsx +++ b/src/components/filters/DateFilter.tsx @@ -15,19 +15,19 @@ import FilterActions from "./shared/FilterActions"; import DatePicker from "../date-picker/DatePicker"; import Dropdown from "../dropdown/Dropdown"; -interface DateFilterProps { - header: HeaderObject; - currentFilter?: FilterCondition; - onApplyFilter: (filter: FilterCondition) => void; +interface DateFilterProps { + header: HeaderObject; + currentFilter?: FilterCondition; + onApplyFilter: (filter: FilterCondition) => void; onClearFilter: () => void; } -const DateFilter: React.FC = ({ +const DateFilter = ({ header, currentFilter, onApplyFilter, onClearFilter, -}) => { +}: DateFilterProps) => { const [selectedOperator, setSelectedOperator] = useState( (currentFilter?.operator as DateFilterOperator) || "equals" ); @@ -59,7 +59,7 @@ const DateFilter: React.FC = ({ }, [currentFilter]); const handleApplyFilter = () => { - const filter: FilterCondition = { + const filter: FilterCondition = { accessor: header.accessor, operator: selectedOperator, }; diff --git a/src/components/filters/EnumFilter.tsx b/src/components/filters/EnumFilter.tsx index a4037ddbb..44f56b440 100644 --- a/src/components/filters/EnumFilter.tsx +++ b/src/components/filters/EnumFilter.tsx @@ -6,19 +6,19 @@ import FilterSection from "./shared/FilterSection"; import FilterActions from "./shared/FilterActions"; import Checkbox from "../Checkbox"; -interface EnumFilterProps { - header: HeaderObject; - currentFilter?: FilterCondition; - onApplyFilter: (filter: FilterCondition) => void; +interface EnumFilterProps { + header: HeaderObject; + currentFilter?: FilterCondition; + onApplyFilter: (filter: FilterCondition) => void; onClearFilter: () => void; } -const EnumFilter: React.FC = ({ +const EnumFilter = ({ header, currentFilter, onApplyFilter, onClearFilter, -}) => { +}: EnumFilterProps) => { const enumOptions = useMemo(() => header.enumOptions || [], [header.enumOptions]); // Work with string values instead of full EnumOption objects @@ -64,7 +64,7 @@ const EnumFilter: React.FC = ({ return; } - const filter: FilterCondition = { + const filter: FilterCondition = { accessor: header.accessor, operator: selectedOperator, values: selectedValues, diff --git a/src/components/filters/FilterBar.tsx b/src/components/filters/FilterBar.tsx index d0d79279a..37829a10f 100644 --- a/src/components/filters/FilterBar.tsx +++ b/src/components/filters/FilterBar.tsx @@ -1,9 +1,11 @@ -import React from "react"; import { useTableContext } from "../../context/TableContext"; import { FILTER_OPERATOR_LABELS, FilterCondition } from "../../types/FilterTypes"; import { HeaderObject } from "../.."; -const getFilterDisplayText = (filter: FilterCondition, currentHeaders: HeaderObject[]) => { +const getFilterDisplayText = ( + filter: FilterCondition, + currentHeaders: HeaderObject[] +) => { const header = currentHeaders.find((h) => h.accessor === filter.accessor); const columnName = header?.label || filter.accessor; const operatorLabel = FILTER_OPERATOR_LABELS[filter.operator]; @@ -25,10 +27,10 @@ const getFilterDisplayText = (filter: FilterCondition, currentHeaders: HeaderObj } if (filter.operator === "isEmpty" || filter.operator === "isNotEmpty") { - return `${columnName}: ${operatorLabel}`; + return `${String(columnName)}: ${operatorLabel}`; } - return `${columnName}: ${operatorLabel} ${valueText}`; + return `${String(columnName)}: ${operatorLabel} ${valueText}`; }; const FilterBar = () => { diff --git a/src/components/filters/FilterDropdown.tsx b/src/components/filters/FilterDropdown.tsx index dcaa28256..3c5d125ec 100644 --- a/src/components/filters/FilterDropdown.tsx +++ b/src/components/filters/FilterDropdown.tsx @@ -1,4 +1,3 @@ -import React from "react"; import HeaderObject from "../../types/HeaderObject"; import { FilterCondition } from "../../types/FilterTypes"; import StringFilter from "./StringFilter"; @@ -7,19 +6,19 @@ import BooleanFilter from "./BooleanFilter"; import DateFilter from "./DateFilter"; import EnumFilter from "./EnumFilter"; -interface FilterDropdownProps { - header: HeaderObject; - currentFilter?: FilterCondition; - onApplyFilter: (filter: FilterCondition) => void; +interface FilterDropdownProps { + header: HeaderObject; + currentFilter?: FilterCondition; + onApplyFilter: (filter: FilterCondition) => void; onClearFilter: () => void; } -const FilterDropdown: React.FC = ({ +const FilterDropdown = ({ header, currentFilter, onApplyFilter, onClearFilter, -}) => { +}: FilterDropdownProps) => { const renderFilterComponent = () => { switch (header.type) { case "number": diff --git a/src/components/filters/NumberFilter.tsx b/src/components/filters/NumberFilter.tsx index c5c45c19a..40a5296a6 100644 --- a/src/components/filters/NumberFilter.tsx +++ b/src/components/filters/NumberFilter.tsx @@ -14,19 +14,19 @@ import FilterInput from "./shared/FilterInput"; import FilterSection from "./shared/FilterSection"; import FilterActions from "./shared/FilterActions"; -interface NumberFilterProps { - header: HeaderObject; - currentFilter?: FilterCondition; - onApplyFilter: (filter: FilterCondition) => void; +interface NumberFilterProps { + header: HeaderObject; + currentFilter?: FilterCondition; + onApplyFilter: (filter: FilterCondition) => void; onClearFilter: () => void; } -const NumberFilter: React.FC = ({ +const NumberFilter = ({ header, currentFilter, onApplyFilter, onClearFilter, -}) => { +}: NumberFilterProps) => { const [selectedOperator, setSelectedOperator] = useState( (currentFilter?.operator as NumberFilterOperator) || "equals" ); @@ -54,7 +54,7 @@ const NumberFilter: React.FC = ({ }, [currentFilter]); const handleApplyFilter = () => { - const filter: FilterCondition = { + const filter: FilterCondition = { accessor: header.accessor, operator: selectedOperator, }; diff --git a/src/components/filters/StringFilter.tsx b/src/components/filters/StringFilter.tsx index 30801bbf9..9c010ae69 100644 --- a/src/components/filters/StringFilter.tsx +++ b/src/components/filters/StringFilter.tsx @@ -13,19 +13,19 @@ import FilterInput from "./shared/FilterInput"; import FilterSection from "./shared/FilterSection"; import FilterActions from "./shared/FilterActions"; -interface StringFilterProps { - header: HeaderObject; - currentFilter?: FilterCondition; - onApplyFilter: (filter: FilterCondition) => void; +interface StringFilterProps { + header: HeaderObject; + currentFilter?: FilterCondition; + onApplyFilter: (filter: FilterCondition) => void; onClearFilter: () => void; } -const StringFilter: React.FC = ({ +const StringFilter = ({ header, currentFilter, onApplyFilter, onClearFilter, -}) => { +}: StringFilterProps) => { const [selectedOperator, setSelectedOperator] = useState( (currentFilter?.operator as StringFilterOperator) || "contains" ); @@ -45,7 +45,7 @@ const StringFilter: React.FC = ({ }, [currentFilter]); const handleApplyFilter = () => { - const filter: FilterCondition = { + const filter: FilterCondition = { accessor: header.accessor, operator: selectedOperator, ...(requiresSingleValue(selectedOperator) && { value: filterValue }), diff --git a/src/components/filters/shared/CustomSelect.tsx b/src/components/filters/shared/CustomSelect.tsx index 2bb328758..3712461aa 100644 --- a/src/components/filters/shared/CustomSelect.tsx +++ b/src/components/filters/shared/CustomSelect.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect } from "react"; import SelectIcon from "../../../icons/SelectIcon"; import Dropdown from "../../dropdown/Dropdown"; diff --git a/src/components/filters/shared/FilterSection.tsx b/src/components/filters/shared/FilterSection.tsx index 0c172a102..d2c1c4318 100644 --- a/src/components/filters/shared/FilterSection.tsx +++ b/src/components/filters/shared/FilterSection.tsx @@ -1,11 +1,11 @@ -import React, { ReactNode } from "react"; +import { ReactNode } from "react"; interface FilterSectionProps { children: ReactNode; className?: string; } -const FilterSection: React.FC = ({ children, className = "" }) => { +const FilterSection = ({ children, className = "" }: FilterSectionProps) => { return
{children}
; }; diff --git a/src/components/filters/shared/FilterSelect.tsx b/src/components/filters/shared/FilterSelect.tsx index c99edd8e9..9b7665f89 100644 --- a/src/components/filters/shared/FilterSelect.tsx +++ b/src/components/filters/shared/FilterSelect.tsx @@ -1,21 +1,20 @@ -import React from "react"; import CustomSelect, { CustomSelectOption } from "./CustomSelect"; interface FilterSelectProps { - value: string; + className?: string; onChange: (value: string) => void; options: CustomSelectOption[]; - className?: string; placeholder?: string; + value: string; } -const FilterSelect: React.FC = ({ - value, +const FilterSelect = ({ + className = "", onChange, options, - className = "", placeholder, -}) => { + value, +}: FilterSelectProps) => { return ( { columnIndexStart?: number; columnIndices: ColumnIndices; - headers: HeaderObject[]; + headers: HeaderObject[]; pinned?: Pinned; rowIndex: number; rowIndices: RowIndices; - tableRow: TableRowType; + tableRow: TableRowType; } -const RenderCells = ({ +const RenderCells = ({ columnIndexStart, columnIndices, headers, @@ -27,7 +27,7 @@ const RenderCells = ({ rowIndex, rowIndices, tableRow, -}: RenderCellsProps) => { +}: RenderCellsProps) => { const { rowIdAccessor } = useTableContext(); const filteredHeaders = headers.filter((header) => displayCell({ header, pinned })); @@ -35,7 +35,7 @@ const RenderCells = ({ <> {filteredHeaders.map((header, index) => { const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); - const cellKey = getCellId({ accessor: header.accessor, rowId }); + const cellKey = getCellId({ headerId: header.id, rowId }); return ( ({ columnIndices, header, headers, @@ -66,17 +66,14 @@ const RecursiveRenderCells = ({ tableRow, }: { columnIndices: ColumnIndices; - header: HeaderObject; - headers: HeaderObject[]; + header: HeaderObject; + headers: HeaderObject[]; nestedIndex: number; pinned?: Pinned; rowIndex: number; rowIndices: RowIndices; - tableRow: TableRowType; + tableRow: TableRowType; }) => { - // Get the column index for this header from our pre-calculated mapping - const colIndex = columnIndices[header.accessor]; - // Get selection state for this cell const { getBorderClass, isSelected, isInitialFocusedCell, rowIdAccessor } = useTableContext(); @@ -91,7 +88,7 @@ const RecursiveRenderCells = ({ return ( {filteredChildren.map((child) => { - const childCellKey = getCellId({ accessor: child.accessor, rowId }); + const childCellKey = getCellId({ headerId: child.id, rowId }); return ( { allowAnimations?: boolean; // Flag for allowing animations cellUpdateFlash?: boolean; // Flag for flash animation after cell update className?: string; // Class name for the table @@ -56,7 +57,7 @@ interface SimpleTableProps { columnEditorText?: string; // Text for the column editor columnReordering?: boolean; // Flag for column reordering columnResizing?: boolean; // Flag for column resizing - defaultHeaders: HeaderObject[]; // Default headers + defaultHeaders: STColumn[]; // Default headers with type safety editColumns?: boolean; // Flag for column editing editColumnsInitOpen?: boolean; // Flag for opening the column editor when the table is loaded enableRowSelection?: boolean; // Flag for enabling row selection with checkboxes @@ -67,34 +68,34 @@ interface SimpleTableProps { height?: string; // Height of the table hideFooter?: boolean; // Flag for hiding the footer nextIcon?: ReactNode; // Next icon - onCellEdit?: (props: CellChangeProps) => void; - onCellClick?: (props: CellClickProps) => void; - onColumnOrderChange?: (newHeaders: HeaderObject[]) => void; - onFilterChange?: (filters: TableFilterState) => void; // Callback when filter is applied + onCellEdit?: (props: CellChangeProps) => void; + onCellClick?: (props: CellClickProps) => void; + onColumnOrderChange?: (newHeaders: HeaderObject[]) => void; + onFilterChange?: (filters: TableFilterState) => void; // Callback when filter is applied onGridReady?: () => void; // Custom handler for when the grid is ready onLoadMore?: () => void; // Callback when user scrolls near bottom to load more data onNextPage?: OnNextPage; // Custom handler for next page - onRowSelectionChange?: (props: RowSelectionChangeProps) => void; // Callback when row selection changes - onSortChange?: (sort: SortColumn | null) => void; // Callback when sort is applied + onRowSelectionChange?: (props: RowSelectionChangeProps) => void; // Callback when row selection changes + onSortChange?: (sort: SortColumn | null) => void; // Callback when sort is applied prevIcon?: ReactNode; // Previous icon - rowGrouping?: Accessor[]; // Array of property names that define row grouping hierarchy + rowGrouping?: RowGrouping; // Array of property names that define row grouping hierarchy rowHeight?: number; // Height of each row - rowIdAccessor: Accessor; // Property name to use as row ID (defaults to index-based ID) - rows: Row[]; // Rows data + rowIdAccessor: Accessor; // Property name to use as row ID (defaults to index-based ID) + rows: T[]; // Rows data with type safety rowsPerPage?: number; // Rows per page selectableCells?: boolean; // Flag if can select cells selectableColumns?: boolean; // Flag for selectable column headers shouldPaginate?: boolean; // Flag for pagination sortDownIcon?: ReactNode; // Sort down icon sortUpIcon?: ReactNode; // Sort up icon - tableRef?: MutableRefObject; + tableRef?: MutableRefObject | null>; theme?: Theme; // Theme useOddColumnBackground?: boolean; // Flag for using column background useHoverRowBackground?: boolean; // Flag for using hover row background useOddEvenRowBackground?: boolean; // Flag for using odd/even row background } -const SimpleTable = (props: SimpleTableProps) => { +const SimpleTable = (props: SimpleTableProps) => { const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); @@ -103,7 +104,7 @@ const SimpleTable = (props: SimpleTableProps) => { return ; }; -const SimpleTableComp = ({ +const SimpleTableComp = ({ allowAnimations = false, cellUpdateFlash = false, className, @@ -147,15 +148,27 @@ const SimpleTableComp = ({ useHoverRowBackground = true, useOddEvenRowBackground = true, useOddColumnBackground = false, -}: SimpleTableProps) => { +}: SimpleTableProps) => { if (useOddColumnBackground) useOddEvenRowBackground = false; + // Recursively add ids to headers + const headersWithIds = useMemo(() => { + const addIds = (headers: STColumn[]): HeaderObject[] => { + return headers.map((header) => ({ + ...header, + id: generateColumnId(header), + children: header.children ? addIds(header.children) : undefined, + })); + }; + return addIds(defaultHeaders); + }, [defaultHeaders]); + // Force update function - needed early for header updates const [, forceUpdate] = useReducer((x) => x + 1, 0); // Refs - const draggedHeaderRef = useRef(null); - const hoveredHeaderRef = useRef(null); + const draggedHeaderRef = useRef | null>(null); + const hoveredHeaderRef = useRef | null>(null); const mainBodyRef = useRef(null); const pinnedLeftRef = useRef(null); @@ -165,14 +178,14 @@ const SimpleTableComp = ({ // Local state const [currentPage, setCurrentPage] = useState(1); - const [headers, setHeaders] = useState(defaultHeaders); + const [headers, setHeaders] = useState(headersWithIds); const [isResizing, setIsResizing] = useState(false); const [isScrolling, setIsScrolling] = useState(false); // Update headers when defaultHeaders prop changes useEffect(() => { - setHeaders(defaultHeaders); - }, [defaultHeaders]); + setHeaders(headersWithIds); + }, [headersWithIds]); // Row selection hook const { @@ -197,7 +210,7 @@ const SimpleTableComp = ({ const effectiveHeaders = useMemo(() => { if (!enableRowSelection || headers?.[0]?.isSelectionColumn) return headers; - const selectionHeader = createSelectionHeader(); + const selectionHeader = createSelectionHeader(); return [selectionHeader, ...headers]; }, [enableRowSelection, headers]); @@ -220,7 +233,7 @@ const SimpleTableComp = ({ const aggregatedRows = useAggregatedRows({ rows, - headers, + headers: headersWithIds, rowGrouping, }); @@ -240,7 +253,7 @@ const SimpleTableComp = ({ // Use custom hook for sorting (now operates on filtered rows) const { sort, sortedRows, updateSort, computeSortedRowsPreview } = useSortableData({ - headers, + headers: headersWithIds, tableRows: filteredRows, externalSortHandling, onSortChange, @@ -300,7 +313,7 @@ const SimpleTableComp = ({ // Memoize handlers const onSort = useCallback( - (accessor: Accessor) => { + (accessor: Accessor) => { // STAGE 1: Prepare animation by adding entering rows before applying sort prepareForSortChange(accessor); @@ -312,7 +325,7 @@ const SimpleTableComp = ({ [prepareForSortChange, updateSort] ); - const onTableHeaderDragEnd = useCallback((newHeaders: HeaderObject[]) => { + const onTableHeaderDragEnd = useCallback((newHeaders: HeaderObject[]) => { setHeaders(newHeaders); }, []); @@ -336,7 +349,7 @@ const SimpleTableComp = ({ // Custom filter handler that respects external filter handling flag const handleApplyFilter = useCallback( - (filter: FilterCondition) => { + (filter: FilterCondition) => { // STAGE 1: Prepare animation by adding entering rows before applying filter prepareForFilterChange(filter); diff --git a/src/components/simple-table/TableBody.tsx b/src/components/simple-table/TableBody.tsx index b23d07527..6e4db9c77 100644 --- a/src/components/simple-table/TableBody.tsx +++ b/src/components/simple-table/TableBody.tsx @@ -8,8 +8,9 @@ import { calculateColumnIndices } from "../../utils/columnIndicesUtils"; import RowIndices from "../../types/RowIndices"; import TableBodyProps from "../../types/TableBodyProps"; import { getRowId } from "../../utils/rowUtils"; +import TableRow from "../../types/TableRow"; -const TableBody = ({ +const TableBody = ({ mainTemplateColumns, pinnedLeftColumns, pinnedLeftTemplateColumns, @@ -20,7 +21,7 @@ const TableBody = ({ rowsToRender, setScrollTop, tableRows, -}: TableBodyProps) => { +}: TableBodyProps) => { // Get stable props from context const { headerContainerRef, @@ -34,7 +35,7 @@ const TableBody = ({ setIsScrolling, shouldPaginate, tableBodyContainerRef, - } = useTableContext(); + } = useTableContext(); // Local state const [hoveredIndex, setHoveredIndex] = useState(null); @@ -86,7 +87,7 @@ const TableBody = ({ const indices: RowIndices = {}; // Map each row's ID to its index in the visible rows array - rowsToRender.forEach((rowsToRender, index) => { + rowsToRender.forEach((rowsToRender: TableRow, index: number) => { const rowId = String(getRowId({ row: rowsToRender.row, rowIdAccessor })); indices[rowId] = index; }); diff --git a/src/components/simple-table/TableCell.tsx b/src/components/simple-table/TableCell.tsx index cb571eefe..b3670bbb6 100644 --- a/src/components/simple-table/TableCell.tsx +++ b/src/components/simple-table/TableCell.tsx @@ -1,6 +1,5 @@ import { useEffect, useState, KeyboardEvent, useCallback, useRef } from "react"; import EditableCell from "./editable-cells/EditableCell"; -import CellValue from "../../types/CellValue"; import { useThrottle } from "../../utils/performanceUtils"; import useDragHandler from "../../hooks/useDragHandler"; import { DRAG_THROTTLE_LIMIT } from "../../consts/general-consts"; @@ -13,7 +12,7 @@ import { getRowId, hasNestedRows } from "../../utils/rowUtils"; import Animate from "../animate/Animate"; import Checkbox from "../Checkbox"; -const displayContent = ({ content, header }: { content: CellValue; header: HeaderObject }) => { +const displayContent = ({ content, header }: { content: any; header: HeaderObject }) => { if (typeof content === "boolean") { return content ? "True" : "False"; } else if (Array.isArray(content)) { @@ -42,7 +41,7 @@ const displayContent = ({ content, header }: { content: CellValue; header: Heade return content; }; -const TableCell = ({ +const TableCell = ({ borderClass, colIndex, header, @@ -51,7 +50,7 @@ const TableCell = ({ nestedIndex, rowIndex, tableRow, -}: TableCellProps) => { +}: TableCellProps) => { // Get shared props from context const { cellRegistry, @@ -77,12 +76,12 @@ const TableCell = ({ theme, unexpandedRows, useOddColumnBackground, - } = useTableContext(); + } = useTableContext(); const { depth, row } = tableRow; // Local state - const [localContent, setLocalContent] = useState(row[header.accessor] as CellValue); + const [localContent, setLocalContent] = useState(row[header.accessor as keyof T]); const [isEditing, setIsEditing] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const updateTimeout = useRef(null); @@ -90,7 +89,9 @@ const TableCell = ({ // Get row ID and check if row has children const rowId = getRowId({ row, rowIdAccessor }); const currentGroupingKey = rowGrouping && rowGrouping[depth]; - const cellHasChildren = currentGroupingKey ? hasNestedRows(row, currentGroupingKey) : false; + const cellHasChildren = currentGroupingKey + ? hasNestedRows(row, currentGroupingKey as keyof T) + : false; const isRowExpanded = !unexpandedRows.has(String(rowId)); // Check if this cell is currently flashing from copy operation @@ -109,10 +110,7 @@ const TableCell = ({ const throttle = useThrottle(); // Cell focus id (used for keyboard navigation) - const cellId = getCellId({ accessor: header.accessor, rowId }); - - // Generate a unique key that includes the content value to force re-render when it changes - const cellKey = getCellKey({ rowId, accessor: header.accessor }); + const cellId = getCellId({ headerId: header.id, rowId }); // Check if this is the selection column const isSelectionColumn = header.isSelectionColumn && enableRowSelection; @@ -120,9 +118,9 @@ const TableCell = ({ // Register this cell with the cell registry for direct updates useEffect(() => { if (cellRegistry) { - const key = `${rowId}-${header.accessor}`; + const key = `${rowId}-${header.id}`; cellRegistry.set(key, { - updateContent: (newValue: CellValue) => { + updateContent: (newValue: any) => { // If the value is different, trigger the update animation if (localContent !== newValue) { setLocalContent(newValue); @@ -153,7 +151,7 @@ const TableCell = ({ } }; } - }, [cellRegistry, cellUpdateFlash, rowId, header.accessor, localContent]); + }, [cellRegistry, cellUpdateFlash, rowId, header.id, localContent]); // Add another effect to ensure animation gets removed useEffect(() => { @@ -168,7 +166,7 @@ const TableCell = ({ // Update local content when row data changes useEffect(() => { - setLocalContent(row[header.accessor] as CellValue); + setLocalContent(row[header.accessor as keyof T]); }, [row, header.accessor]); // Derived state @@ -199,9 +197,9 @@ const TableCell = ({ }`; const updateContent = useCallback( - (newValue: CellValue) => { + (newValue: any) => { setLocalContent(newValue); - row[header.accessor] = newValue; + row[header.accessor as keyof T] = newValue; onCellEdit?.({ accessor: header.accessor, @@ -227,6 +225,14 @@ const TableCell = ({ }); }, [rowId, setUnexpandedRows]); + if (!header.accessor) { + return null; + } + + // Generate a unique key that includes the content value to force re-render when it changes + const accessor = header.accessor; + const cellKey = getCellKey({ rowId, accessor }); + // Handle keyboard events when cell is focused const handleKeyDown = (e: KeyboardEvent) => { // If we're editing or this is a selection column, don't handle table navigation keys @@ -264,7 +270,7 @@ const TableCell = ({ // Handle cell click callback const handleCellClick = () => { - if (onCellClick && !isSelectionColumn) { + if (onCellClick && !isSelectionColumn && header.accessor) { onCellClick({ accessor: header.accessor, colIndex, @@ -281,7 +287,7 @@ const TableCell = ({ return (
e.stopPropagation()} // Prevent cell selection when clicking in edit mode onKeyDown={(e) => e.stopPropagation()} // Prevent table navigation when editing > diff --git a/src/components/simple-table/TableContent.tsx b/src/components/simple-table/TableContent.tsx index da0f19f10..e2af5fb13 100644 --- a/src/components/simple-table/TableContent.tsx +++ b/src/components/simple-table/TableContent.tsx @@ -9,25 +9,25 @@ import TableBodyProps from "../../types/TableBodyProps"; import TableRow from "../../types/TableRow"; // Define props for the frequently changing values not in context -interface TableContentLocalProps { +interface TableContentLocalProps { pinnedLeftWidth: number; pinnedRightWidth: number; setScrollTop: Dispatch>; - sort: SortColumn | null; - tableRows: TableRow[]; - rowsToRender: TableRow[]; + sort: SortColumn | null; + tableRows: TableRow[]; + rowsToRender: TableRow[]; } -const TableContent = ({ +const TableContent = ({ pinnedLeftWidth, pinnedRightWidth, setScrollTop, sort, tableRows, rowsToRender, -}: TableContentLocalProps) => { +}: TableContentLocalProps) => { // Get stable props from context - const { columnResizing, editColumns, headers } = useTableContext(); + const { columnResizing, editColumns, headers } = useTableContext(); // Refs const centerHeaderRef = useRef(null); @@ -48,7 +48,7 @@ const TableContent = ({ return createGridTemplateColumns({ headers: pinnedRightColumns }); }, [pinnedRightColumns]); - const tableHeaderProps: TableHeaderProps = { + const tableHeaderProps: TableHeaderProps = { centerHeaderRef, headers, mainTemplateColumns, @@ -61,7 +61,7 @@ const TableContent = ({ pinnedRightWidth, }; - const tableBodyProps: TableBodyProps = { + const tableBodyProps: TableBodyProps = { tableRows, mainTemplateColumns, pinnedLeftColumns, diff --git a/src/components/simple-table/TableHeader.tsx b/src/components/simple-table/TableHeader.tsx index aeb153828..9d78a0db9 100644 --- a/src/components/simple-table/TableHeader.tsx +++ b/src/components/simple-table/TableHeader.tsx @@ -7,11 +7,11 @@ import { useTableContext } from "../../context/TableContext"; import { calculateColumnIndices } from "../../utils/columnIndicesUtils"; import { canDisplaySection } from "../../utils/generalUtils"; -const getHeaderDepth = (header: HeaderObject): number => { +const getHeaderDepth = (header: HeaderObject): number => { return header.children?.length ? 1 + Math.max(...header.children.map(getHeaderDepth)) : 1; }; -const TableHeader = ({ +const TableHeader = ({ centerHeaderRef, headers, mainTemplateColumns, @@ -22,7 +22,7 @@ const TableHeader = ({ sort, pinnedLeftWidth, pinnedRightWidth, -}: TableHeaderProps) => { +}: TableHeaderProps) => { const { headerContainerRef, pinnedLeftRef, pinnedRightRef } = useTableContext(); // Calculate column indices for all headers to ensure consistent colIndex values diff --git a/src/components/simple-table/TableHeaderCell.tsx b/src/components/simple-table/TableHeaderCell.tsx index d62b6af16..015da2445 100644 --- a/src/components/simple-table/TableHeaderCell.tsx +++ b/src/components/simple-table/TableHeaderCell.tsx @@ -1,7 +1,7 @@ import { DragEvent, useEffect, MouseEvent, TouchEvent, useState } from "react"; import useDragHandler from "../../hooks/useDragHandler"; import { useThrottle } from "../../utils/performanceUtils"; -import HeaderObject from "../../types/HeaderObject"; +import HeaderObject, { Accessor, AggregatedRow } from "../../types/HeaderObject"; import SortColumn from "../../types/SortColumn"; import { DRAG_THROTTLE_LIMIT } from "../../consts/general-consts"; import { getCellId } from "../../utils/cellUtils"; @@ -16,18 +16,18 @@ import { FilterCondition } from "../../types/FilterTypes"; import Animate from "../animate/Animate"; import Checkbox from "../Checkbox"; -interface HeaderCellProps { +interface HeaderCellProps { colIndex: number; gridColumnEnd: number; gridColumnStart: number; gridRowEnd: number; gridRowStart: number; - header: HeaderObject; + header: HeaderObject>; reverse?: boolean; - sort: SortColumn | null; + sort: SortColumn | null; } -const TableHeaderCell = ({ +const TableHeaderCell = ({ colIndex, gridColumnEnd, gridColumnStart, @@ -36,7 +36,7 @@ const TableHeaderCell = ({ header, reverse, sort, -}: HeaderCellProps) => { +}: HeaderCellProps) => { // Local state for filter dropdown const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); @@ -66,12 +66,12 @@ const TableHeaderCell = ({ setSelectedColumns, sortDownIcon, sortUpIcon, - } = useTableContext(); + } = useTableContext(); // Derived state const clickable = Boolean(header?.isSortable); const filterable = Boolean(header?.filterable); - const currentFilter = filters[header.accessor]; + const currentFilter = filters[header.id]; const className = `st-header-cell ${ header.accessor === hoveredHeaderRef.current?.accessor ? "st-hovered" : "" @@ -91,7 +91,7 @@ const TableHeaderCell = ({ const throttle = useThrottle(); // Handlers - const handleDragStartWrapper = (header: HeaderObject) => { + const handleDragStartWrapper = (header: HeaderObject>) => { handleDragStart(header); }; const handleDragEndWrapper = (event: DragEvent) => { @@ -105,13 +105,13 @@ const TableHeaderCell = ({ setIsFilterDropdownOpen(!isFilterDropdownOpen); }; - const handleApplyFilterWrapper = (filter: FilterCondition) => { + const handleApplyFilterWrapper = (filter: FilterCondition) => { handleApplyFilter(filter); setIsFilterDropdownOpen(false); }; const handleClearFilterWrapper = () => { - handleClearFilter(header.accessor); + handleClearFilter(header.id); setIsFilterDropdownOpen(false); }; @@ -119,9 +119,11 @@ const TableHeaderCell = ({ const handleColumnHeaderClick = ({ event, header, + accessor, }: { event: MouseEvent; - header: HeaderObject; + header: HeaderObject; + accessor: Accessor; }) => { // If this is the selection column, don't handle column selection if (header.isSelectionColumn) { @@ -178,7 +180,7 @@ const TableHeaderCell = ({ } if (!header.isSortable) return; - onSort(header.accessor); + onSort(accessor); }; // Drag handler const onDragStart = (event: DragEvent) => { @@ -214,7 +216,7 @@ const TableHeaderCell = ({ onMouseDown={(event: MouseEvent) => { // Get the start width from the DOM element directly if ref is not available const startWidth = document.getElementById( - getCellId({ accessor: header.accessor, rowId: "header" }) + getCellId({ headerId: header.id, rowId: "header" }) )?.offsetWidth; throttle({ @@ -228,14 +230,14 @@ const TableHeaderCell = ({ setHeaders, setIsResizing, startWidth, - } as HandleResizeStartProps, + } as HandleResizeStartProps, limit: 10, }); }} onTouchStart={(event: TouchEvent) => { // Get the start width from the DOM element directly if ref is not available const startWidth = document.getElementById( - getCellId({ accessor: header.accessor, rowId: "header" }) + getCellId({ headerId: header.id, rowId: "header" }) )?.offsetWidth; throttle({ @@ -249,7 +251,7 @@ const TableHeaderCell = ({ setHeaders, setIsResizing, startWidth, - } as HandleResizeStartProps, + } as HandleResizeStartProps, limit: 10, }); }} @@ -261,7 +263,9 @@ const TableHeaderCell = ({ const SortIcon = sort && sort.key.accessor === header.accessor && (
handleColumnHeaderClick({ event, header })} + onClick={(event) => + header.accessor && handleColumnHeaderClick({ event, header, accessor: header.accessor }) + } > {sort.direction === "ascending" && sortUpIcon && sortUpIcon} {sort.direction === "descending" && sortDownIcon && sortDownIcon} @@ -305,7 +309,7 @@ const TableHeaderCell = ({ return ( { if (!isSelectionColumn) { throttle({ @@ -332,7 +336,8 @@ const TableHeaderCell = ({ draggable={columnReordering && !header.disableReorder && !isSelectionColumn} onClick={(event) => { if (!isSelectionColumn) { - handleColumnHeaderClick({ event, header }); + header.accessor && + handleColumnHeaderClick({ event, header, accessor: header.accessor }); } }} onDragEnd={!isSelectionColumn ? handleDragEndWrapper : undefined} diff --git a/src/components/simple-table/TableHeaderSection.tsx b/src/components/simple-table/TableHeaderSection.tsx index c44a95f1c..d7778331d 100644 --- a/src/components/simple-table/TableHeaderSection.tsx +++ b/src/components/simple-table/TableHeaderSection.tsx @@ -5,10 +5,11 @@ import TableHeaderSectionProps from "../../types/TableHeaderSectionProps"; import { HeaderObject } from "../.."; import { ScrollSyncPane } from "../scroll-sync/ScrollSyncPane"; import ConditionalWrapper from "../ConditionalWrapper"; +import { AggregatedRow } from "../../types/HeaderObject"; // Define a type for grid cell position -type GridCell = { - header: HeaderObject; +type GridCell = { + header: HeaderObject>; gridColumnStart: number; gridColumnEnd: number; gridRowStart: number; @@ -16,7 +17,7 @@ type GridCell = { colIndex: number; }; -const TableHeaderSection = ({ +const TableHeaderSection = ({ columnIndices, gridTemplateColumns, handleScroll, @@ -26,14 +27,18 @@ const TableHeaderSection = ({ sectionRef, sort, width, -}: TableHeaderSectionProps) => { +}: TableHeaderSectionProps) => { // First, flatten all headers into grid cells const gridCells = useMemo(() => { - const cells: GridCell[] = []; + const cells: GridCell[] = []; let columnCounter = 1; // Helper function to process a header and its children - const processHeader = (header: HeaderObject, depth: number, isFirst = false) => { + const processHeader = ( + header: HeaderObject>, + depth: number, + isFirst = false + ) => { if (!displayCell({ header, pinned })) return 0; // Only increment for non-first siblings @@ -58,7 +63,7 @@ const TableHeaderSection = ({ gridColumnEnd, gridRowStart, gridRowEnd, - colIndex: columnIndices[header.accessor], + colIndex: columnIndices[header.id], }); // Process children if any @@ -104,7 +109,7 @@ const TableHeaderSection = ({ }} > <> - {gridCells.map((cell) => ( + {gridCells.map((cell, index) => ( diff --git a/src/components/simple-table/TableHorizontalScrollbar.tsx b/src/components/simple-table/TableHorizontalScrollbar.tsx index 3d2a3cf6f..a161b8a6f 100644 --- a/src/components/simple-table/TableHorizontalScrollbar.tsx +++ b/src/components/simple-table/TableHorizontalScrollbar.tsx @@ -3,7 +3,7 @@ import { useTableContext } from "../../context/TableContext"; import { COLUMN_EDIT_WIDTH, PINNED_BORDER_WIDTH } from "../../consts/general-consts"; import { ScrollSyncPane } from "../scroll-sync/ScrollSyncPane"; -const TableHorizontalScrollbar = ({ +const TableHorizontalScrollbar = ({ mainBodyWidth, mainBodyRef, pinnedLeftWidth, @@ -17,7 +17,7 @@ const TableHorizontalScrollbar = ({ tableBodyContainerRef: RefObject; }) => { // Context - const { editColumns } = useTableContext(); + const { editColumns } = useTableContext(); // Local state const [isScrollable, setIsScrollable] = useState(false); diff --git a/src/components/simple-table/TableRow.tsx b/src/components/simple-table/TableRow.tsx index 5dee0415f..d93cb841a 100644 --- a/src/components/simple-table/TableRow.tsx +++ b/src/components/simple-table/TableRow.tsx @@ -9,21 +9,21 @@ import { useTableContext } from "../../context/TableContext"; import { getRowId } from "../../utils/rowUtils"; // Define just the props needed for RenderCells -interface TableRowProps { +interface TableRowProps { columnIndexStart?: number; columnIndices: ColumnIndices; gridTemplateColumns: string; - headers: HeaderObject[]; + headers: HeaderObject[]; hoveredIndex: number | null; index: number; pinned?: Pinned; rowHeight: number; rowIndices: RowIndices; setHoveredIndex: (index: number | null) => void; - tableRow: TableRowType; + tableRow: TableRowType; } -const TableRow = ({ +const TableRow = ({ columnIndices, columnIndexStart, gridTemplateColumns, @@ -35,8 +35,8 @@ const TableRow = ({ rowIndices, setHoveredIndex, tableRow, -}: TableRowProps) => { - const { useHoverRowBackground, rowIdAccessor, isAnimating, isRowSelected } = useTableContext(); +}: TableRowProps) => { + const { useHoverRowBackground, rowIdAccessor, isAnimating, isRowSelected } = useTableContext(); const { position } = tableRow; // Get row index from rowIndices using the row's ID diff --git a/src/components/simple-table/TableSection.tsx b/src/components/simple-table/TableSection.tsx index e4f5507a7..c5555c7c2 100644 --- a/src/components/simple-table/TableSection.tsx +++ b/src/components/simple-table/TableSection.tsx @@ -5,6 +5,7 @@ import { forwardRef, useRef, useImperativeHandle, + ForwardedRef, } from "react"; import TableRow from "./TableRow"; import TableRowType from "../../types/TableRow"; @@ -19,104 +20,104 @@ import { canDisplaySection } from "../../utils/generalUtils"; import { getRowId } from "../../utils/rowUtils"; import { useTableContext } from "../../context/TableContext"; -interface TableSectionProps { - columnIndexStart?: number; // This is to know how many columns there were before this section to see if the columns are odd or even +interface TableSectionProps { + columnIndexStart?: number; columnIndices: ColumnIndices; - headers: HeaderObject[]; + headers: HeaderObject[]; hoveredIndex: number | null; pinned?: Pinned; rowHeight: number; rowIndices: RowIndices; - rowsToRender: TableRowType[]; + rowsToRender: TableRowType[]; setHoveredIndex: (index: number | null) => void; templateColumns: string; totalHeight: number; width?: number; } -const TableSection = forwardRef( - ( - { - columnIndexStart, - columnIndices, - headers, - hoveredIndex, - pinned, - rowHeight, - rowIndices, - setHoveredIndex, - templateColumns, - totalHeight, - rowsToRender, - width, - }, - ref - ) => { - const className = pinned ? `st-body-pinned-${pinned}` : "st-body-main"; - const { rowIdAccessor } = useTableContext(); - const internalRef = useRef(null); +const TableSectionInner = ( + { + columnIndexStart, + columnIndices, + headers, + hoveredIndex, + pinned, + rowHeight, + rowIndices, + setHoveredIndex, + templateColumns, + totalHeight, + rowsToRender, + width, + }: TableSectionProps, + ref: ForwardedRef +) => { + const className = pinned ? `st-body-pinned-${pinned}` : "st-body-main"; + const { rowIdAccessor } = useTableContext(); + const internalRef = useRef(null); - useImperativeHandle(ref, () => internalRef.current!, []); + useImperativeHandle(ref, () => internalRef.current!, []); - const canDisplay = useMemo(() => canDisplaySection(headers, pinned), [headers, pinned]); - if (!canDisplay) return null; + const canDisplay = useMemo(() => canDisplaySection(headers, pinned), [headers, pinned]); + if (!canDisplay) return null; - return ( - ( - }> - {children} - - )} + return ( + ( + }> + {children} + + )} + > +
-
- {rowsToRender.map((tableRow, index) => { - const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); - return ( - - {index !== 0 && ( - - )} - { + const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); + return ( + + {index !== 0 && ( + - - ); - })} -
- - ); - } -); + )} + + + ); + })} +
+
+ ); +}; -TableSection.displayName = "TableSection"; +const TableSection = forwardRef(TableSectionInner) as ( + props: TableSectionProps & { ref?: ForwardedRef } +) => JSX.Element; export default TableSection; diff --git a/src/components/simple-table/editable-cells/DateDropdownEdit.tsx b/src/components/simple-table/editable-cells/DateDropdownEdit.tsx index 6c5db6047..3689bd56c 100644 --- a/src/components/simple-table/editable-cells/DateDropdownEdit.tsx +++ b/src/components/simple-table/editable-cells/DateDropdownEdit.tsx @@ -1,10 +1,9 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import Dropdown from "../../dropdown/Dropdown"; import DatePicker from "../../date-picker/DatePicker"; -import CellValue from "../../../types/CellValue"; // Convert the input value to a Date object -const parseDate = (value: CellValue): Date => { +const parseDate = (value: string): Date => { if (!value) return new Date(); const [year, month, day] = value.toString().split("-").map(Number); @@ -20,7 +19,7 @@ interface DateDropdownEditProps { onChange: (value: string) => void; open: boolean; setOpen: (open: boolean) => void; - value: CellValue; + value: string; } const DateDropdownEdit = ({ onBlur, onChange, open, setOpen, value }: DateDropdownEditProps) => { diff --git a/src/components/simple-table/editable-cells/EditableCell.tsx b/src/components/simple-table/editable-cells/EditableCell.tsx index 1fed5e594..45ccdf4e9 100644 --- a/src/components/simple-table/editable-cells/EditableCell.tsx +++ b/src/components/simple-table/editable-cells/EditableCell.tsx @@ -1,6 +1,5 @@ import BooleanDropdownEdit from "./BooleanDropdownEdit"; import StringEdit from "./StringEdit"; -import CellValue from "../../../types/CellValue"; import NumberEdit from "./NumberEdit"; import DateDropdownEdit from "./DateDropdownEdit"; import EnumDropdownEdit from "./EnumDropdownEdit"; @@ -9,10 +8,10 @@ import { ColumnType } from "../../../types/HeaderObject"; interface EditableCellProps { enumOptions?: EnumOption[]; - onChange: (newValue: CellValue) => void; + onChange: (newValue: any) => void; setIsEditing: (isEditing: boolean) => void; type?: ColumnType; - value: CellValue; + value: any; } const EditableCell = ({ diff --git a/src/components/simple-table/table-column-editor/ColumnEditorCheckbox.tsx b/src/components/simple-table/table-column-editor/ColumnEditorCheckbox.tsx index 716597893..f54c4a562 100644 --- a/src/components/simple-table/table-column-editor/ColumnEditorCheckbox.tsx +++ b/src/components/simple-table/table-column-editor/ColumnEditorCheckbox.tsx @@ -6,17 +6,17 @@ import { areAllChildrenHidden, findAndMarkParentsVisible } from "./columnEditorU import { updateParentHeaders } from "./columnEditorUtils"; // Recursive component to render headers with proper indentation -const ColumnEditorCheckbox = ({ +const ColumnEditorCheckbox = ({ allHeaders, depth = 0, doesAnyHeaderHaveChildren, header, isCheckedOverride, }: { - allHeaders: HeaderObject[]; + allHeaders: HeaderObject[]; depth?: number; doesAnyHeaderHaveChildren: boolean; - header: HeaderObject; + header: HeaderObject; isCheckedOverride?: boolean; }) => { const [isExpanded, setIsExpanded] = useState(true); @@ -38,19 +38,21 @@ const ColumnEditorCheckbox = ({ updateParentHeaders(allHeaders); } else { // If unchecked (visible), ensure all parent headers are also visible - findAndMarkParentsVisible(allHeaders, header.accessor); + findAndMarkParentsVisible(allHeaders, header.id); // If this is a parent header being made visible, and all its children are hidden, // make at least the first child visible for better UX if (hasChildren && header.children && header.children.length > 0) { - const allChildrenCurrentlyHidden = header.children.every((child) => child.hide === true); + const allChildrenCurrentlyHidden = header.children.every( + (child: HeaderObject) => child.hide === true + ); if (allChildrenCurrentlyHidden && header.children[0]) { // Make the first child visible header.children[0].hide = false; // Also make sure any parents of the child we just made visible are also visible - findAndMarkParentsVisible(allHeaders, header.children[0].accessor); + findAndMarkParentsVisible(allHeaders, header.children[0].id); } } } @@ -91,7 +93,7 @@ const ColumnEditorCheckbox = ({ depth={depth + 1} doesAnyHeaderHaveChildren={doesAnyHeaderHaveChildren} header={childHeader} - key={`${childHeader.accessor}-${index}`} + key={index} isCheckedOverride={isChecked ? true : undefined} /> ))} diff --git a/src/components/simple-table/table-column-editor/TableColumnEditor.tsx b/src/components/simple-table/table-column-editor/TableColumnEditor.tsx index 5dcaa13d9..46c55d1f6 100644 --- a/src/components/simple-table/table-column-editor/TableColumnEditor.tsx +++ b/src/components/simple-table/table-column-editor/TableColumnEditor.tsx @@ -3,21 +3,21 @@ import TableColumnEditorPopout from "./TableColumnEditorPopout"; import HeaderObject from "../../../types/HeaderObject"; import { COLUMN_EDIT_WIDTH } from "../../../consts/general-consts"; -type TableColumnEditorProps = { +type TableColumnEditorProps = { columnEditorText: string; editColumns: boolean; editColumnsInitOpen: boolean; - headers: HeaderObject[]; + headers: HeaderObject[]; position: "left" | "right"; }; -const TableColumnEditor = ({ +const TableColumnEditor = ({ columnEditorText, editColumns, editColumnsInitOpen, headers, position = "right", -}: TableColumnEditorProps) => { +}: TableColumnEditorProps) => { const [open, setOpen] = useState(editColumnsInitOpen); const handleClick = (open: boolean) => { diff --git a/src/components/simple-table/table-column-editor/TableColumnEditorPopout.tsx b/src/components/simple-table/table-column-editor/TableColumnEditorPopout.tsx index 0eed9e865..d10df2586 100644 --- a/src/components/simple-table/table-column-editor/TableColumnEditorPopout.tsx +++ b/src/components/simple-table/table-column-editor/TableColumnEditorPopout.tsx @@ -2,13 +2,17 @@ import { useMemo } from "react"; import HeaderObject from "../../../types/HeaderObject"; import ColumnEditorCheckbox from "./ColumnEditorCheckbox"; -type TableColumnEditorPopoutProps = { - headers: HeaderObject[]; +type TableColumnEditorPopoutProps = { + headers: HeaderObject[]; open: boolean; position: "left" | "right"; }; -const TableColumnEditorPopout = ({ headers, open, position }: TableColumnEditorPopoutProps) => { +const TableColumnEditorPopout = ({ + headers, + open, + position, +}: TableColumnEditorPopoutProps) => { const positionClass = position === "left" ? "left" : ""; const doesAnyHeaderHaveChildren = useMemo( () => headers.some((header) => header.children && header.children.length > 0), diff --git a/src/components/simple-table/table-column-editor/columnEditorUtils.ts b/src/components/simple-table/table-column-editor/columnEditorUtils.ts index 0e1d01988..968452230 100644 --- a/src/components/simple-table/table-column-editor/columnEditorUtils.ts +++ b/src/components/simple-table/table-column-editor/columnEditorUtils.ts @@ -1,26 +1,26 @@ -import HeaderObject, { Accessor } from "../../../types/HeaderObject"; +import HeaderObject from "../../../types/HeaderObject"; // Find all parents for a given header to ensure they're visible -export const findAndMarkParentsVisible = ( - headers: HeaderObject[], - childAccessor: Accessor, +export const findAndMarkParentsVisible = ( + headers: HeaderObject[], + childId: string, visited: Set = new Set() ) => { for (const header of headers) { // Skip if already processed this header - if (visited.has(header.accessor)) continue; - visited.add(header.accessor); + if (visited.has(header.id)) continue; + visited.add(header.id); // Check if this header has the child we're looking for if (header.children && header.children.length > 0) { // Check direct children - const hasDirectChild = header.children.some((child) => child.accessor === childAccessor); + const hasDirectChild = header.children.some((child) => child.id === childId); // Or recurse deeper to find in nested children let hasNestedChild = false; if (!hasDirectChild) { for (const child of header.children) { - findAndMarkParentsVisible([child], childAccessor, visited); + findAndMarkParentsVisible([child], childId, visited); // If this child is now visible after recursion, it means it's in the path if (child.hide === false) { hasNestedChild = true; @@ -37,12 +37,12 @@ export const findAndMarkParentsVisible = ( } }; -export const areAllChildrenHidden = (children: HeaderObject[]) => { +export const areAllChildrenHidden = (children: HeaderObject[]) => { return children.every((child) => child.hide); }; // Update parent headers based on children's state -export const updateParentHeaders = (headers: HeaderObject[]) => { +export const updateParentHeaders = (headers: HeaderObject[]) => { // Process each header headers.forEach((header) => { // If it has children, check if all children are hidden diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index 291b8fdf2..4027c2f54 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -10,18 +10,18 @@ import { import { TableFilterState, FilterCondition } from "../types/FilterTypes"; import TableRow from "../types/TableRow"; import Cell from "../types/Cell"; -import HeaderObject, { Accessor } from "../types/HeaderObject"; +import HeaderObject, { Accessor, AggregatedRow } from "../types/HeaderObject"; import OnSortProps from "../types/OnSortProps"; import Theme from "../types/Theme"; -import CellValue from "../types/CellValue"; import CellClickProps from "../types/CellClickProps"; +import RowGrouping from "../types/RowGrouping"; // Define the interface for cell registry entries export interface CellRegistryEntry { - updateContent: (newValue: CellValue) => void; + updateContent: (newValue: any) => void; } -interface TableContextType { +interface TableContextType { // Stable values that don't change frequently allowAnimations?: boolean; areAllRowsSelected?: () => boolean; @@ -30,24 +30,24 @@ interface TableContextType { clearSelection?: () => void; columnReordering: boolean; columnResizing: boolean; - draggedHeaderRef: MutableRefObject; + draggedHeaderRef: MutableRefObject | null>; editColumns?: boolean; enableRowSelection?: boolean; expandIcon?: ReactNode; - filters: TableFilterState; + filters: TableFilterState; forceUpdate: () => void; getBorderClass: (cell: Cell) => string; - handleApplyFilter: (filter: FilterCondition) => void; + handleApplyFilter: (filter: FilterCondition) => void; handleClearAllFilters: () => void; - handleClearFilter: (accessor: Accessor) => void; + handleClearFilter: (id: string) => void; handleMouseDown: (cell: Cell) => void; handleMouseOver: (cell: Cell) => void; handleRowSelect?: (rowId: string, isSelected: boolean) => void; handleSelectAll?: (isSelected: boolean) => void; handleToggleRow?: (rowId: string) => void; headerContainerRef: RefObject; - headers: HeaderObject[]; - hoveredHeaderRef: MutableRefObject; + headers: HeaderObject>[]; + hoveredHeaderRef: MutableRefObject> | null>; isAnimating: boolean; isCopyFlashing: (cell: Cell) => boolean; isInitialFocusedCell: (cell: Cell) => boolean; @@ -59,24 +59,24 @@ interface TableContextType { mainBodyRef: RefObject; nextIcon: ReactNode; onCellEdit?: (props: any) => void; - onCellClick?: (props: CellClickProps) => void; - onColumnOrderChange?: (newHeaders: HeaderObject[]) => void; + onCellClick?: (props: CellClickProps) => void; + onColumnOrderChange?: (newHeaders: HeaderObject[]) => void; onLoadMore?: () => void; - onSort: OnSortProps; - onTableHeaderDragEnd: (newHeaders: HeaderObject[]) => void; + onSort: OnSortProps; + onTableHeaderDragEnd: (newHeaders: HeaderObject[]) => void; pinnedLeftRef: RefObject; pinnedRightRef: RefObject; prevIcon: ReactNode; - rowGrouping?: Accessor[]; + rowGrouping?: RowGrouping; rowHeight: number; - rowIdAccessor: Accessor; + rowIdAccessor: Accessor; scrollbarWidth: number; selectColumns?: (columnIndices: number[], isShiftKey?: boolean) => void; selectableColumns: boolean; selectedRows?: Set; selectedRowCount?: number; selectedRowsData?: any[]; - setHeaders: Dispatch>; + setHeaders: Dispatch[]>>; setInitialFocusedCell: Dispatch>; setIsResizing: Dispatch>; setIsScrolling: Dispatch>; @@ -88,7 +88,7 @@ interface TableContextType { sortDownIcon: ReactNode; sortUpIcon: ReactNode; tableBodyContainerRef: RefObject; - tableRows: TableRow[]; + tableRows: TableRow[]; theme: Theme; unexpandedRows: Set; useHoverRowBackground: boolean; @@ -96,22 +96,25 @@ interface TableContextType { useOddEvenRowBackground: boolean; } -export const TableContext = createContext(undefined); +// Use any for the context creation since React contexts can't be truly generic +export const TableContext = createContext | undefined>(undefined); -export const TableProvider = ({ +export const TableProvider = ({ children, value, }: { children: ReactNode; - value: TableContextType; + value: TableContextType; }) => { return {children}; }; -export const useTableContext = () => { +export const useTableContext = (): TableContextType => { const context = useContext(TableContext); if (context === undefined) { throw new Error("useTableContext must be used within a TableProvider"); } + // This is safe because TableProvider ensures the value matches TableContextType + // at runtime, and the generic T is enforced at compile time at the usage site return context; }; diff --git a/src/hooks/useAggregatedRows.ts b/src/hooks/useAggregatedRows.ts index 626f0cfed..6c4840511 100644 --- a/src/hooks/useAggregatedRows.ts +++ b/src/hooks/useAggregatedRows.ts @@ -1,27 +1,26 @@ import { useMemo } from "react"; import HeaderObject, { Accessor } from "../types/HeaderObject"; import { AggregationConfig } from "../types/AggregationTypes"; -import Row from "../types/Row"; import { flattenAllHeaders } from "../utils/headerUtils"; import { isRowArray } from "../utils/rowUtils"; -interface UseAggregatedRowsProps { - rows: Row[]; - headers: HeaderObject[]; +interface UseAggregatedRowsProps { + rows: T[]; + headers: HeaderObject[]; rowGrouping?: string[]; } /** * Gets all headers that have aggregation configuration */ -const getAllAggregationHeaders = (headers: HeaderObject[]): HeaderObject[] => { +const getAllAggregationHeaders = (headers: HeaderObject[]): HeaderObject[] => { return flattenAllHeaders(headers).filter((header) => header.aggregation); }; /** * Aggregates child row data into parent rows based on header configuration */ -export const useAggregatedRows = ({ rows, headers, rowGrouping }: UseAggregatedRowsProps) => { +export const useAggregatedRows = ({ rows, headers, rowGrouping }: UseAggregatedRowsProps) => { return useMemo(() => { // If no row grouping is configured, return rows as-is if (!rowGrouping || rowGrouping.length === 0) { @@ -37,31 +36,33 @@ export const useAggregatedRows = ({ rows, headers, rowGrouping }: UseAggregatedR } // Deep clone rows to avoid mutating original data - const aggregatedRows = JSON.parse(JSON.stringify(rows)); + const aggregatedRows = JSON.parse(JSON.stringify(rows)) as T[]; // Process each row recursively - const processRows = (rowsToProcess: Row[], groupingLevel: number = 0): Row[] => { + const processRows = (rowsToProcess: T[], groupingLevel: number = 0) => { return rowsToProcess.map((row) => { const currentGroupKey = rowGrouping[groupingLevel]; const nextGroupKey = rowGrouping[groupingLevel + 1]; // If this row has children at the current grouping level - const currentGroupValue = row[currentGroupKey]; + const currentGroupValue = row[currentGroupKey as keyof T] as T[]; if (currentGroupValue && isRowArray(currentGroupValue)) { // Process children recursively first const processedChildren = processRows(currentGroupValue, groupingLevel + 1); // Calculate aggregations for this parent row const aggregatedRow = { ...row }; - aggregatedRow[currentGroupKey] = processedChildren; + aggregatedRow[currentGroupKey as keyof T] = processedChildren as T[keyof T]; // Calculate aggregated values for each configured header aggregationHeaders.forEach((header) => { + if (!header.accessor) return; + const aggregatedValue = calculateAggregation( processedChildren, header.accessor, header.aggregation!, - nextGroupKey + nextGroupKey as keyof T ); if (aggregatedValue !== undefined) { @@ -83,21 +84,21 @@ export const useAggregatedRows = ({ rows, headers, rowGrouping }: UseAggregatedR /** * Calculates aggregation for a specific field across child rows */ -const calculateAggregation = ( - childRows: Row[], - accessor: Accessor, +const calculateAggregation = ( + childRows: T[], + accessor: Accessor, config: AggregationConfig, - nextGroupKey?: string + nextGroupKey?: Accessor ): any => { // Collect all values from child rows const allValues: any[] = []; - const collectValues = (rows: Row[]) => { + const collectValues = (rows: T[]) => { rows.forEach((row) => { // If this row has further children, collect from them too const nextGroupValue = nextGroupKey ? row[nextGroupKey] : undefined; if (nextGroupKey && nextGroupValue && isRowArray(nextGroupValue)) { - collectValues(nextGroupValue); + collectValues(nextGroupValue as T[]); } else { // This is a leaf row, collect its value if (row[accessor] !== undefined && row[accessor] !== null) { diff --git a/src/hooks/useDragHandler.ts b/src/hooks/useDragHandler.ts index e6b835d3c..4515eb5e6 100644 --- a/src/hooks/useDragHandler.ts +++ b/src/hooks/useDragHandler.ts @@ -1,5 +1,5 @@ import { DragEvent } from "react"; -import HeaderObject, { Accessor } from "../types/HeaderObject"; +import HeaderObject, { AggregatedRow } from "../types/HeaderObject"; import DragHandlerProps from "../types/DragHandlerProps"; import usePrevious from "./usePrevious"; import { deepClone } from "../utils/generalUtils"; @@ -9,18 +9,18 @@ const REVERT_TO_PREVIOUS_HEADERS_DELAY = 1500; let prevUpdateTime = Date.now(); let prevDraggingPosition = { screenX: 0, screenY: 0 }; -const getHeaderIndexPath = ( - headers: HeaderObject[], - targetAccessor: Accessor, +const getHeaderIndexPath = ( + headers: HeaderObject>[], + id: string, currentPath: number[] = [] ): number[] | null => { for (let i = 0; i < headers.length; i++) { const header = headers[i]; - if (header.accessor === targetAccessor) { + if (header.id === id) { return [...currentPath, i]; } if (header.children && header.children.length > 0) { - const path = getHeaderIndexPath(header.children, targetAccessor, [...currentPath, i]); + const path = getHeaderIndexPath(header.children, id, [...currentPath, i]); if (path) return path; } } @@ -28,17 +28,17 @@ const getHeaderIndexPath = ( }; // Helper function to determine which section a header belongs to based on its pinned property -const getHeaderSection = (header: HeaderObject): "left" | "main" | "right" => { +const getHeaderSection = (header: HeaderObject): "left" | "main" | "right" => { if (header.pinned === "left") return "left"; if (header.pinned === "right") return "right"; return "main"; }; // Helper function to update header's pinned property based on target section -const updateHeaderPinnedProperty = ( - header: HeaderObject, +const updateHeaderPinnedProperty = ( + header: HeaderObject, targetSection: "left" | "main" | "right" -): HeaderObject => { +): HeaderObject => { const updatedHeader = { ...header }; if (targetSection === "left") { updatedHeader.pinned = "left"; @@ -51,19 +51,19 @@ const updateHeaderPinnedProperty = ( return updatedHeader; }; -function swapHeaders( - headers: HeaderObject[], +function swapHeaders( + headers: HeaderObject[], draggedPath: number[], hoveredPath: number[] -): { newHeaders: HeaderObject[]; emergencyBreak: boolean } { +): { newHeaders: HeaderObject[]; emergencyBreak: boolean } { // Create a deep copy of headers using our custom deep clone function const newHeaders = deepClone(headers); let emergencyBreak = false; // Helper function to get a header at a given path - function getHeaderAtPath(headers: HeaderObject[], path: number[]): HeaderObject { + function getHeaderAtPath(headers: HeaderObject[], path: number[]): HeaderObject { let current = headers; - let header: HeaderObject | undefined; + let header: HeaderObject | undefined; for (let i = 0; i < path.length - 1; i++) { current = current[path[i]].children!; } @@ -72,7 +72,11 @@ function swapHeaders( } // Helper function to set a header at a given path - function setHeaderAtPath(headers: HeaderObject[], path: number[], value: HeaderObject): void { + function setHeaderAtPath( + headers: HeaderObject[], + path: number[], + value: HeaderObject + ): void { let current = headers; for (let i = 0; i < path.length - 1; i++) { if (current[path[i]].children) { @@ -98,11 +102,11 @@ function swapHeaders( return { newHeaders, emergencyBreak }; } -function insertHeaderAcrossSections( - headers: HeaderObject[], - draggedHeader: HeaderObject, - hoveredHeader: HeaderObject -): { newHeaders: HeaderObject[]; emergencyBreak: boolean } { +function insertHeaderAcrossSections( + headers: HeaderObject[], + draggedHeader: HeaderObject, + hoveredHeader: HeaderObject +): { newHeaders: HeaderObject[]; emergencyBreak: boolean } { const newHeaders = deepClone(headers); let emergencyBreak = false; @@ -111,8 +115,8 @@ function insertHeaderAcrossSections( const hoveredSection = getHeaderSection(hoveredHeader); // Find the indices of both headers - const draggedIndex = newHeaders.findIndex((h) => h.accessor === draggedHeader.accessor); - const hoveredIndex = newHeaders.findIndex((h) => h.accessor === hoveredHeader.accessor); + const draggedIndex = newHeaders.findIndex((h) => h.id === draggedHeader.id); + const hoveredIndex = newHeaders.findIndex((h) => h.id === hoveredHeader.id); if (draggedIndex === -1 || hoveredIndex === -1) { emergencyBreak = true; @@ -149,17 +153,17 @@ function insertHeaderAcrossSections( return { newHeaders, emergencyBreak }; } -const useDragHandler = ({ +const useDragHandler = ({ draggedHeaderRef, headers, hoveredHeaderRef, onColumnOrderChange, onTableHeaderDragEnd, -}: DragHandlerProps) => { - const { setHeaders } = useTableContext(); - const prevHeaders = usePrevious(headers); +}: DragHandlerProps) => { + const { setHeaders } = useTableContext(); + const prevHeaders = usePrevious>[] | null>(headers); - const handleDragStart = (header: HeaderObject) => { + const handleDragStart = (header: HeaderObject>) => { draggedHeaderRef.current = header; prevUpdateTime = Date.now(); }; @@ -169,7 +173,7 @@ const useDragHandler = ({ hoveredHeader, }: { event: DragEvent; - hoveredHeader: HeaderObject; + hoveredHeader: HeaderObject>; }) => { // Prevent click event from firing event.preventDefault(); @@ -197,7 +201,7 @@ const useDragHandler = ({ const hoveredSection = getHeaderSection(hoveredHeader); const isCrossSectionDrag = draggedSection !== hoveredSection; - let newHeaders: HeaderObject[]; + let newHeaders: HeaderObject>[]; let emergencyBreak = false; if (isCrossSectionDrag) { @@ -210,8 +214,8 @@ const useDragHandler = ({ const currentHeaders = headers; // Get the index paths of both headers - const draggedHeaderIndexPath = getHeaderIndexPath(currentHeaders, draggedHeader.accessor); - const hoveredHeaderIndexPath = getHeaderIndexPath(currentHeaders, hoveredHeader.accessor); + const draggedHeaderIndexPath = getHeaderIndexPath(currentHeaders, draggedHeader.id); + const hoveredHeaderIndexPath = getHeaderIndexPath(currentHeaders, hoveredHeader.id); if (!draggedHeaderIndexPath || !hoveredHeaderIndexPath) return; @@ -238,7 +242,7 @@ const useDragHandler = ({ // If the header is animating, don't allow the drag isAnimating || // If the header is the same as the dragged header, don't allow the drag - hoveredHeader.accessor === draggedHeader.accessor || + hoveredHeader.id === draggedHeader.id || // If the distance is less than 10, don't allow the drag distance < 10 || // If the new headers are the same as the previous headers, don't allow the drag diff --git a/src/hooks/useExternalFilters.ts b/src/hooks/useExternalFilters.ts index 6a10943e7..916559ea5 100644 --- a/src/hooks/useExternalFilters.ts +++ b/src/hooks/useExternalFilters.ts @@ -1,12 +1,12 @@ import { useEffect } from "react"; import { TableFilterState } from "../types/FilterTypes"; -const useExternalFilters = ({ +const useExternalFilters = ({ filters, onFilterChange, }: { - filters: TableFilterState; - onFilterChange?: (filters: TableFilterState) => void; + filters: TableFilterState; + onFilterChange?: (filters: TableFilterState) => void; }) => { // On filter change, if there is an external filter handling, call the onFilterChange prop useEffect(() => { diff --git a/src/hooks/useFilterableData.ts b/src/hooks/useFilterableData.ts index 5f132f612..7a3a4febf 100644 --- a/src/hooks/useFilterableData.ts +++ b/src/hooks/useFilterableData.ts @@ -1,58 +1,46 @@ import { useState, useCallback, useMemo } from "react"; import { TableFilterState, FilterCondition } from "../types/FilterTypes"; import { applyFilterToValue } from "../utils/filterUtils"; -import Row from "../types/Row"; -import { Accessor } from "../types/HeaderObject"; // Helper function to compute filtered rows for a given filter state -const computeFilteredRows = ({ +const computeFilteredRows = ({ externalFilterHandling, tableRows, filterState, }: { externalFilterHandling: boolean; - tableRows: Row[]; - filterState: TableFilterState | null; -}): Row[] => { + tableRows: T[]; + filterState: TableFilterState | null; +}): T[] => { if (externalFilterHandling) return tableRows; if (!filterState || Object.keys(filterState).length === 0) return tableRows; return tableRows.filter((row) => { - return Object.values(filterState).every((filter) => { + return Object.values(filterState).every((filter: FilterCondition) => { try { const cellValue = row[filter.accessor]; return applyFilterToValue(cellValue, filter); } catch (error) { - console.warn(`Filter error for accessor ${filter.accessor}:`, error); + console.warn(`Filter error for accessor ${String(filter.accessor)}:`, error); return true; // Include row if filter fails } }); }); }; -interface UseFilterableDataProps { - rows: Row[]; +interface UseFilterableDataProps { + rows: T[]; externalFilterHandling: boolean; - onFilterChange?: (filters: TableFilterState) => void; + onFilterChange?: (filters: TableFilterState) => void; } -interface UseFilterableDataReturn { - filteredRows: Row[]; - updateFilter: (filter: FilterCondition) => void; - clearFilter: (accessor: Accessor) => void; - clearAllFilters: () => void; - filters: TableFilterState; - // Function to compute what rows would be after applying a filter (for pre-animation calculation) - computeFilteredRowsPreview: (filter: FilterCondition) => Row[]; -} - -const useFilterableData = ({ +const useFilterableData = ({ rows, externalFilterHandling, onFilterChange, -}: UseFilterableDataProps): UseFilterableDataReturn => { +}: UseFilterableDataProps) => { // Single filter state instead of complex 3-state system - const [filters, setFilters] = useState({}); + const [filters, setFilters] = useState>({}); // Compute current filtered rows const filteredRows = useMemo(() => { @@ -65,7 +53,7 @@ const useFilterableData = ({ // Filter update handler const updateFilter = useCallback( - (filter: FilterCondition) => { + (filter: FilterCondition) => { const newFilterState = { ...filters, [filter.accessor]: filter, @@ -79,9 +67,9 @@ const useFilterableData = ({ // Clear single filter const clearFilter = useCallback( - (accessor: Accessor) => { + (id: string) => { const newFilterState = { ...filters }; - delete newFilterState[accessor]; + delete newFilterState[id]; setFilters(newFilterState); onFilterChange?.(newFilterState); @@ -98,7 +86,7 @@ const useFilterableData = ({ // Function to preview what rows would be after applying a filter // This is used for pre-animation calculation const computeFilteredRowsPreview = useCallback( - (filter: FilterCondition) => { + (filter: FilterCondition) => { const previewFilterState = { ...filters, [filter.accessor]: filter, diff --git a/src/hooks/useRowSelection.ts b/src/hooks/useRowSelection.ts index e77ffac65..698d3c5e1 100644 --- a/src/hooks/useRowSelection.ts +++ b/src/hooks/useRowSelection.ts @@ -1,5 +1,4 @@ import { useState, useCallback, useMemo } from "react"; -import Row from "../types/Row"; import { Accessor } from "../types/HeaderObject"; import { areAllRowsSelected, @@ -12,19 +11,19 @@ import { } from "../utils/rowSelectionUtils"; import RowSelectionChangeProps from "../types/RowSelectionChangeProps"; -interface UseRowSelectionProps { - rows: Row[]; - rowIdAccessor: Accessor; - onRowSelectionChange?: (props: RowSelectionChangeProps) => void; +interface UseRowSelectionProps { + rows: T[]; + rowIdAccessor: Accessor; + onRowSelectionChange?: (props: RowSelectionChangeProps) => void; enableRowSelection?: boolean; } -export const useRowSelection = ({ +export const useRowSelection = ({ rows, rowIdAccessor, onRowSelectionChange, enableRowSelection = false, -}: UseRowSelectionProps) => { +}: UseRowSelectionProps) => { const [selectedRows, setSelectedRows] = useState>(new Set()); // Check if a specific row is selected diff --git a/src/hooks/useSelection.ts b/src/hooks/useSelection.ts index 1fbd01b50..1bf07b466 100644 --- a/src/hooks/useSelection.ts +++ b/src/hooks/useSelection.ts @@ -8,23 +8,23 @@ import { getRowId } from "../utils/rowUtils"; export const createSetString = ({ rowIndex, colIndex, rowId }: Cell) => `${rowIndex}-${colIndex}-${rowId}`; -interface UseSelectionProps { +interface UseSelectionProps { selectableCells: boolean; - headers: HeaderObject[]; - tableRows: TableRowType[]; - rowIdAccessor: Accessor; + headers: HeaderObject[]; + tableRows: TableRowType[]; + rowIdAccessor: Accessor; onCellEdit?: (props: any) => void; cellRegistry?: Map; } -const useSelection = ({ +const useSelection = ({ selectableCells, headers, tableRows, rowIdAccessor, onCellEdit, cellRegistry, -}: UseSelectionProps) => { +}: UseSelectionProps) => { const [selectedCells, setSelectedCells] = useState>(new Set()); const [selectedColumns, setSelectedColumns] = useState>(new Set()); const [lastSelectedColumnIndex, setLastSelectedColumnIndex] = useState(null); @@ -47,7 +47,7 @@ const useSelection = ({ // Example: {0: "name", 1: "age", 2: "email"} const colIndexToAccessor = new Map(); flattenedLeafHeaders.forEach((header, index) => { - colIndexToAccessor.set(index, header.accessor); + colIndexToAccessor.set(index, header.id); }); // Convert selectedCells (Set of "row-col-depth" strings) to a text format suitable for clipboard @@ -63,7 +63,9 @@ const useSelection = ({ // Get the accessor for this column using our mapping // Example: for col=2, might get accessor="email" - const accessor = colIndexToAccessor.get(col); + const accessor = flattenedLeafHeaders.find( + (header) => header.id === colIndexToAccessor.get(col) + )?.accessor; if (accessor && tableRows[row]?.row) { // Use the accessor to get the cell value directly from the row @@ -159,7 +161,11 @@ const useSelection = ({ } // Update the data - targetRow.row[targetHeader.accessor] = convertedValue; + if (targetHeader.accessor) { + targetRow.row[targetHeader.accessor] = convertedValue; + } else { + console.warn("No accessor found for target header", targetHeader); + } // Use cell registry for direct update if available if (cellRegistry) { @@ -214,7 +220,7 @@ const useSelection = ({ const flattenedLeafHeaders = leafHeaders.filter((header) => !header.hide); const colIndexToAccessor = new Map(); flattenedLeafHeaders.forEach((header, index) => { - colIndexToAccessor.set(index, header.accessor); + colIndexToAccessor.set(index, header.id); }); const deletedCells = new Set(); @@ -248,7 +254,7 @@ const useSelection = ({ emptyValue = false; } else if (targetHeader.type === "date") { emptyValue = null; - } else if (Array.isArray(targetRow.row[targetHeader.accessor])) { + } else if (targetHeader.accessor && Array.isArray(targetRow.row[targetHeader.accessor])) { // If the current value is an array, set it to an empty array emptyValue = []; } else { @@ -256,7 +262,11 @@ const useSelection = ({ } // Update the data - targetRow.row[targetHeader.accessor] = emptyValue; + if (targetHeader.accessor) { + targetRow.row[targetHeader.accessor] = emptyValue; + } else { + console.warn("No accessor found for target header", targetHeader); + } // Use cell registry for direct update if available if (cellRegistry) { diff --git a/src/hooks/useSortableData.ts b/src/hooks/useSortableData.ts index f331c2462..60112548a 100644 --- a/src/hooks/useSortableData.ts +++ b/src/hooks/useSortableData.ts @@ -1,12 +1,12 @@ import HeaderObject, { Accessor } from "../types/HeaderObject"; -import Row from "../types/Row"; import { useCallback, useMemo, useState } from "react"; import SortColumn from "../types/SortColumn"; import { handleSort } from "../utils/sortUtils"; import { isRowArray } from "../utils/rowUtils"; +import RowGrouping from "../types/RowGrouping"; // Helper function to compute sorted rows for a given sort column -const computeSortedRows = ({ +const computeSortedRows = ({ externalSortHandling, tableRows, sortColumn, @@ -15,17 +15,17 @@ const computeSortedRows = ({ sortNestedRows, }: { externalSortHandling: boolean; - tableRows: Row[]; - sortColumn: SortColumn | null; - rowGrouping?: string[]; - headers: HeaderObject[]; + tableRows: T[]; + sortColumn: SortColumn | null; + rowGrouping?: RowGrouping; + headers: HeaderObject[]; sortNestedRows: (params: { - groupingKeys: string[]; - headers: HeaderObject[]; - rows: Row[]; - sortColumn: SortColumn; - }) => Row[]; -}): Row[] => { + groupingKeys: RowGrouping; + headers: HeaderObject[]; + rows: T[]; + sortColumn: SortColumn; + }) => T[]; +}): T[] => { if (externalSortHandling) return tableRows; if (!sortColumn) return tableRows; @@ -42,21 +42,21 @@ const computeSortedRows = ({ }; // Extract sort logic to custom hook -const useSortableData = ({ +const useSortableData = ({ headers, tableRows, externalSortHandling, onSortChange, rowGrouping, }: { - headers: HeaderObject[]; - tableRows: Row[]; + headers: HeaderObject[]; + tableRows: T[]; externalSortHandling: boolean; - onSortChange?: (sort: SortColumn | null) => void; - rowGrouping?: string[]; + onSortChange?: (sort: SortColumn | null) => void; + rowGrouping?: RowGrouping; }) => { // Single sort state instead of complex 3-state system - const [sort, setSort] = useState(null); + const [sort, setSort] = useState | null>(null); // Recursive sort function for nested data const sortNestedRows = useCallback( @@ -67,10 +67,10 @@ const useSortableData = ({ sortColumn, }: { groupingKeys: string[]; - headers: HeaderObject[]; - rows: Row[]; - sortColumn: SortColumn; - }): Row[] => { + headers: HeaderObject[]; + rows: T[]; + sortColumn: SortColumn; + }): T[] => { // First sort the current level const sortedData = handleSort({ headers, rows, sortColumn }); @@ -82,12 +82,12 @@ const useSortableData = ({ // For each row, recursively sort its nested data return sortedData.map((row) => { const currentGroupingKey = groupingKeys[0]; - const nestedData = row[currentGroupingKey]; + const nestedData = row[currentGroupingKey as keyof T]; if (isRowArray(nestedData)) { // Recursively sort the nested data with remaining grouping keys const sortedNestedData = sortNestedRows({ - rows: nestedData, + rows: nestedData as T[], sortColumn, headers, groupingKeys: groupingKeys.slice(1), @@ -120,8 +120,8 @@ const useSortableData = ({ // Simple sort handler const updateSort = useCallback( - (accessor: Accessor) => { - const findHeaderRecursively = (headers: HeaderObject[]): HeaderObject | undefined => { + (accessor: Accessor) => { + const findHeaderRecursively = (headers: HeaderObject[]): HeaderObject | undefined => { for (const header of headers) { if (header.accessor === accessor) { return header; @@ -140,7 +140,7 @@ const useSortableData = ({ return; } - let newSortColumn: SortColumn | null = null; + let newSortColumn: SortColumn | null = null; if (!sort || sort.key.accessor !== accessor) { newSortColumn = { @@ -163,8 +163,8 @@ const useSortableData = ({ // Function to preview what rows would be after applying a sort // This is used for pre-animation calculation const computeSortedRowsPreview = useCallback( - (accessor: Accessor) => { - const findHeaderRecursively = (headers: HeaderObject[]): HeaderObject | undefined => { + (accessor: Accessor) => { + const findHeaderRecursively = (headers: HeaderObject[]): HeaderObject | undefined => { for (const header of headers) { if (header.accessor === accessor) { return header; @@ -183,7 +183,7 @@ const useSortableData = ({ return tableRows; } - let previewSortColumn: SortColumn | null = null; + let previewSortColumn: SortColumn | null = null; if (!sort || sort.key.accessor !== accessor) { previewSortColumn = { diff --git a/src/hooks/useTableAPI.ts b/src/hooks/useTableAPI.ts index 7a6cde16b..bd26c08d7 100644 --- a/src/hooks/useTableAPI.ts +++ b/src/hooks/useTableAPI.ts @@ -1,26 +1,26 @@ import { MutableRefObject, useEffect } from "react"; -import { Row, TableRefType, UpdateDataProps } from ".."; +import { TableRefType, UpdateDataProps } from ".."; import { getRowId } from "../utils/rowUtils"; import { getCellKey } from "../utils/cellUtils"; import { CellRegistryEntry } from "../context/TableContext"; import { Accessor } from "../types/HeaderObject"; -const useTableAPI = ({ +const useTableAPI = ({ tableRef, rows, rowIdAccessor, cellRegistryRef, }: { - tableRef?: MutableRefObject; - rows: Row[]; - rowIdAccessor: Accessor; + tableRef?: MutableRefObject | null>; + rows: T[]; + rowIdAccessor: Accessor; cellRegistryRef: MutableRefObject>; }) => { // Set up API methods on the ref if provided useEffect(() => { if (tableRef) { tableRef.current = { - updateData: ({ accessor, rowIndex, newValue }: UpdateDataProps) => { + updateData: ({ accessor, rowIndex, newValue }: UpdateDataProps) => { // Get the row ID using the new utility const row = rows?.[rowIndex]; if (row) { diff --git a/src/hooks/useTableRowProcessing.ts b/src/hooks/useTableRowProcessing.ts index b981735bd..65e04675a 100644 --- a/src/hooks/useTableRowProcessing.ts +++ b/src/hooks/useTableRowProcessing.ts @@ -3,31 +3,31 @@ import { BUFFER_ROW_COUNT } from "../consts/general-consts"; import { getVisibleRows } from "../utils/infiniteScrollUtils"; import { flattenRowsWithGrouping, getRowId } from "../utils/rowUtils"; import { ANIMATION_CONFIGS } from "../components/animate/animation-utils"; -import Row from "../types/Row"; import { Accessor } from "../types/HeaderObject"; import { FilterCondition } from "../types/FilterTypes"; +import RowGrouping from "../types/RowGrouping"; -interface UseTableRowProcessingProps { +interface UseTableRowProcessingProps { allowAnimations: boolean; - sortedRows: Row[]; + sortedRows: T[]; // Original unfiltered rows for establishing baseline positions - originalRows: Row[]; + originalRows: T[]; currentPage: number; rowsPerPage: number; shouldPaginate: boolean; - rowGrouping?: Accessor[]; - rowIdAccessor: Accessor; + rowGrouping?: RowGrouping; + rowIdAccessor: Accessor; unexpandedRows: Set; expandAll: boolean; contentHeight: number; rowHeight: number; scrollTop: number; // Functions to preview what rows would be after changes - computeFilteredRowsPreview: (filter: FilterCondition) => Row[]; - computeSortedRowsPreview: (accessor: Accessor) => Row[]; + computeFilteredRowsPreview: (filter: FilterCondition) => T[]; + computeSortedRowsPreview: (accessor: Accessor) => T[]; } -const useTableRowProcessing = ({ +const useTableRowProcessing = ({ allowAnimations, sortedRows, originalRows, @@ -43,7 +43,7 @@ const useTableRowProcessing = ({ scrollTop, computeFilteredRowsPreview, computeSortedRowsPreview, -}: UseTableRowProcessingProps) => { +}: UseTableRowProcessingProps) => { const [isAnimating, setIsAnimating] = useState(false); const [extendedRows, setExtendedRows] = useState([]); const previousTableRowsRef = useRef([]); // Track ALL processed rows, not just visible @@ -60,7 +60,7 @@ const useTableRowProcessing = ({ // Process rows through pagination and grouping const processRowSet = useCallback( - (rows: Row[]) => { + (rows: T[]) => { // Apply pagination const paginatedRows = shouldPaginate ? rows.slice((currentPage - 1) * rowsPerPage, currentPage * rowsPerPage) @@ -344,7 +344,7 @@ const useTableRowProcessing = ({ ); const prepareForSortChange = useCallback( - (accessor: Accessor) => { + (accessor: Accessor) => { if (!allowAnimations || shouldPaginate) return; // Calculate what rows would be after sort diff --git a/src/index.tsx b/src/index.tsx index 5f96dc825..2859b41cd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,14 +2,12 @@ import SimpleTable from "./components/simple-table/SimpleTable"; import BoundingBox from "./types/BoundingBox"; import Cell from "./types/Cell"; import CellChangeProps from "./types/CellChangeProps"; -import CellValue from "./types/CellValue"; import ColumnEditorPosition from "./types/ColumnEditorPosition"; import DragHandlerProps from "./types/DragHandlerProps"; import EnumOption from "./types/EnumOption"; import HeaderObject, { Accessor, ColumnType } from "./types/HeaderObject"; import { AggregationConfig, AggregationType } from "./types/AggregationTypes"; import OnSortProps from "./types/OnSortProps"; -import Row from "./types/Row"; import SharedTableProps from "./types/SharedTableProps"; import SortColumn from "./types/SortColumn"; import TableCellProps from "./types/TableCellProps"; @@ -21,6 +19,8 @@ import UpdateDataProps from "./types/UpdateCellProps"; import { FilterCondition, TableFilterState } from "./types/FilterTypes"; import RowSelectionChangeProps from "./types/RowSelectionChangeProps"; import CellClickProps from "./types/CellClickProps"; +import HeaderRendererProps from "./types/HeaderRendererProps"; +import CellRendererProps from "./types/CellRendererProps"; export { SimpleTable }; export type { @@ -31,15 +31,15 @@ export type { Cell, CellChangeProps, CellClickProps, - CellValue, + CellRendererProps, ColumnEditorPosition, ColumnType, DragHandlerProps, EnumOption, FilterCondition, HeaderObject, + HeaderRendererProps, OnSortProps, - Row, RowSelectionChangeProps, SharedTableProps, SortColumn, diff --git a/src/stories/data/athlete-data.ts b/src/stories/data/athlete-data.ts index be9216454..ba8fdde2c 100644 --- a/src/stories/data/athlete-data.ts +++ b/src/stories/data/athlete-data.ts @@ -1,7 +1,22 @@ -import Row from "../../types/Row"; -import HeaderObject from "../../types/HeaderObject"; +import HeaderObject, { STColumn } from "../../types/HeaderObject"; -export const generateAthletesData = (): Row[] => { +export type AthleteData = { + id: number; + country: string; + athleteName: string; + medals: number; + gold: number; + event: string; + personalBest: number; + lastCompeted: number; + age: number; + height: number; + weight: number; + team: string; + sponsor: string; +}; + +export const generateAthletesData = (): AthleteData[] => { const countries = ["USA", "China", "Russia", "UK", "Brazil", "Australia", "Japan"]; const firstNames = ["Alex", "Jordan", "Taylor", "Sam", "Chris", "Lee", "Pat"]; const lastNames = ["Smith", "Johnson", "Brown", "Davis", "Wilson", "Clark"]; @@ -37,7 +52,7 @@ export const generateAthletesData = (): Row[] => { }); }; -export const ATHLETES_HEADERS: HeaderObject[] = [ +export const ATHLETES_HEADERS: STColumn[] = [ { accessor: "country", label: "Country", @@ -85,7 +100,7 @@ export const ATHLETES_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => `${(row.personalBest as number).toFixed(2)}`, + cellRenderer: ({ row }: { row: AthleteData }) => `${(row.personalBest as number).toFixed(2)}`, }, { accessor: "lastCompeted", @@ -94,7 +109,7 @@ export const ATHLETES_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "left", - cellRenderer: ({ row }) => `${row.lastCompeted}`, + cellRenderer: ({ row }: { row: AthleteData }) => `${row.lastCompeted}`, }, { accessor: "age", label: "Age", width: 80, isSortable: true, isEditable: true, align: "right" }, { @@ -104,7 +119,7 @@ export const ATHLETES_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => `${(row.height as number).toFixed(2)}m`, + cellRenderer: ({ row }: { row: AthleteData }) => `${(row.height as number).toFixed(2)}m`, }, { accessor: "weight", @@ -113,7 +128,7 @@ export const ATHLETES_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => `${row.weight}kg`, + cellRenderer: ({ row }: { row: AthleteData }) => `${row.weight}kg`, }, { accessor: "team", diff --git a/src/stories/data/retail-data.ts b/src/stories/data/retail-data.ts index b8d24cc87..1ee2e3b9d 100644 --- a/src/stories/data/retail-data.ts +++ b/src/stories/data/retail-data.ts @@ -1,7 +1,39 @@ -import Row from "../../types/Row"; -import HeaderObject from "../../types/HeaderObject"; +import { STColumn } from "../../types/HeaderObject"; -export const generateRetailSalesData = (): Row[] => { +type RetailStore = { + id: number; + name: string; + city: string; + employees: number; + squareFootage: number; + openingDate: string; + customerRating: string; + electronicsSales: number; + clothingSales: number; + groceriesSales: number; + furnitureSales: number; + totalSales: number; +}; + +export type RetailRegion = { + id: number; + name: string; + city: string; // Will be "-" for regions + employees: number; // Aggregated from stores + squareFootage: number; // Aggregated from stores + openingDate: string; // Will be "-" for regions + customerRating: string; // Will be "-" for regions + electronicsSales: number; // Aggregated from stores + clothingSales: number; // Aggregated from stores + groceriesSales: number; // Aggregated from stores + furnitureSales: number; // Aggregated from stores + totalSales: number; // Aggregated from stores + stores: RetailStore[]; +}; + +export type RetailSalesData = RetailStore | RetailRegion; + +export const generateRetailSalesData = (): RetailRegion[] => { const regions = Array.from({ length: 20 }, (_, i) => `Region ${i + 1}`); const storeNames = ["MegaMart", "ShopRite", "TrendyGoods", "ValueStore", "QuickBuy"]; const cities = ["New York", "London", "Tokyo", "Sydney", "Paris", "Toronto", "Berlin"]; @@ -9,7 +41,7 @@ export const generateRetailSalesData = (): Row[] => { return regions.map((region) => { const numStores = Math.floor(Math.random() * 7) + 2; // 2 to 8 children - const stores = Array.from({ length: numStores }, () => { + const stores: RetailStore[] = Array.from({ length: numStores }, () => { const storeName = storeNames[Math.floor(Math.random() * storeNames.length)]; const city = cities[Math.floor(Math.random() * cities.length)]; const electronicsSales = Math.floor(Math.random() * 100000) + 5000; @@ -68,7 +100,7 @@ export const generateRetailSalesData = (): Row[] => { }); }; -export const RETAIL_SALES_HEADERS: HeaderObject[] = [ +export const RETAIL_SALES_HEADERS: STColumn[] = [ { accessor: "name", label: "Name", diff --git a/src/stories/data/saas-data.ts b/src/stories/data/saas-data.ts index eccac81b4..6445134b8 100644 --- a/src/stories/data/saas-data.ts +++ b/src/stories/data/saas-data.ts @@ -1,7 +1,23 @@ -import Row from "../../types/Row"; -import HeaderObject from "../../types/HeaderObject"; +import { STColumn } from "../../types/HeaderObject"; -export const generateSaaSData = (): Row[] => { +export type SaaSData = { + id: number; + tier: string; + segment: string; + monthlyRevenue: number; + activeUsers: number; + churnRate: number; + avgSessionTime: number; + renewalDate: string; + supportTickets: number; + signUpDate: string; + lastLogin: string; + featureUsage: string; + customerSatisfaction: number; + paymentMethod: string; +}; + +export const generateSaaSData = (): SaaSData[] => { const segments = ["Freelancers", "Small Business", "Startups", "Corporations", "Nonprofits"]; const features = ["Analytics", "Collaboration", "Storage", "API Access"]; const paymentMethods = ["Credit Card", "PayPal", "Bank Transfer", "Crypto"]; @@ -48,7 +64,7 @@ export const generateSaaSData = (): Row[] => { }); }; -export const SAAS_HEADERS: HeaderObject[] = [ +export const SAAS_HEADERS: STColumn[] = [ { accessor: "tier", label: "Tier", diff --git a/src/stories/data/space-data.ts b/src/stories/data/space-data.ts index be8f60f18..2d36653d5 100644 --- a/src/stories/data/space-data.ts +++ b/src/stories/data/space-data.ts @@ -1,7 +1,27 @@ -import Row from "../../types/Row"; -import HeaderObject from "../../types/HeaderObject"; +import { STColumn } from "../../types/HeaderObject"; -export const generateSpaceData = (): Row[] => { +export type SpaceData = { + id: number; + agency: string; + missionName: string; + launchDate: string; + destination: string; + status: string; + crewSize: number; + budget?: number; + duration: string | number; + payloadWeight: number; + launchSite: string; + missionCostPerKg: number; + successRate: number; + scientificYield: number; + q1: number; + q2: number; + q3: number; + q4: number; +}; + +export const generateSpaceData = (): SpaceData[] => { const agencies = ["NASA", "ESA", "SpaceX", "Roscosmos", "ISRO"]; const destinations = ["Moon", "Mars", "Venus", "Jupiter", "Asteroid Belt", "Saturn"]; const missionTypes = ["Orbiter", "Rover", "Lander", "Crewed", "Probe"]; @@ -45,7 +65,7 @@ export const generateSpaceData = (): Row[] => { }); }; -export const SPACE_HEADERS: HeaderObject[] = [ +export const SPACE_HEADERS: STColumn[] = [ { accessor: "agency", label: "Agency", @@ -71,7 +91,7 @@ export const SPACE_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "left", - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: { row: SpaceData }) => { const date = new Date(row.launchDate as string); return date.toLocaleDateString("en-US", { month: "numeric", @@ -158,7 +178,7 @@ export const SPACE_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: { row: SpaceData }) => { if (row.duration === "Ongoing") return "Ongoing"; return `${row.duration}y`; }, @@ -170,7 +190,7 @@ export const SPACE_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => `${row.payloadWeight as number}kg`, + cellRenderer: ({ row }: { row: SpaceData }) => `${row.payloadWeight as number}kg`, }, { accessor: "launchSite", @@ -187,7 +207,7 @@ export const SPACE_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: { row: SpaceData }) => { return `$${(row.missionCostPerKg as number).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, @@ -201,7 +221,7 @@ export const SPACE_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => `${row.successRate}%`, + cellRenderer: ({ row }: { row: SpaceData }) => `${row.successRate}%`, }, { accessor: "scientificYield", @@ -210,6 +230,6 @@ export const SPACE_HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, align: "right", - cellRenderer: ({ row }) => `${row.scientificYield}TB`, + cellRenderer: ({ row }: { row: SpaceData }) => `${row.scientificYield}TB`, }, ]; diff --git a/src/stories/examples/AggregateExample.tsx b/src/stories/examples/AggregateExample.tsx index badc9d125..936afc236 100644 --- a/src/stories/examples/AggregateExample.tsx +++ b/src/stories/examples/AggregateExample.tsx @@ -1,5 +1,5 @@ import SimpleTable from "../../components/simple-table/SimpleTable"; -import HeaderObject from "../../types/HeaderObject"; +import { STColumn } from "../../types/HeaderObject"; import { UniversalTableProps } from "./StoryWrapper"; // Default args specific to AggregateExample - exported for reuse in stories and tests @@ -8,7 +8,33 @@ export const aggregateExampleDefaults = { height: "400px", }; -const headers: HeaderObject[] = [ +type RowData = { + id: number; + name: string; + status: string; + followers?: number; + revenue?: number; + rating?: number; + contentCount?: number; + avgViewTime?: number; + categories: { + id: number; + name: string; + status: string; + creators: { + id: number; + name: string; + followers: number; + revenue: string; + rating: number; + contentCount: number; + avgViewTime: number; + status: string; + }[]; + }[]; +}; + +const headers: STColumn[] = [ { accessor: "name", label: "Name", width: 200, expandable: true, type: "string" }, { accessor: "followers", @@ -93,7 +119,7 @@ const headers: HeaderObject[] = [ ]; // Streaming platform data with categories and creators -const rows = [ +const rows: RowData[] = [ // StreamFlix Platform { id: 1, diff --git a/src/stories/examples/BasicExample.tsx b/src/stories/examples/BasicExample.tsx index cd6bee646..f1df20724 100644 --- a/src/stories/examples/BasicExample.tsx +++ b/src/stories/examples/BasicExample.tsx @@ -1,7 +1,195 @@ import { SimpleTable } from "../.."; -import { HeaderObject } from "../.."; +import { STColumn } from "../../types/HeaderObject"; import { UniversalTableProps } from "./StoryWrapper"; +type Employee = { + id: number; + name: string; + age: number; + role: string; + department: string; + startDate: string; +}; + +// Sample data for a quick start demo - now using the new simplified structure +const rows: Employee[] = [ + { + id: 1, + name: "John Doe", + age: 28, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 2, + name: "Jane Smith", + age: 32, + role: "Designer", + department: "Design", + startDate: "2020-01-01", + }, + { + id: 3, + name: "Bob Johnson", + age: 45, + role: "Manager", + department: "Management", + startDate: "2020-01-01", + }, + { + id: 4, + name: "Alice Williams", + age: 24, + role: "Intern", + department: "Internship", + startDate: "2020-01-01", + }, + { + id: 5, + name: "Charlie Brown", + age: 37, + role: "DevOps", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 6, + name: "Diana Prince", + age: 29, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 7, + name: "Ethan Hunt", + age: 31, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 8, + name: "Frank Underwood", + age: 40, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 9, + name: "Grace Hopper", + age: 35, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 10, + name: "Hannah Montana", + age: 22, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 11, + name: "Ian Somerhalder", + age: 33, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 12, + name: "Jake Gyllenhaal", + age: 38, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 13, + name: "Kyle Chandler", + age: 42, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 14, + name: "Liam Neeson", + age: 48, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 15, + name: "abdelrahman", + age: 30, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 16, + name: "Nina Dobrev", + age: 34, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 17, + name: "Omar Sy", + age: 41, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 18, + name: "Pablo Escobar", + age: 50, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 19, + name: "Qasim", + age: 22, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, + { + id: 20, + name: "Rajesh", + age: 22, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + }, +]; + +// Define headers +const headers: STColumn[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, filterable: true }, + { + accessor: "name", + label: "Name", + minWidth: 80, + width: "1fr", + isSortable: true, + filterable: true, + }, + { accessor: "age", label: "Age", width: 100, isSortable: true, filterable: true }, + { accessor: "role", label: "Role", width: 150, isSortable: true, filterable: true }, +]; + // Default args specific to BasicExample - exported for reuse in stories and tests export const basicExampleDefaults = { columnResizing: true, @@ -12,185 +200,6 @@ export const basicExampleDefaults = { }; const BasicExampleComponent = (props: UniversalTableProps) => { - // Sample data for a quick start demo - now using the new simplified structure - const rows = [ - { - id: 1, - name: "John Doe", - age: 28, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 2, - name: "Jane Smith", - age: 32, - role: "Designer", - department: "Design", - startDate: "2020-01-01", - }, - { - id: 3, - name: "Bob Johnson", - age: 45, - role: "Manager", - department: "Management", - startDate: "2020-01-01", - }, - { - id: 4, - name: "Alice Williams", - age: 24, - role: "Intern", - department: "Internship", - startDate: "2020-01-01", - }, - { - id: 5, - name: "Charlie Brown", - age: 37, - role: "DevOps", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 6, - name: "Diana Prince", - age: 29, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 7, - name: "Ethan Hunt", - age: 31, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 8, - name: "Frank Underwood", - age: 40, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 9, - name: "Grace Hopper", - age: 35, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 10, - name: "Hannah Montana", - age: 22, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 11, - name: "Ian Somerhalder", - age: 33, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 12, - name: "Jake Gyllenhaal", - age: 38, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 13, - name: "Kyle Chandler", - age: 42, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 14, - name: "Liam Neeson", - age: 48, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 15, - name: "abdelrahman", - age: 30, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 16, - name: "Nina Dobrev", - age: 34, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 17, - name: "Omar Sy", - age: 41, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 18, - name: "Pablo Escobar", - age: 50, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 19, - name: "Qasim", - age: 22, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - { - id: 20, - name: "Rajesh", - age: 22, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - }, - ]; - - // Define headers - const headers: HeaderObject[] = [ - { accessor: "id", label: "ID", width: 80, isSortable: true, filterable: true }, - { - accessor: "name", - label: "Name", - minWidth: 80, - width: "1fr", - isSortable: true, - filterable: true, - }, - { accessor: "age", label: "Age", width: 100, isSortable: true, filterable: true }, - { accessor: "role", label: "Role", width: 150, isSortable: true, filterable: true }, - ]; - return ; }; diff --git a/src/stories/examples/CellHighlighting.tsx b/src/stories/examples/CellHighlighting.tsx index 755ad7366..381b30329 100644 --- a/src/stories/examples/CellHighlighting.tsx +++ b/src/stories/examples/CellHighlighting.tsx @@ -1,6 +1,16 @@ -import { HeaderObject, SimpleTable } from "../.."; +import { SimpleTable } from "../.."; +import { STColumn } from "../../types/HeaderObject"; import { UniversalTableProps } from "./StoryWrapper"; +type RowData = { + id: number; + product: string; + sales: number; + growth: number; + status: string; + risk: string; +}; + // Default args specific to CellHighlighting - exported for reuse in stories and tests export const cellHighlightingDefaults = { selectableCells: true, @@ -8,7 +18,7 @@ export const cellHighlightingDefaults = { }; // Define headers with conditional cell styling -const headers: HeaderObject[] = [ +const headers: STColumn[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "product", label: "Product", minWidth: 100, width: "1fr", type: "string" }, { diff --git a/src/stories/examples/CellRenderer.tsx b/src/stories/examples/CellRenderer.tsx index 4d1114e84..453cafd74 100644 --- a/src/stories/examples/CellRenderer.tsx +++ b/src/stories/examples/CellRenderer.tsx @@ -1,7 +1,14 @@ import { SimpleTable } from "../.."; -import { HeaderObject } from "../.."; +import { STColumn } from "../../types/HeaderObject"; import { UniversalTableProps } from "./StoryWrapper"; +type RowData = { + id: number; + name: string; + age: number; + role: string; +}; + // Default args specific to CellRenderer - exported for reuse in stories and tests export const cellRendererDefaults = { columnReordering: true, @@ -122,7 +129,7 @@ const CellRendererExample = (props: UniversalTableProps) => { const rows = CELL_RENDERER_DATA; // Define headers - const headers: HeaderObject[] = [ + const headers: STColumn[] = [ { accessor: "id", label: "ID", diff --git a/src/stories/examples/DynamicHeadersExample.tsx b/src/stories/examples/DynamicHeadersExample.tsx index 8408fe7a2..0e97f0e58 100644 --- a/src/stories/examples/DynamicHeadersExample.tsx +++ b/src/stories/examples/DynamicHeadersExample.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { SimpleTable } from "../.."; -import { HeaderObject } from "../.."; +import { STColumn } from "../../types/HeaderObject"; import { UniversalTableProps } from "./StoryWrapper"; // Default args specific to DynamicHeaders - exported for reuse in stories and tests @@ -10,6 +10,16 @@ export const dynamicHeadersDefaults = { selectableCells: true, }; +type EmployeeData = { + id: number; + name: string; + age: number; + role: string; + department: string; + startDate: string; + salary: number; +}; + const DynamicHeadersExample = (props: UniversalTableProps) => { // Sample data for testing dynamic headers const rows = [ @@ -61,7 +71,7 @@ const DynamicHeadersExample = (props: UniversalTableProps) => { ]; // Define all possible headers - const allHeaders: HeaderObject[] = [ + const allHeaders: STColumn[] = [ { accessor: "id", label: "ID", width: 80, isSortable: true }, { accessor: "name", label: "Name", minWidth: 80, width: "1fr", isSortable: true }, { accessor: "age", label: "Age", width: 100, isSortable: true }, @@ -72,7 +82,7 @@ const DynamicHeadersExample = (props: UniversalTableProps) => { ]; // Define reduced headers (hiding department and salary) - const reducedHeaders: HeaderObject[] = [ + const reducedHeaders: STColumn[] = [ { accessor: "id", label: "ID", width: 80, isSortable: true }, { accessor: "name", label: "Name", minWidth: 80, width: "1fr", isSortable: true }, { accessor: "age", label: "Age", width: 100, isSortable: true }, @@ -81,14 +91,14 @@ const DynamicHeadersExample = (props: UniversalTableProps) => { ]; // Define minimal headers (only basic info) - const minimalHeaders: HeaderObject[] = [ + const minimalHeaders: STColumn[] = [ { accessor: "id", label: "ID", width: 80, isSortable: true }, { accessor: "name", label: "Name", minWidth: 80, width: "1fr", isSortable: true }, { accessor: "role", label: "Role", width: 150, isSortable: true }, ]; // State to manage which headers to show - const [currentHeaders, setCurrentHeaders] = useState(allHeaders); + const [currentHeaders, setCurrentHeaders] = useState[]>(allHeaders); const [currentView, setCurrentView] = useState<"all" | "reduced" | "minimal">("all"); // Functions to handle button clicks diff --git a/src/stories/examples/EditableCells.tsx b/src/stories/examples/EditableCells.tsx index bd0902c77..bcc16e2eb 100644 --- a/src/stories/examples/EditableCells.tsx +++ b/src/stories/examples/EditableCells.tsx @@ -1,12 +1,23 @@ import { useState } from "react"; import SimpleTable from "../../components/simple-table/SimpleTable"; import CellChangeProps from "../../types/CellChangeProps"; -import Row from "../../types/Row"; import { RowId } from "../../types/RowId"; -import CellValue from "../../types/CellValue"; -import HeaderObject, { Accessor } from "../../types/HeaderObject"; +import { Accessor, STColumn } from "../../types/HeaderObject"; import { UniversalTableProps } from "./StoryWrapper"; +type EmployeeData = { + id: number; + status: string; + firstName: string; + lastName: string; + email: string; + role: string; + hireDate: string; + isActive: boolean; + salary: number; + reviewDate: string; +}; + // Default args specific to EditableCells - exported for reuse in stories and tests export const editableCellsDefaults = { columnResizing: true, @@ -16,7 +27,7 @@ export const editableCellsDefaults = { }; // Define headers with editable property and various types -const HEADERS: HeaderObject[] = [ +const HEADERS: STColumn[] = [ { accessor: "status", label: "Status", @@ -151,14 +162,14 @@ const ROWS = [ ]; const EditableCellsExample = (props: UniversalTableProps) => { - const [rows, setRows] = useState(ROWS); + const [rows, setRows] = useState(ROWS); const updateRowData = ( - rows: Row[], + rows: EmployeeData[], targetRowId: RowId, - accessor: Accessor, - newValue: CellValue - ): Row[] => { + accessor: Accessor, + newValue: any + ): EmployeeData[] => { return rows.map((row) => { if (row.id === targetRowId) { // Found the row, update its data directly @@ -172,7 +183,7 @@ const EditableCellsExample = (props: UniversalTableProps) => { }); }; - const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { + const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { const rowId = row.id as RowId; // Get the row ID directly from the row setRows((prevRows) => updateRowData(prevRows, rowId, accessor, newValue)); }; diff --git a/src/stories/examples/ExternalFilterExample.tsx b/src/stories/examples/ExternalFilterExample.tsx index cbc9f0cb2..0aded9896 100644 --- a/src/stories/examples/ExternalFilterExample.tsx +++ b/src/stories/examples/ExternalFilterExample.tsx @@ -1,13 +1,22 @@ import React, { useState, useMemo } from "react"; import { SimpleTable } from "../.."; -import HeaderObject from "../../types/HeaderObject"; -import Row from "../../types/Row"; +import { STColumn } from "../../types/HeaderObject"; import { FilterCondition, TableFilterState } from "../../types/FilterTypes"; -import CellValue from "../../types/CellValue"; import { UniversalTableProps } from "./StoryWrapper"; +type EmployeeData = { + id: number; + name: string; + age: number; + email: string; + salary: number; + department: string; + active: boolean; + location: string; +}; + // Sample data with more variety for filtering -const sampleData: Row[] = [ +const sampleData: EmployeeData[] = [ { id: 1, name: "John Doe", @@ -110,7 +119,7 @@ const sampleData: Row[] = [ }, ]; -const headers: HeaderObject[] = [ +const headers: STColumn[] = [ { accessor: "name", label: "Name", @@ -186,7 +195,7 @@ export const externalFilterExampleDefaults = { }; const ExternalFilterExampleComponent: React.FC = (props) => { - const [filters, setFilters] = useState<{ [key: string]: FilterCondition }>({}); + const [filters, setFilters] = useState<{ [key: string]: FilterCondition }>({}); // Filter data externally based on filters const filteredData = useMemo(() => { @@ -194,7 +203,7 @@ const ExternalFilterExampleComponent: React.FC = (props) => return sampleData.filter((row) => { return Object.values(filters).every((filter) => { - const cellValue = row[filter.accessor] as CellValue; + const cellValue = row[filter.accessor]; // Apply filter based on operator switch (filter.operator) { @@ -233,7 +242,7 @@ const ExternalFilterExampleComponent: React.FC = (props) => }); }, [filters]); - const handleFilterChange = (filters: TableFilterState) => { + const handleFilterChange = (filters: TableFilterState) => { setFilters(filters); }; return ( diff --git a/src/stories/examples/ExternalSortExample.tsx b/src/stories/examples/ExternalSortExample.tsx index 8087610ff..81921c78f 100644 --- a/src/stories/examples/ExternalSortExample.tsx +++ b/src/stories/examples/ExternalSortExample.tsx @@ -1,12 +1,20 @@ import React, { useState, useMemo } from "react"; import { SimpleTable } from "../.."; -import HeaderObject from "../../types/HeaderObject"; -import Row from "../../types/Row"; +import { STColumn } from "../../types/HeaderObject"; import SortColumn from "../../types/SortColumn"; import { UniversalTableProps } from "./StoryWrapper"; +type EmployeeData = { + id: number; + name: string; + age: number; + email: string; + salary: number; + department: string; +}; + // Sample data -const sampleData: Row[] = [ +const sampleData: EmployeeData[] = [ { id: 1, name: "John Doe", @@ -73,7 +81,7 @@ const sampleData: Row[] = [ }, ]; -const headers: HeaderObject[] = [ +const headers: STColumn[] = [ { accessor: "name", label: "Name", @@ -108,7 +116,7 @@ const headers: HeaderObject[] = [ width: 120, isSortable: true, type: "number", - cellRenderer: ({ row }) => `$${(row.salary || 0).toLocaleString()}`, + cellRenderer: ({ row }: { row: EmployeeData }) => `$${(row.salary || 0).toLocaleString()}`, align: "right", }, ]; @@ -122,7 +130,7 @@ export const externalSortExampleDefaults = { }; const ExternalSortExampleComponent: React.FC = (props) => { - const [sortColumn, setSortColumn] = useState(null); + const [sortColumn, setSortColumn] = useState | null>(null); // Sort data externally based on sortConfig const sortedData = useMemo(() => { @@ -130,6 +138,9 @@ const ExternalSortExampleComponent: React.FC = (props) => { const sorted = [...sampleData].sort((a, b) => { const accessor = sortColumn.key.accessor; + + if (!accessor) return 0; + const aValue = a[accessor]; const bValue = b[accessor]; diff --git a/src/stories/examples/HiddenColumnsExample.tsx b/src/stories/examples/HiddenColumnsExample.tsx index 5ff1e8d62..3eeb34a72 100644 --- a/src/stories/examples/HiddenColumnsExample.tsx +++ b/src/stories/examples/HiddenColumnsExample.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import SimpleTable from "../../components/simple-table/SimpleTable"; -import { generateSpaceData, SPACE_HEADERS } from "../data/space-data"; +import { generateSpaceData, SPACE_HEADERS, SpaceData } from "../data/space-data"; import CellChangeProps from "../../types/CellChangeProps"; import { UniversalTableProps } from "./StoryWrapper"; @@ -19,7 +19,7 @@ const HEADERS = SPACE_HEADERS; const FilterColumnsExample = (props: UniversalTableProps) => { const [rows, setRows] = useState(EXAMPLE_DATA); - const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { + const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { setRows((prevRows) => { const rowIndex = prevRows.findIndex((r) => r.id === row.id); if (rowIndex !== -1) { diff --git a/src/stories/examples/InfiniteScroll.tsx b/src/stories/examples/InfiniteScroll.tsx index dc5199daf..d4cad6102 100644 --- a/src/stories/examples/InfiniteScroll.tsx +++ b/src/stories/examples/InfiniteScroll.tsx @@ -1,9 +1,8 @@ import { useState, useCallback } from "react"; import SimpleTable from "../../components/simple-table/SimpleTable"; -import { SAAS_HEADERS } from "../data/saas-data"; +import { SAAS_HEADERS, SaaSData } from "../data/saas-data"; import CellChangeProps from "../../types/CellChangeProps"; import { UniversalTableProps } from "./StoryWrapper"; -import Row from "../../types/Row"; // Default args specific to InfiniteScroll - exported for reuse in stories and tests export const infiniteScrollDefaults = { @@ -39,7 +38,7 @@ const HEADERS = SAAS_HEADERS; const simulateApiDelay = (ms: number = 800) => new Promise((resolve) => setTimeout(resolve, ms)); // Local data generation function that accepts parameters -const generateSaaSDataWithParams = (count: number, startId: number = 0): Row[] => { +const generateSaaSDataWithParams = (count: number, startId: number = 0): SaaSData[] => { const segments = ["Freelancers", "Small Business", "Startups", "Corporations", "Nonprofits"]; const features = ["Analytics", "Collaboration", "Storage", "API Access"]; const paymentMethods = ["Credit Card", "PayPal", "Bank Transfer", "Crypto"]; @@ -90,7 +89,7 @@ const InfiniteScrollExample = (props: UniversalTableProps) => { const [isLoading, setIsLoading] = useState(false); const [totalLoadedRows, setTotalLoadedRows] = useState(INITIAL_ROWS_COUNT); - const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { + const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { setRows((prevRows) => { const rowIndex = prevRows.findIndex((r) => r.id === row.id); if (rowIndex !== -1) { diff --git a/src/stories/examples/LiveUpdates.tsx b/src/stories/examples/LiveUpdates.tsx index 1e941e63d..56588994f 100644 --- a/src/stories/examples/LiveUpdates.tsx +++ b/src/stories/examples/LiveUpdates.tsx @@ -1,6 +1,15 @@ import { useRef, useEffect } from "react"; -import { HeaderObject, SimpleTable, TableRefType } from "../.."; +import { SimpleTable, TableRefType } from "../.."; import { UniversalTableProps } from "./StoryWrapper"; +import { STColumn } from "../../types/HeaderObject"; + +type RowData = { + id: number; + product: string; + price: number; + stock: number; + sales: number; +}; // Default args specific to LiveUpdates - exported for reuse in stories and tests export const liveUpdatesDefaults = { @@ -9,7 +18,7 @@ export const liveUpdatesDefaults = { }; // Define headers -export const headers: HeaderObject[] = [ +export const headers: STColumn[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "product", label: "Product", width: 180, type: "string" }, { @@ -119,7 +128,7 @@ const initialData = [ const LiveUpdatesExample = (props: UniversalTableProps) => { // Keep a local copy of the data to update - const tableRef = useRef(null); + const tableRef = useRef | null>(null); // Set up intervals for automatic updates useEffect(() => { diff --git a/src/stories/examples/RowHeightExample.tsx b/src/stories/examples/RowHeightExample.tsx index 01068fde4..eb6ee41b3 100644 --- a/src/stories/examples/RowHeightExample.tsx +++ b/src/stories/examples/RowHeightExample.tsx @@ -1,5 +1,5 @@ import { SimpleTable } from "../.."; -import { HeaderObject } from "../.."; +import { STColumn } from "../../types/HeaderObject"; import { UniversalTableProps } from "./StoryWrapper"; // Default args specific to RowHeight - exported for reuse in stories and tests @@ -7,6 +7,13 @@ export const rowHeightDefaults = { rowHeight: 24, }; +type RowData = { + id: number; + name: string; + age: number; + role: string; +}; + const RowHeightExampleComponent = (props: UniversalTableProps) => { // Sample data for testing row heights const rows = [ @@ -31,7 +38,7 @@ const RowHeightExampleComponent = (props: UniversalTableProps) => { ]; // Define headers - const headers: HeaderObject[] = [ + const headers: STColumn[] = [ { accessor: "id", label: "ID", width: 80 }, { accessor: "name", label: "Name", width: 150 }, { accessor: "age", label: "Age", width: 100 }, diff --git a/src/stories/examples/RowSelectionExample.tsx b/src/stories/examples/RowSelectionExample.tsx index 27cc1a6ae..994931ae9 100644 --- a/src/stories/examples/RowSelectionExample.tsx +++ b/src/stories/examples/RowSelectionExample.tsx @@ -1,9 +1,112 @@ import { useState } from "react"; import { CellClickProps, SimpleTable } from "../.."; -import { HeaderObject } from "../.."; import { UniversalTableProps } from "./StoryWrapper"; -import Row from "../../types/Row"; import RowSelectionChangeProps from "../../types/RowSelectionChangeProps"; +import { STColumn } from "../../types/HeaderObject"; + +type EmployeeData = { + id: number; + name: string; + age: number; + role: string; + department: string; + startDate: string; + status: string; +}; + +// Sample data for the row selection demo +const rows: EmployeeData[] = [ + { + id: 1, + name: "John Doe", + age: 28, + role: "Developer", + department: "Engineering", + startDate: "2020-01-01", + status: "Active", + }, + { + id: 2, + name: "Jane Smith", + age: 32, + role: "Designer", + department: "Design", + startDate: "2019-03-15", + status: "Active", + }, + { + id: 3, + name: "Bob Johnson", + age: 45, + role: "Manager", + department: "Management", + startDate: "2018-07-20", + status: "Active", + }, + { + id: 4, + name: "Alice Williams", + age: 24, + role: "Intern", + department: "Internship", + startDate: "2023-01-10", + status: "Active", + }, + { + id: 5, + name: "Charlie Brown", + age: 37, + role: "DevOps", + department: "Engineering", + startDate: "2021-05-12", + status: "Active", + }, + { + id: 6, + name: "Diana Prince", + age: 29, + role: "Developer", + department: "Engineering", + startDate: "2022-02-28", + status: "Inactive", + }, + { + id: 7, + name: "Ethan Hunt", + age: 31, + role: "Developer", + department: "Engineering", + startDate: "2021-09-15", + status: "Active", + }, + { + id: 8, + name: "Frank Underwood", + age: 40, + role: "Team Lead", + department: "Engineering", + startDate: "2020-11-03", + status: "Active", + }, + { + id: 9, + name: "Grace Hopper", + age: 35, + role: "Senior Developer", + department: "Engineering", + startDate: "2019-08-22", + status: "Active", + }, + { + id: 10, + name: "Hannah Montana", + age: 22, + role: "Junior Developer", + department: "Engineering", + startDate: "2023-06-01", + status: "Active", + }, +]; // Default args specific to RowSelectionExample - exported for reuse in stories and tests export const rowSelectionExampleDefaults = { @@ -17,122 +120,32 @@ export const rowSelectionExampleDefaults = { const RowSelectionExample = (props: UniversalTableProps) => { // State to track selection for demo purposes - const [selectedRowsInfo, setSelectedRowsInfo] = useState([]); + const [selectedRowsInfo, setSelectedRowsInfo] = useState([]); const [lastAction, setLastAction] = useState(""); - // Sample data for the row selection demo - const rows = [ - { - id: 1, - name: "John Doe", - age: 28, - role: "Developer", - department: "Engineering", - startDate: "2020-01-01", - status: "Active", - }, - { - id: 2, - name: "Jane Smith", - age: 32, - role: "Designer", - department: "Design", - startDate: "2019-03-15", - status: "Active", - }, - { - id: 3, - name: "Bob Johnson", - age: 45, - role: "Manager", - department: "Management", - startDate: "2018-07-20", - status: "Active", - }, - { - id: 4, - name: "Alice Williams", - age: 24, - role: "Intern", - department: "Internship", - startDate: "2023-01-10", - status: "Active", - }, - { - id: 5, - name: "Charlie Brown", - age: 37, - role: "DevOps", - department: "Engineering", - startDate: "2021-05-12", - status: "Active", - }, - { - id: 6, - name: "Diana Prince", - age: 29, - role: "Developer", - department: "Engineering", - startDate: "2022-02-28", - status: "Inactive", - }, - { - id: 7, - name: "Ethan Hunt", - age: 31, - role: "Developer", - department: "Engineering", - startDate: "2021-09-15", - status: "Active", - }, - { - id: 8, - name: "Frank Underwood", - age: 40, - role: "Team Lead", - department: "Engineering", - startDate: "2020-11-03", - status: "Active", - }, - { - id: 9, - name: "Grace Hopper", - age: 35, - role: "Senior Developer", - department: "Engineering", - startDate: "2019-08-22", - status: "Active", - }, - { - id: 10, - name: "Hannah Montana", - age: 22, - role: "Junior Developer", - department: "Engineering", - startDate: "2023-06-01", - status: "Active", - }, - ]; - // Handle row selection changes - const handleRowSelectionChange = ({ row, isSelected, selectedRows }: RowSelectionChangeProps) => { + const handleRowSelectionChange = ({ + row, + isSelected, + selectedRows, + }: RowSelectionChangeProps) => { const action = isSelected ? "Selected" : "Deselected"; setLastAction(`${action}: ${row.name} (ID: ${row.id})`); // Convert Set to Array for display const selectedRowsArray = Array.from(selectedRows) .map((rowId) => rows.find((r) => String(r.id) === rowId)) - .filter(Boolean) as Row[]; + .filter(Boolean) as EmployeeData[]; setSelectedRowsInfo(selectedRowsArray); }; - const handleCellClick = ({ row, colIndex, accessor, value }: CellClickProps) => { + const handleCellClick = ({ row, colIndex, accessor, value }: CellClickProps) => { console.log("Cell clicked:", { row, colIndex, accessor, value }); }; // Define headers - const headers: HeaderObject[] = [ + const headers: STColumn[] = [ { accessor: "id", label: "ID", diff --git a/src/stories/examples/Theming.tsx b/src/stories/examples/Theming.tsx index 03a2e0f98..8198b9c11 100644 --- a/src/stories/examples/Theming.tsx +++ b/src/stories/examples/Theming.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import SimpleTable from "../../components/simple-table/SimpleTable"; import CellChangeProps from "../../types/CellChangeProps"; import Theme from "../../types/Theme"; -import { generateSpaceData, SPACE_HEADERS } from "../data/space-data"; +import { generateSpaceData, SPACE_HEADERS, SpaceData } from "../data/space-data"; import { UniversalTableProps } from "./StoryWrapper"; // Default args specific to Theming - exported for reuse in stories and tests @@ -22,10 +22,10 @@ const HEADERS = SPACE_HEADERS; const THEME_OPTIONS: Theme[] = ["sky", "funky", "neutral", "light", "dark"]; const ThemingExample = (props: UniversalTableProps) => { - const [rows, setRows] = useState(EXAMPLE_DATA); + const [rows, setRows] = useState(EXAMPLE_DATA); const [theme, setTheme] = useState(props.theme ?? "light"); - const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { + const updateCell = ({ accessor, newValue, row }: CellChangeProps) => { setRows((prevRows) => { const rowIndex = prevRows.findIndex((r) => r.id === row.id); if (rowIndex !== -1) { diff --git a/src/stories/examples/billing-example/billing-headers.tsx b/src/stories/examples/billing-example/billing-headers.tsx index 8adfd5b66..4685cec50 100644 --- a/src/stories/examples/billing-example/billing-headers.tsx +++ b/src/stories/examples/billing-example/billing-headers.tsx @@ -1,10 +1,88 @@ -import { HeaderObject } from "../../.."; +import { STColumn } from "../../../types/HeaderObject"; -const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const months: ( + | "Jan" + | "Feb" + | "Mar" + | "Apr" + | "May" + | "Jun" + | "Jul" + | "Aug" + | "Sep" + | "Oct" + | "Nov" + | "Dec" +)[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +type BillingCharge = { + id: string; + type: string; + name: string; + createdDate: string; + amount: number; + recognizedRevenue: number; + deferredRevenue: number; + month_Jan_2024: number; + month_Feb_2024: number; + month_Mar_2024: number; + month_Apr_2024: number; + month_May_2024: number; + month_Jun_2024: number; + month_Jul_2024: number; + month_Aug_2024: number; + month_Sep_2024: number; + month_Oct_2024: number; + month_Nov_2024: number; + month_Dec_2024: number; + revenue_Jan_2024: number; + revenue_Feb_2024: number; + revenue_Mar_2024: number; + revenue_Apr_2024: number; + revenue_May_2024: number; + revenue_Jun_2024: number; + revenue_Jul_2024: number; + revenue_Aug_2024: number; + revenue_Sep_2024: number; + revenue_Oct_2024: number; + revenue_Nov_2024: number; + revenue_Dec_2024: number; + balance_Jan_2024: number; + balance_Feb_2024: number; + balance_Mar_2024: number; + balance_Apr_2024: number; + balance_May_2024: number; + balance_Jun_2024: number; + balance_Jul_2024: number; + balance_Aug_2024: number; + balance_Sep_2024: number; + balance_Oct_2024: number; + balance_Nov_2024: number; + balance_Dec_2024: number; +}; + +type BillingInvoice = { + id: string; + type: string; + name: string; + status: string; + createdDate: string; + dueDate: string; + charges: BillingCharge[]; +}; + +export type BillingRowData = { + id: string; + type: string; + name: string; + status: string; + createdDate: string; + invoices: BillingInvoice[]; +}; // Generate header configs for 2024 months const generateMonthHeaders = () => { - const headers: HeaderObject[] = []; + const headers: STColumn[] = []; const year = 2024; // Add all months for 2024 in reverse chronological order (Dec to Jan) @@ -12,7 +90,6 @@ const generateMonthHeaders = () => { const fullMonthName = new Date(year, monthIndex).toLocaleString("default", { month: "long" }); headers.push({ - accessor: `month_${months[monthIndex]}_${year}`, label: `${fullMonthName} ${year}`, width: 200, isSortable: true, @@ -68,7 +145,7 @@ const generateMonthHeaders = () => { }; // Main headers -export const HEADERS: HeaderObject[] = [ +export const HEADERS: STColumn[] = [ { accessor: "name", label: "Name", diff --git a/src/stories/examples/editable-example/employee-headers.tsx b/src/stories/examples/editable-example/employee-headers.tsx index 2cc7444ce..eb6334437 100644 --- a/src/stories/examples/editable-example/employee-headers.tsx +++ b/src/stories/examples/editable-example/employee-headers.tsx @@ -1,6 +1,20 @@ -import HeaderObject from "../../../types/HeaderObject"; +import { STColumn } from "../../../types/HeaderObject"; -export const EMPLOYEE_HEADERS: HeaderObject[] = [ +type EmployeeData = { + name: string; + email: string; + department: string; + position: string; + salary: number; + hireDate: string; + performanceReview: string; + rating: number; + isActive: boolean; + isRemote: boolean; + projectsCompleted: number; +}; + +export const EMPLOYEE_HEADERS: STColumn[] = [ { accessor: "name", label: "Full Name", diff --git a/src/stories/examples/filter-example/filter-data.json b/src/stories/examples/filter-example/filter-data.json index 399dc9efd..719722409 100644 --- a/src/stories/examples/filter-example/filter-data.json +++ b/src/stories/examples/filter-example/filter-data.json @@ -384,4 +384,4 @@ "isActive": true, "releaseDate": "2024-02-24" } -] \ No newline at end of file +] diff --git a/src/stories/examples/filter-example/filter-headers.tsx b/src/stories/examples/filter-example/filter-headers.tsx index b63b8e32a..c700606a8 100644 --- a/src/stories/examples/filter-example/filter-headers.tsx +++ b/src/stories/examples/filter-example/filter-headers.tsx @@ -1,7 +1,18 @@ -import Row from "../../../types/Row"; -import HeaderObject from "../../../types/HeaderObject"; +import { STColumn } from "../../../types/HeaderObject"; -export const PRODUCT_HEADERS: HeaderObject[] = [ +type Product = { + id: number; + productName: string; + category: string; + brand: string; + rating: number | "—"; + price: number | "—"; + stockLevel: number | "—"; + isActive: boolean | "—"; + releaseDate: string; +}; + +export const PRODUCT_HEADERS: STColumn[] = [ { accessor: "productName", label: "Product", @@ -12,7 +23,6 @@ export const PRODUCT_HEADERS: HeaderObject[] = [ type: "string", }, { - accessor: "details", label: "Product Details", width: 500, isSortable: false, @@ -90,7 +100,7 @@ export const PRODUCT_HEADERS: HeaderObject[] = [ align: "center", type: "number", filterable: true, - cellRenderer: ({ row }: { row: Row }) => { + cellRenderer: ({ row }: { row: Product }) => { if (row.rating === "—") return "—"; const rating = row.rating as number; const stars = "★".repeat(Math.floor(rating)) + "☆".repeat(5 - Math.floor(rating)); @@ -114,7 +124,6 @@ export const PRODUCT_HEADERS: HeaderObject[] = [ ], }, { - accessor: "pricing", label: "Pricing & Inventory", width: "1fr", isSortable: false, @@ -160,7 +169,7 @@ export const PRODUCT_HEADERS: HeaderObject[] = [ align: "center", type: "number", filterable: true, - cellRenderer: ({ row }: { row: Row }) => { + cellRenderer: ({ row }: { row: Product }) => { if (row.stockLevel === "—") return "—"; const stock = row.stockLevel as number; @@ -194,7 +203,7 @@ export const PRODUCT_HEADERS: HeaderObject[] = [ align: "center", type: "boolean", filterable: true, - cellRenderer: ({ row }: { row: Row }) => { + cellRenderer: ({ row }: { row: Product }) => { if (row.isActive === "—") return "—"; const isActive = row.isActive as boolean; return ( @@ -217,7 +226,7 @@ export const PRODUCT_HEADERS: HeaderObject[] = [ align: "center", type: "date", filterable: true, - cellRenderer: ({ row }: { row: Row }) => { + cellRenderer: ({ row }: { row: Product }) => { if (row.releaseDate === "—") return "—"; // Parse ISO date string directly to avoid timezone issues const dateString = row.releaseDate as string; diff --git a/src/stories/examples/finance-example/FinancialExample.tsx b/src/stories/examples/finance-example/FinancialExample.tsx index 8bfa9ef38..5bbdb9e80 100644 --- a/src/stories/examples/finance-example/FinancialExample.tsx +++ b/src/stories/examples/finance-example/FinancialExample.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; -import { HEADERS } from "./finance-headers"; +import { HEADERS, type FinancialRowData } from "./finance-headers"; import data from "./finance-data.json"; import TableRefType from "../../../types/TableRefType"; import SimpleTable from "../../../components/simple-table/SimpleTable"; @@ -15,7 +15,7 @@ export const financeExampleDefaults = { }; export const FinancialExample = (props: UniversalTableProps) => { - const tableRef = useRef(null); + const tableRef = useRef | null>(null); useEffect(() => { const interval = setInterval(() => { diff --git a/src/stories/examples/finance-example/finance-data.json b/src/stories/examples/finance-example/finance-data.json index f1957ae1b..61583a0f5 100644 --- a/src/stories/examples/finance-example/finance-data.json +++ b/src/stories/examples/finance-example/finance-data.json @@ -870,4 +870,4 @@ "date": "2025-05-30", "isFollowed": true } -] \ No newline at end of file +] diff --git a/src/stories/examples/finance-example/finance-headers.tsx b/src/stories/examples/finance-example/finance-headers.tsx index c9800816a..7804cccaf 100644 --- a/src/stories/examples/finance-example/finance-headers.tsx +++ b/src/stories/examples/finance-example/finance-headers.tsx @@ -1,6 +1,20 @@ -import HeaderObject from "../../../types/HeaderObject"; +import { STColumn } from "../../../types/HeaderObject"; -export const HEADERS: HeaderObject[] = [ +export type FinancialRowData = { + id: number; + ticker: string; + companyName: string; + price: number | "—"; + priceChangePercent: number | "—"; + marketCap: number | "—"; + peRatio: number | "—"; + dividendYield: number | null | "—"; + analystRating: string; + date: string; + isFollowed: boolean; +}; + +export const HEADERS: STColumn[] = [ { accessor: "ticker", align: "left", @@ -25,7 +39,6 @@ export const HEADERS: HeaderObject[] = [ width: "2fr", }, { - accessor: "priceMetrics", label: "Price Performance", width: 250, isSortable: false, @@ -74,7 +87,6 @@ export const HEADERS: HeaderObject[] = [ ], }, { - accessor: "fundamentals", label: "Fundamentals", width: 380, isSortable: false, @@ -120,7 +132,6 @@ export const HEADERS: HeaderObject[] = [ ], }, { - accessor: "analystInfo", label: "Analyst Information", width: 380, isSortable: false, diff --git a/src/stories/examples/manufacturing/manufacturing-headers.tsx b/src/stories/examples/manufacturing/manufacturing-headers.tsx index fd4496bee..b971c631e 100644 --- a/src/stories/examples/manufacturing/manufacturing-headers.tsx +++ b/src/stories/examples/manufacturing/manufacturing-headers.tsx @@ -1,4 +1,4 @@ -import HeaderObject from "../../../types/HeaderObject"; +import { STColumn } from "../../../types/HeaderObject"; // Custom Tag component const Tag = ({ @@ -112,7 +112,50 @@ const Progress = ({ ); }; -export const HEADERS: HeaderObject[] = [ +export type ManufacturingStation = { + id: string; + productLine: string; + station: string; + machineType: string; + operator: string; + productType: string; + outputRate: number; + cycletime: string; + efficiency: number; + defectRate: string; + defectCount: number; + downtime: string; + utilization: number; + energy: number; + status: string; + maintenanceDate: string; + cycleTimeData: string; +}; + +export type ManufacturingProductionLine = { + id: string; + productLine: string; + stations: ManufacturingStation[]; + // Computed aggregated fields for table display + station?: string; + machineType?: string; + operator?: string; + productType?: string; + outputRate?: number; + cycletime?: number; + efficiency?: number; + defectRate?: string; + defectCount?: number; + downtime?: string; + utilization?: number; + energy?: number; + status?: string; + maintenanceDate?: string; +}; + +export type ManufacturingRowData = ManufacturingStation | ManufacturingProductionLine; + +export const HEADERS: STColumn[] = [ { accessor: "productLine", label: "Production Line", @@ -223,7 +266,7 @@ export const HEADERS: HeaderObject[] = [ const value = row.cycletime as number; return {value?.toFixed(1)}; } - return {row.cycletime as string}; + return {row.cycletime.toFixed(1)}; }, }, { @@ -290,7 +333,7 @@ export const HEADERS: HeaderObject[] = [ cellRenderer: ({ row }) => { const hasChildren = row.stations && Array.isArray(row.stations); if (hasChildren) { - const rate = row.defectRate as number; + const rate = parseFloat(row.defectRate); const color = rate < 1 ? "text-green-600" : rate < 3 ? "text-orange-500" : "text-red-600"; return {rate?.toFixed(2)}%; } @@ -326,7 +369,7 @@ export const HEADERS: HeaderObject[] = [ cellRenderer: ({ row }) => { const hasChildren = row.stations && Array.isArray(row.stations); if (hasChildren) { - const hours = row.downtime as number; + const hours = parseFloat(row.downtime); const color = hours < 1 ? "text-green-600" : hours < 2 ? "text-orange-500" : "text-red-600"; return {hours?.toFixed(2)}; } diff --git a/src/stories/examples/pinned-columns/PinnedColumnsUtil.ts b/src/stories/examples/pinned-columns/PinnedColumnsUtil.ts index 18286dfc0..2210e167a 100644 --- a/src/stories/examples/pinned-columns/PinnedColumnsUtil.ts +++ b/src/stories/examples/pinned-columns/PinnedColumnsUtil.ts @@ -1,6 +1,22 @@ -import HeaderObject from "../../../types/HeaderObject"; +import { STColumn } from "../../../types/HeaderObject"; -export const SAMPLE_HEADERS: HeaderObject[] = [ +type RowData = { + productName: string; + category: string; + quantity: number; + price: number; + supplier: string; + location: string; + reorderLevel: number; + sku: string; + description: string; + weight: number; + dimensions: string; + barcode: string; + expirationDate: string; + manufacturer: string; +}; +export const SAMPLE_HEADERS: STColumn[] = [ { accessor: "productName", label: "Product Name", diff --git a/src/stories/examples/row-grouping/RowGrouping.tsx b/src/stories/examples/row-grouping/RowGrouping.tsx index 0802de3c0..8746ec6b1 100644 --- a/src/stories/examples/row-grouping/RowGrouping.tsx +++ b/src/stories/examples/row-grouping/RowGrouping.tsx @@ -1,6 +1,6 @@ import SimpleTable from "../../../components/simple-table/SimpleTable"; -import { HeaderObject } from "../../.."; import { UniversalTableProps } from "../StoryWrapper"; +import { STColumn } from "../../../types/HeaderObject"; // Default args specific to RowGrouping - exported for reuse in stories and tests export const rowGroupingDefaults = { @@ -8,7 +8,42 @@ export const rowGroupingDefaults = { height: "calc(100dvh - 112px)", }; -const headers: HeaderObject[] = [ +type RowGroupingRowData = { + id: number; + organization: string; + performance: string; + location: string; + growthRate: string; + status: string; + established: string; + divisions: { + id: number; + organization: string; + performance: string; + location: string; + growthRate: string; + status: string; + established: string; + teams: { + id: number; + organization: string; + employees: number; + budget: string; + rating?: number; + projectCount?: number; + minTeamSize?: number; + maxTeamSize?: number; + weightedScore?: number; + performance: string; + location: string; + status: string; + growthRate?: string; + established?: string; + }[]; + }[]; +}; + +const headers: STColumn[] = [ { accessor: "organization", label: "Organization", width: 200, expandable: true, type: "string" }, { accessor: "employees", diff --git a/src/stories/examples/sales-example/sales-headers.tsx b/src/stories/examples/sales-example/sales-headers.tsx index e77401ef5..34bb7ed00 100644 --- a/src/stories/examples/sales-example/sales-headers.tsx +++ b/src/stories/examples/sales-example/sales-headers.tsx @@ -1,7 +1,19 @@ -import Row from "../../../types/Row"; -import HeaderObject from "../../../types/HeaderObject"; +import { STColumn } from "../../../types/HeaderObject"; -export const SALES_HEADERS: HeaderObject[] = [ +type SalesRow = { + id: number; + repName: string; + dealSize: number | "—"; + isWon: boolean; + commission: number; + dealProfit: number | "—"; + dealValue: number | "—"; + profitMargin: number | "—"; + closeDate: string; + category: string; +}; + +export const SALES_HEADERS: STColumn[] = [ { accessor: "repName", label: "Sales Representative", @@ -10,7 +22,6 @@ export const SALES_HEADERS: HeaderObject[] = [ isSortable: true, }, { - accessor: "salesMetrics", label: "Sales Metrics", width: 460, isSortable: false, @@ -23,7 +34,7 @@ export const SALES_HEADERS: HeaderObject[] = [ isEditable: false, align: "right", type: "number", - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: { row: SalesRow }) => { if (row.dealSize === "—") return "—"; return `$${(row.dealSize as number).toLocaleString("en-US", { minimumFractionDigits: 2, @@ -69,8 +80,7 @@ export const SALES_HEADERS: HeaderObject[] = [ isEditable: false, align: "center", type: "boolean", - cellRenderer: ({ row }: { row: Row }) => { - if (row.isWon === "—") return "—"; + cellRenderer: ({ row }: { row: SalesRow }) => { const isWon = row.isWon as boolean; return isWon ? "Won" : "Lost"; }, @@ -79,7 +89,6 @@ export const SALES_HEADERS: HeaderObject[] = [ }, { - accessor: "financialMetrics", label: "Financial Metrics", width: "1fr", isSortable: false, @@ -92,8 +101,7 @@ export const SALES_HEADERS: HeaderObject[] = [ isEditable: false, align: "right", type: "number", - cellRenderer: ({ row }: { row: Row }) => { - if (row.commission === "—") return "—"; + cellRenderer: ({ row }: { row: SalesRow }) => { const value = row.commission as number; if (value === 0) return $0.00; @@ -111,8 +119,7 @@ export const SALES_HEADERS: HeaderObject[] = [ isEditable: false, align: "right", type: "number", - cellRenderer: ({ row }: { row: Row }) => { - if (row.profitMargin === "—") return "—"; + cellRenderer: ({ row }: { row: SalesRow }) => { const value = row.profitMargin as number; // Enhanced color coding based on profit margin tiers @@ -138,7 +145,7 @@ export const SALES_HEADERS: HeaderObject[] = [ isEditable: false, align: "right", type: "number", - cellRenderer: ({ row }: { row: Row }) => { + cellRenderer: ({ row }: { row: SalesRow }) => { if (row.dealProfit === "—") return "—"; const value = row.dealProfit as number; if (value === 0) return $0.00; diff --git a/src/stories/test-utils/aggregationTestUtils.ts b/src/stories/test-utils/aggregationTestUtils.ts index 2bf2e2710..8764afe08 100644 --- a/src/stories/test-utils/aggregationTestUtils.ts +++ b/src/stories/test-utils/aggregationTestUtils.ts @@ -1,6 +1,5 @@ import { expect } from "@storybook/test"; import { waitForTable } from "./commonTestUtils"; -import { Accessor } from "../../types/HeaderObject"; /** * Aggregation Test Utilities for RowGrouping Example @@ -73,7 +72,7 @@ export const ensureRowExpanded = async ( export const getAggregatedValue = ( canvasElement: HTMLElement, rowText: string, - columnAccessor: Accessor + columnAccessor: string ): string | null => { // Find the row that contains the specified text const rows = canvasElement.querySelectorAll(".st-row"); diff --git a/src/stories/test-utils/filterTestUtils.ts b/src/stories/test-utils/filterTestUtils.ts index 1b02fa439..43da20156 100644 --- a/src/stories/test-utils/filterTestUtils.ts +++ b/src/stories/test-utils/filterTestUtils.ts @@ -1,7 +1,6 @@ import { expect } from "@storybook/test"; import { waitForTable } from "./commonTestUtils"; import { PRODUCT_HEADERS } from "../examples/filter-example/filter-headers"; -import HeaderObject, { Accessor } from "../../types/HeaderObject"; /** * Filter Test Utilities for FilterExample @@ -12,12 +11,12 @@ import HeaderObject, { Accessor } from "../../types/HeaderObject"; */ export const getFilterableColumnsFromHeaders = (): Array<{ label: string; - accessor: Accessor; + accessor: string; type: string; }> => { - const filterableColumns: Array<{ label: string; accessor: Accessor; type: string }> = []; + const filterableColumns: Array<{ label: string; accessor: string; type: string }> = []; - const extractFilterableColumns = (headers: HeaderObject[]): void => { + const extractFilterableColumns = (headers: any): void => { for (const header of headers) { if (header.filterable && header.type) { filterableColumns.push({ @@ -79,7 +78,7 @@ export const getPriceDisplayText = (price: number): string => { */ export const extractNumericValueFromDisplay = ( displayText: string, - columnAccessor: Accessor + columnAccessor: string ): number | null => { switch (columnAccessor) { case "stockLevel": @@ -125,7 +124,7 @@ export const doesDisplayValueMatchNumericCondition = ( displayText: string, operator: string, targetValue: number, - columnAccessor: Accessor + columnAccessor: string ): boolean => { const actualValue = extractNumericValueFromDisplay(displayText, columnAccessor); @@ -1210,7 +1209,7 @@ export const getVisibleRowCount = (canvasElement: HTMLElement): number => { /** * Get visible row data for a specific column */ -export const getVisibleColumnData = (canvasElement: HTMLElement, accessor: Accessor): string[] => { +export const getVisibleColumnData = (canvasElement: HTMLElement, accessor: string): string[] => { const cells = canvasElement.querySelectorAll(`[data-accessor="${accessor}"] .st-cell-content`); const data = Array.from(cells).map((cell) => cell.textContent?.trim() || ""); return data; diff --git a/src/stories/test-utils/liveUpdatesTestUtils.ts b/src/stories/test-utils/liveUpdatesTestUtils.ts index 928dd5ec1..94cb0d8a6 100644 --- a/src/stories/test-utils/liveUpdatesTestUtils.ts +++ b/src/stories/test-utils/liveUpdatesTestUtils.ts @@ -1,12 +1,11 @@ import { expect } from "@storybook/test"; -import { Accessor } from "../../types/HeaderObject"; // Assertion helpers -export const getCellElement = (rowIndex: number, accessor: Accessor): Element | null => { +export const getCellElement = (rowIndex: number, accessor: string): Element | null => { return document.querySelector(`[data-row-index="${rowIndex}"][data-accessor="${accessor}"]`); }; -export const getCellValue = (rowIndex: number, accessor: Accessor): string | null => { +export const getCellValue = (rowIndex: number, accessor: string): string | null => { const cell = getCellElement(rowIndex, accessor); if (!cell) return null; @@ -14,7 +13,7 @@ export const getCellValue = (rowIndex: number, accessor: Accessor): string | nul return contentSpan?.textContent || null; }; -export const hasCellUpdatingClass = (rowIndex: number, accessor: Accessor): boolean => { +export const hasCellUpdatingClass = (rowIndex: number, accessor: string): boolean => { const cell = getCellElement(rowIndex, accessor); return cell?.classList.contains("st-cell-updating") || false; }; diff --git a/src/stories/tests/CellSelectionTests.stories.ts b/src/stories/tests/CellSelectionTests.stories.ts index e34ed61c3..f325d4e2d 100644 --- a/src/stories/tests/CellSelectionTests.stories.ts +++ b/src/stories/tests/CellSelectionTests.stories.ts @@ -52,7 +52,6 @@ export const ComprehensiveCellSelectionTests: StoryObj = { try { // Run the comprehensive test suite await testCellSelectionComprehensive(canvasElement); - return; // Additional specific tests with multiple expect statements diff --git a/src/types/CellChangeProps.ts b/src/types/CellChangeProps.ts index 921a41d0f..bd41a6293 100644 --- a/src/types/CellChangeProps.ts +++ b/src/types/CellChangeProps.ts @@ -1,11 +1,9 @@ -import CellValue from "./CellValue"; import { Accessor } from "./HeaderObject"; -import Row from "./Row"; -type CellChangeProps = { - accessor: Accessor; - newValue: CellValue; - row: Row; +type CellChangeProps = { + accessor: Accessor; + newValue: any; + row: T; }; export default CellChangeProps; diff --git a/src/types/CellClickProps.ts b/src/types/CellClickProps.ts index da7e8ae38..4e112b9a4 100644 --- a/src/types/CellClickProps.ts +++ b/src/types/CellClickProps.ts @@ -1,15 +1,13 @@ -import CellValue from "./CellValue"; import { Accessor } from "./HeaderObject"; -import Row from "./Row"; import { RowId } from "./RowId"; -type CellClickProps = { - accessor: Accessor; +type CellClickProps = { + accessor: Accessor; colIndex: number; - row: Row; + row: T; rowId: RowId; rowIndex: number; - value: CellValue; + value: any; }; export default CellClickProps; diff --git a/src/types/CellRendererProps.ts b/src/types/CellRendererProps.ts new file mode 100644 index 000000000..8f2645fb5 --- /dev/null +++ b/src/types/CellRendererProps.ts @@ -0,0 +1,11 @@ +import { Accessor } from "./HeaderObject"; +import Theme from "./Theme"; + +type CellRendererProps = { + accessor: Accessor; + colIndex: number; + row: any; + theme: Theme; +}; + +export default CellRendererProps; diff --git a/src/types/CellValue.ts b/src/types/CellValue.ts deleted file mode 100644 index 49ad425cd..000000000 --- a/src/types/CellValue.ts +++ /dev/null @@ -1,11 +0,0 @@ -type CellValue = - | string - | number - | boolean - | undefined - | null - | string[] - | number[] - | Record[]; - -export default CellValue; diff --git a/src/types/DragHandlerProps.ts b/src/types/DragHandlerProps.ts index 32376abd4..c98abcc6f 100644 --- a/src/types/DragHandlerProps.ts +++ b/src/types/DragHandlerProps.ts @@ -1,12 +1,12 @@ -import { MutableRefObject, Dispatch, SetStateAction } from "react"; -import HeaderObject from "./HeaderObject"; +import { MutableRefObject } from "react"; +import HeaderObject, { AggregatedRow } from "./HeaderObject"; -type useDragHandlerProps = { - draggedHeaderRef: MutableRefObject; - headers: HeaderObject[]; - hoveredHeaderRef: MutableRefObject; - onColumnOrderChange?: (newHeaders: HeaderObject[]) => void; - onTableHeaderDragEnd: (newHeaders: HeaderObject[]) => void; +type useDragHandlerProps = { + draggedHeaderRef: MutableRefObject> | null>; + headers: HeaderObject>[]; + hoveredHeaderRef: MutableRefObject> | null>; + onColumnOrderChange?: (newHeaders: HeaderObject>[]) => void; + onTableHeaderDragEnd: (newHeaders: HeaderObject>[]) => void; }; export default useDragHandlerProps; diff --git a/src/types/FilterTypes.ts b/src/types/FilterTypes.ts index 21f320ebb..4da12f55f 100644 --- a/src/types/FilterTypes.ts +++ b/src/types/FilterTypes.ts @@ -1,4 +1,3 @@ -import CellValue from "./CellValue"; import { Accessor } from "./HeaderObject"; // Filter operators for different data types @@ -47,17 +46,17 @@ export type FilterOperator = | EnumFilterOperator; // Filter condition interface -export interface FilterCondition { - accessor: Accessor; +export interface FilterCondition { + accessor: Accessor; operator: FilterOperator; - value?: CellValue; - values?: CellValue[]; // For operators like 'between', 'in', etc. + value?: any; + values?: any[]; // For operators like 'between', 'in', etc. } // Filter state for the entire table -export interface TableFilterState { - [accessor: Accessor]: FilterCondition; -} +export type TableFilterState = { + [accessor: string]: FilterCondition; +}; // Human-readable labels for filter operators export const FILTER_OPERATOR_LABELS: Record = { diff --git a/src/types/HandleResizeStartProps.ts b/src/types/HandleResizeStartProps.ts index 1d1ed1bf2..2615f579f 100644 --- a/src/types/HandleResizeStartProps.ts +++ b/src/types/HandleResizeStartProps.ts @@ -1,14 +1,14 @@ import { Dispatch, RefObject, SetStateAction, TouchEvent } from "react"; import { HeaderObject } from ".."; -export type HandleResizeStartProps = { +export type HandleResizeStartProps = { event: MouseEvent | TouchEvent; forceUpdate: () => void; gridColumnEnd: number; gridColumnStart: number; - header: HeaderObject; - headers: HeaderObject[]; - setHeaders: Dispatch>; + header: HeaderObject; + headers: HeaderObject[]; + setHeaders: Dispatch[]>>; setIsResizing: Dispatch>; mainBodyRef: RefObject; pinnedLeftRef: RefObject; diff --git a/src/types/HeaderObject.ts b/src/types/HeaderObject.ts index 671329b0e..351c502e5 100644 --- a/src/types/HeaderObject.ts +++ b/src/types/HeaderObject.ts @@ -1,28 +1,47 @@ import { ReactNode } from "react"; -import Row from "./Row"; import { Pinned } from "./Pinned"; -import Theme from "./Theme"; import EnumOption from "./EnumOption"; import { AggregationConfig } from "./AggregationTypes"; +import CellRendererProps from "./CellRendererProps"; +import HeaderRendererProps from "./HeaderRendererProps"; -export type Accessor = keyof Row; +// Helper type to extract keys from nested array objects +type NestedArrayObjectKeys = T extends readonly (infer U)[] + ? U extends object + ? keyof U | NestedArrayObjectKeys + : never + : T extends object + ? NestedArrayObjectKeys + : never; + +// Helper type to extract all possible field types from nested structures +type NestedFieldTypes = T extends readonly (infer U)[] + ? U extends object + ? U[keyof U] | NestedFieldTypes + : never + : T extends object + ? NestedFieldTypes + : never; + +// Type representing what a row looks like after aggregation processing +export type AggregatedRow = T & { + [K in NestedArrayObjectKeys]?: NestedFieldTypes; +}; + +// Flattened union type that includes keys from the main type and nested objects +export type Accessor = keyof T | NestedArrayObjectKeys; export type ColumnType = "string" | "number" | "boolean" | "date" | "enum" | "other"; -type HeaderObject = { - accessor: Accessor; +type HeaderObject = { + accessor?: Accessor; aggregation?: AggregationConfig; align?: "left" | "center" | "right"; cellRenderer?: ({ accessor, colIndex, row, - }: { - accessor: Accessor; - colIndex: number; - row: Row; - theme: Theme; - }) => ReactNode | string; - children?: HeaderObject[]; + }: CellRendererProps>) => ReactNode | string; + children?: HeaderObject[]; disableReorder?: boolean; enumOptions?: EnumOption[]; expandable?: boolean; @@ -31,12 +50,9 @@ type HeaderObject = { accessor, colIndex, header, - }: { - accessor: Accessor; - colIndex: number; - header: HeaderObject; - }) => ReactNode | string; + }: HeaderRendererProps>) => ReactNode | string; hide?: boolean; + id: string; isEditable?: boolean; isSelectionColumn?: boolean; isSortable?: boolean; @@ -48,4 +64,9 @@ type HeaderObject = { maxWidth?: number | string; }; +// Header object without id +export type STColumn = Omit, "id" | "children"> & { + children?: STColumn[]; +}; + export default HeaderObject; diff --git a/src/types/HeaderRendererProps.ts b/src/types/HeaderRendererProps.ts new file mode 100644 index 000000000..e79204ffd --- /dev/null +++ b/src/types/HeaderRendererProps.ts @@ -0,0 +1,10 @@ +import { Accessor } from "./HeaderObject"; +import HeaderObject from "./HeaderObject"; + +type HeaderRendererProps = { + accessor?: Accessor; + colIndex: number; + header: HeaderObject; +}; + +export default HeaderRendererProps; diff --git a/src/types/OnSortProps.ts b/src/types/OnSortProps.ts index 04e46579f..08927ecb7 100644 --- a/src/types/OnSortProps.ts +++ b/src/types/OnSortProps.ts @@ -1,5 +1,5 @@ import { Accessor } from "./HeaderObject"; -type OnSortProps = (accessor: Accessor) => void; +type OnSortProps = (accessor: Accessor) => void; export default OnSortProps; diff --git a/src/types/Row.ts b/src/types/Row.ts deleted file mode 100644 index 111a39c4d..000000000 --- a/src/types/Row.ts +++ /dev/null @@ -1,7 +0,0 @@ -import CellValue from "./CellValue"; - -// Row is now just a record of data - users can put whatever they want in it -// The table will use rowGrouping prop to understand how to group/expand rows -type Row = Record; - -export default Row; diff --git a/src/types/RowGrouping.ts b/src/types/RowGrouping.ts new file mode 100644 index 000000000..e311ba8dc --- /dev/null +++ b/src/types/RowGrouping.ts @@ -0,0 +1,3 @@ +type RowGrouping = string[]; + +export default RowGrouping; diff --git a/src/types/RowSelectionChangeProps.ts b/src/types/RowSelectionChangeProps.ts index ad44e6861..0acf4ca16 100644 --- a/src/types/RowSelectionChangeProps.ts +++ b/src/types/RowSelectionChangeProps.ts @@ -1,7 +1,5 @@ -import Row from "./Row"; - -type RowSelectionChangeProps = { - row: Row; +type RowSelectionChangeProps = { + row: T; isSelected: boolean; selectedRows: Set; }; diff --git a/src/types/SharedTableProps.ts b/src/types/SharedTableProps.ts index 1e295f309..093024e4a 100644 --- a/src/types/SharedTableProps.ts +++ b/src/types/SharedTableProps.ts @@ -1,20 +1,20 @@ import { RefObject, MutableRefObject } from "react"; import HeaderObject from "./HeaderObject"; -interface SharedTableProps { +interface SharedTableProps { allowAnimations: boolean; centerHeaderRef: RefObject; - draggedHeaderRef: MutableRefObject; + draggedHeaderRef: MutableRefObject | null>; headerContainerRef: RefObject; - headers: HeaderObject[]; - hoveredHeaderRef: MutableRefObject; + headers: HeaderObject[]; + hoveredHeaderRef: MutableRefObject | null>; mainBodyRef: RefObject; mainTemplateColumns: string; - onTableHeaderDragEnd: (newHeaders: HeaderObject[]) => void; - pinnedLeftColumns: HeaderObject[]; + onTableHeaderDragEnd: (newHeaders: HeaderObject[]) => void; + pinnedLeftColumns: HeaderObject[]; pinnedLeftHeaderRef: RefObject; pinnedLeftTemplateColumns: string; - pinnedRightColumns: HeaderObject[]; + pinnedRightColumns: HeaderObject[]; pinnedRightHeaderRef: RefObject; pinnedRightTemplateColumns: string; rowHeight: number; diff --git a/src/types/SortColumn.ts b/src/types/SortColumn.ts index 8d760f4b4..4b8578cf4 100644 --- a/src/types/SortColumn.ts +++ b/src/types/SortColumn.ts @@ -1,8 +1,8 @@ import HeaderObject from "./HeaderObject"; // Type for a single sort column -type SortColumn = { - key: HeaderObject; +type SortColumn = { + key: HeaderObject; direction: "ascending" | "descending"; }; diff --git a/src/types/TableBodyProps.ts b/src/types/TableBodyProps.ts index e8fb166d9..94f1071d3 100644 --- a/src/types/TableBodyProps.ts +++ b/src/types/TableBodyProps.ts @@ -1,20 +1,19 @@ -import { RefObject } from "react"; import { HeaderObject } from ".."; import { Dispatch } from "react"; import { SetStateAction } from "react"; import TableRow from "./TableRow"; -interface TableBodyProps { +interface TableBodyProps { mainTemplateColumns: string; - pinnedLeftColumns: HeaderObject[]; + pinnedLeftColumns: HeaderObject[]; pinnedLeftTemplateColumns: string; pinnedLeftWidth: number; - pinnedRightColumns: HeaderObject[]; + pinnedRightColumns: HeaderObject[]; pinnedRightTemplateColumns: string; pinnedRightWidth: number; - rowsToRender: TableRow[]; + rowsToRender: TableRow[]; setScrollTop: Dispatch>; - tableRows: TableRow[]; + tableRows: TableRow[]; } export default TableBodyProps; diff --git a/src/types/TableCellProps.ts b/src/types/TableCellProps.ts index 1cc22b3f1..4c51d92d4 100644 --- a/src/types/TableCellProps.ts +++ b/src/types/TableCellProps.ts @@ -1,16 +1,15 @@ import HeaderObject from "./HeaderObject"; import TableRow from "./TableRow"; -import Cell from "./Cell"; -export interface TableCellProps { +export interface TableCellProps { borderClass?: string; colIndex: number; - header: HeaderObject; + header: HeaderObject; isHighlighted?: boolean; isInitialFocused?: boolean; nestedIndex: number; rowIndex: number; - tableRow: TableRow; + tableRow: TableRow; } export default TableCellProps; diff --git a/src/types/TableHeaderProps.ts b/src/types/TableHeaderProps.ts index 0e34d762b..c9b2f1cf9 100644 --- a/src/types/TableHeaderProps.ts +++ b/src/types/TableHeaderProps.ts @@ -2,15 +2,15 @@ import { RefObject } from "react"; import SortColumn from "./SortColumn"; import HeaderObject from "./HeaderObject"; -type TableHeaderProps = { +type TableHeaderProps = { centerHeaderRef: RefObject; - headers: HeaderObject[]; + headers: HeaderObject[]; mainTemplateColumns: string; - pinnedLeftColumns: HeaderObject[]; + pinnedLeftColumns: HeaderObject[]; pinnedLeftTemplateColumns: string; - pinnedRightColumns: HeaderObject[]; + pinnedRightColumns: HeaderObject[]; pinnedRightTemplateColumns: string; - sort: SortColumn | null; + sort: SortColumn | null; pinnedLeftWidth: number; pinnedRightWidth: number; }; diff --git a/src/types/TableHeaderSectionProps.ts b/src/types/TableHeaderSectionProps.ts index 554b32694..fbcc6ea00 100644 --- a/src/types/TableHeaderSectionProps.ts +++ b/src/types/TableHeaderSectionProps.ts @@ -3,17 +3,17 @@ import { Pinned } from "./Pinned"; import SortColumn from "./SortColumn"; import { HeaderObject } from ".."; import { RefObject } from "react"; -import { ColumnIndices } from "../utils/columnIndicesUtils"; +import ColumnIndices from "./ColumnIndices"; -interface TableHeaderSectionProps { +interface TableHeaderSectionProps { columnIndices: ColumnIndices; gridTemplateColumns: string; handleScroll?: UIEventHandler; - headers: HeaderObject[]; + headers: HeaderObject[]; maxDepth: number; pinned?: Pinned; sectionRef: RefObject; - sort: SortColumn | null; + sort: SortColumn | null; width?: number; } diff --git a/src/types/TableRefType.ts b/src/types/TableRefType.ts index dffb06558..07654ddcc 100644 --- a/src/types/TableRefType.ts +++ b/src/types/TableRefType.ts @@ -1,7 +1,7 @@ -import UpdateDataProps from "./UpdateCellProps"; +import UpdateCellProps from "./UpdateCellProps"; -type TableRefType = { - updateData: (props: UpdateDataProps) => void; +type TableRefType = { + updateData: (props: UpdateCellProps) => void; }; export default TableRefType; diff --git a/src/types/TableRow.ts b/src/types/TableRow.ts index f5b4b563e..2137a578f 100644 --- a/src/types/TableRow.ts +++ b/src/types/TableRow.ts @@ -1,11 +1,9 @@ -import Row from "./Row"; - -type TableRow = { +type TableRow = { depth: number; - groupingKey?: string; + groupingKey?: keyof T; isLastGroupRow: boolean; position: number; - row: Row; + row: T; }; export default TableRow; diff --git a/src/types/TableRowProps.ts b/src/types/TableRowProps.ts index 33298c69f..b981e603b 100644 --- a/src/types/TableRowProps.ts +++ b/src/types/TableRowProps.ts @@ -1,24 +1,23 @@ import { MutableRefObject } from "react"; import CellChangeProps from "./CellChangeProps"; import HeaderObject from "./HeaderObject"; -import Row from "./Row"; import Cell from "./Cell"; -type TableRowProps = { +type TableRowProps = { allowAnimations: boolean; currentRows: { [key: string]: any }[]; - draggedHeaderRef: MutableRefObject; + draggedHeaderRef: MutableRefObject | null>; getBorderClass: (rowIndex: number, columnIndex: number) => string; handleMouseDown: (props: Cell) => void; handleMouseOver: (rowIndex: number, columnIndex: number) => void; - headers: HeaderObject[]; - hoveredHeaderRef: MutableRefObject; + headers: HeaderObject[]; + hoveredHeaderRef: MutableRefObject | null>; isSelected: (rowIndex: number, columnIndex: number) => boolean; isInitialFocusedCell: (rowIndex: number, columnIndex: number) => boolean; - onCellEdit?: (props: CellChangeProps) => void; - onTableHeaderDragEnd: (newHeaders: HeaderObject[]) => void; + onCellEdit?: (props: CellChangeProps) => void; + onTableHeaderDragEnd: (newHeaders: HeaderObject[]) => void; onToggleGroup: (rowId: number) => void; - row: Row; + row: T; rowIndex: number; shouldPaginate: boolean; tableRef: MutableRefObject; diff --git a/src/types/UpdateCellProps.ts b/src/types/UpdateCellProps.ts index ca93b5657..2d7e1adeb 100644 --- a/src/types/UpdateCellProps.ts +++ b/src/types/UpdateCellProps.ts @@ -1,10 +1,9 @@ -import { CellValue } from ".."; import { Accessor } from "./HeaderObject"; -type UpdateDataProps = { - accessor: Accessor; +type UpdateCellProps = { + accessor: Accessor; rowIndex: number; - newValue: CellValue; + newValue: any; }; -export default UpdateDataProps; +export default UpdateCellProps; diff --git a/src/utils/cellUtils.ts b/src/utils/cellUtils.ts index d63a77cd1..89b50d68f 100644 --- a/src/utils/cellUtils.ts +++ b/src/utils/cellUtils.ts @@ -2,16 +2,22 @@ import HeaderObject, { Accessor } from "../types/HeaderObject"; import { Pinned } from "../types/Pinned"; import { RowId } from "../types/RowId"; -export const getCellId = ({ accessor, rowId }: { accessor: Accessor; rowId: RowId }) => { - return `${rowId}-${accessor}`; +export const getCellId = ({ headerId, rowId }: { headerId: string; rowId: RowId }) => { + return `${rowId}-${headerId}`; }; -export const displayCell = ({ header, pinned }: { header: HeaderObject; pinned?: Pinned }) => { +export const displayCell = ({ + header, + pinned, +}: { + header: HeaderObject; + pinned?: Pinned; +}) => { if (header.hide) return null; else if ((pinned || header.pinned) && header.pinned !== pinned) return null; return true; }; -export const getCellKey = ({ rowId, accessor }: { rowId: RowId; accessor: Accessor }) => { - return `${rowId}-${accessor}`; +export const getCellKey = ({ rowId, accessor }: { rowId: RowId; accessor: Accessor }) => { + return `${rowId}-${String(accessor)}`; }; diff --git a/src/utils/columnIndicesUtils.ts b/src/utils/columnIndicesUtils.ts index c373b8604..084ee710e 100644 --- a/src/utils/columnIndicesUtils.ts +++ b/src/utils/columnIndicesUtils.ts @@ -1,8 +1,7 @@ +import ColumnIndices from "../types/ColumnIndices"; import HeaderObject from "../types/HeaderObject"; import { displayCell } from "./cellUtils"; -export type ColumnIndices = Record; - /** * Calculates column indices for all headers to ensure consistent colIndex values * This function is used in both TableBody and TableHeader components @@ -10,26 +9,25 @@ export type ColumnIndices = Record; * Note: In hierarchical headers, a parent header and its first child can share * the same column index, which is needed for proper alignment in the grid. */ -export function calculateColumnIndices({ +export function calculateColumnIndices({ headers, pinnedLeftColumns, pinnedRightColumns, }: { - headers: HeaderObject[]; - pinnedLeftColumns: HeaderObject[]; - pinnedRightColumns: HeaderObject[]; + headers: HeaderObject[]; + pinnedLeftColumns: HeaderObject[]; + pinnedRightColumns: HeaderObject[]; }): ColumnIndices { const indices: ColumnIndices = {}; let columnCounter = 0; - const processHeader = (header: HeaderObject, isFirst: boolean = false): void => { + const processHeader = (header: HeaderObject, isFirst: boolean = false): void => { // Only increment for non-first children or top-level headers if (!isFirst) { columnCounter++; } - // Store the column index for this header - indices[header.accessor] = columnCounter; + indices[header.id] = columnCounter; // Process children recursively, if any if (header.children && header.children.length > 0) { diff --git a/src/utils/columnUtils.ts b/src/utils/columnUtils.ts index f3c00fb32..a579b813c 100644 --- a/src/utils/columnUtils.ts +++ b/src/utils/columnUtils.ts @@ -1,6 +1,25 @@ -import HeaderObject from "../types/HeaderObject"; +import HeaderObject, { STColumn } from "../types/HeaderObject"; -const getColumnWidth = (header: HeaderObject) => { +export function generateColumnId( + header: STColumn, + path: string[] = [], + initialIndex: number = 0 +): string { + // Use accessor if available + if (header.accessor) { + return String(header.accessor); + } + + // Generate stable ID from path and label + const pathPrefix = path.length > 0 ? path.join("-") + "-" : ""; + const labelSlug = header.label + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + + return `${pathPrefix}${labelSlug}-${initialIndex}`; +} +const getColumnWidth = (header: HeaderObject) => { let { minWidth, width } = header; if (typeof width === "number") { @@ -20,15 +39,15 @@ const getColumnWidth = (header: HeaderObject) => { return width; }; -export const createGridTemplateColumns = ({ headers }: { headers: HeaderObject[] }) => { +export const createGridTemplateColumns = ({ headers }: { headers: HeaderObject[] }) => { // We only care about the most children headers to create the grid template columns const flattenHeaders = ({ headers, flattenedHeaders, }: { - headers: HeaderObject[]; - flattenedHeaders: HeaderObject[]; - }): HeaderObject[] => { + headers: HeaderObject[]; + flattenedHeaders: HeaderObject[]; + }): HeaderObject[] => { headers.forEach((header) => { if (header.hide) return; if (header.children) { diff --git a/src/utils/filterUtils.ts b/src/utils/filterUtils.ts index 062755f1a..5198f6fb2 100644 --- a/src/utils/filterUtils.ts +++ b/src/utils/filterUtils.ts @@ -10,7 +10,7 @@ const normalizeDate = (date: Date): Date => { /** * Applies a filter condition to a cell value */ -export const applyFilterToValue = (cellValue: any, filter: FilterCondition): boolean => { +export const applyFilterToValue = (cellValue: any, filter: FilterCondition): boolean => { const { operator, value, values } = filter; // Handle null/undefined values for isEmpty/isNotEmpty diff --git a/src/utils/generalUtils.ts b/src/utils/generalUtils.ts index 4a9883fc2..2e80dbc54 100644 --- a/src/utils/generalUtils.ts +++ b/src/utils/generalUtils.ts @@ -20,6 +20,6 @@ export const deepClone = (obj: T): T => { return clonedObj; }; -export const canDisplaySection = (headers: HeaderObject[], pinned?: Pinned) => { +export const canDisplaySection = (headers: HeaderObject[], pinned?: Pinned) => { return headers.filter((header) => header.pinned === pinned).some((header) => !header.hide); }; diff --git a/src/utils/headerUtils.ts b/src/utils/headerUtils.ts index 8586e3a7e..d202ba5ca 100644 --- a/src/utils/headerUtils.ts +++ b/src/utils/headerUtils.ts @@ -7,7 +7,7 @@ import HeaderObject from "../types/HeaderObject"; * @param colIndex The column index of the header in the current context * @returns Array of column indices that belong to this header branch */ -export const getHeaderLeafIndices = (header: HeaderObject, colIndex: number): number[] => { +export const getHeaderLeafIndices = (header: HeaderObject, colIndex: number): number[] => { // For a leaf node (no children), just return the current index if (!header.children || header.children.length === 0) { return [colIndex]; @@ -17,7 +17,7 @@ export const getHeaderLeafIndices = (header: HeaderObject, colIndex: number): nu const columnsToSelect: number[] = []; // Recursive function to collect column indices - const collectChildIndices = (childHeader: HeaderObject, startIndex: number): number => { + const collectChildIndices = (childHeader: HeaderObject, startIndex: number): number => { // If this is a leaf node, add its index and increment if (!childHeader.children || childHeader.children.length === 0) { columnsToSelect.push(startIndex); @@ -44,7 +44,7 @@ export const getHeaderLeafIndices = (header: HeaderObject, colIndex: number): nu * @param headers The headers array to flatten * @returns Flattened array of all leaf headers */ -export const flattenHeaders = (headers: HeaderObject[]): HeaderObject[] => { +export const flattenHeaders = (headers: HeaderObject[]): HeaderObject[] => { return headers.flatMap((header) => { if (!header.children || header.children.length === 0) { return [header]; @@ -58,7 +58,7 @@ export const flattenHeaders = (headers: HeaderObject[]): HeaderObject[] => { * @param headers The headers array to flatten * @returns Flattened array of all headers in the hierarchy */ -export const flattenAllHeaders = (headers: HeaderObject[]): HeaderObject[] => { +export const flattenAllHeaders = (headers: HeaderObject[]): HeaderObject[] => { return headers.flatMap((header) => { const result = [header]; if (header.children && header.children.length > 0) { diff --git a/src/utils/headerWidthUtils.ts b/src/utils/headerWidthUtils.ts index ddc3247bf..469f34e34 100644 --- a/src/utils/headerWidthUtils.ts +++ b/src/utils/headerWidthUtils.ts @@ -5,7 +5,7 @@ import { getCellId } from "./cellUtils"; /** * Find all leaf headers (headers without children) in a header tree */ -export const findLeafHeaders = (header: HeaderObject): HeaderObject[] => { +export const findLeafHeaders = (header: HeaderObject): HeaderObject[] => { // Skip hidden headers if (header.hide) { return []; @@ -21,7 +21,7 @@ export const findLeafHeaders = (header: HeaderObject): HeaderObject[] => { /** * Get actual width of a header in pixels */ -export const getHeaderWidthInPixels = (header: HeaderObject): number => { +export const getHeaderWidthInPixels = (header: HeaderObject): number => { // Skip hidden headers if (header.hide) { return 0; @@ -38,7 +38,7 @@ export const getHeaderWidthInPixels = (header: HeaderObject): number => { // For fr, %, or any other format, get the actual DOM element width else { const cellElement = document.getElementById( - getCellId({ accessor: header.accessor, rowId: "header" }) + getCellId({ headerId: header.id, rowId: "header" }) ); return cellElement?.offsetWidth || TABLE_HEADER_CELL_WIDTH_DEFAULT; } @@ -47,12 +47,12 @@ export const getHeaderWidthInPixels = (header: HeaderObject): number => { /** * Convert fractional widths to pixel values */ -export const removeAllFractionalWidths = (header: HeaderObject): void => { +export const removeAllFractionalWidths = (header: HeaderObject): void => { const headerWidth = header.width; if (typeof headerWidth === "string" && headerWidth.includes("fr")) { header.width = - document.getElementById(getCellId({ accessor: header.accessor, rowId: "header" })) - ?.offsetWidth || TABLE_HEADER_CELL_WIDTH_DEFAULT; + document.getElementById(getCellId({ headerId: header.id, rowId: "header" }))?.offsetWidth || + TABLE_HEADER_CELL_WIDTH_DEFAULT; } if (header.children) { header.children.forEach((child) => { @@ -64,6 +64,6 @@ export const removeAllFractionalWidths = (header: HeaderObject): void => { /** * Calculate the minimum width for a header */ -export const getHeaderMinWidth = (header: HeaderObject): number => { +export const getHeaderMinWidth = (header: HeaderObject): number => { return typeof header.minWidth === "number" ? header.minWidth : 40; }; diff --git a/src/utils/infiniteScrollUtils.ts b/src/utils/infiniteScrollUtils.ts index c6cddb612..6eb5f30ae 100644 --- a/src/utils/infiniteScrollUtils.ts +++ b/src/utils/infiniteScrollUtils.ts @@ -4,12 +4,12 @@ import TableRow from "../types/TableRow"; const SEPARATOR_HEIGHT = 1; // Calculate total row count - now just the array length -export const getTotalRowCount = (tableRows: TableRow[]): number => { +export const getTotalRowCount = (tableRows: TableRow[]): number => { return tableRows.length; }; // Get visible rows with simple array slicing -export const getVisibleRows = ({ +export const getVisibleRows = ({ bufferRowCount, contentHeight, rowHeight, @@ -20,8 +20,8 @@ export const getVisibleRows = ({ contentHeight: number; rowHeight: number; scrollTop: number; - tableRows: TableRow[]; -}): TableRow[] => { + tableRows: TableRow[]; +}): TableRow[] => { const rowHeightWithSeparator = rowHeight + SEPARATOR_HEIGHT; // Calculate start and end indices directly diff --git a/src/utils/performanceUtils.ts b/src/utils/performanceUtils.ts index 8f05dacda..02578ec26 100644 --- a/src/utils/performanceUtils.ts +++ b/src/utils/performanceUtils.ts @@ -21,16 +21,13 @@ export const useThrottle = () => { }; }; -export const logArrayDifferences = ( - original: HeaderObject[], - updated: HeaderObject[] -) => { +export const logArrayDifferences = (original: HeaderObject[], updated: HeaderObject[]) => { const differences = original.reduce((diff, header, index) => { if (header.accessor !== updated[index]?.accessor) { diff.push({ original: header, updated: updated[index] }); } return diff; - }, [] as { original: HeaderObject; updated: HeaderObject | undefined }[]); + }, [] as { original: HeaderObject; updated: HeaderObject | undefined }[]); console.info("Differences between arrays:", differences); console.info("\n"); diff --git a/src/utils/resizeUtils.ts b/src/utils/resizeUtils.ts index f304b0c65..ffc6a0a22 100644 --- a/src/utils/resizeUtils.ts +++ b/src/utils/resizeUtils.ts @@ -1,4 +1,4 @@ -import { HeaderObject } from ".."; +import { HeaderObject, Row } from ".."; import { HandleResizeStartProps } from "../types/HandleResizeStartProps"; import { calculatePinnedWidth } from "./headerUtils"; import { @@ -137,10 +137,10 @@ const handleParentHeaderResize = ({ /** * Recalculate widths for all sections (left, right, main) */ -export const recalculateAllSectionWidths = ({ +export const recalculateAllSectionWidths = ({ headers, }: { - headers: HeaderObject[]; + headers: HeaderObject[]; }): { leftWidth: number; rightWidth: number; diff --git a/src/utils/rowSelectionUtils.ts b/src/utils/rowSelectionUtils.ts index 57cc840c9..9e7aa21f5 100644 --- a/src/utils/rowSelectionUtils.ts +++ b/src/utils/rowSelectionUtils.ts @@ -1,10 +1,9 @@ -import Row from "../types/Row"; import HeaderObject, { Accessor } from "../types/HeaderObject"; /** * Get the set of selected row IDs from an array of rows */ -export const getSelectedRowIds = (rows: Row[], rowIdAccessor: Accessor): string[] => { +export const getSelectedRowIds = (rows: T[], rowIdAccessor: Accessor): string[] => { return rows.map((row) => String(row[rowIdAccessor])); }; @@ -18,9 +17,9 @@ export const isRowSelected = (rowId: string, selectedRows: Set): boolean /** * Check if all rows are selected */ -export const areAllRowsSelected = ( - rows: Row[], - rowIdAccessor: Accessor, +export const areAllRowsSelected = ( + rows: T[], + rowIdAccessor: Accessor, selectedRows: Set ): boolean => { if (rows.length === 0) return false; @@ -43,7 +42,7 @@ export const toggleRowSelection = (rowId: string, selectedRows: Set): Se /** * Select all rows */ -export const selectAllRows = (rows: Row[], rowIdAccessor: Accessor): Set => { +export const selectAllRows = (rows: T[], rowIdAccessor: Accessor): Set => { return new Set(rows.map((row) => String(row[rowIdAccessor]))); }; @@ -57,11 +56,11 @@ export const deselectAllRows = (): Set => { /** * Get the selected rows from the rows array */ -export const getSelectedRows = ( - rows: Row[], - rowIdAccessor: Accessor, +export const getSelectedRows = ( + rows: T[], + rowIdAccessor: Accessor, selectedRows: Set -): Row[] => { +): T[] => { return rows.filter((row) => selectedRows.has(String(row[rowIdAccessor]))); }; @@ -75,9 +74,10 @@ export const getSelectedRowCount = (selectedRows: Set): number => { /** * Create a selection header for the checkbox column */ -export const createSelectionHeader = () => { - const selectionHeader: HeaderObject = { - accessor: "__row_selection__" as Accessor, +export const createSelectionHeader = (): HeaderObject => { + const selectionHeader: HeaderObject = { + id: "__row_selection__", + accessor: "__row_selection__" as Accessor, label: "", width: 42, isEditable: false, diff --git a/src/utils/rowUtils.ts b/src/utils/rowUtils.ts index 34c5fda70..c09ec55f1 100644 --- a/src/utils/rowUtils.ts +++ b/src/utils/rowUtils.ts @@ -1,26 +1,32 @@ import TableRow from "../types/TableRow"; -import Row from "../types/Row"; import { RowId } from "../types/RowId"; import { Accessor } from "../types/HeaderObject"; +import RowGrouping from "../types/RowGrouping"; /** * Check if an array contains Row objects (vs primitive arrays like string[] or number[]) */ -export const isRowArray = (data: any): data is Row[] => { +export const isRowArray = (data: any): data is T[] => { return Array.isArray(data) && data.length > 0 && typeof data[0] === "object" && data[0] !== null; }; /** * Get the row ID from a row using the specified accessor or fall back to index */ -export const getRowId = ({ row, rowIdAccessor }: { row: Row; rowIdAccessor: Accessor }): RowId => { +export const getRowId = ({ + row, + rowIdAccessor, +}: { + row: T; + rowIdAccessor: Accessor; +}): RowId => { return row[rowIdAccessor] as RowId; }; /** * Get nested rows from a row based on the grouping path */ -export const getNestedRows = (row: Row, groupingKey: string): Row[] => { +export const getNestedRows = (row: T, groupingKey: keyof T) => { const nestedData = row[groupingKey]; // Only return as Row[] if it's an array of objects (potential rows) if (isRowArray(nestedData)) { @@ -32,7 +38,7 @@ export const getNestedRows = (row: Row, groupingKey: string): Row[] => { /** * Check if a row has nested rows for a given grouping key */ -export const hasNestedRows = (row: Row, groupingKey?: string): boolean => { +export const hasNestedRows = (row: T, groupingKey?: keyof T): boolean => { if (!groupingKey) return false; const nestedData = row[groupingKey]; return isRowArray(nestedData); @@ -42,7 +48,7 @@ export const hasNestedRows = (row: Row, groupingKey?: string): boolean => { * Flatten rows recursively based on row grouping configuration * Now calculates ALL properties including position and isLastGroupRow */ -export const flattenRowsWithGrouping = ({ +export const flattenRowsWithGrouping = ({ depth = 0, expandAll = false, unexpandedRows, @@ -53,13 +59,14 @@ export const flattenRowsWithGrouping = ({ depth?: number; expandAll?: boolean; unexpandedRows: Set; - rowGrouping?: Accessor[]; - rowIdAccessor: Accessor; - rows: Row[]; -}): TableRow[] => { - const result: TableRow[] = []; + rowGrouping?: RowGrouping; + rowIdAccessor: Accessor; + rows: T[]; +}): TableRow[] => { + const result: TableRow[] = []; - const processRows = (currentRows: Row[], currentDepth: number, parentPosition = 0): number => { + //'never[] | (T[keyof T] & unknown[])' + const processRows = (currentRows: T[], currentDepth: number, parentPosition = 0): number => { let position = parentPosition; currentRows.forEach((row, index) => { @@ -73,7 +80,7 @@ export const flattenRowsWithGrouping = ({ result.push({ row, depth: currentDepth, - groupingKey: currentGroupingKey, + groupingKey: currentGroupingKey as keyof T, position, isLastGroupRow, }); @@ -88,7 +95,7 @@ export const flattenRowsWithGrouping = ({ // If row is expanded and has nested data for the current grouping level if (isExpanded && currentDepth < rowGrouping.length) { - const nestedRows = getNestedRows(row, currentGroupingKey); + const nestedRows = getNestedRows(row, currentGroupingKey as keyof T) as T[]; if (nestedRows.length > 0) { // Recursively process nested rows and update position diff --git a/src/utils/sortUtils.ts b/src/utils/sortUtils.ts index 9a21b4dcd..78bf6baf6 100644 --- a/src/utils/sortUtils.ts +++ b/src/utils/sortUtils.ts @@ -1,5 +1,4 @@ import HeaderObject, { Accessor } from "../types/HeaderObject"; -import Row from "../types/Row"; import SortColumn from "../types/SortColumn"; // Type-specific comparators for different data types @@ -127,20 +126,20 @@ const compareValues = ( }; // Basic sort function for flat data (no grouping) -const sortFlatRows = ({ +const sortFlatRows = ({ rows, sortColumn, headers, }: { - rows: Row[]; - sortColumn: SortColumn; - headers: HeaderObject[]; -}): Row[] => { + rows: T[]; + sortColumn: SortColumn; + headers: HeaderObject[]; +}): T[] => { // Recursively search for the header in nested structure const findHeaderRecursively = ( - headers: HeaderObject[], - accessor: Accessor - ): HeaderObject | undefined => { + headers: HeaderObject[], + accessor?: Accessor + ): HeaderObject | undefined => { for (const header of headers) { if (header.accessor === accessor) { return header; @@ -159,6 +158,7 @@ const sortFlatRows = ({ return [...rows].sort((a, b) => { const accessor = sortColumn.key.accessor; + if (!accessor) return 0; const aValue = a[accessor]; const bValue = b[accessor]; @@ -166,14 +166,14 @@ const sortFlatRows = ({ }); }; -export const handleSort = ({ +export const handleSort = ({ headers, rows, sortColumn, }: { - headers: HeaderObject[]; - rows: Row[]; - sortColumn: SortColumn; + headers: HeaderObject[]; + rows: T[]; + sortColumn: SortColumn; }) => { // For now, use simple flat sorting since we've simplified the row structure // Row grouping will be handled by the table internally using the rowGrouping prop