import { type ComponentType, useEffect, useMemo, useRef } from "react"; import { useRendererHost, useUiHost } from "../../../ui"; import { OpenTuiDataTable } from "./opentui"; import type { DataTableColumn, DataTableProps, } from "./types"; import { useRemoteUiNode } from "../../../remote/semantic-tree"; import { remoteNumberValue, resolveRemoteItemIndex } from "../../../remote/semantic-helpers"; import { useOptionalPaneInstanceId } from "../../../state/app/context"; import { registerPaneTableExporter } from "../../../state/pane-table-export-registry"; import { createDataTableCsv } from "../../data-table/export"; import { headerCase } from "../header-case"; export type { DataTableCell, DataTableColumn, DataTableProps, DataTableSectionHeader, DataTableVisibleRange, } from "./types"; export function DataTable( props: DataTableProps, ) { const paneId = useOptionalPaneInstanceId(); const renderer = useRendererHost(); const propsRef = useRef(props); propsRef.current = props; useEffect(() => { if (!paneId || !renderer.saveTextFile) return; return registerPaneTableExporter(paneId, (filename) => renderer.saveTextFile!({ name: filename, text: createDataTableCsv(propsRef.current), mimeType: "text/csv;charset=utf-8", })); }, [paneId, renderer]); useRemoteUiNode({ role: "table", label: "Data table", actions: { selectRow: (input) => { const index = resolveTableIndex(input, props); const item = index >= 0 ? props.items[index] : undefined; if (item) props.onSelect(item, index); }, activateRow: (input) => { const index = resolveTableIndex(input, props); const item = index >= 0 ? props.items[index] : undefined; if (item) { props.onSelect(item, index); props.onActivate?.(item, index); } }, sort: (input) => { const columnId = typeof input === "string" ? input : input && typeof input === "object" && typeof (input as { columnId?: unknown }).columnId === "string" ? (input as { columnId: string }).columnId : null; if (columnId && props.columns.some((column) => column.id === columnId)) { props.onHeaderClick?.(columnId); } }, scrollTo: (input) => { const box = props.scrollRef.current; if (!box) return; box.scrollTo(Math.max(0, Math.round(remoteNumberValue(input, ["top", "index"])))); props.onBodyScrollActivity(); }, scrollBy: (input) => { const box = props.scrollRef.current; if (!box) return; const direction = input && typeof input === "object" ? (input as { direction?: unknown }).direction : undefined; const delta = direction === "up" ? remoteNumberValue(input, ["delta"], -1) : remoteNumberValue(input, ["delta"], 1); box.scrollTo(Math.max(0, Math.round((box.scrollTop ?? 0) + delta))); props.onBodyScrollActivity(); }, }, getMetadata: () => ({ paneInstanceId: paneId, sortColumnId: props.sortColumnId, sortDirection: props.sortDirection, columns: props.columns.map((column) => ({ id: column.id, label: column.label })), rows: props.items.slice(0, 200).map((item, index) => ({ index, key: props.getItemKey(item, index), selected: props.isSelected(item, index), })), rowCount: props.items.length, }), }); // Header labels read the same in every table whatever case a pane wrote // them in. Exports and automation keep the pane's own labels. const columns = useMemo( () => props.columns.map((column) => { const label = headerCase(column.label); return label === column.label ? column : { ...column, label }; }), [props.columns], ); const HostDataTable = useUiHost().DataTable as | ComponentType> | undefined; if (HostDataTable) { return ; } return ; } function resolveTableIndex( input: unknown, props: DataTableProps, ): number { return resolveRemoteItemIndex(input, props.items, { key: (item, index) => props.getItemKey(item, index), }); }