@burdenoff/microfe-bigconsole 2026.731.8 → 2026.801.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.
Files changed (51) hide show
  1. package/dist/bigconsole/AdminRoutes.js +1 -1
  2. package/dist/bigconsole/AdminRoutes.js.map +1 -1
  3. package/dist/bigconsole/BigConsoleRoot.js +1 -1
  4. package/dist/bigconsole/BigConsoleRoot.js.map +1 -1
  5. package/dist/bigconsole/BigConsoleRoutes.js +1 -1
  6. package/dist/bigconsole/BigConsoleRoutes.js.map +1 -1
  7. package/dist/bigconsole/components/datasink/LinkedParsersPanel.js +1 -1
  8. package/dist/bigconsole/components/datasink/LinkedParsersPanel.js.map +1 -1
  9. package/dist/bigconsole/components/embed/EmbedErrorBoundary.js +1 -1
  10. package/dist/bigconsole/components/embed/EmbedErrorBoundary.js.map +1 -1
  11. package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js +1 -1
  12. package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js.map +1 -1
  13. package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js +7 -7
  14. package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js.map +1 -1
  15. package/dist/bigconsole/components/embed/widgets/KpiComparisonEmbed.js +20 -14
  16. package/dist/bigconsole/components/embed/widgets/KpiComparisonEmbed.js.map +1 -1
  17. package/dist/bigconsole/components/embed/widgets/ListEmbed.js +8 -8
  18. package/dist/bigconsole/components/embed/widgets/ListEmbed.js.map +1 -1
  19. package/dist/bigconsole/components/embed/widgets/MetricCardEmbed.js +17 -9
  20. package/dist/bigconsole/components/embed/widgets/MetricCardEmbed.js.map +1 -1
  21. package/dist/bigconsole/components/embed/widgets/TableEmbed.js +34 -22
  22. package/dist/bigconsole/components/embed/widgets/TableEmbed.js.map +1 -1
  23. package/dist/bigconsole/components/embed/widgets/TextEmbed.js +3 -3
  24. package/dist/bigconsole/components/embed/widgets/TextEmbed.js.map +1 -1
  25. package/dist/bigconsole/components/embed/widgets/shared.js +48 -3
  26. package/dist/bigconsole/components/embed/widgets/shared.js.map +1 -1
  27. package/dist/bigconsole/contexts/NavigationContext.js +1 -1
  28. package/dist/bigconsole/contexts/NavigationContext.js.map +1 -1
  29. package/dist/bigconsole/hooks/useFilterUrlSync.js +1 -1
  30. package/dist/bigconsole/hooks/useFilterUrlSync.js.map +1 -1
  31. package/dist/bigconsole/pages/DashboardBuilderPage.js +1 -1
  32. package/dist/bigconsole/pages/DashboardBuilderPage.js.map +1 -1
  33. package/dist/bigconsole/pages/DashboardViewPage.js +1 -1
  34. package/dist/bigconsole/pages/DashboardViewPage.js.map +1 -1
  35. package/dist/bigconsole/pages/DataParsersPage.js +1 -1
  36. package/dist/bigconsole/pages/DataParsersPage.js.map +1 -1
  37. package/dist/bigconsole/pages/DataSinkBuilderPage.js +1 -1
  38. package/dist/bigconsole/pages/DataSinkBuilderPage.js.map +1 -1
  39. package/dist/bigconsole/pages/DataSinkViewPage.js +1 -1
  40. package/dist/bigconsole/pages/DataSinkViewPage.js.map +1 -1
  41. package/dist/bigconsole/pages/ParserBuilderPage.js +1 -1
  42. package/dist/bigconsole/pages/ParserBuilderPage.js.map +1 -1
  43. package/dist/bigconsole/pages/ParserViewPage.js +1 -1
  44. package/dist/bigconsole/pages/ParserViewPage.js.map +1 -1
  45. package/dist/bigconsole/pages/PipelineBuilderPage.js +1 -1
  46. package/dist/bigconsole/pages/PipelineBuilderPage.js.map +1 -1
  47. package/dist/bigconsole/pages/PipelineViewPage.js +1 -1
  48. package/dist/bigconsole/pages/PipelineViewPage.js.map +1 -1
  49. package/dist/bigconsole/pages/WorkflowViewPage.js +1 -1
  50. package/dist/bigconsole/pages/WorkflowViewPage.js.map +1 -1
  51. package/package.json +3 -3
@@ -2,16 +2,24 @@ import { asFiniteNumber as e, asRecord as t, asString as n, formatNumber as r }
2
2
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/embed/widgets/MetricCardEmbed.tsx
4
4
  function o({ widget: o }) {
5
- let s = t(o.config), c = e(t(o.data).value ?? s.value), l = n(s.label ?? o.title, "Metric"), u = c === null ? "—" : r(c, s);
5
+ let s = t(o.config), c = e(t(o.data).value ?? s.value), l = n(s.label ?? o.title, "Metric"), u = n(s.subtitle), d = c === null ? "—" : r(c, s);
6
6
  return /* @__PURE__ */ a("div", {
7
- className: "flex h-full w-full flex-col justify-center gap-1 bg-surface p-4",
8
- children: [/* @__PURE__ */ i("div", {
9
- className: "text-xs uppercase tracking-wide text-secondary",
10
- children: l
11
- }), /* @__PURE__ */ i("div", {
12
- className: "text-3xl font-semibold text-primary",
13
- children: u
14
- })]
7
+ className: "flex h-full w-full flex-col justify-center gap-1 bg-bg-surface p-4",
8
+ children: [
9
+ /* @__PURE__ */ i("div", {
10
+ className: "truncate text-xs font-medium uppercase tracking-wide text-text-secondary",
11
+ children: l
12
+ }),
13
+ /* @__PURE__ */ i("div", {
14
+ className: "text-3xl font-semibold tracking-tight text-text-primary",
15
+ title: d,
16
+ children: d
17
+ }),
18
+ u && /* @__PURE__ */ i("div", {
19
+ className: "truncate text-xs text-text-tertiary",
20
+ children: u
21
+ })
22
+ ]
15
23
  });
16
24
  }
17
25
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"MetricCardEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/MetricCardEmbed.tsx"],"sourcesContent":["/** Read-only metric card embed renderer (BOFF-2986). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString, asFiniteNumber, formatNumber } from './shared';\n\nexport function MetricCardEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rawValue = asFiniteNumber(data.value ?? config.value);\n const label = asString(config.label ?? widget.title, 'Metric');\n const display = rawValue !== null ? formatNumber(rawValue, config) : '—';\n\n return (\n <div className=\"flex h-full w-full flex-col justify-center gap-1 bg-surface p-4\">\n <div className=\"text-xs uppercase tracking-wide text-secondary\">{label}</div>\n <div className=\"text-3xl font-semibold text-primary\">{display}</div>\n </div>\n );\n}\n"],"mappings":";;;AAIA,SAAgB,EAAgB,EAAE,aAAkC;CAClE,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAW,EADJ,EAAS,EAAO,KAAK,CACG,SAAS,EAAO,MAAM,EACrD,IAAQ,EAAS,EAAO,SAAS,EAAO,OAAO,SAAS,EACxD,IAAU,MAAa,OAAwC,MAAjC,EAAa,GAAU,EAAO;AAElE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAkD;GAAY,CAAA,EAC7E,kBAAC,OAAD;GAAK,WAAU;aAAuC;GAAc,CAAA,CAChE"}
1
+ {"version":3,"file":"MetricCardEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/MetricCardEmbed.tsx"],"sourcesContent":["/** Read-only metric card embed renderer (BOFF-2986 / BOFF-5733). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString, asFiniteNumber, formatNumber } from './shared';\n\nexport function MetricCardEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rawValue = asFiniteNumber(data.value ?? config.value);\n const label = asString(config.label ?? widget.title, 'Metric');\n const subtitle = asString(config.subtitle);\n const display = rawValue !== null ? formatNumber(rawValue, config) : '—';\n\n return (\n <div className=\"flex h-full w-full flex-col justify-center gap-1 bg-bg-surface p-4\">\n <div className=\"truncate text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</div>\n <div className=\"text-3xl font-semibold tracking-tight text-text-primary\" title={display}>\n {display}\n </div>\n {subtitle && <div className=\"truncate text-xs text-text-tertiary\">{subtitle}</div>}\n </div>\n );\n}\n"],"mappings":";;;AAIA,SAAgB,EAAgB,EAAE,aAAkC;CAClE,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAW,EADJ,EAAS,EAAO,KAAK,CACG,SAAS,EAAO,MAAM,EACrD,IAAQ,EAAS,EAAO,SAAS,EAAO,OAAO,SAAS,EACxD,IAAW,EAAS,EAAO,SAAS,EACpC,IAAU,MAAa,OAAwC,MAAjC,EAAa,GAAU,EAAO;AAElE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAA4E;IAAY,CAAA;GACvG,kBAAC,OAAD;IAAK,WAAU;IAA0D,OAAO;cAC7E;IACG,CAAA;GACL,KAAY,kBAAC,OAAD;IAAK,WAAU;cAAuC;IAAe,CAAA;GAC9E"}
@@ -1,40 +1,52 @@
1
- import { asArray as e, asRecord as t, asString as n, displayValue as r } from "./shared.js";
2
- import { jsx as i, jsxs as a } from "react/jsx-runtime";
1
+ import { asArray as e, asRecord as t, asString as n, formatCell as r, isNumeric as i } from "./shared.js";
2
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/embed/widgets/TableEmbed.tsx
4
- var o = 500;
5
- function s(r, i) {
6
- let a = e(r.columns).map((e) => t(e)).map((e) => ({
4
+ var s = 500;
5
+ function c(i, a) {
6
+ let o = e(i.columns).map((e) => t(e)).map((e) => ({
7
7
  key: n(e.key),
8
8
  label: n(e.label ?? e.key)
9
9
  })).filter((e) => e.key.length > 0);
10
- return a.length > 0 ? a : Object.keys(i).map((e) => ({
10
+ if (o.length > 0) return o;
11
+ let s = /* @__PURE__ */ new Set(), c = [];
12
+ for (let e of a) for (let t of Object.keys(e)) s.has(t) || (s.add(t), c.push(t));
13
+ return c.filter((e) => !e.startsWith("_")).filter((e) => a.some((t) => r(t[e]) !== "")).map((e) => ({
11
14
  key: e,
12
15
  label: e
13
16
  }));
14
17
  }
15
- function c({ widget: n }) {
16
- let c = t(n.config), l = e(t(n.data).rows).map((e) => t(e)).slice(0, o), u = s(c, l[0] ?? {});
17
- return u.length === 0 ? /* @__PURE__ */ i("div", {
18
- className: "flex h-full items-center justify-center bg-surface p-4 text-sm text-secondary",
18
+ function l({ widget: n }) {
19
+ let l = t(n.config), u = e(t(n.data).rows).map((e) => t(e)).slice(0, s), d = c(l, u);
20
+ if (d.length === 0 || u.length === 0) return /* @__PURE__ */ a("div", {
21
+ className: "flex h-full items-center justify-center bg-bg-surface p-4 text-sm text-text-secondary",
19
22
  children: "No data"
20
- }) : /* @__PURE__ */ a("div", {
21
- className: "h-full w-full overflow-auto bg-surface",
22
- children: [n.title && /* @__PURE__ */ i("div", {
23
- className: "px-3 py-2 text-sm font-medium text-primary",
23
+ });
24
+ let f = new Set(d.filter((e) => {
25
+ let t = u.map((t) => t[e.key]).filter((e) => e != null && e !== "");
26
+ return t.length > 0 && t.every(i);
27
+ }).map((e) => e.key));
28
+ return /* @__PURE__ */ o("div", {
29
+ className: "h-full w-full overflow-auto bg-bg-surface",
30
+ children: [n.title && /* @__PURE__ */ a("div", {
31
+ className: "border-b border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary",
24
32
  children: n.title
25
- }), /* @__PURE__ */ a("table", {
33
+ }), /* @__PURE__ */ o("table", {
26
34
  className: "w-full border-collapse text-sm",
27
- children: [/* @__PURE__ */ i("thead", { children: /* @__PURE__ */ i("tr", { children: u.map((e) => /* @__PURE__ */ i("th", {
28
- className: "border-b border-border-default px-3 py-2 text-left font-medium text-secondary",
35
+ children: [/* @__PURE__ */ a("thead", { children: /* @__PURE__ */ a("tr", { children: d.map((e) => /* @__PURE__ */ a("th", {
36
+ scope: "col",
37
+ className: `sticky top-0 z-10 border-b border-border-default bg-bg-sunken px-3 py-2 font-medium text-text-secondary ${f.has(e.key) ? "text-right tabular-nums" : "text-left"}`,
29
38
  children: e.label
30
- }, e.key)) }) }), /* @__PURE__ */ i("tbody", { children: l.map((e, t) => /* @__PURE__ */ i("tr", { children: u.map((t) => /* @__PURE__ */ i("td", {
31
- className: "border-b border-border-default px-3 py-2 text-primary",
32
- children: r(e[t.key])
33
- }, t.key)) }, t)) })]
39
+ }, e.key)) }) }), /* @__PURE__ */ a("tbody", { children: u.map((e, t) => /* @__PURE__ */ a("tr", {
40
+ className: "hover:bg-bg-sunken/50",
41
+ children: d.map((t) => /* @__PURE__ */ a("td", {
42
+ className: `border-b border-border-subtle px-3 py-2 text-text-primary ${f.has(t.key) ? "text-right tabular-nums" : "text-left"}`,
43
+ children: r(e[t.key])
44
+ }, t.key))
45
+ }, t)) })]
34
46
  })]
35
47
  });
36
48
  }
37
49
  //#endregion
38
- export { c as TableEmbed };
50
+ export { l as TableEmbed };
39
51
 
40
52
  //# sourceMappingURL=TableEmbed.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TableEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TableEmbed.tsx"],"sourcesContent":["/** Read-only table embed renderer (BOFF-2986). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asArray, asString, displayValue } from './shared';\n\ninterface Column {\n key: string;\n label: string;\n}\n\n/** Hard client-side cap; the publication builder is expected to page/limit rows. */\nconst MAX_ROWS = 500;\n\nfunction readColumns(config: Record<string, unknown>, firstRow: Record<string, unknown>): Column[] {\n const configured = asArray(config.columns)\n .map((c) => asRecord(c))\n .map((c) => ({ key: asString(c.key), label: asString(c.label ?? c.key) }))\n .filter((c) => c.key.length > 0);\n if (configured.length > 0) return configured;\n // Derive from the first row when the publication didn't pin columns.\n return Object.keys(firstRow).map((key) => ({ key, label: key }));\n}\n\nexport function TableEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rows = asArray(data.rows)\n .map((r) => asRecord(r))\n .slice(0, MAX_ROWS);\n const columns = readColumns(config, rows[0] ?? {});\n\n if (columns.length === 0) {\n return <div className=\"flex h-full items-center justify-center bg-surface p-4 text-sm text-secondary\">No data</div>;\n }\n\n return (\n <div className=\"h-full w-full overflow-auto bg-surface\">\n {widget.title && <div className=\"px-3 py-2 text-sm font-medium text-primary\">{widget.title}</div>}\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr>\n {columns.map((col) => (\n <th\n key={col.key}\n className=\"border-b border-border-default px-3 py-2 text-left font-medium text-secondary\"\n >\n {col.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, i) => (\n <tr key={i}>\n {columns.map((col) => (\n <td key={col.key} className=\"border-b border-border-default px-3 py-2 text-primary\">\n {displayValue(row[col.key])}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":";;;AAUA,IAAM,IAAW;AAEjB,SAAS,EAAY,GAAiC,GAA6C;CACjG,IAAM,IAAa,EAAQ,EAAO,QAAQ,CACvC,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,KAAK,OAAO;EAAE,KAAK,EAAS,EAAE,IAAI;EAAE,OAAO,EAAS,EAAE,SAAS,EAAE,IAAI;EAAE,EAAE,CACzE,QAAQ,MAAM,EAAE,IAAI,SAAS,EAAE;AAGlC,QAFI,EAAW,SAAS,IAAU,IAE3B,OAAO,KAAK,EAAS,CAAC,KAAK,OAAS;EAAE;EAAK,OAAO;EAAK,EAAE;;AAGlE,SAAgB,EAAW,EAAE,aAAkC;CAC7D,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAO,EADA,EAAS,EAAO,KAAK,CACR,KAAK,CAC5B,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,MAAM,GAAG,EAAS,EACf,IAAU,EAAY,GAAQ,EAAK,MAAM,EAAE,CAAC;AAMlD,QAJI,EAAQ,WAAW,IACd,kBAAC,OAAD;EAAK,WAAU;YAAgF;EAAa,CAAA,GAInH,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SAAS,kBAAC,OAAD;GAAK,WAAU;aAA8C,EAAO;GAAY,CAAA,EACjG,kBAAC,SAAD;GAAO,WAAU;aAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,MAAD;IAEE,WAAU;cAET,EAAI;IACF,EAJE,EAAI,IAIN,CACL,EACC,CAAA,EACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAK,KAAK,GAAK,MACd,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,MAAD;IAAkB,WAAU;cACzB,EAAa,EAAI,EAAI,KAAK;IACxB,EAFI,EAAI,IAER,CACL,EACC,EANI,EAMJ,CACL,EACI,CAAA,CACF;KACJ"}
1
+ {"version":3,"file":"TableEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TableEmbed.tsx"],"sourcesContent":["/** Read-only table embed renderer (BOFF-2986 / BOFF-5733). */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asArray, asString, formatCell, isNumeric } from './shared';\n\ninterface Column {\n key: string;\n label: string;\n}\n\n/** Hard client-side cap; the publication builder is expected to page/limit rows. */\nconst MAX_ROWS = 500;\n\nfunction readColumns(config: Record<string, unknown>, rows: Record<string, unknown>[]): Column[] {\n const configured = asArray(config.columns)\n .map((c) => asRecord(c))\n .map((c) => ({ key: asString(c.key), label: asString(c.label ?? c.key) }))\n .filter((c) => c.key.length > 0);\n // A publisher that pinned columns chose exactly what to show — respect it.\n if (configured.length > 0) return configured;\n\n // Derive from the data when the publication didn't pin columns. Union the keys\n // across all rows (rows can be sparse), then drop noise so an embed never shows\n // a raw internal dump: `_`-prefixed technical keys and columns that are empty\n // in every row (the \"empty metadata/badge column\" gap from BOFF-5733).\n const seen = new Set<string>();\n const keys: string[] = [];\n for (const row of rows) {\n for (const key of Object.keys(row)) {\n if (!seen.has(key)) {\n seen.add(key);\n keys.push(key);\n }\n }\n }\n return keys\n .filter((key) => !key.startsWith('_'))\n .filter((key) => rows.some((row) => formatCell(row[key]) !== ''))\n .map((key) => ({ key, label: key }));\n}\n\nexport function TableEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const data = asRecord(widget.data);\n const rows = asArray(data.rows)\n .map((r) => asRecord(r))\n .slice(0, MAX_ROWS);\n const columns = readColumns(config, rows);\n\n if (columns.length === 0 || rows.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center bg-bg-surface p-4 text-sm text-text-secondary\">\n No data\n </div>\n );\n }\n\n // Right-align a column when its populated cells are all numeric (matches the\n // in-app TableCell), so currency/counts line up on the decimal edge.\n const numericColumns = new Set(\n columns\n .filter((col) => {\n const values = rows.map((r) => r[col.key]).filter((v) => v !== null && v !== undefined && v !== '');\n return values.length > 0 && values.every(isNumeric);\n })\n .map((col) => col.key)\n );\n\n return (\n <div className=\"h-full w-full overflow-auto bg-bg-surface\">\n {widget.title && (\n <div className=\"border-b border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary\">\n {widget.title}\n </div>\n )}\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr>\n {columns.map((col) => (\n // `sticky` lives on the cells (not the title, which scrolls away)\n // so column headers stay visible while scrolling the 500-row cap.\n // Cells carry their own opaque background so rows don't show through.\n <th\n key={col.key}\n scope=\"col\"\n className={`sticky top-0 z-10 border-b border-border-default bg-bg-sunken px-3 py-2 font-medium text-text-secondary ${\n numericColumns.has(col.key) ? 'text-right tabular-nums' : 'text-left'\n }`}\n >\n {col.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, i) => (\n <tr key={i} className=\"hover:bg-bg-sunken/50\">\n {columns.map((col) => (\n <td\n key={col.key}\n className={`border-b border-border-subtle px-3 py-2 text-text-primary ${\n numericColumns.has(col.key) ? 'text-right tabular-nums' : 'text-left'\n }`}\n >\n {formatCell(row[col.key])}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":";;;AAUA,IAAM,IAAW;AAEjB,SAAS,EAAY,GAAiC,GAA2C;CAC/F,IAAM,IAAa,EAAQ,EAAO,QAAQ,CACvC,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,KAAK,OAAO;EAAE,KAAK,EAAS,EAAE,IAAI;EAAE,OAAO,EAAS,EAAE,SAAS,EAAE,IAAI;EAAE,EAAE,CACzE,QAAQ,MAAM,EAAE,IAAI,SAAS,EAAE;AAElC,KAAI,EAAW,SAAS,EAAG,QAAO;CAMlC,IAAM,oBAAO,IAAI,KAAa,EACxB,IAAiB,EAAE;AACzB,MAAK,IAAM,KAAO,EAChB,MAAK,IAAM,KAAO,OAAO,KAAK,EAAI,CAChC,CAAK,EAAK,IAAI,EAAI,KAChB,EAAK,IAAI,EAAI,EACb,EAAK,KAAK,EAAI;AAIpB,QAAO,EACJ,QAAQ,MAAQ,CAAC,EAAI,WAAW,IAAI,CAAC,CACrC,QAAQ,MAAQ,EAAK,MAAM,MAAQ,EAAW,EAAI,GAAK,KAAK,GAAG,CAAC,CAChE,KAAK,OAAS;EAAE;EAAK,OAAO;EAAK,EAAE;;AAGxC,SAAgB,EAAW,EAAE,aAAkC;CAC7D,IAAM,IAAS,EAAS,EAAO,OAAO,EAEhC,IAAO,EADA,EAAS,EAAO,KAAK,CACR,KAAK,CAC5B,KAAK,MAAM,EAAS,EAAE,CAAC,CACvB,MAAM,GAAG,EAAS,EACf,IAAU,EAAY,GAAQ,EAAK;AAEzC,KAAI,EAAQ,WAAW,KAAK,EAAK,WAAW,EAC1C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAwF;EAEjG,CAAA;CAMV,IAAM,IAAiB,IAAI,IACzB,EACG,QAAQ,MAAQ;EACf,IAAM,IAAS,EAAK,KAAK,MAAM,EAAE,EAAI,KAAK,CAAC,QAAQ,MAAM,KAAM,QAA2B,MAAM,GAAG;AACnG,SAAO,EAAO,SAAS,KAAK,EAAO,MAAM,EAAU;GACnD,CACD,KAAK,MAAQ,EAAI,IAAI,CACzB;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SACN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO;GACJ,CAAA,EAER,kBAAC,SAAD;GAAO,WAAU;aAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD,EAAA,UACG,EAAQ,KAAK,MAIZ,kBAAC,MAAD;IAEE,OAAM;IACN,WAAW,2GACT,EAAe,IAAI,EAAI,IAAI,GAAG,4BAA4B;cAG3D,EAAI;IACF,EAPE,EAAI,IAON,CACL,EACC,CAAA,EACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAK,KAAK,GAAK,MACd,kBAAC,MAAD;IAAY,WAAU;cACnB,EAAQ,KAAK,MACZ,kBAAC,MAAD;KAEE,WAAW,6DACT,EAAe,IAAI,EAAI,IAAI,GAAG,4BAA4B;eAG3D,EAAW,EAAI,EAAI,KAAK;KACtB,EANE,EAAI,IAMN,CACL;IACC,EAXI,EAWJ,CACL,EACI,CAAA,CACF;KACJ"}
@@ -4,12 +4,12 @@ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
4
  function i({ widget: i }) {
5
5
  let a = e(i.config), o = t(a.content ?? a.text ?? a.markdown);
6
6
  return /* @__PURE__ */ r("div", {
7
- className: "h-full w-full overflow-auto bg-surface p-4",
7
+ className: "h-full w-full overflow-auto bg-bg-surface p-4",
8
8
  children: [i.title && /* @__PURE__ */ n("div", {
9
- className: "mb-2 text-sm font-medium text-primary",
9
+ className: "mb-2 text-sm font-medium text-text-primary",
10
10
  children: i.title
11
11
  }), /* @__PURE__ */ n("div", {
12
- className: "whitespace-pre-wrap break-words text-sm text-primary",
12
+ className: "whitespace-pre-wrap break-words text-sm leading-relaxed text-text-secondary",
13
13
  children: o
14
14
  })]
15
15
  });
@@ -1 +1 @@
1
- {"version":3,"file":"TextEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TextEmbed.tsx"],"sourcesContent":["/**\n * Read-only text embed renderer (BOFF-2986).\n *\n * Renders the text content as PLAIN TEXT — React escapes it, so no HTML/script\n * from the content can execute. Rich Markdown/HTML rendering is deferred until a\n * vetted sanitizer is wired; until then plain text is the safe default rather\n * than `dangerouslySetInnerHTML`.\n */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString } from './shared';\n\nexport function TextEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const content = asString(config.content ?? config.text ?? config.markdown);\n\n return (\n <div className=\"h-full w-full overflow-auto bg-surface p-4\">\n {widget.title && <div className=\"mb-2 text-sm font-medium text-primary\">{widget.title}</div>}\n <div className=\"whitespace-pre-wrap break-words text-sm text-primary\">{content}</div>\n </div>\n );\n}\n"],"mappings":";;;AAWA,SAAgB,EAAU,EAAE,aAAkC;CAC5D,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAU,EAAS,EAAO,WAAW,EAAO,QAAQ,EAAO,SAAS;AAE1E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SAAS,kBAAC,OAAD;GAAK,WAAU;aAAyC,EAAO;GAAY,CAAA,EAC5F,kBAAC,OAAD;GAAK,WAAU;aAAwD;GAAc,CAAA,CACjF"}
1
+ {"version":3,"file":"TextEmbed.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/TextEmbed.tsx"],"sourcesContent":["/**\n * Read-only text embed renderer (BOFF-2986 / BOFF-5733).\n *\n * Renders the text content as PLAIN TEXT — React escapes it, so no HTML/script\n * from the content can execute. Rich Markdown/HTML rendering is deferred until a\n * vetted sanitizer is wired; until then plain text is the safe default rather\n * than `dangerouslySetInnerHTML`.\n */\nimport type { EmbedWidgetRenderProps } from '../types';\nimport { asRecord, asString } from './shared';\n\nexport function TextEmbed({ widget }: EmbedWidgetRenderProps) {\n const config = asRecord(widget.config);\n const content = asString(config.content ?? config.text ?? config.markdown);\n\n return (\n <div className=\"h-full w-full overflow-auto bg-bg-surface p-4\">\n {widget.title && <div className=\"mb-2 text-sm font-medium text-text-primary\">{widget.title}</div>}\n <div className=\"whitespace-pre-wrap break-words text-sm leading-relaxed text-text-secondary\">{content}</div>\n </div>\n );\n}\n"],"mappings":";;;AAWA,SAAgB,EAAU,EAAE,aAAkC;CAC5D,IAAM,IAAS,EAAS,EAAO,OAAO,EAChC,IAAU,EAAS,EAAO,WAAW,EAAO,QAAQ,EAAO,SAAS;AAE1E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAO,SAAS,kBAAC,OAAD;GAAK,WAAU;aAA8C,EAAO;GAAY,CAAA,EACjG,kBAAC,OAAD;GAAK,WAAU;aAA+E;GAAc,CAAA,CACxG"}
@@ -20,10 +20,55 @@ function i(e) {
20
20
  return e == null ? "" : typeof e == "string" ? e : typeof e == "number" ? Number.isFinite(e) ? String(e) : "" : typeof e == "boolean" ? e ? "true" : "false" : "";
21
21
  }
22
22
  function a(n, r) {
23
- let i = t(r.decimals), a = e(r.prefix), o = e(r.suffix);
24
- return `${a}${i === null ? String(n) : n.toFixed(Math.min(Math.max(i, 0), 10))}${o}`;
23
+ let i = e(r.format, "number"), a = t(r.decimals), c = e(r.prefix), l = e(r.suffix), u;
24
+ switch (i) {
25
+ case "currency": {
26
+ let e = s(r.currency), t = a === null ? 2 : o(a);
27
+ try {
28
+ u = new Intl.NumberFormat("en-US", {
29
+ style: "currency",
30
+ currency: e,
31
+ minimumFractionDigits: 0,
32
+ maximumFractionDigits: t
33
+ }).format(n);
34
+ } catch {
35
+ u = new Intl.NumberFormat("en-US", { maximumFractionDigits: t }).format(n);
36
+ }
37
+ break;
38
+ }
39
+ case "percentage": {
40
+ let e = a === null ? 1 : o(a);
41
+ u = `${new Intl.NumberFormat("en-US", {
42
+ minimumFractionDigits: e,
43
+ maximumFractionDigits: e
44
+ }).format(n * 100)}%`;
45
+ break;
46
+ }
47
+ case "compact":
48
+ u = new Intl.NumberFormat("en-US", {
49
+ notation: "compact",
50
+ maximumFractionDigits: a === null ? 1 : o(a)
51
+ }).format(n);
52
+ break;
53
+ default: u = new Intl.NumberFormat("en-US", { maximumFractionDigits: a === null ? 2 : o(a) }).format(n);
54
+ }
55
+ return c && i !== "currency" && (u = `${c}${u}`), l && (u = `${u}${l}`), u;
56
+ }
57
+ function o(e) {
58
+ return Math.min(Math.max(Math.trunc(e), 0), 10);
59
+ }
60
+ function s(t) {
61
+ let n = e(t, "USD").toUpperCase();
62
+ return /^[A-Z]{3}$/.test(n) ? n : "USD";
63
+ }
64
+ var c = new Intl.NumberFormat("en-US", { maximumFractionDigits: 20 });
65
+ function l(e) {
66
+ return typeof e == "number" && Number.isFinite(e) ? c.format(e) : i(e);
67
+ }
68
+ function u(e) {
69
+ return typeof e == "number" && Number.isFinite(e);
25
70
  }
26
71
  //#endregion
27
- export { n as asArray, t as asFiniteNumber, r as asRecord, e as asString, i as displayValue, a as formatNumber };
72
+ export { n as asArray, t as asFiniteNumber, r as asRecord, e as asString, l as formatCell, a as formatNumber, u as isNumeric };
28
73
 
29
74
  //# sourceMappingURL=shared.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/shared.ts"],"sourcesContent":["/**\n * Defensive value coercion for embed widget renderers (BOFF-2986).\n *\n * The render model is sanitized by the publication builder, but the renderer\n * re-narrows every value it reads: an embedded widget is anonymous, untrusted\n * customer-facing surface, so we never assume a field is the type we expect.\n */\n\nexport function asString(value: unknown, fallback = ''): string {\n return typeof value === 'string' ? value : fallback;\n}\n\nexport function asFiniteNumber(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n }\n return null;\n}\n\nexport function asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};\n}\n\n/**\n * Render an arbitrary cell value as a display string. Objects/arrays collapse to\n * an empty string rather than `[object Object]`, so no internal structure leaks.\n */\nexport function displayValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return '';\n}\n\n/** Format a number with the widget's optional locale/format hints, safely. */\nexport function formatNumber(value: number, config: Record<string, unknown>): string {\n const decimals = asFiniteNumber(config.decimals);\n const prefix = asString(config.prefix);\n const suffix = asString(config.suffix);\n const fixed = decimals !== null ? value.toFixed(Math.min(Math.max(decimals, 0), 10)) : String(value);\n return `${prefix}${fixed}${suffix}`;\n}\n"],"mappings":";AAQA,SAAgB,EAAS,GAAgB,IAAW,IAAY;AAC9D,QAAO,OAAO,KAAU,WAAW,IAAQ;;AAG7C,SAAgB,EAAe,GAA+B;AAC5D,KAAI,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM,CAAE,QAAO;AAChE,KAAI,OAAO,KAAU,YAAY,EAAM,MAAM,KAAK,IAAI;EACpD,IAAM,IAAI,OAAO,EAAM;AACvB,MAAI,OAAO,SAAS,EAAE,CAAE,QAAO;;AAEjC,QAAO;;AAGT,SAAgB,EAAQ,GAA2B;AACjD,QAAO,MAAM,QAAQ,EAAM,GAAG,IAAQ,EAAE;;AAG1C,SAAgB,EAAS,GAAyC;AAChE,QAAO,KAAS,OAAO,KAAU,YAAY,CAAC,MAAM,QAAQ,EAAM,GAAI,IAAoC,EAAE;;AAO9G,SAAgB,EAAa,GAAwB;AAKnD,QAJI,KAAU,OAAoC,KAC9C,OAAO,KAAU,WAAiB,IAClC,OAAO,KAAU,WAAiB,OAAO,SAAS,EAAM,GAAG,OAAO,EAAM,GAAG,KAC3E,OAAO,KAAU,YAAkB,IAAQ,SAAS,UACjD;;AAIT,SAAgB,EAAa,GAAe,GAAyC;CACnF,IAAM,IAAW,EAAe,EAAO,SAAS,EAC1C,IAAS,EAAS,EAAO,OAAO,EAChC,IAAS,EAAS,EAAO,OAAO;AAEtC,QAAO,GAAG,IADI,MAAa,OAA4D,OAAO,EAAM,GAAlE,EAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAU,EAAE,EAAE,GAAG,CAAC,GACzD"}
1
+ {"version":3,"file":"shared.js","names":[],"sources":["../../../../../src/bigconsole/components/embed/widgets/shared.ts"],"sourcesContent":["/**\n * Defensive value coercion for embed widget renderers (BOFF-2986).\n *\n * The render model is sanitized by the publication builder, but the renderer\n * re-narrows every value it reads: an embedded widget is anonymous, untrusted\n * customer-facing surface, so we never assume a field is the type we expect.\n */\n\nexport function asString(value: unknown, fallback = ''): string {\n return typeof value === 'string' ? value : fallback;\n}\n\nexport function asFiniteNumber(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n }\n return null;\n}\n\nexport function asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};\n}\n\n/**\n * Render an arbitrary cell value as a display string. Objects/arrays collapse to\n * an empty string rather than `[object Object]`, so no internal structure leaks.\n */\nexport function displayValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return '';\n}\n\n/**\n * Format a number with the widget's optional format hints, safely — mirrors the\n * in-app MetricCardWidget `formatValue` so embedded numbers read identically\n * (thousands separators, currency, percentage, compact notation) instead of a\n * bare `1284530.5`. `config.format` is the same enum the in-app widgets use.\n */\nexport function formatNumber(value: number, config: Record<string, unknown>): string {\n const format = asString(config.format, 'number');\n const decimals = asFiniteNumber(config.decimals);\n const prefix = asString(config.prefix);\n const suffix = asString(config.suffix);\n\n let formatted: string;\n switch (format) {\n case 'currency': {\n // `Intl.NumberFormat` throws `RangeError` on a currency code that isn't a\n // well-formed ISO 4217 string (e.g. '', 'US', 'ABCD'). This is untrusted\n // customer-facing config, so validate the shape and fall back to USD;\n // a defensive try/catch keeps a malformed value from crashing the widget.\n const currency = resolveCurrencyCode(config.currency);\n const maximumFractionDigits = decimals !== null ? clampDecimals(decimals) : 2;\n try {\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n minimumFractionDigits: 0,\n maximumFractionDigits,\n }).format(value);\n } catch {\n formatted = new Intl.NumberFormat('en-US', { maximumFractionDigits }).format(value);\n }\n break;\n }\n case 'percentage': {\n const pctDecimals = decimals !== null ? clampDecimals(decimals) : 1;\n formatted = `${new Intl.NumberFormat('en-US', {\n minimumFractionDigits: pctDecimals,\n maximumFractionDigits: pctDecimals,\n }).format(value * 100)}%`;\n break;\n }\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: decimals !== null ? clampDecimals(decimals) : 1,\n }).format(value);\n break;\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: decimals !== null ? clampDecimals(decimals) : 2,\n }).format(value);\n }\n\n // `currency` already carries its own symbol; only apply an explicit prefix to\n // the other formats so we never render e.g. \"$$1,284,530\".\n if (prefix && format !== 'currency') formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n return formatted;\n}\n\nfunction clampDecimals(decimals: number): number {\n return Math.min(Math.max(Math.trunc(decimals), 0), 10);\n}\n\n/**\n * Narrow an untrusted `currency` config value to a well-formed ISO 4217 alpha\n * code (three letters), defaulting to USD. Prevents the `RangeError` that\n * `Intl.NumberFormat({ style: 'currency' })` throws on a malformed code.\n */\nfunction resolveCurrencyCode(value: unknown): string {\n const code = asString(value, 'USD').toUpperCase();\n return /^[A-Z]{3}$/.test(code) ? code : 'USD';\n}\n\n/**\n * Shared cell number formatter — constructed once (a table can render hundreds of\n * cells, and `new Intl.NumberFormat` per call is a measurable hot-path cost).\n *\n * `maximumFractionDigits: 20` (the API max) is LOSSLESS for JS doubles (~17\n * significant digits), so this only adds grouping separators — it never rounds\n * away precision. A blanket round (e.g. 4 dp) would corrupt customer-facing\n * high-precision ratios/coordinates/measurements (`0.0000123` → `0`), whereas\n * embed cells with no explicit column format must preserve the raw value.\n */\nconst CELL_NUMBER_FORMAT = new Intl.NumberFormat('en-US', { maximumFractionDigits: 20 });\n\n/**\n * Format an arbitrary table cell: finite numbers get thousands separators\n * (matching the in-app TableCell) at full precision, everything else falls back\n * to the safe string coercion that never leaks internal object structure.\n */\nexport function formatCell(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return CELL_NUMBER_FORMAT.format(value);\n }\n return displayValue(value);\n}\n\n/** True when a value is a finite number — used for right-aligning numeric columns. */\nexport function isNumeric(value: unknown): boolean {\n return typeof value === 'number' && Number.isFinite(value);\n}\n"],"mappings":";AAQA,SAAgB,EAAS,GAAgB,IAAW,IAAY;AAC9D,QAAO,OAAO,KAAU,WAAW,IAAQ;;AAG7C,SAAgB,EAAe,GAA+B;AAC5D,KAAI,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM,CAAE,QAAO;AAChE,KAAI,OAAO,KAAU,YAAY,EAAM,MAAM,KAAK,IAAI;EACpD,IAAM,IAAI,OAAO,EAAM;AACvB,MAAI,OAAO,SAAS,EAAE,CAAE,QAAO;;AAEjC,QAAO;;AAGT,SAAgB,EAAQ,GAA2B;AACjD,QAAO,MAAM,QAAQ,EAAM,GAAG,IAAQ,EAAE;;AAG1C,SAAgB,EAAS,GAAyC;AAChE,QAAO,KAAS,OAAO,KAAU,YAAY,CAAC,MAAM,QAAQ,EAAM,GAAI,IAAoC,EAAE;;AAO9G,SAAgB,EAAa,GAAwB;AAKnD,QAJI,KAAU,OAAoC,KAC9C,OAAO,KAAU,WAAiB,IAClC,OAAO,KAAU,WAAiB,OAAO,SAAS,EAAM,GAAG,OAAO,EAAM,GAAG,KAC3E,OAAO,KAAU,YAAkB,IAAQ,SAAS,UACjD;;AAST,SAAgB,EAAa,GAAe,GAAyC;CACnF,IAAM,IAAS,EAAS,EAAO,QAAQ,SAAS,EAC1C,IAAW,EAAe,EAAO,SAAS,EAC1C,IAAS,EAAS,EAAO,OAAO,EAChC,IAAS,EAAS,EAAO,OAAO,EAElC;AACJ,SAAQ,GAAR;EACE,KAAK,YAAY;GAKf,IAAM,IAAW,EAAoB,EAAO,SAAS,EAC/C,IAAwB,MAAa,OAAiC,IAA1B,EAAc,EAAS;AACzE,OAAI;AACF,QAAY,IAAI,KAAK,aAAa,SAAS;KACzC,OAAO;KACP;KACA,uBAAuB;KACvB;KACD,CAAC,CAAC,OAAO,EAAM;WACV;AACN,QAAY,IAAI,KAAK,aAAa,SAAS,EAAE,0BAAuB,CAAC,CAAC,OAAO,EAAM;;AAErF;;EAEF,KAAK,cAAc;GACjB,IAAM,IAAc,MAAa,OAAiC,IAA1B,EAAc,EAAS;AAC/D,OAAY,GAAG,IAAI,KAAK,aAAa,SAAS;IAC5C,uBAAuB;IACvB,uBAAuB;IACxB,CAAC,CAAC,OAAO,IAAQ,IAAI,CAAC;AACvB;;EAEF,KAAK;AACH,OAAY,IAAI,KAAK,aAAa,SAAS;IACzC,UAAU;IACV,uBAAuB,MAAa,OAAiC,IAA1B,EAAc,EAAS;IACnE,CAAC,CAAC,OAAO,EAAM;AAChB;EAEF,QACE,KAAY,IAAI,KAAK,aAAa,SAAS,EACzC,uBAAuB,MAAa,OAAiC,IAA1B,EAAc,EAAS,EACnE,CAAC,CAAC,OAAO,EAAM;;AAOpB,QAFI,KAAU,MAAW,eAAY,IAAY,GAAG,IAAS,MACzD,MAAQ,IAAY,GAAG,IAAY,MAChC;;AAGT,SAAS,EAAc,GAA0B;AAC/C,QAAO,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,EAAS,EAAE,EAAE,EAAE,GAAG;;AAQxD,SAAS,EAAoB,GAAwB;CACnD,IAAM,IAAO,EAAS,GAAO,MAAM,CAAC,aAAa;AACjD,QAAO,aAAa,KAAK,EAAK,GAAG,IAAO;;AAa1C,IAAM,IAAqB,IAAI,KAAK,aAAa,SAAS,EAAE,uBAAuB,IAAI,CAAC;AAOxF,SAAgB,EAAW,GAAwB;AAIjD,QAHI,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM,GAC9C,EAAmB,OAAO,EAAM,GAElC,EAAa,EAAM;;AAI5B,SAAgB,EAAU,GAAyB;AACjD,QAAO,OAAO,KAAU,YAAY,OAAO,SAAS,EAAM"}
@@ -1,5 +1,5 @@
1
1
  import { createContext as e, forwardRef as t, useCallback as n, useContext as r, useMemo as i } from "react";
2
- import { Link as a, useNavigate as o } from "react-router-dom";
2
+ import { Link as a, useNavigate as o } from "react-router";
3
3
  import { jsx as s } from "react/jsx-runtime";
4
4
  //#region src/bigconsole/contexts/NavigationContext.tsx
5
5
  var c = e(null);
@@ -1 +1 @@
1
- {"version":3,"file":"NavigationContext.js","names":[],"sources":["../../../src/bigconsole/contexts/NavigationContext.tsx"],"sourcesContent":["/**\n * NavigationContext\n *\n * Provides basePath-aware navigation for BigConsole microfrontend.\n * Ensures all internal navigation stays within the /bigconsole route prefix.\n */\n\nimport { createContext, useContext, useCallback, useMemo, forwardRef, type ReactNode } from 'react';\nimport { useNavigate as useRouterNavigate, Link, type NavigateOptions, type LinkProps } from 'react-router-dom';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface NavigationContextValue {\n /** Base path for all routes (e.g., '/bigconsole') */\n basePath: string;\n /** Navigate to a path within the microfrontend */\n navigate: (to: string, options?: NavigateOptions) => void;\n /** Build a full path with basePath prefix */\n buildPath: (path: string) => string;\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst NavigationContext = createContext<NavigationContextValue | null>(null);\n\n// ============================================================================\n// Provider\n// ============================================================================\n\ninterface NavigationProviderProps {\n /** Base path for the microfrontend (e.g., '/bigconsole') */\n basePath: string;\n children: ReactNode;\n}\n\nexport function NavigationProvider({ basePath, children }: NavigationProviderProps) {\n const routerNavigate = useRouterNavigate();\n\n // Normalize basePath (remove trailing slash)\n const normalizedBasePath = useMemo(() => {\n return basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n }, [basePath]);\n\n // Build full path with basePath prefix\n const buildPath = useCallback(\n (path: string): string => {\n // If path is empty or just '/', return basePath\n if (!path || path === '/') {\n return normalizedBasePath || '/';\n }\n\n // If path already starts with basePath, return as-is.\n // Guard against false prefix matches (e.g. basePath '/big' matching '/bigconsole/foo')\n // and empty normalizedBasePath (when basePath is '/') which is a prefix of everything.\n if (normalizedBasePath && (path === normalizedBasePath || path.startsWith(normalizedBasePath + '/'))) {\n return path;\n }\n\n // If path starts with '/', it's an absolute path within the microfrontend\n // Prepend basePath\n if (path.startsWith('/')) {\n return `${normalizedBasePath}${path}`;\n }\n\n // Relative path - just prepend basePath with slash\n return `${normalizedBasePath}/${path}`;\n },\n [normalizedBasePath]\n );\n\n // Navigate with basePath support\n const navigate = useCallback(\n (to: string, options?: NavigateOptions) => {\n const fullPath = buildPath(to);\n routerNavigate(fullPath, options);\n },\n [buildPath, routerNavigate]\n );\n\n const value = useMemo(\n () => ({\n basePath: normalizedBasePath,\n navigate,\n buildPath,\n }),\n [normalizedBasePath, navigate, buildPath]\n );\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * Hook to access navigation context\n * @throws Error if used outside NavigationProvider\n */\nexport function useNavigation(): NavigationContextValue {\n const context = useContext(NavigationContext);\n if (!context) {\n throw new Error('useNavigation must be used within a NavigationProvider');\n }\n return context;\n}\n\n/**\n * Hook for basePath-aware navigation\n * Returns a navigate function that automatically prepends basePath\n */\nexport function useAppNavigate() {\n const { navigate } = useNavigation();\n return navigate;\n}\n\n/**\n * Hook to build full paths with basePath prefix\n */\nexport function useBuildPath() {\n const { buildPath } = useNavigation();\n return buildPath;\n}\n\n/**\n * Hook to get the current basePath\n */\nexport function useBasePath() {\n const { basePath } = useNavigation();\n return basePath;\n}\n\n// ============================================================================\n// Components\n// ============================================================================\n\n/**\n * AppLink - basePath-aware Link component\n * Automatically prepends basePath to the 'to' prop\n */\nexport interface AppLinkProps extends Omit<LinkProps, 'to'> {\n to: string;\n}\n\nexport const AppLink = forwardRef<HTMLAnchorElement, AppLinkProps>(function AppLink({ to, ...props }, ref) {\n const { buildPath } = useNavigation();\n const fullPath = buildPath(to);\n return <Link ref={ref} to={fullPath} {...props} />;\n});\n"],"mappings":";;;;AA2BA,IAAM,IAAoB,EAA6C,KAAK;AAY5E,SAAgB,EAAmB,EAAE,aAAU,eAAqC;CAClF,IAAM,IAAiB,GAAmB,EAGpC,IAAqB,QAClB,EAAS,SAAS,IAAI,GAAG,EAAS,MAAM,GAAG,GAAG,GAAG,GACvD,CAAC,EAAS,CAAC,EAGR,IAAY,GACf,MAEK,CAAC,KAAQ,MAAS,MACb,KAAsB,MAM3B,MAAuB,MAAS,KAAsB,EAAK,WAAW,IAAqB,IAAI,IAC1F,IAKL,EAAK,WAAW,IAAI,GACf,GAAG,IAAqB,MAI1B,GAAG,EAAmB,GAAG,KAElC,CAAC,EAAmB,CACrB,EAGK,IAAW,GACd,GAAY,MAA8B;AAEzC,IADiB,EAAU,EAAG,EACL,EAAQ;IAEnC,CAAC,GAAW,EAAe,CAC5B,EAEK,IAAQ,SACL;EACL,UAAU;EACV;EACA;EACD,GACD;EAAC;EAAoB;EAAU;EAAU,CAC1C;AAED,QAAO,kBAAC,EAAkB,UAAnB;EAAmC;EAAQ;EAAsC,CAAA;;AAW1F,SAAgB,IAAwC;CACtD,IAAM,IAAU,EAAW,EAAkB;AAC7C,KAAI,CAAC,EACH,OAAU,MAAM,yDAAyD;AAE3E,QAAO;;AAOT,SAAgB,IAAiB;CAC/B,IAAM,EAAE,gBAAa,GAAe;AACpC,QAAO;;AA+BT,IAAa,IAAU,EAA4C,SAAiB,EAAE,OAAI,GAAG,KAAS,GAAK;CACzG,IAAM,EAAE,iBAAc,GAAe;AAErC,QAAO,kBAAC,GAAD;EAAW;EAAK,IADN,EAAU,EAAG;EACO,GAAI;EAAS,CAAA;EAClD"}
1
+ {"version":3,"file":"NavigationContext.js","names":[],"sources":["../../../src/bigconsole/contexts/NavigationContext.tsx"],"sourcesContent":["/**\n * NavigationContext\n *\n * Provides basePath-aware navigation for BigConsole microfrontend.\n * Ensures all internal navigation stays within the /bigconsole route prefix.\n */\n\nimport { createContext, useContext, useCallback, useMemo, forwardRef, type ReactNode } from 'react';\nimport { useNavigate as useRouterNavigate, Link, type NavigateOptions, type LinkProps } from 'react-router';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface NavigationContextValue {\n /** Base path for all routes (e.g., '/bigconsole') */\n basePath: string;\n /** Navigate to a path within the microfrontend */\n navigate: (to: string, options?: NavigateOptions) => void;\n /** Build a full path with basePath prefix */\n buildPath: (path: string) => string;\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst NavigationContext = createContext<NavigationContextValue | null>(null);\n\n// ============================================================================\n// Provider\n// ============================================================================\n\ninterface NavigationProviderProps {\n /** Base path for the microfrontend (e.g., '/bigconsole') */\n basePath: string;\n children: ReactNode;\n}\n\nexport function NavigationProvider({ basePath, children }: NavigationProviderProps) {\n const routerNavigate = useRouterNavigate();\n\n // Normalize basePath (remove trailing slash)\n const normalizedBasePath = useMemo(() => {\n return basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n }, [basePath]);\n\n // Build full path with basePath prefix\n const buildPath = useCallback(\n (path: string): string => {\n // If path is empty or just '/', return basePath\n if (!path || path === '/') {\n return normalizedBasePath || '/';\n }\n\n // If path already starts with basePath, return as-is.\n // Guard against false prefix matches (e.g. basePath '/big' matching '/bigconsole/foo')\n // and empty normalizedBasePath (when basePath is '/') which is a prefix of everything.\n if (normalizedBasePath && (path === normalizedBasePath || path.startsWith(normalizedBasePath + '/'))) {\n return path;\n }\n\n // If path starts with '/', it's an absolute path within the microfrontend\n // Prepend basePath\n if (path.startsWith('/')) {\n return `${normalizedBasePath}${path}`;\n }\n\n // Relative path - just prepend basePath with slash\n return `${normalizedBasePath}/${path}`;\n },\n [normalizedBasePath]\n );\n\n // Navigate with basePath support\n const navigate = useCallback(\n (to: string, options?: NavigateOptions) => {\n const fullPath = buildPath(to);\n routerNavigate(fullPath, options);\n },\n [buildPath, routerNavigate]\n );\n\n const value = useMemo(\n () => ({\n basePath: normalizedBasePath,\n navigate,\n buildPath,\n }),\n [normalizedBasePath, navigate, buildPath]\n );\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * Hook to access navigation context\n * @throws Error if used outside NavigationProvider\n */\nexport function useNavigation(): NavigationContextValue {\n const context = useContext(NavigationContext);\n if (!context) {\n throw new Error('useNavigation must be used within a NavigationProvider');\n }\n return context;\n}\n\n/**\n * Hook for basePath-aware navigation\n * Returns a navigate function that automatically prepends basePath\n */\nexport function useAppNavigate() {\n const { navigate } = useNavigation();\n return navigate;\n}\n\n/**\n * Hook to build full paths with basePath prefix\n */\nexport function useBuildPath() {\n const { buildPath } = useNavigation();\n return buildPath;\n}\n\n/**\n * Hook to get the current basePath\n */\nexport function useBasePath() {\n const { basePath } = useNavigation();\n return basePath;\n}\n\n// ============================================================================\n// Components\n// ============================================================================\n\n/**\n * AppLink - basePath-aware Link component\n * Automatically prepends basePath to the 'to' prop\n */\nexport interface AppLinkProps extends Omit<LinkProps, 'to'> {\n to: string;\n}\n\nexport const AppLink = forwardRef<HTMLAnchorElement, AppLinkProps>(function AppLink({ to, ...props }, ref) {\n const { buildPath } = useNavigation();\n const fullPath = buildPath(to);\n return <Link ref={ref} to={fullPath} {...props} />;\n});\n"],"mappings":";;;;AA2BA,IAAM,IAAoB,EAA6C,KAAK;AAY5E,SAAgB,EAAmB,EAAE,aAAU,eAAqC;CAClF,IAAM,IAAiB,GAAmB,EAGpC,IAAqB,QAClB,EAAS,SAAS,IAAI,GAAG,EAAS,MAAM,GAAG,GAAG,GAAG,GACvD,CAAC,EAAS,CAAC,EAGR,IAAY,GACf,MAEK,CAAC,KAAQ,MAAS,MACb,KAAsB,MAM3B,MAAuB,MAAS,KAAsB,EAAK,WAAW,IAAqB,IAAI,IAC1F,IAKL,EAAK,WAAW,IAAI,GACf,GAAG,IAAqB,MAI1B,GAAG,EAAmB,GAAG,KAElC,CAAC,EAAmB,CACrB,EAGK,IAAW,GACd,GAAY,MAA8B;AAEzC,IADiB,EAAU,EAAG,EACL,EAAQ;IAEnC,CAAC,GAAW,EAAe,CAC5B,EAEK,IAAQ,SACL;EACL,UAAU;EACV;EACA;EACD,GACD;EAAC;EAAoB;EAAU;EAAU,CAC1C;AAED,QAAO,kBAAC,EAAkB,UAAnB;EAAmC;EAAQ;EAAsC,CAAA;;AAW1F,SAAgB,IAAwC;CACtD,IAAM,IAAU,EAAW,EAAkB;AAC7C,KAAI,CAAC,EACH,OAAU,MAAM,yDAAyD;AAE3E,QAAO;;AAOT,SAAgB,IAAiB;CAC/B,IAAM,EAAE,gBAAa,GAAe;AACpC,QAAO;;AA+BT,IAAa,IAAU,EAA4C,SAAiB,EAAE,OAAI,GAAG,KAAS,GAAK;CACzG,IAAM,EAAE,iBAAc,GAAe;AAErC,QAAO,kBAAC,GAAD;EAAW;EAAK,IADN,EAAU,EAAG;EACO,GAAI;EAAS,CAAA;EAClD"}
@@ -2,7 +2,7 @@ import { useDashboardStore as e, useGlobalFilterValues as t } from "../store/das
2
2
  import "../store/index.js";
3
3
  import { CONTEXT_PARAM_PREFIX as n, DRILLDOWN_PARAM_PREFIX as r, FILTER_PARAM_PREFIX as i } from "./useDrilldown.js";
4
4
  import { useCallback as a, useEffect as o, useMemo as s, useRef as c } from "react";
5
- import { useLocation as l } from "react-router-dom";
5
+ import { useLocation as l } from "react-router";
6
6
  //#region src/bigconsole/hooks/useFilterUrlSync.ts
7
7
  function u(e) {
8
8
  if (e == null) return "";
@@ -1 +1 @@
1
- {"version":3,"file":"useFilterUrlSync.js","names":[],"sources":["../../../src/bigconsole/hooks/useFilterUrlSync.ts"],"sourcesContent":["/**\n * useFilterUrlSync Hook\n *\n * Syncs global filter values to URL query parameters for sharing filtered views.\n * Supports all filter types: DATE_RANGE, CATEGORY, TEXT_SEARCH, NUMERIC_RANGE, CUSTOM\n *\n * Also handles drilldown context parameters:\n * - filter_* : Global filter values\n * - ctx_* : Drilldown context values (clicked item data)\n * - drilldown_* : Drilldown metadata (depth, parent, path)\n */\n\nimport { useEffect, useCallback, useRef, useMemo } from 'react';\nimport { useLocation } from 'react-router-dom';\nimport { useGlobalFilterValues, useDashboardStore } from '../store';\nimport { CONTEXT_PARAM_PREFIX, FILTER_PARAM_PREFIX, DRILLDOWN_PARAM_PREFIX } from './useDrilldown';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface UseFilterUrlSyncOptions {\n /** Whether URL sync is enabled */\n enabled?: boolean;\n /** Prefix for URL params (default: 'filter_') */\n paramPrefix?: string;\n /** Debounce delay in ms for URL updates (default: 300) */\n debounceMs?: number;\n}\n\ninterface DrilldownMetadata {\n /** Current drilldown depth */\n depth: number;\n /** Parent dashboard ID */\n parent: string | null;\n /** Array of dashboard IDs in the drilldown path */\n path: string[];\n}\n\ninterface FilterUrlSyncResult {\n /** Manually sync current filters to URL */\n syncToUrl: () => void;\n /** Get shareable URL with current filters */\n getShareableUrl: () => string;\n /** Clear all filter params from URL */\n clearUrlParams: () => void;\n /** Get context params from URL (ctx_*) */\n getContextFromUrl: () => Record<string, unknown>;\n /** Get drilldown metadata from URL */\n getDrilldownMetadata: () => DrilldownMetadata;\n /** Check if currently in drilldown mode */\n isInDrilldown: boolean;\n /** Current drilldown depth */\n drilldownDepth: number;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Encode filter value for URL\n */\nfunction encodeFilterValue(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n\n // Array (multi-select): [\"a\", \"b\"] -> \"a,b\"\n if (Array.isArray(value)) {\n return value.map((v) => encodeURIComponent(String(v))).join(',');\n }\n\n // Object types\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n\n // Date range: {start, end} -> \"start,end\"\n if (obj.start || obj.end) {\n return `${obj.start || ''},${obj.end || ''}`;\n }\n\n // Numeric range: {min, max} -> \"min,max\"\n if (obj.min !== undefined || obj.max !== undefined) {\n return `${obj.min ?? ''},${obj.max ?? ''}`;\n }\n\n // Object with value property (category)\n if (obj.value !== undefined) {\n return encodeURIComponent(String(obj.value));\n }\n\n // Generic object - JSON stringify\n return encodeURIComponent(JSON.stringify(obj));\n }\n\n // Simple value\n return encodeURIComponent(String(value));\n}\n\n/**\n * Decode filter value from URL\n */\nfunction decodeFilterValue(encoded: string, filterType?: string): unknown {\n if (!encoded) {\n return undefined;\n }\n\n // Date range\n if (filterType === 'DATE_RANGE' || (encoded.includes('T') && encoded.includes(','))) {\n const parts = encoded.split(',');\n if (parts.length === 2) {\n return {\n start: parts[0] || undefined,\n end: parts[1] || undefined,\n };\n }\n }\n\n // Numeric range (check if both parts are numbers)\n if (filterType === 'NUMERIC_RANGE' || /^-?\\d*\\.?\\d*,-?\\d*\\.?\\d*$/.test(encoded)) {\n const parts = encoded.split(',');\n if (parts.length === 2) {\n const min = parts[0] ? parseFloat(parts[0]) : undefined;\n const max = parts[1] ? parseFloat(parts[1]) : undefined;\n if (!isNaN(min as number) || !isNaN(max as number)) {\n return { min, max };\n }\n }\n }\n\n // Multi-select array\n if (encoded.includes(',') && filterType === 'CATEGORY') {\n return encoded.split(',').map((v) => decodeURIComponent(v));\n }\n\n // Try to parse as JSON\n try {\n const decoded = decodeURIComponent(encoded);\n return JSON.parse(decoded);\n } catch {\n // Simple value\n return decodeURIComponent(encoded);\n }\n}\n\n/**\n * Decode a context param value from URL\n */\nfunction decodeContextValue(encoded: string): unknown {\n if (!encoded) {\n return undefined;\n }\n const decoded = decodeURIComponent(encoded);\n // Try to parse as JSON\n try {\n return JSON.parse(decoded);\n } catch {\n return decoded;\n }\n}\n\n/**\n * Parse URL search params to filter values\n */\nfunction parseUrlToFilters(search: string, paramPrefix: string): Record<string, unknown> {\n const params = new URLSearchParams(search);\n const filters: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith(paramPrefix)) {\n const filterId = key.slice(paramPrefix.length);\n // Try to detect type from value format\n filters[filterId] = decodeFilterValue(value);\n }\n });\n\n return filters;\n}\n\n/**\n * Parse URL search params to context values\n */\nfunction parseUrlToContext(search: string): Record<string, unknown> {\n const params = new URLSearchParams(search);\n const context: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith(CONTEXT_PARAM_PREFIX)) {\n const contextKey = key.slice(CONTEXT_PARAM_PREFIX.length);\n context[contextKey] = decodeContextValue(value);\n }\n });\n\n return context;\n}\n\n/**\n * Parse drilldown metadata from URL\n */\nfunction parseUrlToDrilldownMetadata(search: string): DrilldownMetadata {\n const params = new URLSearchParams(search);\n const depthStr = params.get(`${DRILLDOWN_PARAM_PREFIX}depth`);\n const parent = params.get(`${DRILLDOWN_PARAM_PREFIX}parent`);\n const pathStr = params.get(`${DRILLDOWN_PARAM_PREFIX}path`);\n\n let path: string[] = [];\n if (pathStr) {\n try {\n path = JSON.parse(decodeURIComponent(pathStr));\n } catch {\n path = [];\n }\n }\n\n return {\n depth: depthStr ? parseInt(depthStr, 10) : 0,\n parent: parent || null,\n path,\n };\n}\n\n/**\n * Build URL search params from filter values\n */\nfunction filtersToUrlParams(filters: Record<string, unknown>, paramPrefix: string): URLSearchParams {\n const params = new URLSearchParams();\n\n Object.entries(filters).forEach(([filterId, value]) => {\n if (value !== undefined && value !== null && value !== '') {\n const encoded = encodeFilterValue(value);\n if (encoded) {\n params.set(`${paramPrefix}${filterId}`, encoded);\n }\n }\n });\n\n return params;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useFilterUrlSync(options: UseFilterUrlSyncOptions = {}): FilterUrlSyncResult {\n const { enabled = true, paramPrefix = FILTER_PARAM_PREFIX, debounceMs = 300 } = options;\n\n // Use react-router's useLocation to react to URL changes\n const location = useLocation();\n\n const filterValues = useGlobalFilterValues();\n const setGlobalFilterValue = useDashboardStore((state) => state.setGlobalFilterValue);\n\n // Track if initial load from URL is done\n const initialLoadDone = useRef(false);\n // Track last synced values to avoid loops\n const lastSyncedValues = useRef<string>('');\n // Debounce timer\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Track the last pathname to detect same-route navigations (e.g. drilldown)\n const lastPathname = useRef(location.pathname);\n\n // Reset initialLoadDone when pathname changes (e.g. drilldown to different dashboard).\n // Without this, same-route navigation reuses the component and the filter_* URL params\n // from the drilldown URL are never loaded into the Zustand store.\n useEffect(() => {\n if (location.pathname !== lastPathname.current) {\n initialLoadDone.current = false;\n lastSyncedValues.current = '';\n lastPathname.current = location.pathname;\n }\n }, [location.pathname]);\n\n // Compute drilldown state from URL - now reactive to location.search changes\n const drilldownMetadata = useMemo(() => {\n if (typeof window === 'undefined') {\n return { depth: 0, parent: null, path: [] };\n }\n return parseUrlToDrilldownMetadata(location.search);\n }, [location.search]);\n\n const isInDrilldown = drilldownMetadata.depth > 0;\n const drilldownDepth = drilldownMetadata.depth;\n\n // Load filters from URL on initial mount\n useEffect(() => {\n if (!enabled || initialLoadDone.current) return;\n\n // Parse filter params\n const urlFilters = parseUrlToFilters(window.location.search, paramPrefix);\n\n if (Object.keys(urlFilters).length > 0) {\n // Set each filter value\n Object.entries(urlFilters).forEach(([filterId, value]) => {\n setGlobalFilterValue(filterId, value);\n });\n }\n\n // IMPORTANT: Set lastSyncedValues to current filterValues AFTER the sync effect runs\n // This prevents the race condition where sync effect runs with stale (empty) filterValues\n // and strips the URL params that we just loaded.\n // We use the URL filters as the initial synced values since that's what we loaded from.\n lastSyncedValues.current = JSON.stringify(urlFilters);\n\n initialLoadDone.current = true;\n }, [enabled, paramPrefix, setGlobalFilterValue]);\n\n // Sync filter values to URL when they change\n useEffect(() => {\n if (!enabled || !initialLoadDone.current) return;\n\n const currentValues = JSON.stringify(filterValues);\n\n // Skip if values haven't changed\n if (currentValues === lastSyncedValues.current) {\n return;\n }\n\n // Debounce URL updates\n if (debounceTimer.current) {\n clearTimeout(debounceTimer.current);\n }\n\n debounceTimer.current = setTimeout(() => {\n const params = filtersToUrlParams(filterValues, paramPrefix);\n\n // Get current URL without filter params (but keep other params like ctx_*, drilldown_*)\n const currentParams = new URLSearchParams(window.location.search);\n // Remove existing filter params only\n Array.from(currentParams.keys()).forEach((key) => {\n if (key.startsWith(paramPrefix)) {\n currentParams.delete(key);\n }\n });\n\n // Merge with new filter params\n params.forEach((value, key) => {\n currentParams.set(key, value);\n });\n\n const finalSearch = currentParams.toString();\n const newUrl = finalSearch ? `${window.location.pathname}?${finalSearch}` : window.location.pathname;\n\n // Update URL without triggering navigation\n window.history.replaceState({ ...window.history.state, filters: filterValues }, '', newUrl);\n\n lastSyncedValues.current = currentValues;\n }, debounceMs);\n\n return () => {\n if (debounceTimer.current) {\n clearTimeout(debounceTimer.current);\n }\n };\n }, [enabled, filterValues, paramPrefix, debounceMs]);\n\n // Manual sync to URL\n const syncToUrl = useCallback(() => {\n if (!enabled) return;\n\n const params = filtersToUrlParams(filterValues, paramPrefix);\n const currentParams = new URLSearchParams(window.location.search);\n\n // Remove existing filter params only\n Array.from(currentParams.keys()).forEach((key) => {\n if (key.startsWith(paramPrefix)) {\n currentParams.delete(key);\n }\n });\n\n // Merge with new filter params\n params.forEach((value, key) => {\n currentParams.set(key, value);\n });\n\n const finalSearch = currentParams.toString();\n const newUrl = finalSearch ? `${window.location.pathname}?${finalSearch}` : window.location.pathname;\n\n window.history.replaceState({ ...window.history.state, filters: filterValues }, '', newUrl);\n\n lastSyncedValues.current = JSON.stringify(filterValues);\n }, [enabled, filterValues, paramPrefix]);\n\n // Get shareable URL\n const getShareableUrl = useCallback((): string => {\n const params = filtersToUrlParams(filterValues, paramPrefix);\n const baseUrl = `${window.location.origin}${window.location.pathname}`;\n\n // Preserve non-filter params (ctx_*, drilldown_*)\n const currentParams = new URLSearchParams(window.location.search);\n Array.from(currentParams.keys()).forEach((key) => {\n if (!key.startsWith(paramPrefix)) {\n params.set(key, currentParams.get(key) || '');\n }\n });\n\n const search = params.toString();\n return search ? `${baseUrl}?${search}` : baseUrl;\n }, [filterValues, paramPrefix]);\n\n // Clear all filter params from URL\n const clearUrlParams = useCallback(() => {\n const currentParams = new URLSearchParams(window.location.search);\n\n // Remove filter params only (keep ctx_*, drilldown_*)\n Array.from(currentParams.keys()).forEach((key) => {\n if (key.startsWith(paramPrefix)) {\n currentParams.delete(key);\n }\n });\n\n const finalSearch = currentParams.toString();\n const newUrl = finalSearch ? `${window.location.pathname}?${finalSearch}` : window.location.pathname;\n\n window.history.replaceState({}, '', newUrl);\n lastSyncedValues.current = '{}';\n }, [paramPrefix]);\n\n // Get context params from URL\n const getContextFromUrl = useCallback((): Record<string, unknown> => {\n return parseUrlToContext(window.location.search);\n }, []);\n\n // Get drilldown metadata from URL\n const getDrilldownMetadata = useCallback((): DrilldownMetadata => {\n return parseUrlToDrilldownMetadata(window.location.search);\n }, []);\n\n return {\n syncToUrl,\n getShareableUrl,\n clearUrlParams,\n getContextFromUrl,\n getDrilldownMetadata,\n isInDrilldown,\n drilldownDepth,\n };\n}\n\nexport default useFilterUrlSync;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useFilterUrlSync.js","names":[],"sources":["../../../src/bigconsole/hooks/useFilterUrlSync.ts"],"sourcesContent":["/**\n * useFilterUrlSync Hook\n *\n * Syncs global filter values to URL query parameters for sharing filtered views.\n * Supports all filter types: DATE_RANGE, CATEGORY, TEXT_SEARCH, NUMERIC_RANGE, CUSTOM\n *\n * Also handles drilldown context parameters:\n * - filter_* : Global filter values\n * - ctx_* : Drilldown context values (clicked item data)\n * - drilldown_* : Drilldown metadata (depth, parent, path)\n */\n\nimport { useEffect, useCallback, useRef, useMemo } from 'react';\nimport { useLocation } from 'react-router';\nimport { useGlobalFilterValues, useDashboardStore } from '../store';\nimport { CONTEXT_PARAM_PREFIX, FILTER_PARAM_PREFIX, DRILLDOWN_PARAM_PREFIX } from './useDrilldown';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface UseFilterUrlSyncOptions {\n /** Whether URL sync is enabled */\n enabled?: boolean;\n /** Prefix for URL params (default: 'filter_') */\n paramPrefix?: string;\n /** Debounce delay in ms for URL updates (default: 300) */\n debounceMs?: number;\n}\n\ninterface DrilldownMetadata {\n /** Current drilldown depth */\n depth: number;\n /** Parent dashboard ID */\n parent: string | null;\n /** Array of dashboard IDs in the drilldown path */\n path: string[];\n}\n\ninterface FilterUrlSyncResult {\n /** Manually sync current filters to URL */\n syncToUrl: () => void;\n /** Get shareable URL with current filters */\n getShareableUrl: () => string;\n /** Clear all filter params from URL */\n clearUrlParams: () => void;\n /** Get context params from URL (ctx_*) */\n getContextFromUrl: () => Record<string, unknown>;\n /** Get drilldown metadata from URL */\n getDrilldownMetadata: () => DrilldownMetadata;\n /** Check if currently in drilldown mode */\n isInDrilldown: boolean;\n /** Current drilldown depth */\n drilldownDepth: number;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Encode filter value for URL\n */\nfunction encodeFilterValue(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n\n // Array (multi-select): [\"a\", \"b\"] -> \"a,b\"\n if (Array.isArray(value)) {\n return value.map((v) => encodeURIComponent(String(v))).join(',');\n }\n\n // Object types\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n\n // Date range: {start, end} -> \"start,end\"\n if (obj.start || obj.end) {\n return `${obj.start || ''},${obj.end || ''}`;\n }\n\n // Numeric range: {min, max} -> \"min,max\"\n if (obj.min !== undefined || obj.max !== undefined) {\n return `${obj.min ?? ''},${obj.max ?? ''}`;\n }\n\n // Object with value property (category)\n if (obj.value !== undefined) {\n return encodeURIComponent(String(obj.value));\n }\n\n // Generic object - JSON stringify\n return encodeURIComponent(JSON.stringify(obj));\n }\n\n // Simple value\n return encodeURIComponent(String(value));\n}\n\n/**\n * Decode filter value from URL\n */\nfunction decodeFilterValue(encoded: string, filterType?: string): unknown {\n if (!encoded) {\n return undefined;\n }\n\n // Date range\n if (filterType === 'DATE_RANGE' || (encoded.includes('T') && encoded.includes(','))) {\n const parts = encoded.split(',');\n if (parts.length === 2) {\n return {\n start: parts[0] || undefined,\n end: parts[1] || undefined,\n };\n }\n }\n\n // Numeric range (check if both parts are numbers)\n if (filterType === 'NUMERIC_RANGE' || /^-?\\d*\\.?\\d*,-?\\d*\\.?\\d*$/.test(encoded)) {\n const parts = encoded.split(',');\n if (parts.length === 2) {\n const min = parts[0] ? parseFloat(parts[0]) : undefined;\n const max = parts[1] ? parseFloat(parts[1]) : undefined;\n if (!isNaN(min as number) || !isNaN(max as number)) {\n return { min, max };\n }\n }\n }\n\n // Multi-select array\n if (encoded.includes(',') && filterType === 'CATEGORY') {\n return encoded.split(',').map((v) => decodeURIComponent(v));\n }\n\n // Try to parse as JSON\n try {\n const decoded = decodeURIComponent(encoded);\n return JSON.parse(decoded);\n } catch {\n // Simple value\n return decodeURIComponent(encoded);\n }\n}\n\n/**\n * Decode a context param value from URL\n */\nfunction decodeContextValue(encoded: string): unknown {\n if (!encoded) {\n return undefined;\n }\n const decoded = decodeURIComponent(encoded);\n // Try to parse as JSON\n try {\n return JSON.parse(decoded);\n } catch {\n return decoded;\n }\n}\n\n/**\n * Parse URL search params to filter values\n */\nfunction parseUrlToFilters(search: string, paramPrefix: string): Record<string, unknown> {\n const params = new URLSearchParams(search);\n const filters: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith(paramPrefix)) {\n const filterId = key.slice(paramPrefix.length);\n // Try to detect type from value format\n filters[filterId] = decodeFilterValue(value);\n }\n });\n\n return filters;\n}\n\n/**\n * Parse URL search params to context values\n */\nfunction parseUrlToContext(search: string): Record<string, unknown> {\n const params = new URLSearchParams(search);\n const context: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith(CONTEXT_PARAM_PREFIX)) {\n const contextKey = key.slice(CONTEXT_PARAM_PREFIX.length);\n context[contextKey] = decodeContextValue(value);\n }\n });\n\n return context;\n}\n\n/**\n * Parse drilldown metadata from URL\n */\nfunction parseUrlToDrilldownMetadata(search: string): DrilldownMetadata {\n const params = new URLSearchParams(search);\n const depthStr = params.get(`${DRILLDOWN_PARAM_PREFIX}depth`);\n const parent = params.get(`${DRILLDOWN_PARAM_PREFIX}parent`);\n const pathStr = params.get(`${DRILLDOWN_PARAM_PREFIX}path`);\n\n let path: string[] = [];\n if (pathStr) {\n try {\n path = JSON.parse(decodeURIComponent(pathStr));\n } catch {\n path = [];\n }\n }\n\n return {\n depth: depthStr ? parseInt(depthStr, 10) : 0,\n parent: parent || null,\n path,\n };\n}\n\n/**\n * Build URL search params from filter values\n */\nfunction filtersToUrlParams(filters: Record<string, unknown>, paramPrefix: string): URLSearchParams {\n const params = new URLSearchParams();\n\n Object.entries(filters).forEach(([filterId, value]) => {\n if (value !== undefined && value !== null && value !== '') {\n const encoded = encodeFilterValue(value);\n if (encoded) {\n params.set(`${paramPrefix}${filterId}`, encoded);\n }\n }\n });\n\n return params;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useFilterUrlSync(options: UseFilterUrlSyncOptions = {}): FilterUrlSyncResult {\n const { enabled = true, paramPrefix = FILTER_PARAM_PREFIX, debounceMs = 300 } = options;\n\n // Use react-router's useLocation to react to URL changes\n const location = useLocation();\n\n const filterValues = useGlobalFilterValues();\n const setGlobalFilterValue = useDashboardStore((state) => state.setGlobalFilterValue);\n\n // Track if initial load from URL is done\n const initialLoadDone = useRef(false);\n // Track last synced values to avoid loops\n const lastSyncedValues = useRef<string>('');\n // Debounce timer\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Track the last pathname to detect same-route navigations (e.g. drilldown)\n const lastPathname = useRef(location.pathname);\n\n // Reset initialLoadDone when pathname changes (e.g. drilldown to different dashboard).\n // Without this, same-route navigation reuses the component and the filter_* URL params\n // from the drilldown URL are never loaded into the Zustand store.\n useEffect(() => {\n if (location.pathname !== lastPathname.current) {\n initialLoadDone.current = false;\n lastSyncedValues.current = '';\n lastPathname.current = location.pathname;\n }\n }, [location.pathname]);\n\n // Compute drilldown state from URL - now reactive to location.search changes\n const drilldownMetadata = useMemo(() => {\n if (typeof window === 'undefined') {\n return { depth: 0, parent: null, path: [] };\n }\n return parseUrlToDrilldownMetadata(location.search);\n }, [location.search]);\n\n const isInDrilldown = drilldownMetadata.depth > 0;\n const drilldownDepth = drilldownMetadata.depth;\n\n // Load filters from URL on initial mount\n useEffect(() => {\n if (!enabled || initialLoadDone.current) return;\n\n // Parse filter params\n const urlFilters = parseUrlToFilters(window.location.search, paramPrefix);\n\n if (Object.keys(urlFilters).length > 0) {\n // Set each filter value\n Object.entries(urlFilters).forEach(([filterId, value]) => {\n setGlobalFilterValue(filterId, value);\n });\n }\n\n // IMPORTANT: Set lastSyncedValues to current filterValues AFTER the sync effect runs\n // This prevents the race condition where sync effect runs with stale (empty) filterValues\n // and strips the URL params that we just loaded.\n // We use the URL filters as the initial synced values since that's what we loaded from.\n lastSyncedValues.current = JSON.stringify(urlFilters);\n\n initialLoadDone.current = true;\n }, [enabled, paramPrefix, setGlobalFilterValue]);\n\n // Sync filter values to URL when they change\n useEffect(() => {\n if (!enabled || !initialLoadDone.current) return;\n\n const currentValues = JSON.stringify(filterValues);\n\n // Skip if values haven't changed\n if (currentValues === lastSyncedValues.current) {\n return;\n }\n\n // Debounce URL updates\n if (debounceTimer.current) {\n clearTimeout(debounceTimer.current);\n }\n\n debounceTimer.current = setTimeout(() => {\n const params = filtersToUrlParams(filterValues, paramPrefix);\n\n // Get current URL without filter params (but keep other params like ctx_*, drilldown_*)\n const currentParams = new URLSearchParams(window.location.search);\n // Remove existing filter params only\n Array.from(currentParams.keys()).forEach((key) => {\n if (key.startsWith(paramPrefix)) {\n currentParams.delete(key);\n }\n });\n\n // Merge with new filter params\n params.forEach((value, key) => {\n currentParams.set(key, value);\n });\n\n const finalSearch = currentParams.toString();\n const newUrl = finalSearch ? `${window.location.pathname}?${finalSearch}` : window.location.pathname;\n\n // Update URL without triggering navigation\n window.history.replaceState({ ...window.history.state, filters: filterValues }, '', newUrl);\n\n lastSyncedValues.current = currentValues;\n }, debounceMs);\n\n return () => {\n if (debounceTimer.current) {\n clearTimeout(debounceTimer.current);\n }\n };\n }, [enabled, filterValues, paramPrefix, debounceMs]);\n\n // Manual sync to URL\n const syncToUrl = useCallback(() => {\n if (!enabled) return;\n\n const params = filtersToUrlParams(filterValues, paramPrefix);\n const currentParams = new URLSearchParams(window.location.search);\n\n // Remove existing filter params only\n Array.from(currentParams.keys()).forEach((key) => {\n if (key.startsWith(paramPrefix)) {\n currentParams.delete(key);\n }\n });\n\n // Merge with new filter params\n params.forEach((value, key) => {\n currentParams.set(key, value);\n });\n\n const finalSearch = currentParams.toString();\n const newUrl = finalSearch ? `${window.location.pathname}?${finalSearch}` : window.location.pathname;\n\n window.history.replaceState({ ...window.history.state, filters: filterValues }, '', newUrl);\n\n lastSyncedValues.current = JSON.stringify(filterValues);\n }, [enabled, filterValues, paramPrefix]);\n\n // Get shareable URL\n const getShareableUrl = useCallback((): string => {\n const params = filtersToUrlParams(filterValues, paramPrefix);\n const baseUrl = `${window.location.origin}${window.location.pathname}`;\n\n // Preserve non-filter params (ctx_*, drilldown_*)\n const currentParams = new URLSearchParams(window.location.search);\n Array.from(currentParams.keys()).forEach((key) => {\n if (!key.startsWith(paramPrefix)) {\n params.set(key, currentParams.get(key) || '');\n }\n });\n\n const search = params.toString();\n return search ? `${baseUrl}?${search}` : baseUrl;\n }, [filterValues, paramPrefix]);\n\n // Clear all filter params from URL\n const clearUrlParams = useCallback(() => {\n const currentParams = new URLSearchParams(window.location.search);\n\n // Remove filter params only (keep ctx_*, drilldown_*)\n Array.from(currentParams.keys()).forEach((key) => {\n if (key.startsWith(paramPrefix)) {\n currentParams.delete(key);\n }\n });\n\n const finalSearch = currentParams.toString();\n const newUrl = finalSearch ? `${window.location.pathname}?${finalSearch}` : window.location.pathname;\n\n window.history.replaceState({}, '', newUrl);\n lastSyncedValues.current = '{}';\n }, [paramPrefix]);\n\n // Get context params from URL\n const getContextFromUrl = useCallback((): Record<string, unknown> => {\n return parseUrlToContext(window.location.search);\n }, []);\n\n // Get drilldown metadata from URL\n const getDrilldownMetadata = useCallback((): DrilldownMetadata => {\n return parseUrlToDrilldownMetadata(window.location.search);\n }, []);\n\n return {\n syncToUrl,\n getShareableUrl,\n clearUrlParams,\n getContextFromUrl,\n getDrilldownMetadata,\n isInDrilldown,\n drilldownDepth,\n };\n}\n\nexport default useFilterUrlSync;\n"],"mappings":"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"}
@@ -18,7 +18,7 @@ import "../components/dashboard/index.js";
18
18
  import f from "../customWidget/CustomWidgetBuilderSheet.js";
19
19
  import "../customWidget/index.js";
20
20
  import { memo as p, useCallback as m, useEffect as h, useState as g } from "react";
21
- import { useParams as ee } from "react-router-dom";
21
+ import { useParams as ee } from "react-router";
22
22
  import { jsx as _, jsxs as v } from "react/jsx-runtime";
23
23
  //#region src/bigconsole/pages/DashboardBuilderPage.tsx
24
24
  function y() {
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardBuilderPage.js","names":[],"sources":["../../../src/bigconsole/pages/DashboardBuilderPage.tsx"],"sourcesContent":["/**\n * DashboardBuilderPage\n *\n * Full dashboard builder experience with side panels.\n */\n\nimport { type FC, memo, useState, useEffect, useCallback } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useBigConsoleNavigate } from '../context';\nimport { DashboardCanvas, ExportDashboardDialog } from '../components/dashboard';\nimport { BuilderCanvas, BuilderTopbar, DashboardEditorShell } from '../components/builder';\nimport { useDashboardStore, useWidgetStore } from '../store';\nimport { useDashboardOperations, useWidgetOperations, usePageOperations } from '../hooks';\nimport { CustomWidgetBuilderSheet } from '../customWidget';\nimport type { Dashboard, DashboardPage } from '../types';\nimport { findNextAvailablePosition } from '../utils/widgetPositioning';\n\n// Breadcrumb name registration helpers (get from shell at call time to avoid timing issues)\nfunction getBreadcrumbRegistry() {\n try {\n return (\n window as unknown as {\n __breadcrumbRegistry?: { register: (id: string, name: string) => void; unregister: (id: string) => void };\n }\n ).__breadcrumbRegistry;\n } catch {\n return undefined;\n }\n}\n\nfunction registerBreadcrumbName(id: string, name: string) {\n const registry = getBreadcrumbRegistry();\n if (registry?.register) {\n registry.register(id, name);\n }\n}\n\nfunction unregisterBreadcrumbName(id: string) {\n const registry = getBreadcrumbRegistry();\n if (registry?.unregister) {\n registry.unregister(id);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardBuilderPage: FC = memo(function DashboardBuilderPage() {\n const { dashboardId } = useParams<{ dashboardId: string }>();\n const navigate = useBigConsoleNavigate();\n const [isSaving, setIsSaving] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);\n // BOFF-5532: AI custom-widget authoring/review surface.\n const [isCustomWidgetBuilderOpen, setIsCustomWidgetBuilderOpen] = useState(false);\n const [activePageId, setActivePageId] = useState<string | null>(null);\n\n // Dashboard operations from GraphQL\n const {\n fetchDashboard,\n updateDashboard,\n loading: dashboardLoading,\n error: dashboardError,\n } = useDashboardOperations();\n\n // Widget operations from GraphQL\n const {\n createWidget: createWidgetMutation,\n updateWidget: updateWidgetMutation,\n deleteWidget: deleteWidgetMutation,\n duplicateWidget: duplicateWidgetMutation,\n batchUpdatePositions,\n } = useWidgetOperations(undefined, dashboardId);\n\n // Page operations from GraphQL\n const {\n createPage: createPageMutation,\n updatePage: updatePageMutation,\n deletePage: deletePageMutation,\n reorderPages: reorderPagesMutation,\n } = usePageOperations();\n\n // Store actions\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setViewMode = useDashboardStore((state) => state.setViewMode);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const widgets = useWidgetStore((state) => state.widgets);\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const clipboardAction = useWidgetStore((state) => state.clipboardAction);\n const clearClipboard = useWidgetStore((state) => state.clearClipboard);\n\n // Set edit mode on mount\n useEffect(() => {\n setViewMode('edit');\n return () => setViewMode('view');\n }, [setViewMode]);\n\n // Load dashboard data from API\n useEffect(() => {\n if (!dashboardId) {\n navigate('/dashboards');\n return;\n }\n\n // Fetch dashboard from API\n fetchDashboard(dashboardId).then((dashboard) => {\n if (dashboard) {\n const builderDashboard: Dashboard = {\n ...dashboard,\n title: dashboard.name,\n description: dashboard.description ?? '',\n pages: dashboard.pages.map((page, index) => ({\n ...page,\n id: page.id,\n title: page.name ?? '',\n index,\n layoutConfig: page.layoutConfig ?? {},\n })),\n };\n setCurrentDashboard(builderDashboard);\n // Set active page to first page\n if (dashboard.pages.length > 0 && !activePageId) {\n setActivePageId(dashboard.pages[0].id);\n }\n\n // Register dashboard name for breadcrumb display\n if (dashboard.name) {\n registerBreadcrumbName(dashboardId, dashboard.name);\n }\n } else {\n // Dashboard not found, redirect\n navigate('/dashboards');\n }\n });\n\n // Cleanup: unregister breadcrumb name on unmount\n return () => {\n if (dashboardId) {\n unregisterBreadcrumbName(dashboardId);\n }\n };\n }, [dashboardId, navigate, setCurrentDashboard, fetchDashboard, activePageId]);\n\n // Handle paste widget from clipboard\n const handlePaste = useCallback(async () => {\n const pageId = activePageId ?? currentDashboard?.pages?.[0]?.id;\n if (!clipboardWidgetId || !dashboardId || !pageId) {\n return;\n }\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) {\n clearClipboard();\n return;\n }\n\n // Use smart placement algorithm - horizontal first, then next row\n const widgetsArray = Array.from(widgets.values()).filter((widget) => widget.pageId === pageId);\n const widgetSize = {\n width: sourceWidget.positionWidth ?? 4,\n height: sourceWidget.positionHeight ?? 4,\n };\n\n const { x, y } = findNextAvailablePosition(widgetsArray, widgetSize, { cols: 12 });\n\n const newPosition = {\n x,\n y,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n\n const newTitle = clipboardAction === 'copy' ? `${sourceWidget.title} (Copy)` : sourceWidget.title;\n\n // Try duplicateWidget API first. A null result intentionally falls through\n // to the create mutation for older backends without duplicate support.\n let newWidget = await duplicateWidgetMutation(clipboardWidgetId, newPosition, newTitle).catch(() => null);\n\n // Fallback: Create new widget with source widget's data\n if (!newWidget) {\n newWidget = await createWidgetMutation({\n pageId,\n dashboardId,\n type: sourceWidget.type,\n title: newTitle,\n description: sourceWidget.description,\n // v2.0: Use dataSinkId and parserId instead of dataSourceId/dataPath\n dataSinkId: sourceWidget.dataSinkId ?? undefined,\n datasetId: sourceWidget.datasetId ?? undefined,\n parserId: sourceWidget.parserId ?? undefined,\n parserRules: sourceWidget.parserRules ?? undefined,\n config: sourceWidget.config || {},\n position: newPosition,\n refreshInterval: sourceWidget.refreshInterval ?? undefined,\n metadata: sourceWidget.metadata,\n });\n }\n\n if (newWidget) {\n selectWidget(newWidget.id);\n\n // If cut action, delete the original widget from backend\n if (clipboardAction === 'cut') {\n await deleteWidgetMutation(clipboardWidgetId);\n }\n }\n\n clearClipboard();\n }, [\n clipboardWidgetId,\n clipboardAction,\n dashboardId,\n activePageId,\n currentDashboard,\n widgets,\n duplicateWidgetMutation,\n createWidgetMutation,\n deleteWidgetMutation,\n selectWidget,\n clearClipboard,\n ]);\n\n // Keyboard shortcuts for canvas editing. Each shortcut is backed by the exact\n // same operation the on-screen controls use, so no new state path is\n // introduced — Delete/⌫ removes the selected widget, ⌘/Ctrl+C/X copy/cut it,\n // ⌘/Ctrl+V pastes, Esc deselects. State is read fresh from the stores at\n // event time (not via closure) so a keypress right after selecting is never\n // stale. Typing in form fields and dialog/sheet surfaces is never hijacked.\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (useDashboardStore.getState().viewMode !== 'edit') return;\n\n // Never hijack text entry (including fields inside the docked panel).\n const target = e.target as HTMLElement | null;\n if (target?.closest('input, textarea, select, [contenteditable=\"true\"]')) {\n return;\n }\n // Don't act while a modal dialog is open (e.g. the Add Widget wizard,\n // which renders an overlay). The properties panel is a non-modal Sheet\n // with no overlay, so canvas shortcuts stay live while it is focused.\n if (document.querySelector('[data-slot=\"dialog-overlay\"]')) {\n return;\n }\n\n const widgetStore = useWidgetStore.getState();\n const selectedId = widgetStore.selectedWidgetId;\n\n if (e.key === 'Escape') {\n widgetStore.selectWidget(null);\n return;\n }\n\n if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId) {\n e.preventDefault();\n void deleteWidgetMutation(selectedId);\n widgetStore.selectWidget(null);\n return;\n }\n\n if (!(e.metaKey || e.ctrlKey)) return;\n\n if (e.key === 'c' && selectedId) {\n widgetStore.copyWidget(selectedId);\n } else if (e.key === 'x' && selectedId) {\n widgetStore.cutWidget(selectedId);\n } else if (e.key === 'v') {\n e.preventDefault();\n void handlePaste();\n }\n };\n\n // Capture phase: the app shell and Radix also register document keydown\n // listeners; intercepting here keeps the builder's editing keys reliable.\n // Only the keys we handle are preventDefault-ed, so global shortcuts\n // (e.g. \"g a\" navigation) are left untouched.\n document.addEventListener('keydown', handleKeyDown, true);\n return () => document.removeEventListener('keydown', handleKeyDown, true);\n }, [deleteWidgetMutation, handlePaste]);\n\n // Navigate back to the dashboards list\n const handleBack = useCallback(() => {\n navigate('/dashboards');\n }, [navigate]);\n\n // Handle save - save dirty widgets and batch update positions\n const handleSave = useCallback(async () => {\n if (!dashboardId) return;\n\n setIsSaving(true);\n setSaveError(null);\n try {\n // Get the LATEST state directly from store (avoid stale closure)\n const storeState = useWidgetStore.getState();\n const currentWidgets = Array.from(storeState.widgets.values());\n const currentDirtyIds = storeState.dirtyWidgetIds;\n\n // Debug: Log dirty widget IDs\n\n // 1. Save dirty widgets (property changes like title, config, etc.)\n const dirtyWidgets = currentWidgets.filter((w) => currentDirtyIds.has(w.id));\n\n if (dirtyWidgets.length > 0) {\n // Save each dirty widget\n const updatePromises = dirtyWidgets.map((widget) => {\n const updateInput = {\n id: widget.id,\n title: widget.title,\n config: widget.config,\n refreshInterval: widget.refreshInterval ?? undefined,\n // v2.0: Use dataSinkId and parserId instead of dataSourceId/dataPath\n dataSinkId: widget.dataSinkId ?? undefined,\n datasetId: widget.datasetId ?? undefined,\n parserId: widget.parserId ?? undefined,\n parserRules: widget.parserRules ?? undefined,\n };\n\n return updateWidgetMutation(updateInput);\n });\n\n await Promise.all(updatePromises);\n\n // Clear dirty flags after successful save\n storeState.clearAllDirty();\n }\n\n // 2. Batch update widget positions\n if (currentWidgets.length > 0) {\n const positionUpdates = currentWidgets.map((widget) => ({\n id: widget.id,\n position: widget.position || {\n x: widget.positionX ?? 0,\n y: widget.positionY ?? 0,\n width: widget.positionWidth ?? 4,\n height: widget.positionHeight ?? 4,\n },\n }));\n\n await batchUpdatePositions(positionUpdates);\n }\n\n // 3. Update dashboard metadata if needed\n if (currentDashboard) {\n await updateDashboard({\n id: dashboardId,\n name: currentDashboard.title,\n description: currentDashboard.description,\n });\n }\n } catch (error) {\n setSaveError(error instanceof Error ? error.message : 'Failed to save dashboard');\n } finally {\n setIsSaving(false);\n }\n }, [dashboardId, currentDashboard, updateWidgetMutation, batchUpdatePositions, updateDashboard]);\n\n // Handle export\n const handleExport = useCallback(() => {\n setIsExportDialogOpen(true);\n }, []);\n\n const handleExportClose = useCallback(() => {\n setIsExportDialogOpen(false);\n }, []);\n\n // ============================================================================\n // Page Management Callbacks\n // ============================================================================\n\n // Handle page selection\n const handlePageChange = useCallback((pageId: string) => {\n setActivePageId(pageId);\n }, []);\n\n // Handle adding a new page\n const handlePageAdd = useCallback(async () => {\n if (!currentDashboard || !dashboardId) return;\n\n const newPageCount = currentDashboard.pages.length + 1;\n const newPageName = `Page ${newPageCount}`;\n\n // Call GraphQL mutation to create page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: newPageName,\n });\n\n if (newPage) {\n // Update local state with the created page\n setCurrentDashboard({\n ...currentDashboard,\n pages: [\n ...currentDashboard.pages,\n {\n id: newPage.id,\n title: newPage.name,\n index: newPage.order,\n layoutConfig: newPage.layoutConfig || {},\n },\n ],\n });\n\n // Switch to the new page\n setActivePageId(newPage.id);\n }\n }, [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]);\n\n // Handle renaming a page\n const handlePageRename = useCallback(\n async (pageId: string, newName: string) => {\n if (!currentDashboard) return;\n\n // Call GraphQL mutation to update page\n const updatedPage = await updatePageMutation({\n id: pageId,\n name: newName,\n });\n\n if (updatedPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: currentDashboard.pages.map((page) => (page.id === pageId ? { ...page, title: newName } : page)),\n });\n }\n },\n [currentDashboard, setCurrentDashboard, updatePageMutation]\n );\n\n // Handle duplicating a page\n const handlePageDuplicate = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || !dashboardId) return;\n\n const sourcePage = currentDashboard.pages.find((p) => p.id === pageId);\n if (!sourcePage) return;\n\n // Call GraphQL mutation to create a copy of the page\n // Note: 'order' is not available for create - backend auto-assigns order\n // Note: 'layout' should be PageLayout enum ('GRID', 'FLEX', 'FREEFORM'), not layoutConfig object\n const newPage = await createPageMutation({\n dashboardId,\n name: `${sourcePage.title} (Copy)`,\n });\n\n if (newPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: [\n ...currentDashboard.pages,\n {\n id: newPage.id,\n title: newPage.name,\n index: newPage.order,\n layoutConfig: newPage.layoutConfig || {},\n },\n ],\n });\n\n setActivePageId(newPage.id);\n }\n },\n [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]\n );\n\n // Handle deleting a page\n const handlePageDelete = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || currentDashboard.pages.length <= 1) return;\n\n // Call GraphQL mutation to delete page\n const success = await deletePageMutation(pageId);\n\n if (success) {\n const updatedPages = currentDashboard.pages.filter((p) => p.id !== pageId).map((p, index) => ({ ...p, index }));\n\n setCurrentDashboard({\n ...currentDashboard,\n pages: updatedPages,\n });\n\n // Switch to first page if current page was deleted\n if (activePageId === pageId) {\n setActivePageId(updatedPages[0]?.id || null);\n }\n }\n },\n [currentDashboard, setCurrentDashboard, activePageId, deletePageMutation]\n );\n\n // Handle reordering pages\n const handlePagesReorder = useCallback(\n async (reorderedPages: DashboardPage[]) => {\n if (!currentDashboard || !dashboardId) return;\n\n // Update local state optimistically\n setCurrentDashboard({\n ...currentDashboard,\n pages: reorderedPages.map((page, index) => ({\n ...page,\n index,\n })),\n });\n\n // Call GraphQL mutation to persist the reorder\n const pageIds = reorderedPages.map((p) => p.id);\n await reorderPagesMutation(dashboardId, pageIds);\n },\n [currentDashboard, dashboardId, setCurrentDashboard, reorderPagesMutation]\n );\n\n // Show error state\n if (dashboardError) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full p-6\">\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-4 max-w-md\">\n <h3 className=\"text-sm font-medium text-status-error-text\">Failed to load dashboard</h3>\n <p className=\"text-sm text-text-secondary mt-1\">{dashboardError.message}</p>\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"mt-3 px-3 py-1.5 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Back to Dashboards\n </button>\n </div>\n </div>\n );\n }\n\n // Show loading state\n if (dashboardLoading || !currentDashboard) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"animate-spin w-8 h-8 border-4 border-action-primary-bg border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n return (\n <DashboardEditorShell\n topbar={\n <BuilderTopbar\n title={currentDashboard.title || 'Untitled dashboard'}\n onBack={handleBack}\n actions={\n <div className=\"flex items-center gap-2\">\n {saveError ? (\n <span className=\"text-xs font-medium text-status-error-text\" role=\"alert\">\n {saveError}\n </span>\n ) : null}\n <button\n type=\"button\"\n data-testid=\"open-custom-widget-builder\"\n onClick={() => setIsCustomWidgetBuilderOpen(true)}\n className=\"rounded-md border border-border-default px-2 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg\"\n >\n AI widget\n </button>\n </div>\n }\n />\n }\n >\n <BuilderCanvas\n canvas={\n <DashboardCanvas\n dashboardId={currentDashboard.id}\n pageId={activePageId || currentDashboard.pages?.[0]?.id}\n title={currentDashboard.title}\n onPaste={handlePaste}\n onSave={handleSave}\n isSaving={isSaving}\n onExport={handleExport}\n // Page tabs props\n pages={currentDashboard.pages.map((page) => ({\n id: page.id,\n name: page.title || `Page ${(page.index ?? 0) + 1}`,\n order: page.index ?? 0,\n }))}\n activePageId={activePageId || currentDashboard.pages?.[0]?.id}\n onPageChange={handlePageChange}\n onPageAdd={handlePageAdd}\n onPageRename={handlePageRename}\n onPageDuplicate={handlePageDuplicate}\n onPageDelete={handlePageDelete}\n onPagesReorder={handlePagesReorder}\n showPageTabs={true}\n />\n }\n />\n\n <ExportDashboardDialog\n isOpen={isExportDialogOpen}\n dashboardId={currentDashboard.id}\n dashboardName={currentDashboard.title}\n onClose={handleExportClose}\n />\n\n <CustomWidgetBuilderSheet\n isOpen={isCustomWidgetBuilderOpen}\n onClose={() => setIsCustomWidgetBuilderOpen(false)}\n dashboardId={currentDashboard.id}\n pageId={activePageId || currentDashboard.pages?.[0]?.id || ''}\n />\n </DashboardEditorShell>\n );\n});\n\nexport default DashboardBuilderPage;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardBuilderPage.js","names":[],"sources":["../../../src/bigconsole/pages/DashboardBuilderPage.tsx"],"sourcesContent":["/**\n * DashboardBuilderPage\n *\n * Full dashboard builder experience with side panels.\n */\n\nimport { type FC, memo, useState, useEffect, useCallback } from 'react';\nimport { useParams } from 'react-router';\nimport { useBigConsoleNavigate } from '../context';\nimport { DashboardCanvas, ExportDashboardDialog } from '../components/dashboard';\nimport { BuilderCanvas, BuilderTopbar, DashboardEditorShell } from '../components/builder';\nimport { useDashboardStore, useWidgetStore } from '../store';\nimport { useDashboardOperations, useWidgetOperations, usePageOperations } from '../hooks';\nimport { CustomWidgetBuilderSheet } from '../customWidget';\nimport type { Dashboard, DashboardPage } from '../types';\nimport { findNextAvailablePosition } from '../utils/widgetPositioning';\n\n// Breadcrumb name registration helpers (get from shell at call time to avoid timing issues)\nfunction getBreadcrumbRegistry() {\n try {\n return (\n window as unknown as {\n __breadcrumbRegistry?: { register: (id: string, name: string) => void; unregister: (id: string) => void };\n }\n ).__breadcrumbRegistry;\n } catch {\n return undefined;\n }\n}\n\nfunction registerBreadcrumbName(id: string, name: string) {\n const registry = getBreadcrumbRegistry();\n if (registry?.register) {\n registry.register(id, name);\n }\n}\n\nfunction unregisterBreadcrumbName(id: string) {\n const registry = getBreadcrumbRegistry();\n if (registry?.unregister) {\n registry.unregister(id);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardBuilderPage: FC = memo(function DashboardBuilderPage() {\n const { dashboardId } = useParams<{ dashboardId: string }>();\n const navigate = useBigConsoleNavigate();\n const [isSaving, setIsSaving] = useState(false);\n const [saveError, setSaveError] = useState<string | null>(null);\n const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);\n // BOFF-5532: AI custom-widget authoring/review surface.\n const [isCustomWidgetBuilderOpen, setIsCustomWidgetBuilderOpen] = useState(false);\n const [activePageId, setActivePageId] = useState<string | null>(null);\n\n // Dashboard operations from GraphQL\n const {\n fetchDashboard,\n updateDashboard,\n loading: dashboardLoading,\n error: dashboardError,\n } = useDashboardOperations();\n\n // Widget operations from GraphQL\n const {\n createWidget: createWidgetMutation,\n updateWidget: updateWidgetMutation,\n deleteWidget: deleteWidgetMutation,\n duplicateWidget: duplicateWidgetMutation,\n batchUpdatePositions,\n } = useWidgetOperations(undefined, dashboardId);\n\n // Page operations from GraphQL\n const {\n createPage: createPageMutation,\n updatePage: updatePageMutation,\n deletePage: deletePageMutation,\n reorderPages: reorderPagesMutation,\n } = usePageOperations();\n\n // Store actions\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setViewMode = useDashboardStore((state) => state.setViewMode);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const widgets = useWidgetStore((state) => state.widgets);\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const clipboardAction = useWidgetStore((state) => state.clipboardAction);\n const clearClipboard = useWidgetStore((state) => state.clearClipboard);\n\n // Set edit mode on mount\n useEffect(() => {\n setViewMode('edit');\n return () => setViewMode('view');\n }, [setViewMode]);\n\n // Load dashboard data from API\n useEffect(() => {\n if (!dashboardId) {\n navigate('/dashboards');\n return;\n }\n\n // Fetch dashboard from API\n fetchDashboard(dashboardId).then((dashboard) => {\n if (dashboard) {\n const builderDashboard: Dashboard = {\n ...dashboard,\n title: dashboard.name,\n description: dashboard.description ?? '',\n pages: dashboard.pages.map((page, index) => ({\n ...page,\n id: page.id,\n title: page.name ?? '',\n index,\n layoutConfig: page.layoutConfig ?? {},\n })),\n };\n setCurrentDashboard(builderDashboard);\n // Set active page to first page\n if (dashboard.pages.length > 0 && !activePageId) {\n setActivePageId(dashboard.pages[0].id);\n }\n\n // Register dashboard name for breadcrumb display\n if (dashboard.name) {\n registerBreadcrumbName(dashboardId, dashboard.name);\n }\n } else {\n // Dashboard not found, redirect\n navigate('/dashboards');\n }\n });\n\n // Cleanup: unregister breadcrumb name on unmount\n return () => {\n if (dashboardId) {\n unregisterBreadcrumbName(dashboardId);\n }\n };\n }, [dashboardId, navigate, setCurrentDashboard, fetchDashboard, activePageId]);\n\n // Handle paste widget from clipboard\n const handlePaste = useCallback(async () => {\n const pageId = activePageId ?? currentDashboard?.pages?.[0]?.id;\n if (!clipboardWidgetId || !dashboardId || !pageId) {\n return;\n }\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) {\n clearClipboard();\n return;\n }\n\n // Use smart placement algorithm - horizontal first, then next row\n const widgetsArray = Array.from(widgets.values()).filter((widget) => widget.pageId === pageId);\n const widgetSize = {\n width: sourceWidget.positionWidth ?? 4,\n height: sourceWidget.positionHeight ?? 4,\n };\n\n const { x, y } = findNextAvailablePosition(widgetsArray, widgetSize, { cols: 12 });\n\n const newPosition = {\n x,\n y,\n width: widgetSize.width,\n height: widgetSize.height,\n };\n\n const newTitle = clipboardAction === 'copy' ? `${sourceWidget.title} (Copy)` : sourceWidget.title;\n\n // Try duplicateWidget API first. A null result intentionally falls through\n // to the create mutation for older backends without duplicate support.\n let newWidget = await duplicateWidgetMutation(clipboardWidgetId, newPosition, newTitle).catch(() => null);\n\n // Fallback: Create new widget with source widget's data\n if (!newWidget) {\n newWidget = await createWidgetMutation({\n pageId,\n dashboardId,\n type: sourceWidget.type,\n title: newTitle,\n description: sourceWidget.description,\n // v2.0: Use dataSinkId and parserId instead of dataSourceId/dataPath\n dataSinkId: sourceWidget.dataSinkId ?? undefined,\n datasetId: sourceWidget.datasetId ?? undefined,\n parserId: sourceWidget.parserId ?? undefined,\n parserRules: sourceWidget.parserRules ?? undefined,\n config: sourceWidget.config || {},\n position: newPosition,\n refreshInterval: sourceWidget.refreshInterval ?? undefined,\n metadata: sourceWidget.metadata,\n });\n }\n\n if (newWidget) {\n selectWidget(newWidget.id);\n\n // If cut action, delete the original widget from backend\n if (clipboardAction === 'cut') {\n await deleteWidgetMutation(clipboardWidgetId);\n }\n }\n\n clearClipboard();\n }, [\n clipboardWidgetId,\n clipboardAction,\n dashboardId,\n activePageId,\n currentDashboard,\n widgets,\n duplicateWidgetMutation,\n createWidgetMutation,\n deleteWidgetMutation,\n selectWidget,\n clearClipboard,\n ]);\n\n // Keyboard shortcuts for canvas editing. Each shortcut is backed by the exact\n // same operation the on-screen controls use, so no new state path is\n // introduced — Delete/⌫ removes the selected widget, ⌘/Ctrl+C/X copy/cut it,\n // ⌘/Ctrl+V pastes, Esc deselects. State is read fresh from the stores at\n // event time (not via closure) so a keypress right after selecting is never\n // stale. Typing in form fields and dialog/sheet surfaces is never hijacked.\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (useDashboardStore.getState().viewMode !== 'edit') return;\n\n // Never hijack text entry (including fields inside the docked panel).\n const target = e.target as HTMLElement | null;\n if (target?.closest('input, textarea, select, [contenteditable=\"true\"]')) {\n return;\n }\n // Don't act while a modal dialog is open (e.g. the Add Widget wizard,\n // which renders an overlay). The properties panel is a non-modal Sheet\n // with no overlay, so canvas shortcuts stay live while it is focused.\n if (document.querySelector('[data-slot=\"dialog-overlay\"]')) {\n return;\n }\n\n const widgetStore = useWidgetStore.getState();\n const selectedId = widgetStore.selectedWidgetId;\n\n if (e.key === 'Escape') {\n widgetStore.selectWidget(null);\n return;\n }\n\n if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId) {\n e.preventDefault();\n void deleteWidgetMutation(selectedId);\n widgetStore.selectWidget(null);\n return;\n }\n\n if (!(e.metaKey || e.ctrlKey)) return;\n\n if (e.key === 'c' && selectedId) {\n widgetStore.copyWidget(selectedId);\n } else if (e.key === 'x' && selectedId) {\n widgetStore.cutWidget(selectedId);\n } else if (e.key === 'v') {\n e.preventDefault();\n void handlePaste();\n }\n };\n\n // Capture phase: the app shell and Radix also register document keydown\n // listeners; intercepting here keeps the builder's editing keys reliable.\n // Only the keys we handle are preventDefault-ed, so global shortcuts\n // (e.g. \"g a\" navigation) are left untouched.\n document.addEventListener('keydown', handleKeyDown, true);\n return () => document.removeEventListener('keydown', handleKeyDown, true);\n }, [deleteWidgetMutation, handlePaste]);\n\n // Navigate back to the dashboards list\n const handleBack = useCallback(() => {\n navigate('/dashboards');\n }, [navigate]);\n\n // Handle save - save dirty widgets and batch update positions\n const handleSave = useCallback(async () => {\n if (!dashboardId) return;\n\n setIsSaving(true);\n setSaveError(null);\n try {\n // Get the LATEST state directly from store (avoid stale closure)\n const storeState = useWidgetStore.getState();\n const currentWidgets = Array.from(storeState.widgets.values());\n const currentDirtyIds = storeState.dirtyWidgetIds;\n\n // Debug: Log dirty widget IDs\n\n // 1. Save dirty widgets (property changes like title, config, etc.)\n const dirtyWidgets = currentWidgets.filter((w) => currentDirtyIds.has(w.id));\n\n if (dirtyWidgets.length > 0) {\n // Save each dirty widget\n const updatePromises = dirtyWidgets.map((widget) => {\n const updateInput = {\n id: widget.id,\n title: widget.title,\n config: widget.config,\n refreshInterval: widget.refreshInterval ?? undefined,\n // v2.0: Use dataSinkId and parserId instead of dataSourceId/dataPath\n dataSinkId: widget.dataSinkId ?? undefined,\n datasetId: widget.datasetId ?? undefined,\n parserId: widget.parserId ?? undefined,\n parserRules: widget.parserRules ?? undefined,\n };\n\n return updateWidgetMutation(updateInput);\n });\n\n await Promise.all(updatePromises);\n\n // Clear dirty flags after successful save\n storeState.clearAllDirty();\n }\n\n // 2. Batch update widget positions\n if (currentWidgets.length > 0) {\n const positionUpdates = currentWidgets.map((widget) => ({\n id: widget.id,\n position: widget.position || {\n x: widget.positionX ?? 0,\n y: widget.positionY ?? 0,\n width: widget.positionWidth ?? 4,\n height: widget.positionHeight ?? 4,\n },\n }));\n\n await batchUpdatePositions(positionUpdates);\n }\n\n // 3. Update dashboard metadata if needed\n if (currentDashboard) {\n await updateDashboard({\n id: dashboardId,\n name: currentDashboard.title,\n description: currentDashboard.description,\n });\n }\n } catch (error) {\n setSaveError(error instanceof Error ? error.message : 'Failed to save dashboard');\n } finally {\n setIsSaving(false);\n }\n }, [dashboardId, currentDashboard, updateWidgetMutation, batchUpdatePositions, updateDashboard]);\n\n // Handle export\n const handleExport = useCallback(() => {\n setIsExportDialogOpen(true);\n }, []);\n\n const handleExportClose = useCallback(() => {\n setIsExportDialogOpen(false);\n }, []);\n\n // ============================================================================\n // Page Management Callbacks\n // ============================================================================\n\n // Handle page selection\n const handlePageChange = useCallback((pageId: string) => {\n setActivePageId(pageId);\n }, []);\n\n // Handle adding a new page\n const handlePageAdd = useCallback(async () => {\n if (!currentDashboard || !dashboardId) return;\n\n const newPageCount = currentDashboard.pages.length + 1;\n const newPageName = `Page ${newPageCount}`;\n\n // Call GraphQL mutation to create page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: newPageName,\n });\n\n if (newPage) {\n // Update local state with the created page\n setCurrentDashboard({\n ...currentDashboard,\n pages: [\n ...currentDashboard.pages,\n {\n id: newPage.id,\n title: newPage.name,\n index: newPage.order,\n layoutConfig: newPage.layoutConfig || {},\n },\n ],\n });\n\n // Switch to the new page\n setActivePageId(newPage.id);\n }\n }, [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]);\n\n // Handle renaming a page\n const handlePageRename = useCallback(\n async (pageId: string, newName: string) => {\n if (!currentDashboard) return;\n\n // Call GraphQL mutation to update page\n const updatedPage = await updatePageMutation({\n id: pageId,\n name: newName,\n });\n\n if (updatedPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: currentDashboard.pages.map((page) => (page.id === pageId ? { ...page, title: newName } : page)),\n });\n }\n },\n [currentDashboard, setCurrentDashboard, updatePageMutation]\n );\n\n // Handle duplicating a page\n const handlePageDuplicate = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || !dashboardId) return;\n\n const sourcePage = currentDashboard.pages.find((p) => p.id === pageId);\n if (!sourcePage) return;\n\n // Call GraphQL mutation to create a copy of the page\n // Note: 'order' is not available for create - backend auto-assigns order\n // Note: 'layout' should be PageLayout enum ('GRID', 'FLEX', 'FREEFORM'), not layoutConfig object\n const newPage = await createPageMutation({\n dashboardId,\n name: `${sourcePage.title} (Copy)`,\n });\n\n if (newPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: [\n ...currentDashboard.pages,\n {\n id: newPage.id,\n title: newPage.name,\n index: newPage.order,\n layoutConfig: newPage.layoutConfig || {},\n },\n ],\n });\n\n setActivePageId(newPage.id);\n }\n },\n [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]\n );\n\n // Handle deleting a page\n const handlePageDelete = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || currentDashboard.pages.length <= 1) return;\n\n // Call GraphQL mutation to delete page\n const success = await deletePageMutation(pageId);\n\n if (success) {\n const updatedPages = currentDashboard.pages.filter((p) => p.id !== pageId).map((p, index) => ({ ...p, index }));\n\n setCurrentDashboard({\n ...currentDashboard,\n pages: updatedPages,\n });\n\n // Switch to first page if current page was deleted\n if (activePageId === pageId) {\n setActivePageId(updatedPages[0]?.id || null);\n }\n }\n },\n [currentDashboard, setCurrentDashboard, activePageId, deletePageMutation]\n );\n\n // Handle reordering pages\n const handlePagesReorder = useCallback(\n async (reorderedPages: DashboardPage[]) => {\n if (!currentDashboard || !dashboardId) return;\n\n // Update local state optimistically\n setCurrentDashboard({\n ...currentDashboard,\n pages: reorderedPages.map((page, index) => ({\n ...page,\n index,\n })),\n });\n\n // Call GraphQL mutation to persist the reorder\n const pageIds = reorderedPages.map((p) => p.id);\n await reorderPagesMutation(dashboardId, pageIds);\n },\n [currentDashboard, dashboardId, setCurrentDashboard, reorderPagesMutation]\n );\n\n // Show error state\n if (dashboardError) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full p-6\">\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-4 max-w-md\">\n <h3 className=\"text-sm font-medium text-status-error-text\">Failed to load dashboard</h3>\n <p className=\"text-sm text-text-secondary mt-1\">{dashboardError.message}</p>\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"mt-3 px-3 py-1.5 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Back to Dashboards\n </button>\n </div>\n </div>\n );\n }\n\n // Show loading state\n if (dashboardLoading || !currentDashboard) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"animate-spin w-8 h-8 border-4 border-action-primary-bg border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n return (\n <DashboardEditorShell\n topbar={\n <BuilderTopbar\n title={currentDashboard.title || 'Untitled dashboard'}\n onBack={handleBack}\n actions={\n <div className=\"flex items-center gap-2\">\n {saveError ? (\n <span className=\"text-xs font-medium text-status-error-text\" role=\"alert\">\n {saveError}\n </span>\n ) : null}\n <button\n type=\"button\"\n data-testid=\"open-custom-widget-builder\"\n onClick={() => setIsCustomWidgetBuilderOpen(true)}\n className=\"rounded-md border border-border-default px-2 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg\"\n >\n AI widget\n </button>\n </div>\n }\n />\n }\n >\n <BuilderCanvas\n canvas={\n <DashboardCanvas\n dashboardId={currentDashboard.id}\n pageId={activePageId || currentDashboard.pages?.[0]?.id}\n title={currentDashboard.title}\n onPaste={handlePaste}\n onSave={handleSave}\n isSaving={isSaving}\n onExport={handleExport}\n // Page tabs props\n pages={currentDashboard.pages.map((page) => ({\n id: page.id,\n name: page.title || `Page ${(page.index ?? 0) + 1}`,\n order: page.index ?? 0,\n }))}\n activePageId={activePageId || currentDashboard.pages?.[0]?.id}\n onPageChange={handlePageChange}\n onPageAdd={handlePageAdd}\n onPageRename={handlePageRename}\n onPageDuplicate={handlePageDuplicate}\n onPageDelete={handlePageDelete}\n onPagesReorder={handlePagesReorder}\n showPageTabs={true}\n />\n }\n />\n\n <ExportDashboardDialog\n isOpen={isExportDialogOpen}\n dashboardId={currentDashboard.id}\n dashboardName={currentDashboard.title}\n onClose={handleExportClose}\n />\n\n <CustomWidgetBuilderSheet\n isOpen={isCustomWidgetBuilderOpen}\n onClose={() => setIsCustomWidgetBuilderOpen(false)}\n dashboardId={currentDashboard.id}\n pageId={activePageId || currentDashboard.pages?.[0]?.id || ''}\n />\n </DashboardEditorShell>\n );\n});\n\nexport default DashboardBuilderPage;\n"],"mappings":"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"}
@@ -15,7 +15,7 @@ import ae from "../components/dashboard/DrilldownBreadcrumb.js";
15
15
  import oe from "../components/dashboard/PublishToStoreDialog.js";
16
16
  import "../components/dashboard/index.js";
17
17
  import { memo as a, useCallback as o, useEffect as s, useRef as se, useState as c } from "react";
18
- import { useParams as l } from "react-router-dom";
18
+ import { useParams as l } from "react-router";
19
19
  import { ArrowLeft as u } from "lucide-react";
20
20
  import { Button as d, IllustratedEmptyState as f } from "@burdenoff/fe-libs/ui";
21
21
  import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";