@burdenoff/microfe-bigconsole 2026.925.1 → 2026.928.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/ShareDialog/EmbedTab.js +142 -145
- package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js.map +1 -1
- package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js +43 -13
- package/dist/bigconsole/components/embed/EmbedWidgetRenderer.js.map +1 -1
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js +5 -5
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js.map +1 -1
- package/dist/bigconsole/components/embed/renderer-manifest.js +14 -47
- package/dist/bigconsole/components/embed/renderer-manifest.js.map +1 -1
- package/dist/bigconsole/components/embed/types.js.map +1 -1
- package/dist/bigconsole/components/embed/widgetShim.js +28 -0
- package/dist/bigconsole/components/embed/widgetShim.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/CalendarEmbed.js +38 -0
- package/dist/bigconsole/components/embed/widgets/CalendarEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/ChartEmbed.js +225 -0
- package/dist/bigconsole/components/embed/widgets/ChartEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/EmbedFrame.js +18 -0
- package/dist/bigconsole/components/embed/widgets/EmbedFrame.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/FormEmbed.js +147 -0
- package/dist/bigconsole/components/embed/widgets/FormEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/GaugeEmbed.js +44 -0
- package/dist/bigconsole/components/embed/widgets/GaugeEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/HeatmapEmbed.js +35 -0
- package/dist/bigconsole/components/embed/widgets/HeatmapEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/KanbanEmbed.js +50 -0
- package/dist/bigconsole/components/embed/widgets/KanbanEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/PivotEmbed.js +146 -0
- package/dist/bigconsole/components/embed/widgets/PivotEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/ProgressEmbed.js +30 -0
- package/dist/bigconsole/components/embed/widgets/ProgressEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/RetentionEmbed.js +41 -0
- package/dist/bigconsole/components/embed/widgets/RetentionEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/TimelineEmbed.js +40 -0
- package/dist/bigconsole/components/embed/widgets/TimelineEmbed.js.map +1 -0
- package/dist/bigconsole/components/embed/widgets/shared.js +13 -1
- package/dist/bigconsole/components/embed/widgets/shared.js.map +1 -1
- package/dist/bigconsole/components/widgets/calendar/CalendarView.js +152 -0
- package/dist/bigconsole/components/widgets/calendar/CalendarView.js.map +1 -0
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +10 -147
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/ChartWidget.js +19 -19
- package/dist/bigconsole/components/widgets/chart/index.js +8 -8
- package/dist/bigconsole/components/widgets/index.js +5 -5
- package/dist/bigconsole/components/widgets/kanban/KanbanView.js +192 -0
- package/dist/bigconsole/components/widgets/kanban/KanbanView.js.map +1 -0
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +10 -187
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +20 -20
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js +23 -22
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/utils/translate.js +9 -0
- package/dist/bigconsole/components/widgets/utils/translate.js.map +1 -0
- package/dist/bigconsole/hooks/useDashboardEmbedPolicy.js +2 -4
- package/dist/bigconsole/hooks/useDashboardEmbedPolicy.js.map +1 -1
- package/dist/generated/wspace-operations.js +0 -1
- package/dist/generated/wspace-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,193 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import e from "./KanbanView.js";
|
|
2
|
+
import { memo as t } from "react";
|
|
3
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
4
|
+
import { useI18n as r } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
4
5
|
//#region src/bigconsole/components/widgets/kanban/KanbanWidget.tsx
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
default: return "bg-bg-sunken text-text-secondary";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
var o = [
|
|
14
|
-
"status",
|
|
15
|
-
"state",
|
|
16
|
-
"column",
|
|
17
|
-
"stage",
|
|
18
|
-
"category",
|
|
19
|
-
"phase",
|
|
20
|
-
"group"
|
|
21
|
-
], s = [
|
|
22
|
-
"title",
|
|
23
|
-
"name",
|
|
24
|
-
"summary",
|
|
25
|
-
"label",
|
|
26
|
-
"identifier",
|
|
27
|
-
"id"
|
|
28
|
-
];
|
|
29
|
-
function c(e) {
|
|
30
|
-
return typeof e == "string" && e.length > 0 ? e : void 0;
|
|
31
|
-
}
|
|
32
|
-
function l(e) {
|
|
33
|
-
let t = (e) => e.filter((e) => !!e && typeof e == "object" && !Array.isArray(e));
|
|
34
|
-
if (Array.isArray(e)) return t(e);
|
|
35
|
-
if (e && typeof e == "object") {
|
|
36
|
-
let n = e;
|
|
37
|
-
for (let e of [
|
|
38
|
-
"tasks",
|
|
39
|
-
"rows",
|
|
40
|
-
"items",
|
|
41
|
-
"data",
|
|
42
|
-
"nodes",
|
|
43
|
-
"results",
|
|
44
|
-
"records"
|
|
45
|
-
]) if (Array.isArray(n[e])) return t(n[e]);
|
|
46
|
-
let r = Object.values(n).find((e) => Array.isArray(e));
|
|
47
|
-
if (Array.isArray(r)) return t(r);
|
|
48
|
-
}
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
function u(e, t) {
|
|
52
|
-
for (let n of t) if (n && n in e) return n;
|
|
53
|
-
}
|
|
54
|
-
function d(e) {
|
|
55
|
-
let t = c(e)?.toLowerCase();
|
|
56
|
-
if (t) {
|
|
57
|
-
if (t === "urgent" || t === "high") return "high";
|
|
58
|
-
if (t === "medium" || t === "normal") return "medium";
|
|
59
|
-
if (t === "low") return "low";
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function f(e, t) {
|
|
63
|
-
if (e && typeof e == "object" && !Array.isArray(e) && Array.isArray(e.columns)) {
|
|
64
|
-
let t = e;
|
|
65
|
-
return {
|
|
66
|
-
columns: t.columns ?? [],
|
|
67
|
-
tasks: t.tasks ?? []
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
let n = l(e);
|
|
71
|
-
if (n.length === 0) return {
|
|
72
|
-
columns: [],
|
|
73
|
-
tasks: []
|
|
74
|
-
};
|
|
75
|
-
let r = n[0], i = u(r, [
|
|
76
|
-
c(t?.columnField),
|
|
77
|
-
c(t?.groupField),
|
|
78
|
-
c(t?.statusField),
|
|
79
|
-
...o,
|
|
80
|
-
c(t?.xAxisField),
|
|
81
|
-
c(t?.labelField)
|
|
82
|
-
]);
|
|
83
|
-
if (!i) return {
|
|
84
|
-
columns: [],
|
|
85
|
-
tasks: []
|
|
86
|
-
};
|
|
87
|
-
let a = u(r, [
|
|
88
|
-
c(t?.cardTitleField),
|
|
89
|
-
c(t?.titleField),
|
|
90
|
-
...s,
|
|
91
|
-
c(t?.labelField)
|
|
92
|
-
]), f = {}, p = [], m = n.map((e, t) => {
|
|
93
|
-
let n = e[i], r = n == null || n === "" ? "Uncategorized" : String(n);
|
|
94
|
-
r in f || (f[r] = r, p.push(r));
|
|
95
|
-
let o = e.id ?? e.identifier ?? e.key, s = Array.isArray(e.labels) ? e.labels.filter((e) => typeof e == "string") : void 0;
|
|
96
|
-
return {
|
|
97
|
-
id: o == null ? `task-${t}` : String(o),
|
|
98
|
-
title: a && e[a] != null ? String(e[a]) : `Item ${t + 1}`,
|
|
99
|
-
column: r,
|
|
100
|
-
assignee: c(e.assignee),
|
|
101
|
-
priority: d(e.priority ?? e.priorityLabel),
|
|
102
|
-
labels: s && s.length > 0 ? s : void 0
|
|
103
|
-
};
|
|
104
|
-
});
|
|
105
|
-
return {
|
|
106
|
-
columns: p.map((e) => ({
|
|
107
|
-
id: e,
|
|
108
|
-
title: f[e] ?? e
|
|
109
|
-
})),
|
|
110
|
-
tasks: m
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
var p = e(function({ widget: e, data: o, onClick: s }) {
|
|
114
|
-
let { t: c } = i(), l = e.config, { columns: u, tasks: d } = t(() => f(o, l), [o, l]), p = t(() => {
|
|
115
|
-
let e = {};
|
|
116
|
-
return u.forEach((t) => {
|
|
117
|
-
e[t.id] = d.filter((e) => e.column === t.id);
|
|
118
|
-
}), e;
|
|
119
|
-
}, [u, d]);
|
|
120
|
-
return u.length ? /* @__PURE__ */ n("div", {
|
|
121
|
-
className: "flex h-full p-2 gap-2 overflow-x-auto",
|
|
122
|
-
children: u.map((e) => {
|
|
123
|
-
let t = p[e.id] || [];
|
|
124
|
-
return /* @__PURE__ */ r("div", {
|
|
125
|
-
className: "flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg",
|
|
126
|
-
children: [/* @__PURE__ */ r("div", {
|
|
127
|
-
className: "flex items-center gap-2 p-2 border-b border-border-default",
|
|
128
|
-
children: [
|
|
129
|
-
/* @__PURE__ */ n("div", {
|
|
130
|
-
className: "w-2 h-2 rounded-full",
|
|
131
|
-
style: { backgroundColor: e.color || "var(--color-text-muted)" }
|
|
132
|
-
}),
|
|
133
|
-
/* @__PURE__ */ n("span", {
|
|
134
|
-
className: "text-xs font-medium text-text-primary truncate",
|
|
135
|
-
children: e.title
|
|
136
|
-
}),
|
|
137
|
-
/* @__PURE__ */ n("span", {
|
|
138
|
-
className: "ml-auto text-xs text-text-muted",
|
|
139
|
-
children: t.length
|
|
140
|
-
})
|
|
141
|
-
]
|
|
142
|
-
}), /* @__PURE__ */ r("div", {
|
|
143
|
-
className: "flex-1 p-2 space-y-2 overflow-y-auto",
|
|
144
|
-
children: [t.map((e) => /* @__PURE__ */ r("div", {
|
|
145
|
-
className: `
|
|
146
|
-
p-2 bg-bg-surface rounded border border-border-default
|
|
147
|
-
${s ? "cursor-pointer hover:border-action-primary-bg" : ""}
|
|
148
|
-
transition-colors
|
|
149
|
-
`,
|
|
150
|
-
onClick: () => s?.(e),
|
|
151
|
-
children: [/* @__PURE__ */ n("p", {
|
|
152
|
-
className: "text-xs font-medium text-text-primary line-clamp-2",
|
|
153
|
-
children: e.title
|
|
154
|
-
}), /* @__PURE__ */ r("div", {
|
|
155
|
-
className: "flex items-center gap-1 mt-2 flex-wrap",
|
|
156
|
-
children: [e.priority && /* @__PURE__ */ n("span", {
|
|
157
|
-
className: `px-1.5 py-0.5 text-[10px] rounded ${a(e.priority)}`,
|
|
158
|
-
children: e.priority
|
|
159
|
-
}), e.assignee && /* @__PURE__ */ n("span", {
|
|
160
|
-
className: "px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text",
|
|
161
|
-
children: e.assignee
|
|
162
|
-
})]
|
|
163
|
-
})]
|
|
164
|
-
}, e.id)), t.length === 0 && /* @__PURE__ */ n("p", {
|
|
165
|
-
className: "text-[10px] text-text-muted text-center py-4",
|
|
166
|
-
children: c("bigconsole.widget.kanban.noTasks", "No tasks")
|
|
167
|
-
})]
|
|
168
|
-
})]
|
|
169
|
-
}, e.id);
|
|
170
|
-
})
|
|
171
|
-
}) : /* @__PURE__ */ n("div", {
|
|
172
|
-
className: "flex items-center justify-center h-full text-text-secondary text-sm",
|
|
173
|
-
children: /* @__PURE__ */ r("div", {
|
|
174
|
-
className: "text-center",
|
|
175
|
-
children: [/* @__PURE__ */ n("svg", {
|
|
176
|
-
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
177
|
-
fill: "none",
|
|
178
|
-
stroke: "currentColor",
|
|
179
|
-
viewBox: "0 0 24 24",
|
|
180
|
-
children: /* @__PURE__ */ n("path", {
|
|
181
|
-
strokeLinecap: "round",
|
|
182
|
-
strokeLinejoin: "round",
|
|
183
|
-
strokeWidth: 1.5,
|
|
184
|
-
d: "M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"
|
|
185
|
-
})
|
|
186
|
-
}), /* @__PURE__ */ n("p", { children: c("bigconsole.widget.kanban.emptyState", "No board data") })]
|
|
187
|
-
})
|
|
6
|
+
var i = t(function(t) {
|
|
7
|
+
let { t: i } = r();
|
|
8
|
+
return /* @__PURE__ */ n(e, {
|
|
9
|
+
...t,
|
|
10
|
+
t: i
|
|
188
11
|
});
|
|
189
12
|
});
|
|
190
13
|
//#endregion
|
|
191
|
-
export {
|
|
14
|
+
export { i as default };
|
|
192
15
|
|
|
193
16
|
//# sourceMappingURL=KanbanWidget.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-status-error-bg/20 text-status-error-text';\n case 'medium':\n return 'bg-status-warning-bg/20 text-[var(--color-status-warning-text)]';\n case 'low':\n return 'bg-status-success-bg/20 text-status-success-text';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// Field names we try, in order, when the widget config doesn't name one\n// explicitly. A DataSink/parser typically emits a flat array of rows (e.g. the\n// Linear \"Kanban board\" parser -> [{ id, title, status, assignee, ... }]), so\n// the board is derived from a status/column field on those rows.\nconst COLUMN_FIELD_CANDIDATES = ['status', 'state', 'column', 'stage', 'category', 'phase', 'group'];\nconst TITLE_FIELD_CANDIDATES = ['title', 'name', 'summary', 'label', 'identifier', 'id'];\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/** Coerce parser output into an array of object rows (handles common envelopes). */\nfunction asRows(data: unknown): Record<string, unknown>[] {\n const onlyObjects = (arr: unknown[]): Record<string, unknown>[] =>\n arr.filter((r): r is Record<string, unknown> => !!r && typeof r === 'object' && !Array.isArray(r));\n\n if (Array.isArray(data)) return onlyObjects(data);\n\n if (data && typeof data === 'object') {\n const obj = data as Record<string, unknown>;\n for (const key of ['tasks', 'rows', 'items', 'data', 'nodes', 'results', 'records']) {\n if (Array.isArray(obj[key])) return onlyObjects(obj[key] as unknown[]);\n }\n const firstArray = Object.values(obj).find((v) => Array.isArray(v));\n if (Array.isArray(firstArray)) return onlyObjects(firstArray);\n }\n return [];\n}\n\n/** First key that actually exists on the sample row. */\nfunction firstPresent(row: Record<string, unknown>, keys: (string | undefined)[]): string | undefined {\n for (const key of keys) {\n if (key && key in row) return key;\n }\n return undefined;\n}\n\nfunction normalizePriority(value: unknown): KanbanTask['priority'] {\n const v = asString(value)?.toLowerCase();\n if (!v) return undefined;\n if (v === 'urgent' || v === 'high') return 'high';\n if (v === 'medium' || v === 'normal') return 'medium';\n if (v === 'low') return 'low';\n return undefined;\n}\n\n/**\n * Normalize widget data into the board's { columns, tasks } shape.\n *\n * KanbanWidget previously required the exact canonical shape and rendered\n * \"No board data\" for any DataSink/parser output that was a plain array of\n * rows (which is what parsers actually emit) — BOFF-4110. This adapts a flat\n * array into columns (distinct values of a status/column field, in first-seen\n * order) plus one card per row, honoring config field mappings with fallbacks.\n */\nfunction normalizeKanban(\n data: unknown,\n config?: Record<string, unknown>\n): { columns: KanbanColumn[]; tasks: KanbanTask[] } {\n // Canonical shape passes through untouched (back-compat).\n if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray((data as KanbanData).columns)) {\n const d = data as KanbanData;\n return { columns: d.columns ?? [], tasks: d.tasks ?? [] };\n }\n\n const rows = asRows(data);\n if (rows.length === 0) return { columns: [], tasks: [] };\n\n const sample = rows[0];\n const columnField = firstPresent(sample, [\n asString(config?.columnField),\n asString(config?.groupField),\n asString(config?.statusField),\n ...COLUMN_FIELD_CANDIDATES,\n asString(config?.xAxisField),\n asString(config?.labelField),\n ]);\n if (!columnField) return { columns: [], tasks: [] };\n\n const titleField = firstPresent(sample, [\n asString(config?.cardTitleField),\n asString(config?.titleField),\n ...TITLE_FIELD_CANDIDATES,\n asString(config?.labelField),\n ]);\n\n const columnTitles: Record<string, string> = {};\n const columnOrder: string[] = [];\n const tasks: KanbanTask[] = rows.map((row, i) => {\n const columnRaw = row[columnField];\n const column = columnRaw == null || columnRaw === '' ? 'Uncategorized' : String(columnRaw);\n if (!(column in columnTitles)) {\n columnTitles[column] = column;\n columnOrder.push(column);\n }\n const idRaw = row.id ?? row.identifier ?? row.key;\n const labels = Array.isArray(row.labels) ? row.labels.filter((l): l is string => typeof l === 'string') : undefined;\n return {\n id: idRaw != null ? String(idRaw) : `task-${i}`,\n title: titleField && row[titleField] != null ? String(row[titleField]) : `Item ${i + 1}`,\n column,\n assignee: asString(row.assignee),\n priority: normalizePriority(row.priority ?? row.priorityLabel),\n labels: labels && labels.length > 0 ? labels : undefined,\n };\n });\n\n const columns: KanbanColumn[] = columnOrder.map((id) => ({ id, title: columnTitles[id] ?? id }));\n return { columns, tasks };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget, data, onClick }) {\n const { t } = useI18n();\n const config = widget.config as Record<string, unknown> | undefined;\n const { columns, tasks } = useMemo(() => normalizeKanban(data, config), [data, config]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-2 text-text-muted\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>{t('bigconsole.widget.kanban.emptyState', 'No board data')}</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-muted\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && (\n <p className=\"text-[10px] text-text-muted text-center py-4\">\n {t('bigconsole.widget.kanban.noTasks', 'No tasks')}\n </p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget — in-app wrapper around the pure KanbanView (BOFF-7926).\n *\n * The view is i18n-provider-free so the public embed bundle can render it with a\n * static translator (the fe-libs I18nProvider touches localStorage, which throws\n * in third-party iframes when the visitor blocks third-party storage).\n */\n\nimport { type FC, memo } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { KanbanView, type KanbanWidgetProps } from './KanbanView';\n\nexport * from './KanbanView';\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget(props) {\n const { t } = useI18n();\n return <KanbanView {...props} t={t} />;\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;;;AAcA,IAAa,IAAsC,EAAK,SAAsB,GAAO;CACnF,IAAM,EAAE,SAAM,GAAS;AACvB,QAAO,kBAAC,GAAD;EAAY,GAAI;EAAU;EAAK,CAAA;EACtC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { firstObjectRow as e, inferValueField as t, unwrapRows as n } from "../utils/inferFields.js";
|
|
2
|
+
import { conditionalStyleToClassName as r, resolveConditionalStyle as i } from "../utils/conditionalFormat.js";
|
|
3
3
|
import a from "./SparklineChart.js";
|
|
4
4
|
import { clampProgressWidth as o, getTargetProgress as s } from "../utils/targetProgress.js";
|
|
5
5
|
import c from "./TrendIndicator.js";
|
|
@@ -56,19 +56,19 @@ function y(e, t) {
|
|
|
56
56
|
var b = u(function({ widget: u, data: h, onClick: b, onDrilldown: x }) {
|
|
57
57
|
let S = u.config, C = f(() => {
|
|
58
58
|
if (!h) return;
|
|
59
|
-
let
|
|
59
|
+
let r = (e, t) => t.split(".").reduce((e, t) => e && typeof e == "object" ? e[t] : void 0, e);
|
|
60
60
|
if (typeof h == "number" || typeof h == "string") return { value: h };
|
|
61
|
-
let
|
|
62
|
-
if (
|
|
63
|
-
let
|
|
64
|
-
if (
|
|
65
|
-
let
|
|
61
|
+
let i = S?.valueField;
|
|
62
|
+
if (i && typeof h == "object") {
|
|
63
|
+
let e = h, t = r(e, i);
|
|
64
|
+
if (t !== void 0) {
|
|
65
|
+
let n = S?.previousValueField, i = n ? r(e, n) : void 0, a = i !== void 0 && i !== 0 && Number.isFinite(i) && typeof t == "number" ? (t - i) / i * 100 : void 0, o = h.changePercent, s = i === void 0 ? {} : {
|
|
66
66
|
previousValue: i,
|
|
67
67
|
changePercent: a ?? (Number.isFinite(o) ? o : void 0)
|
|
68
68
|
};
|
|
69
69
|
return {
|
|
70
70
|
...h,
|
|
71
|
-
value:
|
|
71
|
+
value: t,
|
|
72
72
|
...s
|
|
73
73
|
};
|
|
74
74
|
}
|
|
@@ -98,20 +98,20 @@ var b = u(function({ widget: u, data: h, onClick: b, onDrilldown: x }) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
if (typeof h == "number" || typeof h == "string") return { value: h };
|
|
101
|
-
let o =
|
|
101
|
+
let o = e(h);
|
|
102
102
|
if (o) {
|
|
103
|
-
let
|
|
104
|
-
if (
|
|
105
|
-
let
|
|
106
|
-
for (let
|
|
107
|
-
let
|
|
108
|
-
if (typeof
|
|
103
|
+
let e = S?.valueField;
|
|
104
|
+
if (e) {
|
|
105
|
+
let t = n(h) ?? [o];
|
|
106
|
+
for (let n of t) if (n && typeof n == "object") {
|
|
107
|
+
let t = r(n, e);
|
|
108
|
+
if (typeof t == "number" || typeof t == "string") return { value: t };
|
|
109
109
|
}
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
-
let
|
|
113
|
-
if (
|
|
114
|
-
let e = o[
|
|
112
|
+
let i = t(o);
|
|
113
|
+
if (i) {
|
|
114
|
+
let e = o[i];
|
|
115
115
|
if (typeof e == "number" || typeof e == "string") return { value: e };
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -119,7 +119,7 @@ var b = u(function({ widget: u, data: h, onClick: b, onDrilldown: x }) {
|
|
|
119
119
|
C?.value,
|
|
120
120
|
S,
|
|
121
121
|
D
|
|
122
|
-
]), k = f(() => s(C?.value, C?.target), [C?.value, C?.target]), A = f(() => y(w, E), [w, E]), j = f(() =>
|
|
122
|
+
]), k = f(() => s(C?.value, C?.target), [C?.value, C?.target]), A = f(() => y(w, E), [w, E]), j = f(() => r(i(C?.value, S?.conditionalRules)), [C?.value, S?.conditionalRules]), M = d(() => {
|
|
123
123
|
x ? x({
|
|
124
124
|
value: C?.value,
|
|
125
125
|
changePercent: C?.changePercent,
|
|
@@ -40,20 +40,38 @@ function o(e) {
|
|
|
40
40
|
return e >= 100 ? "bg-status-success-bg" : e >= 50 ? "bg-action-primary-bg" : e > 0 ? "bg-status-warning-bg" : "bg-bg-sunken";
|
|
41
41
|
}
|
|
42
42
|
var s = e(function({ widget: e, data: i, onClick: s }) {
|
|
43
|
-
let c = t(() => a(i), [i]), l = i?.milestones
|
|
44
|
-
|
|
43
|
+
let c = t(() => a(i), [i]), l = t(() => i?.milestones ?? [], [i]), { minDate: u, maxDate: d, totalDays: f } = t(() => {
|
|
44
|
+
let e = c.length ? c.flatMap((e) => [new Date(e.start), new Date(e.end)]) : l.map((e) => new Date(e.date));
|
|
45
|
+
if (!e.length) return {
|
|
45
46
|
minDate: /* @__PURE__ */ new Date(),
|
|
46
47
|
maxDate: /* @__PURE__ */ new Date(),
|
|
47
48
|
totalDays: 1
|
|
48
49
|
};
|
|
49
|
-
let
|
|
50
|
+
let t = new Date(Math.min(...e.map((e) => e.getTime()))), n = new Date(Math.max(...e.map((e) => e.getTime())));
|
|
50
51
|
return {
|
|
51
52
|
minDate: t,
|
|
52
53
|
maxDate: n,
|
|
53
54
|
totalDays: Math.ceil((n.getTime() - t.getTime()) / (1e3 * 60 * 60 * 24)) + 1
|
|
54
55
|
};
|
|
55
|
-
}, [c]);
|
|
56
|
-
return c.length ? /* @__PURE__ */
|
|
56
|
+
}, [c, l]);
|
|
57
|
+
return !c.length && !l.length ? /* @__PURE__ */ n("div", {
|
|
58
|
+
className: "flex items-center justify-center h-full text-text-secondary text-sm",
|
|
59
|
+
children: /* @__PURE__ */ r("div", {
|
|
60
|
+
className: "text-center",
|
|
61
|
+
children: [/* @__PURE__ */ n("svg", {
|
|
62
|
+
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
63
|
+
fill: "none",
|
|
64
|
+
stroke: "currentColor",
|
|
65
|
+
viewBox: "0 0 24 24",
|
|
66
|
+
children: /* @__PURE__ */ n("path", {
|
|
67
|
+
strokeLinecap: "round",
|
|
68
|
+
strokeLinejoin: "round",
|
|
69
|
+
strokeWidth: 1.5,
|
|
70
|
+
d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
|
71
|
+
})
|
|
72
|
+
}), /* @__PURE__ */ n("p", { children: "No timeline data" })]
|
|
73
|
+
})
|
|
74
|
+
}) : /* @__PURE__ */ r("div", {
|
|
57
75
|
className: "flex flex-col h-full p-4 overflow-auto",
|
|
58
76
|
children: [
|
|
59
77
|
/* @__PURE__ */ r("div", {
|
|
@@ -157,23 +175,6 @@ var s = e(function({ widget: e, data: i, onClick: s }) {
|
|
|
157
175
|
})]
|
|
158
176
|
})
|
|
159
177
|
]
|
|
160
|
-
}) : /* @__PURE__ */ n("div", {
|
|
161
|
-
className: "flex items-center justify-center h-full text-text-secondary text-sm",
|
|
162
|
-
children: /* @__PURE__ */ r("div", {
|
|
163
|
-
className: "text-center",
|
|
164
|
-
children: [/* @__PURE__ */ n("svg", {
|
|
165
|
-
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
166
|
-
fill: "none",
|
|
167
|
-
stroke: "currentColor",
|
|
168
|
-
viewBox: "0 0 24 24",
|
|
169
|
-
children: /* @__PURE__ */ n("path", {
|
|
170
|
-
strokeLinecap: "round",
|
|
171
|
-
strokeLinejoin: "round",
|
|
172
|
-
strokeWidth: 1.5,
|
|
173
|
-
d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
|
174
|
-
})
|
|
175
|
-
}), /* @__PURE__ */ n("p", { children: "No timeline data" })]
|
|
176
|
-
})
|
|
177
178
|
});
|
|
178
179
|
});
|
|
179
180
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimelineWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/timeline/TimelineWidget.tsx"],"sourcesContent":["/**\n * TimelineWidget Component\n *\n * Simple timeline/gantt view showing project tasks and milestones.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TimelineWidgetProps {\n widget: Widget;\n data?: TimelineData;\n onClick?: (task: TimelineTask) => void;\n}\n\nexport interface TimelineTask {\n id: string;\n name: string;\n start: string;\n end: string;\n progress?: number;\n color?: string;\n duration?: number;\n dependencies?: string[];\n}\n\nexport interface TimelineMilestone {\n id: string;\n name: string;\n date: string;\n color?: string;\n}\n\nexport interface TimelineData {\n tasks?: TimelineTask[];\n milestones?: TimelineMilestone[];\n}\n\n// ============================================================================\n// Normalization\n// ============================================================================\n\nfunction firstDefined<T = unknown>(obj: Record<string, unknown>, keys: string[]): T | undefined {\n for (const key of keys) {\n if (obj[key] !== undefined && obj[key] !== null) return obj[key] as T;\n }\n return undefined;\n}\n\n/**\n * Normalize whatever the widget is bound to into `TimelineTask[]`. Accepts the\n * documented `{ tasks, ... }` shape, a tasks array under a different common key,\n * or a bare array of task-like records — a DataSink parser naturally outputs a\n * bare array, and WidgetWrapper has no timeline-specific reshaping step (unlike\n * table/list/chart), so this widget has to tolerate it itself, the same way\n * MapWidget tolerates a bare marker array instead of requiring `{ markers }`.\n * Field names are matched against common aliases (e.g. a parser emitting\n * `title` instead of `name`) so one exact parser output shape isn't required.\n */\nexport function normalizeTimelineTasks(\n data: TimelineData | TimelineTask[] | Record<string, unknown> | undefined\n): TimelineTask[] {\n const rawTasks: unknown = Array.isArray(data)\n ? data\n : Array.isArray((data as TimelineData | undefined)?.tasks)\n ? (data as TimelineData).tasks\n : Array.isArray((data as Record<string, unknown> | undefined)?.items)\n ? (data as Record<string, unknown>).items\n : Array.isArray((data as Record<string, unknown> | undefined)?.data)\n ? (data as Record<string, unknown>).data\n : undefined;\n\n if (!Array.isArray(rawTasks)) return [];\n\n return rawTasks\n .map((raw, i): TimelineTask | null => {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const start = firstDefined<string>(r, ['start', 'startDate', 'date']);\n const end = firstDefined<string>(r, ['end', 'endDate']) ?? start;\n if (!start || !end) return null;\n const name = firstDefined<string>(r, ['name', 'title', 'label', 'task']) ?? 'Untitled task';\n const id = firstDefined<string>(r, ['id', 'taskId', '_id']) ?? `${name}-${start}-${i}`;\n return {\n id: String(id),\n name: String(name),\n start: String(start),\n end: String(end),\n progress: typeof r.progress === 'number' ? r.progress : undefined,\n color: typeof r.color === 'string' ? r.color : undefined,\n duration: typeof r.duration === 'number' ? r.duration : undefined,\n dependencies: Array.isArray(r.dependencies) ? (r.dependencies as string[]) : undefined,\n };\n })\n .filter((t): t is TimelineTask => t !== null);\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getProgressColor(progress: number): string {\n if (progress >= 100) return 'bg-status-success-bg';\n if (progress >= 50) return 'bg-action-primary-bg';\n if (progress > 0) return 'bg-status-warning-bg';\n return 'bg-bg-sunken';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TimelineWidget: FC<TimelineWidgetProps> = memo(function TimelineWidget({\n widget: _widget,\n data,\n onClick,\n}) {\n const tasks = useMemo(() => normalizeTimelineTasks(data), [data]);\n const milestones = data?.milestones || [];\n\n // Calculate date range\n const { minDate, maxDate, totalDays } = useMemo(() => {\n if (!tasks.length) {\n return { minDate: new Date(), maxDate: new Date(), totalDays: 1 };\n }\n\n const dates = tasks.flatMap((t) => [new Date(t.start), new Date(t.end)]);\n const min = new Date(Math.min(...dates.map((d) => d.getTime())));\n const max = new Date(Math.max(...dates.map((d) => d.getTime())));\n const days = Math.ceil((max.getTime() - min.getTime()) / (1000 * 60 * 60 * 24)) + 1;\n\n return { minDate: min, maxDate: max, totalDays: days };\n }, [tasks]);\n\n if (!tasks.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-2 text-text-muted\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"\n />\n </svg>\n <p>No timeline data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full p-4 overflow-auto\">\n {/* Header with date range */}\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-sm font-medium text-text-primary\">Project Timeline</h3>\n <span className=\"text-xs text-text-secondary\">\n {minDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} -{' '}\n {maxDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n </span>\n </div>\n\n {/* Timeline content */}\n <div className=\"flex-1 space-y-3 overflow-auto\">\n {tasks.map((task) => {\n const startDate = new Date(task.start);\n const endDate = new Date(task.end);\n const taskDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;\n const startOffset = Math.ceil((startDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24));\n const progress = task.progress ?? 0;\n\n // Calculate bar position and width as percentages\n const leftPercent = (startOffset / totalDays) * 100;\n const widthPercent = (taskDays / totalDays) * 100;\n\n return (\n <div\n key={task.id}\n className={`\n ${onClick ? 'cursor-pointer' : ''}\n `}\n onClick={() => onClick?.(task)}\n >\n {/* Task name and progress */}\n <div className=\"flex items-center justify-between mb-1\">\n <span className=\"text-xs font-medium text-text-primary truncate max-w-[60%]\">{task.name}</span>\n <span className=\"text-[10px] text-text-secondary\">{progress}%</span>\n </div>\n\n {/* Timeline bar */}\n <div className=\"relative h-6 bg-bg-sunken rounded overflow-hidden\">\n {/* Task bar */}\n <div\n className=\"absolute top-1 bottom-1 rounded\"\n style={{\n left: `${leftPercent}%`,\n width: `${Math.max(widthPercent, 5)}%`,\n backgroundColor: task.color || 'var(--color-chart-1)',\n opacity: 0.3,\n }}\n />\n\n {/* Progress fill */}\n <div\n className={`absolute top-1 bottom-1 rounded ${getProgressColor(progress)}`}\n style={{\n left: `${leftPercent}%`,\n width: `${Math.max((widthPercent * progress) / 100, 2)}%`,\n }}\n />\n\n {/* Date labels */}\n <div\n className=\"absolute top-1/2 -translate-y-1/2 text-[9px] text-text-muted\"\n style={{ left: `${leftPercent + 1}%` }}\n >\n {startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Milestones */}\n {milestones.length > 0 && (\n <div className=\"mt-4 pt-3 border-t border-border-default\">\n <p className=\"text-xs font-medium text-text-secondary mb-2\">Milestones</p>\n <div className=\"flex flex-wrap gap-2\">\n {milestones.map((milestone) => (\n <div key={milestone.id} className=\"flex items-center gap-1.5 px-2 py-1 rounded bg-bg-sunken\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: milestone.color || 'var(--color-chart-negative)' }}\n />\n <span className=\"text-[10px] text-text-primary\">{milestone.name}</span>\n <span className=\"text-[10px] text-text-muted\">\n {new Date(milestone.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n});\n\nexport default TimelineWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TimelineWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/timeline/TimelineWidget.tsx"],"sourcesContent":["/**\n * TimelineWidget Component\n *\n * Simple timeline/gantt view showing project tasks and milestones.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TimelineWidgetProps {\n widget: Widget;\n data?: TimelineData;\n onClick?: (task: TimelineTask) => void;\n}\n\nexport interface TimelineTask {\n id: string;\n name: string;\n start: string;\n end: string;\n progress?: number;\n color?: string;\n duration?: number;\n dependencies?: string[];\n}\n\nexport interface TimelineMilestone {\n id: string;\n name: string;\n date: string;\n color?: string;\n}\n\nexport interface TimelineData {\n tasks?: TimelineTask[];\n milestones?: TimelineMilestone[];\n}\n\n// ============================================================================\n// Normalization\n// ============================================================================\n\nfunction firstDefined<T = unknown>(obj: Record<string, unknown>, keys: string[]): T | undefined {\n for (const key of keys) {\n if (obj[key] !== undefined && obj[key] !== null) return obj[key] as T;\n }\n return undefined;\n}\n\n/**\n * Normalize whatever the widget is bound to into `TimelineTask[]`. Accepts the\n * documented `{ tasks, ... }` shape, a tasks array under a different common key,\n * or a bare array of task-like records — a DataSink parser naturally outputs a\n * bare array, and WidgetWrapper has no timeline-specific reshaping step (unlike\n * table/list/chart), so this widget has to tolerate it itself, the same way\n * MapWidget tolerates a bare marker array instead of requiring `{ markers }`.\n * Field names are matched against common aliases (e.g. a parser emitting\n * `title` instead of `name`) so one exact parser output shape isn't required.\n */\nexport function normalizeTimelineTasks(\n data: TimelineData | TimelineTask[] | Record<string, unknown> | undefined\n): TimelineTask[] {\n const rawTasks: unknown = Array.isArray(data)\n ? data\n : Array.isArray((data as TimelineData | undefined)?.tasks)\n ? (data as TimelineData).tasks\n : Array.isArray((data as Record<string, unknown> | undefined)?.items)\n ? (data as Record<string, unknown>).items\n : Array.isArray((data as Record<string, unknown> | undefined)?.data)\n ? (data as Record<string, unknown>).data\n : undefined;\n\n if (!Array.isArray(rawTasks)) return [];\n\n return rawTasks\n .map((raw, i): TimelineTask | null => {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const start = firstDefined<string>(r, ['start', 'startDate', 'date']);\n const end = firstDefined<string>(r, ['end', 'endDate']) ?? start;\n if (!start || !end) return null;\n const name = firstDefined<string>(r, ['name', 'title', 'label', 'task']) ?? 'Untitled task';\n const id = firstDefined<string>(r, ['id', 'taskId', '_id']) ?? `${name}-${start}-${i}`;\n return {\n id: String(id),\n name: String(name),\n start: String(start),\n end: String(end),\n progress: typeof r.progress === 'number' ? r.progress : undefined,\n color: typeof r.color === 'string' ? r.color : undefined,\n duration: typeof r.duration === 'number' ? r.duration : undefined,\n dependencies: Array.isArray(r.dependencies) ? (r.dependencies as string[]) : undefined,\n };\n })\n .filter((t): t is TimelineTask => t !== null);\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getProgressColor(progress: number): string {\n if (progress >= 100) return 'bg-status-success-bg';\n if (progress >= 50) return 'bg-action-primary-bg';\n if (progress > 0) return 'bg-status-warning-bg';\n return 'bg-bg-sunken';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TimelineWidget: FC<TimelineWidgetProps> = memo(function TimelineWidget({\n widget: _widget,\n data,\n onClick,\n}) {\n const tasks = useMemo(() => normalizeTimelineTasks(data), [data]);\n const milestones = useMemo(() => data?.milestones ?? [], [data]);\n\n // Calculate date range from the tasks; a milestone-only timeline ranges over its milestones.\n const { minDate, maxDate, totalDays } = useMemo(() => {\n const dates = tasks.length\n ? tasks.flatMap((t) => [new Date(t.start), new Date(t.end)])\n : milestones.map((m) => new Date(m.date));\n if (!dates.length) {\n return { minDate: new Date(), maxDate: new Date(), totalDays: 1 };\n }\n\n const min = new Date(Math.min(...dates.map((d) => d.getTime())));\n const max = new Date(Math.max(...dates.map((d) => d.getTime())));\n const days = Math.ceil((max.getTime() - min.getTime()) / (1000 * 60 * 60 * 24)) + 1;\n\n return { minDate: min, maxDate: max, totalDays: days };\n }, [tasks, milestones]);\n\n if (!tasks.length && !milestones.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-2 text-text-muted\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"\n />\n </svg>\n <p>No timeline data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col h-full p-4 overflow-auto\">\n {/* Header with date range */}\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-sm font-medium text-text-primary\">Project Timeline</h3>\n <span className=\"text-xs text-text-secondary\">\n {minDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} -{' '}\n {maxDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n </span>\n </div>\n\n {/* Timeline content */}\n <div className=\"flex-1 space-y-3 overflow-auto\">\n {tasks.map((task) => {\n const startDate = new Date(task.start);\n const endDate = new Date(task.end);\n const taskDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;\n const startOffset = Math.ceil((startDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24));\n const progress = task.progress ?? 0;\n\n // Calculate bar position and width as percentages\n const leftPercent = (startOffset / totalDays) * 100;\n const widthPercent = (taskDays / totalDays) * 100;\n\n return (\n <div\n key={task.id}\n className={`\n ${onClick ? 'cursor-pointer' : ''}\n `}\n onClick={() => onClick?.(task)}\n >\n {/* Task name and progress */}\n <div className=\"flex items-center justify-between mb-1\">\n <span className=\"text-xs font-medium text-text-primary truncate max-w-[60%]\">{task.name}</span>\n <span className=\"text-[10px] text-text-secondary\">{progress}%</span>\n </div>\n\n {/* Timeline bar */}\n <div className=\"relative h-6 bg-bg-sunken rounded overflow-hidden\">\n {/* Task bar */}\n <div\n className=\"absolute top-1 bottom-1 rounded\"\n style={{\n left: `${leftPercent}%`,\n width: `${Math.max(widthPercent, 5)}%`,\n backgroundColor: task.color || 'var(--color-chart-1)',\n opacity: 0.3,\n }}\n />\n\n {/* Progress fill */}\n <div\n className={`absolute top-1 bottom-1 rounded ${getProgressColor(progress)}`}\n style={{\n left: `${leftPercent}%`,\n width: `${Math.max((widthPercent * progress) / 100, 2)}%`,\n }}\n />\n\n {/* Date labels */}\n <div\n className=\"absolute top-1/2 -translate-y-1/2 text-[9px] text-text-muted\"\n style={{ left: `${leftPercent + 1}%` }}\n >\n {startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Milestones */}\n {milestones.length > 0 && (\n <div className=\"mt-4 pt-3 border-t border-border-default\">\n <p className=\"text-xs font-medium text-text-secondary mb-2\">Milestones</p>\n <div className=\"flex flex-wrap gap-2\">\n {milestones.map((milestone) => (\n <div key={milestone.id} className=\"flex items-center gap-1.5 px-2 py-1 rounded bg-bg-sunken\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: milestone.color || 'var(--color-chart-negative)' }}\n />\n <span className=\"text-[10px] text-text-primary\">{milestone.name}</span>\n <span className=\"text-[10px] text-text-muted\">\n {new Date(milestone.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n});\n\nexport default TimelineWidget;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/bigconsole/components/widgets/utils/translate.ts
|
|
2
|
+
var e = (e, t, n) => {
|
|
3
|
+
let r = typeof t == "string" ? t : e, i = typeof t == "object" ? t : n;
|
|
4
|
+
return i ? r.replace(/\{\{\s*(\w+)\s*\}\}/g, (e, t) => Object.prototype.hasOwnProperty.call(i, t) ? String(i[t]) : e) : r;
|
|
5
|
+
};
|
|
6
|
+
//#endregion
|
|
7
|
+
export { e as fallbackTranslate };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=translate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/utils/translate.ts"],"sourcesContent":["/**\n * Provider-free translator contract (BOFF-7926).\n *\n * Mirrors the fe-libs `useI18n().t` call shape used by widget views, so a view\n * can be rendered either in-app (real translator) or in the public embed bundle\n * (static fallback translator, no provider, no storage access).\n */\n\nexport type WidgetTranslateFn = (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n) => string;\n\n/**\n * Static translator: returns the inline English fallback with `{{name}}`\n * placeholders interpolated. Used where no I18nProvider is mounted.\n */\nexport const fallbackTranslate: WidgetTranslateFn = (key, fallbackOrParams, params) => {\n const template = typeof fallbackOrParams === 'string' ? fallbackOrParams : key;\n const values = typeof fallbackOrParams === 'object' ? fallbackOrParams : params;\n if (!values) return template;\n return template.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : match\n );\n};\n"],"mappings":";AAkBA,IAAa,KAAwC,GAAK,GAAkB,MAAW;CACrF,IAAM,IAAW,OAAO,KAAqB,WAAW,IAAmB,GACrE,IAAS,OAAO,KAAqB,WAAW,IAAmB;AAEzE,QADK,IACE,EAAS,QAAQ,yBAAyB,GAAO,MACtD,OAAO,UAAU,eAAe,KAAK,GAAQ,EAAK,GAAG,OAAO,EAAO,GAAM,GAAG,EAC7E,GAHmB"}
|
|
@@ -50,8 +50,7 @@ function p() {
|
|
|
50
50
|
allowedParentOrigins: r,
|
|
51
51
|
...a ? {
|
|
52
52
|
presentationTheme: u[a.theme],
|
|
53
|
-
showDashboardTitle: a.showTitle
|
|
54
|
-
showBrandingFooter: a.showBranding
|
|
53
|
+
showDashboardTitle: a.showTitle
|
|
55
54
|
} : {}
|
|
56
55
|
} }
|
|
57
56
|
})).data?.createDashboardEmbedPolicy;
|
|
@@ -81,8 +80,7 @@ function p() {
|
|
|
81
80
|
variables: { input: {
|
|
82
81
|
policyId: e,
|
|
83
82
|
presentationTheme: u[t.theme],
|
|
84
|
-
showDashboardTitle: t.showTitle
|
|
85
|
-
showBrandingFooter: t.showBranding
|
|
83
|
+
showDashboardTitle: t.showTitle
|
|
86
84
|
} }
|
|
87
85
|
})).data?.updateDashboardEmbedPolicy ?? null;
|
|
88
86
|
return n && d(n), n;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDashboardEmbedPolicy.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardEmbedPolicy.ts"],"sourcesContent":["/**\n * useDashboardEmbedPolicy Hook\n *\n * Owner-facing operations for publishing a dashboard to the web (anonymous-link\n * embed). Mirrors {@link useDashboardSharing}. All mutations are gateway-enforced\n * by `@rbac(manage_embed)` (owner-only). Publishing serves the dashboard's LIVE\n * data anonymously — the caller confirms content-safety before enabling.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport {\n ListDashboardEmbedPoliciesDocument,\n CreateDashboardEmbedPolicyDocument,\n PublishDashboardEmbedPolicyDocument,\n UpdateDashboardEmbedPolicyDocument,\n RotateDashboardEmbedPolicyDocument,\n RevokeDashboardEmbedPolicyDocument,\n} from '../../generated/wspace-operations';\nimport { DashboardEmbedTheme } from '../../generated/wspace-types';\nimport type { EmbedThemeMode } from '../components/embed/types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type EmbedPolicyStatus = 'DRAFT' | 'ACTIVE' | 'NEEDS_REVIEW' | 'BROKEN' | 'REVOKED' | 'EXPIRED';\n\n/**\n * Owner-facing color mode (BOFF-5735); maps to the backend {@link DashboardEmbedTheme}\n * enum. Same shape as the render-model's {@link EmbedThemeMode} — aliased to avoid a\n * duplicate union.\n */\nexport type EmbedThemeChoice = EmbedThemeMode;\n\n/** Publish-time presentation options the owner controls in the Share dialog. */\nexport interface EmbedPresentationChoice {\n theme: EmbedThemeChoice;\n showTitle: boolean;\n showBranding: boolean;\n}\n\nconst THEME_TO_ENUM: Record<EmbedThemeChoice, DashboardEmbedTheme> = {\n light: DashboardEmbedTheme.Light,\n dark: DashboardEmbedTheme.Dark,\n auto: DashboardEmbedTheme.Auto,\n};\n\n/** Map the backend enum back to the lowercase owner-facing choice (defaults to light). */\nexport function themeChoiceFromPolicy(value: string | null | undefined): EmbedThemeChoice {\n switch (value) {\n case DashboardEmbedTheme.Dark:\n return 'dark';\n case DashboardEmbedTheme.Auto:\n return 'auto';\n default:\n return 'light';\n }\n}\n\nexport interface DashboardEmbedPolicy {\n id: string;\n dashboardId: string;\n status: EmbedPolicyStatus;\n accessMode: string;\n version: number;\n allowedParentOrigins: string[];\n expiresAt?: string | null;\n sessionTtlSeconds: number;\n contentReviewState: string;\n presentationTheme: DashboardEmbedTheme;\n showDashboardTitle: boolean;\n showBrandingFooter: boolean;\n activePublicationId?: string | null;\n locatorPublicId?: string | null;\n fingerprint?: string | null;\n createdAt: string;\n updatedAt: string;\n activatedAt?: string | null;\n revokedAt?: string | null;\n}\n\n/** Returned once on create / rotate — carries the secret embed URL + snippet. */\nexport interface EmbedPolicySecret {\n embedUrl: string;\n iframeSnippet: string;\n fingerprint: string;\n policy: DashboardEmbedPolicy;\n}\n\nexport interface UseDashboardEmbedPolicyResult {\n /** The dashboard's active (or most recent) embed policy, if any. */\n policy: DashboardEmbedPolicy | null;\n /** The one-time secret from the last create/rotate (embed URL + iframe snippet). */\n secret: EmbedPolicySecret | null;\n loading: boolean;\n error: Error | null;\n\n fetchPolicy: (dashboardId: string) => Promise<DashboardEmbedPolicy | null>;\n /** Create (if needed) + publish → returns the live embed secret. */\n publishToWeb: (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean,\n presentation?: EmbedPresentationChoice\n ) => Promise<EmbedPolicySecret | null>;\n /**\n * Persist the presentation options of an already-published policy. They are\n * read live by the embed renderer, so the change applies on the next render\n * with no re-publish — the embed link/key are untouched.\n */\n applyPresentation: (policyId: string, presentation: EmbedPresentationChoice) => Promise<DashboardEmbedPolicy | null>;\n setAllowedOrigins: (policyId: string, allowedParentOrigins: string[]) => Promise<DashboardEmbedPolicy | null>;\n rotateKey: (policyId: string) => Promise<EmbedPolicySecret | null>;\n /** Unpublish: revoke the policy (embed stops serving immediately). */\n unpublish: (policyId: string) => Promise<boolean>;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/** Pick the live-serving policy (ACTIVE) else the most recently updated one. */\nfunction pickCurrent(policies: DashboardEmbedPolicy[]): DashboardEmbedPolicy | null {\n if (policies.length === 0) return null;\n const active = policies.find((p) => p.status === 'ACTIVE');\n if (active) return active;\n return [...policies].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;\n}\n\nexport function useDashboardEmbedPolicy(): UseDashboardEmbedPolicyResult {\n const [policy, setPolicy] = useState<DashboardEmbedPolicy | null>(null);\n const [secret, setSecret] = useState<EmbedPolicySecret | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const apolloClient = useApolloClient();\n\n const fetchPolicy = useCallback(\n async (dashboardId: string): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.query<{\n listDashboardEmbedPolicies?: { nodes?: DashboardEmbedPolicy[] };\n }>({\n query: ListDashboardEmbedPoliciesDocument,\n variables: { dashboardId },\n fetchPolicy: 'network-only',\n });\n const current = pickCurrent(result.data?.listDashboardEmbedPolicies?.nodes ?? []);\n setPolicy(current);\n return current;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load embed policy'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const publishToWeb = useCallback(\n async (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean,\n presentation?: EmbedPresentationChoice\n ): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n\n // Create a DRAFT policy + anonymous-link locator (returns the secret once).\n // Presentation options (BOFF-5735) are captured here so the very first\n // render reflects them; they're read live thereafter.\n const created = await apolloClient.mutate<{ createDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: CreateDashboardEmbedPolicyDocument,\n variables: {\n input: {\n dashboardId,\n allowedParentOrigins,\n ...(presentation\n ? {\n presentationTheme: THEME_TO_ENUM[presentation.theme],\n showDashboardTitle: presentation.showTitle,\n showBrandingFooter: presentation.showBranding,\n }\n : {}),\n },\n },\n });\n const createdSecret = created.data?.createDashboardEmbedPolicy;\n if (!createdSecret) return null;\n\n // Activate it for live embedding (owner attests content-safety).\n const published = await apolloClient.mutate<{ publishDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: PublishDashboardEmbedPolicyDocument,\n variables: { policyId: createdSecret.policy.id, contentSafetyAttested },\n });\n const activePolicy = published.data?.publishDashboardEmbedPolicy ?? createdSecret.policy;\n\n const result: EmbedPolicySecret = { ...createdSecret, policy: activePolicy };\n setSecret(result);\n setPolicy(activePolicy);\n return result;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to publish to web'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const applyPresentation = useCallback(\n async (policyId: string, presentation: EmbedPresentationChoice): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n // Presentation is read LIVE at view time by the embed renderer, so simply\n // persisting the choices on the policy is enough — no re-publish, and the\n // embed link/key are untouched. Live embeds pick up the change on their\n // next render (bounded by the service's short render-cache window).\n const result = await apolloClient.mutate<{ updateDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: UpdateDashboardEmbedPolicyDocument,\n variables: {\n input: {\n policyId,\n presentationTheme: THEME_TO_ENUM[presentation.theme],\n showDashboardTitle: presentation.showTitle,\n showBrandingFooter: presentation.showBranding,\n },\n },\n });\n const updated = result.data?.updateDashboardEmbedPolicy ?? null;\n if (updated) setPolicy(updated);\n return updated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to update presentation'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const setAllowedOrigins = useCallback(\n async (policyId: string, allowedParentOrigins: string[]): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ updateDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: UpdateDashboardEmbedPolicyDocument,\n variables: { input: { policyId, allowedParentOrigins } },\n });\n const updated = result.data?.updateDashboardEmbedPolicy ?? null;\n if (updated) setPolicy(updated);\n return updated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to update allowed domains'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const rotateKey = useCallback(\n async (policyId: string): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ rotateDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: RotateDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const rotated = result.data?.rotateDashboardEmbedPolicy ?? null;\n if (rotated) {\n setSecret(rotated);\n setPolicy(rotated.policy);\n }\n return rotated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to rotate embed link'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const unpublish = useCallback(\n async (policyId: string): Promise<boolean> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ revokeDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: RevokeDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const revoked = result.data?.revokeDashboardEmbedPolicy ?? null;\n if (revoked) {\n setPolicy(revoked);\n setSecret(null);\n return true;\n }\n return false;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to unpublish'));\n return false;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n return {\n policy,\n secret,\n loading,\n error,\n fetchPolicy,\n publishToWeb,\n applyPresentation,\n setAllowedOrigins,\n rotateKey,\n unpublish,\n };\n}\n\nexport default useDashboardEmbedPolicy;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useDashboardEmbedPolicy.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardEmbedPolicy.ts"],"sourcesContent":["/**\n * useDashboardEmbedPolicy Hook\n *\n * Owner-facing operations for publishing a dashboard to the web (anonymous-link\n * embed). Mirrors {@link useDashboardSharing}. All mutations are gateway-enforced\n * by `@rbac(manage_embed)` (owner-only). Publishing serves the dashboard's LIVE\n * data anonymously — the caller confirms content-safety before enabling.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport {\n ListDashboardEmbedPoliciesDocument,\n CreateDashboardEmbedPolicyDocument,\n PublishDashboardEmbedPolicyDocument,\n UpdateDashboardEmbedPolicyDocument,\n RotateDashboardEmbedPolicyDocument,\n RevokeDashboardEmbedPolicyDocument,\n} from '../../generated/wspace-operations';\nimport { DashboardEmbedTheme } from '../../generated/wspace-types';\nimport type { EmbedThemeMode } from '../components/embed/types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type EmbedPolicyStatus = 'DRAFT' | 'ACTIVE' | 'NEEDS_REVIEW' | 'BROKEN' | 'REVOKED' | 'EXPIRED';\n\n/**\n * Owner-facing color mode (BOFF-5735); maps to the backend {@link DashboardEmbedTheme}\n * enum. Same shape as the render-model's {@link EmbedThemeMode} — aliased to avoid a\n * duplicate union.\n */\nexport type EmbedThemeChoice = EmbedThemeMode;\n\n/**\n * Publish-time presentation options the owner controls in the Share dialog.\n * Branding is deliberately absent: every embed carries the \"Powered by\n * BigConsole\" footer (Burdenoff does no white-labelling).\n */\nexport interface EmbedPresentationChoice {\n theme: EmbedThemeChoice;\n showTitle: boolean;\n}\n\nconst THEME_TO_ENUM: Record<EmbedThemeChoice, DashboardEmbedTheme> = {\n light: DashboardEmbedTheme.Light,\n dark: DashboardEmbedTheme.Dark,\n auto: DashboardEmbedTheme.Auto,\n};\n\n/** Map the backend enum back to the lowercase owner-facing choice (defaults to light). */\nexport function themeChoiceFromPolicy(value: string | null | undefined): EmbedThemeChoice {\n switch (value) {\n case DashboardEmbedTheme.Dark:\n return 'dark';\n case DashboardEmbedTheme.Auto:\n return 'auto';\n default:\n return 'light';\n }\n}\n\nexport interface DashboardEmbedPolicy {\n id: string;\n dashboardId: string;\n status: EmbedPolicyStatus;\n accessMode: string;\n version: number;\n allowedParentOrigins: string[];\n expiresAt?: string | null;\n sessionTtlSeconds: number;\n contentReviewState: string;\n presentationTheme: DashboardEmbedTheme;\n showDashboardTitle: boolean;\n activePublicationId?: string | null;\n locatorPublicId?: string | null;\n fingerprint?: string | null;\n createdAt: string;\n updatedAt: string;\n activatedAt?: string | null;\n revokedAt?: string | null;\n}\n\n/** Returned once on create / rotate — carries the secret embed URL + snippet. */\nexport interface EmbedPolicySecret {\n embedUrl: string;\n iframeSnippet: string;\n fingerprint: string;\n policy: DashboardEmbedPolicy;\n}\n\nexport interface UseDashboardEmbedPolicyResult {\n /** The dashboard's active (or most recent) embed policy, if any. */\n policy: DashboardEmbedPolicy | null;\n /** The one-time secret from the last create/rotate (embed URL + iframe snippet). */\n secret: EmbedPolicySecret | null;\n loading: boolean;\n error: Error | null;\n\n fetchPolicy: (dashboardId: string) => Promise<DashboardEmbedPolicy | null>;\n /** Create (if needed) + publish → returns the live embed secret. */\n publishToWeb: (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean,\n presentation?: EmbedPresentationChoice\n ) => Promise<EmbedPolicySecret | null>;\n /**\n * Persist the presentation options of an already-published policy. They are\n * read live by the embed renderer, so the change applies on the next render\n * with no re-publish — the embed link/key are untouched.\n */\n applyPresentation: (policyId: string, presentation: EmbedPresentationChoice) => Promise<DashboardEmbedPolicy | null>;\n setAllowedOrigins: (policyId: string, allowedParentOrigins: string[]) => Promise<DashboardEmbedPolicy | null>;\n rotateKey: (policyId: string) => Promise<EmbedPolicySecret | null>;\n /** Unpublish: revoke the policy (embed stops serving immediately). */\n unpublish: (policyId: string) => Promise<boolean>;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/** Pick the live-serving policy (ACTIVE) else the most recently updated one. */\nfunction pickCurrent(policies: DashboardEmbedPolicy[]): DashboardEmbedPolicy | null {\n if (policies.length === 0) return null;\n const active = policies.find((p) => p.status === 'ACTIVE');\n if (active) return active;\n return [...policies].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;\n}\n\nexport function useDashboardEmbedPolicy(): UseDashboardEmbedPolicyResult {\n const [policy, setPolicy] = useState<DashboardEmbedPolicy | null>(null);\n const [secret, setSecret] = useState<EmbedPolicySecret | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const apolloClient = useApolloClient();\n\n const fetchPolicy = useCallback(\n async (dashboardId: string): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.query<{\n listDashboardEmbedPolicies?: { nodes?: DashboardEmbedPolicy[] };\n }>({\n query: ListDashboardEmbedPoliciesDocument,\n variables: { dashboardId },\n fetchPolicy: 'network-only',\n });\n const current = pickCurrent(result.data?.listDashboardEmbedPolicies?.nodes ?? []);\n setPolicy(current);\n return current;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load embed policy'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const publishToWeb = useCallback(\n async (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean,\n presentation?: EmbedPresentationChoice\n ): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n\n // Create a DRAFT policy + anonymous-link locator (returns the secret once).\n // Presentation options (BOFF-5735) are captured here so the very first\n // render reflects them; they're read live thereafter.\n const created = await apolloClient.mutate<{ createDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: CreateDashboardEmbedPolicyDocument,\n variables: {\n input: {\n dashboardId,\n allowedParentOrigins,\n ...(presentation\n ? {\n presentationTheme: THEME_TO_ENUM[presentation.theme],\n showDashboardTitle: presentation.showTitle,\n }\n : {}),\n },\n },\n });\n const createdSecret = created.data?.createDashboardEmbedPolicy;\n if (!createdSecret) return null;\n\n // Activate it for live embedding (owner attests content-safety).\n const published = await apolloClient.mutate<{ publishDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: PublishDashboardEmbedPolicyDocument,\n variables: { policyId: createdSecret.policy.id, contentSafetyAttested },\n });\n const activePolicy = published.data?.publishDashboardEmbedPolicy ?? createdSecret.policy;\n\n const result: EmbedPolicySecret = { ...createdSecret, policy: activePolicy };\n setSecret(result);\n setPolicy(activePolicy);\n return result;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to publish to web'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const applyPresentation = useCallback(\n async (policyId: string, presentation: EmbedPresentationChoice): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n // Presentation is read LIVE at view time by the embed renderer, so simply\n // persisting the choices on the policy is enough — no re-publish, and the\n // embed link/key are untouched. Live embeds pick up the change on their\n // next render (bounded by the service's short render-cache window).\n const result = await apolloClient.mutate<{ updateDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: UpdateDashboardEmbedPolicyDocument,\n variables: {\n input: {\n policyId,\n presentationTheme: THEME_TO_ENUM[presentation.theme],\n showDashboardTitle: presentation.showTitle,\n },\n },\n });\n const updated = result.data?.updateDashboardEmbedPolicy ?? null;\n if (updated) setPolicy(updated);\n return updated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to update presentation'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const setAllowedOrigins = useCallback(\n async (policyId: string, allowedParentOrigins: string[]): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ updateDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: UpdateDashboardEmbedPolicyDocument,\n variables: { input: { policyId, allowedParentOrigins } },\n });\n const updated = result.data?.updateDashboardEmbedPolicy ?? null;\n if (updated) setPolicy(updated);\n return updated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to update allowed domains'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const rotateKey = useCallback(\n async (policyId: string): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ rotateDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: RotateDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const rotated = result.data?.rotateDashboardEmbedPolicy ?? null;\n if (rotated) {\n setSecret(rotated);\n setPolicy(rotated.policy);\n }\n return rotated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to rotate embed link'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const unpublish = useCallback(\n async (policyId: string): Promise<boolean> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ revokeDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: RevokeDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const revoked = result.data?.revokeDashboardEmbedPolicy ?? null;\n if (revoked) {\n setPolicy(revoked);\n setSecret(null);\n return true;\n }\n return false;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to unpublish'));\n return false;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n return {\n policy,\n secret,\n loading,\n error,\n fetchPolicy,\n publishToWeb,\n applyPresentation,\n setAllowedOrigins,\n rotateKey,\n unpublish,\n };\n}\n\nexport default useDashboardEmbedPolicy;\n"],"mappings":"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"}
|