@burdenoff/microfe-bigconsole 2026.615.1 → 2026.615.3
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/WidgetHeader.js +78 -59
- package/dist/bigconsole/components/widgets/WidgetHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetWrapper.js +201 -185
- package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js +200 -0
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js.map +1 -0
- 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/hooks/index.js +1 -0
- package/dist/bigconsole/hooks/useWidgetComments.js +81 -0
- package/dist/bigconsole/hooks/useWidgetComments.js.map +1 -0
- package/dist/generated/wspace-operations.js +282 -201
- package/dist/generated/wspace-operations.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":";;;;AAmDA,IAAM,IAAqE;CACzE,MAAM,MAAW,EAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE;CAClD,QAAQ,MAAW,EAAO;CAC1B,UAAU,MAAY,EAAO,SAAS,EAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,EAAO,SAAS;CAC1F,MAAM,MAAY,EAAO,SAAS,KAAK,IAAI,GAAG,EAAO,GAAG;CACxD,MAAM,MAAY,EAAO,SAAS,KAAK,IAAI,GAAG,EAAO,GAAG;CACzD;AAMD,SAAS,EACP,GACA,GACA,GACA,IAAgB,GACL;CACX,IAAM,IAAkB;EACtB,KAAK;EACL,QAAQ,EAAE;EACV,0BAAU,IAAI,KAAK;EACnB,UAAU;EACV;EACD;AAED,KAAI,EAAU,WAAW,GAAG;AAE1B,OAAK,IAAM,KAAM,GAAa;GAC5B,IAAM,IAAS,EAAK,KAAK,MAAQ,OAAO,EAAI,EAAG,OAAO,IAAI,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;AACrF,KAAK,OAAO,EAAG,SAAS,EAAqB,EAAG,aAAa,EAAO;;AAEtE,SAAO;;CAGT,IAAM,IAAe,EAAU,IACzB,IAAkB,EAAU,MAAM,EAAE,EAGpC,oBAAS,IAAI,KAAwC;AAC3D,MAAK,IAAM,KAAO,GAAM;EACtB,IAAM,IAAM,OAAO,EAAI,MAAiB,UAAU;AAIlD,EAHK,EAAO,IAAI,EAAI,IAClB,EAAO,IAAI,GAAK,EAAE,CAAC,EAErB,EAAO,IAAI,EAAI,CAAE,KAAK,EAAI;;AAI5B,MAAK,IAAM,CAAC,GAAK,MAAc,GAAQ;EACrC,IAAM,IAAY,EAAe,GAAW,GAAiB,GAAa,IAAQ,EAAE;AAEpF,EADA,EAAU,MAAM,GAChB,EAAK,SAAS,IAAI,GAAK,EAAU;;AAInC,MAAK,IAAM,KAAM,GAAa;EAC5B,IAAM,IAAS,EAAK,KAAK,MAAQ,OAAO,EAAI,EAAG,OAAO,IAAI,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;AACrF,IAAK,OAAO,EAAG,SAAS,EAAqB,EAAG,aAAa,EAAO;;AAGtE,QAAO;;AAGT,SAAS,EAAgB,GAAiC,GAAkC;AAC1F,KAAI,EAAa,WAAW,EAAG,QAAO,CAAC,QAAQ;CAE/C,IAAM,IAAc,EAAa,IAC3B,oBAAe,IAAI,KAAa;AACtC,MAAK,IAAM,KAAO,EAChB,GAAa,IAAI,OAAO,EAAI,MAAgB,UAAU,CAAC;AAEzD,QAAO,MAAM,KAAK,EAAa,CAAC,MAAM;;AAGxC,SAAS,EACP,GACA,GACA,GACA,GACqC;CAErC,IAAI,IAAe;AACnB,MAAK,IAAM,CAAC,GAAO,MAAU,OAAO,QAAQ,EAAU,CACpD,KAAe,EAAa,QAAQ,MAAQ,OAAO,EAAI,GAAO,KAAK,EAAM;CAG3E,IAAM,oBAAS,IAAI,KAAqC;AAExD,KAAI,EAAa,WAAW,GAAG;EAE7B,IAAM,IAAiC,EAAE;AACzC,OAAK,IAAM,KAAM,GAAa;GAC5B,IAAM,IAAO,EAAa,KAAK,MAAQ,OAAO,EAAI,EAAG,OAAO,IAAI,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;AAC3F,KAAO,EAAG,SAAS,EAAqB,EAAG,aAAa,EAAK;;AAG/D,SADA,EAAO,IAAI,SAAS,EAAO,EACpB;;CAGT,IAAM,IAAc,EAAa,IAC3B,IAAe,EAAgB,GAAM,EAAa;AAExD,MAAK,IAAM,KAAY,GAAc;EACnC,IAAM,IAAU,EAAa,QAAQ,MAAQ,OAAO,EAAI,GAAa,KAAK,EAAS,EAC7E,IAAiC,EAAE;AACzC,OAAK,IAAM,KAAM,GAAa;GAC5B,IAAM,IAAO,EAAQ,KAAK,MAAQ,OAAO,EAAI,EAAG,OAAO,IAAI,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;AACtF,KAAO,EAAG,SAAS,EAAqB,EAAG,aAAa,EAAK;;AAE/D,IAAO,IAAI,GAAU,EAAO;;AAG9B,QAAO;;AAOT,IAAa,IAA8C,EAAK,SAA0B,EACxF,WACA,MAAM,IAAU,EAAE,EAClB,kBACC;CAED,IAAM,IAAO,QAAe,MAAM,QAAQ,EAAQ,GAAG,IAAU,EAAE,EAAG,CAAC,EAAQ,CAAC,EACxE,IAA2B,QAAc;EAC7C,IAAM,IAAkC;GACtC,MAAM,EAAE;GACR,SAAS,EAAE;GACX,QAAQ,EAAE;GACV,eAAe;GACf,kBAAkB;GAClB,gBAAgB;GAChB,YAAY;GACZ,cAAc;GACd,eAAe;GAChB;AACD,SAAO,EAAO,SAAS;GAAE,GAAG;GAAe,GAAI,EAAO;GAAsC,GAAG;IAC9F,CAAC,EAAO,OAAO,CAAC,EAEb,CAAC,GAAc,KAAmB,EAAsB,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,EAG1E,IAAY,QACT,EAAe,GAAM,EAAO,MAAM,EAAO,OAAO,EACtD;EAAC;EAAM,EAAO;EAAM,EAAO;EAAO,CAAC,EAEhC,IAAe,QACZ,EAAgB,GAAM,EAAO,QAAQ,EAC3C,CAAC,GAAM,EAAO,QAAQ,CAAC,EAGpB,IAAc,GACjB,MAA0B;AACzB,MAAI,MAAM,EAAM,CAAE,QAAO;AAEzB,UAAQ,EAAO,cAAf;GACE,KAAK,WACH,QAAO,IAAI,KAAK,aAAa,SAAS;IACpC,OAAO;IACP,UAAU;IACV,uBAAuB,EAAO;IAC9B,uBAAuB,EAAO;IAC/B,CAAC,CAAC,OAAO,EAAM;GAClB,KAAK,aACH,QAAO,IAAI,KAAK,aAAa,SAAS;IACpC,OAAO;IACP,uBAAuB,EAAO;IAC9B,uBAAuB,EAAO;IAC/B,CAAC,CAAC,OAAO,IAAQ,IAAI;GACxB,QACE,QAAO,IAAI,KAAK,aAAa,SAAS;IACpC,uBAAuB,EAAO;IAC9B,uBAAuB,EAAO;IAC/B,CAAC,CAAC,OAAO,EAAM;;IAGtB,CAAC,EAAO,cAAc,EAAO,cAAc,CAC5C,EAGK,IAAY,GAAa,MAAoB;AACjD,KAAiB,MAAS;GACxB,IAAM,IAAO,IAAI,IAAI,EAAK;AAM1B,UALI,EAAK,IAAI,EAAQ,GACnB,EAAK,OAAO,EAAQ,GAEpB,EAAK,IAAI,EAAQ,EAEZ;IACP;IACD,EAAE,CAAC,EAGA,IAAkB,GACrB,GAAmC,MAAwB;AAC1D,EAAI,KACF,EAAY;GACV;GACA;GACA,MAAM;GACP,CAAC;IAGN,CAAC,EAAY,CACd,EAGK,IAAa,GAChB,GAAiB,GAAmC,GAAoB,MAAiC;EACxG,IAAM,IAAsB,EAAE,EACxB,IAAc,EAAK,SAAS,OAAO,GACnC,IAAa,EAAa,IAAI,EAAW,EACzC,IAAW,EAAO,KAAK,IAAQ;AAErC,MAAI,IAAQ,GAAG;GACb,IAAM,IAAa,EAAkB,GAAM,EAAO,SAAS,EAAO,QAAQ,EAAU;AAEpF,KAAK,KACH,kBAAC,MAAD;IAAqB,WAAU;cAA/B;KAEE,kBAAC,MAAD;MACE,WAAU;MACV,OAAO,EAAE,aAAa,GAAG,IAAQ,KAAK,EAAE,KAAK;gBAE7C,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACG,EAAO,cAAc,KACpB,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAU,EAAW;SACpC,WAAU;mBAGR,EADD,IACE,IAEA,GAFD,EAAa,WAAU,+BAAgC,CAEC;SAEnD,CAAA;QAEV,CAAC,KAAe,EAAO,cAAc,kBAAC,QAAD,EAAM,WAAU,OAAQ,CAAA;QAC9D,kBAAC,QAAD,EAAA,UAAO,EAAK,KAAW,CAAA;QACnB;;MACH,CAAA;KAGJ,EAAa,KAAK,MAAa;MAC9B,IAAM,IAAa,EAAW,IAAI,EAAS,IAAI,EAAE;AACjD,aAAO,EAAO,OAAO,KAAK,MACxB,kBAAC,MAAD;OAEE,WAAU;OACV,eAAe,EAAgB,GAAW,EAAS;iBAElD,EAAY,EAAW,EAAG,UAAU,EAAE;OACpC,EALE,GAAG,EAAS,GAAG,EAAG,QAKpB,CACL;OACF;KAGD,EAAO,iBACN,EAAO,OAAO,KAAK,MACjB,kBAAC,MAAD;MAEE,WAAU;gBAET,EAAY,EAAK,OAAO,EAAG,UAAU,EAAE;MACrC,EAJE,SAAS,EAAG,QAId,CACL;KACD;MAjDI,EAiDJ,CACN;;AAIH,MAAI,MAAgB,KAAc,MAAU,IAAI;GAC9C,IAAM,IAAa,MAAM,KAAK,EAAK,SAAS,MAAM,CAAC,CAAC,MAAM;AAC1D,QAAK,IAAM,KAAY,GAAY;IACjC,IAAM,IAAY,EAAK,SAAS,IAAI,EAAS,EACvC,IAAY,GAAG,EAAW,GAAG,KAC7B,IAAc,IAAW;KAAE,GAAG;MAAY,IAAW;KAAU,GAAG;AACxE,MAAK,KAAK,GAAG,EAAW,GAAW,GAAa,GAAW,IAAQ,EAAE,CAAC;;;AAI1E,SAAO;IAET;EAAC;EAAM;EAAQ;EAAc;EAAc;EAAa;EAAiB;EAAU,CACpF;AAwBD,QArBI,EAAK,WAAW,IAEhB,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD,EAAU,WAAU,6BAA8B,CAAA;GAClD,kBAAC,KAAD;IAAG,WAAU;cAAU;IAAqB,CAAA;GAC5C,kBAAC,KAAD;IAAG,WAAU;cAAe;IAAiD,CAAA;GACzE;MAKN,EAAO,KAAK,WAAW,KAAK,EAAO,OAAO,WAAW,IAErD,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD,EAAU,WAAU,6BAA8B,CAAA;GAClD,kBAAC,KAAD;IAAG,WAAU;cAAU;IAAyB,CAAA;GAChD,kBAAC,KAAD;IAAG,WAAU;cAAe;IAAsD,CAAA;GAC9E;MAKR,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;cAAjB;KAEE,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CAEE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAO,KAAK,SAAS,IAAI,EAAO,KAAK,KAAK,MAAM,EAAE,QAAQ,MAAM,IAAI,CAAC,CAAC,KAAK,MAAM,GAAG;SAClF,CAAA;QAGJ,EAAa,KAAK,MACjB,kBAAC,MAAD;SAEE,SAAS,EAAO,OAAO;SACvB,WAAU;mBAET;SACE,EALE,EAKF,CACL;QAGD,EAAO,iBACN,kBAAC,MAAD;SACE,SAAS,EAAO,OAAO;SACvB,WAAU;mBACX;SAEI,CAAA;QAEJ;UAGJ,EAAO,OAAO,SAAS,KACtB,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,MAAD,EAAI,WAAU,uEAAwE,CAAA;QACrF,EAAa,KAAK,MACjB,EAAO,OAAO,KAAK,MACjB,kBAAC,MAAD;SAEE,WAAU;mBAET,EAAG,SAAS,EAAG,MAAM,QAAQ,MAAM,IAAI;SACrC,EAJE,GAAG,EAAS,GAAG,EAAG,QAIpB,CACL,CACH;QACA,EAAO,iBACN,EAAO,OAAO,KAAK,MACjB,kBAAC,MAAD;SAEE,WAAU;mBAET,EAAG,SAAS,EAAG,MAAM,QAAQ,MAAM,IAAI;SACrC,EAJE,SAAS,EAAG,QAId,CACL;QACD;SAED;;KAGR,kBAAC,SAAD;MAAO,WAAU;gBAAkC,EAAW,GAAW,EAAE,EAAE,QAAQ,EAAE;MAAS,CAAA;KAG/F,EAAO,oBACN,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAyF;QAElG,CAAA;OACJ,EAAa,KAAK,MAAa;QAE9B,IAAM,IADa,EAAkB,GAAM,EAAO,SAAS,EAAO,QAAQ,EAAE,CAAC,CAC/C,IAAI,EAAS,IAAI,EAAE;AACjD,eAAO,EAAO,OAAO,KAAK,MACxB,kBAAC,MAAD;SAEE,WAAU;mBAET,EAAY,EAAW,EAAG,UAAU,EAAE;SACpC,EAJE,UAAU,EAAS,GAAG,EAAG,QAI3B,CACL;SACF;OACD,EAAO,iBACN,EAAO,kBACP,EAAO,OAAO,KAAK,MACjB,kBAAC,MAAD;QAEE,WAAU;kBAET,EAAY,EAAU,OAAO,EAAG,UAAU,EAAE;QAC1C,EAJE,SAAS,EAAG,QAId,CACL;OACD;SACC,CAAA;KAEJ;;GACJ,CAAA;EACF,CAAA;EAER"}
|
|
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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"}
|
|
@@ -4,6 +4,7 @@ import "./useWidgetData.js";
|
|
|
4
4
|
import "./useWidgetCatalog.js";
|
|
5
5
|
import "./useAutoRefresh.js";
|
|
6
6
|
import "./useWidgetOperations.js";
|
|
7
|
+
import "./useWidgetComments.js";
|
|
7
8
|
import "./useWidgetConfigPersist.js";
|
|
8
9
|
import "./useDashboardOperations.js";
|
|
9
10
|
import "./useWidgetExport.js";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useCreateEntityCommentMutation as e, useDeleteEntityCommentMutation as t, useGetEntityCommentsQuery as n, useResolveEntityCommentMutation as r } from "../../generated/wspace-operations.js";
|
|
2
|
+
import { useCallback as i, useMemo as a } from "react";
|
|
3
|
+
//#region src/bigconsole/hooks/useWidgetComments.ts
|
|
4
|
+
var o = "bigconsole-widget";
|
|
5
|
+
function s(s, c = !0) {
|
|
6
|
+
let { data: l, loading: u, error: d, refetch: f } = n({
|
|
7
|
+
variables: { input: {
|
|
8
|
+
entityId: s ?? "",
|
|
9
|
+
entityType: o,
|
|
10
|
+
take: 100,
|
|
11
|
+
sortBy: "createdAt",
|
|
12
|
+
sortOrder: "asc"
|
|
13
|
+
} },
|
|
14
|
+
skip: !c || !s,
|
|
15
|
+
fetchPolicy: "cache-and-network",
|
|
16
|
+
notifyOnNetworkStatusChange: !0
|
|
17
|
+
}), [p, m] = e(), [h, g] = r(), [_, v] = t(), y = a(() => l?.getEntityComments.items ?? [], [l]), b = l?.getEntityComments.total ?? y.length, x = a(() => {
|
|
18
|
+
let e = y.filter((e) => !e.parentCommentId), t = /* @__PURE__ */ new Map();
|
|
19
|
+
for (let e of y) if (e.parentCommentId) {
|
|
20
|
+
let n = t.get(e.parentCommentId) ?? [];
|
|
21
|
+
n.push(e), t.set(e.parentCommentId, n);
|
|
22
|
+
}
|
|
23
|
+
return e.map((e) => ({
|
|
24
|
+
comment: e,
|
|
25
|
+
replies: t.get(e.id) ?? []
|
|
26
|
+
}));
|
|
27
|
+
}, [y]), S = i(async (e, t) => {
|
|
28
|
+
let n = e.trim();
|
|
29
|
+
if (!n || !s) return !1;
|
|
30
|
+
try {
|
|
31
|
+
return (await p({ variables: { input: {
|
|
32
|
+
entityId: s,
|
|
33
|
+
entityType: "bigconsole-widget",
|
|
34
|
+
content: { text: n },
|
|
35
|
+
contentType: "TEXT",
|
|
36
|
+
...t ? { parentCommentId: t } : {}
|
|
37
|
+
} } })).data?.createEntityComment ? (await f(), !0) : !1;
|
|
38
|
+
} catch {
|
|
39
|
+
return !1;
|
|
40
|
+
}
|
|
41
|
+
}, [
|
|
42
|
+
p,
|
|
43
|
+
f,
|
|
44
|
+
s
|
|
45
|
+
]), C = i(async (e, t) => {
|
|
46
|
+
try {
|
|
47
|
+
return (await h({ variables: { input: {
|
|
48
|
+
id: e,
|
|
49
|
+
resolved: t
|
|
50
|
+
} } })).data?.resolveEntityComment ? (await f(), !0) : !1;
|
|
51
|
+
} catch {
|
|
52
|
+
return !1;
|
|
53
|
+
}
|
|
54
|
+
}, [h, f]), w = i(async (e) => {
|
|
55
|
+
try {
|
|
56
|
+
return (await _({ variables: { input: {
|
|
57
|
+
id: e,
|
|
58
|
+
soft: !0
|
|
59
|
+
} } })).data?.deleteEntityComment ? (await f(), !0) : !1;
|
|
60
|
+
} catch {
|
|
61
|
+
return !1;
|
|
62
|
+
}
|
|
63
|
+
}, [_, f]);
|
|
64
|
+
return {
|
|
65
|
+
threads: x,
|
|
66
|
+
count: b,
|
|
67
|
+
loading: u,
|
|
68
|
+
error: d,
|
|
69
|
+
mutating: m.loading || g.loading || v.loading,
|
|
70
|
+
addComment: S,
|
|
71
|
+
resolveComment: C,
|
|
72
|
+
deleteComment: w,
|
|
73
|
+
refetch: i(() => {
|
|
74
|
+
f();
|
|
75
|
+
}, [f])
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { o as WIDGET_COMMENT_ENTITY_TYPE, s as default };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=useWidgetComments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWidgetComments.js","names":[],"sources":["../../../src/bigconsole/hooks/useWidgetComments.ts"],"sourcesContent":["/**\n * useWidgetComments Hook\n *\n * Comment thread for a single widget, backed by the federated polymorphic\n * EntityComment API (module-conversations-svc) with `entityType` =\n * `bigconsole-widget`. Returns the flat list plus a parent/child threaded\n * view, and create / resolve / delete actions that refetch on success.\n *\n * The query fragment is intentionally slim (no nested replies/reactions/files)\n * to stay under the gateway cost-limit; threads are assembled here from\n * `parentCommentId`.\n */\n\nimport { useCallback, useMemo } from 'react';\nimport {\n useGetEntityCommentsQuery,\n useCreateEntityCommentMutation,\n useResolveEntityCommentMutation,\n useDeleteEntityCommentMutation,\n} from '../../generated/wspace-operations';\nimport type { EntityCommentFieldsFragment } from '../../generated/wspace-operations';\n\n/** Entity type used to scope comments to a BigConsole widget. */\nexport const WIDGET_COMMENT_ENTITY_TYPE = 'bigconsole-widget';\n\n/** A top-level comment with its (one level of) replies attached. */\nexport interface WidgetCommentThread {\n comment: EntityCommentFieldsFragment;\n replies: EntityCommentFieldsFragment[];\n}\n\nexport interface UseWidgetCommentsResult {\n threads: WidgetCommentThread[];\n /** Total comment count (top-level + replies), excluding deleted. */\n count: number;\n loading: boolean;\n error?: Error;\n mutating: boolean;\n addComment: (text: string, parentCommentId?: string) => Promise<boolean>;\n resolveComment: (id: string, resolved: boolean) => Promise<boolean>;\n deleteComment: (id: string) => Promise<boolean>;\n refetch: () => void;\n}\n\nexport function useWidgetComments(widgetId: string | undefined, enabled = true): UseWidgetCommentsResult {\n const skip = !enabled || !widgetId;\n\n const { data, loading, error, refetch } = useGetEntityCommentsQuery({\n variables: {\n input: {\n entityId: widgetId ?? '',\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n take: 100,\n sortBy: 'createdAt',\n sortOrder: 'asc',\n },\n },\n skip,\n fetchPolicy: 'cache-and-network',\n notifyOnNetworkStatusChange: true,\n });\n\n const [createComment, createState] = useCreateEntityCommentMutation();\n const [resolveCommentMutation, resolveState] = useResolveEntityCommentMutation();\n const [deleteCommentMutation, deleteState] = useDeleteEntityCommentMutation();\n\n const items: EntityCommentFieldsFragment[] = useMemo(() => data?.getEntityComments.items ?? [], [data]);\n // Use the server-reported total for the count (items is capped at the page size).\n const total = data?.getEntityComments.total ?? items.length;\n\n // Reassemble a flat list into top-level threads + their direct replies.\n const threads = useMemo<WidgetCommentThread[]>(() => {\n const roots = items.filter((c) => !c.parentCommentId);\n const repliesByParent = new Map<string, EntityCommentFieldsFragment[]>();\n for (const c of items) {\n if (c.parentCommentId) {\n const list = repliesByParent.get(c.parentCommentId) ?? [];\n list.push(c);\n repliesByParent.set(c.parentCommentId, list);\n }\n }\n return roots.map((comment) => ({ comment, replies: repliesByParent.get(comment.id) ?? [] }));\n }, [items]);\n\n // All actions catch their own errors and resolve to a boolean (never reject),\n // so callers can `await` for success/failure without unhandled rejections and\n // can keep the user's draft when a post fails.\n const addComment = useCallback(\n async (text: string, parentCommentId?: string): Promise<boolean> => {\n const body = text.trim();\n if (!body || !widgetId) return false;\n try {\n const res = await createComment({\n variables: {\n input: {\n entityId: widgetId,\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n content: { text: body },\n contentType: 'TEXT',\n ...(parentCommentId ? { parentCommentId } : {}),\n },\n },\n });\n if (res.data?.createEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [createComment, refetch, widgetId]\n );\n\n const resolveComment = useCallback(\n async (id: string, resolved: boolean): Promise<boolean> => {\n try {\n const res = await resolveCommentMutation({ variables: { input: { id, resolved } } });\n if (res.data?.resolveEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [resolveCommentMutation, refetch]\n );\n\n const deleteComment = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const res = await deleteCommentMutation({ variables: { input: { id, soft: true } } });\n if (res.data?.deleteEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [deleteCommentMutation, refetch]\n );\n\n return {\n threads,\n count: total,\n loading,\n error: error as Error | undefined,\n mutating: createState.loading || resolveState.loading || deleteState.loading,\n addComment,\n resolveComment,\n deleteComment,\n refetch: useCallback(() => {\n void refetch();\n }, [refetch]),\n };\n}\n\nexport default useWidgetComments;\n"],"mappings":";;;AAuBA,IAAa,IAA6B;AAqB1C,SAAgB,EAAkB,GAA8B,IAAU,IAA+B;CAGvG,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EAA0B;EAClE,WAAW,EACT,OAAO;GACL,UAAU,KAAY;GACtB,YAAY;GACZ,MAAM;GACN,QAAQ;GACR,WAAW;GACZ,EACF;EACD,MAZW,CAAC,KAAW,CAAC;EAaxB,aAAa;EACb,6BAA6B;EAC9B,CAAC,EAEI,CAAC,GAAe,KAAe,GAAgC,EAC/D,CAAC,GAAwB,KAAgB,GAAiC,EAC1E,CAAC,GAAuB,KAAe,GAAgC,EAEvE,IAAuC,QAAc,GAAM,kBAAkB,SAAS,EAAE,EAAE,CAAC,EAAK,CAAC,EAEjG,IAAQ,GAAM,kBAAkB,SAAS,EAAM,QAG/C,IAAU,QAAqC;EACnD,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,EAAE,gBAAgB,EAC/C,oBAAkB,IAAI,KAA4C;AACxE,OAAK,IAAM,KAAK,EACd,KAAI,EAAE,iBAAiB;GACrB,IAAM,IAAO,EAAgB,IAAI,EAAE,gBAAgB,IAAI,EAAE;AAEzD,GADA,EAAK,KAAK,EAAE,EACZ,EAAgB,IAAI,EAAE,iBAAiB,EAAK;;AAGhD,SAAO,EAAM,KAAK,OAAa;GAAE;GAAS,SAAS,EAAgB,IAAI,EAAQ,GAAG,IAAI,EAAE;GAAE,EAAE;IAC3F,CAAC,EAAM,CAAC,EAKL,IAAa,EACjB,OAAO,GAAc,MAA+C;EAClE,IAAM,IAAO,EAAK,MAAM;AACxB,MAAI,CAAC,KAAQ,CAAC,EAAU,QAAO;AAC/B,MAAI;AAgBF,WAfY,MAAM,EAAc,EAC9B,WAAW,EACT,OAAO;IACL,UAAU;IACV,YAAA;IACA,SAAS,EAAE,MAAM,GAAM;IACvB,aAAa;IACb,GAAI,IAAkB,EAAE,oBAAiB,GAAG,EAAE;IAC/C,EACF,EACF,CAAC,EACM,MAAM,uBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX;EAAC;EAAe;EAAS;EAAS,CACnC,EAEK,IAAiB,EACrB,OAAO,GAAY,MAAwC;AACzD,MAAI;AAMF,WALY,MAAM,EAAuB,EAAE,WAAW,EAAE,OAAO;IAAE;IAAI;IAAU,EAAE,EAAE,CAAC,EAC5E,MAAM,wBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX,CAAC,GAAwB,EAAQ,CAClC,EAEK,IAAgB,EACpB,OAAO,MAAiC;AACtC,MAAI;AAMF,WALY,MAAM,EAAsB,EAAE,WAAW,EAAE,OAAO;IAAE;IAAI,MAAM;IAAM,EAAE,EAAE,CAAC,EAC7E,MAAM,uBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX,CAAC,GAAuB,EAAQ,CACjC;AAED,QAAO;EACL;EACA,OAAO;EACP;EACO;EACP,UAAU,EAAY,WAAW,EAAa,WAAW,EAAY;EACrE;EACA;EACA;EACA,SAAS,QAAkB;AACpB,MAAS;KACb,CAAC,EAAQ,CAAC;EACd"}
|