@burdenoff/microfe-bigconsole 2026.801.3 → 2026.803.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bigconsole/components/dashboard/PublishToStoreDialog.js +35 -33
- package/dist/bigconsole/components/dashboard/PublishToStoreDialog.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetHeader.js +22 -17
- package/dist/bigconsole/components/widgets/WidgetHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetWrapper.js +22 -17
- package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
- package/dist/bigconsole/graphql/storeTemplates.js +3 -0
- package/dist/bigconsole/graphql/storeTemplates.js.map +1 -1
- package/dist/bigconsole/hooks/useStoreDashboardTemplates.js +12 -7
- package/dist/bigconsole/hooks/useStoreDashboardTemplates.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,63 +7,65 @@ function _(e) {
|
|
|
7
7
|
return e.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-").slice(0, 60);
|
|
8
8
|
}
|
|
9
9
|
var v = t(function({ isOpen: t, dashboardId: v, dashboardName: y, dashboardDescription: b, onClose: x, onPublished: S }) {
|
|
10
|
-
let { publishToStore: C } = e({ autoLoad: !1 }), [w, T] = i(y), [E, D] = i(_(y)), [O, k] = i(b ?? ""), [A, j] = i(!1), [M, N] = i(
|
|
10
|
+
let { publishToStore: C } = e({ autoLoad: !1 }), [w, T] = i(y), [E, D] = i(_(y)), [O, k] = i(b ?? ""), [A, j] = i(!1), [M, N] = i(!1), [P, F] = i(null), [I, L] = i(null);
|
|
11
11
|
r(() => {
|
|
12
|
-
t && (T(y), D(_(y)), k(b ?? ""),
|
|
12
|
+
t && (T(y), D(_(y)), k(b ?? ""), j(!1), F(null), L(null));
|
|
13
13
|
}, [
|
|
14
14
|
t,
|
|
15
15
|
y,
|
|
16
16
|
b
|
|
17
17
|
]);
|
|
18
|
-
let
|
|
18
|
+
let R = n(async () => {
|
|
19
19
|
if (!w.trim()) {
|
|
20
|
-
|
|
20
|
+
F("A template name is required");
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
if (!E.trim()) {
|
|
24
|
-
|
|
24
|
+
F("A slug is required");
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
if (!O.trim()) {
|
|
28
|
-
|
|
28
|
+
F("A description is required — it is what people see in the gallery");
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
F(null), N(!0);
|
|
32
32
|
try {
|
|
33
33
|
let e = await C({
|
|
34
34
|
dashboardId: v,
|
|
35
35
|
name: w.trim(),
|
|
36
36
|
slug: E.trim(),
|
|
37
|
-
description: O.trim()
|
|
37
|
+
description: O.trim(),
|
|
38
|
+
includeSampleData: A
|
|
38
39
|
});
|
|
39
40
|
if (!e.success) {
|
|
40
|
-
|
|
41
|
+
F(e.errorMessage ?? "Failed to publish to the store");
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
L(e), S?.(e);
|
|
44
45
|
} finally {
|
|
45
|
-
|
|
46
|
+
N(!1);
|
|
46
47
|
}
|
|
47
48
|
}, [
|
|
48
49
|
w,
|
|
49
50
|
E,
|
|
50
51
|
O,
|
|
52
|
+
A,
|
|
51
53
|
v,
|
|
52
54
|
C,
|
|
53
55
|
S
|
|
54
|
-
]),
|
|
56
|
+
]), z = {
|
|
55
57
|
display: "block",
|
|
56
58
|
marginBottom: "var(--space-2xs)",
|
|
57
59
|
fontSize: "var(--font-size-sm)"
|
|
58
60
|
};
|
|
59
61
|
return /* @__PURE__ */ h(o, {
|
|
60
62
|
open: t,
|
|
61
|
-
onOpenChange: (e) => !e && !
|
|
63
|
+
onOpenChange: (e) => !e && !M && x(),
|
|
62
64
|
children: /* @__PURE__ */ g(s, {
|
|
63
65
|
style: { maxWidth: "520px" },
|
|
64
66
|
children: [
|
|
65
67
|
/* @__PURE__ */ g(u, { children: [/* @__PURE__ */ h(d, { children: "Publish as Template" }), /* @__PURE__ */ h(c, { children: "Share this dashboard as a reusable template. Your data sources are replaced with named slots, so nothing from this workspace is published." })] }),
|
|
66
|
-
|
|
68
|
+
I ? /* @__PURE__ */ g("div", {
|
|
67
69
|
style: { padding: "var(--space-md) 0" },
|
|
68
70
|
children: [
|
|
69
71
|
/* @__PURE__ */ h("p", {
|
|
@@ -76,21 +78,21 @@ var v = t(function({ isOpen: t, dashboardId: v, dashboardName: y, dashboardDescr
|
|
|
76
78
|
color: "var(--color-text-muted)"
|
|
77
79
|
},
|
|
78
80
|
children: [
|
|
79
|
-
|
|
81
|
+
I.sinkCount,
|
|
80
82
|
" data source",
|
|
81
|
-
|
|
83
|
+
I.sinkCount === 1 ? "" : "s",
|
|
82
84
|
" turned into slots. Finish in the Developer Portal: review the listing, then submit it for approval. It appears in the gallery once approved."
|
|
83
85
|
]
|
|
84
86
|
}),
|
|
85
|
-
|
|
87
|
+
I.unknownSinkIds.length > 0 && /* @__PURE__ */ g("p", {
|
|
86
88
|
style: {
|
|
87
89
|
fontSize: "var(--font-size-sm)",
|
|
88
90
|
color: "var(--color-text-warning)"
|
|
89
91
|
},
|
|
90
92
|
children: [
|
|
91
|
-
|
|
93
|
+
I.unknownSinkIds.length,
|
|
92
94
|
" data source",
|
|
93
|
-
|
|
95
|
+
I.unknownSinkIds.length === 1 ? "" : "s",
|
|
94
96
|
" could not be named — those slots use generic labels."
|
|
95
97
|
]
|
|
96
98
|
})
|
|
@@ -104,56 +106,56 @@ var v = t(function({ isOpen: t, dashboardId: v, dashboardName: y, dashboardDescr
|
|
|
104
106
|
children: [
|
|
105
107
|
/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
|
|
106
108
|
htmlFor: "tpl-name",
|
|
107
|
-
style:
|
|
109
|
+
style: z,
|
|
108
110
|
children: "Template name"
|
|
109
111
|
}), /* @__PURE__ */ h(f, {
|
|
110
112
|
id: "tpl-name",
|
|
111
113
|
value: w,
|
|
112
|
-
disabled:
|
|
114
|
+
disabled: M,
|
|
113
115
|
onChange: (e) => {
|
|
114
116
|
T(e.target.value), D(_(e.target.value));
|
|
115
117
|
}
|
|
116
118
|
})] }),
|
|
117
119
|
/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
|
|
118
120
|
htmlFor: "tpl-slug",
|
|
119
|
-
style:
|
|
121
|
+
style: z,
|
|
120
122
|
children: "Slug"
|
|
121
123
|
}), /* @__PURE__ */ h(f, {
|
|
122
124
|
id: "tpl-slug",
|
|
123
125
|
value: E,
|
|
124
|
-
disabled:
|
|
126
|
+
disabled: M,
|
|
125
127
|
onChange: (e) => D(_(e.target.value))
|
|
126
128
|
})] }),
|
|
127
129
|
/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
|
|
128
130
|
htmlFor: "tpl-desc",
|
|
129
|
-
style:
|
|
131
|
+
style: z,
|
|
130
132
|
children: "Description"
|
|
131
133
|
}), /* @__PURE__ */ h(m, {
|
|
132
134
|
id: "tpl-desc",
|
|
133
135
|
rows: 3,
|
|
134
136
|
value: O,
|
|
135
|
-
disabled:
|
|
137
|
+
disabled: M,
|
|
136
138
|
placeholder: "What is this dashboard for, and what data does it need?",
|
|
137
139
|
onChange: (e) => k(e.target.value)
|
|
138
140
|
})] }),
|
|
139
|
-
|
|
141
|
+
P && /* @__PURE__ */ h("p", {
|
|
140
142
|
style: {
|
|
141
143
|
color: "var(--color-text-danger)",
|
|
142
144
|
fontSize: "var(--font-size-sm)"
|
|
143
145
|
},
|
|
144
|
-
children:
|
|
146
|
+
children: P
|
|
145
147
|
})
|
|
146
148
|
]
|
|
147
149
|
}),
|
|
148
150
|
/* @__PURE__ */ g(l, { children: [/* @__PURE__ */ h(a, {
|
|
149
151
|
variant: "outline",
|
|
150
152
|
onClick: x,
|
|
151
|
-
disabled:
|
|
152
|
-
children:
|
|
153
|
-
}), !
|
|
154
|
-
onClick:
|
|
155
|
-
disabled:
|
|
156
|
-
children:
|
|
153
|
+
disabled: M,
|
|
154
|
+
children: I ? "Done" : "Cancel"
|
|
155
|
+
}), !I && /* @__PURE__ */ h(a, {
|
|
156
|
+
onClick: R,
|
|
157
|
+
disabled: M,
|
|
158
|
+
children: M ? /* @__PURE__ */ h(p, {}) : "Publish"
|
|
157
159
|
})] })
|
|
158
160
|
]
|
|
159
161
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PublishToStoreDialog.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/PublishToStoreDialog.tsx"],"sourcesContent":["/**\n * PublishToStoreDialog\n *\n * Turns the open dashboard into a store template.\n *\n * WHY THIS EXISTS:\n * A workflow template's {nodes, edges} can plausibly be hand-written into the\n * developer portal. A dashboard export — every widget with its layout\n * coordinates, bindings and filters — cannot. Without this, authoring a\n * dashboard template means hand-crafting a large JSON document, which nobody\n * will do, and the gallery stays empty.\n *\n * The backend tokenises the board (replacing this workspace's data-sink\n * references with named slots), then a DRAFT Dashboard Template application is\n * created in the developer portal from the result. It deliberately stops there\n * rather than writing a store product directly: devportal already owns\n * versioning, review and publisher identity for dashboard templates, and\n * forking that pipeline would orphan the listing.\n *\n * @module bigconsole/components/dashboard/PublishToStoreDialog\n */\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Spinner,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { type FC, memo, useCallback, useEffect, useState } from 'react';\n\nimport { useStoreDashboardTemplates, type PublishToStoreResult } from '../../hooks/useStoreDashboardTemplates';\n\nexport interface PublishToStoreDialogProps {\n isOpen: boolean;\n dashboardId: string;\n dashboardName: string;\n dashboardDescription?: string | null;\n onClose: () => void;\n onPublished?: (result: PublishToStoreResult) => void;\n}\n\n/** Store slugs are lowercase, hyphenated and unique. */\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter(Boolean)\n .join('-')\n .slice(0, 60);\n}\n\nexport const PublishToStoreDialog: FC<PublishToStoreDialogProps> = memo(function PublishToStoreDialog({\n isOpen,\n dashboardId,\n dashboardName,\n dashboardDescription,\n onClose,\n onPublished,\n}) {\n const { publishToStore } = useStoreDashboardTemplates({ autoLoad: false });\n\n const [name, setName] = useState(dashboardName);\n const [slug, setSlug] = useState(slugify(dashboardName));\n const [description, setDescription] = useState(dashboardDescription ?? '');\n const [publishing, setPublishing] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [result, setResult] = useState<PublishToStoreResult | null>(null);\n\n useEffect(() => {\n if (!isOpen) return;\n setName(dashboardName);\n setSlug(slugify(dashboardName));\n setDescription(dashboardDescription ?? '');\n setError(null);\n setResult(null);\n }, [isOpen, dashboardName, dashboardDescription]);\n\n const handlePublish = useCallback(async () => {\n if (!name.trim()) {\n setError('A template name is required');\n return;\n }\n if (!slug.trim()) {\n setError('A slug is required');\n return;\n }\n if (!description.trim()) {\n setError('A description is required — it is what people see in the gallery');\n return;\n }\n\n setError(null);\n setPublishing(true);\n try {\n const published = await publishToStore({\n dashboardId,\n name: name.trim(),\n slug: slug.trim(),\n description: description.trim(),\n });\n if (!published.success) {\n setError(published.errorMessage ?? 'Failed to publish to the store');\n return;\n }\n setResult(published);\n onPublished?.(published);\n } finally {\n setPublishing(false);\n }\n }, [name, slug, description, dashboardId, publishToStore, onPublished]);\n\n const label = { display: 'block', marginBottom: 'var(--space-2xs)', fontSize: 'var(--font-size-sm)' };\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && !publishing && onClose()}>\n <DialogContent style={{ maxWidth: '520px' }}>\n <DialogHeader>\n <DialogTitle>Publish as Template</DialogTitle>\n <DialogDescription>\n Share this dashboard as a reusable template. Your data sources are replaced with named slots, so nothing\n from this workspace is published.\n </DialogDescription>\n </DialogHeader>\n\n {result ? (\n <div style={{ padding: 'var(--space-md) 0' }}>\n <p style={{ fontWeight: 'var(--font-weight-medium)' }}>Draft template created</p>\n <p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>\n {result.sinkCount} data source{result.sinkCount === 1 ? '' : 's'} turned into slots. Finish in the\n Developer Portal: review the listing, then submit it for approval. It appears in the gallery once\n approved.\n </p>\n {result.unknownSinkIds.length > 0 && (\n <p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-warning)' }}>\n {result.unknownSinkIds.length} data source\n {result.unknownSinkIds.length === 1 ? '' : 's'} could not be named — those slots use generic labels.\n </p>\n )}\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>\n <div>\n <label htmlFor=\"tpl-name\" style={label}>\n Template name\n </label>\n <Input\n id=\"tpl-name\"\n value={name}\n disabled={publishing}\n onChange={(e) => {\n setName(e.target.value);\n setSlug(slugify(e.target.value));\n }}\n />\n </div>\n\n <div>\n <label htmlFor=\"tpl-slug\" style={label}>\n Slug\n </label>\n <Input\n id=\"tpl-slug\"\n value={slug}\n disabled={publishing}\n onChange={(e) => setSlug(slugify(e.target.value))}\n />\n </div>\n\n <div>\n <label htmlFor=\"tpl-desc\" style={label}>\n Description\n </label>\n <Textarea\n id=\"tpl-desc\"\n rows={3}\n value={description}\n disabled={publishing}\n placeholder=\"What is this dashboard for, and what data does it need?\"\n onChange={(e) => setDescription(e.target.value)}\n />\n </div>\n\n {error && <p style={{ color: 'var(--color-text-danger)', fontSize: 'var(--font-size-sm)' }}>{error}</p>}\n </div>\n )}\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={onClose} disabled={publishing}>\n {result ? 'Done' : 'Cancel'}\n </Button>\n {!result && (\n <Button onClick={handlePublish} disabled={publishing}>\n {publishing ? <Spinner /> : 'Publish'}\n </Button>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n});\n\nexport default PublishToStoreDialog;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PublishToStoreDialog.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/PublishToStoreDialog.tsx"],"sourcesContent":["/**\n * PublishToStoreDialog\n *\n * Turns the open dashboard into a store template.\n *\n * WHY THIS EXISTS:\n * A workflow template's {nodes, edges} can plausibly be hand-written into the\n * developer portal. A dashboard export — every widget with its layout\n * coordinates, bindings and filters — cannot. Without this, authoring a\n * dashboard template means hand-crafting a large JSON document, which nobody\n * will do, and the gallery stays empty.\n *\n * The backend tokenises the board (replacing this workspace's data-sink\n * references with named slots), then a DRAFT Dashboard Template application is\n * created in the developer portal from the result. It deliberately stops there\n * rather than writing a store product directly: devportal already owns\n * versioning, review and publisher identity for dashboard templates, and\n * forking that pipeline would orphan the listing.\n *\n * @module bigconsole/components/dashboard/PublishToStoreDialog\n */\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Spinner,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { type FC, memo, useCallback, useEffect, useState } from 'react';\n\nimport { useStoreDashboardTemplates, type PublishToStoreResult } from '../../hooks/useStoreDashboardTemplates';\n\nexport interface PublishToStoreDialogProps {\n isOpen: boolean;\n dashboardId: string;\n dashboardName: string;\n dashboardDescription?: string | null;\n onClose: () => void;\n onPublished?: (result: PublishToStoreResult) => void;\n}\n\n/** Store slugs are lowercase, hyphenated and unique. */\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter(Boolean)\n .join('-')\n .slice(0, 60);\n}\n\nexport const PublishToStoreDialog: FC<PublishToStoreDialogProps> = memo(function PublishToStoreDialog({\n isOpen,\n dashboardId,\n dashboardName,\n dashboardDescription,\n onClose,\n onPublished,\n}) {\n const { publishToStore } = useStoreDashboardTemplates({ autoLoad: false });\n\n const [name, setName] = useState(dashboardName);\n const [slug, setSlug] = useState(slugify(dashboardName));\n const [description, setDescription] = useState(dashboardDescription ?? '');\n const [includeSampleData, setIncludeSampleData] = useState(false);\n const [publishing, setPublishing] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [result, setResult] = useState<PublishToStoreResult | null>(null);\n\n useEffect(() => {\n if (!isOpen) return;\n setName(dashboardName);\n setSlug(slugify(dashboardName));\n setDescription(dashboardDescription ?? '');\n setIncludeSampleData(false);\n setError(null);\n setResult(null);\n }, [isOpen, dashboardName, dashboardDescription]);\n\n const handlePublish = useCallback(async () => {\n if (!name.trim()) {\n setError('A template name is required');\n return;\n }\n if (!slug.trim()) {\n setError('A slug is required');\n return;\n }\n if (!description.trim()) {\n setError('A description is required — it is what people see in the gallery');\n return;\n }\n\n setError(null);\n setPublishing(true);\n try {\n const published = await publishToStore({\n dashboardId,\n name: name.trim(),\n slug: slug.trim(),\n description: description.trim(),\n includeSampleData,\n });\n if (!published.success) {\n setError(published.errorMessage ?? 'Failed to publish to the store');\n return;\n }\n setResult(published);\n onPublished?.(published);\n } finally {\n setPublishing(false);\n }\n }, [name, slug, description, includeSampleData, dashboardId, publishToStore, onPublished]);\n\n const label = { display: 'block', marginBottom: 'var(--space-2xs)', fontSize: 'var(--font-size-sm)' };\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && !publishing && onClose()}>\n <DialogContent style={{ maxWidth: '520px' }}>\n <DialogHeader>\n <DialogTitle>Publish as Template</DialogTitle>\n <DialogDescription>\n Share this dashboard as a reusable template. Your data sources are replaced with named slots, so nothing\n from this workspace is published.\n </DialogDescription>\n </DialogHeader>\n\n {result ? (\n <div style={{ padding: 'var(--space-md) 0' }}>\n <p style={{ fontWeight: 'var(--font-weight-medium)' }}>Draft template created</p>\n <p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>\n {result.sinkCount} data source{result.sinkCount === 1 ? '' : 's'} turned into slots. Finish in the\n Developer Portal: review the listing, then submit it for approval. It appears in the gallery once\n approved.\n </p>\n {result.unknownSinkIds.length > 0 && (\n <p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-warning)' }}>\n {result.unknownSinkIds.length} data source\n {result.unknownSinkIds.length === 1 ? '' : 's'} could not be named — those slots use generic labels.\n </p>\n )}\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>\n <div>\n <label htmlFor=\"tpl-name\" style={label}>\n Template name\n </label>\n <Input\n id=\"tpl-name\"\n value={name}\n disabled={publishing}\n onChange={(e) => {\n setName(e.target.value);\n setSlug(slugify(e.target.value));\n }}\n />\n </div>\n\n <div>\n <label htmlFor=\"tpl-slug\" style={label}>\n Slug\n </label>\n <Input\n id=\"tpl-slug\"\n value={slug}\n disabled={publishing}\n onChange={(e) => setSlug(slugify(e.target.value))}\n />\n </div>\n\n <div>\n <label htmlFor=\"tpl-desc\" style={label}>\n Description\n </label>\n <Textarea\n id=\"tpl-desc\"\n rows={3}\n value={description}\n disabled={publishing}\n placeholder=\"What is this dashboard for, and what data does it need?\"\n onChange={(e) => setDescription(e.target.value)}\n />\n </div>\n\n {error && <p style={{ color: 'var(--color-text-danger)', fontSize: 'var(--font-size-sm)' }}>{error}</p>}\n </div>\n )}\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={onClose} disabled={publishing}>\n {result ? 'Done' : 'Cancel'}\n </Button>\n {!result && (\n <Button onClick={handlePublish} disabled={publishing}>\n {publishing ? <Spinner /> : 'Publish'}\n </Button>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n});\n\nexport default PublishToStoreDialog;\n"],"mappings":";;;;;AA+CA,SAAS,EAAQ,GAAuB;AACtC,QAAO,EACJ,aAAa,CACb,MAAM,aAAa,CACnB,OAAO,QAAQ,CACf,KAAK,IAAI,CACT,MAAM,GAAG,GAAG;;AAGjB,IAAa,IAAsD,EAAK,SAA8B,EACpG,WACA,gBACA,kBACA,yBACA,YACA,kBACC;CACD,IAAM,EAAE,sBAAmB,EAA2B,EAAE,UAAU,IAAO,CAAC,EAEpE,CAAC,GAAM,KAAW,EAAS,EAAc,EACzC,CAAC,GAAM,KAAW,EAAS,EAAQ,EAAc,CAAC,EAClD,CAAC,GAAa,KAAkB,EAAS,KAAwB,GAAG,EACpE,CAAC,GAAmB,KAAwB,EAAS,GAAM,EAC3D,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAQ,KAAa,EAAsC,KAAK;AAEvE,SAAgB;AACT,QACL,EAAQ,EAAc,EACtB,EAAQ,EAAQ,EAAc,CAAC,EAC/B,EAAe,KAAwB,GAAG,EAC1C,EAAqB,GAAM,EAC3B,EAAS,KAAK,EACd,EAAU,KAAK;IACd;EAAC;EAAQ;EAAe;EAAqB,CAAC;CAEjD,IAAM,IAAgB,EAAY,YAAY;AAC5C,MAAI,CAAC,EAAK,MAAM,EAAE;AAChB,KAAS,8BAA8B;AACvC;;AAEF,MAAI,CAAC,EAAK,MAAM,EAAE;AAChB,KAAS,qBAAqB;AAC9B;;AAEF,MAAI,CAAC,EAAY,MAAM,EAAE;AACvB,KAAS,mEAAmE;AAC5E;;AAIF,EADA,EAAS,KAAK,EACd,EAAc,GAAK;AACnB,MAAI;GACF,IAAM,IAAY,MAAM,EAAe;IACrC;IACA,MAAM,EAAK,MAAM;IACjB,MAAM,EAAK,MAAM;IACjB,aAAa,EAAY,MAAM;IAC/B;IACD,CAAC;AACF,OAAI,CAAC,EAAU,SAAS;AACtB,MAAS,EAAU,gBAAgB,iCAAiC;AACpE;;AAGF,GADA,EAAU,EAAU,EACpB,IAAc,EAAU;YAChB;AACR,KAAc,GAAM;;IAErB;EAAC;EAAM;EAAM;EAAa;EAAmB;EAAa;EAAgB;EAAY,CAAC,EAEpF,IAAQ;EAAE,SAAS;EAAS,cAAc;EAAoB,UAAU;EAAuB;AAErG,QACE,kBAAC,GAAD;EAAQ,MAAM;EAAQ,eAAe,MAAS,CAAC,KAAQ,CAAC,KAAc,GAAS;YAC7E,kBAAC,GAAD;GAAe,OAAO,EAAE,UAAU,SAAS;aAA3C;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAa,uBAAiC,CAAA,EAC9C,kBAAC,GAAD,EAAA,UAAmB,8IAGC,CAAA,CACP,EAAA,CAAA;IAEd,IACC,kBAAC,OAAD;KAAK,OAAO,EAAE,SAAS,qBAAqB;eAA5C;MACE,kBAAC,KAAD;OAAG,OAAO,EAAE,YAAY,6BAA6B;iBAAE;OAA0B,CAAA;MACjF,kBAAC,KAAD;OAAG,OAAO;QAAE,UAAU;QAAuB,OAAO;QAA2B;iBAA/E;QACG,EAAO;QAAU;QAAa,EAAO,cAAc,IAAI,KAAK;QAAI;QAG/D;;MACH,EAAO,eAAe,SAAS,KAC9B,kBAAC,KAAD;OAAG,OAAO;QAAE,UAAU;QAAuB,OAAO;QAA6B;iBAAjF;QACG,EAAO,eAAe;QAAO;QAC7B,EAAO,eAAe,WAAW,IAAI,KAAK;QAAI;QAC7C;;MAEF;SAEN,kBAAC,OAAD;KAAK,OAAO;MAAE,SAAS;MAAQ,eAAe;MAAU,KAAK;MAAmB;eAAhF;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAQ;OAAW,OAAO;iBAAO;OAEhC,CAAA,EACR,kBAAC,GAAD;OACE,IAAG;OACH,OAAO;OACP,UAAU;OACV,WAAW,MAAM;AAEf,QADA,EAAQ,EAAE,OAAO,MAAM,EACvB,EAAQ,EAAQ,EAAE,OAAO,MAAM,CAAC;;OAElC,CAAA,CACE,EAAA,CAAA;MAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAQ;OAAW,OAAO;iBAAO;OAEhC,CAAA,EACR,kBAAC,GAAD;OACE,IAAG;OACH,OAAO;OACP,UAAU;OACV,WAAW,MAAM,EAAQ,EAAQ,EAAE,OAAO,MAAM,CAAC;OACjD,CAAA,CACE,EAAA,CAAA;MAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAQ;OAAW,OAAO;iBAAO;OAEhC,CAAA,EACR,kBAAC,GAAD;OACE,IAAG;OACH,MAAM;OACN,OAAO;OACP,UAAU;OACV,aAAY;OACZ,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;OAC/C,CAAA,CACE,EAAA,CAAA;MAEL,KAAS,kBAAC,KAAD;OAAG,OAAO;QAAE,OAAO;QAA4B,UAAU;QAAuB;iBAAG;OAAU,CAAA;MACnG;;IAGR,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,SAAS;KAAS,UAAU;eACnD,IAAS,SAAS;KACZ,CAAA,EACR,CAAC,KACA,kBAAC,GAAD;KAAQ,SAAS;KAAe,UAAU;eACvC,IAAa,kBAAC,GAAD,EAAW,CAAA,GAAG;KACrB,CAAA,CAEE,EAAA,CAAA;IACD;;EACT,CAAA;EAEX"}
|
|
@@ -3,7 +3,7 @@ import { memo as t } from "react";
|
|
|
3
3
|
import { MessageSquare as n } from "lucide-react";
|
|
4
4
|
import { jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
5
5
|
//#region src/bigconsole/components/widgets/WidgetHeader.tsx
|
|
6
|
-
var a = t(function({ widget: t, definition: a, isSelected: s, isEditMode: c, isLoading: l,
|
|
6
|
+
var a = t(function({ widget: t, definition: a, isSelected: s, isEditMode: c, isLoading: l, isSampleData: u, timeUntilRefresh: d, hasDrilldown: f, onRefresh: p, onExportCSV: m, onExportPNG: h, onDrilldown: g, onToggleComments: _, commentsOpen: v, commentCount: y = 0 }) {
|
|
7
7
|
return /* @__PURE__ */ i("div", {
|
|
8
8
|
className: `
|
|
9
9
|
flex items-center justify-between
|
|
@@ -32,7 +32,12 @@ var a = t(function({ widget: t, definition: a, isSelected: s, isEditMode: c, isL
|
|
|
32
32
|
title: t.title,
|
|
33
33
|
children: t.title
|
|
34
34
|
}),
|
|
35
|
-
|
|
35
|
+
u && /* @__PURE__ */ r("span", {
|
|
36
|
+
className: "\n flex-shrink-0\n px-1.5 py-0.5\n rounded\n text-[10px] font-medium uppercase tracking-wide\n bg-status-info-bg-subtle text-status-info-text\n ",
|
|
37
|
+
title: "Example values from the template. Connect a data source to see your own data.",
|
|
38
|
+
children: "Sample"
|
|
39
|
+
}),
|
|
40
|
+
f && /* @__PURE__ */ i("span", {
|
|
36
41
|
className: "\n flex-shrink-0 inline-flex items-center gap-0.5\n px-1.5 py-0.5 rounded-md\n bg-action-primary-bg/10 text-action-primary-text\n text-[10px] font-medium\n ",
|
|
37
42
|
title: "Click data to drill down",
|
|
38
43
|
children: [/* @__PURE__ */ r("svg", {
|
|
@@ -48,19 +53,19 @@ var a = t(function({ widget: t, definition: a, isSelected: s, isEditMode: c, isL
|
|
|
48
53
|
})
|
|
49
54
|
}), "Drilldown"]
|
|
50
55
|
}),
|
|
51
|
-
|
|
56
|
+
d && !c && /* @__PURE__ */ r("span", {
|
|
52
57
|
className: "\n flex-shrink-0 text-xs\n text-text-secondary\n tabular-nums\n ",
|
|
53
58
|
title: "Time until next refresh",
|
|
54
|
-
children:
|
|
59
|
+
children: d
|
|
55
60
|
})
|
|
56
61
|
]
|
|
57
62
|
}), /* @__PURE__ */ i("div", {
|
|
58
63
|
className: "flex items-center gap-1 ml-2",
|
|
59
64
|
children: [
|
|
60
|
-
|
|
65
|
+
g && !c && /* @__PURE__ */ r("button", {
|
|
61
66
|
type: "button",
|
|
62
67
|
onClick: (e) => {
|
|
63
|
-
e.stopPropagation(),
|
|
68
|
+
e.stopPropagation(), g();
|
|
64
69
|
},
|
|
65
70
|
className: "\n p-1.5 rounded-md\n text-text-secondary\n hover:text-text-primary\n hover:bg-bg-sunken\n transition-colors\n ",
|
|
66
71
|
title: "Drill down to details",
|
|
@@ -77,10 +82,10 @@ var a = t(function({ widget: t, definition: a, isSelected: s, isEditMode: c, isL
|
|
|
77
82
|
})
|
|
78
83
|
})
|
|
79
84
|
}),
|
|
80
|
-
|
|
85
|
+
p && /* @__PURE__ */ r("button", {
|
|
81
86
|
type: "button",
|
|
82
87
|
onClick: (e) => {
|
|
83
|
-
e.stopPropagation(),
|
|
88
|
+
e.stopPropagation(), p();
|
|
84
89
|
},
|
|
85
90
|
disabled: l,
|
|
86
91
|
className: `
|
|
@@ -106,22 +111,22 @@ var a = t(function({ widget: t, definition: a, isSelected: s, isEditMode: c, isL
|
|
|
106
111
|
})
|
|
107
112
|
})
|
|
108
113
|
}),
|
|
109
|
-
|
|
114
|
+
_ && /* @__PURE__ */ i("button", {
|
|
110
115
|
type: "button",
|
|
111
116
|
onClick: (e) => {
|
|
112
|
-
e.stopPropagation(),
|
|
117
|
+
e.stopPropagation(), _();
|
|
113
118
|
},
|
|
114
119
|
"aria-label": "Toggle comments",
|
|
115
|
-
"aria-pressed":
|
|
120
|
+
"aria-pressed": v,
|
|
116
121
|
className: `
|
|
117
122
|
relative p-1.5 rounded-md
|
|
118
123
|
transition-colors
|
|
119
|
-
${
|
|
124
|
+
${v ? "text-action-primary-text bg-action-primary-bg/10" : "text-text-secondary hover:text-text-primary hover:bg-bg-sunken"}
|
|
120
125
|
`,
|
|
121
126
|
title: "Comments",
|
|
122
|
-
children: [/* @__PURE__ */ r(n, { className: "w-4 h-4" }),
|
|
127
|
+
children: [/* @__PURE__ */ r(n, { className: "w-4 h-4" }), y > 0 && /* @__PURE__ */ r("span", {
|
|
123
128
|
className: "absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-0.5 rounded-full bg-action-primary-bg text-action-primary-text text-[9px] font-semibold leading-[14px] text-center tabular-nums",
|
|
124
|
-
children:
|
|
129
|
+
children: y > 99 ? "99+" : y
|
|
125
130
|
})]
|
|
126
131
|
}),
|
|
127
132
|
/* @__PURE__ */ r("div", {
|
|
@@ -129,9 +134,9 @@ var a = t(function({ widget: t, definition: a, isSelected: s, isEditMode: c, isL
|
|
|
129
134
|
children: /* @__PURE__ */ r(e, {
|
|
130
135
|
widget: t,
|
|
131
136
|
isEditMode: c,
|
|
132
|
-
onRefresh:
|
|
133
|
-
onExportCSV:
|
|
134
|
-
onExportPNG:
|
|
137
|
+
onRefresh: p,
|
|
138
|
+
onExportCSV: m,
|
|
139
|
+
onExportPNG: h
|
|
135
140
|
})
|
|
136
141
|
})
|
|
137
142
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WidgetHeader.js","names":[],"sources":["../../../../src/bigconsole/components/widgets/WidgetHeader.tsx"],"sourcesContent":["/**\n * WidgetHeader Component\n *\n * Header section of a widget with title, icon, and action menu.\n */\n\nimport { type FC, type ReactElement, memo } from 'react';\nimport { MessageSquare } from 'lucide-react';\nimport type { Widget } from '../../types';\nimport type { WidgetDefinition } from './WidgetRegistry';\nimport { WidgetMenu } from './WidgetMenu';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WidgetHeaderProps {\n /** The widget data */\n widget: Widget;\n /** Widget definition from registry */\n definition: WidgetDefinition;\n /** Whether the widget is selected */\n isSelected?: boolean;\n /** Whether in edit mode */\n isEditMode?: boolean;\n /** Whether the widget is loading */\n isLoading?: boolean;\n /** Time until next refresh (formatted string) */\n timeUntilRefresh?: string;\n /** Whether drilldown is enabled on this widget */\n hasDrilldown?: boolean;\n /** Callback to refresh widget */\n onRefresh?: () => void;\n /** Callback to export as CSV */\n onExportCSV?: () => void;\n /** Callback to export as PNG */\n onExportPNG?: () => void;\n /** Callback for drilldown */\n onDrilldown?: () => void;\n /** Toggle the widget comments panel */\n onToggleComments?: () => void;\n /** Whether the comments panel is currently open */\n commentsOpen?: boolean;\n /** Number of comments on this widget (badge) */\n commentCount?: number;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetHeader: FC<WidgetHeaderProps> = memo(function WidgetHeader({\n widget,\n definition,\n isSelected,\n isEditMode,\n isLoading,\n timeUntilRefresh,\n hasDrilldown,\n onRefresh,\n onExportCSV,\n onExportPNG,\n onDrilldown,\n onToggleComments,\n commentsOpen,\n commentCount = 0,\n}) {\n return (\n <div\n className={`\n flex items-center justify-between\n px-4 py-3\n border-b border-border-default\n bg-bg-sunken\n ${isSelected && isEditMode ? 'bg-action-primary-bg/5' : ''}\n `}\n >\n {/* Left side: Icon and Title - THIS IS THE DRAG HANDLE */}\n <div\n className={`\n widget-drag-handle\n flex items-center gap-2 min-w-0 flex-1\n ${isEditMode ? 'cursor-move' : ''}\n `}\n >\n {/* Widget type icon */}\n <div\n className={`\n flex-shrink-0 w-5 h-5\n text-text-secondary\n ${isLoading ? 'animate-pulse' : ''}\n `}\n >\n <WidgetIcon name={definition.icon} />\n </div>\n\n {/* Title */}\n <h3\n className=\"\n text-sm font-medium\n text-text-primary\n truncate\n \"\n title={widget.title}\n >\n {widget.title}\n </h3>\n\n {/* Drilldown indicator badge */}\n {hasDrilldown && (\n <span\n className=\"\n flex-shrink-0 inline-flex items-center gap-0.5\n px-1.5 py-0.5 rounded-md\n bg-action-primary-bg/10 text-action-primary-text\n text-[10px] font-medium\n \"\n title=\"Click data to drill down\"\n >\n <svg className=\"w-2.5 h-2.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2.5}\n d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n />\n </svg>\n Drilldown\n </span>\n )}\n\n {/* Refresh indicator */}\n {timeUntilRefresh && !isEditMode && (\n <span\n className=\"\n flex-shrink-0 text-xs\n text-text-secondary\n tabular-nums\n \"\n title=\"Time until next refresh\"\n >\n {timeUntilRefresh}\n </span>\n )}\n </div>\n\n {/* Right side: Actions */}\n <div className=\"flex items-center gap-1 ml-2\">\n {/* Drilldown button (shown in view mode) */}\n {onDrilldown && !isEditMode && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onDrilldown();\n }}\n className=\"\n p-1.5 rounded-md\n text-text-secondary\n hover:text-text-primary\n hover:bg-bg-sunken\n transition-colors\n \"\n title=\"Drill down to details\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n />\n </svg>\n </button>\n )}\n\n {/* Refresh button */}\n {onRefresh && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onRefresh();\n }}\n disabled={isLoading}\n className={`\n p-1.5 rounded-md\n text-text-secondary\n hover:text-text-primary\n hover:bg-bg-sunken\n transition-colors\n disabled:opacity-50 disabled:cursor-not-allowed\n ${isLoading ? 'animate-spin' : ''}\n `}\n title=\"Refresh data\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\"\n />\n </svg>\n </button>\n )}\n\n {/* Comments toggle */}\n {onToggleComments && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggleComments();\n }}\n aria-label=\"Toggle comments\"\n aria-pressed={commentsOpen}\n className={`\n relative p-1.5 rounded-md\n transition-colors\n ${\n commentsOpen\n ? 'text-action-primary-text bg-action-primary-bg/10'\n : 'text-text-secondary hover:text-text-primary hover:bg-bg-sunken'\n }\n `}\n title=\"Comments\"\n >\n <MessageSquare className=\"w-4 h-4\" />\n {commentCount > 0 && (\n <span className=\"absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-0.5 rounded-full bg-action-primary-bg text-action-primary-text text-[9px] font-semibold leading-[14px] text-center tabular-nums\">\n {commentCount > 99 ? '99+' : commentCount}\n </span>\n )}\n </button>\n )}\n\n {/* Menu (always visible) */}\n <div className=\"block\">\n <WidgetMenu\n widget={widget}\n isEditMode={isEditMode}\n onRefresh={onRefresh}\n onExportCSV={onExportCSV}\n onExportPNG={onExportPNG}\n />\n </div>\n </div>\n </div>\n );\n});\n\n// ============================================================================\n// Widget Icon Component\n// ============================================================================\n\ninterface WidgetIconProps {\n name: string;\n className?: string;\n}\n\nconst WidgetIcon: FC<WidgetIconProps> = ({ name, className = '' }) => {\n // Simple icon mapping - in production, use lucide-react or similar\n const icons: Record<string, ReactElement> = {\n TrendingUp: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 7h8m0 0v8m0-8l-8 8-4-4-6 6\" />\n </svg>\n ),\n BarChart3: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M18 20V10M12 20V4M6 20v-6\" />\n </svg>\n ),\n LineChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M7 20l4-16m2 16l4-16M6 9h14M4 15h14\" />\n </svg>\n ),\n Table: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z\"\n />\n </svg>\n ),\n Gauge: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n ),\n Activity: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M22 12h-4l-3 9L9 3l-3 9H2\" />\n </svg>\n ),\n List: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 6h16M4 10h16M4 14h16M4 18h16\" />\n </svg>\n ),\n PieChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z\"\n />\n </svg>\n ),\n BarChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n ),\n AreaChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4v16\"\n />\n </svg>\n ),\n };\n\n // Default icon\n const defaultIcon = (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z\"\n />\n </svg>\n );\n\n return icons[name] || defaultIcon;\n};\n\nexport default WidgetHeader;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WidgetHeader.js","names":[],"sources":["../../../../src/bigconsole/components/widgets/WidgetHeader.tsx"],"sourcesContent":["/**\n * WidgetHeader Component\n *\n * Header section of a widget with title, icon, and action menu.\n */\n\nimport { type FC, type ReactElement, memo } from 'react';\nimport { MessageSquare } from 'lucide-react';\nimport type { Widget } from '../../types';\nimport type { WidgetDefinition } from './WidgetRegistry';\nimport { WidgetMenu } from './WidgetMenu';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WidgetHeaderProps {\n /** The widget data */\n widget: Widget;\n /** Widget definition from registry */\n definition: WidgetDefinition;\n /** Whether the widget is selected */\n isSelected?: boolean;\n /** Whether in edit mode */\n isEditMode?: boolean;\n /**\n * The rendered values come from a template's embedded sample, not a\n * connected data source. Badged so nobody mistakes example numbers for\n * their own.\n */\n isSampleData?: boolean;\n /** Whether the widget is loading */\n isLoading?: boolean;\n /** Time until next refresh (formatted string) */\n timeUntilRefresh?: string;\n /** Whether drilldown is enabled on this widget */\n hasDrilldown?: boolean;\n /** Callback to refresh widget */\n onRefresh?: () => void;\n /** Callback to export as CSV */\n onExportCSV?: () => void;\n /** Callback to export as PNG */\n onExportPNG?: () => void;\n /** Callback for drilldown */\n onDrilldown?: () => void;\n /** Toggle the widget comments panel */\n onToggleComments?: () => void;\n /** Whether the comments panel is currently open */\n commentsOpen?: boolean;\n /** Number of comments on this widget (badge) */\n commentCount?: number;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetHeader: FC<WidgetHeaderProps> = memo(function WidgetHeader({\n widget,\n definition,\n isSelected,\n isEditMode,\n isLoading,\n isSampleData,\n timeUntilRefresh,\n hasDrilldown,\n onRefresh,\n onExportCSV,\n onExportPNG,\n onDrilldown,\n onToggleComments,\n commentsOpen,\n commentCount = 0,\n}) {\n return (\n <div\n className={`\n flex items-center justify-between\n px-4 py-3\n border-b border-border-default\n bg-bg-sunken\n ${isSelected && isEditMode ? 'bg-action-primary-bg/5' : ''}\n `}\n >\n {/* Left side: Icon and Title - THIS IS THE DRAG HANDLE */}\n <div\n className={`\n widget-drag-handle\n flex items-center gap-2 min-w-0 flex-1\n ${isEditMode ? 'cursor-move' : ''}\n `}\n >\n {/* Widget type icon */}\n <div\n className={`\n flex-shrink-0 w-5 h-5\n text-text-secondary\n ${isLoading ? 'animate-pulse' : ''}\n `}\n >\n <WidgetIcon name={definition.icon} />\n </div>\n\n {/* Title */}\n <h3\n className=\"\n text-sm font-medium\n text-text-primary\n truncate\n \"\n title={widget.title}\n >\n {widget.title}\n </h3>\n\n {/* Sample-data badge — example values, not the viewer's own data */}\n {isSampleData && (\n <span\n className=\"\n flex-shrink-0\n px-1.5 py-0.5\n rounded\n text-[10px] font-medium uppercase tracking-wide\n bg-status-info-bg-subtle text-status-info-text\n \"\n title=\"Example values from the template. Connect a data source to see your own data.\"\n >\n Sample\n </span>\n )}\n\n {/* Drilldown indicator badge */}\n {hasDrilldown && (\n <span\n className=\"\n flex-shrink-0 inline-flex items-center gap-0.5\n px-1.5 py-0.5 rounded-md\n bg-action-primary-bg/10 text-action-primary-text\n text-[10px] font-medium\n \"\n title=\"Click data to drill down\"\n >\n <svg className=\"w-2.5 h-2.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2.5}\n d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n />\n </svg>\n Drilldown\n </span>\n )}\n\n {/* Refresh indicator */}\n {timeUntilRefresh && !isEditMode && (\n <span\n className=\"\n flex-shrink-0 text-xs\n text-text-secondary\n tabular-nums\n \"\n title=\"Time until next refresh\"\n >\n {timeUntilRefresh}\n </span>\n )}\n </div>\n\n {/* Right side: Actions */}\n <div className=\"flex items-center gap-1 ml-2\">\n {/* Drilldown button (shown in view mode) */}\n {onDrilldown && !isEditMode && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onDrilldown();\n }}\n className=\"\n p-1.5 rounded-md\n text-text-secondary\n hover:text-text-primary\n hover:bg-bg-sunken\n transition-colors\n \"\n title=\"Drill down to details\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n />\n </svg>\n </button>\n )}\n\n {/* Refresh button */}\n {onRefresh && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onRefresh();\n }}\n disabled={isLoading}\n className={`\n p-1.5 rounded-md\n text-text-secondary\n hover:text-text-primary\n hover:bg-bg-sunken\n transition-colors\n disabled:opacity-50 disabled:cursor-not-allowed\n ${isLoading ? 'animate-spin' : ''}\n `}\n title=\"Refresh data\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\"\n />\n </svg>\n </button>\n )}\n\n {/* Comments toggle */}\n {onToggleComments && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggleComments();\n }}\n aria-label=\"Toggle comments\"\n aria-pressed={commentsOpen}\n className={`\n relative p-1.5 rounded-md\n transition-colors\n ${\n commentsOpen\n ? 'text-action-primary-text bg-action-primary-bg/10'\n : 'text-text-secondary hover:text-text-primary hover:bg-bg-sunken'\n }\n `}\n title=\"Comments\"\n >\n <MessageSquare className=\"w-4 h-4\" />\n {commentCount > 0 && (\n <span className=\"absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-0.5 rounded-full bg-action-primary-bg text-action-primary-text text-[9px] font-semibold leading-[14px] text-center tabular-nums\">\n {commentCount > 99 ? '99+' : commentCount}\n </span>\n )}\n </button>\n )}\n\n {/* Menu (always visible) */}\n <div className=\"block\">\n <WidgetMenu\n widget={widget}\n isEditMode={isEditMode}\n onRefresh={onRefresh}\n onExportCSV={onExportCSV}\n onExportPNG={onExportPNG}\n />\n </div>\n </div>\n </div>\n );\n});\n\n// ============================================================================\n// Widget Icon Component\n// ============================================================================\n\ninterface WidgetIconProps {\n name: string;\n className?: string;\n}\n\nconst WidgetIcon: FC<WidgetIconProps> = ({ name, className = '' }) => {\n // Simple icon mapping - in production, use lucide-react or similar\n const icons: Record<string, ReactElement> = {\n TrendingUp: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 7h8m0 0v8m0-8l-8 8-4-4-6 6\" />\n </svg>\n ),\n BarChart3: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M18 20V10M12 20V4M6 20v-6\" />\n </svg>\n ),\n LineChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M7 20l4-16m2 16l4-16M6 9h14M4 15h14\" />\n </svg>\n ),\n Table: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z\"\n />\n </svg>\n ),\n Gauge: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n ),\n Activity: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M22 12h-4l-3 9L9 3l-3 9H2\" />\n </svg>\n ),\n List: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 6h16M4 10h16M4 14h16M4 18h16\" />\n </svg>\n ),\n PieChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z\"\n />\n </svg>\n ),\n BarChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n ),\n AreaChart: (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4v16\"\n />\n </svg>\n ),\n };\n\n // Default icon\n const defaultIcon = (\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className={className}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z\"\n />\n </svg>\n );\n\n return icons[name] || defaultIcon;\n};\n\nexport default WidgetHeader;\n"],"mappings":"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"}
|
|
@@ -282,9 +282,13 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
282
282
|
Ye,
|
|
283
283
|
F,
|
|
284
284
|
G
|
|
285
|
-
]), q = p(() => K ?? null, [K]),
|
|
285
|
+
]), q = p(() => K ?? null, [K]), Xe = p(() => !q || M ? !1 : u.config?.mockData != null, [
|
|
286
|
+
q,
|
|
287
|
+
M,
|
|
288
|
+
u.config
|
|
289
|
+
]), { executeDrilldown: Ze, canDrilldown: Qe } = ie({ onDrilldown: (e, t) => {
|
|
286
290
|
Re?.(e);
|
|
287
|
-
} }), J = p(() =>
|
|
291
|
+
} }), J = p(() => Qe(u), [u, Qe]), Y = f((e) => {
|
|
288
292
|
if (!u.drilldown?.enabled) return;
|
|
289
293
|
let t = {
|
|
290
294
|
selectedData: e || {},
|
|
@@ -297,18 +301,18 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
297
301
|
},
|
|
298
302
|
sourceDashboardId: u.dashboardId
|
|
299
303
|
};
|
|
300
|
-
|
|
304
|
+
Ze(u.drilldown, t);
|
|
301
305
|
}, [
|
|
302
306
|
u,
|
|
303
307
|
q,
|
|
304
308
|
D,
|
|
305
|
-
|
|
306
|
-
]), X = m(null),
|
|
309
|
+
Ze
|
|
310
|
+
]), X = m(null), $e = f((e) => {
|
|
307
311
|
X.current = {
|
|
308
312
|
x: e.clientX,
|
|
309
313
|
y: e.clientY
|
|
310
314
|
};
|
|
311
|
-
}, []),
|
|
315
|
+
}, []), et = f((e) => {
|
|
312
316
|
if (e.stopPropagation(), X.current) {
|
|
313
317
|
let t = Math.abs(e.clientX - X.current.x), n = Math.abs(e.clientY - X.current.y);
|
|
314
318
|
if (t > 5 || n > 5) {
|
|
@@ -321,20 +325,20 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
321
325
|
C,
|
|
322
326
|
w,
|
|
323
327
|
u.id
|
|
324
|
-
]),
|
|
328
|
+
]), tt = f((e) => {
|
|
325
329
|
(e.key === "Enter" || e.key === " ") && (e.preventDefault(), e.stopPropagation(), C && w(u.id));
|
|
326
330
|
}, [
|
|
327
331
|
C,
|
|
328
332
|
w,
|
|
329
333
|
u.id
|
|
330
|
-
]),
|
|
334
|
+
]), nt = p(() => u.type === "custom" && le(u.config), [u.type, u.config]), rt = p(() => u.type === "custom" && ce(u.config), [u.type, u.config]), Z = J && !rt, it = !L && !R && !q && !nt, Q = p(() => u.renderer, [u]), at = p(() => Q && Q !== "BIGCONSOLE", [Q]), ot = p(() => ({
|
|
331
335
|
widgetId: u.id,
|
|
332
336
|
data: q || {}
|
|
333
337
|
}), [u.id, q]), $ = f((e, t) => {
|
|
334
338
|
if (!x) return;
|
|
335
339
|
let n = b?.find((t) => t.name === e.name);
|
|
336
340
|
x(n || e, t);
|
|
337
|
-
}, [x, b]),
|
|
341
|
+
}, [x, b]), st = f(() => {
|
|
338
342
|
let e = {
|
|
339
343
|
widget: u,
|
|
340
344
|
data: (() => {
|
|
@@ -394,7 +398,7 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
394
398
|
})(),
|
|
395
399
|
onDrilldown: J ? Y : void 0
|
|
396
400
|
};
|
|
397
|
-
if (
|
|
401
|
+
if (at) {
|
|
398
402
|
let e = {};
|
|
399
403
|
if (K && typeof K == "object" && !Array.isArray(K) && Object.keys(K).length > 0 && (e = { ...K }), u.config?._internalWidgetType === "custom" && Object.keys(e).length > 0) {
|
|
400
404
|
let t = u.config?.valueField;
|
|
@@ -448,7 +452,7 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
448
452
|
d,
|
|
449
453
|
J,
|
|
450
454
|
Y,
|
|
451
|
-
|
|
455
|
+
at,
|
|
452
456
|
C,
|
|
453
457
|
T,
|
|
454
458
|
L,
|
|
@@ -468,9 +472,9 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
468
472
|
${C ? "cursor-pointer" : ""}
|
|
469
473
|
${v}
|
|
470
474
|
`,
|
|
471
|
-
onMouseDown:
|
|
472
|
-
onClick:
|
|
473
|
-
onKeyDown:
|
|
475
|
+
onMouseDown: $e,
|
|
476
|
+
onClick: et,
|
|
477
|
+
onKeyDown: tt,
|
|
474
478
|
tabIndex: C ? 0 : -1,
|
|
475
479
|
role: C ? "button" : void 0,
|
|
476
480
|
"aria-selected": T,
|
|
@@ -484,6 +488,7 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
484
488
|
isSelected: T,
|
|
485
489
|
isEditMode: C,
|
|
486
490
|
isLoading: L,
|
|
491
|
+
isSampleData: Xe,
|
|
487
492
|
timeUntilRefresh: u.refreshInterval && !Ue ? ee(He) : void 0,
|
|
488
493
|
hasDrilldown: J,
|
|
489
494
|
onRefresh: y,
|
|
@@ -508,7 +513,7 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
508
513
|
error: R,
|
|
509
514
|
onRetry: y
|
|
510
515
|
});
|
|
511
|
-
if (
|
|
516
|
+
if (it && !d) return /* @__PURE__ */ g(l, {
|
|
512
517
|
widgetType: u.type,
|
|
513
518
|
hasDataSource: M,
|
|
514
519
|
templateSlot: typeof u.metadata?.templateDataSinkSlot == "string" ? u.metadata.templateDataSinkSlot : null
|
|
@@ -521,7 +526,7 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
521
526
|
resetKey: e,
|
|
522
527
|
children: d ?? /* @__PURE__ */ g(pe, {
|
|
523
528
|
fallback: /* @__PURE__ */ g(c, {}),
|
|
524
|
-
children:
|
|
529
|
+
children: st()
|
|
525
530
|
})
|
|
526
531
|
});
|
|
527
532
|
})()
|
|
@@ -532,7 +537,7 @@ var v = d(function({ widget: u, children: d, data: _, className: v = "", isLoadi
|
|
|
532
537
|
style: { flexShrink: 0 },
|
|
533
538
|
children: /* @__PURE__ */ g(fe, {
|
|
534
539
|
actions: b,
|
|
535
|
-
context:
|
|
540
|
+
context: ot,
|
|
536
541
|
onAction: x,
|
|
537
542
|
disabled: C,
|
|
538
543
|
layout: "horizontal",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WidgetWrapper.js","names":[],"sources":["../../../../src/bigconsole/components/widgets/WidgetWrapper.tsx"],"sourcesContent":["/**\n * WidgetWrapper Component (v2.1)\n *\n * Common wrapper for all widget types. Handles:\n * - Header with title and menu\n * - Loading, error, and empty states\n * - Selection highlighting\n * - Edit mode controls\n * - Real data only — no mock fallback; widgets show empty state when data is missing\n * - v2.0: Multi-renderer support via WidgetRendererFactory\n * - v2.0: Widget actions support\n */\n\nimport {\n type FC,\n type ReactNode,\n type RefObject,\n useCallback,\n memo,\n useMemo,\n useRef,\n useState,\n useEffect,\n lazy,\n Suspense,\n} from 'react';\nimport type { Widget } from '../../types';\nimport {\n useWidgetStore,\n useDashboardStore,\n useIsWidgetSelected,\n useWidgetUIState,\n useGlobalFilterValues,\n} from '../../store';\nimport {\n useWidgetAutoRefresh,\n formatTimeUntilRefresh,\n useWidgetExport,\n useDrilldown,\n useWidgetData,\n useWidgetComments,\n type DrilldownContext,\n} from '../../hooks';\nimport { useApolloClient } from '@apollo/client/react';\nimport { GetDashboardDocument } from '../../../generated/wspace-operations';\nimport { getWidgetDefinition } from './WidgetRegistry';\nimport { WidgetHeader } from './WidgetHeader';\nimport { WidgetLoadingState } from './states/LoadingState';\nimport { WidgetErrorState } from './states/ErrorState';\nimport { WidgetEmptyState } from './states/EmptyState';\nimport { WidgetErrorBoundary } from './WidgetErrorBoundary';\nimport { hasRenderableCustomConfig, hasCustomIframeConfig } from './custom/config';\nimport { getBindingForWidget } from '../../utils/widgetBinding';\n\n// v2.0: Renderer factory and actions\nimport { WidgetRendererFactory } from '../../renderers/factory/WidgetRendererFactory';\nimport { WidgetActions } from './widget-actions';\nimport type { WidgetAction, ActionContext, ActionResult, RendererType } from '../../renderers/types/renderer';\n// Widget components - lazy loaded for better code splitting\n// Each widget loads only when its type is rendered\nconst MetricCardWidget = lazy(() => import('./metric-card').then((m) => ({ default: m.MetricCardWidget })));\nconst ChartWidget = lazy(() => import('./chart').then((m) => ({ default: m.ChartWidget })));\nconst TableWidget = lazy(() => import('./table').then((m) => ({ default: m.TableWidget })));\nconst PivotTableWidget = lazy(() => import('./pivot-table').then((m) => ({ default: m.PivotTableWidget })));\nconst GaugeWidget = lazy(() => import('./gauge').then((m) => ({ default: m.GaugeWidget })));\nconst ProgressWidget = lazy(() => import('./progress').then((m) => ({ default: m.ProgressWidget })));\nconst ListWidget = lazy(() => import('./list').then((m) => ({ default: m.ListWidget })));\nconst HeatmapWidget = lazy(() => import('./heatmap').then((m) => ({ default: m.HeatmapWidget })));\nconst RetentionWidget = lazy(() => import('./retention').then((m) => ({ default: m.RetentionWidget })));\nconst CalendarWidget = lazy(() => import('./calendar').then((m) => ({ default: m.CalendarWidget })));\nconst KanbanWidget = lazy(() => import('./kanban').then((m) => ({ default: m.KanbanWidget })));\nconst TimelineWidget = lazy(() => import('./timeline').then((m) => ({ default: m.TimelineWidget })));\nconst KPIComparisonWidget = lazy(() => import('./kpi-comparison').then((m) => ({ default: m.KPIComparisonWidget })));\nconst TextWidget = lazy(() => import('./text-widget').then((m) => ({ default: m.TextWidget })));\nconst MapWidget = lazy(() => import('./map-widget').then((m) => ({ default: m.MapWidget })));\nconst IframeWidget = lazy(() => import('./iframe-widget').then((m) => ({ default: m.IframeWidget })));\nconst CustomWidget = lazy(() => import('./custom').then((m) => ({ default: m.CustomWidget })));\n// AdaptiveCardWidget is HUGE (984KB) - lazy load only when 'form' type is used\nconst AdaptiveCardWidget = lazy(() => import('./adaptive-card').then((m) => ({ default: m.AdaptiveCardWidget })));\n// Comments panel - lazy loaded; only mounts when the user opens comments\nconst WidgetComments = lazy(() => import('./comments/WidgetComments').then((m) => ({ default: m.WidgetComments })));\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WidgetWrapperProps {\n /** The widget data */\n widget: Widget;\n /** Widget content to render */\n children?: ReactNode;\n /** Data for the widget */\n data?: Record<string, unknown> | null;\n /** Custom class name */\n className?: string;\n /** Whether the widget is loading */\n isLoading?: boolean;\n /** Error message */\n error?: string | null;\n /** Callback to refresh widget data */\n onRefresh?: () => Promise<void>;\n /** Callback for drilldown navigation */\n onDrilldown?: (params: Record<string, unknown>) => void;\n /** v2.0: Widget actions */\n actions?: WidgetAction[];\n /** v2.0: Callback for action events */\n onAction?: (action: WidgetAction, context: ActionContext) => Promise<ActionResult | void>;\n /** Phase 2: Global filter values to apply to widget data */\n filterValues?: Record<string, unknown>;\n}\n\n// ============================================================================\n// Custom comparison for memo - ensures actions changes trigger re-render\n// ============================================================================\n\nfunction arePropsEqual(prevProps: WidgetWrapperProps, nextProps: WidgetWrapperProps): boolean {\n // Always re-render if widget ID changes\n if (prevProps.widget.id !== nextProps.widget.id) return false;\n // Re-render if widget was updated\n if (prevProps.widget.updatedAt !== nextProps.widget.updatedAt) return false;\n // Re-render if loading state changes\n if (prevProps.isLoading !== nextProps.isLoading) return false;\n // Re-render if error changes\n if (prevProps.error !== nextProps.error) return false;\n // Re-render if actions count changes\n if ((prevProps.actions?.length ?? 0) !== (nextProps.actions?.length ?? 0)) return false;\n // Check if any action ID changed\n if (prevProps.actions && nextProps.actions) {\n for (let i = 0; i < prevProps.actions.length; i++) {\n if (prevProps.actions[i]?.id !== nextProps.actions[i]?.id) return false;\n }\n }\n // Also check data changes\n if (prevProps.data !== nextProps.data) return false;\n // Phase 2: Re-render if filter values changed\n if (JSON.stringify(prevProps.filterValues) !== JSON.stringify(nextProps.filterValues)) return false;\n return true;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetWrapper: FC<WidgetWrapperProps> = memo(function WidgetWrapper({\n widget,\n children,\n data,\n className = '',\n isLoading: isLoadingProp,\n error: errorProp,\n onRefresh,\n onDrilldown,\n actions,\n onAction,\n filterValues,\n}) {\n // Store state - use Dashboard store for edit mode (synced with toolbar)\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const isSelected = useIsWidgetSelected(widget.id);\n const uiState = useWidgetUIState(widget.id);\n const globalFilterValues = useGlobalFilterValues();\n const apolloClient = useApolloClient();\n\n // Map a global-filter's id -> the data field it targets (`config.field`).\n // Global CATEGORY/TEXT filter values are keyed in the store by filter id (a\n // UUID), not by the data column they filter. Without this map, applyClientFilters\n // can only fall back to value/heuristic matching, which fails to narrow rows\n // whose targeted column (e.g. `country`, `cohort`) isn't in the hardcoded\n // heuristic list — so a selected Country/Cohort would not actually filter the\n // widget. We resolve the targeted field from the dashboard's globalFilters\n // (already in the Apollo cache from the page load) so a UUID-keyed filter\n // matches the row's configured field exactly.\n const widgetDashboardId = (widget as unknown as Record<string, unknown>).dashboardId as string | undefined;\n const filterFieldMap = useMemo<Record<string, string>>(() => {\n if (!widgetDashboardId) return {};\n try {\n const cached = apolloClient.readQuery<{\n getDashboard?: {\n globalFilters?: Array<{ id: string; config?: Record<string, unknown> | null }>;\n };\n }>({ query: GetDashboardDocument, variables: { id: widgetDashboardId } });\n const map: Record<string, string> = {};\n for (const f of cached?.getDashboard?.globalFilters ?? []) {\n const field = f.config?.field;\n if (typeof field === 'string' && field) map[f.id] = field;\n }\n return map;\n } catch {\n return {};\n }\n }, [apolloClient, widgetDashboardId]);\n\n // Fetch per-widget data from backend (parser-processed data)\n // This is needed because listWidgetsByDashboard uses WidgetListFields (without data)\n // for performance, so each widget fetches its own data individually\n const widgetBinding = useMemo(() => getBindingForWidget(widget), [widget]);\n const hasDataSource =\n widgetBinding.source.kind !== 'DATASINK' ||\n !!widgetBinding.source.ref ||\n !!widgetBinding.parserId ||\n !!widget.datasetId;\n const { data: fetchedWidgetData, loading: widgetDataLoading } = useWidgetData(widget.id, {\n skip: !hasDataSource,\n pollInterval: (widget.refreshInterval ?? 0) * 1000 || 0,\n });\n\n // WORKAROUND: If Zustand store is empty, directly read filter_* params from URL\n // This handles race conditions where the store hasn't been populated yet\n const urlFilterValues = useMemo(() => {\n if (typeof window === 'undefined') return {};\n\n const params = new URLSearchParams(window.location.search);\n const filters: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith('filter_')) {\n const filterId = key.slice('filter_'.length);\n // Skip internal metadata keys\n if (!filterId.startsWith('__')) {\n filters[filterId] = value;\n }\n }\n });\n\n return filters;\n }, []);\n\n // Phase 2: Merge global filter values (from URL) with explicit filterValues prop\n // This ensures both URL params (filter_*) and drilldown params work together\n const mergedFilterValues = useMemo(() => {\n const merged: Record<string, unknown> = {};\n\n // First, add URL filter values as fallback (in case Zustand store hasn't loaded yet)\n if (urlFilterValues && Object.keys(urlFilterValues).length > 0) {\n Object.assign(merged, urlFilterValues);\n }\n\n // Then, add global filter values from Zustand store (if populated)\n if (globalFilterValues && Object.keys(globalFilterValues).length > 0) {\n Object.assign(merged, globalFilterValues);\n }\n\n // Finally, add/override with explicit filterValues prop (e.g., from drilldown modal)\n if (filterValues && Object.keys(filterValues).length > 0) {\n Object.assign(merged, filterValues);\n }\n\n return merged;\n }, [urlFilterValues, globalFilterValues, filterValues]);\n\n // Track active filters (client-side filtering until backend supports it)\n const hasFilters = Object.keys(mergedFilterValues).length > 0;\n\n // Merge loading/error from props and store\n // Show loading if widget data is being fetched for the first time (no data yet)\n const isLoading = isLoadingProp ?? (widgetDataLoading && !fetchedWidgetData) ?? uiState.isLoading;\n const error = errorProp ?? uiState.errorMessage;\n\n // Get widget definition\n const definition = getWidgetDefinition(widget.type);\n\n // Stable refresh callback - use ref to avoid creating new function on each render\n const onRefreshRef = useRef(onRefresh);\n useEffect(() => {\n onRefreshRef.current = onRefresh;\n }, [onRefresh]);\n\n const stableOnRefresh = useCallback(async () => {\n if (onRefreshRef.current) {\n await onRefreshRef.current();\n }\n }, []);\n\n // Auto-refresh\n const { timeUntilRefresh, isPaused } = useWidgetAutoRefresh({\n widgetId: widget.id,\n refreshInterval: widget.refreshInterval,\n onRefresh: stableOnRefresh,\n enabled: !isEditMode && !!widget.refreshInterval,\n });\n\n // Container ref for PNG export\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Comments panel. The count query stays disabled until the panel is first\n // opened, so a dashboard of N widgets doesn't fire N comment queries on load.\n const [showComments, setShowComments] = useState(false);\n const [commentsEverOpened, setCommentsEverOpened] = useState(false);\n const { count: commentCount } = useWidgetComments(widget.id, commentsEverOpened);\n const toggleComments = useCallback(() => {\n setShowComments((open) => {\n const next = !open;\n if (next) setCommentsEverOpened(true);\n return next;\n });\n }, []);\n\n // Get widget data for export (computed early for hook)\n const exportData = useMemo(() => {\n if (data && Array.isArray(data)) return data;\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && Array.isArray(rawData.processed)) {\n return rawData.processed;\n }\n if (rawData?.raw) {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (Array.isArray(parsed)) return parsed;\n }\n return null;\n }, [data, widget]);\n\n // Export functionality\n const { exportCSV, exportPNG } = useWidgetExport(widget, {\n data: exportData,\n containerRef: containerRef as unknown as RefObject<HTMLElement>,\n });\n\n // Helper to check if data is actually usable (has real content)\n const isUsableData = useCallback((d: unknown): boolean => {\n if (d === null || d === undefined) return false;\n if (Array.isArray(d)) return d.length > 0;\n if (typeof d === 'object') {\n const obj = d as Record<string, unknown>;\n const keys = Object.keys(obj);\n if (keys.length === 0) return false;\n // Check recursively for actual data content\n return keys.some((key) => {\n const val = obj[key];\n if (val === null || val === undefined || val === '') return false;\n if (Array.isArray(val)) return val.length > 0;\n if (typeof val === 'object') return isUsableData(val);\n return true;\n });\n }\n return true;\n }, []);\n\n // Phase 2: Client-side filter function for widget data\n const applyClientFilters = useCallback(\n (\n widgetData: Record<string, unknown> | Record<string, unknown>[]\n ): Record<string, unknown> | Record<string, unknown>[] => {\n if (!hasFilters || !widgetData) return widgetData;\n\n // Separate drilldown context filters (ctx_*) from regular filters\n const contextFilters: Record<string, unknown> = {};\n const regularFilters: Record<string, unknown> = {};\n\n Object.entries(mergedFilterValues || {}).forEach(([key, value]) => {\n if (key.startsWith('ctx_')) {\n // Strip ctx_ prefix to get the target field name\n const fieldName = key.slice(4); // Remove 'ctx_'\n contextFilters[fieldName] = value;\n } else if (!key.startsWith('__')) {\n // Skip internal metadata keys (__drilldown_depth, etc.)\n regularFilters[key] = value;\n }\n });\n\n // Helper to match a value against a filter (case-insensitive)\n const matchesFilter = (itemValue: unknown, filterValue: unknown): boolean => {\n if (filterValue === undefined || filterValue === null || filterValue === '') return true;\n if (itemValue === undefined || itemValue === null) return false;\n\n const itemStr = String(itemValue).toLowerCase();\n const filterStr = String(filterValue).toLowerCase();\n\n // Exact match or contains\n return itemStr === filterStr || itemStr.includes(filterStr);\n };\n\n // Helper to find a matching field in the item (case-insensitive key matching)\n const findFieldValue = (item: Record<string, unknown>, targetField: string): unknown => {\n const targetLower = targetField.toLowerCase();\n\n // Try exact match first\n if (item[targetField] !== undefined) return item[targetField];\n\n // Try case-insensitive match\n const matchingKey = Object.keys(item).find((k) => k.toLowerCase() === targetLower);\n if (matchingKey) return item[matchingKey];\n\n // Try partial match (e.g., 'region' matches 'regionName' or 'user_region')\n const partialMatch = Object.keys(item).find(\n (k) => k.toLowerCase().includes(targetLower) || targetLower.includes(k.toLowerCase())\n );\n if (partialMatch) return item[partialMatch];\n\n // Try stripping common prefixes from target field (e.g., 'kpiId' -> 'id', 'rowName' -> 'name')\n // This handles drilldown context where widget passes 'kpiId' but data has 'id'\n const commonPrefixes = ['kpi', 'row', 'item', 'data', 'cell', 'record', 'entry'];\n for (const prefix of commonPrefixes) {\n if (targetLower.startsWith(prefix) && targetLower.length > prefix.length) {\n const strippedField = targetLower.slice(prefix.length);\n // Try exact match with stripped field\n const strippedMatch = Object.keys(item).find((k) => k.toLowerCase() === strippedField);\n if (strippedMatch) return item[strippedMatch];\n }\n }\n\n return undefined;\n };\n\n // Helper function to filter an array of items\n const filterItems = (items: Record<string, unknown>[]): Record<string, unknown>[] => {\n return items.filter((item: Record<string, unknown>) => {\n // Only object rows are field-filterable. An array of primitives (e.g.\n // a heatmap's `rows: [\"5W-MedSurg\", …]`) carries no named fields — keep\n // every element. Without this, `findFieldValue` indexes into the string\n // by character offset and spuriously \"matches\" a filter, dropping rows.\n if (item === null || typeof item !== 'object' || Array.isArray(item)) {\n return true;\n }\n // First, apply drilldown context filters (ctx_*) - these have explicit field names\n for (const [fieldName, filterValue] of Object.entries(contextFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n const itemValue = findFieldValue(item, fieldName);\n // If the field doesn't exist in this data item, skip this context filter.\n // Drilldown context params (e.g., source=support_status) are metadata about\n // the drilldown origin and should NOT filter out data when the target\n // dashboard's widgets don't have a matching field.\n if (itemValue === undefined) continue;\n if (!matchesFilter(itemValue, filterValue)) {\n return false;\n }\n }\n\n // Then, apply regular filters\n for (const [filterKey, filterValue] of Object.entries(regularFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n // For date range filters\n if (typeof filterValue === 'object') {\n const fv = filterValue as Record<string, unknown>;\n if (fv.start || fv.end) {\n const dateFields = Object.entries(item).filter(\n ([k, _v]) => k.toLowerCase().includes('date') || k.toLowerCase().includes('time')\n );\n if (dateFields.length > 0) {\n const itemDate = new Date(dateFields[0][1] as string);\n if (fv.start && itemDate < new Date(fv.start as string)) return false;\n if (fv.end && itemDate > new Date(fv.end as string)) return false;\n }\n continue;\n }\n }\n\n // For numeric range filters\n if (typeof filterValue === 'object') {\n const nv = filterValue as Record<string, unknown>;\n if (nv.min !== undefined || nv.max !== undefined) {\n const numericFields = Object.entries(item).filter(([_k, v]) => typeof v === 'number');\n if (numericFields.length > 0) {\n const itemNum = numericFields[0][1] as number;\n if (nv.min !== undefined && itemNum < (nv.min as number)) return false;\n if (nv.max !== undefined && itemNum > (nv.max as number)) return false;\n }\n continue;\n }\n }\n\n // Any other object-shaped filter value (e.g. a DATE_RANGE filter set\n // to a preset like { preset: \"last_30_days\" } with no resolved\n // start/end, or a structured value we don't understand) cannot be\n // compared with the scalar matchers below. Passing it through would\n // stringify it to \"[object Object]\" and, via fuzzy field matching\n // (e.g. an `enrolledAt` filter partial-matching an `enrolled` number\n // column), spuriously exclude EVERY row. Treat such values as a no-op\n // for client-side filtering — the row set is unchanged until the\n // filter resolves to concrete start/end or scalar values.\n if (filterValue !== null && typeof filterValue === 'object' && !Array.isArray(filterValue)) {\n continue;\n }\n\n // Global CATEGORY/TEXT filters are keyed by filter id (UUID). Resolve\n // the data column they target from the filter's config.field. When the\n // widget's rows carry that column, the filter is authoritative — a\n // mismatch excludes the row; a missing column is a no-op (the filter\n // simply doesn't apply to this widget's shape). This is what makes a\n // selected Country/Cohort actually narrow the visible data.\n const targetField = filterFieldMap[filterKey];\n if (targetField) {\n const mappedValue = findFieldValue(item, targetField);\n if (mappedValue !== undefined) {\n if (!matchesFilter(mappedValue, filterValue)) {\n return false;\n }\n continue;\n }\n // Configured field absent from this widget's rows → filter is a\n // no-op for this widget (do not fall through to value heuristics,\n // which could spuriously match an unrelated column).\n continue;\n }\n\n // Try to match by filter key name first (for named filters). When\n // the widget's data actually carries the filter's field, the filter\n // is authoritative — a mismatch excludes the row.\n const fieldValue = findFieldValue(item, filterKey);\n if (fieldValue !== undefined) {\n if (!matchesFilter(fieldValue, filterValue)) {\n return false;\n }\n continue;\n }\n\n // For text/category filters - try multiple matching strategies\n const filterStr = String(filterValue).toLowerCase();\n\n // Strategy 1: Check all field values for exact or partial match\n const itemValues = Object.values(item).map((v) => String(v).toLowerCase());\n const hasMatch = itemValues.some((v) => v === filterStr || v.includes(filterStr));\n\n if (!hasMatch) {\n // Strategy 2: Try to find a field name that matches the filter value context\n const fieldNames = Object.keys(item).map((k) => k.toLowerCase());\n const possibleFields = fieldNames.filter(\n (f) =>\n f.includes('category') ||\n f.includes('type') ||\n f.includes('status') ||\n f.includes('name') ||\n f.includes('region') ||\n f.includes('group')\n );\n\n let fieldMatch = false;\n for (const fieldName of possibleFields) {\n const originalKey = Object.keys(item).find((k) => k.toLowerCase() === fieldName);\n if (originalKey) {\n const itemFieldValue = String(item[originalKey]).toLowerCase();\n if (itemFieldValue === filterStr || itemFieldValue.includes(filterStr)) {\n fieldMatch = true;\n break;\n }\n }\n }\n\n // The filter's field is NOT present in this dataset (named-field\n // lookup failed AND no value/heuristic match). A global filter that\n // doesn't apply to a widget's shape must be a no-op for that widget\n // — not a silent \"exclude every row\". This mirrors the ctx_* rule\n // above (skip when the targeted field is absent). Without this, an\n // always-active required filter (e.g. campus=main) blanks every\n // parser-derived widget whose output has no campus column.\n if (!fieldMatch) {\n continue;\n }\n }\n }\n return true;\n });\n };\n\n // If data is an array, filter the items directly\n if (Array.isArray(widgetData)) {\n const filtered = filterItems(widgetData);\n return filtered;\n }\n\n // Handle object with array properties (rows, items, members, questions, data, etc.)\n if (widgetData && typeof widgetData === 'object') {\n // Common array property names in widget data\n const arrayPropertyNames = [\n 'kpis', // KPI Comparison widget\n 'rows', // Table widget\n 'items', // List widget\n 'members', // Team/member widgets\n 'questions', // Form widget\n 'data', // Generic data\n 'records', // Database-style data\n 'results', // Search/query results\n 'entries', // Log/feed entries\n 'list', // Generic list\n 'values', // Value arrays\n 'series', // Chart series\n 'datasets', // Chart datasets\n ];\n\n for (const propName of arrayPropertyNames) {\n if (widgetData[propName] && Array.isArray(widgetData[propName])) {\n const filteredArray = filterItems(widgetData[propName]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n ...(propName === 'rows' ? { totalRows: filteredArray.length } : {}),\n };\n }\n }\n\n // If no known array property found, check for any array property\n const arrayProps = Object.entries(widgetData).filter(([_key, v]) => Array.isArray(v));\n if (arrayProps.length > 0) {\n const [propName, propValue] = arrayProps[0];\n const filteredArray = filterItems(propValue as Record<string, unknown>[]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n };\n }\n }\n\n // For non-filterable data, return as-is\n return widgetData;\n },\n [hasFilters, mergedFilterValues, widget.id, filterFieldMap]\n );\n\n // Get widget data from the widget itself (fetched from backend)\n const realWidgetData = useMemo(() => {\n let rawWidgetData = null;\n\n // Priority 1: Explicitly passed data prop\n if (data && isUsableData(data)) {\n rawWidgetData = data;\n }\n // Priority 2: Per-widget fetched data (from useWidgetData hook)\n // fetchedWidgetData IS a WidgetData object: { raw, processed, fetchedAt, cached }\n else if (fetchedWidgetData) {\n const fetched = fetchedWidgetData as unknown as Record<string, unknown>;\n if (fetched?.processed && isUsableData(fetched.processed)) {\n rawWidgetData = fetched.processed;\n } else if (fetched?.raw) {\n try {\n const parsed = typeof fetched.raw === 'string' ? JSON.parse(fetched.raw as string) : fetched.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n // Priority 3: widget.data from GraphQL response (if already on widget object)\n else {\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && isUsableData(rawData.processed)) {\n rawWidgetData = rawData.processed;\n } else if (rawData?.raw) {\n try {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n\n // Priority 4: config.mockData — user-configured sample JSON from Widget Properties Data tab\n // This is NOT auto-generated mock data; it's explicit JSON the user pasted in the UI\n if (!rawWidgetData) {\n const configMockData = (widget.config as Record<string, unknown> | undefined)?.mockData;\n if (configMockData) {\n try {\n const parsed = typeof configMockData === 'string' ? JSON.parse(configMockData) : configMockData;\n if (isUsableData(parsed)) {\n rawWidgetData = parsed;\n }\n } catch (e) {}\n }\n }\n\n // Generic parser-output resolution: a widget may bind to a parser whose\n // output is an envelope ({ byUnit:[...], house:{...} }) rather than the\n // exact array/object the renderer wants. config.dataKey / dataPath /\n // seriesPath is a dot-path INTO the resolved data that points at the\n // renderable subtree (e.g. \"byUnit\" for a table, \"rows\" for a chart).\n // Resolving it here keeps every renderer generic — the use-case-specific\n // shape knowledge lives in the widget config (set by the seed/UI), not in\n // the renderer. valueField (single-value metric path) is intentionally NOT\n // applied here: MetricCardWidget resolves it itself against the envelope.\n if (rawWidgetData && typeof rawWidgetData === 'object' && !Array.isArray(rawWidgetData)) {\n const cfg = widget.config as Record<string, unknown> | undefined;\n const pathKey = (cfg?.dataKey ?? cfg?.dataPath ?? cfg?.seriesPath) as string | undefined;\n if (pathKey && typeof pathKey === 'string') {\n const resolved = pathKey.split('.').reduce<unknown>((cur, seg) => {\n return cur && typeof cur === 'object' ? (cur as Record<string, unknown>)[seg] : undefined;\n }, rawWidgetData);\n if (resolved !== undefined && resolved !== null && isUsableData(resolved)) {\n rawWidgetData = resolved as Record<string, unknown>;\n }\n }\n }\n\n // Phase 2: Apply client-side filters if active\n if (hasFilters && rawWidgetData) {\n const filtered = applyClientFilters(rawWidgetData);\n return filtered;\n }\n\n return rawWidgetData;\n }, [data, widget, fetchedWidgetData, hasFilters, applyClientFilters, mergedFilterValues, isUsableData]);\n\n // Final widget data - real data only, no mock fallback\n const widgetData = useMemo(() => {\n return realWidgetData ?? null;\n }, [realWidgetData]);\n\n // Drilldown navigation (must be after widgetData is defined)\n const { executeDrilldown, canDrilldown } = useDrilldown({\n onDrilldown: (params, _mode) => {\n onDrilldown?.(params);\n },\n });\n\n // Check if widget has drilldown configured\n const hasDrilldown = useMemo(() => canDrilldown(widget), [widget, canDrilldown]);\n\n // Handle drilldown execution with context\n const handleDrilldown = useCallback(\n (selectedData?: Record<string, unknown>) => {\n if (!widget.drilldown?.enabled) return;\n\n const context: DrilldownContext = {\n selectedData: selectedData || {},\n globalFilters: globalFilterValues || {},\n widgetData: widgetData || {},\n metadata: {\n widgetId: widget.id,\n widgetType: widget.type,\n widgetTitle: widget.title,\n },\n sourceDashboardId: widget.dashboardId,\n };\n\n executeDrilldown(widget.drilldown, context);\n },\n [widget, widgetData, globalFilterValues, executeDrilldown]\n );\n\n // Track mouse position to distinguish click from drag\n const mouseDownPos = useRef<{ x: number; y: number } | null>(null);\n const DRAG_THRESHOLD = 5; // pixels - if mouse moves more than this, it's a drag\n\n // Handle mouse down - record position\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n mouseDownPos.current = { x: e.clientX, y: e.clientY };\n }, []);\n\n // Handle widget click - Edit mode only (View mode drilldown is triggered by data clicks)\n // Only trigger if mouse hasn't moved much (not a drag)\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation(); // Prevent DashboardGrid from also handling\n\n // Check if this was a drag (mouse moved significantly)\n if (mouseDownPos.current) {\n const dx = Math.abs(e.clientX - mouseDownPos.current.x);\n const dy = Math.abs(e.clientY - mouseDownPos.current.y);\n if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {\n // This was a drag, not a click - don't open config panel\n mouseDownPos.current = null;\n return;\n }\n }\n mouseDownPos.current = null;\n\n if (isEditMode) {\n // Edit mode: select widget for editing\n selectWidget(widget.id);\n }\n // View mode: drilldown is triggered by clicking specific data elements\n // (table rows, chart points, etc.) - not by clicking the widget itself\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n // Handle keyboard navigation (Edit mode only)\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n if (isEditMode) {\n selectWidget(widget.id);\n }\n }\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n const hasRenderableCustomWidgetConfig = useMemo(\n () => widget.type === 'custom' && hasRenderableCustomConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const customWidgetHasIframeConfig = useMemo(\n () => widget.type === 'custom' && hasCustomIframeConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const showContainerDrilldownHints = hasDrilldown && !customWidgetHasIframeConfig;\n\n // Determine if data is empty\n const isEmpty = !isLoading && !error && !widgetData && !hasRenderableCustomWidgetConfig;\n\n // v2.0: Check if widget has a renderer specified (non-BIGCONSOLE)\n const widgetRenderer = useMemo(() => {\n const widgetWithRenderer = widget as Widget & { renderer?: RendererType };\n return widgetWithRenderer.renderer;\n }, [widget]);\n\n // v2.0: Determine if we should use the factory (non-BIGCONSOLE renderer)\n const useRendererFactory = useMemo(() => {\n const shouldUse = widgetRenderer && widgetRenderer !== 'BIGCONSOLE';\n return shouldUse;\n }, [widgetRenderer]);\n\n // v2.0: Create action context\n const actionContext = useMemo<ActionContext>(\n () => ({\n widgetId: widget.id,\n data: widgetData || {},\n }),\n [widget.id, widgetData]\n );\n\n // v2.0: Handle action callback from WidgetRendererFactory\n // Receives (action: WidgetAction, context: ActionContext) and forwards to parent\n // Checks if a more detailed backend action exists with same name\n const handleActionFromFactory = useCallback(\n (action: WidgetAction, context: ActionContext) => {\n if (!onAction) return;\n\n // Try to find a matching backend action by name (has more details like confirmation)\n const existingAction = actions?.find((a) => a.name === action.name);\n\n if (existingAction) {\n // Use existing backend action (has confirmation, style, icon, etc.)\n onAction(existingAction, context);\n } else {\n // Use the action from WidgetRendererFactory as-is\n onAction(action, context);\n }\n },\n [onAction, actions]\n );\n\n // Render the appropriate widget component based on type (v1.0 snake_case)\n const renderWidgetComponent = useCallback(() => {\n // Extract data based on widget type expectations\n // Different widgets expect different data formats\n const getDataForWidget = () => {\n if (!widgetData) return widgetData;\n\n // If data has 'rows' property (table data), extract rows\n if (widget.type === 'table' || widget.type === 'pivot_table') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'rows' in widgetData) {\n return (widgetData as { rows: unknown[] }).rows;\n }\n }\n\n // If data has 'items' property (list data), extract items\n if (widget.type === 'list') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'items' in widgetData) {\n return (widgetData as { items: unknown[] }).items;\n }\n }\n\n // If data has 'stages' property (old funnel format), extract stages\n if (widget.type === 'funnel_chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'stages' in widgetData) {\n return (widgetData as { stages: unknown[] }).stages;\n }\n }\n\n // If data has 'datasets' property (old chart format), convert to array format\n if (widget.type === 'chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'datasets' in widgetData) {\n // Convert old format to new array format\n const oldData = widgetData as { labels?: string[]; datasets?: { name: string; data: number[] }[] };\n if (oldData.labels && oldData.datasets) {\n return oldData.labels.map((label, i) => {\n const point: Record<string, unknown> = { name: label };\n oldData.datasets?.forEach((ds) => {\n point[ds.name.toLowerCase()] = ds.data[i];\n });\n return point;\n });\n }\n }\n }\n\n // For text widgets: convert parsed object data to {content, format} shape\n if (widget.type === 'text') {\n if (widgetData && typeof widgetData === 'object' && !('content' in (widgetData as Record<string, unknown>))) {\n // Auto-format parsed data as markdown content\n const obj = widgetData as Record<string, unknown>;\n const lines: string[] = [];\n for (const [key, val] of Object.entries(obj)) {\n if (val && typeof val === 'object' && !Array.isArray(val)) {\n lines.push(`### ${key}`);\n for (const [k2, v2] of Object.entries(val as Record<string, unknown>)) {\n lines.push(`- **${k2}:** ${v2}`);\n }\n } else if (Array.isArray(val)) {\n lines.push(`### ${key}`);\n val.slice(0, 10).forEach((item: unknown) => {\n if (item && typeof item === 'object') {\n const entries = Object.entries(item as Record<string, unknown>);\n lines.push(`- ${entries.map(([k, v]) => `**${k}:** ${v}`).join(' | ')}`);\n } else {\n lines.push(`- ${item}`);\n }\n });\n } else {\n lines.push(`- **${key}:** ${val}`);\n }\n }\n return { content: lines.join('\\n'), format: 'markdown' };\n }\n }\n\n // For custom widgets: pass data through with extracted value if configured\n if (widget.type === 'custom') {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && widgetData && typeof widgetData === 'object' && !Array.isArray(widgetData)) {\n const obj = widgetData as Record<string, unknown>;\n if (valueField in obj) {\n return { value: obj[valueField], label: widget.title, data: obj };\n }\n }\n }\n\n return widgetData;\n };\n\n const normalizedData = getDataForWidget();\n const props = {\n widget,\n data: normalizedData,\n onDrilldown: hasDrilldown ? handleDrilldown : undefined,\n };\n\n // v2.0: Use WidgetRendererFactory for non-BIGCONSOLE renderers\n if (useRendererFactory) {\n // For factory renderers (Adaptive Card, BlockKit, Custom), pass the RAW\n // processed data — NOT the type-normalized data. Normalization (e.g., text→markdown)\n // is only for BIGCONSOLE renderer widgets. Factory renderers handle their own\n // template interpolation against the raw data object.\n let factoryData: Record<string, unknown> = {};\n\n if (\n realWidgetData &&\n typeof realWidgetData === 'object' &&\n !Array.isArray(realWidgetData) &&\n Object.keys(realWidgetData as Record<string, unknown>).length > 0\n ) {\n // Use real data from backend (raw processed, not type-normalized)\n factoryData = { ...(realWidgetData as Record<string, unknown>) };\n }\n\n // For CUSTOM renderer: enrich data with standard fields expected by built-in components.\n // Built-in components (Counter, Card, SimpleText) read specific fields from data:\n // Counter: data.value, Card: data.title/data.description, SimpleText: data.text\n // Parsed data from datasinks has domain-specific fields (totalFiles, totalMessages, etc.)\n // Use config.valueField to extract the primary value and map it to standard fields.\n const internalType = (widget.config as Record<string, unknown> | undefined)?._internalWidgetType as\n string | undefined;\n if (internalType === 'custom' && Object.keys(factoryData).length > 0) {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && valueField in factoryData) {\n const val = factoryData[valueField];\n const formatted = typeof val === 'number' ? val.toLocaleString() : String(val);\n // Map to standard fields for all built-in custom components\n factoryData = {\n ...factoryData,\n value: val, // Counter component\n title: factoryData.title || formatted, // Card component\n description: factoryData.description || widget.title || '', // Card component\n text: factoryData.text || formatted, // SimpleText component\n };\n }\n }\n\n return (\n <WidgetRendererFactory\n widget={widget}\n data={factoryData}\n isEditMode={isEditMode}\n isSelected={isSelected}\n isLoading={isLoading}\n error={error}\n onDrilldown={hasDrilldown ? handleDrilldown : undefined}\n onAction={handleActionFromFactory}\n />\n );\n }\n\n // v1.0: Use switch-case for BIGCONSOLE renderer (default)\n // Normalize widget type to lowercase for case-insensitive matching\n const normalizedType = widget.type?.toLowerCase() || '';\n switch (normalizedType) {\n case 'metric_card':\n return <MetricCardWidget {...props} />;\n case 'kpi_card_comparison':\n return <KPIComparisonWidget {...props} />;\n case 'chart':\n case 'funnel_chart':\n return <ChartWidget {...props} />;\n case 'table':\n return <TableWidget {...props} />;\n case 'pivot_table':\n return <PivotTableWidget {...props} />;\n case 'gauge':\n return <GaugeWidget {...props} />;\n case 'progress':\n return <ProgressWidget {...props} />;\n case 'list':\n return <ListWidget {...props} />;\n case 'heatmap':\n return <HeatmapWidget {...props} />;\n case 'retention':\n return <RetentionWidget {...props} />;\n case 'calendar':\n return <CalendarWidget {...props} />;\n case 'timeline':\n return <TimelineWidget {...props} />;\n case 'kanban':\n return <KanbanWidget {...props} />;\n case 'form':\n return <AdaptiveCardWidget {...props} />;\n case 'text':\n return <TextWidget {...props} />;\n case 'iframe':\n return <IframeWidget {...props} />;\n case 'map':\n return <MapWidget {...props} />;\n case 'custom':\n return <CustomWidget {...props} />;\n default:\n return children || <WidgetEmptyState widgetType={widget.type} />;\n }\n }, [\n widget,\n widgetData,\n children,\n hasDrilldown,\n handleDrilldown,\n useRendererFactory,\n isEditMode,\n isSelected,\n isLoading,\n error,\n handleActionFromFactory,\n realWidgetData,\n ]);\n\n // Determine content to render\n const renderContent = () => {\n if (isLoading) {\n return <WidgetLoadingState />;\n }\n\n if (error) {\n return <WidgetErrorState error={error} onRetry={onRefresh} />;\n }\n\n if (isEmpty && !children) {\n // v2.0: Check parserId (primary) or dataSinkId (for context/drilldowns)\n return (\n <WidgetEmptyState\n widgetType={widget.type}\n hasDataSource={hasDataSource}\n templateSlot={\n typeof widget.metadata?.['templateDataSinkSlot'] === 'string'\n ? (widget.metadata['templateDataSinkSlot'] as string)\n : null\n }\n />\n );\n }\n\n // resetKey auto-resets the boundary on parser/sink re-bind or config edit without widget.id changing (BOFF-2242).\n const resetKey = `${widgetBinding.source.kind}|${widgetBinding.source.ref ?? ''}|${widgetBinding.parserId ?? ''}|${widget.updatedAt ?? ''}`;\n\n return (\n <WidgetErrorBoundary widgetId={widget.id} widgetType={widget.type} widgetTitle={widget.title} resetKey={resetKey}>\n {children ?? <Suspense fallback={<WidgetLoadingState />}>{renderWidgetComponent()}</Suspense>}\n </WidgetErrorBoundary>\n );\n };\n\n return (\n <div\n ref={containerRef}\n className={`\n group relative flex flex-col\n h-full w-full\n rounded-lg border\n bg-bg-surface\n transition-all duration-200\n ${\n isSelected && isEditMode\n ? 'border-action-primary-border ring-2 ring-action-primary-border/30'\n : 'border-border-default hover:border-action-primary-bg'\n }\n ${isEditMode ? 'cursor-pointer' : ''}\n ${className}\n `}\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n tabIndex={isEditMode ? 0 : -1}\n role={isEditMode ? 'button' : undefined}\n aria-selected={isSelected}\n aria-label={`Widget: ${widget.title}`}\n data-widget-id={widget.id}\n data-widget-type={widget.type}\n >\n {/* Header */}\n <WidgetHeader\n widget={widget}\n definition={definition}\n isSelected={isSelected}\n isEditMode={isEditMode}\n isLoading={isLoading}\n timeUntilRefresh={widget.refreshInterval && !isPaused ? formatTimeUntilRefresh(timeUntilRefresh) : undefined}\n hasDrilldown={hasDrilldown}\n onRefresh={onRefresh}\n onExportCSV={exportData ? exportCSV : undefined}\n onExportPNG={exportPNG}\n onDrilldown={hasDrilldown ? () => handleDrilldown() : undefined}\n onToggleComments={toggleComments}\n commentsOpen={showComments}\n commentCount={commentsEverOpened ? commentCount : undefined}\n />\n\n {/* Content Area */}\n <div\n className={`\n flex-1 overflow-hidden p-4 min-h-0\n ${showContainerDrilldownHints && !isEditMode ? 'cursor-pointer' : ''}\n `}\n title={showContainerDrilldownHints && !isEditMode ? 'Click data to drill down' : undefined}\n >\n <div className=\"h-full overflow-auto\">{renderContent()}</div>\n </div>\n\n {/* v2.0: Widget Actions */}\n {actions && actions.length > 0 && !isLoading && !error && (\n <div className=\"px-4 py-3 border-t border-border-default bg-bg-sunken\" style={{ flexShrink: 0 }}>\n <WidgetActions\n actions={actions}\n context={actionContext}\n onAction={onAction}\n disabled={isEditMode}\n layout=\"horizontal\"\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Edit mode: react-grid-layout provides resize handles */}\n\n {/* Drilldown Indicator */}\n {showContainerDrilldownHints && !isEditMode && (\n <div\n className=\"\n absolute bottom-2 right-2\n opacity-0 group-hover:opacity-100\n transition-opacity duration-200\n \"\n title=\"Click to drill down\"\n >\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 7l5 5m0 0l-5 5m5-5H6\" />\n </svg>\n </div>\n )}\n\n {/* Comments panel (overlay) */}\n {showComments && (\n <Suspense fallback={null}>\n <WidgetComments widgetId={widget.id} onClose={() => setShowComments(false)} />\n </Suspense>\n )}\n </div>\n );\n}, arePropsEqual);\n\nexport default WidgetWrapper;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WidgetWrapper.js","names":[],"sources":["../../../../src/bigconsole/components/widgets/WidgetWrapper.tsx"],"sourcesContent":["/**\n * WidgetWrapper Component (v2.1)\n *\n * Common wrapper for all widget types. Handles:\n * - Header with title and menu\n * - Loading, error, and empty states\n * - Selection highlighting\n * - Edit mode controls\n * - Real data only — no mock fallback; widgets show empty state when data is missing\n * - v2.0: Multi-renderer support via WidgetRendererFactory\n * - v2.0: Widget actions support\n */\n\nimport {\n type FC,\n type ReactNode,\n type RefObject,\n useCallback,\n memo,\n useMemo,\n useRef,\n useState,\n useEffect,\n lazy,\n Suspense,\n} from 'react';\nimport type { Widget } from '../../types';\nimport {\n useWidgetStore,\n useDashboardStore,\n useIsWidgetSelected,\n useWidgetUIState,\n useGlobalFilterValues,\n} from '../../store';\nimport {\n useWidgetAutoRefresh,\n formatTimeUntilRefresh,\n useWidgetExport,\n useDrilldown,\n useWidgetData,\n useWidgetComments,\n type DrilldownContext,\n} from '../../hooks';\nimport { useApolloClient } from '@apollo/client/react';\nimport { GetDashboardDocument } from '../../../generated/wspace-operations';\nimport { getWidgetDefinition } from './WidgetRegistry';\nimport { WidgetHeader } from './WidgetHeader';\nimport { WidgetLoadingState } from './states/LoadingState';\nimport { WidgetErrorState } from './states/ErrorState';\nimport { WidgetEmptyState } from './states/EmptyState';\nimport { WidgetErrorBoundary } from './WidgetErrorBoundary';\nimport { hasRenderableCustomConfig, hasCustomIframeConfig } from './custom/config';\nimport { getBindingForWidget } from '../../utils/widgetBinding';\n\n// v2.0: Renderer factory and actions\nimport { WidgetRendererFactory } from '../../renderers/factory/WidgetRendererFactory';\nimport { WidgetActions } from './widget-actions';\nimport type { WidgetAction, ActionContext, ActionResult, RendererType } from '../../renderers/types/renderer';\n// Widget components - lazy loaded for better code splitting\n// Each widget loads only when its type is rendered\nconst MetricCardWidget = lazy(() => import('./metric-card').then((m) => ({ default: m.MetricCardWidget })));\nconst ChartWidget = lazy(() => import('./chart').then((m) => ({ default: m.ChartWidget })));\nconst TableWidget = lazy(() => import('./table').then((m) => ({ default: m.TableWidget })));\nconst PivotTableWidget = lazy(() => import('./pivot-table').then((m) => ({ default: m.PivotTableWidget })));\nconst GaugeWidget = lazy(() => import('./gauge').then((m) => ({ default: m.GaugeWidget })));\nconst ProgressWidget = lazy(() => import('./progress').then((m) => ({ default: m.ProgressWidget })));\nconst ListWidget = lazy(() => import('./list').then((m) => ({ default: m.ListWidget })));\nconst HeatmapWidget = lazy(() => import('./heatmap').then((m) => ({ default: m.HeatmapWidget })));\nconst RetentionWidget = lazy(() => import('./retention').then((m) => ({ default: m.RetentionWidget })));\nconst CalendarWidget = lazy(() => import('./calendar').then((m) => ({ default: m.CalendarWidget })));\nconst KanbanWidget = lazy(() => import('./kanban').then((m) => ({ default: m.KanbanWidget })));\nconst TimelineWidget = lazy(() => import('./timeline').then((m) => ({ default: m.TimelineWidget })));\nconst KPIComparisonWidget = lazy(() => import('./kpi-comparison').then((m) => ({ default: m.KPIComparisonWidget })));\nconst TextWidget = lazy(() => import('./text-widget').then((m) => ({ default: m.TextWidget })));\nconst MapWidget = lazy(() => import('./map-widget').then((m) => ({ default: m.MapWidget })));\nconst IframeWidget = lazy(() => import('./iframe-widget').then((m) => ({ default: m.IframeWidget })));\nconst CustomWidget = lazy(() => import('./custom').then((m) => ({ default: m.CustomWidget })));\n// AdaptiveCardWidget is HUGE (984KB) - lazy load only when 'form' type is used\nconst AdaptiveCardWidget = lazy(() => import('./adaptive-card').then((m) => ({ default: m.AdaptiveCardWidget })));\n// Comments panel - lazy loaded; only mounts when the user opens comments\nconst WidgetComments = lazy(() => import('./comments/WidgetComments').then((m) => ({ default: m.WidgetComments })));\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WidgetWrapperProps {\n /** The widget data */\n widget: Widget;\n /** Widget content to render */\n children?: ReactNode;\n /** Data for the widget */\n data?: Record<string, unknown> | null;\n /** Custom class name */\n className?: string;\n /** Whether the widget is loading */\n isLoading?: boolean;\n /** Error message */\n error?: string | null;\n /** Callback to refresh widget data */\n onRefresh?: () => Promise<void>;\n /** Callback for drilldown navigation */\n onDrilldown?: (params: Record<string, unknown>) => void;\n /** v2.0: Widget actions */\n actions?: WidgetAction[];\n /** v2.0: Callback for action events */\n onAction?: (action: WidgetAction, context: ActionContext) => Promise<ActionResult | void>;\n /** Phase 2: Global filter values to apply to widget data */\n filterValues?: Record<string, unknown>;\n}\n\n// ============================================================================\n// Custom comparison for memo - ensures actions changes trigger re-render\n// ============================================================================\n\nfunction arePropsEqual(prevProps: WidgetWrapperProps, nextProps: WidgetWrapperProps): boolean {\n // Always re-render if widget ID changes\n if (prevProps.widget.id !== nextProps.widget.id) return false;\n // Re-render if widget was updated\n if (prevProps.widget.updatedAt !== nextProps.widget.updatedAt) return false;\n // Re-render if loading state changes\n if (prevProps.isLoading !== nextProps.isLoading) return false;\n // Re-render if error changes\n if (prevProps.error !== nextProps.error) return false;\n // Re-render if actions count changes\n if ((prevProps.actions?.length ?? 0) !== (nextProps.actions?.length ?? 0)) return false;\n // Check if any action ID changed\n if (prevProps.actions && nextProps.actions) {\n for (let i = 0; i < prevProps.actions.length; i++) {\n if (prevProps.actions[i]?.id !== nextProps.actions[i]?.id) return false;\n }\n }\n // Also check data changes\n if (prevProps.data !== nextProps.data) return false;\n // Phase 2: Re-render if filter values changed\n if (JSON.stringify(prevProps.filterValues) !== JSON.stringify(nextProps.filterValues)) return false;\n return true;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const WidgetWrapper: FC<WidgetWrapperProps> = memo(function WidgetWrapper({\n widget,\n children,\n data,\n className = '',\n isLoading: isLoadingProp,\n error: errorProp,\n onRefresh,\n onDrilldown,\n actions,\n onAction,\n filterValues,\n}) {\n // Store state - use Dashboard store for edit mode (synced with toolbar)\n const isEditMode = useDashboardStore((state) => state.viewMode === 'edit');\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n const isSelected = useIsWidgetSelected(widget.id);\n const uiState = useWidgetUIState(widget.id);\n const globalFilterValues = useGlobalFilterValues();\n const apolloClient = useApolloClient();\n\n // Map a global-filter's id -> the data field it targets (`config.field`).\n // Global CATEGORY/TEXT filter values are keyed in the store by filter id (a\n // UUID), not by the data column they filter. Without this map, applyClientFilters\n // can only fall back to value/heuristic matching, which fails to narrow rows\n // whose targeted column (e.g. `country`, `cohort`) isn't in the hardcoded\n // heuristic list — so a selected Country/Cohort would not actually filter the\n // widget. We resolve the targeted field from the dashboard's globalFilters\n // (already in the Apollo cache from the page load) so a UUID-keyed filter\n // matches the row's configured field exactly.\n const widgetDashboardId = (widget as unknown as Record<string, unknown>).dashboardId as string | undefined;\n const filterFieldMap = useMemo<Record<string, string>>(() => {\n if (!widgetDashboardId) return {};\n try {\n const cached = apolloClient.readQuery<{\n getDashboard?: {\n globalFilters?: Array<{ id: string; config?: Record<string, unknown> | null }>;\n };\n }>({ query: GetDashboardDocument, variables: { id: widgetDashboardId } });\n const map: Record<string, string> = {};\n for (const f of cached?.getDashboard?.globalFilters ?? []) {\n const field = f.config?.field;\n if (typeof field === 'string' && field) map[f.id] = field;\n }\n return map;\n } catch {\n return {};\n }\n }, [apolloClient, widgetDashboardId]);\n\n // Fetch per-widget data from backend (parser-processed data)\n // This is needed because listWidgetsByDashboard uses WidgetListFields (without data)\n // for performance, so each widget fetches its own data individually\n const widgetBinding = useMemo(() => getBindingForWidget(widget), [widget]);\n const hasDataSource =\n widgetBinding.source.kind !== 'DATASINK' ||\n !!widgetBinding.source.ref ||\n !!widgetBinding.parserId ||\n !!widget.datasetId;\n const { data: fetchedWidgetData, loading: widgetDataLoading } = useWidgetData(widget.id, {\n skip: !hasDataSource,\n pollInterval: (widget.refreshInterval ?? 0) * 1000 || 0,\n });\n\n // WORKAROUND: If Zustand store is empty, directly read filter_* params from URL\n // This handles race conditions where the store hasn't been populated yet\n const urlFilterValues = useMemo(() => {\n if (typeof window === 'undefined') return {};\n\n const params = new URLSearchParams(window.location.search);\n const filters: Record<string, unknown> = {};\n\n params.forEach((value, key) => {\n if (key.startsWith('filter_')) {\n const filterId = key.slice('filter_'.length);\n // Skip internal metadata keys\n if (!filterId.startsWith('__')) {\n filters[filterId] = value;\n }\n }\n });\n\n return filters;\n }, []);\n\n // Phase 2: Merge global filter values (from URL) with explicit filterValues prop\n // This ensures both URL params (filter_*) and drilldown params work together\n const mergedFilterValues = useMemo(() => {\n const merged: Record<string, unknown> = {};\n\n // First, add URL filter values as fallback (in case Zustand store hasn't loaded yet)\n if (urlFilterValues && Object.keys(urlFilterValues).length > 0) {\n Object.assign(merged, urlFilterValues);\n }\n\n // Then, add global filter values from Zustand store (if populated)\n if (globalFilterValues && Object.keys(globalFilterValues).length > 0) {\n Object.assign(merged, globalFilterValues);\n }\n\n // Finally, add/override with explicit filterValues prop (e.g., from drilldown modal)\n if (filterValues && Object.keys(filterValues).length > 0) {\n Object.assign(merged, filterValues);\n }\n\n return merged;\n }, [urlFilterValues, globalFilterValues, filterValues]);\n\n // Track active filters (client-side filtering until backend supports it)\n const hasFilters = Object.keys(mergedFilterValues).length > 0;\n\n // Merge loading/error from props and store\n // Show loading if widget data is being fetched for the first time (no data yet)\n const isLoading = isLoadingProp ?? (widgetDataLoading && !fetchedWidgetData) ?? uiState.isLoading;\n const error = errorProp ?? uiState.errorMessage;\n\n // Get widget definition\n const definition = getWidgetDefinition(widget.type);\n\n // Stable refresh callback - use ref to avoid creating new function on each render\n const onRefreshRef = useRef(onRefresh);\n useEffect(() => {\n onRefreshRef.current = onRefresh;\n }, [onRefresh]);\n\n const stableOnRefresh = useCallback(async () => {\n if (onRefreshRef.current) {\n await onRefreshRef.current();\n }\n }, []);\n\n // Auto-refresh\n const { timeUntilRefresh, isPaused } = useWidgetAutoRefresh({\n widgetId: widget.id,\n refreshInterval: widget.refreshInterval,\n onRefresh: stableOnRefresh,\n enabled: !isEditMode && !!widget.refreshInterval,\n });\n\n // Container ref for PNG export\n const containerRef = useRef<HTMLDivElement>(null);\n\n // Comments panel. The count query stays disabled until the panel is first\n // opened, so a dashboard of N widgets doesn't fire N comment queries on load.\n const [showComments, setShowComments] = useState(false);\n const [commentsEverOpened, setCommentsEverOpened] = useState(false);\n const { count: commentCount } = useWidgetComments(widget.id, commentsEverOpened);\n const toggleComments = useCallback(() => {\n setShowComments((open) => {\n const next = !open;\n if (next) setCommentsEverOpened(true);\n return next;\n });\n }, []);\n\n // Get widget data for export (computed early for hook)\n const exportData = useMemo(() => {\n if (data && Array.isArray(data)) return data;\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && Array.isArray(rawData.processed)) {\n return rawData.processed;\n }\n if (rawData?.raw) {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (Array.isArray(parsed)) return parsed;\n }\n return null;\n }, [data, widget]);\n\n // Export functionality\n const { exportCSV, exportPNG } = useWidgetExport(widget, {\n data: exportData,\n containerRef: containerRef as unknown as RefObject<HTMLElement>,\n });\n\n // Helper to check if data is actually usable (has real content)\n const isUsableData = useCallback((d: unknown): boolean => {\n if (d === null || d === undefined) return false;\n if (Array.isArray(d)) return d.length > 0;\n if (typeof d === 'object') {\n const obj = d as Record<string, unknown>;\n const keys = Object.keys(obj);\n if (keys.length === 0) return false;\n // Check recursively for actual data content\n return keys.some((key) => {\n const val = obj[key];\n if (val === null || val === undefined || val === '') return false;\n if (Array.isArray(val)) return val.length > 0;\n if (typeof val === 'object') return isUsableData(val);\n return true;\n });\n }\n return true;\n }, []);\n\n // Phase 2: Client-side filter function for widget data\n const applyClientFilters = useCallback(\n (\n widgetData: Record<string, unknown> | Record<string, unknown>[]\n ): Record<string, unknown> | Record<string, unknown>[] => {\n if (!hasFilters || !widgetData) return widgetData;\n\n // Separate drilldown context filters (ctx_*) from regular filters\n const contextFilters: Record<string, unknown> = {};\n const regularFilters: Record<string, unknown> = {};\n\n Object.entries(mergedFilterValues || {}).forEach(([key, value]) => {\n if (key.startsWith('ctx_')) {\n // Strip ctx_ prefix to get the target field name\n const fieldName = key.slice(4); // Remove 'ctx_'\n contextFilters[fieldName] = value;\n } else if (!key.startsWith('__')) {\n // Skip internal metadata keys (__drilldown_depth, etc.)\n regularFilters[key] = value;\n }\n });\n\n // Helper to match a value against a filter (case-insensitive)\n const matchesFilter = (itemValue: unknown, filterValue: unknown): boolean => {\n if (filterValue === undefined || filterValue === null || filterValue === '') return true;\n if (itemValue === undefined || itemValue === null) return false;\n\n const itemStr = String(itemValue).toLowerCase();\n const filterStr = String(filterValue).toLowerCase();\n\n // Exact match or contains\n return itemStr === filterStr || itemStr.includes(filterStr);\n };\n\n // Helper to find a matching field in the item (case-insensitive key matching)\n const findFieldValue = (item: Record<string, unknown>, targetField: string): unknown => {\n const targetLower = targetField.toLowerCase();\n\n // Try exact match first\n if (item[targetField] !== undefined) return item[targetField];\n\n // Try case-insensitive match\n const matchingKey = Object.keys(item).find((k) => k.toLowerCase() === targetLower);\n if (matchingKey) return item[matchingKey];\n\n // Try partial match (e.g., 'region' matches 'regionName' or 'user_region')\n const partialMatch = Object.keys(item).find(\n (k) => k.toLowerCase().includes(targetLower) || targetLower.includes(k.toLowerCase())\n );\n if (partialMatch) return item[partialMatch];\n\n // Try stripping common prefixes from target field (e.g., 'kpiId' -> 'id', 'rowName' -> 'name')\n // This handles drilldown context where widget passes 'kpiId' but data has 'id'\n const commonPrefixes = ['kpi', 'row', 'item', 'data', 'cell', 'record', 'entry'];\n for (const prefix of commonPrefixes) {\n if (targetLower.startsWith(prefix) && targetLower.length > prefix.length) {\n const strippedField = targetLower.slice(prefix.length);\n // Try exact match with stripped field\n const strippedMatch = Object.keys(item).find((k) => k.toLowerCase() === strippedField);\n if (strippedMatch) return item[strippedMatch];\n }\n }\n\n return undefined;\n };\n\n // Helper function to filter an array of items\n const filterItems = (items: Record<string, unknown>[]): Record<string, unknown>[] => {\n return items.filter((item: Record<string, unknown>) => {\n // Only object rows are field-filterable. An array of primitives (e.g.\n // a heatmap's `rows: [\"5W-MedSurg\", …]`) carries no named fields — keep\n // every element. Without this, `findFieldValue` indexes into the string\n // by character offset and spuriously \"matches\" a filter, dropping rows.\n if (item === null || typeof item !== 'object' || Array.isArray(item)) {\n return true;\n }\n // First, apply drilldown context filters (ctx_*) - these have explicit field names\n for (const [fieldName, filterValue] of Object.entries(contextFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n const itemValue = findFieldValue(item, fieldName);\n // If the field doesn't exist in this data item, skip this context filter.\n // Drilldown context params (e.g., source=support_status) are metadata about\n // the drilldown origin and should NOT filter out data when the target\n // dashboard's widgets don't have a matching field.\n if (itemValue === undefined) continue;\n if (!matchesFilter(itemValue, filterValue)) {\n return false;\n }\n }\n\n // Then, apply regular filters\n for (const [filterKey, filterValue] of Object.entries(regularFilters)) {\n if (filterValue === undefined || filterValue === null || filterValue === '') continue;\n\n // For date range filters\n if (typeof filterValue === 'object') {\n const fv = filterValue as Record<string, unknown>;\n if (fv.start || fv.end) {\n const dateFields = Object.entries(item).filter(\n ([k, _v]) => k.toLowerCase().includes('date') || k.toLowerCase().includes('time')\n );\n if (dateFields.length > 0) {\n const itemDate = new Date(dateFields[0][1] as string);\n if (fv.start && itemDate < new Date(fv.start as string)) return false;\n if (fv.end && itemDate > new Date(fv.end as string)) return false;\n }\n continue;\n }\n }\n\n // For numeric range filters\n if (typeof filterValue === 'object') {\n const nv = filterValue as Record<string, unknown>;\n if (nv.min !== undefined || nv.max !== undefined) {\n const numericFields = Object.entries(item).filter(([_k, v]) => typeof v === 'number');\n if (numericFields.length > 0) {\n const itemNum = numericFields[0][1] as number;\n if (nv.min !== undefined && itemNum < (nv.min as number)) return false;\n if (nv.max !== undefined && itemNum > (nv.max as number)) return false;\n }\n continue;\n }\n }\n\n // Any other object-shaped filter value (e.g. a DATE_RANGE filter set\n // to a preset like { preset: \"last_30_days\" } with no resolved\n // start/end, or a structured value we don't understand) cannot be\n // compared with the scalar matchers below. Passing it through would\n // stringify it to \"[object Object]\" and, via fuzzy field matching\n // (e.g. an `enrolledAt` filter partial-matching an `enrolled` number\n // column), spuriously exclude EVERY row. Treat such values as a no-op\n // for client-side filtering — the row set is unchanged until the\n // filter resolves to concrete start/end or scalar values.\n if (filterValue !== null && typeof filterValue === 'object' && !Array.isArray(filterValue)) {\n continue;\n }\n\n // Global CATEGORY/TEXT filters are keyed by filter id (UUID). Resolve\n // the data column they target from the filter's config.field. When the\n // widget's rows carry that column, the filter is authoritative — a\n // mismatch excludes the row; a missing column is a no-op (the filter\n // simply doesn't apply to this widget's shape). This is what makes a\n // selected Country/Cohort actually narrow the visible data.\n const targetField = filterFieldMap[filterKey];\n if (targetField) {\n const mappedValue = findFieldValue(item, targetField);\n if (mappedValue !== undefined) {\n if (!matchesFilter(mappedValue, filterValue)) {\n return false;\n }\n continue;\n }\n // Configured field absent from this widget's rows → filter is a\n // no-op for this widget (do not fall through to value heuristics,\n // which could spuriously match an unrelated column).\n continue;\n }\n\n // Try to match by filter key name first (for named filters). When\n // the widget's data actually carries the filter's field, the filter\n // is authoritative — a mismatch excludes the row.\n const fieldValue = findFieldValue(item, filterKey);\n if (fieldValue !== undefined) {\n if (!matchesFilter(fieldValue, filterValue)) {\n return false;\n }\n continue;\n }\n\n // For text/category filters - try multiple matching strategies\n const filterStr = String(filterValue).toLowerCase();\n\n // Strategy 1: Check all field values for exact or partial match\n const itemValues = Object.values(item).map((v) => String(v).toLowerCase());\n const hasMatch = itemValues.some((v) => v === filterStr || v.includes(filterStr));\n\n if (!hasMatch) {\n // Strategy 2: Try to find a field name that matches the filter value context\n const fieldNames = Object.keys(item).map((k) => k.toLowerCase());\n const possibleFields = fieldNames.filter(\n (f) =>\n f.includes('category') ||\n f.includes('type') ||\n f.includes('status') ||\n f.includes('name') ||\n f.includes('region') ||\n f.includes('group')\n );\n\n let fieldMatch = false;\n for (const fieldName of possibleFields) {\n const originalKey = Object.keys(item).find((k) => k.toLowerCase() === fieldName);\n if (originalKey) {\n const itemFieldValue = String(item[originalKey]).toLowerCase();\n if (itemFieldValue === filterStr || itemFieldValue.includes(filterStr)) {\n fieldMatch = true;\n break;\n }\n }\n }\n\n // The filter's field is NOT present in this dataset (named-field\n // lookup failed AND no value/heuristic match). A global filter that\n // doesn't apply to a widget's shape must be a no-op for that widget\n // — not a silent \"exclude every row\". This mirrors the ctx_* rule\n // above (skip when the targeted field is absent). Without this, an\n // always-active required filter (e.g. campus=main) blanks every\n // parser-derived widget whose output has no campus column.\n if (!fieldMatch) {\n continue;\n }\n }\n }\n return true;\n });\n };\n\n // If data is an array, filter the items directly\n if (Array.isArray(widgetData)) {\n const filtered = filterItems(widgetData);\n return filtered;\n }\n\n // Handle object with array properties (rows, items, members, questions, data, etc.)\n if (widgetData && typeof widgetData === 'object') {\n // Common array property names in widget data\n const arrayPropertyNames = [\n 'kpis', // KPI Comparison widget\n 'rows', // Table widget\n 'items', // List widget\n 'members', // Team/member widgets\n 'questions', // Form widget\n 'data', // Generic data\n 'records', // Database-style data\n 'results', // Search/query results\n 'entries', // Log/feed entries\n 'list', // Generic list\n 'values', // Value arrays\n 'series', // Chart series\n 'datasets', // Chart datasets\n ];\n\n for (const propName of arrayPropertyNames) {\n if (widgetData[propName] && Array.isArray(widgetData[propName])) {\n const filteredArray = filterItems(widgetData[propName]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n ...(propName === 'rows' ? { totalRows: filteredArray.length } : {}),\n };\n }\n }\n\n // If no known array property found, check for any array property\n const arrayProps = Object.entries(widgetData).filter(([_key, v]) => Array.isArray(v));\n if (arrayProps.length > 0) {\n const [propName, propValue] = arrayProps[0];\n const filteredArray = filterItems(propValue as Record<string, unknown>[]);\n return {\n ...widgetData,\n [propName]: filteredArray,\n };\n }\n }\n\n // For non-filterable data, return as-is\n return widgetData;\n },\n [hasFilters, mergedFilterValues, widget.id, filterFieldMap]\n );\n\n // Get widget data from the widget itself (fetched from backend)\n const realWidgetData = useMemo(() => {\n let rawWidgetData = null;\n\n // Priority 1: Explicitly passed data prop\n if (data && isUsableData(data)) {\n rawWidgetData = data;\n }\n // Priority 2: Per-widget fetched data (from useWidgetData hook)\n // fetchedWidgetData IS a WidgetData object: { raw, processed, fetchedAt, cached }\n else if (fetchedWidgetData) {\n const fetched = fetchedWidgetData as unknown as Record<string, unknown>;\n if (fetched?.processed && isUsableData(fetched.processed)) {\n rawWidgetData = fetched.processed;\n } else if (fetched?.raw) {\n try {\n const parsed = typeof fetched.raw === 'string' ? JSON.parse(fetched.raw as string) : fetched.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n // Priority 3: widget.data from GraphQL response (if already on widget object)\n else {\n const rawData = (widget as unknown as Record<string, unknown>).data as Record<string, unknown> | undefined;\n if (rawData?.processed && isUsableData(rawData.processed)) {\n rawWidgetData = rawData.processed;\n } else if (rawData?.raw) {\n try {\n const parsed = typeof rawData.raw === 'string' ? JSON.parse(rawData.raw) : rawData.raw;\n if (isUsableData(parsed)) rawWidgetData = parsed;\n } catch {\n // Invalid JSON, ignore\n }\n }\n }\n\n // Priority 4: config.mockData — user-configured sample JSON from Widget Properties Data tab\n // This is NOT auto-generated mock data; it's explicit JSON the user pasted in the UI\n if (!rawWidgetData) {\n const configMockData = (widget.config as Record<string, unknown> | undefined)?.mockData;\n if (configMockData) {\n try {\n const parsed = typeof configMockData === 'string' ? JSON.parse(configMockData) : configMockData;\n if (isUsableData(parsed)) {\n rawWidgetData = parsed;\n }\n } catch (e) {}\n }\n }\n\n // Generic parser-output resolution: a widget may bind to a parser whose\n // output is an envelope ({ byUnit:[...], house:{...} }) rather than the\n // exact array/object the renderer wants. config.dataKey / dataPath /\n // seriesPath is a dot-path INTO the resolved data that points at the\n // renderable subtree (e.g. \"byUnit\" for a table, \"rows\" for a chart).\n // Resolving it here keeps every renderer generic — the use-case-specific\n // shape knowledge lives in the widget config (set by the seed/UI), not in\n // the renderer. valueField (single-value metric path) is intentionally NOT\n // applied here: MetricCardWidget resolves it itself against the envelope.\n if (rawWidgetData && typeof rawWidgetData === 'object' && !Array.isArray(rawWidgetData)) {\n const cfg = widget.config as Record<string, unknown> | undefined;\n const pathKey = (cfg?.dataKey ?? cfg?.dataPath ?? cfg?.seriesPath) as string | undefined;\n if (pathKey && typeof pathKey === 'string') {\n const resolved = pathKey.split('.').reduce<unknown>((cur, seg) => {\n return cur && typeof cur === 'object' ? (cur as Record<string, unknown>)[seg] : undefined;\n }, rawWidgetData);\n if (resolved !== undefined && resolved !== null && isUsableData(resolved)) {\n rawWidgetData = resolved as Record<string, unknown>;\n }\n }\n }\n\n // Phase 2: Apply client-side filters if active\n if (hasFilters && rawWidgetData) {\n const filtered = applyClientFilters(rawWidgetData);\n return filtered;\n }\n\n return rawWidgetData;\n }, [data, widget, fetchedWidgetData, hasFilters, applyClientFilters, mergedFilterValues, isUsableData]);\n\n // Final widget data - real data only, no mock fallback\n const widgetData = useMemo(() => {\n return realWidgetData ?? null;\n }, [realWidgetData]);\n\n // Values rendered from a template's embedded sample rather than a connected\n // source. Only possible when no data source is configured, since a real\n // source outranks config.mockData in the resolution chain above.\n const isSampleData = useMemo(() => {\n if (!widgetData || hasDataSource) return false;\n const mock = (widget.config as Record<string, unknown> | undefined)?.mockData;\n return mock !== undefined && mock !== null;\n }, [widgetData, hasDataSource, widget.config]);\n\n // Drilldown navigation (must be after widgetData is defined)\n const { executeDrilldown, canDrilldown } = useDrilldown({\n onDrilldown: (params, _mode) => {\n onDrilldown?.(params);\n },\n });\n\n // Check if widget has drilldown configured\n const hasDrilldown = useMemo(() => canDrilldown(widget), [widget, canDrilldown]);\n\n // Handle drilldown execution with context\n const handleDrilldown = useCallback(\n (selectedData?: Record<string, unknown>) => {\n if (!widget.drilldown?.enabled) return;\n\n const context: DrilldownContext = {\n selectedData: selectedData || {},\n globalFilters: globalFilterValues || {},\n widgetData: widgetData || {},\n metadata: {\n widgetId: widget.id,\n widgetType: widget.type,\n widgetTitle: widget.title,\n },\n sourceDashboardId: widget.dashboardId,\n };\n\n executeDrilldown(widget.drilldown, context);\n },\n [widget, widgetData, globalFilterValues, executeDrilldown]\n );\n\n // Track mouse position to distinguish click from drag\n const mouseDownPos = useRef<{ x: number; y: number } | null>(null);\n const DRAG_THRESHOLD = 5; // pixels - if mouse moves more than this, it's a drag\n\n // Handle mouse down - record position\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n mouseDownPos.current = { x: e.clientX, y: e.clientY };\n }, []);\n\n // Handle widget click - Edit mode only (View mode drilldown is triggered by data clicks)\n // Only trigger if mouse hasn't moved much (not a drag)\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation(); // Prevent DashboardGrid from also handling\n\n // Check if this was a drag (mouse moved significantly)\n if (mouseDownPos.current) {\n const dx = Math.abs(e.clientX - mouseDownPos.current.x);\n const dy = Math.abs(e.clientY - mouseDownPos.current.y);\n if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {\n // This was a drag, not a click - don't open config panel\n mouseDownPos.current = null;\n return;\n }\n }\n mouseDownPos.current = null;\n\n if (isEditMode) {\n // Edit mode: select widget for editing\n selectWidget(widget.id);\n }\n // View mode: drilldown is triggered by clicking specific data elements\n // (table rows, chart points, etc.) - not by clicking the widget itself\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n // Handle keyboard navigation (Edit mode only)\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n if (isEditMode) {\n selectWidget(widget.id);\n }\n }\n },\n [isEditMode, selectWidget, widget.id]\n );\n\n const hasRenderableCustomWidgetConfig = useMemo(\n () => widget.type === 'custom' && hasRenderableCustomConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const customWidgetHasIframeConfig = useMemo(\n () => widget.type === 'custom' && hasCustomIframeConfig(widget.config),\n [widget.type, widget.config]\n );\n\n const showContainerDrilldownHints = hasDrilldown && !customWidgetHasIframeConfig;\n\n // Determine if data is empty\n const isEmpty = !isLoading && !error && !widgetData && !hasRenderableCustomWidgetConfig;\n\n // v2.0: Check if widget has a renderer specified (non-BIGCONSOLE)\n const widgetRenderer = useMemo(() => {\n const widgetWithRenderer = widget as Widget & { renderer?: RendererType };\n return widgetWithRenderer.renderer;\n }, [widget]);\n\n // v2.0: Determine if we should use the factory (non-BIGCONSOLE renderer)\n const useRendererFactory = useMemo(() => {\n const shouldUse = widgetRenderer && widgetRenderer !== 'BIGCONSOLE';\n return shouldUse;\n }, [widgetRenderer]);\n\n // v2.0: Create action context\n const actionContext = useMemo<ActionContext>(\n () => ({\n widgetId: widget.id,\n data: widgetData || {},\n }),\n [widget.id, widgetData]\n );\n\n // v2.0: Handle action callback from WidgetRendererFactory\n // Receives (action: WidgetAction, context: ActionContext) and forwards to parent\n // Checks if a more detailed backend action exists with same name\n const handleActionFromFactory = useCallback(\n (action: WidgetAction, context: ActionContext) => {\n if (!onAction) return;\n\n // Try to find a matching backend action by name (has more details like confirmation)\n const existingAction = actions?.find((a) => a.name === action.name);\n\n if (existingAction) {\n // Use existing backend action (has confirmation, style, icon, etc.)\n onAction(existingAction, context);\n } else {\n // Use the action from WidgetRendererFactory as-is\n onAction(action, context);\n }\n },\n [onAction, actions]\n );\n\n // Render the appropriate widget component based on type (v1.0 snake_case)\n const renderWidgetComponent = useCallback(() => {\n // Extract data based on widget type expectations\n // Different widgets expect different data formats\n const getDataForWidget = () => {\n if (!widgetData) return widgetData;\n\n // If data has 'rows' property (table data), extract rows\n if (widget.type === 'table' || widget.type === 'pivot_table') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'rows' in widgetData) {\n return (widgetData as { rows: unknown[] }).rows;\n }\n }\n\n // If data has 'items' property (list data), extract items\n if (widget.type === 'list') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'items' in widgetData) {\n return (widgetData as { items: unknown[] }).items;\n }\n }\n\n // If data has 'stages' property (old funnel format), extract stages\n if (widget.type === 'funnel_chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'stages' in widgetData) {\n return (widgetData as { stages: unknown[] }).stages;\n }\n }\n\n // If data has 'datasets' property (old chart format), convert to array format\n if (widget.type === 'chart') {\n if (Array.isArray(widgetData)) return widgetData;\n if (widgetData && typeof widgetData === 'object' && 'datasets' in widgetData) {\n // Convert old format to new array format\n const oldData = widgetData as { labels?: string[]; datasets?: { name: string; data: number[] }[] };\n if (oldData.labels && oldData.datasets) {\n return oldData.labels.map((label, i) => {\n const point: Record<string, unknown> = { name: label };\n oldData.datasets?.forEach((ds) => {\n point[ds.name.toLowerCase()] = ds.data[i];\n });\n return point;\n });\n }\n }\n }\n\n // For text widgets: convert parsed object data to {content, format} shape\n if (widget.type === 'text') {\n if (widgetData && typeof widgetData === 'object' && !('content' in (widgetData as Record<string, unknown>))) {\n // Auto-format parsed data as markdown content\n const obj = widgetData as Record<string, unknown>;\n const lines: string[] = [];\n for (const [key, val] of Object.entries(obj)) {\n if (val && typeof val === 'object' && !Array.isArray(val)) {\n lines.push(`### ${key}`);\n for (const [k2, v2] of Object.entries(val as Record<string, unknown>)) {\n lines.push(`- **${k2}:** ${v2}`);\n }\n } else if (Array.isArray(val)) {\n lines.push(`### ${key}`);\n val.slice(0, 10).forEach((item: unknown) => {\n if (item && typeof item === 'object') {\n const entries = Object.entries(item as Record<string, unknown>);\n lines.push(`- ${entries.map(([k, v]) => `**${k}:** ${v}`).join(' | ')}`);\n } else {\n lines.push(`- ${item}`);\n }\n });\n } else {\n lines.push(`- **${key}:** ${val}`);\n }\n }\n return { content: lines.join('\\n'), format: 'markdown' };\n }\n }\n\n // For custom widgets: pass data through with extracted value if configured\n if (widget.type === 'custom') {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && widgetData && typeof widgetData === 'object' && !Array.isArray(widgetData)) {\n const obj = widgetData as Record<string, unknown>;\n if (valueField in obj) {\n return { value: obj[valueField], label: widget.title, data: obj };\n }\n }\n }\n\n return widgetData;\n };\n\n const normalizedData = getDataForWidget();\n const props = {\n widget,\n data: normalizedData,\n onDrilldown: hasDrilldown ? handleDrilldown : undefined,\n };\n\n // v2.0: Use WidgetRendererFactory for non-BIGCONSOLE renderers\n if (useRendererFactory) {\n // For factory renderers (Adaptive Card, BlockKit, Custom), pass the RAW\n // processed data — NOT the type-normalized data. Normalization (e.g., text→markdown)\n // is only for BIGCONSOLE renderer widgets. Factory renderers handle their own\n // template interpolation against the raw data object.\n let factoryData: Record<string, unknown> = {};\n\n if (\n realWidgetData &&\n typeof realWidgetData === 'object' &&\n !Array.isArray(realWidgetData) &&\n Object.keys(realWidgetData as Record<string, unknown>).length > 0\n ) {\n // Use real data from backend (raw processed, not type-normalized)\n factoryData = { ...(realWidgetData as Record<string, unknown>) };\n }\n\n // For CUSTOM renderer: enrich data with standard fields expected by built-in components.\n // Built-in components (Counter, Card, SimpleText) read specific fields from data:\n // Counter: data.value, Card: data.title/data.description, SimpleText: data.text\n // Parsed data from datasinks has domain-specific fields (totalFiles, totalMessages, etc.)\n // Use config.valueField to extract the primary value and map it to standard fields.\n const internalType = (widget.config as Record<string, unknown> | undefined)?._internalWidgetType as\n string | undefined;\n if (internalType === 'custom' && Object.keys(factoryData).length > 0) {\n const config = widget.config as Record<string, unknown> | undefined;\n const valueField = config?.valueField as string | undefined;\n if (valueField && valueField in factoryData) {\n const val = factoryData[valueField];\n const formatted = typeof val === 'number' ? val.toLocaleString() : String(val);\n // Map to standard fields for all built-in custom components\n factoryData = {\n ...factoryData,\n value: val, // Counter component\n title: factoryData.title || formatted, // Card component\n description: factoryData.description || widget.title || '', // Card component\n text: factoryData.text || formatted, // SimpleText component\n };\n }\n }\n\n return (\n <WidgetRendererFactory\n widget={widget}\n data={factoryData}\n isEditMode={isEditMode}\n isSelected={isSelected}\n isLoading={isLoading}\n error={error}\n onDrilldown={hasDrilldown ? handleDrilldown : undefined}\n onAction={handleActionFromFactory}\n />\n );\n }\n\n // v1.0: Use switch-case for BIGCONSOLE renderer (default)\n // Normalize widget type to lowercase for case-insensitive matching\n const normalizedType = widget.type?.toLowerCase() || '';\n switch (normalizedType) {\n case 'metric_card':\n return <MetricCardWidget {...props} />;\n case 'kpi_card_comparison':\n return <KPIComparisonWidget {...props} />;\n case 'chart':\n case 'funnel_chart':\n return <ChartWidget {...props} />;\n case 'table':\n return <TableWidget {...props} />;\n case 'pivot_table':\n return <PivotTableWidget {...props} />;\n case 'gauge':\n return <GaugeWidget {...props} />;\n case 'progress':\n return <ProgressWidget {...props} />;\n case 'list':\n return <ListWidget {...props} />;\n case 'heatmap':\n return <HeatmapWidget {...props} />;\n case 'retention':\n return <RetentionWidget {...props} />;\n case 'calendar':\n return <CalendarWidget {...props} />;\n case 'timeline':\n return <TimelineWidget {...props} />;\n case 'kanban':\n return <KanbanWidget {...props} />;\n case 'form':\n return <AdaptiveCardWidget {...props} />;\n case 'text':\n return <TextWidget {...props} />;\n case 'iframe':\n return <IframeWidget {...props} />;\n case 'map':\n return <MapWidget {...props} />;\n case 'custom':\n return <CustomWidget {...props} />;\n default:\n return children || <WidgetEmptyState widgetType={widget.type} />;\n }\n }, [\n widget,\n widgetData,\n children,\n hasDrilldown,\n handleDrilldown,\n useRendererFactory,\n isEditMode,\n isSelected,\n isLoading,\n error,\n handleActionFromFactory,\n realWidgetData,\n ]);\n\n // Determine content to render\n const renderContent = () => {\n if (isLoading) {\n return <WidgetLoadingState />;\n }\n\n if (error) {\n return <WidgetErrorState error={error} onRetry={onRefresh} />;\n }\n\n if (isEmpty && !children) {\n // v2.0: Check parserId (primary) or dataSinkId (for context/drilldowns)\n return (\n <WidgetEmptyState\n widgetType={widget.type}\n hasDataSource={hasDataSource}\n templateSlot={\n typeof widget.metadata?.['templateDataSinkSlot'] === 'string'\n ? (widget.metadata['templateDataSinkSlot'] as string)\n : null\n }\n />\n );\n }\n\n // resetKey auto-resets the boundary on parser/sink re-bind or config edit without widget.id changing (BOFF-2242).\n const resetKey = `${widgetBinding.source.kind}|${widgetBinding.source.ref ?? ''}|${widgetBinding.parserId ?? ''}|${widget.updatedAt ?? ''}`;\n\n return (\n <WidgetErrorBoundary widgetId={widget.id} widgetType={widget.type} widgetTitle={widget.title} resetKey={resetKey}>\n {children ?? <Suspense fallback={<WidgetLoadingState />}>{renderWidgetComponent()}</Suspense>}\n </WidgetErrorBoundary>\n );\n };\n\n return (\n <div\n ref={containerRef}\n className={`\n group relative flex flex-col\n h-full w-full\n rounded-lg border\n bg-bg-surface\n transition-all duration-200\n ${\n isSelected && isEditMode\n ? 'border-action-primary-border ring-2 ring-action-primary-border/30'\n : 'border-border-default hover:border-action-primary-bg'\n }\n ${isEditMode ? 'cursor-pointer' : ''}\n ${className}\n `}\n onMouseDown={handleMouseDown}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n tabIndex={isEditMode ? 0 : -1}\n role={isEditMode ? 'button' : undefined}\n aria-selected={isSelected}\n aria-label={`Widget: ${widget.title}`}\n data-widget-id={widget.id}\n data-widget-type={widget.type}\n >\n {/* Header */}\n <WidgetHeader\n widget={widget}\n definition={definition}\n isSelected={isSelected}\n isEditMode={isEditMode}\n isLoading={isLoading}\n isSampleData={isSampleData}\n timeUntilRefresh={widget.refreshInterval && !isPaused ? formatTimeUntilRefresh(timeUntilRefresh) : undefined}\n hasDrilldown={hasDrilldown}\n onRefresh={onRefresh}\n onExportCSV={exportData ? exportCSV : undefined}\n onExportPNG={exportPNG}\n onDrilldown={hasDrilldown ? () => handleDrilldown() : undefined}\n onToggleComments={toggleComments}\n commentsOpen={showComments}\n commentCount={commentsEverOpened ? commentCount : undefined}\n />\n\n {/* Content Area */}\n <div\n className={`\n flex-1 overflow-hidden p-4 min-h-0\n ${showContainerDrilldownHints && !isEditMode ? 'cursor-pointer' : ''}\n `}\n title={showContainerDrilldownHints && !isEditMode ? 'Click data to drill down' : undefined}\n >\n <div className=\"h-full overflow-auto\">{renderContent()}</div>\n </div>\n\n {/* v2.0: Widget Actions */}\n {actions && actions.length > 0 && !isLoading && !error && (\n <div className=\"px-4 py-3 border-t border-border-default bg-bg-sunken\" style={{ flexShrink: 0 }}>\n <WidgetActions\n actions={actions}\n context={actionContext}\n onAction={onAction}\n disabled={isEditMode}\n layout=\"horizontal\"\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Edit mode: react-grid-layout provides resize handles */}\n\n {/* Drilldown Indicator */}\n {showContainerDrilldownHints && !isEditMode && (\n <div\n className=\"\n absolute bottom-2 right-2\n opacity-0 group-hover:opacity-100\n transition-opacity duration-200\n \"\n title=\"Click to drill down\"\n >\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 7l5 5m0 0l-5 5m5-5H6\" />\n </svg>\n </div>\n )}\n\n {/* Comments panel (overlay) */}\n {showComments && (\n <Suspense fallback={null}>\n <WidgetComments widgetId={widget.id} onClose={() => setShowComments(false)} />\n </Suspense>\n )}\n </div>\n );\n}, arePropsEqual);\n\nexport default WidgetWrapper;\n"],"mappings":"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"}
|
|
@@ -26,16 +26,19 @@ var t = e`
|
|
|
26
26
|
$defaultDashboardName: String
|
|
27
27
|
$defaultCategory: String
|
|
28
28
|
$defaultTags: [String!]
|
|
29
|
+
$includeSampleData: Boolean
|
|
29
30
|
) {
|
|
30
31
|
buildDashboardTemplateContent(
|
|
31
32
|
dashboardId: $dashboardId
|
|
32
33
|
defaultDashboardName: $defaultDashboardName
|
|
33
34
|
defaultCategory: $defaultCategory
|
|
34
35
|
defaultTags: $defaultTags
|
|
36
|
+
includeSampleData: $includeSampleData
|
|
35
37
|
) {
|
|
36
38
|
content
|
|
37
39
|
sinkCount
|
|
38
40
|
unknownSinkIds
|
|
41
|
+
sampleDataSinkCount
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storeTemplates.js","names":[],"sources":["../../../src/bigconsole/graphql/storeTemplates.ts"],"sourcesContent":["/**\n * GraphQL documents for store-distributed dashboard templates.\n *\n * The two catalog reads are generated from\n * `src/operations/global/store/queries/*.graphql` in the usual way.\n *\n * `IMPORT_DASHBOARD_TEMPLATE` is defined inline as an interim measure.\n * `importDashboardTemplate` is new in wspace-bigconsole-svc, so it is not yet\n * present in `graphql/wspace.graphql` — that file is only ever refreshed by\n * `bun run schema:pull` against a live gateway (hand-editing it is a repo\n * guardrail), and the gateway supergraph only carries the field once the\n * service has deployed and Hive has reconciled.\n *\n * It cannot simply be dropped into `src/operations/wspace/**` yet either:\n * `src/generated` is gitignored and CI regenerates it, so an operation file\n * referencing a field the checked-in schema lacks fails codegen and breaks the\n * build. Once `schema:pull` has run, move this document into\n * `src/operations/wspace/dashboard/mutations/importDashboardTemplate.graphql`\n * and import the generated `ImportDashboardTemplateDocument` instead — the\n * shape is unchanged.\n *\n * @module bigconsole/graphql/storeTemplates\n */\nimport { gql } from '@apollo/client';\n\nexport {\n BrowseDashboardTemplatesDocument as BROWSE_DASHBOARD_TEMPLATES,\n GetDashboardTemplateDetailDocument as GET_DASHBOARD_TEMPLATE_DETAIL,\n CreateDashboardTemplateAppDocument as CREATE_TEMPLATE_APP,\n CreateDashboardTemplateVersionDocument as CREATE_TEMPLATE_VERSION,\n} from '../../generated/global-operations';\n\nexport const IMPORT_DASHBOARD_TEMPLATE = gql`\n mutation ImportDashboardTemplate($input: ImportDashboardTemplateInput!) {\n importDashboardTemplate(input: $input) {\n success\n errorMessage\n unresolvedSlots\n resolvedSlots\n widgetsNeedingData\n warnings\n stats {\n pagesCreated\n widgetsCreated\n }\n dashboard {\n id\n name\n }\n }\n }\n`;\n\n/**\n * Same interim treatment as IMPORT_DASHBOARD_TEMPLATE, and for the same reason:\n * buildDashboardTemplateContent is new in wspace-bigconsole-svc and is not yet\n * in the checked-in wspace schema.\n */\nexport const BUILD_DASHBOARD_TEMPLATE_CONTENT = gql`\n query BuildDashboardTemplateContent(\n $dashboardId: ID!\n $defaultDashboardName: String\n $defaultCategory: String\n $defaultTags: [String!]\n ) {\n buildDashboardTemplateContent(\n dashboardId: $dashboardId\n defaultDashboardName: $defaultDashboardName\n defaultCategory: $defaultCategory\n defaultTags: $defaultTags\n ) {\n content\n sinkCount\n unknownSinkIds\n }\n }\n`;\n"],"mappings":";;;AAgCA,IAAa,IAA4B,CAAG;;;;;;;;;;;;;;;;;;;GA0B/B,IAAmC,CAAG"}
|
|
1
|
+
{"version":3,"file":"storeTemplates.js","names":[],"sources":["../../../src/bigconsole/graphql/storeTemplates.ts"],"sourcesContent":["/**\n * GraphQL documents for store-distributed dashboard templates.\n *\n * The two catalog reads are generated from\n * `src/operations/global/store/queries/*.graphql` in the usual way.\n *\n * `IMPORT_DASHBOARD_TEMPLATE` is defined inline as an interim measure.\n * `importDashboardTemplate` is new in wspace-bigconsole-svc, so it is not yet\n * present in `graphql/wspace.graphql` — that file is only ever refreshed by\n * `bun run schema:pull` against a live gateway (hand-editing it is a repo\n * guardrail), and the gateway supergraph only carries the field once the\n * service has deployed and Hive has reconciled.\n *\n * It cannot simply be dropped into `src/operations/wspace/**` yet either:\n * `src/generated` is gitignored and CI regenerates it, so an operation file\n * referencing a field the checked-in schema lacks fails codegen and breaks the\n * build. Once `schema:pull` has run, move this document into\n * `src/operations/wspace/dashboard/mutations/importDashboardTemplate.graphql`\n * and import the generated `ImportDashboardTemplateDocument` instead — the\n * shape is unchanged.\n *\n * @module bigconsole/graphql/storeTemplates\n */\nimport { gql } from '@apollo/client';\n\nexport {\n BrowseDashboardTemplatesDocument as BROWSE_DASHBOARD_TEMPLATES,\n GetDashboardTemplateDetailDocument as GET_DASHBOARD_TEMPLATE_DETAIL,\n CreateDashboardTemplateAppDocument as CREATE_TEMPLATE_APP,\n CreateDashboardTemplateVersionDocument as CREATE_TEMPLATE_VERSION,\n} from '../../generated/global-operations';\n\nexport const IMPORT_DASHBOARD_TEMPLATE = gql`\n mutation ImportDashboardTemplate($input: ImportDashboardTemplateInput!) {\n importDashboardTemplate(input: $input) {\n success\n errorMessage\n unresolvedSlots\n resolvedSlots\n widgetsNeedingData\n warnings\n stats {\n pagesCreated\n widgetsCreated\n }\n dashboard {\n id\n name\n }\n }\n }\n`;\n\n/**\n * Same interim treatment as IMPORT_DASHBOARD_TEMPLATE, and for the same reason:\n * buildDashboardTemplateContent is new in wspace-bigconsole-svc and is not yet\n * in the checked-in wspace schema.\n */\nexport const BUILD_DASHBOARD_TEMPLATE_CONTENT = gql`\n query BuildDashboardTemplateContent(\n $dashboardId: ID!\n $defaultDashboardName: String\n $defaultCategory: String\n $defaultTags: [String!]\n $includeSampleData: Boolean\n ) {\n buildDashboardTemplateContent(\n dashboardId: $dashboardId\n defaultDashboardName: $defaultDashboardName\n defaultCategory: $defaultCategory\n defaultTags: $defaultTags\n includeSampleData: $includeSampleData\n ) {\n content\n sinkCount\n unknownSinkIds\n sampleDataSinkCount\n }\n }\n`;\n"],"mappings":";;;AAgCA,IAAa,IAA4B,CAAG;;;;;;;;;;;;;;;;;;;GA0B/B,IAAmC,CAAG"}
|
|
@@ -117,10 +117,11 @@ function p(p = {}) {
|
|
|
117
117
|
success: !1,
|
|
118
118
|
errorMessage: e,
|
|
119
119
|
sinkCount: 0,
|
|
120
|
-
unknownSinkIds: []
|
|
120
|
+
unknownSinkIds: [],
|
|
121
|
+
sampleDataSinkCount: 0
|
|
121
122
|
});
|
|
122
123
|
if (!_) return r("Store is unavailable right now.");
|
|
123
|
-
let a, o = 0, s = [];
|
|
124
|
+
let a, o = 0, s = [], c = 0;
|
|
124
125
|
try {
|
|
125
126
|
let t = (await v.query({
|
|
126
127
|
query: i,
|
|
@@ -128,12 +129,13 @@ function p(p = {}) {
|
|
|
128
129
|
dashboardId: e.dashboardId,
|
|
129
130
|
defaultDashboardName: e.name,
|
|
130
131
|
defaultCategory: e.category ?? null,
|
|
131
|
-
defaultTags: e.tags ?? []
|
|
132
|
+
defaultTags: e.tags ?? [],
|
|
133
|
+
includeSampleData: e.includeSampleData ?? !1
|
|
132
134
|
},
|
|
133
135
|
fetchPolicy: "network-only"
|
|
134
136
|
})).data?.buildDashboardTemplateContent;
|
|
135
137
|
if (!t?.content) return r("Could not build a template from this dashboard.");
|
|
136
|
-
a = t.content, o = t.sinkCount, s = t.unknownSinkIds ?? [];
|
|
138
|
+
a = t.content, o = t.sinkCount, s = t.unknownSinkIds ?? [], c = t.sampleDataSinkCount ?? 0;
|
|
137
139
|
} catch (e) {
|
|
138
140
|
return r(e instanceof Error ? e.message : "Failed to build the template.");
|
|
139
141
|
}
|
|
@@ -165,17 +167,20 @@ function p(p = {}) {
|
|
|
165
167
|
applicationId: i.id,
|
|
166
168
|
status: i.status,
|
|
167
169
|
sinkCount: o,
|
|
168
|
-
unknownSinkIds: s
|
|
170
|
+
unknownSinkIds: s,
|
|
171
|
+
sampleDataSinkCount: c
|
|
169
172
|
}) : {
|
|
170
173
|
...r("The developer portal did not create the app."),
|
|
171
174
|
sinkCount: o,
|
|
172
|
-
unknownSinkIds: s
|
|
175
|
+
unknownSinkIds: s,
|
|
176
|
+
sampleDataSinkCount: c
|
|
173
177
|
};
|
|
174
178
|
} catch (e) {
|
|
175
179
|
return {
|
|
176
180
|
...r(e instanceof Error ? e.message : "Failed to publish to the developer portal."),
|
|
177
181
|
sinkCount: o,
|
|
178
|
-
unknownSinkIds: s
|
|
182
|
+
unknownSinkIds: s,
|
|
183
|
+
sampleDataSinkCount: c
|
|
179
184
|
};
|
|
180
185
|
}
|
|
181
186
|
}, [_, v]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStoreDashboardTemplates.js","names":[],"sources":["../../../src/bigconsole/hooks/useStoreDashboardTemplates.ts"],"sourcesContent":["/**\n * Store-distributed dashboard templates.\n *\n * Dashboard templates live in the global store as `StoreProduct` rows of type\n * `DASHBOARD` whose `content` column holds a `DashboardTemplateContent`\n * document. This mirrors how FluidGrids sources its workflow templates\n * (`browseStore(type: WORKFLOW)` → `content.definitionJson` →\n * `importWorkflowTemplate`).\n *\n * Two gateways are involved: the catalog is read from the **global** gateway,\n * and the install mutation runs against the **workspace** gateway, which is\n * where the dashboard is created.\n *\n * @module bigconsole/hooks/useStoreDashboardTemplates\n */\nimport { useApolloClient } from '@apollo/client/react';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport {\n BROWSE_DASHBOARD_TEMPLATES,\n BUILD_DASHBOARD_TEMPLATE_CONTENT,\n CREATE_TEMPLATE_APP,\n CREATE_TEMPLATE_VERSION,\n GET_DASHBOARD_TEMPLATE_DETAIL,\n IMPORT_DASHBOARD_TEMPLATE,\n} from '../graphql/storeTemplates';\n\n/** A column a widget expects from the sink bound to its slot. */\nexport interface TemplateExpectedField {\n name: string;\n role?: string;\n}\n\n/** A data-binding slot the template needs filled. */\nexport interface TemplateRequiredDataSink {\n slot: string;\n label: string;\n description?: string | null;\n expectedFields: TemplateExpectedField[];\n widgetTitles: string[];\n}\n\nexport interface TemplateRequiredWidgetType {\n widgetType: string;\n widgetTypeKey?: string | null;\n publishType: 'BUILTIN' | 'STORE' | 'CUSTOM';\n displayName?: string;\n requiresPurchase?: boolean;\n}\n\nexport interface TemplateRequiredInput {\n name: string;\n type: 'string' | 'number' | 'boolean' | 'select' | 'secret';\n label: string;\n placeholder?: string;\n required: boolean;\n defaultValue?: string | number | boolean | null;\n options?: { label: string; value: string }[];\n helpText?: string;\n}\n\nexport interface DashboardTemplateContent {\n definition: unknown;\n requiredDataSinks: TemplateRequiredDataSink[];\n requiredWidgetTypes: TemplateRequiredWidgetType[];\n requiredInputs: TemplateRequiredInput[];\n defaultDashboardName?: string;\n defaultCategory?: string | null;\n defaultTags?: string[];\n difficultyLevel?: 'easy' | 'medium' | 'hard';\n}\n\n/** A store product presented as a template card. */\nexport interface StoreDashboardTemplate {\n id: string;\n name: string;\n slug: string;\n description?: string | null;\n icon?: string | null;\n category?: string | null;\n tags: string[];\n featured: boolean;\n downloads: number;\n rating?: number | null;\n pricingModel: string;\n price: number;\n /** True for anything the workspace has to buy before it can be installed. */\n requiresPurchase: boolean;\n}\n\ninterface RawStoreProduct {\n id: string;\n name: string;\n slug: string;\n description?: string | null;\n icon?: string | null;\n category?: string | null;\n tags?: string[] | null;\n featured?: boolean | null;\n downloads?: number | null;\n rating?: number | null;\n pricingModel?: string | null;\n price?: number | null;\n}\n\nexport interface InstallTemplateArgs {\n templateId: string;\n templateName: string;\n content: DashboardTemplateContent;\n name?: string;\n visibility?: string;\n dataSinkBindings?: { slot: string; dataSinkId: string }[];\n inputValues?: Record<string, unknown>;\n}\n\nexport interface InstallTemplateResult {\n success: boolean;\n dashboardId?: string;\n errorMessage?: string;\n unresolvedSlots: string[];\n resolvedSlots: string[];\n widgetsNeedingData: number;\n warnings: string[];\n}\n\nfunction normalizeProduct(raw: RawStoreProduct): StoreDashboardTemplate {\n const pricingModel = raw.pricingModel ?? 'FREE';\n return {\n id: raw.id,\n name: raw.name,\n slug: raw.slug,\n description: raw.description ?? null,\n icon: raw.icon ?? null,\n category: raw.category ?? null,\n tags: raw.tags ?? [],\n featured: raw.featured ?? false,\n downloads: raw.downloads ?? 0,\n rating: raw.rating ?? null,\n pricingModel,\n price: raw.price ?? 0,\n requiresPurchase: pricingModel !== 'FREE' && (raw.price ?? 0) > 0,\n };\n}\n\nexport interface PublishToStoreArgs {\n dashboardId: string;\n name: string;\n slug: string;\n description: string;\n category?: string;\n tags?: string[];\n}\n\nexport interface PublishToStoreResult {\n success: boolean;\n /** The DRAFT devportal application created for this template. */\n applicationId?: string;\n status?: string;\n errorMessage?: string;\n /** Distinct data sinks the dashboard referenced, all tokenised into slots. */\n sinkCount: number;\n /** Sinks that could not be resolved; their slots fall back to positional names. */\n unknownSinkIds: string[];\n}\n\nexport interface UseStoreDashboardTemplatesResult {\n templates: StoreDashboardTemplate[];\n loading: boolean;\n error: string | null;\n totalCount: number;\n refetch: (search?: string) => Promise<void>;\n /** Fetch the full template document. Kept lazy: content is large. */\n fetchTemplateContent: (templateId: string) => Promise<DashboardTemplateContent | null>;\n installTemplate: (args: InstallTemplateArgs) => Promise<InstallTemplateResult>;\n publishToStore: (args: PublishToStoreArgs) => Promise<PublishToStoreResult>;\n}\n\nexport function useStoreDashboardTemplates(\n options: { search?: string; category?: string; autoLoad?: boolean } = {}\n): UseStoreDashboardTemplatesResult {\n const { search, category, autoLoad = true } = options;\n\n const globalClient = useGatewayClient('global');\n const workspaceClient = useApolloClient();\n\n const [templates, setTemplates] = useState<StoreDashboardTemplate[]>([]);\n const [totalCount, setTotalCount] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const refetch = useCallback(\n async (overrideSearch?: string): Promise<void> => {\n if (!globalClient) return;\n setLoading(true);\n setError(null);\n try {\n const result = await globalClient.query<{\n browseStore?: { products?: RawStoreProduct[]; totalCount?: number };\n }>({\n query: BROWSE_DASHBOARD_TEMPLATES,\n variables: {\n input: {\n filter: {\n type: 'DASHBOARD',\n status: 'PUBLISHED',\n // Without this a template published from another product's\n // devportal listing would show up in BigConsole's gallery.\n compatibleWith: 'bigconsole',\n ...((overrideSearch ?? search) ? { search: overrideSearch ?? search } : {}),\n ...(category && category !== 'ALL' ? { category } : {}),\n },\n sortBy: 'RELEVANCE',\n pagination: { limit: 50, offset: 0 },\n },\n },\n fetchPolicy: 'network-only',\n });\n\n setTemplates((result.data?.browseStore?.products ?? []).map(normalizeProduct));\n setTotalCount(result.data?.browseStore?.totalCount ?? 0);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load templates from the store');\n setTemplates([]);\n setTotalCount(0);\n } finally {\n setLoading(false);\n }\n },\n [globalClient, search, category]\n );\n\n useEffect(() => {\n if (autoLoad) void refetch();\n }, [autoLoad, refetch]);\n\n const fetchTemplateContent = useCallback(\n async (templateId: string): Promise<DashboardTemplateContent | null> => {\n if (!globalClient) return null;\n try {\n const result = await globalClient.query<{ product?: { content?: unknown } }>({\n query: GET_DASHBOARD_TEMPLATE_DETAIL,\n variables: { id: templateId },\n fetchPolicy: 'network-only',\n });\n const content = result.data?.product?.content;\n if (!content || typeof content !== 'object') return null;\n const doc = content as Partial<DashboardTemplateContent>;\n if (!doc.definition) return null;\n return {\n definition: doc.definition,\n requiredDataSinks: doc.requiredDataSinks ?? [],\n requiredWidgetTypes: doc.requiredWidgetTypes ?? [],\n requiredInputs: doc.requiredInputs ?? [],\n defaultDashboardName: doc.defaultDashboardName,\n defaultCategory: doc.defaultCategory ?? null,\n defaultTags: doc.defaultTags ?? [],\n difficultyLevel: doc.difficultyLevel ?? 'medium',\n };\n } catch {\n return null;\n }\n },\n [globalClient]\n );\n\n const installTemplate = useCallback(\n async (args: InstallTemplateArgs): Promise<InstallTemplateResult> => {\n const result = await workspaceClient.mutate<{\n importDashboardTemplate?: InstallTemplateResult & { dashboard?: { id: string } | null };\n }>({\n mutation: IMPORT_DASHBOARD_TEMPLATE,\n variables: {\n input: {\n content: args.content,\n sourceTemplateId: args.templateId,\n sourceTemplateName: args.templateName,\n ...(args.name ? { name: args.name } : {}),\n ...(args.visibility ? { visibility: args.visibility } : {}),\n ...(args.dataSinkBindings?.length ? { dataSinkBindings: args.dataSinkBindings } : {}),\n ...(args.inputValues && Object.keys(args.inputValues).length ? { inputValues: args.inputValues } : {}),\n },\n },\n });\n\n const payload = result.data?.importDashboardTemplate;\n if (!payload) {\n return {\n success: false,\n errorMessage: 'No response from the server',\n unresolvedSlots: [],\n resolvedSlots: [],\n widgetsNeedingData: 0,\n warnings: [],\n };\n }\n\n return {\n success: payload.success,\n dashboardId: payload.dashboard?.id,\n errorMessage: payload.errorMessage,\n unresolvedSlots: payload.unresolvedSlots ?? [],\n resolvedSlots: payload.resolvedSlots ?? [],\n widgetsNeedingData: payload.widgetsNeedingData ?? 0,\n warnings: payload.warnings ?? [],\n };\n },\n [workspaceClient]\n );\n\n /**\n * Turn a live dashboard into a Dashboard Template application in the\n * developer portal.\n *\n * It deliberately does NOT create a StoreProduct directly. devportal already\n * owns dashboard-template publishing end to end — versioning, submit/review,\n * publisher identity, and the `metadata.sourceApplicationId` the store uses\n * for ownership on later updates. Writing a product straight to the store\n * would fork that pipeline and orphan the listing: a later devportal publish\n * of the same slug would hit the store's slug-collision guard and refuse.\n *\n * So this fills in the one thing devportal cannot do — produce the template\n * document from a live dashboard — and hands off to the existing flow. The\n * app is left in DRAFT so the author reviews the listing and submits it\n * themselves.\n *\n * Two hops: the workspace gateway tokenises the dashboard (it owns the data\n * sinks), the global gateway owns devportal. Brokering through the client\n * keeps both directions free of cross-layer M2M, exactly like install.\n */\n const publishToStore = useCallback(\n async (args: PublishToStoreArgs): Promise<PublishToStoreResult> => {\n const failure = (errorMessage: string): PublishToStoreResult => ({\n success: false,\n errorMessage,\n sinkCount: 0,\n unknownSinkIds: [],\n });\n\n if (!globalClient) return failure('Store is unavailable right now.');\n\n let content: unknown;\n let sinkCount = 0;\n let unknownSinkIds: string[] = [];\n try {\n const built = await workspaceClient.query<{\n buildDashboardTemplateContent?: {\n content: unknown;\n sinkCount: number;\n unknownSinkIds: string[];\n };\n }>({\n query: BUILD_DASHBOARD_TEMPLATE_CONTENT,\n variables: {\n dashboardId: args.dashboardId,\n defaultDashboardName: args.name,\n defaultCategory: args.category ?? null,\n defaultTags: args.tags ?? [],\n },\n fetchPolicy: 'network-only',\n });\n const payload = built.data?.buildDashboardTemplateContent;\n if (!payload?.content) return failure('Could not build a template from this dashboard.');\n content = payload.content;\n sinkCount = payload.sinkCount;\n unknownSinkIds = payload.unknownSinkIds ?? [];\n } catch (err) {\n return failure(err instanceof Error ? err.message : 'Failed to build the template.');\n }\n\n try {\n const app = await globalClient.mutate<{\n createApplication?: { id: string; status: string };\n }>({\n mutation: CREATE_TEMPLATE_APP,\n variables: {\n input: {\n appType: 'DASHBOARD',\n name: args.name,\n slug: args.slug,\n shortDescription: args.description.slice(0, 200),\n description: args.description,\n ...(args.category ? { category: args.category } : {}),\n tags: args.tags ?? [],\n compatibleProducts: ['bigconsole'],\n },\n },\n });\n\n const application = app.data?.createApplication;\n if (!application) {\n return { ...failure('The developer portal did not create the app.'), sinkCount, unknownSinkIds };\n }\n\n // The version manifest is what devportal maps onto StoreProduct.content\n // when the app is submitted and approved.\n await globalClient.mutate({\n mutation: CREATE_TEMPLATE_VERSION,\n variables: {\n applicationId: application.id,\n input: { version: '1.0.0', manifest: content },\n },\n });\n\n return {\n success: true,\n applicationId: application.id,\n status: application.status,\n sinkCount,\n unknownSinkIds,\n };\n } catch (err) {\n return {\n ...failure(err instanceof Error ? err.message : 'Failed to publish to the developer portal.'),\n sinkCount,\n unknownSinkIds,\n };\n }\n },\n [globalClient, workspaceClient]\n );\n\n return useMemo(\n () => ({\n templates,\n loading,\n error,\n totalCount,\n refetch,\n fetchTemplateContent,\n installTemplate,\n publishToStore,\n }),\n [templates, loading, error, totalCount, refetch, fetchTemplateContent, installTemplate, publishToStore]\n );\n}\n\nexport default useStoreDashboardTemplates;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useStoreDashboardTemplates.js","names":[],"sources":["../../../src/bigconsole/hooks/useStoreDashboardTemplates.ts"],"sourcesContent":["/**\n * Store-distributed dashboard templates.\n *\n * Dashboard templates live in the global store as `StoreProduct` rows of type\n * `DASHBOARD` whose `content` column holds a `DashboardTemplateContent`\n * document. This mirrors how FluidGrids sources its workflow templates\n * (`browseStore(type: WORKFLOW)` → `content.definitionJson` →\n * `importWorkflowTemplate`).\n *\n * Two gateways are involved: the catalog is read from the **global** gateway,\n * and the install mutation runs against the **workspace** gateway, which is\n * where the dashboard is created.\n *\n * @module bigconsole/hooks/useStoreDashboardTemplates\n */\nimport { useApolloClient } from '@apollo/client/react';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport {\n BROWSE_DASHBOARD_TEMPLATES,\n BUILD_DASHBOARD_TEMPLATE_CONTENT,\n CREATE_TEMPLATE_APP,\n CREATE_TEMPLATE_VERSION,\n GET_DASHBOARD_TEMPLATE_DETAIL,\n IMPORT_DASHBOARD_TEMPLATE,\n} from '../graphql/storeTemplates';\n\n/** A column a widget expects from the sink bound to its slot. */\nexport interface TemplateExpectedField {\n name: string;\n role?: string;\n}\n\n/** A data-binding slot the template needs filled. */\nexport interface TemplateRequiredDataSink {\n slot: string;\n label: string;\n description?: string | null;\n expectedFields: TemplateExpectedField[];\n widgetTitles: string[];\n}\n\nexport interface TemplateRequiredWidgetType {\n widgetType: string;\n widgetTypeKey?: string | null;\n publishType: 'BUILTIN' | 'STORE' | 'CUSTOM';\n displayName?: string;\n requiresPurchase?: boolean;\n}\n\nexport interface TemplateRequiredInput {\n name: string;\n type: 'string' | 'number' | 'boolean' | 'select' | 'secret';\n label: string;\n placeholder?: string;\n required: boolean;\n defaultValue?: string | number | boolean | null;\n options?: { label: string; value: string }[];\n helpText?: string;\n}\n\nexport interface DashboardTemplateContent {\n definition: unknown;\n requiredDataSinks: TemplateRequiredDataSink[];\n requiredWidgetTypes: TemplateRequiredWidgetType[];\n requiredInputs: TemplateRequiredInput[];\n defaultDashboardName?: string;\n defaultCategory?: string | null;\n defaultTags?: string[];\n difficultyLevel?: 'easy' | 'medium' | 'hard';\n}\n\n/** A store product presented as a template card. */\nexport interface StoreDashboardTemplate {\n id: string;\n name: string;\n slug: string;\n description?: string | null;\n icon?: string | null;\n category?: string | null;\n tags: string[];\n featured: boolean;\n downloads: number;\n rating?: number | null;\n pricingModel: string;\n price: number;\n /** True for anything the workspace has to buy before it can be installed. */\n requiresPurchase: boolean;\n}\n\ninterface RawStoreProduct {\n id: string;\n name: string;\n slug: string;\n description?: string | null;\n icon?: string | null;\n category?: string | null;\n tags?: string[] | null;\n featured?: boolean | null;\n downloads?: number | null;\n rating?: number | null;\n pricingModel?: string | null;\n price?: number | null;\n}\n\nexport interface InstallTemplateArgs {\n templateId: string;\n templateName: string;\n content: DashboardTemplateContent;\n name?: string;\n visibility?: string;\n dataSinkBindings?: { slot: string; dataSinkId: string }[];\n inputValues?: Record<string, unknown>;\n}\n\nexport interface InstallTemplateResult {\n success: boolean;\n dashboardId?: string;\n errorMessage?: string;\n unresolvedSlots: string[];\n resolvedSlots: string[];\n widgetsNeedingData: number;\n warnings: string[];\n}\n\nfunction normalizeProduct(raw: RawStoreProduct): StoreDashboardTemplate {\n const pricingModel = raw.pricingModel ?? 'FREE';\n return {\n id: raw.id,\n name: raw.name,\n slug: raw.slug,\n description: raw.description ?? null,\n icon: raw.icon ?? null,\n category: raw.category ?? null,\n tags: raw.tags ?? [],\n featured: raw.featured ?? false,\n downloads: raw.downloads ?? 0,\n rating: raw.rating ?? null,\n pricingModel,\n price: raw.price ?? 0,\n requiresPurchase: pricingModel !== 'FREE' && (raw.price ?? 0) > 0,\n };\n}\n\nexport interface PublishToStoreArgs {\n dashboardId: string;\n name: string;\n slug: string;\n description: string;\n category?: string;\n tags?: string[];\n /** Embed a few real rows per data source so the board renders populated. */\n includeSampleData?: boolean;\n}\n\nexport interface PublishToStoreResult {\n success: boolean;\n /** The DRAFT devportal application created for this template. */\n applicationId?: string;\n status?: string;\n errorMessage?: string;\n /** Distinct data sinks the dashboard referenced, all tokenised into slots. */\n sinkCount: number;\n /** Sinks that could not be resolved; their slots fall back to positional names. */\n unknownSinkIds: string[];\n /** How many data sources contributed sample rows. */\n sampleDataSinkCount: number;\n}\n\nexport interface UseStoreDashboardTemplatesResult {\n templates: StoreDashboardTemplate[];\n loading: boolean;\n error: string | null;\n totalCount: number;\n refetch: (search?: string) => Promise<void>;\n /** Fetch the full template document. Kept lazy: content is large. */\n fetchTemplateContent: (templateId: string) => Promise<DashboardTemplateContent | null>;\n installTemplate: (args: InstallTemplateArgs) => Promise<InstallTemplateResult>;\n publishToStore: (args: PublishToStoreArgs) => Promise<PublishToStoreResult>;\n}\n\nexport function useStoreDashboardTemplates(\n options: { search?: string; category?: string; autoLoad?: boolean } = {}\n): UseStoreDashboardTemplatesResult {\n const { search, category, autoLoad = true } = options;\n\n const globalClient = useGatewayClient('global');\n const workspaceClient = useApolloClient();\n\n const [templates, setTemplates] = useState<StoreDashboardTemplate[]>([]);\n const [totalCount, setTotalCount] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const refetch = useCallback(\n async (overrideSearch?: string): Promise<void> => {\n if (!globalClient) return;\n setLoading(true);\n setError(null);\n try {\n const result = await globalClient.query<{\n browseStore?: { products?: RawStoreProduct[]; totalCount?: number };\n }>({\n query: BROWSE_DASHBOARD_TEMPLATES,\n variables: {\n input: {\n filter: {\n type: 'DASHBOARD',\n status: 'PUBLISHED',\n // Without this a template published from another product's\n // devportal listing would show up in BigConsole's gallery.\n compatibleWith: 'bigconsole',\n ...((overrideSearch ?? search) ? { search: overrideSearch ?? search } : {}),\n ...(category && category !== 'ALL' ? { category } : {}),\n },\n sortBy: 'RELEVANCE',\n pagination: { limit: 50, offset: 0 },\n },\n },\n fetchPolicy: 'network-only',\n });\n\n setTemplates((result.data?.browseStore?.products ?? []).map(normalizeProduct));\n setTotalCount(result.data?.browseStore?.totalCount ?? 0);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load templates from the store');\n setTemplates([]);\n setTotalCount(0);\n } finally {\n setLoading(false);\n }\n },\n [globalClient, search, category]\n );\n\n useEffect(() => {\n if (autoLoad) void refetch();\n }, [autoLoad, refetch]);\n\n const fetchTemplateContent = useCallback(\n async (templateId: string): Promise<DashboardTemplateContent | null> => {\n if (!globalClient) return null;\n try {\n const result = await globalClient.query<{ product?: { content?: unknown } }>({\n query: GET_DASHBOARD_TEMPLATE_DETAIL,\n variables: { id: templateId },\n fetchPolicy: 'network-only',\n });\n const content = result.data?.product?.content;\n if (!content || typeof content !== 'object') return null;\n const doc = content as Partial<DashboardTemplateContent>;\n if (!doc.definition) return null;\n return {\n definition: doc.definition,\n requiredDataSinks: doc.requiredDataSinks ?? [],\n requiredWidgetTypes: doc.requiredWidgetTypes ?? [],\n requiredInputs: doc.requiredInputs ?? [],\n defaultDashboardName: doc.defaultDashboardName,\n defaultCategory: doc.defaultCategory ?? null,\n defaultTags: doc.defaultTags ?? [],\n difficultyLevel: doc.difficultyLevel ?? 'medium',\n };\n } catch {\n return null;\n }\n },\n [globalClient]\n );\n\n const installTemplate = useCallback(\n async (args: InstallTemplateArgs): Promise<InstallTemplateResult> => {\n const result = await workspaceClient.mutate<{\n importDashboardTemplate?: InstallTemplateResult & { dashboard?: { id: string } | null };\n }>({\n mutation: IMPORT_DASHBOARD_TEMPLATE,\n variables: {\n input: {\n content: args.content,\n sourceTemplateId: args.templateId,\n sourceTemplateName: args.templateName,\n ...(args.name ? { name: args.name } : {}),\n ...(args.visibility ? { visibility: args.visibility } : {}),\n ...(args.dataSinkBindings?.length ? { dataSinkBindings: args.dataSinkBindings } : {}),\n ...(args.inputValues && Object.keys(args.inputValues).length ? { inputValues: args.inputValues } : {}),\n },\n },\n });\n\n const payload = result.data?.importDashboardTemplate;\n if (!payload) {\n return {\n success: false,\n errorMessage: 'No response from the server',\n unresolvedSlots: [],\n resolvedSlots: [],\n widgetsNeedingData: 0,\n warnings: [],\n };\n }\n\n return {\n success: payload.success,\n dashboardId: payload.dashboard?.id,\n errorMessage: payload.errorMessage,\n unresolvedSlots: payload.unresolvedSlots ?? [],\n resolvedSlots: payload.resolvedSlots ?? [],\n widgetsNeedingData: payload.widgetsNeedingData ?? 0,\n warnings: payload.warnings ?? [],\n };\n },\n [workspaceClient]\n );\n\n /**\n * Turn a live dashboard into a Dashboard Template application in the\n * developer portal.\n *\n * It deliberately does NOT create a StoreProduct directly. devportal already\n * owns dashboard-template publishing end to end — versioning, submit/review,\n * publisher identity, and the `metadata.sourceApplicationId` the store uses\n * for ownership on later updates. Writing a product straight to the store\n * would fork that pipeline and orphan the listing: a later devportal publish\n * of the same slug would hit the store's slug-collision guard and refuse.\n *\n * So this fills in the one thing devportal cannot do — produce the template\n * document from a live dashboard — and hands off to the existing flow. The\n * app is left in DRAFT so the author reviews the listing and submits it\n * themselves.\n *\n * Two hops: the workspace gateway tokenises the dashboard (it owns the data\n * sinks), the global gateway owns devportal. Brokering through the client\n * keeps both directions free of cross-layer M2M, exactly like install.\n */\n const publishToStore = useCallback(\n async (args: PublishToStoreArgs): Promise<PublishToStoreResult> => {\n const failure = (errorMessage: string): PublishToStoreResult => ({\n success: false,\n errorMessage,\n sinkCount: 0,\n unknownSinkIds: [],\n sampleDataSinkCount: 0,\n });\n\n if (!globalClient) return failure('Store is unavailable right now.');\n\n let content: unknown;\n let sinkCount = 0;\n let unknownSinkIds: string[] = [];\n let sampleDataSinkCount = 0;\n try {\n const built = await workspaceClient.query<{\n buildDashboardTemplateContent?: {\n content: unknown;\n sinkCount: number;\n unknownSinkIds: string[];\n sampleDataSinkCount: number;\n };\n }>({\n query: BUILD_DASHBOARD_TEMPLATE_CONTENT,\n variables: {\n dashboardId: args.dashboardId,\n defaultDashboardName: args.name,\n defaultCategory: args.category ?? null,\n defaultTags: args.tags ?? [],\n includeSampleData: args.includeSampleData ?? false,\n },\n fetchPolicy: 'network-only',\n });\n const payload = built.data?.buildDashboardTemplateContent;\n if (!payload?.content) return failure('Could not build a template from this dashboard.');\n content = payload.content;\n sinkCount = payload.sinkCount;\n unknownSinkIds = payload.unknownSinkIds ?? [];\n sampleDataSinkCount = payload.sampleDataSinkCount ?? 0;\n } catch (err) {\n return failure(err instanceof Error ? err.message : 'Failed to build the template.');\n }\n\n try {\n const app = await globalClient.mutate<{\n createApplication?: { id: string; status: string };\n }>({\n mutation: CREATE_TEMPLATE_APP,\n variables: {\n input: {\n appType: 'DASHBOARD',\n name: args.name,\n slug: args.slug,\n shortDescription: args.description.slice(0, 200),\n description: args.description,\n ...(args.category ? { category: args.category } : {}),\n tags: args.tags ?? [],\n compatibleProducts: ['bigconsole'],\n },\n },\n });\n\n const application = app.data?.createApplication;\n if (!application) {\n return {\n ...failure('The developer portal did not create the app.'),\n sinkCount,\n unknownSinkIds,\n sampleDataSinkCount,\n };\n }\n\n // The version manifest is what devportal maps onto StoreProduct.content\n // when the app is submitted and approved.\n await globalClient.mutate({\n mutation: CREATE_TEMPLATE_VERSION,\n variables: {\n applicationId: application.id,\n input: { version: '1.0.0', manifest: content },\n },\n });\n\n return {\n success: true,\n applicationId: application.id,\n status: application.status,\n sinkCount,\n unknownSinkIds,\n sampleDataSinkCount,\n };\n } catch (err) {\n return {\n ...failure(err instanceof Error ? err.message : 'Failed to publish to the developer portal.'),\n sinkCount,\n unknownSinkIds,\n sampleDataSinkCount,\n };\n }\n },\n [globalClient, workspaceClient]\n );\n\n return useMemo(\n () => ({\n templates,\n loading,\n error,\n totalCount,\n refetch,\n fetchTemplateContent,\n installTemplate,\n publishToStore,\n }),\n [templates, loading, error, totalCount, refetch, fetchTemplateContent, installTemplate, publishToStore]\n );\n}\n\nexport default useStoreDashboardTemplates;\n"],"mappings":"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"}
|