@burdenoff/microfe-bigconsole 2026.1001.1 → 2026.1001.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bigconsole/components/preview/AiPreviewContent.d.ts +3 -11
- package/dist/bigconsole/components/preview/AiPreviewContent.d.ts.map +1 -1
- package/dist/bigconsole/components/preview/AiPreviewContent.js +78 -83
- package/dist/bigconsole/components/preview/AiPreviewContent.js.map +1 -1
- package/dist/bigconsole/components/preview/parserPreview.d.ts +19 -0
- package/dist/bigconsole/components/preview/parserPreview.d.ts.map +1 -0
- package/dist/bigconsole/components/preview/parserPreview.js +31 -0
- package/dist/bigconsole/components/preview/parserPreview.js.map +1 -0
- package/dist/bigconsole/components/widgets/kanban/KanbanView.d.ts +4 -18
- package/dist/bigconsole/components/widgets/kanban/KanbanView.d.ts.map +1 -1
- package/dist/bigconsole/components/widgets/kanban/KanbanView.js +145 -161
- package/dist/bigconsole/components/widgets/kanban/KanbanView.js.map +1 -1
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.d.ts.map +1 -1
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +13 -10
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/kanban/model.d.ts +42 -0
- package/dist/bigconsole/components/widgets/kanban/model.d.ts.map +1 -0
- package/dist/bigconsole/components/widgets/kanban/model.js +145 -0
- package/dist/bigconsole/components/widgets/kanban/model.js.map +1 -0
- package/package.json +1 -1
|
@@ -7,17 +7,9 @@ export declare function DashboardContent({ dashboardId, live }: {
|
|
|
7
7
|
live?: boolean;
|
|
8
8
|
}): import("react").JSX.Element;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* A parser writes its transformed rows into a data sink named by its own
|
|
14
|
-
* `outputKey` (`ParserCoreFields`), and `getDataSinkData` accepts `{ key }`. So
|
|
15
|
-
* the output is fully readable without executing anything: read the parser,
|
|
16
|
-
* follow `outputKey`, read the sink.
|
|
17
|
-
*
|
|
18
|
-
* ★ Apollo is queried directly rather than via `useParserOperations.fetchParser`,
|
|
19
|
-
* because that helper calls `addParser()` and would publish into the global
|
|
20
|
-
* parser store — trading defect 2's leak for the same leak one store over.
|
|
10
|
+
* Read-only evaluation of the saved expression against up to 50 current source
|
|
11
|
+
* rows. outputKey is a widget binding, not a physical DataSink. The governed
|
|
12
|
+
* query applies masking and returns sampling metadata; it persists no output.
|
|
21
13
|
*/
|
|
22
14
|
export declare function ParserContent({ parserId, live }: {
|
|
23
15
|
parserId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiPreviewContent.d.ts","sourceRoot":"","sources":["../../../../src/bigconsole/components/preview/AiPreviewContent.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AiPreviewContent.d.ts","sourceRoot":"","sources":["../../../../src/bigconsole/components/preview/AiPreviewContent.tsx"],"names":[],"mappings":"AA6JA,wBAAgB,eAAe,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,+BAmB3E;AAED,wBAAgB,gBAAgB,CAAC,EAAE,WAAW,EAAE,IAAY,EAAE,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,+BAiBvG;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,+BAwBrF;AA6CD,wBAAgB,cAAc,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,+BA0B5F"}
|
|
@@ -1,44 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useAssistantRunStore as
|
|
3
|
-
import
|
|
4
|
-
import { normalizeWidget as
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
1
|
+
import { ListWidgetsByDashboardDocument as e } from "../../../generated/wspace-operations.js";
|
|
2
|
+
import { useAssistantRunStore as t } from "../../assistant/assistantRunStore.js";
|
|
3
|
+
import n from "../datasink/DataSinkTableViewer.js";
|
|
4
|
+
import { normalizeWidget as r } from "../../hooks/useWidgetOperations.js";
|
|
5
|
+
import i from "../../hooks/useDataSinkOperations.js";
|
|
6
|
+
import a from "../widgets/WidgetWrapper.js";
|
|
7
|
+
import o from "../DrilldownDashboardRenderer.js";
|
|
8
|
+
import { loadParserPreview as s, parserSampleCaption as c } from "./parserPreview.js";
|
|
9
|
+
import { useEffect as l, useRef as u, useState as d } from "react";
|
|
10
|
+
import { Spinner as f } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
import { useApolloClient as p } from "@apollo/client/react";
|
|
12
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
12
13
|
//#region src/bigconsole/components/preview/AiPreviewContent.tsx
|
|
13
|
-
var
|
|
14
|
-
function
|
|
15
|
-
return /* @__PURE__ */
|
|
16
|
-
className:
|
|
14
|
+
var g = 4e3, _ = "flex min-h-[6rem] min-w-0 items-center justify-center gap-2 rounded-card bg-bg-sunken px-3 py-6 text-center text-sm text-text-muted";
|
|
15
|
+
function v({ label: e, testId: t }) {
|
|
16
|
+
return /* @__PURE__ */ h("div", {
|
|
17
|
+
className: _,
|
|
17
18
|
"data-testid": t,
|
|
18
|
-
children: [/* @__PURE__ */
|
|
19
|
+
children: [/* @__PURE__ */ m(f, { className: "size-4 shrink-0" }), e]
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
|
-
function
|
|
22
|
-
return /* @__PURE__ */
|
|
23
|
-
className:
|
|
22
|
+
function y({ text: e, testId: t }) {
|
|
23
|
+
return /* @__PURE__ */ m("p", {
|
|
24
|
+
className: _,
|
|
24
25
|
"data-testid": t,
|
|
25
26
|
children: e
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
function
|
|
29
|
-
return /* @__PURE__ */
|
|
30
|
-
className: `${
|
|
29
|
+
function b({ text: e, testId: t }) {
|
|
30
|
+
return /* @__PURE__ */ m("p", {
|
|
31
|
+
className: `${_} text-status-error-text`,
|
|
31
32
|
"data-testid": t,
|
|
32
33
|
children: e
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
|
-
function
|
|
36
|
-
let i =
|
|
37
|
-
return _.current = e,
|
|
38
|
-
if (!
|
|
39
|
-
let e = !1,
|
|
40
|
-
if (!
|
|
41
|
-
|
|
36
|
+
function x(e, n, r) {
|
|
37
|
+
let i = t((e) => e.contentRevision), [a, o] = d(null), [s, c] = d(!!n), [f, p] = d(null), [m, h] = d(n), _ = u(e);
|
|
38
|
+
return _.current = e, n !== m && (h(n), o(null), p(null), c(!!n)), l(() => {
|
|
39
|
+
if (!n) return;
|
|
40
|
+
let e = !1, t = !1, i = async () => {
|
|
41
|
+
if (!t) {
|
|
42
|
+
t = !0;
|
|
42
43
|
try {
|
|
43
44
|
let t = await _.current();
|
|
44
45
|
if (e) return;
|
|
@@ -47,19 +48,19 @@ function b(e, t, r) {
|
|
|
47
48
|
if (e) return;
|
|
48
49
|
p(t instanceof Error ? t : /* @__PURE__ */ Error("Failed to load preview content"));
|
|
49
50
|
} finally {
|
|
50
|
-
|
|
51
|
+
t = !1, e || c(!1);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
55
|
i();
|
|
55
56
|
let a = window.setInterval(() => {
|
|
56
57
|
r && (typeof document < "u" && document.visibilityState !== "visible" || i());
|
|
57
|
-
},
|
|
58
|
+
}, g);
|
|
58
59
|
return () => {
|
|
59
60
|
e = !0, window.clearInterval(a);
|
|
60
61
|
};
|
|
61
62
|
}, [
|
|
62
|
-
|
|
63
|
+
n,
|
|
63
64
|
r,
|
|
64
65
|
i
|
|
65
66
|
]), {
|
|
@@ -68,121 +69,115 @@ function b(e, t, r) {
|
|
|
68
69
|
error: f
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
|
-
function
|
|
72
|
-
let { getDataSinkData:
|
|
73
|
-
let t = await
|
|
72
|
+
function S({ id: e, live: t }) {
|
|
73
|
+
let { getDataSinkData: r } = i({ skipFetch: !0 }), { data: a, loading: o, error: s } = x(async () => {
|
|
74
|
+
let t = await r({ id: e }, { limit: 50 });
|
|
74
75
|
return t ? t.raw : null;
|
|
75
76
|
}, e, t);
|
|
76
|
-
return e ? s ? /* @__PURE__ */
|
|
77
|
+
return e ? s ? /* @__PURE__ */ m(b, {
|
|
77
78
|
text: "Could not load the data sink's rows.",
|
|
78
79
|
testId: "ai-preview-datasink-error"
|
|
79
|
-
}) : o ? /* @__PURE__ */
|
|
80
|
+
}) : o ? /* @__PURE__ */ m(v, {
|
|
80
81
|
label: "Loading the data sink’s data…",
|
|
81
82
|
testId: "ai-preview-datasink-loading"
|
|
82
|
-
}) : /* @__PURE__ */
|
|
83
|
+
}) : /* @__PURE__ */ m("div", {
|
|
83
84
|
"data-testid": "ai-preview-content-datasink",
|
|
84
85
|
className: "min-w-0",
|
|
85
|
-
children: /* @__PURE__ */
|
|
86
|
-
data:
|
|
86
|
+
children: /* @__PURE__ */ m(n, {
|
|
87
|
+
data: a,
|
|
87
88
|
maxHeight: "300px"
|
|
88
89
|
})
|
|
89
|
-
}) : /* @__PURE__ */
|
|
90
|
+
}) : /* @__PURE__ */ m(y, {
|
|
90
91
|
text: "Waiting for the data sink to be created…",
|
|
91
92
|
testId: "ai-preview-datasink-waiting"
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
|
-
function
|
|
95
|
-
let r =
|
|
96
|
-
return e ? /* @__PURE__ */
|
|
95
|
+
function C({ dashboardId: e, live: n = !1 }) {
|
|
96
|
+
let r = t((e) => e.contentRevision);
|
|
97
|
+
return e ? /* @__PURE__ */ m("div", {
|
|
97
98
|
"data-testid": "ai-preview-content-dashboard",
|
|
98
99
|
className: "min-w-0",
|
|
99
|
-
children: /* @__PURE__ */
|
|
100
|
+
children: /* @__PURE__ */ m(o, {
|
|
100
101
|
dashboardId: e,
|
|
101
102
|
params: {},
|
|
102
|
-
live:
|
|
103
|
+
live: n,
|
|
103
104
|
refreshRevision: r
|
|
104
105
|
}, e)
|
|
105
|
-
}) : /* @__PURE__ */
|
|
106
|
+
}) : /* @__PURE__ */ m(y, {
|
|
106
107
|
text: "Waiting for the dashboard to be created…",
|
|
107
108
|
testId: "ai-preview-dashboard-waiting"
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
|
-
function
|
|
111
|
-
let
|
|
112
|
-
|
|
113
|
-
let n = (await i.query({
|
|
114
|
-
query: e,
|
|
115
|
-
variables: { id: t },
|
|
116
|
-
fetchPolicy: "network-only"
|
|
117
|
-
})).data?.parser?.outputKey;
|
|
118
|
-
if (!n) return null;
|
|
119
|
-
let r = await o({ key: n }, { limit: 50 });
|
|
120
|
-
return r ? r.raw : null;
|
|
121
|
-
}, t, n);
|
|
122
|
-
return t ? l ? /* @__PURE__ */ p(y, {
|
|
111
|
+
function w({ parserId: e, live: t }) {
|
|
112
|
+
let r = p(), { data: i, loading: a, error: o } = x(() => e ? s(r, e) : Promise.resolve(null), e, t), l = c(i);
|
|
113
|
+
return e ? o ? /* @__PURE__ */ m(b, {
|
|
123
114
|
text: "Could not read the parser's output.",
|
|
124
115
|
testId: "ai-preview-parser-error"
|
|
125
|
-
}) :
|
|
116
|
+
}) : a ? /* @__PURE__ */ m(v, {
|
|
126
117
|
label: "Loading the parser’s output…",
|
|
127
118
|
testId: "ai-preview-parser-loading"
|
|
128
|
-
}) : /* @__PURE__ */
|
|
119
|
+
}) : /* @__PURE__ */ h("div", {
|
|
129
120
|
"data-testid": "ai-preview-content-parser",
|
|
130
121
|
className: "min-w-0",
|
|
131
|
-
children: /* @__PURE__ */ p
|
|
132
|
-
data:
|
|
122
|
+
children: [l && /* @__PURE__ */ m("p", {
|
|
123
|
+
"data-testid": "ai-preview-parser-sample",
|
|
124
|
+
className: "mb-2 text-xs text-text-muted",
|
|
125
|
+
children: l
|
|
126
|
+
}), /* @__PURE__ */ m(n, {
|
|
127
|
+
data: i?.output ?? null,
|
|
133
128
|
maxHeight: "280px"
|
|
134
|
-
})
|
|
135
|
-
}) : /* @__PURE__ */
|
|
129
|
+
})]
|
|
130
|
+
}) : /* @__PURE__ */ m(y, {
|
|
136
131
|
text: "Waiting for the parser to be created…",
|
|
137
132
|
testId: "ai-preview-parser-waiting"
|
|
138
133
|
});
|
|
139
134
|
}
|
|
140
|
-
function
|
|
135
|
+
function T(e) {
|
|
141
136
|
let t = e.toUpperCase();
|
|
142
137
|
return t.includes("METRIC") || t.includes("KPI") || t.includes("CARD") || t.includes("STAT") ? "min(160px, 40vh)" : t.includes("TABLE") || t.includes("LIST") ? "min(280px, 50vh)" : "min(320px, 50vh)";
|
|
143
138
|
}
|
|
144
|
-
function
|
|
145
|
-
let
|
|
146
|
-
query:
|
|
147
|
-
variables: { dashboardId:
|
|
139
|
+
function E(t, n) {
|
|
140
|
+
let i = p(), { data: a, loading: o, error: s } = x(async () => t ? ((await i.query({
|
|
141
|
+
query: e,
|
|
142
|
+
variables: { dashboardId: t },
|
|
148
143
|
fetchPolicy: "network-only"
|
|
149
|
-
})).data?.listWidgetsByDashboard || []).map(
|
|
144
|
+
})).data?.listWidgetsByDashboard || []).map(r) : null, t, n);
|
|
150
145
|
return {
|
|
151
146
|
widgets: a ?? [],
|
|
152
147
|
loading: o,
|
|
153
148
|
error: s
|
|
154
149
|
};
|
|
155
150
|
}
|
|
156
|
-
function
|
|
157
|
-
let { widgets: n, loading: r, error: i } =
|
|
158
|
-
return e ? i ? /* @__PURE__ */
|
|
151
|
+
function D({ dashboardId: e, live: t }) {
|
|
152
|
+
let { widgets: n, loading: r, error: i } = E(e, t);
|
|
153
|
+
return e ? i ? /* @__PURE__ */ m(b, {
|
|
159
154
|
text: "Could not load this dashboard's widgets.",
|
|
160
155
|
testId: "ai-preview-widget-error"
|
|
161
|
-
}) : r && n.length === 0 ? /* @__PURE__ */
|
|
156
|
+
}) : r && n.length === 0 ? /* @__PURE__ */ m(v, {
|
|
162
157
|
label: "Loading widgets…",
|
|
163
158
|
testId: "ai-preview-widget-loading"
|
|
164
|
-
}) : n.length === 0 ? /* @__PURE__ */
|
|
159
|
+
}) : n.length === 0 ? /* @__PURE__ */ m(y, {
|
|
165
160
|
text: "No widgets on this dashboard yet.",
|
|
166
161
|
testId: "ai-preview-widget-empty"
|
|
167
|
-
}) : /* @__PURE__ */
|
|
162
|
+
}) : /* @__PURE__ */ m("div", {
|
|
168
163
|
"data-testid": "ai-preview-content-widget",
|
|
169
164
|
className: "flex min-w-0 flex-col gap-list-gap",
|
|
170
|
-
children: n.map((e) => /* @__PURE__ */
|
|
165
|
+
children: n.map((e) => /* @__PURE__ */ m("div", {
|
|
171
166
|
className: "min-w-0 overflow-hidden rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas",
|
|
172
|
-
style: { height:
|
|
173
|
-
children: /* @__PURE__ */
|
|
167
|
+
style: { height: T(e.type) },
|
|
168
|
+
children: /* @__PURE__ */ m(a, {
|
|
174
169
|
widget: e,
|
|
175
170
|
isLoading: !1,
|
|
176
171
|
error: null,
|
|
177
172
|
filterValues: {}
|
|
178
173
|
})
|
|
179
174
|
}, e.id))
|
|
180
|
-
}) : /* @__PURE__ */
|
|
175
|
+
}) : /* @__PURE__ */ m(y, {
|
|
181
176
|
text: "Waiting for the dashboard — widgets live on it.",
|
|
182
177
|
testId: "ai-preview-widget-waiting"
|
|
183
178
|
});
|
|
184
179
|
}
|
|
185
180
|
//#endregion
|
|
186
|
-
export {
|
|
181
|
+
export { C as DashboardContent, S as DataSinkContent, w as ParserContent, D as WidgetsContent };
|
|
187
182
|
|
|
188
183
|
//# sourceMappingURL=AiPreviewContent.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiPreviewContent.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewContent.tsx"],"sourcesContent":["/**\n * Live content renderers for the AI preview tabs.\n *\n * Each tab shows the REAL content of the entity the assistant just built —\n * the data sink's rows, the live dashboard, the parser's transformed output,\n * the rendered widget charts — not a list with an Open button. They reuse the\n * same components the real pages use (DataSinkTableViewer, DrilldownDashboard-\n * Renderer, WidgetWrapper) so what you see in the preview is what you get.\n *\n * These require Apollo + the BigConsole providers, which is why they live only\n * in the full AiPreviewPanel (mounted inside BigConsoleRoot), never in the\n * store-only app-shell mini panel.\n *\n * ── BC0 (BOFF-7333; BOFF-7270, BOFF-7271) ────────────────────────────────────\n * A PREVIEW MUST OBSERVE, NEVER MUTATE, AND NEVER PUBLISH.\n *\n * Three defects made this panel change the system it was previewing:\n *\n * 1. The Parser tab polled `executeParser` — a MUTATION — every 4s for 60s,\n * re-running the transform server-side up to ~15 times per turn. It now\n * reads the parser (query), follows its `outputKey`, and reads that sink\n * read-only. Zero mutations.\n * 2. The Widgets tab went through `useWidgetOperations`, which publishes into\n * the GLOBAL widget store — so opening the preview overwrote the widgets an\n * already-open dashboard was rendering. It now holds widgets in local state.\n * 3. `useLiveContent` never reset on key change, so the PREVIOUS entity's rows\n * stayed on screen as though current, and an un-caught rejection left the\n * tab spinning forever with no error surface.\n *\n * ★ Deliberately NOT changed: the other `executeParser` callers\n * (`ParserViewPage`, `CustomWidgetBuilderSheet`, `PropertiesPanel`). Those are\n * user-initiated \"run this parser\" actions where executing IS the intent. Only\n * the passive preview had no business mutating.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { DataSinkTableViewer } from '../datasink/DataSinkTableViewer';\nimport { DrilldownDashboardRenderer } from '../DrilldownDashboardRenderer';\nimport { WidgetWrapper } from '../widgets/WidgetWrapper';\nimport { useDataSinkOperations } from '../../hooks/useDataSinkOperations';\nimport { normalizeWidget } from '../../hooks/useWidgetOperations';\nimport { GetParserDocument, ListWidgetsByDashboardDocument } from '../../../generated/wspace-operations';\nimport type { Parser, Widget } from '../../types';\nimport { useAssistantRunStore } from '../../assistant/assistantRunStore';\n\n/** While the build is live, re-pull content on this cadence so it fills in. */\nconst CONTENT_POLL_MS = 4000;\n\n/**\n * Loading and waiting states share one recessed, height-reserved block so the\n * panel does not jump every time a tab fills in — previously each tab had its\n * own bare paragraph and its own height.\n */\nconst PLACEHOLDER_BOX =\n 'flex min-h-[6rem] min-w-0 items-center justify-center gap-2 rounded-card bg-bg-sunken px-3 py-6 text-center text-sm text-text-muted';\n\nfunction CenteredSpinner({ label, testId }: { label: string; testId?: string }) {\n return (\n <div className={PLACEHOLDER_BOX} data-testid={testId}>\n <Spinner className=\"size-4 shrink-0\" />\n {label}\n </div>\n );\n}\n\nfunction Waiting({ text, testId }: { text: string; testId?: string }) {\n return (\n <p className={PLACEHOLDER_BOX} data-testid={testId}>\n {text}\n </p>\n );\n}\n\n/**\n * ★ BC0: the tab had no error surface at all. An un-caught rejection inside the\n * poller left `loading` true forever, so a failed read was indistinguishable\n * from a slow one — and it surfaced as an unhandled promise rejection.\n */\nfunction ErrorBox({ text, testId }: { text: string; testId?: string }) {\n return (\n <p className={`${PLACEHOLDER_BOX} text-status-error-text`} data-testid={testId}>\n {text}\n </p>\n );\n}\n\n/**\n * Fetch-on-mount and confirmed-result revisions, then re-fetch every CONTENT_POLL_MS while `live`. Returns { data, loading, error }. `fetcher` must be\n * stable-ish; we hold it in a ref so the effect keys only on `key`/`live`.\n *\n * ★★ BC0: RESETS ON KEY CHANGE. Previously `data` survived a key change, so\n * switching to a different entity rendered the PREVIOUS entity's rows as though\n * they were the new one's — with `loading` already false, nothing said otherwise.\n */\nfunction useLiveContent<T>(fetcher: () => Promise<T | null>, key: string | undefined, live: boolean) {\n const revision = useAssistantRunStore((state) => state.contentRevision);\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(Boolean(key));\n const [error, setError] = useState<Error | null>(null);\n const [renderedKey, setRenderedKey] = useState(key);\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n // ★★ The reset happens DURING RENDER, not in an effect. An effect-based reset\n // commits one frame carrying the PREVIOUS entity's rows before clearing them —\n // which is the very defect being fixed here. Adjusting state while rendering is\n // React's supported pattern for \"a prop changed, discard the derived state\": it\n // re-runs this component immediately, before anything reaches the DOM, so the\n // stale rows are never painted. A key going undefined clears the render too.\n if (key !== renderedKey) {\n setRenderedKey(key);\n setData(null);\n setError(null);\n setLoading(Boolean(key));\n }\n\n useEffect(() => {\n if (!key) return;\n\n let cancelled = false;\n let fetching = false;\n const pull = async () => {\n if (fetching) return;\n fetching = true;\n try {\n const result = await fetcherRef.current();\n if (cancelled) return;\n setData(result ?? null);\n setError(null);\n } catch (err) {\n if (cancelled) return;\n // A later successful poll clears this, so a transient blip does not\n // strand the tab on an error.\n setError(err instanceof Error ? err : new Error('Failed to load preview content'));\n } finally {\n fetching = false;\n if (!cancelled) setLoading(false);\n }\n };\n void pull();\n const intervalId = window.setInterval(() => {\n if (!live) return;\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n void pull();\n }, CONTENT_POLL_MS);\n return () => {\n cancelled = true;\n window.clearInterval(intervalId);\n };\n }, [key, live, revision]);\n\n return { data, loading, error };\n}\n\nexport function DataSinkContent({ id, live }: { id?: string; live: boolean }) {\n const { getDataSinkData } = useDataSinkOperations({ skipFetch: true });\n const { data, loading, error } = useLiveContent(\n async () => {\n const res = await getDataSinkData({ id }, { limit: 50 });\n return res ? res.raw : null;\n },\n id,\n live\n );\n\n if (!id) return <Waiting text=\"Waiting for the data sink to be created…\" testId=\"ai-preview-datasink-waiting\" />;\n if (error) return <ErrorBox text=\"Could not load the data sink's rows.\" testId=\"ai-preview-datasink-error\" />;\n if (loading) return <CenteredSpinner label=\"Loading the data sink’s data…\" testId=\"ai-preview-datasink-loading\" />;\n return (\n <div data-testid=\"ai-preview-content-datasink\" className=\"min-w-0\">\n <DataSinkTableViewer data={data} maxHeight=\"300px\" />\n </div>\n );\n}\n\nexport function DashboardContent({ dashboardId, live = false }: { dashboardId?: string; live?: boolean }) {\n const revision = useAssistantRunStore((state) => state.contentRevision);\n if (!dashboardId)\n return <Waiting text=\"Waiting for the dashboard to be created…\" testId=\"ai-preview-dashboard-waiting\" />;\n // No inner `max-h + overflow-y-auto`: the panel already owns a vertical\n // scroll, and nesting a second one put two scrollbars in ~440px of space.\n return (\n <div data-testid=\"ai-preview-content-dashboard\" className=\"min-w-0\">\n <DrilldownDashboardRenderer\n key={dashboardId}\n dashboardId={dashboardId}\n params={{}}\n live={live}\n refreshRevision={revision}\n />\n </div>\n );\n}\n\n/**\n * ★★★ BC0 — READ-ONLY. This tab used to call `executeParser`, a mutation, on\n * every poll: ~15 server-side re-executions per turn purely to look at output.\n *\n * A parser writes its transformed rows into a data sink named by its own\n * `outputKey` (`ParserCoreFields`), and `getDataSinkData` accepts `{ key }`. So\n * the output is fully readable without executing anything: read the parser,\n * follow `outputKey`, read the sink.\n *\n * ★ Apollo is queried directly rather than via `useParserOperations.fetchParser`,\n * because that helper calls `addParser()` and would publish into the global\n * parser store — trading defect 2's leak for the same leak one store over.\n */\nexport function ParserContent({ parserId, live }: { parserId?: string; live: boolean }) {\n const client = useApolloClient();\n const { getDataSinkData } = useDataSinkOperations({ skipFetch: true });\n const { data, loading, error } = useLiveContent(\n async () => {\n if (!parserId) return null;\n const res = await client.query<{ parser?: Parser | null }>({\n query: GetParserDocument,\n variables: { id: parserId },\n fetchPolicy: 'network-only',\n });\n const outputKey = res.data?.parser?.outputKey;\n if (!outputKey) return null;\n const sink = await getDataSinkData({ key: outputKey }, { limit: 50 });\n return sink ? sink.raw : null;\n },\n parserId,\n live\n );\n\n if (!parserId) return <Waiting text=\"Waiting for the parser to be created…\" testId=\"ai-preview-parser-waiting\" />;\n if (error) return <ErrorBox text=\"Could not read the parser's output.\" testId=\"ai-preview-parser-error\" />;\n // \"Running the parser…\" would now be a lie — nothing is executed.\n if (loading) return <CenteredSpinner label=\"Loading the parser’s output…\" testId=\"ai-preview-parser-loading\" />;\n // The \"Parser output (transformed rows)\" caption used to sit here; it now\n // lives on the section header, so the tab does not label itself twice.\n return (\n <div data-testid=\"ai-preview-content-parser\" className=\"min-w-0\">\n <DataSinkTableViewer data={data} maxHeight=\"280px\" />\n </div>\n );\n}\n\n/**\n * Recharts needs a definite height or it renders 0px tall, but a hard 320px box\n * eats a short viewport whole. `min(px, vh)` keeps the definite height Recharts\n * needs while letting the box shrink on small screens.\n */\nfunction widgetBoxHeight(type: string): string {\n const t = type.toUpperCase();\n if (t.includes('METRIC') || t.includes('KPI') || t.includes('CARD') || t.includes('STAT')) return 'min(160px, 40vh)';\n if (t.includes('TABLE') || t.includes('LIST')) return 'min(280px, 50vh)';\n return 'min(320px, 50vh)';\n}\n\n/**\n * ★★★ BC0 — COMPONENT-LOCAL widgets.\n *\n * `useWidgetOperations` publishes every fetch into the GLOBAL widget store, so\n * merely opening this preview replaced the widget set an already-open dashboard\n * was rendering. The query and the normalisation are reused — `normalizeWidget`\n * is exported from that hook precisely so the two paths cannot drift — but the\n * result stays here, in this component's state.\n *\n * It also inherits `useLiveContent`'s result refresh and visibility check,\n * replacing the bespoke interval this tab used to run.\n */\nfunction usePreviewWidgets(dashboardId: string | undefined, live: boolean) {\n const client = useApolloClient();\n const { data, loading, error } = useLiveContent<Widget[]>(\n async () => {\n if (!dashboardId) return null;\n const result = await client.query<Record<string, unknown>>({\n query: ListWidgetsByDashboardDocument,\n variables: { dashboardId },\n fetchPolicy: 'network-only',\n });\n const list = (result.data?.listWidgetsByDashboard as Record<string, unknown>[]) || [];\n return list.map(normalizeWidget);\n },\n dashboardId,\n live\n );\n return { widgets: data ?? [], loading, error };\n}\n\nexport function WidgetsContent({ dashboardId, live }: { dashboardId?: string; live: boolean }) {\n const { widgets, loading, error } = usePreviewWidgets(dashboardId, live);\n\n if (!dashboardId)\n return <Waiting text=\"Waiting for the dashboard — widgets live on it.\" testId=\"ai-preview-widget-waiting\" />;\n if (error) return <ErrorBox text=\"Could not load this dashboard's widgets.\" testId=\"ai-preview-widget-error\" />;\n if (loading && widgets.length === 0)\n return <CenteredSpinner label=\"Loading widgets…\" testId=\"ai-preview-widget-loading\" />;\n if (widgets.length === 0)\n return <Waiting text=\"No widgets on this dashboard yet.\" testId=\"ai-preview-widget-empty\" />;\n\n return (\n <div data-testid=\"ai-preview-content-widget\" className=\"flex min-w-0 flex-col gap-list-gap\">\n {widgets.map((widget) => (\n <div\n key={widget.id}\n // `border-border-subtle` is registered as a frozen light hex in this\n // shell, so the box kept a light border in dark mode.\n className=\"min-w-0 overflow-hidden rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas\"\n style={{ height: widgetBoxHeight(widget.type) }}\n >\n <WidgetWrapper widget={widget} isLoading={false} error={null} filterValues={{}} />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiPreviewContent.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewContent.tsx"],"sourcesContent":["/**\n * Live content renderers for the AI preview tabs.\n *\n * Each tab shows the REAL content of the entity the assistant just built —\n * the data sink's rows, the live dashboard, the parser's transformed output,\n * the rendered widget charts — not a list with an Open button. They reuse the\n * same components the real pages use (DataSinkTableViewer, DrilldownDashboard-\n * Renderer, WidgetWrapper) so what you see in the preview is what you get.\n *\n * These require Apollo + the BigConsole providers, which is why they live only\n * in the full AiPreviewPanel (mounted inside BigConsoleRoot), never in the\n * store-only app-shell mini panel.\n *\n * ── BC0 (BOFF-7333; BOFF-7270, BOFF-7271) ────────────────────────────────────\n * A PREVIEW MUST OBSERVE, NEVER MUTATE, AND NEVER PUBLISH.\n *\n * Three defects made this panel change the system it was previewing:\n *\n * 1. The Parser tab polled `executeParser` — a MUTATION — every 4s for 60s,\n * re-running the transform server-side up to ~15 times per turn. It now\n * reads the parser and evaluates a governed source sample through queries.\n * It does not persist output or call mutations.\n * 2. The Widgets tab went through `useWidgetOperations`, which publishes into\n * the GLOBAL widget store — so opening the preview overwrote the widgets an\n * already-open dashboard was rendering. It now holds widgets in local state.\n * 3. `useLiveContent` never reset on key change, so the PREVIOUS entity's rows\n * stayed on screen as though current, and an un-caught rejection left the\n * tab spinning forever with no error surface.\n *\n * ★ Deliberately NOT changed: the other `executeParser` callers\n * (`ParserViewPage`, `CustomWidgetBuilderSheet`, `PropertiesPanel`). Those are\n * user-initiated \"run this parser\" actions where executing IS the intent. Only\n * the passive preview had no business mutating.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { DataSinkTableViewer } from '../datasink/DataSinkTableViewer';\nimport { DrilldownDashboardRenderer } from '../DrilldownDashboardRenderer';\nimport { WidgetWrapper } from '../widgets/WidgetWrapper';\nimport { useDataSinkOperations } from '../../hooks/useDataSinkOperations';\nimport { normalizeWidget } from '../../hooks/useWidgetOperations';\nimport { ListWidgetsByDashboardDocument } from '../../../generated/wspace-operations';\nimport type { Widget } from '../../types';\nimport { useAssistantRunStore } from '../../assistant/assistantRunStore';\nimport { loadParserPreview, parserSampleCaption } from './parserPreview';\n\n/** While the build is live, re-pull content on this cadence so it fills in. */\nconst CONTENT_POLL_MS = 4000;\n\n/**\n * Loading and waiting states share one recessed, height-reserved block so the\n * panel does not jump every time a tab fills in — previously each tab had its\n * own bare paragraph and its own height.\n */\nconst PLACEHOLDER_BOX =\n 'flex min-h-[6rem] min-w-0 items-center justify-center gap-2 rounded-card bg-bg-sunken px-3 py-6 text-center text-sm text-text-muted';\n\nfunction CenteredSpinner({ label, testId }: { label: string; testId?: string }) {\n return (\n <div className={PLACEHOLDER_BOX} data-testid={testId}>\n <Spinner className=\"size-4 shrink-0\" />\n {label}\n </div>\n );\n}\n\nfunction Waiting({ text, testId }: { text: string; testId?: string }) {\n return (\n <p className={PLACEHOLDER_BOX} data-testid={testId}>\n {text}\n </p>\n );\n}\n\n/**\n * ★ BC0: the tab had no error surface at all. An un-caught rejection inside the\n * poller left `loading` true forever, so a failed read was indistinguishable\n * from a slow one — and it surfaced as an unhandled promise rejection.\n */\nfunction ErrorBox({ text, testId }: { text: string; testId?: string }) {\n return (\n <p className={`${PLACEHOLDER_BOX} text-status-error-text`} data-testid={testId}>\n {text}\n </p>\n );\n}\n\n/**\n * Fetch-on-mount and confirmed-result revisions, then re-fetch every CONTENT_POLL_MS while `live`. Returns { data, loading, error }. `fetcher` must be\n * stable-ish; we hold it in a ref so the effect keys only on `key`/`live`.\n *\n * ★★ BC0: RESETS ON KEY CHANGE. Previously `data` survived a key change, so\n * switching to a different entity rendered the PREVIOUS entity's rows as though\n * they were the new one's — with `loading` already false, nothing said otherwise.\n */\nfunction useLiveContent<T>(fetcher: () => Promise<T | null>, key: string | undefined, live: boolean) {\n const revision = useAssistantRunStore((state) => state.contentRevision);\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(Boolean(key));\n const [error, setError] = useState<Error | null>(null);\n const [renderedKey, setRenderedKey] = useState(key);\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n // ★★ The reset happens DURING RENDER, not in an effect. An effect-based reset\n // commits one frame carrying the PREVIOUS entity's rows before clearing them —\n // which is the very defect being fixed here. Adjusting state while rendering is\n // React's supported pattern for \"a prop changed, discard the derived state\": it\n // re-runs this component immediately, before anything reaches the DOM, so the\n // stale rows are never painted. A key going undefined clears the render too.\n if (key !== renderedKey) {\n setRenderedKey(key);\n setData(null);\n setError(null);\n setLoading(Boolean(key));\n }\n\n useEffect(() => {\n if (!key) return;\n\n let cancelled = false;\n let fetching = false;\n const pull = async () => {\n if (fetching) return;\n fetching = true;\n try {\n const result = await fetcherRef.current();\n if (cancelled) return;\n setData(result ?? null);\n setError(null);\n } catch (err) {\n if (cancelled) return;\n // A later successful poll clears this, so a transient blip does not\n // strand the tab on an error.\n setError(err instanceof Error ? err : new Error('Failed to load preview content'));\n } finally {\n fetching = false;\n if (!cancelled) setLoading(false);\n }\n };\n void pull();\n const intervalId = window.setInterval(() => {\n if (!live) return;\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n void pull();\n }, CONTENT_POLL_MS);\n return () => {\n cancelled = true;\n window.clearInterval(intervalId);\n };\n }, [key, live, revision]);\n\n return { data, loading, error };\n}\n\nexport function DataSinkContent({ id, live }: { id?: string; live: boolean }) {\n const { getDataSinkData } = useDataSinkOperations({ skipFetch: true });\n const { data, loading, error } = useLiveContent(\n async () => {\n const res = await getDataSinkData({ id }, { limit: 50 });\n return res ? res.raw : null;\n },\n id,\n live\n );\n\n if (!id) return <Waiting text=\"Waiting for the data sink to be created…\" testId=\"ai-preview-datasink-waiting\" />;\n if (error) return <ErrorBox text=\"Could not load the data sink's rows.\" testId=\"ai-preview-datasink-error\" />;\n if (loading) return <CenteredSpinner label=\"Loading the data sink’s data…\" testId=\"ai-preview-datasink-loading\" />;\n return (\n <div data-testid=\"ai-preview-content-datasink\" className=\"min-w-0\">\n <DataSinkTableViewer data={data} maxHeight=\"300px\" />\n </div>\n );\n}\n\nexport function DashboardContent({ dashboardId, live = false }: { dashboardId?: string; live?: boolean }) {\n const revision = useAssistantRunStore((state) => state.contentRevision);\n if (!dashboardId)\n return <Waiting text=\"Waiting for the dashboard to be created…\" testId=\"ai-preview-dashboard-waiting\" />;\n // No inner `max-h + overflow-y-auto`: the panel already owns a vertical\n // scroll, and nesting a second one put two scrollbars in ~440px of space.\n return (\n <div data-testid=\"ai-preview-content-dashboard\" className=\"min-w-0\">\n <DrilldownDashboardRenderer\n key={dashboardId}\n dashboardId={dashboardId}\n params={{}}\n live={live}\n refreshRevision={revision}\n />\n </div>\n );\n}\n\n/**\n * Read-only evaluation of the saved expression against up to 50 current source\n * rows. outputKey is a widget binding, not a physical DataSink. The governed\n * query applies masking and returns sampling metadata; it persists no output.\n */\nexport function ParserContent({ parserId, live }: { parserId?: string; live: boolean }) {\n const client = useApolloClient();\n const { data, loading, error } = useLiveContent(\n () => (parserId ? loadParserPreview(client, parserId) : Promise.resolve(null)),\n parserId,\n live\n );\n const sampleCaption = parserSampleCaption(data);\n\n if (!parserId) return <Waiting text=\"Waiting for the parser to be created…\" testId=\"ai-preview-parser-waiting\" />;\n if (error) return <ErrorBox text=\"Could not read the parser's output.\" testId=\"ai-preview-parser-error\" />;\n if (loading) return <CenteredSpinner label=\"Loading the parser’s output…\" testId=\"ai-preview-parser-loading\" />;\n // The \"Parser output (transformed rows)\" caption used to sit here; it now\n // lives on the section header, so the tab does not label itself twice.\n return (\n <div data-testid=\"ai-preview-content-parser\" className=\"min-w-0\">\n {sampleCaption && (\n <p data-testid=\"ai-preview-parser-sample\" className=\"mb-2 text-xs text-text-muted\">\n {sampleCaption}\n </p>\n )}\n <DataSinkTableViewer data={data?.output ?? null} maxHeight=\"280px\" />\n </div>\n );\n}\n\n/**\n * Recharts needs a definite height or it renders 0px tall, but a hard 320px box\n * eats a short viewport whole. `min(px, vh)` keeps the definite height Recharts\n * needs while letting the box shrink on small screens.\n */\nfunction widgetBoxHeight(type: string): string {\n const t = type.toUpperCase();\n if (t.includes('METRIC') || t.includes('KPI') || t.includes('CARD') || t.includes('STAT')) return 'min(160px, 40vh)';\n if (t.includes('TABLE') || t.includes('LIST')) return 'min(280px, 50vh)';\n return 'min(320px, 50vh)';\n}\n\n/**\n * ★★★ BC0 — COMPONENT-LOCAL widgets.\n *\n * `useWidgetOperations` publishes every fetch into the GLOBAL widget store, so\n * merely opening this preview replaced the widget set an already-open dashboard\n * was rendering. The query and the normalisation are reused — `normalizeWidget`\n * is exported from that hook precisely so the two paths cannot drift — but the\n * result stays here, in this component's state.\n *\n * It also inherits `useLiveContent`'s result refresh and visibility check,\n * replacing the bespoke interval this tab used to run.\n */\nfunction usePreviewWidgets(dashboardId: string | undefined, live: boolean) {\n const client = useApolloClient();\n const { data, loading, error } = useLiveContent<Widget[]>(\n async () => {\n if (!dashboardId) return null;\n const result = await client.query<Record<string, unknown>>({\n query: ListWidgetsByDashboardDocument,\n variables: { dashboardId },\n fetchPolicy: 'network-only',\n });\n const list = (result.data?.listWidgetsByDashboard as Record<string, unknown>[]) || [];\n return list.map(normalizeWidget);\n },\n dashboardId,\n live\n );\n return { widgets: data ?? [], loading, error };\n}\n\nexport function WidgetsContent({ dashboardId, live }: { dashboardId?: string; live: boolean }) {\n const { widgets, loading, error } = usePreviewWidgets(dashboardId, live);\n\n if (!dashboardId)\n return <Waiting text=\"Waiting for the dashboard — widgets live on it.\" testId=\"ai-preview-widget-waiting\" />;\n if (error) return <ErrorBox text=\"Could not load this dashboard's widgets.\" testId=\"ai-preview-widget-error\" />;\n if (loading && widgets.length === 0)\n return <CenteredSpinner label=\"Loading widgets…\" testId=\"ai-preview-widget-loading\" />;\n if (widgets.length === 0)\n return <Waiting text=\"No widgets on this dashboard yet.\" testId=\"ai-preview-widget-empty\" />;\n\n return (\n <div data-testid=\"ai-preview-content-widget\" className=\"flex min-w-0 flex-col gap-list-gap\">\n {widgets.map((widget) => (\n <div\n key={widget.id}\n // `border-border-subtle` is registered as a frozen light hex in this\n // shell, so the box kept a light border in dark mode.\n className=\"min-w-0 overflow-hidden rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas\"\n style={{ height: widgetBoxHeight(widget.type) }}\n >\n <WidgetWrapper widget={widget} isLoading={false} error={null} filterValues={{}} />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ApolloClient } from '@apollo/client';
|
|
2
|
+
|
|
3
|
+
/** Evaluate a bounded, governed source sample without persisting parser output. */
|
|
4
|
+
export declare function loadParserPreview(client: Pick<ApolloClient, 'query'>, parserId: string): Promise<{
|
|
5
|
+
__typename?: "ParserTestResult";
|
|
6
|
+
success: boolean;
|
|
7
|
+
output?: Record<string, unknown> | null;
|
|
8
|
+
error?: string | null;
|
|
9
|
+
executionTimeMs?: number | null;
|
|
10
|
+
sampled?: boolean | null;
|
|
11
|
+
sampledRows?: number | null;
|
|
12
|
+
totalRows?: number | null;
|
|
13
|
+
} | null>;
|
|
14
|
+
export declare function parserSampleCaption(preview: {
|
|
15
|
+
sampled?: boolean | null;
|
|
16
|
+
sampledRows?: number | null;
|
|
17
|
+
totalRows?: number | null;
|
|
18
|
+
} | null): string | null;
|
|
19
|
+
//# sourceMappingURL=parserPreview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parserPreview.d.ts","sourceRoot":"","sources":["../../../../src/bigconsole/components/preview/parserPreview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQnD,mFAAmF;AACnF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;UAuB5F;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GACnG,MAAM,GAAG,IAAI,CAMf"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { GetParserDocument as e, TestParserExpressionDocument as t } from "../../../generated/wspace-operations.js";
|
|
2
|
+
//#region src/bigconsole/components/preview/parserPreview.ts
|
|
3
|
+
async function n(n, r) {
|
|
4
|
+
let i = (await n.query({
|
|
5
|
+
query: e,
|
|
6
|
+
variables: { id: r },
|
|
7
|
+
fetchPolicy: "network-only"
|
|
8
|
+
})).data?.parser;
|
|
9
|
+
if (!i) return null;
|
|
10
|
+
let a = (await n.query({
|
|
11
|
+
query: t,
|
|
12
|
+
variables: { input: {
|
|
13
|
+
type: i.type,
|
|
14
|
+
expression: i.expression,
|
|
15
|
+
dataSinkKey: i.inputSinkKey,
|
|
16
|
+
sampleLimit: 50
|
|
17
|
+
} },
|
|
18
|
+
fetchPolicy: "network-only"
|
|
19
|
+
})).data?.testParserExpression;
|
|
20
|
+
if (!a?.success) throw Error(a?.error || "Could not evaluate the parser preview");
|
|
21
|
+
return a;
|
|
22
|
+
}
|
|
23
|
+
function r(e) {
|
|
24
|
+
if (!e?.sampled) return null;
|
|
25
|
+
let t = e.sampledRows ?? 50;
|
|
26
|
+
return e.totalRows !== null && e.totalRows !== void 0 ? `Preview evaluated ${t} of ${e.totalRows} source rows. Full execution may differ.` : `Preview evaluated ${t} source rows. Full execution may differ.`;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { n as loadParserPreview, r as parserSampleCaption };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=parserPreview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parserPreview.js","names":[],"sources":["../../../../src/bigconsole/components/preview/parserPreview.ts"],"sourcesContent":["import type { ApolloClient } from '@apollo/client';\nimport {\n GetParserDocument,\n TestParserExpressionDocument,\n type GetParserQuery,\n type TestParserExpressionQuery,\n} from '../../../generated/wspace-operations';\n\n/** Evaluate a bounded, governed source sample without persisting parser output. */\nexport async function loadParserPreview(client: Pick<ApolloClient, 'query'>, parserId: string) {\n const parserResult = await client.query<GetParserQuery>({\n query: GetParserDocument,\n variables: { id: parserId },\n fetchPolicy: 'network-only',\n });\n const parser = parserResult.data?.parser;\n if (!parser) return null;\n const result = await client.query<TestParserExpressionQuery>({\n query: TestParserExpressionDocument,\n variables: {\n input: {\n type: parser.type,\n expression: parser.expression,\n dataSinkKey: parser.inputSinkKey,\n sampleLimit: 50,\n },\n },\n fetchPolicy: 'network-only',\n });\n const preview = result.data?.testParserExpression;\n if (!preview?.success) throw new Error(preview?.error || 'Could not evaluate the parser preview');\n return preview;\n}\n\nexport function parserSampleCaption(\n preview: { sampled?: boolean | null; sampledRows?: number | null; totalRows?: number | null } | null\n): string | null {\n if (!preview?.sampled) return null;\n const rows = preview.sampledRows ?? 50;\n return preview.totalRows !== null && preview.totalRows !== undefined\n ? `Preview evaluated ${rows} of ${preview.totalRows} source rows. Full execution may differ.`\n : `Preview evaluated ${rows} source rows. Full execution may differ.`;\n}\n"],"mappings":";;AASA,eAAsB,EAAkB,GAAqC,GAAkB;CAM7F,IAAM,KALe,MAAM,EAAO,MAAsB;EACtD,OAAO;EACP,WAAW,EAAE,IAAI,GAAU;EAC3B,aAAa;EACd,CAAC,EAC0B,MAAM;AAClC,KAAI,CAAC,EAAQ,QAAO;CAapB,IAAM,KAZS,MAAM,EAAO,MAAiC;EAC3D,OAAO;EACP,WAAW,EACT,OAAO;GACL,MAAM,EAAO;GACb,YAAY,EAAO;GACnB,aAAa,EAAO;GACpB,aAAa;GACd,EACF;EACD,aAAa;EACd,CAAC,EACqB,MAAM;AAC7B,KAAI,CAAC,GAAS,QAAS,OAAU,MAAM,GAAS,SAAS,wCAAwC;AACjG,QAAO;;AAGT,SAAgB,EACd,GACe;AACf,KAAI,CAAC,GAAS,QAAS,QAAO;CAC9B,IAAM,IAAO,EAAQ,eAAe;AACpC,QAAO,EAAQ,cAAc,QAAQ,EAAQ,cAAc,KAAA,IACvD,qBAAqB,EAAK,MAAM,EAAQ,UAAU,4CAClD,qBAAqB,EAAK"}
|
|
@@ -1,34 +1,20 @@
|
|
|
1
1
|
import { FC } from 'react';
|
|
2
|
+
import { KanbanData, KanbanTask, KanbanColumnMove } from './model';
|
|
2
3
|
import { Widget } from '../../../types';
|
|
3
4
|
import { WidgetTranslateFn } from '../utils/translate';
|
|
4
5
|
|
|
6
|
+
export type { KanbanData, KanbanTask, KanbanColumn } from './model';
|
|
5
7
|
export interface KanbanViewProps extends KanbanWidgetProps {
|
|
6
8
|
/** Translator — the app passes useI18n().t; the public embed passes a static fallback translator. */
|
|
7
9
|
t: WidgetTranslateFn;
|
|
10
|
+
interactive?: boolean;
|
|
11
|
+
onSaveMoves?: (moves: KanbanColumnMove[]) => Promise<boolean>;
|
|
8
12
|
}
|
|
9
13
|
export interface KanbanWidgetProps {
|
|
10
14
|
widget: Widget;
|
|
11
15
|
data?: KanbanData;
|
|
12
16
|
onClick?: (task: KanbanTask) => void;
|
|
13
17
|
}
|
|
14
|
-
export interface KanbanColumn {
|
|
15
|
-
id: string;
|
|
16
|
-
title: string;
|
|
17
|
-
color?: string;
|
|
18
|
-
}
|
|
19
|
-
export interface KanbanTask {
|
|
20
|
-
id: string;
|
|
21
|
-
title: string;
|
|
22
|
-
column: string;
|
|
23
|
-
priority?: 'high' | 'medium' | 'low';
|
|
24
|
-
assignee?: string;
|
|
25
|
-
labels?: string[];
|
|
26
|
-
dueDate?: string;
|
|
27
|
-
}
|
|
28
|
-
export interface KanbanData {
|
|
29
|
-
columns?: KanbanColumn[];
|
|
30
|
-
tasks?: KanbanTask[];
|
|
31
|
-
}
|
|
32
18
|
export declare const KanbanView: FC<KanbanViewProps>;
|
|
33
19
|
export default KanbanView;
|
|
34
20
|
//# sourceMappingURL=KanbanView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KanbanView.d.ts","sourceRoot":"","sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanView.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"KanbanView.d.ts","sourceRoot":"","sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanView.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,EAAmC,MAAM,OAAO,CAAC;AACjE,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,gBAAgB,EACtB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAM5D,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,qGAAqG;IACrG,CAAC,EAAE,iBAAiB,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACtC;AAmBD,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAmNzC,CAAC;AAEH,eAAe,UAAU,CAAC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { applyKanbanMoves as e, normalizeKanban as t, validKanbanMoves as n } from "./model.js";
|
|
2
|
+
import { memo as r, useMemo as i, useRef as a, useState as o } from "react";
|
|
3
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
3
4
|
//#region src/bigconsole/components/widgets/kanban/KanbanView.tsx
|
|
4
|
-
function
|
|
5
|
+
function l(e) {
|
|
5
6
|
switch (e) {
|
|
6
7
|
case "high": return "bg-status-error-bg/20 text-status-error-text";
|
|
7
8
|
case "medium": return "bg-status-warning-bg/20 text-[var(--color-status-warning-text)]";
|
|
@@ -9,184 +10,167 @@ function i(e) {
|
|
|
9
10
|
default: return "bg-bg-sunken text-text-secondary";
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
|
-
var
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (t === "urgent" || t === "high") return "high";
|
|
57
|
-
if (t === "medium" || t === "normal") return "medium";
|
|
58
|
-
if (t === "low") return "low";
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function d(e, t) {
|
|
62
|
-
if (e && typeof e == "object" && !Array.isArray(e) && Array.isArray(e.columns)) {
|
|
63
|
-
let t = e;
|
|
64
|
-
return {
|
|
65
|
-
columns: t.columns ?? [],
|
|
66
|
-
tasks: t.tasks ?? []
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
let n = c(e);
|
|
70
|
-
if (n.length === 0) return {
|
|
71
|
-
columns: [],
|
|
72
|
-
tasks: []
|
|
73
|
-
};
|
|
74
|
-
let r = n[0], i = l(r, [
|
|
75
|
-
s(t?.columnField),
|
|
76
|
-
s(t?.groupField),
|
|
77
|
-
s(t?.statusField),
|
|
78
|
-
...a,
|
|
79
|
-
s(t?.xAxisField),
|
|
80
|
-
s(t?.labelField)
|
|
81
|
-
]);
|
|
82
|
-
if (!i) return {
|
|
83
|
-
columns: [],
|
|
84
|
-
tasks: []
|
|
85
|
-
};
|
|
86
|
-
let d = l(r, [
|
|
87
|
-
s(t?.cardTitleField),
|
|
88
|
-
s(t?.titleField),
|
|
89
|
-
...o,
|
|
90
|
-
s(t?.labelField)
|
|
91
|
-
]), f = {}, p = [], m = n.map((e, t) => {
|
|
92
|
-
let n = e[i], r = n == null || n === "" ? "Uncategorized" : String(n);
|
|
93
|
-
r in f || (f[r] = r, p.push(r));
|
|
94
|
-
let a = e.id ?? e.identifier ?? e.key, o = Array.isArray(e.labels) ? e.labels.filter((e) => typeof e == "string") : void 0;
|
|
95
|
-
return {
|
|
96
|
-
id: a == null ? `task-${t}` : String(a),
|
|
97
|
-
title: d && e[d] != null ? String(e[d]) : `Item ${t + 1}`,
|
|
98
|
-
column: r,
|
|
99
|
-
assignee: s(e.assignee),
|
|
100
|
-
priority: u(e.priority ?? e.priorityLabel),
|
|
101
|
-
labels: o && o.length > 0 ? o : void 0
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
return {
|
|
105
|
-
columns: p.map((e) => ({
|
|
106
|
-
id: e,
|
|
107
|
-
title: f[e] ?? e
|
|
108
|
-
})),
|
|
109
|
-
tasks: m
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
var f = e(function({ widget: e, data: a, onClick: o, t: s }) {
|
|
113
|
-
let c = e.config, { columns: l, tasks: u } = t(() => d(a, c), [a, c]), f = t(() => {
|
|
114
|
-
let e = {};
|
|
115
|
-
return l.forEach((t) => {
|
|
116
|
-
e[t.id] = u.filter((e) => e.column === t.id);
|
|
13
|
+
var u = r(function({ widget: r, data: u, onClick: d, t: f, onSaveMoves: p, interactive: m = !1 }) {
|
|
14
|
+
let h = r.config, { columns: g, tasks: _ } = i(() => t(u, h), [u, h]), [v, y] = o(null), [b, x] = o(!1), [S, C] = o(""), w = a(null), T = v && v.data === u && v.widgetId === r.id ? v.moves : n(h?.kanbanColumnMoves), E = i(() => e(_, g, T), [
|
|
15
|
+
_,
|
|
16
|
+
g,
|
|
17
|
+
T
|
|
18
|
+
]), D = async (e, t) => {
|
|
19
|
+
if (!m || b || !g.some((e) => e.id === t)) return;
|
|
20
|
+
let i = _.find((t) => t.id === e);
|
|
21
|
+
if (!i || E.find((t) => t.id === e)?.column === t) return;
|
|
22
|
+
let a = n(T), o = [...a.filter((t) => t.taskId !== e), {
|
|
23
|
+
taskId: e,
|
|
24
|
+
sourceColumn: i.column,
|
|
25
|
+
targetColumn: t
|
|
26
|
+
}];
|
|
27
|
+
if (y({
|
|
28
|
+
data: u,
|
|
29
|
+
widgetId: r.id,
|
|
30
|
+
moves: o
|
|
31
|
+
}), !p || !i.stableId) {
|
|
32
|
+
C(f("bigconsole.widget.kanban.move.temporary", "{{title}} moved to {{column}} for this view.", {
|
|
33
|
+
title: i.title,
|
|
34
|
+
column: t
|
|
35
|
+
}));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
x(!0);
|
|
39
|
+
let s;
|
|
40
|
+
try {
|
|
41
|
+
s = await p(o.filter((e) => _.some((t) => t.id === e.taskId && t.stableId)));
|
|
42
|
+
} catch {
|
|
43
|
+
s = !1;
|
|
44
|
+
}
|
|
45
|
+
x(!1), s ? C(f("bigconsole.widget.kanban.move.saved", "{{title}} moved to {{column}} and saved.", {
|
|
46
|
+
title: i.title,
|
|
47
|
+
column: t
|
|
48
|
+
})) : (y({
|
|
49
|
+
data: u,
|
|
50
|
+
widgetId: r.id,
|
|
51
|
+
moves: a
|
|
52
|
+
}), C(f("bigconsole.widget.kanban.move.failed", "Could not save the move. The card was returned to its previous column.")));
|
|
53
|
+
}, O = i(() => {
|
|
54
|
+
let e = /* @__PURE__ */ new Map();
|
|
55
|
+
return g.forEach((t) => {
|
|
56
|
+
e.set(t.id, E.filter((e) => e.column === t.id));
|
|
117
57
|
}), e;
|
|
118
|
-
}, [
|
|
119
|
-
return
|
|
120
|
-
className: "flex h-full
|
|
121
|
-
children:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
58
|
+
}, [g, E]);
|
|
59
|
+
return g.length ? /* @__PURE__ */ c("div", {
|
|
60
|
+
className: "flex h-full flex-col",
|
|
61
|
+
children: [m && /* @__PURE__ */ s("p", {
|
|
62
|
+
role: "status",
|
|
63
|
+
className: "px-2 pt-2 text-xs text-text-secondary",
|
|
64
|
+
children: S || (p ? f("bigconsole.widget.kanban.move.editorHint", "Moves are saved to this widget.") : f("bigconsole.widget.kanban.move.viewerHint", "Moves are temporary in this view."))
|
|
65
|
+
}), /* @__PURE__ */ s("div", {
|
|
66
|
+
className: "flex min-h-0 flex-1 p-2 gap-2 overflow-x-auto",
|
|
67
|
+
children: g.map((e) => {
|
|
68
|
+
let t = O.get(e.id) || [];
|
|
69
|
+
return /* @__PURE__ */ c("div", {
|
|
70
|
+
"aria-label": `Column ${e.title}`,
|
|
71
|
+
className: "flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg",
|
|
72
|
+
onDragOver: (e) => {
|
|
73
|
+
w.current && !b && (e.preventDefault(), e.dataTransfer.dropEffect = "move");
|
|
74
|
+
},
|
|
75
|
+
onDrop: (t) => {
|
|
76
|
+
t.preventDefault(), t.stopPropagation();
|
|
77
|
+
let n = w.current;
|
|
78
|
+
w.current = null, n && D(n, e.id);
|
|
79
|
+
},
|
|
80
|
+
children: [/* @__PURE__ */ c("div", {
|
|
81
|
+
className: "flex items-center gap-2 p-2 border-b border-border-default",
|
|
82
|
+
children: [
|
|
83
|
+
/* @__PURE__ */ s("div", {
|
|
84
|
+
className: "w-2 h-2 rounded-full",
|
|
85
|
+
style: { backgroundColor: e.color || "var(--color-text-muted)" }
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ s("span", {
|
|
88
|
+
className: "text-xs font-medium text-text-primary truncate",
|
|
89
|
+
children: e.title
|
|
90
|
+
}),
|
|
91
|
+
/* @__PURE__ */ s("span", {
|
|
92
|
+
className: "ml-auto text-xs text-text-muted",
|
|
93
|
+
children: t.length
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
}), /* @__PURE__ */ c("div", {
|
|
97
|
+
className: "flex-1 p-2 space-y-2 overflow-y-auto",
|
|
98
|
+
children: [t.map((e) => /* @__PURE__ */ c("div", {
|
|
99
|
+
draggable: m && !b,
|
|
100
|
+
onDragStart: (t) => {
|
|
101
|
+
t.stopPropagation(), w.current = e.id, t.dataTransfer.effectAllowed = "move", t.dataTransfer.setData("text/plain", e.id);
|
|
102
|
+
},
|
|
103
|
+
onDragEnd: () => {
|
|
104
|
+
w.current = null;
|
|
105
|
+
},
|
|
106
|
+
className: `
|
|
145
107
|
p-2 bg-bg-surface rounded border border-border-default
|
|
146
|
-
${
|
|
108
|
+
${d ? "cursor-pointer hover:border-action-primary-bg" : ""}
|
|
147
109
|
transition-colors
|
|
148
110
|
`,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
111
|
+
onClick: () => d?.(e),
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ s("p", {
|
|
114
|
+
className: "text-xs font-medium text-text-primary line-clamp-2",
|
|
115
|
+
children: e.title
|
|
116
|
+
}),
|
|
117
|
+
e.description && /* @__PURE__ */ s("p", {
|
|
118
|
+
className: "mt-1 text-xs text-text-secondary line-clamp-3",
|
|
119
|
+
children: e.description
|
|
120
|
+
}),
|
|
121
|
+
m && /* @__PURE__ */ s("select", {
|
|
122
|
+
"aria-label": `Move ${e.title} to column`,
|
|
123
|
+
value: e.column,
|
|
124
|
+
disabled: b,
|
|
125
|
+
onClick: (e) => e.stopPropagation(),
|
|
126
|
+
onChange: (t) => {
|
|
127
|
+
D(e.id, t.target.value);
|
|
128
|
+
},
|
|
129
|
+
className: "mt-2 w-full rounded border border-border-default bg-bg-surface text-xs text-text-secondary",
|
|
130
|
+
children: g.map((e) => /* @__PURE__ */ s("option", {
|
|
131
|
+
value: e.id,
|
|
132
|
+
children: e.title
|
|
133
|
+
}, e.id))
|
|
134
|
+
}),
|
|
135
|
+
/* @__PURE__ */ c("div", {
|
|
136
|
+
className: "flex items-center gap-1 mt-2 flex-wrap",
|
|
137
|
+
children: [e.priority && /* @__PURE__ */ s("span", {
|
|
138
|
+
className: `px-1.5 py-0.5 text-[10px] rounded ${l(e.priority)}`,
|
|
139
|
+
children: e.priority
|
|
140
|
+
}), e.assignee && /* @__PURE__ */ s("span", {
|
|
141
|
+
className: "px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text",
|
|
142
|
+
children: e.assignee
|
|
143
|
+
})]
|
|
144
|
+
})
|
|
145
|
+
]
|
|
146
|
+
}, e.id)), t.length === 0 && /* @__PURE__ */ s("p", {
|
|
147
|
+
className: "text-[10px] text-text-muted text-center py-4",
|
|
148
|
+
children: f("bigconsole.widget.kanban.noTasks", "No tasks")
|
|
162
149
|
})]
|
|
163
|
-
}, e.id)), t.length === 0 && /* @__PURE__ */ n("p", {
|
|
164
|
-
className: "text-[10px] text-text-muted text-center py-4",
|
|
165
|
-
children: s("bigconsole.widget.kanban.noTasks", "No tasks")
|
|
166
150
|
})]
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
}) : /* @__PURE__ */
|
|
151
|
+
}, e.id);
|
|
152
|
+
})
|
|
153
|
+
})]
|
|
154
|
+
}) : /* @__PURE__ */ s("div", {
|
|
171
155
|
className: "flex items-center justify-center h-full text-text-secondary text-sm",
|
|
172
|
-
children: /* @__PURE__ */
|
|
156
|
+
children: /* @__PURE__ */ c("div", {
|
|
173
157
|
className: "text-center",
|
|
174
|
-
children: [/* @__PURE__ */
|
|
158
|
+
children: [/* @__PURE__ */ s("svg", {
|
|
175
159
|
className: "w-12 h-12 mx-auto mb-2 text-text-muted",
|
|
176
160
|
fill: "none",
|
|
177
161
|
stroke: "currentColor",
|
|
178
162
|
viewBox: "0 0 24 24",
|
|
179
|
-
children: /* @__PURE__ */
|
|
163
|
+
children: /* @__PURE__ */ s("path", {
|
|
180
164
|
strokeLinecap: "round",
|
|
181
165
|
strokeLinejoin: "round",
|
|
182
166
|
strokeWidth: 1.5,
|
|
183
167
|
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"
|
|
184
168
|
})
|
|
185
|
-
}), /* @__PURE__ */
|
|
169
|
+
}), /* @__PURE__ */ s("p", { children: f("bigconsole.widget.kanban.emptyState", "No board data") })]
|
|
186
170
|
})
|
|
187
171
|
});
|
|
188
172
|
});
|
|
189
173
|
//#endregion
|
|
190
|
-
export {
|
|
174
|
+
export { u as default };
|
|
191
175
|
|
|
192
176
|
//# sourceMappingURL=KanbanView.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KanbanView.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanView.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 type { Widget } from '../../../types';\nimport type { WidgetTranslateFn } from '../utils/translate';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanViewProps extends KanbanWidgetProps {\n /** Translator — the app passes useI18n().t; the public embed passes a static fallback translator. */\n t: WidgetTranslateFn;\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 KanbanView: FC<KanbanViewProps> = memo(function KanbanView({ widget, data, onClick, t }) {\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 KanbanView;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"KanbanView.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanView.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo, useRef, useState } from 'react';\nimport {\n normalizeKanban,\n applyKanbanMoves,\n validKanbanMoves,\n type KanbanData,\n type KanbanTask,\n type KanbanColumnMove,\n} from './model';\nexport type { KanbanData, KanbanTask, KanbanColumn } from './model';\nimport type { Widget } from '../../../types';\nimport type { WidgetTranslateFn } from '../utils/translate';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanViewProps extends KanbanWidgetProps {\n /** Translator — the app passes useI18n().t; the public embed passes a static fallback translator. */\n t: WidgetTranslateFn;\n interactive?: boolean;\n onSaveMoves?: (moves: KanbanColumnMove[]) => Promise<boolean>;\n}\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\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// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanView: FC<KanbanViewProps> = memo(function KanbanView({\n widget,\n data,\n onClick,\n t,\n onSaveMoves,\n interactive = false,\n}) {\n const config = widget.config as Record<string, unknown> | undefined;\n const { columns, tasks: sourceTasks } = useMemo(() => normalizeKanban(data, config), [data, config]);\n const [local, setLocal] = useState<{ data: unknown; widgetId: string; moves: KanbanColumnMove[] } | null>(null);\n const [saving, setSaving] = useState(false);\n const [announcement, setAnnouncement] = useState('');\n const dragging = useRef<string | null>(null);\n const moves =\n local && local.data === data && local.widgetId === widget.id\n ? local.moves\n : validKanbanMoves(config?.kanbanColumnMoves);\n const tasks = useMemo(() => applyKanbanMoves(sourceTasks, columns, moves), [sourceTasks, columns, moves]);\n const moveTask = async (taskId: string, targetColumn: string) => {\n if (!interactive || saving || !columns.some((column) => column.id === targetColumn)) return;\n const task = sourceTasks.find((item) => item.id === taskId);\n if (!task || tasks.find((item) => item.id === taskId)?.column === targetColumn) return;\n const previous = validKanbanMoves(moves);\n const next = [\n ...previous.filter((move) => move.taskId !== taskId),\n { taskId, sourceColumn: task.column, targetColumn },\n ];\n setLocal({ data, widgetId: widget.id, moves: next });\n if (!onSaveMoves || !task.stableId) {\n setAnnouncement(\n t('bigconsole.widget.kanban.move.temporary', '{{title}} moved to {{column}} for this view.', {\n title: task.title,\n column: targetColumn,\n })\n );\n return;\n }\n setSaving(true);\n let saved: boolean;\n try {\n saved = await onSaveMoves(\n next.filter((move) => sourceTasks.some((source) => source.id === move.taskId && source.stableId))\n );\n } catch {\n saved = false;\n }\n setSaving(false);\n if (!saved) {\n setLocal({ data, widgetId: widget.id, moves: previous });\n setAnnouncement(\n t(\n 'bigconsole.widget.kanban.move.failed',\n 'Could not save the move. The card was returned to its previous column.'\n )\n );\n } else {\n setAnnouncement(\n t('bigconsole.widget.kanban.move.saved', '{{title}} moved to {{column}} and saved.', {\n title: task.title,\n column: targetColumn,\n })\n );\n }\n };\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped = new Map<string, KanbanTask[]>();\n columns.forEach((col) => {\n grouped.set(\n col.id,\n tasks.filter((t) => t.column === col.id)\n );\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 flex-col\">\n {interactive && (\n <p role=\"status\" className=\"px-2 pt-2 text-xs text-text-secondary\">\n {announcement ||\n (onSaveMoves\n ? t('bigconsole.widget.kanban.move.editorHint', 'Moves are saved to this widget.')\n : t('bigconsole.widget.kanban.move.viewerHint', 'Moves are temporary in this view.'))}\n </p>\n )}\n <div className=\"flex min-h-0 flex-1 p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn.get(column.id) || [];\n\n return (\n <div\n key={column.id}\n aria-label={`Column ${column.title}`}\n className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\"\n onDragOver={(event) => {\n if (dragging.current && !saving) {\n event.preventDefault();\n event.dataTransfer.dropEffect = 'move';\n }\n }}\n onDrop={(event) => {\n event.preventDefault();\n event.stopPropagation();\n const id = dragging.current;\n dragging.current = null;\n if (id) void moveTask(id, column.id);\n }}\n >\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 draggable={interactive && !saving}\n onDragStart={(event) => {\n event.stopPropagation();\n dragging.current = task.id;\n event.dataTransfer.effectAllowed = 'move';\n event.dataTransfer.setData('text/plain', task.id);\n }}\n onDragEnd={() => {\n dragging.current = null;\n }}\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 {task.description && (\n <p className=\"mt-1 text-xs text-text-secondary line-clamp-3\">{task.description}</p>\n )}\n {interactive && (\n <select\n aria-label={`Move ${task.title} to column`}\n value={task.column}\n disabled={saving}\n onClick={(event) => event.stopPropagation()}\n onChange={(event) => {\n void moveTask(task.id, event.target.value);\n }}\n className=\"mt-2 w-full rounded border border-border-default bg-bg-surface text-xs text-text-secondary\"\n >\n {columns.map((target) => (\n <option key={target.id} value={target.id}>\n {target.title}\n </option>\n ))}\n </select>\n )}\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 </div>\n );\n});\n\nexport default KanbanView;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KanbanWidget.d.ts","sourceRoot":"","sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,EAAQ,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"KanbanWidget.d.ts","sourceRoot":"","sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,EAAQ,MAAM,OAAO,CAAC;AAGtC,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAElE,cAAc,cAAc,CAAC;AAE7B,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAW7C,CAAC;AAEH,eAAe,YAAY,CAAC"}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import e from "
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import e from "../../../hooks/useWidgetConfigPersist.js";
|
|
2
|
+
import t from "./KanbanView.js";
|
|
3
|
+
import { memo as n } from "react";
|
|
4
|
+
import { jsx as r } from "react/jsx-runtime";
|
|
5
|
+
import { useI18n as i } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
5
6
|
//#region src/bigconsole/components/widgets/kanban/KanbanWidget.tsx
|
|
6
|
-
var
|
|
7
|
-
let { t: i } =
|
|
8
|
-
return /* @__PURE__ */
|
|
9
|
-
...
|
|
10
|
-
t:
|
|
7
|
+
var a = n(function(n) {
|
|
8
|
+
let { t: a } = i(), { canPersist: o, persistConfig: s } = e(n.widget);
|
|
9
|
+
return /* @__PURE__ */ r(t, {
|
|
10
|
+
...n,
|
|
11
|
+
t: a,
|
|
12
|
+
interactive: !0,
|
|
13
|
+
onSaveMoves: o ? (e) => s({ kanbanColumnMoves: e }) : void 0
|
|
11
14
|
});
|
|
12
15
|
});
|
|
13
16
|
//#endregion
|
|
14
|
-
export {
|
|
17
|
+
export { a as default };
|
|
15
18
|
|
|
16
19
|
//# sourceMappingURL=KanbanWidget.js.map
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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 { useWidgetConfigPersist } from '../../../hooks/useWidgetConfigPersist';\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 const { canPersist, persistConfig } = useWidgetConfigPersist(props.widget);\n return (\n <KanbanView\n {...props}\n t={t}\n interactive\n onSaveMoves={canPersist ? (moves) => persistConfig({ kanbanColumnMoves: moves }) : undefined}\n />\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;;;;AAeA,IAAa,IAAsC,EAAK,SAAsB,GAAO;CACnF,IAAM,EAAE,SAAM,GAAS,EACjB,EAAE,eAAY,qBAAkB,EAAuB,EAAM,OAAO;AAC1E,QACE,kBAAC,GAAD;EACE,GAAI;EACD;EACH,aAAA;EACA,aAAa,KAAc,MAAU,EAAc,EAAE,mBAAmB,GAAO,CAAC,GAAG,KAAA;EACnF,CAAA;EAEJ"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface KanbanColumn {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface KanbanTask {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
column: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
stableId?: boolean;
|
|
12
|
+
priority?: 'high' | 'medium' | 'low';
|
|
13
|
+
assignee?: string;
|
|
14
|
+
labels?: string[];
|
|
15
|
+
dueDate?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface KanbanData {
|
|
18
|
+
columns?: KanbanColumn[];
|
|
19
|
+
tasks?: KanbanTask[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Normalize widget data into the board's { columns, tasks } shape.
|
|
23
|
+
*
|
|
24
|
+
* KanbanWidget previously required the exact canonical shape and rendered
|
|
25
|
+
* "No board data" for any DataSink/parser output that was a plain array of
|
|
26
|
+
* rows (which is what parsers actually emit) — BOFF-4110. This adapts a flat
|
|
27
|
+
* array into columns (distinct values of a status/column field, in first-seen
|
|
28
|
+
* order) plus one card per row, honoring config field mappings with fallbacks.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeKanban(data: unknown, config?: Record<string, unknown>): {
|
|
31
|
+
columns: KanbanColumn[];
|
|
32
|
+
tasks: KanbanTask[];
|
|
33
|
+
};
|
|
34
|
+
export interface KanbanColumnMove {
|
|
35
|
+
taskId: string;
|
|
36
|
+
sourceColumn: string;
|
|
37
|
+
targetColumn: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function validKanbanMoves(value: unknown): KanbanColumnMove[];
|
|
40
|
+
/** Overrides only affect a current card in its original source column. */
|
|
41
|
+
export declare function applyKanbanMoves(tasks: KanbanTask[], columns: KanbanColumn[], value: unknown): KanbanTask[];
|
|
42
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../../../src/bigconsole/components/widgets/kanban/model.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;CACtB;AAqED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B;IAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAA;CAAE,CAwDlD;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,EAAE,CAWnE;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,OAAO,GAAG,UAAU,EAAE,CAY3G"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
//#region src/bigconsole/components/widgets/kanban/model.ts
|
|
2
|
+
var e = [
|
|
3
|
+
"status",
|
|
4
|
+
"state",
|
|
5
|
+
"column",
|
|
6
|
+
"stage",
|
|
7
|
+
"category",
|
|
8
|
+
"phase",
|
|
9
|
+
"group"
|
|
10
|
+
], t = [
|
|
11
|
+
"title",
|
|
12
|
+
"name",
|
|
13
|
+
"summary",
|
|
14
|
+
"label",
|
|
15
|
+
"identifier",
|
|
16
|
+
"id"
|
|
17
|
+
];
|
|
18
|
+
function n(e) {
|
|
19
|
+
return typeof e == "string" && e.length > 0 ? e : void 0;
|
|
20
|
+
}
|
|
21
|
+
function r(e) {
|
|
22
|
+
let t = (e) => e.filter((e) => !!e && typeof e == "object" && !Array.isArray(e));
|
|
23
|
+
if (Array.isArray(e)) return t(e);
|
|
24
|
+
if (e && typeof e == "object") {
|
|
25
|
+
let n = e;
|
|
26
|
+
for (let e of [
|
|
27
|
+
"tasks",
|
|
28
|
+
"rows",
|
|
29
|
+
"items",
|
|
30
|
+
"data",
|
|
31
|
+
"nodes",
|
|
32
|
+
"results",
|
|
33
|
+
"records"
|
|
34
|
+
]) if (Array.isArray(n[e])) return t(n[e]);
|
|
35
|
+
let r = Object.values(n).find((e) => Array.isArray(e));
|
|
36
|
+
if (Array.isArray(r)) return t(r);
|
|
37
|
+
}
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
function i(e, t) {
|
|
41
|
+
for (let n of t) if (n && n in e) return n;
|
|
42
|
+
}
|
|
43
|
+
function a(e) {
|
|
44
|
+
let t = n(e)?.toLowerCase();
|
|
45
|
+
if (t) {
|
|
46
|
+
if (t === "urgent" || t === "high") return "high";
|
|
47
|
+
if (t === "medium" || t === "normal") return "medium";
|
|
48
|
+
if (t === "low") return "low";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function o(e) {
|
|
52
|
+
let t = e.map((e) => typeof e == "string" || typeof e == "number" ? String(e) : ""), n = /* @__PURE__ */ new Map();
|
|
53
|
+
t.forEach((e) => {
|
|
54
|
+
e && n.set(e, (n.get(e) ?? 0) + 1);
|
|
55
|
+
});
|
|
56
|
+
let r = new Set(t);
|
|
57
|
+
return t.map((e, t) => {
|
|
58
|
+
if (e && n.get(e) === 1) return {
|
|
59
|
+
id: e,
|
|
60
|
+
stableId: !0
|
|
61
|
+
};
|
|
62
|
+
let i = `row-${t}`;
|
|
63
|
+
for (; r.has(i);) i += "-local";
|
|
64
|
+
return r.add(i), {
|
|
65
|
+
id: i,
|
|
66
|
+
stableId: !1
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function s(s, c) {
|
|
71
|
+
if (s && typeof s == "object" && !Array.isArray(s) && Array.isArray(s.columns)) {
|
|
72
|
+
let e = s, t = o((e.tasks ?? []).map((e) => e.id));
|
|
73
|
+
return {
|
|
74
|
+
columns: e.columns ?? [],
|
|
75
|
+
tasks: (e.tasks ?? []).map((e, n) => ({
|
|
76
|
+
...e,
|
|
77
|
+
...t[n]
|
|
78
|
+
}))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
let l = r(s);
|
|
82
|
+
if (l.length === 0) return {
|
|
83
|
+
columns: [],
|
|
84
|
+
tasks: []
|
|
85
|
+
};
|
|
86
|
+
let u = l[0], d = i(u, [
|
|
87
|
+
n(c?.columnField),
|
|
88
|
+
n(c?.groupField),
|
|
89
|
+
n(c?.statusField),
|
|
90
|
+
...e,
|
|
91
|
+
n(c?.xAxisField),
|
|
92
|
+
n(c?.labelField)
|
|
93
|
+
]);
|
|
94
|
+
if (!d) return {
|
|
95
|
+
columns: [],
|
|
96
|
+
tasks: []
|
|
97
|
+
};
|
|
98
|
+
let f = i(u, [
|
|
99
|
+
n(c?.cardTitleField),
|
|
100
|
+
n(c?.titleField),
|
|
101
|
+
...t,
|
|
102
|
+
n(c?.labelField)
|
|
103
|
+
]), p = o(l.map((e) => e.id ?? e.identifier ?? e.key)), m = /* @__PURE__ */ new Map(), h = [], g = l.map((e, t) => {
|
|
104
|
+
let r = e[d], i = r == null || r === "" ? "Uncategorized" : String(r);
|
|
105
|
+
m.has(i) || (m.set(i, i), h.push(i));
|
|
106
|
+
let o = Array.isArray(e.labels) ? e.labels.filter((e) => typeof e == "string") : void 0;
|
|
107
|
+
return {
|
|
108
|
+
...p[t],
|
|
109
|
+
description: n(e[n(c?.cardDescriptionField) ?? "description"]),
|
|
110
|
+
title: f && e[f] != null ? String(e[f]) : `Item ${t + 1}`,
|
|
111
|
+
column: i,
|
|
112
|
+
assignee: n(e.assignee),
|
|
113
|
+
priority: a(e.priority ?? e.priorityLabel),
|
|
114
|
+
labels: o && o.length > 0 ? o : void 0
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
columns: h.map((e) => ({
|
|
119
|
+
id: e,
|
|
120
|
+
title: m.get(e) ?? e
|
|
121
|
+
})),
|
|
122
|
+
tasks: g
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function c(e) {
|
|
126
|
+
return Array.isArray(e) ? e.filter((e) => {
|
|
127
|
+
if (!e || typeof e != "object") return !1;
|
|
128
|
+
let t = e;
|
|
129
|
+
return typeof t.taskId == "string" && typeof t.sourceColumn == "string" && typeof t.targetColumn == "string";
|
|
130
|
+
}) : [];
|
|
131
|
+
}
|
|
132
|
+
function l(e, t, n) {
|
|
133
|
+
let r = c(n), i = new Set(t.map((e) => e.id));
|
|
134
|
+
return e.map((e) => {
|
|
135
|
+
let t = [...r].reverse().find((t) => t.taskId === e.id && t.sourceColumn === e.column && i.has(t.targetColumn));
|
|
136
|
+
return t ? {
|
|
137
|
+
...e,
|
|
138
|
+
column: t.targetColumn
|
|
139
|
+
} : e;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { l as applyKanbanMoves, s as normalizeKanban, c as validKanbanMoves };
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/model.ts"],"sourcesContent":["export 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 description?: string;\n stableId?: boolean;\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\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/** Index fallbacks are local-only and cannot collide with source IDs. */\nfunction taskIdentities(values: unknown[]): { id: string; stableId: boolean }[] {\n const ids = values.map((value) => (typeof value === 'string' || typeof value === 'number' ? String(value) : ''));\n const counts = new Map<string, number>();\n ids.forEach((id) => {\n if (id) counts.set(id, (counts.get(id) ?? 0) + 1);\n });\n const used = new Set(ids);\n return ids.map((id, index) => {\n if (id && counts.get(id) === 1) return { id, stableId: true };\n let localId = `row-${index}`;\n while (used.has(localId)) localId += '-local';\n used.add(localId);\n return { id: localId, stableId: false };\n });\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 */\nexport function 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 const identities = taskIdentities((d.tasks ?? []).map((task) => task.id));\n return {\n columns: d.columns ?? [],\n tasks: (d.tasks ?? []).map((task, index) => ({ ...task, ...identities[index] })),\n };\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 identities = taskIdentities(rows.map((row) => row.id ?? row.identifier ?? row.key));\n const columnTitles = new Map<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 (!columnTitles.has(column)) {\n columnTitles.set(column, column);\n columnOrder.push(column);\n }\n const labels = Array.isArray(row.labels) ? row.labels.filter((l): l is string => typeof l === 'string') : undefined;\n return {\n ...identities[i],\n description: asString(row[asString(config?.cardDescriptionField) ?? 'description']),\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.get(id) ?? id }));\n return { columns, tasks };\n}\n\nexport interface KanbanColumnMove {\n taskId: string;\n sourceColumn: string;\n targetColumn: string;\n}\n\nexport function validKanbanMoves(value: unknown): KanbanColumnMove[] {\n if (!Array.isArray(value)) return [];\n return value.filter((move): move is KanbanColumnMove => {\n if (!move || typeof move !== 'object') return false;\n const candidate = move as Partial<KanbanColumnMove>;\n return (\n typeof candidate.taskId === 'string' &&\n typeof candidate.sourceColumn === 'string' &&\n typeof candidate.targetColumn === 'string'\n );\n });\n}\n\n/** Overrides only affect a current card in its original source column. */\nexport function applyKanbanMoves(tasks: KanbanTask[], columns: KanbanColumn[], value: unknown): KanbanTask[] {\n const moves = validKanbanMoves(value);\n const targets = new Set(columns.map((column) => column.id));\n return tasks.map((task) => {\n const move = [...moves]\n .reverse()\n .find(\n (candidate) =>\n candidate.taskId === task.id && candidate.sourceColumn === task.column && targets.has(candidate.targetColumn)\n );\n return move ? { ...task, column: move.targetColumn } : task;\n });\n}\n"],"mappings":"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"}
|