import type { InteractionPolicy } from '@ankhorage/surface'; import { Show } from '@ankhorage/surface'; import React from 'react'; import { ScrollView, type ViewStyle } from 'react-native'; import type { DataTableCellContext, DataTableColumn, DataTableColumnAlign, DataTableDensity, DataTableProps, DataTableRowAction, DataTableSortDirection, } from '../../../../types/data-table'; import { Button } from '../../../button/public'; import { IconButton } from '../../../button/public'; import { Card } from '../../../card/public'; import { EmptyState } from '../../../empty-state/public'; import { View } from '../../../layout/public'; import { PopoverMenu, type PopoverMenuAction } from '../../../popover-menu/public'; import { SkeletonList } from '../../../skeleton/public'; import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope'; import { Text, type TextAlign } from '../../../typography/public'; import { resolveDataTableRowKey } from '../../utils/resolveDataTableRowKey'; /*** * Displays structured tabular data with responsive desktop/mobile layouts. */ export const DataTable = withZoraThemeScope(DataTableInner); function resolveTextAlign(align: DataTableColumnAlign | undefined): TextAlign { switch (align) { case 'center': return 'center'; case 'end': return 'right'; case 'start': default: return 'left'; } } function resolveCellJustify(align: DataTableColumnAlign | undefined): ViewStyle['alignItems'] { switch (align) { case 'center': return 'center'; case 'end': return 'flex-end'; case 'start': default: return 'flex-start'; } } function resolveCellStyle(column: DataTableColumn): ViewStyle { return { alignItems: resolveCellJustify(column.align), flexGrow: column.width === undefined ? 1 : 0, flexShrink: 0, minWidth: column.minWidth ?? 140, width: column.width, }; } function resolveRowPadding(density: DataTableDensity) { return density === 'compact' ? { px: 'm' as const, py: 's' as const } : { px: 'm' as const, py: 'm' as const }; } function resolveAccessorValue(row: TRow, column: DataTableColumn) { if (column.accessor === undefined) { return undefined; } return row[column.accessor]; } function renderDefaultCell(value: unknown): React.ReactNode { if (value === null || value === undefined) { return '—'; } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); } if (typeof value === 'bigint') { return value.toString(); } if (value instanceof Date) { return value.toLocaleDateString(); } return '—'; } function createCellContext( column: DataTableColumn, row: TRow, rowIndex: number, ): DataTableCellContext { return { column, row, rowIndex, value: resolveAccessorValue(row, column), }; } function renderCell( column: DataTableColumn, row: TRow, rowIndex: number, ) { const context = createCellContext(column, row, rowIndex); return column.renderCell ? column.renderCell(context) : renderDefaultCell(context.value); } function renderTableCell( column: DataTableColumn, row: TRow, rowIndex: number, ) { if (column.renderCell) { return column.renderCell(createCellContext(column, row, rowIndex)); } return ( {renderDefaultCell(resolveAccessorValue(row, column))} ); } function resolveNextSortDirection( current: DataTableSortDirection | undefined, ): DataTableSortDirection { return current === 'asc' ? 'desc' : 'asc'; } /*** Maps row-scoped DataTable actions into the shared PopoverMenu action contract. */ function mapRowActions( row: TRow, actions: readonly DataTableRowAction[], ): readonly PopoverMenuAction[] { return actions.map((action) => ({ description: action.description, disabled: action.disabled, icon: action.icon, id: action.id, intent: action.intent, onPress: action.onPress ? () => action.onPress?.(row) : undefined, title: action.title, })); } /*** Renders the row action trigger and anchored PopoverMenu when actions exist. */ function renderRowActions({ row, rowIndex, rowActions, testID, interactionPolicy, }: { row: TRow; rowIndex: number; rowActions: DataTableProps['rowActions']; testID?: string; interactionPolicy?: InteractionPolicy; }): React.ReactNode { const actions = rowActions?.(row, rowIndex) ?? []; if (actions.length === 0) { return null; } return ( ( )} /> ); } function DataTableHeader({ columns, sort, onSortChange, density, interactionPolicy, }: Pick, 'columns' | 'density' | 'onSortChange' | 'sort'> & { interactionPolicy?: InteractionPolicy; }) { const padding = resolveRowPadding(density ?? 'comfortable'); const passive = interactionPolicy === 'passive'; return ( {columns.map((column) => { const currentDirection = sort?.columnId === column.id ? sort.direction : undefined; const sortable = Boolean(column.sortable && onSortChange); const directionLabel = currentDirection === undefined ? '' : currentDirection === 'asc' ? ' ↑' : ' ↓'; return ( {sortable ? ( ) : ( {column.header} )} ); })} Actions ); } function DataTableDesktop({ columns, rows, rowActions, rowId, rowKey, sort, onSortChange, density, testID, interactionPolicy, }: DataTableProps) { const padding = resolveRowPadding(density ?? 'comfortable'); return ( {rows.map((row, rowIndex) => ( {columns.map((column) => ( {renderTableCell(column, row, rowIndex)} ))} {renderRowActions({ row, rowActions, rowIndex, testID, interactionPolicy })} ))} ); } function DataTableMobile({ columns, rows, rowActions, rowId, rowKey, testID, interactionPolicy, }: DataTableProps) { const [primaryColumn, ...detailColumns] = columns; return ( {rows.map((row, rowIndex) => { const title = primaryColumn ? renderCell(primaryColumn, row, rowIndex) : `Row ${rowIndex + 1}`; const actions = renderRowActions({ row, rowActions, rowIndex, testID, interactionPolicy }); return ( {detailColumns.map((column) => ( {column.header} {column.renderCell ? ( renderCell(column, row, rowIndex) ) : ( {renderDefaultCell(resolveAccessorValue(row, column))} )} ))} ); })} ); } function DataTableInner({ themeId: _themeId, mode: _mode, rows, loading = false, loadingRows = 5, emptyTitle = 'No data', emptyDescription = 'There are no rows to display.', testID, density = 'comfortable', interactionPolicy, ...props }: DataTableProps) { if (loading) { return ; } if (rows.length === 0) { return ; } const tableProps: DataTableProps = { ...props, density, emptyDescription, emptyTitle, interactionPolicy, loading, loadingRows, rows, testID, }; return ( }> ); }