@burdenoff/microfe-bigconsole 2026.613.6 → 2026.615.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bigconsole/components/widgets/WidgetWrapper.js +183 -183
- package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
- package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js +275 -114
- package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableHeader.js +89 -29
- package/dist/bigconsole/components/widgets/table/TableHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableWidget.js +218 -148
- package/dist/bigconsole/components/widgets/table/TableWidget.js.map +1 -1
- package/dist/bigconsole/hooks/index.js +1 -0
- package/dist/bigconsole/hooks/useWidgetConfigPersist.js +32 -0
- package/dist/bigconsole/hooks/useWidgetConfigPersist.js.map +1 -0
- package/dist/bigconsole/store/dashboardStore.js +2 -2
- package/dist/bigconsole/store/dashboardStore.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PivotTableWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/pivot-table/PivotTableWidget.tsx"],"sourcesContent":["/**\n * PivotTableWidget Component\n *\n * Interactive pivot table for data analysis with rows, columns, and aggregations.\n * v2.0 spec compliant widget type.\n */\n\nimport type { JSX } from 'react';\nimport { type FC, memo, useMemo, useState, useCallback } from 'react';\nimport type { Widget, PivotTableConfig as PivotTableConfigType, PivotValueField } from '../../../types';\nimport { ChevronDown, ChevronRight, Settings } from 'lucide-react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from main types file\nexport type PivotTableConfig = PivotTableConfigType;\nexport type ValueField = PivotValueField;\n\nexport interface PivotTableWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Raw data array */\n data?: Record<string, unknown> | Record<string, unknown>[];\n /** Whether the widget is in edit mode */\n isEditMode?: boolean;\n /** Whether the widget is selected */\n isSelected?: boolean;\n /** Whether data is loading */\n isLoading?: boolean;\n /** Error message if any */\n error?: string | null;\n /** Callback when widget is clicked */\n onClick?: () => void;\n /** Drilldown handler */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\ninterface PivotNode {\n key: string;\n values: Record<string, number>;\n children: Map<string, PivotNode>;\n expanded: boolean;\n depth: number;\n}\n\n// ============================================================================\n// Aggregation Functions\n// ============================================================================\n\nconst aggregationFunctions: Record<string, (values: number[]) => number> = {\n sum: (values) => values.reduce((a, b) => a + b, 0),\n count: (values) => values.length,\n average: (values) => (values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0),\n min: (values) => (values.length ? Math.min(...values) : 0),\n max: (values) => (values.length ? Math.max(...values) : 0),\n};\n\n// ============================================================================\n// Pivot Engine\n// ============================================================================\n\nfunction buildPivotTree(\n data: Record<string, unknown>[],\n rowFields: string[],\n valueFields: ValueField[],\n depth: number = 0\n): PivotNode {\n const root: PivotNode = {\n key: 'root',\n values: {},\n children: new Map(),\n expanded: true,\n depth,\n };\n\n if (rowFields.length === 0) {\n // Leaf level - aggregate values\n for (const vf of valueFields) {\n const values = data.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n root.values[vf.field] = aggregationFunctions[vf.aggregation](values);\n }\n return root;\n }\n\n const currentField = rowFields[0];\n const remainingFields = rowFields.slice(1);\n\n // Group data by current field\n const groups = new Map<string, Record<string, unknown>[]>();\n for (const row of data) {\n const key = String(row[currentField] ?? 'Unknown');\n if (!groups.has(key)) {\n groups.set(key, []);\n }\n groups.get(key)!.push(row);\n }\n\n // Build children recursively\n for (const [key, groupData] of groups) {\n const childNode = buildPivotTree(groupData, remainingFields, valueFields, depth + 1);\n childNode.key = key;\n root.children.set(key, childNode);\n }\n\n // Calculate aggregates for this level\n for (const vf of valueFields) {\n const values = data.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n root.values[vf.field] = aggregationFunctions[vf.aggregation](values);\n }\n\n return root;\n}\n\nfunction getColumnValues(data: Record<string, unknown>[], columnFields: string[]): string[] {\n if (columnFields.length === 0) return ['Total'];\n\n const columnField = columnFields[0];\n const uniqueValues = new Set<string>();\n for (const row of data) {\n uniqueValues.add(String(row[columnField] ?? 'Unknown'));\n }\n return Array.from(uniqueValues).sort();\n}\n\nfunction getValuesByColumn(\n data: Record<string, unknown>[],\n columnFields: string[],\n valueFields: ValueField[],\n rowFilter: Record<string, string>\n): Map<string, Record<string, number>> {\n // Filter data by row values\n let filteredData = data;\n for (const [field, value] of Object.entries(rowFilter)) {\n filteredData = filteredData.filter((row) => String(row[field]) === value);\n }\n\n const result = new Map<string, Record<string, number>>();\n\n if (columnFields.length === 0) {\n // No column grouping - just aggregate\n const values: Record<string, number> = {};\n for (const vf of valueFields) {\n const nums = filteredData.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n values[vf.field] = aggregationFunctions[vf.aggregation](nums);\n }\n result.set('Total', values);\n return result;\n }\n\n const columnField = columnFields[0];\n const columnValues = getColumnValues(data, columnFields);\n\n for (const colValue of columnValues) {\n const colData = filteredData.filter((row) => String(row[columnField]) === colValue);\n const values: Record<string, number> = {};\n for (const vf of valueFields) {\n const nums = colData.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n values[vf.field] = aggregationFunctions[vf.aggregation](nums);\n }\n result.set(colValue, values);\n }\n\n return result;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const PivotTableWidget: FC<PivotTableWidgetProps> = memo(function PivotTableWidget({\n widget,\n data: rawData = [],\n onDrilldown,\n}) {\n // Normalize data to array format\n const data = useMemo(() => (Array.isArray(rawData) ? rawData : []), [rawData]);\n const config: PivotTableConfig = useMemo(() => {\n const defaultConfig: PivotTableConfig = {\n rows: [],\n columns: [],\n values: [],\n showRowTotals: true,\n showColumnTotals: true,\n showGrandTotal: true,\n expandable: true,\n numberFormat: 'number',\n decimalPlaces: 0,\n };\n return widget.config ? { ...defaultConfig, ...(widget.config as Partial<PivotTableConfig>) } : defaultConfig;\n }, [widget.config]);\n\n const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set(['root']));\n\n // Build pivot structure\n const pivotTree = useMemo(() => {\n return buildPivotTree(data, config.rows, config.values);\n }, [data, config.rows, config.values]);\n\n const columnValues = useMemo(() => {\n return getColumnValues(data, config.columns);\n }, [data, config.columns]);\n\n // Format number based on config\n const formatValue = useCallback(\n (value: number): string => {\n if (isNaN(value)) return '-';\n\n switch (config.numberFormat) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: config.decimalPlaces,\n maximumFractionDigits: config.decimalPlaces,\n }).format(value);\n case 'percentage':\n return new Intl.NumberFormat('en-US', {\n style: 'percent',\n minimumFractionDigits: config.decimalPlaces,\n maximumFractionDigits: config.decimalPlaces,\n }).format(value / 100);\n default:\n return new Intl.NumberFormat('en-US', {\n minimumFractionDigits: config.decimalPlaces,\n maximumFractionDigits: config.decimalPlaces,\n }).format(value);\n }\n },\n [config.numberFormat, config.decimalPlaces]\n );\n\n // Toggle row expansion\n const toggleRow = useCallback((rowPath: string) => {\n setExpandedRows((prev) => {\n const next = new Set(prev);\n if (next.has(rowPath)) {\n next.delete(rowPath);\n } else {\n next.add(rowPath);\n }\n return next;\n });\n }, []);\n\n // Handle cell click for drilldown\n const handleCellClick = useCallback(\n (rowFilter: Record<string, string>, columnValue: string) => {\n if (onDrilldown) {\n onDrilldown({\n rowFilter,\n columnValue,\n type: 'pivot_cell_click',\n });\n }\n },\n [onDrilldown]\n );\n\n // Render row recursively\n const renderRows = useCallback(\n (node: PivotNode, rowFilter: Record<string, string>, parentPath: string, depth: number): JSX.Element[] => {\n const rows: JSX.Element[] = [];\n const hasChildren = node.children.size > 0;\n const isExpanded = expandedRows.has(parentPath);\n const rowField = config.rows[depth - 1];\n\n if (depth > 0) {\n const columnData = getValuesByColumn(data, config.columns, config.values, rowFilter);\n\n rows.push(\n <tr key={parentPath} className=\"hover:bg-bg-muted/50 transition-colors\">\n {/* Row header */}\n <td\n className=\"sticky left-0 bg-bg-surface border-r border-border-default px-3 py-2 font-medium text-sm\"\n style={{ paddingLeft: `${depth * 16 + 8}px` }}\n >\n <div className=\"flex items-center gap-1\">\n {config.expandable && hasChildren && (\n <button\n type=\"button\"\n onClick={() => toggleRow(parentPath)}\n className=\"p-0.5 hover:bg-bg-muted rounded\"\n >\n {isExpanded ? (\n <ChevronDown className=\"w-4 h-4 text-text-secondary\" />\n ) : (\n <ChevronRight className=\"w-4 h-4 text-text-secondary\" />\n )}\n </button>\n )}\n {!hasChildren && config.expandable && <span className=\"w-5\" />}\n <span>{node.key}</span>\n </div>\n </td>\n\n {/* Value cells */}\n {columnValues.map((colValue) => {\n const cellValues = columnData.get(colValue) || {};\n return config.values.map((vf) => (\n <td\n key={`${colValue}-${vf.field}`}\n className=\"px-3 py-2 text-sm text-right tabular-nums cursor-pointer hover:bg-action-primary-bg/10\"\n onClick={() => handleCellClick(rowFilter, colValue)}\n >\n {formatValue(cellValues[vf.field] || 0)}\n </td>\n ));\n })}\n\n {/* Row total */}\n {config.showRowTotals &&\n config.values.map((vf) => (\n <td\n key={`total-${vf.field}`}\n className=\"px-3 py-2 text-sm text-right tabular-nums font-medium bg-bg-muted/30\"\n >\n {formatValue(node.values[vf.field] || 0)}\n </td>\n ))}\n </tr>\n );\n }\n\n // Render children if expanded\n if (hasChildren && (isExpanded || depth === 0)) {\n const sortedKeys = Array.from(node.children.keys()).sort();\n for (const childKey of sortedKeys) {\n const childNode = node.children.get(childKey)!;\n const childPath = `${parentPath}/${childKey}`;\n const childFilter = rowField ? { ...rowFilter, [rowField]: childKey } : rowFilter;\n rows.push(...renderRows(childNode, childFilter, childPath, depth + 1));\n }\n }\n\n return rows;\n },\n [data, config, columnValues, expandedRows, formatValue, handleCellClick, toggleRow]\n );\n\n // No data state\n if (data.length === 0) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full p-8 text-text-secondary\">\n <Settings className=\"w-12 h-12 mb-4 opacity-50\" />\n <p className=\"text-sm\">No data available</p>\n <p className=\"text-xs mt-1\">Configure data source to populate pivot table</p>\n </div>\n );\n }\n\n // No configuration state\n if (config.rows.length === 0 && config.values.length === 0) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full p-8 text-text-secondary\">\n <Settings className=\"w-12 h-12 mb-4 opacity-50\" />\n <p className=\"text-sm\">Configure Pivot Table</p>\n <p className=\"text-xs mt-1\">Add row fields and value fields in widget settings</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full overflow-hidden\">\n <div className=\"flex-1 overflow-auto\">\n <table className=\"w-full border-collapse\">\n {/* Header */}\n <thead className=\"sticky top-0 z-10\">\n {/* Column headers row */}\n <tr className=\"bg-bg-muted\">\n <th className=\"sticky left-0 bg-bg-muted border-r border-b border-border-default px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-text-secondary\">\n {config.rows.length > 0 ? config.rows.map((r) => r.replace(/_/g, ' ')).join(' / ') : 'Values'}\n </th>\n\n {/* Column value headers */}\n {columnValues.map((colValue) => (\n <th\n key={colValue}\n colSpan={config.values.length}\n className=\"border-b border-border-default px-3 py-2 text-center text-xs font-semibold uppercase tracking-wider text-text-secondary\"\n >\n {colValue}\n </th>\n ))}\n\n {/* Total header */}\n {config.showRowTotals && (\n <th\n colSpan={config.values.length}\n className=\"border-b border-border-default px-3 py-2 text-center text-xs font-semibold uppercase tracking-wider text-text-secondary bg-bg-muted/50\"\n >\n Total\n </th>\n )}\n </tr>\n\n {/* Value field headers row (if multiple values) */}\n {config.values.length > 1 && (\n <tr className=\"bg-bg-surface\">\n <th className=\"sticky left-0 bg-bg-surface border-r border-b border-border-default\" />\n {columnValues.map((colValue) =>\n config.values.map((vf) => (\n <th\n key={`${colValue}-${vf.field}`}\n className=\"border-b border-border-default px-3 py-1 text-center text-xs text-text-secondary\"\n >\n {vf.label || vf.field.replace(/_/g, ' ')}\n </th>\n ))\n )}\n {config.showRowTotals &&\n config.values.map((vf) => (\n <th\n key={`total-${vf.field}`}\n className=\"border-b border-border-default px-3 py-1 text-center text-xs text-text-secondary bg-bg-muted/30\"\n >\n {vf.label || vf.field.replace(/_/g, ' ')}\n </th>\n ))}\n </tr>\n )}\n </thead>\n\n {/* Body */}\n <tbody className=\"divide-y divide-border-default\">{renderRows(pivotTree, {}, 'root', 0)}</tbody>\n\n {/* Footer with column totals */}\n {config.showColumnTotals && (\n <tfoot>\n <tr className=\"bg-bg-muted/50 font-medium\">\n <td className=\"sticky left-0 bg-bg-muted/50 border-r border-t border-border-default px-3 py-2 text-sm\">\n Total\n </td>\n {columnValues.map((colValue) => {\n const columnData = getValuesByColumn(data, config.columns, config.values, {});\n const cellValues = columnData.get(colValue) || {};\n return config.values.map((vf) => (\n <td\n key={`footer-${colValue}-${vf.field}`}\n className=\"border-t border-border-default px-3 py-2 text-sm text-right tabular-nums\"\n >\n {formatValue(cellValues[vf.field] || 0)}\n </td>\n ));\n })}\n {config.showRowTotals &&\n config.showGrandTotal &&\n config.values.map((vf) => (\n <td\n key={`grand-${vf.field}`}\n className=\"border-t border-border-default px-3 py-2 text-sm text-right tabular-nums font-bold bg-bg-muted\"\n >\n {formatValue(pivotTree.values[vf.field] || 0)}\n </td>\n ))}\n </tr>\n </tfoot>\n )}\n </table>\n </div>\n </div>\n );\n});\n\nexport default PivotTableWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PivotTableWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/pivot-table/PivotTableWidget.tsx"],"sourcesContent":["/**\n * PivotTableWidget Component\n *\n * Interactive pivot table for data analysis with rows, columns, and aggregations.\n * v2.0 spec compliant widget type.\n *\n * In edit mode it exposes a drag-and-drop field shelf: fields can be dragged\n * between the Rows / Columns / Values / Available zones, the pivot recomputes\n * live, and the new layout is persisted to widget config so it survives reload.\n */\n\nimport type { JSX } from 'react';\nimport { type FC, memo, useMemo, useState, useCallback } from 'react';\nimport type { Widget, PivotTableConfig as PivotTableConfigType, PivotValueField } from '../../../types';\nimport { ChevronDown, ChevronRight, Settings, GripVertical } from 'lucide-react';\nimport { useIsEditMode } from '../../../store';\nimport { useWidgetOperations } from '../../../hooks';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from main types file\nexport type PivotTableConfig = PivotTableConfigType;\nexport type ValueField = PivotValueField;\n\nexport interface PivotTableWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Raw data array */\n data?: Record<string, unknown> | Record<string, unknown>[];\n /** Whether the widget is in edit mode */\n isEditMode?: boolean;\n /** Whether the widget is selected */\n isSelected?: boolean;\n /** Whether data is loading */\n isLoading?: boolean;\n /** Error message if any */\n error?: string | null;\n /** Callback when widget is clicked */\n onClick?: () => void;\n /** Drilldown handler */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\ninterface PivotNode {\n key: string;\n values: Record<string, number>;\n children: Map<string, PivotNode>;\n expanded: boolean;\n depth: number;\n}\n\n/** The arrangement of fields across the pivot's three field zones. */\ninterface PivotLayout {\n rows: string[];\n columns: string[];\n values: ValueField[];\n}\n\n/** Drop targets for the drag-and-drop field shelf. */\ntype PivotZone = 'rows' | 'columns' | 'values' | 'available';\n\nconst AGGREGATIONS: ValueField['aggregation'][] = ['sum', 'count', 'average', 'min', 'max'];\n\n// ============================================================================\n// Aggregation Functions\n// ============================================================================\n\nconst aggregationFunctions: Record<string, (values: number[]) => number> = {\n sum: (values) => values.reduce((a, b) => a + b, 0),\n count: (values) => values.length,\n average: (values) => (values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0),\n min: (values) => (values.length ? Math.min(...values) : 0),\n max: (values) => (values.length ? Math.max(...values) : 0),\n};\n\n// ============================================================================\n// Pivot Engine\n// ============================================================================\n\nfunction buildPivotTree(\n data: Record<string, unknown>[],\n rowFields: string[],\n valueFields: ValueField[],\n depth: number = 0\n): PivotNode {\n const root: PivotNode = {\n key: 'root',\n values: {},\n children: new Map(),\n expanded: true,\n depth,\n };\n\n if (rowFields.length === 0) {\n // Leaf level - aggregate values\n for (const vf of valueFields) {\n const values = data.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n root.values[vf.field] = aggregationFunctions[vf.aggregation](values);\n }\n return root;\n }\n\n const currentField = rowFields[0];\n const remainingFields = rowFields.slice(1);\n\n // Group data by current field\n const groups = new Map<string, Record<string, unknown>[]>();\n for (const row of data) {\n const key = String(row[currentField] ?? 'Unknown');\n if (!groups.has(key)) {\n groups.set(key, []);\n }\n groups.get(key)!.push(row);\n }\n\n // Build children recursively\n for (const [key, groupData] of groups) {\n const childNode = buildPivotTree(groupData, remainingFields, valueFields, depth + 1);\n childNode.key = key;\n root.children.set(key, childNode);\n }\n\n // Calculate aggregates for this level\n for (const vf of valueFields) {\n const values = data.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n root.values[vf.field] = aggregationFunctions[vf.aggregation](values);\n }\n\n return root;\n}\n\nfunction getColumnValues(data: Record<string, unknown>[], columnFields: string[]): string[] {\n if (columnFields.length === 0) return ['Total'];\n\n const columnField = columnFields[0];\n const uniqueValues = new Set<string>();\n for (const row of data) {\n uniqueValues.add(String(row[columnField] ?? 'Unknown'));\n }\n return Array.from(uniqueValues).sort();\n}\n\nfunction getValuesByColumn(\n data: Record<string, unknown>[],\n columnFields: string[],\n valueFields: ValueField[],\n rowFilter: Record<string, string>\n): Map<string, Record<string, number>> {\n // Filter data by row values\n let filteredData = data;\n for (const [field, value] of Object.entries(rowFilter)) {\n filteredData = filteredData.filter((row) => String(row[field]) === value);\n }\n\n const result = new Map<string, Record<string, number>>();\n\n if (columnFields.length === 0) {\n // No column grouping - just aggregate\n const values: Record<string, number> = {};\n for (const vf of valueFields) {\n const nums = filteredData.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n values[vf.field] = aggregationFunctions[vf.aggregation](nums);\n }\n result.set('Total', values);\n return result;\n }\n\n const columnField = columnFields[0];\n const columnValues = getColumnValues(data, columnFields);\n\n for (const colValue of columnValues) {\n const colData = filteredData.filter((row) => String(row[columnField]) === colValue);\n const values: Record<string, number> = {};\n for (const vf of valueFields) {\n const nums = colData.map((row) => Number(row[vf.field]) || 0).filter((v) => !isNaN(v));\n values[vf.field] = aggregationFunctions[vf.aggregation](nums);\n }\n result.set(colValue, values);\n }\n\n return result;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const PivotTableWidget: FC<PivotTableWidgetProps> = memo(function PivotTableWidget({\n widget,\n data: rawData = [],\n onDrilldown,\n}) {\n // Normalize data to array format\n const data = useMemo(() => (Array.isArray(rawData) ? rawData : []), [rawData]);\n const config: PivotTableConfig = useMemo(() => {\n const defaultConfig: PivotTableConfig = {\n rows: [],\n columns: [],\n values: [],\n showRowTotals: true,\n showColumnTotals: true,\n showGrandTotal: true,\n expandable: true,\n numberFormat: 'number',\n decimalPlaces: 0,\n };\n return widget.config ? { ...defaultConfig, ...(widget.config as Partial<PivotTableConfig>) } : defaultConfig;\n }, [widget.config]);\n\n const isEditMode = useIsEditMode();\n const { updateWidget } = useWidgetOperations(undefined, undefined, { skipFetch: true });\n\n // Field arrangement is local state so dragging recomputes the pivot instantly.\n // Seeded from config and kept in sync with it: if config changes externally\n // (properties panel, undo, another editor), we re-seed during render via the\n // \"adjust state on prop change\" pattern. Our own persist round-trip re-seeds\n // to the identical layout, so it can't loop.\n const configSig = JSON.stringify({ rows: config.rows, columns: config.columns, values: config.values });\n const [layout, setLayout] = useState<PivotLayout>({\n rows: config.rows,\n columns: config.columns,\n values: config.values,\n });\n const [seededSig, setSeededSig] = useState(configSig);\n if (configSig !== seededSig) {\n // Config changed externally → re-seed during render (React's documented\n // \"adjusting state on prop change\" pattern). Our own persist re-seeds to the\n // identical layout, so this can't loop.\n setSeededSig(configSig);\n setLayout({ rows: config.rows, columns: config.columns, values: config.values });\n }\n\n const { rows: rowFields, columns: columnFields, values: valueFields } = layout;\n\n // Persist the layout to config (edit mode only — viewers explore locally).\n const persistLayout = useCallback(\n (next: PivotLayout) => {\n if (!isEditMode) return;\n void updateWidget({\n id: widget.id,\n config: {\n ...(widget.config as Record<string, unknown> | undefined),\n rows: next.rows,\n columns: next.columns,\n values: next.values,\n },\n });\n },\n [isEditMode, updateWidget, widget.id, widget.config]\n );\n\n const applyLayout = useCallback(\n (next: PivotLayout) => {\n setLayout(next);\n persistLayout(next);\n },\n [persistLayout]\n );\n\n const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set(['root']));\n\n // Build pivot structure\n const pivotTree = useMemo(() => {\n return buildPivotTree(data, rowFields, valueFields);\n }, [data, rowFields, valueFields]);\n\n const columnValues = useMemo(() => {\n return getColumnValues(data, columnFields);\n }, [data, columnFields]);\n\n // Grand totals per column for the footer — computed once (was previously\n // recomputed for every column value inside the footer's render loop).\n const footerColumnData = useMemo(\n () => (config.showColumnTotals ? getValuesByColumn(data, columnFields, valueFields, {}) : null),\n [config.showColumnTotals, data, columnFields, valueFields]\n );\n\n // All fields present in the data, and those not yet placed in a zone.\n const allFields = useMemo(() => (data.length > 0 ? Object.keys(data[0]) : []), [data]);\n const availableFields = useMemo(() => {\n const used = new Set<string>([...rowFields, ...columnFields, ...valueFields.map((v) => v.field)]);\n return allFields.filter((f) => !used.has(f));\n }, [allFields, rowFields, columnFields, valueFields]);\n\n // Move a field from one zone to another and recompute + persist.\n const moveField = useCallback(\n (field: string, from: PivotZone, to: PivotZone) => {\n if (from === to) return;\n const next: PivotLayout = {\n rows: from === 'rows' ? rowFields.filter((f) => f !== field) : [...rowFields],\n columns: from === 'columns' ? columnFields.filter((f) => f !== field) : [...columnFields],\n values: from === 'values' ? valueFields.filter((v) => v.field !== field) : [...valueFields],\n };\n if (to === 'rows' && !next.rows.includes(field)) next.rows.push(field);\n else if (to === 'columns' && !next.columns.includes(field)) next.columns.push(field);\n else if (to === 'values' && !next.values.some((v) => v.field === field)) {\n next.values.push({ field, aggregation: 'sum' });\n }\n applyLayout(next);\n },\n [rowFields, columnFields, valueFields, applyLayout]\n );\n\n const changeAggregation = useCallback(\n (field: string, aggregation: ValueField['aggregation']) => {\n applyLayout({\n rows: [...rowFields],\n columns: [...columnFields],\n values: valueFields.map((v) => (v.field === field ? { ...v, aggregation } : v)),\n });\n },\n [rowFields, columnFields, valueFields, applyLayout]\n );\n\n // Format number based on config\n const formatValue = useCallback(\n (value: number): string => {\n if (isNaN(value)) return '-';\n\n switch (config.numberFormat) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: config.decimalPlaces,\n maximumFractionDigits: config.decimalPlaces,\n }).format(value);\n case 'percentage':\n return new Intl.NumberFormat('en-US', {\n style: 'percent',\n minimumFractionDigits: config.decimalPlaces,\n maximumFractionDigits: config.decimalPlaces,\n }).format(value / 100);\n default:\n return new Intl.NumberFormat('en-US', {\n minimumFractionDigits: config.decimalPlaces,\n maximumFractionDigits: config.decimalPlaces,\n }).format(value);\n }\n },\n [config.numberFormat, config.decimalPlaces]\n );\n\n // Toggle row expansion\n const toggleRow = useCallback((rowPath: string) => {\n setExpandedRows((prev) => {\n const next = new Set(prev);\n if (next.has(rowPath)) {\n next.delete(rowPath);\n } else {\n next.add(rowPath);\n }\n return next;\n });\n }, []);\n\n // Handle cell click for drilldown\n const handleCellClick = useCallback(\n (rowFilter: Record<string, string>, columnValue: string) => {\n if (onDrilldown) {\n onDrilldown({\n rowFilter,\n columnValue,\n type: 'pivot_cell_click',\n });\n }\n },\n [onDrilldown]\n );\n\n // Render row recursively\n const renderRows = useCallback(\n (node: PivotNode, rowFilter: Record<string, string>, parentPath: string, depth: number): JSX.Element[] => {\n const rows: JSX.Element[] = [];\n const hasChildren = node.children.size > 0;\n const isExpanded = expandedRows.has(parentPath);\n const rowField = rowFields[depth - 1];\n\n if (depth > 0) {\n const columnData = getValuesByColumn(data, columnFields, valueFields, rowFilter);\n\n rows.push(\n <tr key={parentPath} className=\"hover:bg-bg-muted/50 transition-colors\">\n {/* Row header */}\n <td\n className=\"sticky left-0 bg-bg-surface border-r border-border-default px-3 py-2 font-medium text-sm\"\n style={{ paddingLeft: `${depth * 16 + 8}px` }}\n >\n <div className=\"flex items-center gap-1\">\n {config.expandable && hasChildren && (\n <button\n type=\"button\"\n onClick={() => toggleRow(parentPath)}\n className=\"p-0.5 hover:bg-bg-muted rounded\"\n >\n {isExpanded ? (\n <ChevronDown className=\"w-4 h-4 text-text-secondary\" />\n ) : (\n <ChevronRight className=\"w-4 h-4 text-text-secondary\" />\n )}\n </button>\n )}\n {!hasChildren && config.expandable && <span className=\"w-5\" />}\n <span>{node.key}</span>\n </div>\n </td>\n\n {/* Value cells */}\n {columnValues.map((colValue) => {\n const cellValues = columnData.get(colValue) || {};\n return valueFields.map((vf) => (\n <td\n key={`${colValue}-${vf.field}`}\n className=\"px-3 py-2 text-sm text-right tabular-nums cursor-pointer hover:bg-action-primary-bg/10\"\n onClick={() => handleCellClick(rowFilter, colValue)}\n >\n {formatValue(cellValues[vf.field] || 0)}\n </td>\n ));\n })}\n\n {/* Row total */}\n {config.showRowTotals &&\n valueFields.map((vf) => (\n <td\n key={`total-${vf.field}`}\n className=\"px-3 py-2 text-sm text-right tabular-nums font-medium bg-bg-muted/30\"\n >\n {formatValue(node.values[vf.field] || 0)}\n </td>\n ))}\n </tr>\n );\n }\n\n // Render children if expanded\n if (hasChildren && (isExpanded || depth === 0)) {\n const sortedKeys = Array.from(node.children.keys()).sort();\n for (const childKey of sortedKeys) {\n const childNode = node.children.get(childKey)!;\n const childPath = `${parentPath}/${childKey}`;\n const childFilter = rowField ? { ...rowFilter, [rowField]: childKey } : rowFilter;\n rows.push(...renderRows(childNode, childFilter, childPath, depth + 1));\n }\n }\n\n return rows;\n },\n [\n data,\n config,\n rowFields,\n columnFields,\n valueFields,\n columnValues,\n expandedRows,\n formatValue,\n handleCellClick,\n toggleRow,\n ]\n );\n\n const hasLayout = rowFields.length > 0 || valueFields.length > 0;\n\n // No data state\n if (data.length === 0) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full p-8 text-text-secondary\">\n <Settings className=\"w-12 h-12 mb-4 opacity-50\" />\n <p className=\"text-sm\">No data available</p>\n <p className=\"text-xs mt-1\">Configure data source to populate pivot table</p>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full overflow-hidden\">\n {/* Drag-and-drop field shelf (edit mode) */}\n {isEditMode && (\n <PivotFieldShelf\n rowFields={rowFields}\n columnFields={columnFields}\n valueFields={valueFields}\n availableFields={availableFields}\n onMoveField={moveField}\n onChangeAggregation={changeAggregation}\n />\n )}\n\n {!hasLayout ? (\n <div className=\"flex flex-col items-center justify-center flex-1 p-8 text-text-secondary\">\n <Settings className=\"w-12 h-12 mb-4 opacity-50\" />\n <p className=\"text-sm\">Configure Pivot Table</p>\n <p className=\"text-xs mt-1\">\n {isEditMode\n ? 'Drag fields into the Rows and Values zones above'\n : 'Add row fields and value fields in widget settings'}\n </p>\n </div>\n ) : (\n <div className=\"flex-1 overflow-auto\">\n <table className=\"w-full border-collapse\">\n {/* Header */}\n <thead className=\"sticky top-0 z-10\">\n {/* Column headers row */}\n <tr className=\"bg-bg-muted\">\n <th className=\"sticky left-0 bg-bg-muted border-r border-b border-border-default px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-text-secondary\">\n {rowFields.length > 0 ? rowFields.map((r) => r.replace(/_/g, ' ')).join(' / ') : 'Values'}\n </th>\n\n {/* Column value headers */}\n {columnValues.map((colValue) => (\n <th\n key={colValue}\n colSpan={valueFields.length || 1}\n className=\"border-b border-border-default px-3 py-2 text-center text-xs font-semibold uppercase tracking-wider text-text-secondary\"\n >\n {colValue}\n </th>\n ))}\n\n {/* Total header */}\n {config.showRowTotals && (\n <th\n colSpan={valueFields.length || 1}\n className=\"border-b border-border-default px-3 py-2 text-center text-xs font-semibold uppercase tracking-wider text-text-secondary bg-bg-muted/50\"\n >\n Total\n </th>\n )}\n </tr>\n\n {/* Value field headers row (if multiple values) */}\n {valueFields.length > 1 && (\n <tr className=\"bg-bg-surface\">\n <th className=\"sticky left-0 bg-bg-surface border-r border-b border-border-default\" />\n {columnValues.map((colValue) =>\n valueFields.map((vf) => (\n <th\n key={`${colValue}-${vf.field}`}\n className=\"border-b border-border-default px-3 py-1 text-center text-xs text-text-secondary\"\n >\n {vf.label || vf.field.replace(/_/g, ' ')}\n </th>\n ))\n )}\n {config.showRowTotals &&\n valueFields.map((vf) => (\n <th\n key={`total-${vf.field}`}\n className=\"border-b border-border-default px-3 py-1 text-center text-xs text-text-secondary bg-bg-muted/30\"\n >\n {vf.label || vf.field.replace(/_/g, ' ')}\n </th>\n ))}\n </tr>\n )}\n </thead>\n\n {/* Body */}\n <tbody className=\"divide-y divide-border-default\">{renderRows(pivotTree, {}, 'root', 0)}</tbody>\n\n {/* Footer with column totals */}\n {config.showColumnTotals && (\n <tfoot>\n <tr className=\"bg-bg-muted/50 font-medium\">\n <td className=\"sticky left-0 bg-bg-muted/50 border-r border-t border-border-default px-3 py-2 text-sm\">\n Total\n </td>\n {columnValues.map((colValue) => {\n const cellValues = footerColumnData?.get(colValue) || {};\n return valueFields.map((vf) => (\n <td\n key={`footer-${colValue}-${vf.field}`}\n className=\"border-t border-border-default px-3 py-2 text-sm text-right tabular-nums\"\n >\n {formatValue(cellValues[vf.field] || 0)}\n </td>\n ));\n })}\n {config.showRowTotals &&\n config.showGrandTotal &&\n valueFields.map((vf) => (\n <td\n key={`grand-${vf.field}`}\n className=\"border-t border-border-default px-3 py-2 text-sm text-right tabular-nums font-bold bg-bg-muted\"\n >\n {formatValue(pivotTree.values[vf.field] || 0)}\n </td>\n ))}\n </tr>\n </tfoot>\n )}\n </table>\n </div>\n )}\n </div>\n );\n});\n\n// ============================================================================\n// Field shelf (drag-and-drop zones)\n// ============================================================================\n\ninterface PivotFieldShelfProps {\n rowFields: string[];\n columnFields: string[];\n valueFields: ValueField[];\n availableFields: string[];\n onMoveField: (field: string, from: PivotZone, to: PivotZone) => void;\n onChangeAggregation: (field: string, aggregation: ValueField['aggregation']) => void;\n}\n\nconst DND_MIME = 'application/x-pivot-field';\n\nconst PivotFieldShelf: FC<PivotFieldShelfProps> = ({\n rowFields,\n columnFields,\n valueFields,\n availableFields,\n onMoveField,\n onChangeAggregation,\n}) => {\n const [dragOverZone, setDragOverZone] = useState<PivotZone | null>(null);\n\n const handleDrop = (zone: PivotZone) => (e: React.DragEvent) => {\n e.preventDefault();\n setDragOverZone(null);\n const raw = e.dataTransfer.getData(DND_MIME) || e.dataTransfer.getData('text/plain');\n if (!raw) return;\n try {\n const { field, from } = JSON.parse(raw) as { field: string; from: PivotZone };\n if (field) onMoveField(field, from, zone);\n } catch {\n // Ignore malformed drag payloads.\n }\n };\n\n const chip = (field: string, from: PivotZone, vf?: ValueField) => (\n <span\n key={field}\n draggable\n onDragStart={(e) => {\n const payload = JSON.stringify({ field, from });\n e.dataTransfer.setData(DND_MIME, payload);\n e.dataTransfer.setData('text/plain', payload);\n e.dataTransfer.effectAllowed = 'move';\n }}\n className=\"inline-flex items-center gap-1 px-2 py-1 rounded-md bg-bg-surface border border-border-default text-xs text-text-primary cursor-grab\"\n >\n <GripVertical className=\"w-3 h-3 text-text-tertiary\" aria-hidden=\"true\" />\n <span>{(vf?.label || field).replace(/_/g, ' ')}</span>\n {vf && (\n <select\n value={vf.aggregation}\n onChange={(e) => onChangeAggregation(field, e.target.value as ValueField['aggregation'])}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Aggregation for ${field}`}\n className=\"ml-1 bg-bg-muted text-text-secondary rounded px-1 py-0.5 text-[10px] uppercase\"\n >\n {AGGREGATIONS.map((agg) => (\n <option key={agg} value={agg}>\n {agg}\n </option>\n ))}\n </select>\n )}\n </span>\n );\n\n const zone = (label: string, name: PivotZone, children: JSX.Element[] | JSX.Element) => (\n <div\n onDragOver={(e) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n if (dragOverZone !== name) setDragOverZone(name);\n }}\n onDragLeave={() => setDragOverZone((z) => (z === name ? null : z))}\n onDrop={handleDrop(name)}\n className={`flex-1 min-w-[8rem] rounded-md border p-2 transition-colors ${\n dragOverZone === name\n ? 'border-action-primary-bg bg-action-primary-bg/5'\n : 'border-border-default bg-bg-muted/30'\n }`}\n >\n <div className=\"text-[10px] font-semibold uppercase tracking-wider text-text-tertiary mb-1.5\">{label}</div>\n <div className=\"flex flex-wrap gap-1.5 min-h-[1.75rem]\">{children}</div>\n </div>\n );\n\n return (\n <div className=\"flex flex-wrap gap-2 p-2 border-b border-border-default bg-bg-surface\">\n {zone(\n 'Rows',\n 'rows',\n rowFields.map((f) => chip(f, 'rows'))\n )}\n {zone(\n 'Columns',\n 'columns',\n columnFields.map((f) => chip(f, 'columns'))\n )}\n {zone(\n 'Values',\n 'values',\n valueFields.map((vf) => chip(vf.field, 'values', vf))\n )}\n {zone(\n 'Available',\n 'available',\n availableFields.length > 0 ? (\n availableFields.map((f) => chip(f, 'available'))\n ) : (\n <span className=\"text-[11px] text-text-tertiary italic\">All fields in use</span>\n )\n )}\n </div>\n );\n};\n\nexport default PivotTableWidget;\n"],"mappings":"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"}
|
|
@@ -1,45 +1,60 @@
|
|
|
1
|
-
import { memo as e, useCallback as t } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { memo as e, useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
|
|
2
|
+
import { GripVertical as a } from "lucide-react";
|
|
3
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
3
4
|
//#region src/bigconsole/components/widgets/table/TableHeader.tsx
|
|
4
|
-
var
|
|
5
|
-
let
|
|
6
|
-
e.sortable !== !1 &&
|
|
7
|
-
}, [
|
|
5
|
+
var c = 60, l = e(function({ columns: e, sortState: l, onSort: u, sticky: d = !0, frozenLefts: f, editable: p = !1, onColumnResize: m, onColumnResizeEnd: h, onColumnReorder: g }) {
|
|
6
|
+
let _ = t((e) => {
|
|
7
|
+
e.sortable !== !1 && u(e.key);
|
|
8
|
+
}, [u]), v = r({}), [y, b] = i(null), [x, S] = i(null), C = r(null), w = r(null), T = t((e, t) => {
|
|
9
|
+
t.preventDefault(), t.stopPropagation();
|
|
10
|
+
let n = v.current[e], r = n ? n.offsetWidth : c, i = t.clientX;
|
|
11
|
+
b(e);
|
|
12
|
+
let a = r, o = (t) => {
|
|
13
|
+
a = Math.max(c, Math.round(r + (t.clientX - i))), m?.(e, a);
|
|
14
|
+
}, s = () => {
|
|
15
|
+
window.removeEventListener("pointermove", o), window.removeEventListener("pointerup", l), w.current = null;
|
|
16
|
+
}, l = () => {
|
|
17
|
+
s(), b(null), h?.(e, a);
|
|
18
|
+
};
|
|
19
|
+
w.current = s, window.addEventListener("pointermove", o), window.addEventListener("pointerup", l);
|
|
20
|
+
}, [m, h]);
|
|
21
|
+
n(() => () => w.current?.(), []);
|
|
22
|
+
let E = (e) => {
|
|
8
23
|
if (e.sortable === !1) return null;
|
|
9
|
-
let t =
|
|
10
|
-
return /* @__PURE__ */
|
|
24
|
+
let t = l.column === e.key ? l.direction : null;
|
|
25
|
+
return /* @__PURE__ */ s("span", {
|
|
11
26
|
className: "inline-flex ml-1",
|
|
12
27
|
children: [
|
|
13
|
-
t === "asc" && /* @__PURE__ */
|
|
28
|
+
t === "asc" && /* @__PURE__ */ o("svg", {
|
|
14
29
|
className: "w-4 h-4 text-action-primary-bg",
|
|
15
30
|
fill: "none",
|
|
16
31
|
stroke: "currentColor",
|
|
17
32
|
viewBox: "0 0 24 24",
|
|
18
|
-
children: /* @__PURE__ */
|
|
33
|
+
children: /* @__PURE__ */ o("path", {
|
|
19
34
|
strokeLinecap: "round",
|
|
20
35
|
strokeLinejoin: "round",
|
|
21
36
|
strokeWidth: 2,
|
|
22
37
|
d: "M5 15l7-7 7 7"
|
|
23
38
|
})
|
|
24
39
|
}),
|
|
25
|
-
t === "desc" && /* @__PURE__ */
|
|
40
|
+
t === "desc" && /* @__PURE__ */ o("svg", {
|
|
26
41
|
className: "w-4 h-4 text-action-primary-bg",
|
|
27
42
|
fill: "none",
|
|
28
43
|
stroke: "currentColor",
|
|
29
44
|
viewBox: "0 0 24 24",
|
|
30
|
-
children: /* @__PURE__ */
|
|
45
|
+
children: /* @__PURE__ */ o("path", {
|
|
31
46
|
strokeLinecap: "round",
|
|
32
47
|
strokeLinejoin: "round",
|
|
33
48
|
strokeWidth: 2,
|
|
34
49
|
d: "M19 9l-7 7-7-7"
|
|
35
50
|
})
|
|
36
51
|
}),
|
|
37
|
-
!t && /* @__PURE__ */
|
|
52
|
+
!t && /* @__PURE__ */ o("svg", {
|
|
38
53
|
className: "w-4 h-4 text-text-secondary opacity-0 group-hover:opacity-100",
|
|
39
54
|
fill: "none",
|
|
40
55
|
stroke: "currentColor",
|
|
41
56
|
viewBox: "0 0 24 24",
|
|
42
|
-
children: /* @__PURE__ */
|
|
57
|
+
children: /* @__PURE__ */ o("path", {
|
|
43
58
|
strokeLinecap: "round",
|
|
44
59
|
strokeLinejoin: "round",
|
|
45
60
|
strokeWidth: 2,
|
|
@@ -49,16 +64,37 @@ var i = e(function({ columns: e, sortState: i, onSort: a, sticky: o = !0, frozen
|
|
|
49
64
|
]
|
|
50
65
|
});
|
|
51
66
|
};
|
|
52
|
-
return /* @__PURE__ */
|
|
67
|
+
return /* @__PURE__ */ o("thead", {
|
|
53
68
|
className: `
|
|
54
69
|
bg-bg-muted
|
|
55
|
-
${
|
|
70
|
+
${d ? "sticky top-0 z-10" : ""}
|
|
56
71
|
`,
|
|
57
|
-
children: /* @__PURE__ */
|
|
58
|
-
let t =
|
|
59
|
-
return /* @__PURE__ */
|
|
72
|
+
children: /* @__PURE__ */ o("tr", { children: e.map((e) => {
|
|
73
|
+
let t = f?.[e.key], n = e.width, r = p && !!g && y === null, i = x === e.key && C.current !== e.key;
|
|
74
|
+
return /* @__PURE__ */ s("th", {
|
|
75
|
+
ref: (t) => {
|
|
76
|
+
v.current[e.key] = t;
|
|
77
|
+
},
|
|
78
|
+
draggable: r,
|
|
79
|
+
onDragStart: r ? (t) => {
|
|
80
|
+
C.current = e.key, t.dataTransfer.effectAllowed = "move", t.dataTransfer.setData("text/plain", e.key);
|
|
81
|
+
} : void 0,
|
|
82
|
+
onDragOver: p && g ? (t) => {
|
|
83
|
+
C.current && C.current !== e.key && (t.preventDefault(), t.dataTransfer.dropEffect = "move", x !== e.key && S(e.key));
|
|
84
|
+
} : void 0,
|
|
85
|
+
onDragLeave: p && g ? () => {
|
|
86
|
+
x === e.key && S(null);
|
|
87
|
+
} : void 0,
|
|
88
|
+
onDrop: p && g ? (t) => {
|
|
89
|
+
t.preventDefault();
|
|
90
|
+
let n = C.current;
|
|
91
|
+
C.current = null, S(null), n && n !== e.key && g(n, e.key);
|
|
92
|
+
} : void 0,
|
|
93
|
+
onDragEnd: r ? () => {
|
|
94
|
+
C.current = null, S(null);
|
|
95
|
+
} : void 0,
|
|
60
96
|
className: `
|
|
61
|
-
px-4 py-3
|
|
97
|
+
relative px-4 py-3
|
|
62
98
|
text-xs font-semibold uppercase tracking-wider
|
|
63
99
|
text-text-secondary
|
|
64
100
|
border-b border-border-default
|
|
@@ -66,27 +102,51 @@ var i = e(function({ columns: e, sortState: i, onSort: a, sticky: o = !0, frozen
|
|
|
66
102
|
${e.align === "center" ? "text-center" : ""}
|
|
67
103
|
${e.align === "right" ? "text-right" : "text-left"}
|
|
68
104
|
${t == null ? "" : "sticky z-20 bg-bg-muted"}
|
|
105
|
+
${i ? "border-l-2 border-l-action-primary-bg" : ""}
|
|
69
106
|
`,
|
|
70
|
-
style: t == null ? { width:
|
|
71
|
-
width:
|
|
107
|
+
style: t == null ? { width: n } : {
|
|
108
|
+
width: n,
|
|
72
109
|
left: t
|
|
73
110
|
},
|
|
74
|
-
onClick: () =>
|
|
111
|
+
onClick: () => _(e),
|
|
75
112
|
role: e.sortable === !1 ? void 0 : "button",
|
|
76
113
|
tabIndex: e.sortable === !1 ? void 0 : 0,
|
|
77
114
|
onKeyDown: (t) => {
|
|
78
|
-
e.sortable !== !1 && (t.key === "Enter" || t.key === " ") && (t.preventDefault(),
|
|
115
|
+
e.sortable !== !1 && (t.key === "Enter" || t.key === " ") && (t.preventDefault(), _(e));
|
|
79
116
|
},
|
|
80
|
-
"aria-sort":
|
|
81
|
-
children: /* @__PURE__ */
|
|
117
|
+
"aria-sort": l.column === e.key ? l.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
118
|
+
children: [/* @__PURE__ */ s("div", {
|
|
82
119
|
className: "inline-flex items-center",
|
|
83
|
-
children: [
|
|
84
|
-
|
|
120
|
+
children: [
|
|
121
|
+
p && g && /* @__PURE__ */ o("span", {
|
|
122
|
+
className: "mr-1 -ml-1 cursor-grab text-text-tertiary opacity-0 group-hover:opacity-100",
|
|
123
|
+
onClick: (e) => e.stopPropagation(),
|
|
124
|
+
"aria-hidden": "true",
|
|
125
|
+
title: "Drag to reorder column",
|
|
126
|
+
children: /* @__PURE__ */ o(a, { className: "w-3.5 h-3.5" })
|
|
127
|
+
}),
|
|
128
|
+
e.header,
|
|
129
|
+
E(e)
|
|
130
|
+
]
|
|
131
|
+
}), p && m && /* @__PURE__ */ o("span", {
|
|
132
|
+
role: "separator",
|
|
133
|
+
"aria-orientation": "vertical",
|
|
134
|
+
"aria-label": `Resize ${e.header} column`,
|
|
135
|
+
className: `
|
|
136
|
+
absolute top-0 right-0 z-30 h-full w-1.5
|
|
137
|
+
cursor-col-resize select-none
|
|
138
|
+
hover:bg-action-primary-bg/40
|
|
139
|
+
${y === e.key ? "bg-action-primary-bg/60" : ""}
|
|
140
|
+
`,
|
|
141
|
+
onPointerDown: (t) => T(e.key, t),
|
|
142
|
+
onClick: (e) => e.stopPropagation(),
|
|
143
|
+
draggable: !1
|
|
144
|
+
})]
|
|
85
145
|
}, e.key);
|
|
86
146
|
}) })
|
|
87
147
|
});
|
|
88
148
|
});
|
|
89
149
|
//#endregion
|
|
90
|
-
export {
|
|
150
|
+
export { l as default };
|
|
91
151
|
|
|
92
152
|
//# sourceMappingURL=TableHeader.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TableHeader.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableHeader.tsx"],"sourcesContent":["/**\n * TableHeader Component\n *\n * Sortable table header with column definitions.\n */\n\nimport { type FC, memo, useCallback } from 'react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ColumnDefinition {\n /** Unique key for the column */\n key: string;\n /** Display header text */\n header: string;\n /** Whether column is sortable */\n sortable?: boolean;\n /** Column width (CSS value) */\n width?: string;\n /** Text alignment */\n align?: 'left' | 'center' | 'right';\n}\n\nexport type SortDirection = 'asc' | 'desc' | null;\n\nexport interface SortState {\n column: string | null;\n direction: SortDirection;\n}\n\nexport interface TableHeaderProps {\n /** Column definitions */\n columns: ColumnDefinition[];\n /** Current sort state */\n sortState: SortState;\n /** Callback when sort changes */\n onSort: (column: string) => void;\n /** Whether header is sticky */\n sticky?: boolean;\n /** column key → sticky-left px offset for frozen leading columns */\n frozenLefts?: Record<string, number>;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableHeader: FC<TableHeaderProps> = memo(function TableHeader({\n columns,\n sortState,\n onSort,\n sticky = true,\n frozenLefts,\n}) {\n const handleSort = useCallback(\n (column: ColumnDefinition) => {\n if (column.sortable !== false) {\n onSort(column.key);\n }\n },\n [onSort]\n );\n\n const getSortIcon = (column: ColumnDefinition) => {\n if (column.sortable === false) return null;\n\n const isActive = sortState.column === column.key;\n const direction = isActive ? sortState.direction : null;\n\n return (\n <span className=\"inline-flex ml-1\">\n {direction === 'asc' && (\n <svg className=\"w-4 h-4 text-action-primary-bg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 15l7-7 7 7\" />\n </svg>\n )}\n {direction === 'desc' && (\n <svg className=\"w-4 h-4 text-action-primary-bg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n )}\n {!direction && (\n <svg\n className=\"w-4 h-4 text-text-secondary opacity-0 group-hover:opacity-100\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4\"\n />\n </svg>\n )}\n </span>\n );\n };\n\n return (\n <thead\n className={`\n bg-bg-muted\n ${sticky ? 'sticky top-0 z-10' : ''}\n `}\n >\n <tr>\n {columns.map((column) => {\n const frozen = frozenLefts?.[column.key];\n return (\n <th\n key={column.key}\n className={`\n px-4 py-3\n text-xs font-semibold uppercase tracking-wider\n text-text-secondary\n border-b border-border-default\n ${column.sortable !== false ? 'cursor-pointer select-none group hover:bg-bg-surface' : ''}\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${frozen != null ? 'sticky z-20 bg-bg-muted' : ''}\n `}\n style={frozen != null ? { width: column.width, left: frozen } : { width: column.width }}\n onClick={() => handleSort(column)}\n role={column.sortable !== false ? 'button' : undefined}\n tabIndex={column.sortable !== false ? 0 : undefined}\n onKeyDown={(e) => {\n if (column.sortable !== false && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleSort(column);\n }\n }}\n aria-sort={\n sortState.column === column.key\n ? sortState.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined\n }\n >\n <div className=\"inline-flex items-center\">\n {column.header}\n {getSortIcon(column)}\n </div>\n </th>\n );\n })}\n </tr>\n </thead>\n );\n});\n\nexport default TableHeader;\n"],"mappings":";;;AAiDA,IAAa,IAAoC,EAAK,SAAqB,EACzE,YACA,cACA,WACA,YAAS,IACT,kBACC;CACD,IAAM,IAAa,GAChB,MAA6B;AAC5B,EAAI,EAAO,aAAa,MACtB,EAAO,EAAO,IAAI;IAGtB,CAAC,EAAO,CACT,EAEK,KAAe,MAA6B;AAChD,MAAI,EAAO,aAAa,GAAO,QAAO;EAGtC,IAAM,IADW,EAAU,WAAW,EAAO,MAChB,EAAU,YAAY;AAEnD,SACE,kBAAC,QAAD;GAAM,WAAU;aAAhB;IACG,MAAc,SACb,kBAAC,OAAD;KAAK,WAAU;KAAiC,MAAK;KAAO,QAAO;KAAe,SAAQ;eACxF,kBAAC,QAAD;MAAM,eAAc;MAAQ,gBAAe;MAAQ,aAAa;MAAG,GAAE;MAAkB,CAAA;KACnF,CAAA;IAEP,MAAc,UACb,kBAAC,OAAD;KAAK,WAAU;KAAiC,MAAK;KAAO,QAAO;KAAe,SAAQ;eACxF,kBAAC,QAAD;MAAM,eAAc;MAAQ,gBAAe;MAAQ,aAAa;MAAG,GAAE;MAAmB,CAAA;KACpF,CAAA;IAEP,CAAC,KACA,kBAAC,OAAD;KACE,WAAU;KACV,MAAK;KACL,QAAO;KACP,SAAQ;eAER,kBAAC,QAAD;MACE,eAAc;MACd,gBAAe;MACf,aAAa;MACb,GAAE;MACF,CAAA;KACE,CAAA;IAEH;;;AAIX,QACE,kBAAC,SAAD;EACE,WAAW;;UAEP,IAAS,sBAAsB,GAAG;;YAGtC,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAS,IAAc,EAAO;AACpC,UACE,kBAAC,MAAD;IAEE,WAAW;;;;;gBAKT,EAAO,aAAa,KAAiE,KAAzD,uDAA4D;gBACxF,EAAO,UAAU,WAAW,gBAAgB,GAAG;gBAC/C,EAAO,UAAU,UAAU,eAAe,YAAY;gBACtD,KAAU,OAAmC,KAA5B,0BAA+B;;IAElD,OAAO,KAAU,OAA+C,EAAE,OAAO,EAAO,OAAO,GAA/D;KAAE,OAAO,EAAO;KAAO,MAAM;KAAQ;IAC7D,eAAe,EAAW,EAAO;IACjC,MAAM,EAAO,aAAa,KAAmB,KAAA,IAAX;IAClC,UAAU,EAAO,aAAa,KAAY,KAAA,IAAJ;IACtC,YAAY,MAAM;AAChB,KAAI,EAAO,aAAa,OAAU,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAC/D,EAAE,gBAAgB,EAClB,EAAW,EAAO;;IAGtB,aACE,EAAU,WAAW,EAAO,MACxB,EAAU,cAAc,QACtB,cACA,eACF,KAAA;cAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAO,QACP,EAAY,EAAO,CAChB;;IACH,EAjCE,EAAO,IAiCT;IAEP,EACC,CAAA;EACC,CAAA;EAEV"}
|
|
1
|
+
{"version":3,"file":"TableHeader.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableHeader.tsx"],"sourcesContent":["/**\n * TableHeader Component\n *\n * Sortable table header with column definitions. In edit mode it also supports\n * interactive column resize (drag the right border) and reorder (drag the grip\n * handle), each surfaced through callbacks the parent persists to widget config.\n */\n\nimport { type FC, memo, useCallback, useEffect, useRef, useState } from 'react';\nimport { GripVertical } from 'lucide-react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ColumnDefinition {\n /** Unique key for the column */\n key: string;\n /** Display header text */\n header: string;\n /** Whether column is sortable */\n sortable?: boolean;\n /** Column width (CSS value) */\n width?: string;\n /** Text alignment */\n align?: 'left' | 'center' | 'right';\n}\n\nexport type SortDirection = 'asc' | 'desc' | null;\n\nexport interface SortState {\n column: string | null;\n direction: SortDirection;\n}\n\nexport interface TableHeaderProps {\n /** Column definitions */\n columns: ColumnDefinition[];\n /** Current sort state */\n sortState: SortState;\n /** Callback when sort changes */\n onSort: (column: string) => void;\n /** Whether header is sticky */\n sticky?: boolean;\n /** column key → sticky-left px offset for frozen leading columns */\n frozenLefts?: Record<string, number>;\n /** Enable interactive resize + reorder (edit mode). */\n editable?: boolean;\n /** Called on every resize move with the in-progress width (px) for live feedback. */\n onColumnResize?: (key: string, widthPx: number) => void;\n /** Called once when a resize drag ends with the final width (px) — parent persists. */\n onColumnResizeEnd?: (key: string, widthPx: number) => void;\n /** Called when a column is dropped onto another to reorder — parent persists. */\n onColumnReorder?: (sourceKey: string, targetKey: string) => void;\n}\n\nconst MIN_COLUMN_WIDTH = 60;\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableHeader: FC<TableHeaderProps> = memo(function TableHeader({\n columns,\n sortState,\n onSort,\n sticky = true,\n frozenLefts,\n editable = false,\n onColumnResize,\n onColumnResizeEnd,\n onColumnReorder,\n}) {\n const handleSort = useCallback(\n (column: ColumnDefinition) => {\n if (column.sortable !== false) {\n onSort(column.key);\n }\n },\n [onSort]\n );\n\n // Refs to each <th> so a resize drag can read its current rendered width.\n const thRefs = useRef<Record<string, HTMLTableCellElement | null>>({});\n // Key being resized — disabled drag-reorder while resizing, and used for cursor.\n const [resizingKey, setResizingKey] = useState<string | null>(null);\n const [dragOverKey, setDragOverKey] = useState<string | null>(null);\n const dragSourceKey = useRef<string | null>(null);\n // Removes the active resize window-listeners; set during a drag, called on\n // pointer-up and on unmount so a drag interrupted by unmount can't leak.\n const teardownResizeRef = useRef<(() => void) | null>(null);\n\n // Start a resize drag from the right-border handle. Uses window listeners so\n // the pointer can leave the (narrow) handle without dropping the gesture.\n const startResize = useCallback(\n (key: string, startEvent: React.PointerEvent) => {\n startEvent.preventDefault();\n startEvent.stopPropagation();\n const th = thRefs.current[key];\n const startWidth = th ? th.offsetWidth : MIN_COLUMN_WIDTH;\n const startX = startEvent.clientX;\n setResizingKey(key);\n\n let lastWidth = startWidth;\n const onMove = (e: PointerEvent) => {\n lastWidth = Math.max(MIN_COLUMN_WIDTH, Math.round(startWidth + (e.clientX - startX)));\n onColumnResize?.(key, lastWidth);\n };\n const teardown = () => {\n window.removeEventListener('pointermove', onMove);\n window.removeEventListener('pointerup', onUp);\n teardownResizeRef.current = null;\n };\n const onUp = () => {\n teardown();\n setResizingKey(null);\n onColumnResizeEnd?.(key, lastWidth);\n };\n teardownResizeRef.current = teardown;\n window.addEventListener('pointermove', onMove);\n window.addEventListener('pointerup', onUp);\n },\n [onColumnResize, onColumnResizeEnd]\n );\n\n // Detach any in-progress resize listeners if the header unmounts mid-drag.\n useEffect(() => () => teardownResizeRef.current?.(), []);\n\n const getSortIcon = (column: ColumnDefinition) => {\n if (column.sortable === false) return null;\n\n const isActive = sortState.column === column.key;\n const direction = isActive ? sortState.direction : null;\n\n return (\n <span className=\"inline-flex ml-1\">\n {direction === 'asc' && (\n <svg className=\"w-4 h-4 text-action-primary-bg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 15l7-7 7 7\" />\n </svg>\n )}\n {direction === 'desc' && (\n <svg className=\"w-4 h-4 text-action-primary-bg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n )}\n {!direction && (\n <svg\n className=\"w-4 h-4 text-text-secondary opacity-0 group-hover:opacity-100\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4\"\n />\n </svg>\n )}\n </span>\n );\n };\n\n return (\n <thead\n className={`\n bg-bg-muted\n ${sticky ? 'sticky top-0 z-10' : ''}\n `}\n >\n <tr>\n {columns.map((column) => {\n const frozen = frozenLefts?.[column.key];\n // Width (incl. live resize overrides) is baked into column.width by the parent.\n const width = column.width;\n const reorderable = editable && !!onColumnReorder && resizingKey === null;\n const isDropTarget = dragOverKey === column.key && dragSourceKey.current !== column.key;\n return (\n <th\n key={column.key}\n ref={(el) => {\n thRefs.current[column.key] = el;\n }}\n draggable={reorderable}\n onDragStart={\n reorderable\n ? (e) => {\n dragSourceKey.current = column.key;\n e.dataTransfer.effectAllowed = 'move';\n // Some browsers require data to be set for a drag to start.\n e.dataTransfer.setData('text/plain', column.key);\n }\n : undefined\n }\n onDragOver={\n editable && onColumnReorder\n ? (e) => {\n if (dragSourceKey.current && dragSourceKey.current !== column.key) {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n if (dragOverKey !== column.key) setDragOverKey(column.key);\n }\n }\n : undefined\n }\n onDragLeave={\n editable && onColumnReorder\n ? () => {\n if (dragOverKey === column.key) setDragOverKey(null);\n }\n : undefined\n }\n onDrop={\n editable && onColumnReorder\n ? (e) => {\n e.preventDefault();\n const source = dragSourceKey.current;\n dragSourceKey.current = null;\n setDragOverKey(null);\n if (source && source !== column.key) {\n onColumnReorder(source, column.key);\n }\n }\n : undefined\n }\n onDragEnd={\n reorderable\n ? () => {\n dragSourceKey.current = null;\n setDragOverKey(null);\n }\n : undefined\n }\n className={`\n relative px-4 py-3\n text-xs font-semibold uppercase tracking-wider\n text-text-secondary\n border-b border-border-default\n ${column.sortable !== false ? 'cursor-pointer select-none group hover:bg-bg-surface' : ''}\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${frozen != null ? 'sticky z-20 bg-bg-muted' : ''}\n ${isDropTarget ? 'border-l-2 border-l-action-primary-bg' : ''}\n `}\n style={frozen != null ? { width, left: frozen } : { width }}\n onClick={() => handleSort(column)}\n role={column.sortable !== false ? 'button' : undefined}\n tabIndex={column.sortable !== false ? 0 : undefined}\n onKeyDown={(e) => {\n if (column.sortable !== false && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleSort(column);\n }\n }}\n aria-sort={\n sortState.column === column.key\n ? sortState.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined\n }\n >\n <div className=\"inline-flex items-center\">\n {editable && onColumnReorder && (\n // Reorder grip — its own drag surface so dragging it (not the\n // label) starts a reorder, leaving the label click free to sort.\n <span\n className=\"mr-1 -ml-1 cursor-grab text-text-tertiary opacity-0 group-hover:opacity-100\"\n onClick={(e) => e.stopPropagation()}\n aria-hidden=\"true\"\n title=\"Drag to reorder column\"\n >\n <GripVertical className=\"w-3.5 h-3.5\" />\n </span>\n )}\n {column.header}\n {getSortIcon(column)}\n </div>\n {editable && onColumnResize && (\n // Resize handle on the right border. Pointer-only; stops the sort\n // click and the reorder drag from firing.\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label={`Resize ${column.header} column`}\n className={`\n absolute top-0 right-0 z-30 h-full w-1.5\n cursor-col-resize select-none\n hover:bg-action-primary-bg/40\n ${resizingKey === column.key ? 'bg-action-primary-bg/60' : ''}\n `}\n onPointerDown={(e) => startResize(column.key, e)}\n onClick={(e) => e.stopPropagation()}\n draggable={false}\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n );\n});\n\nexport default TableHeader;\n"],"mappings":"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"}
|