@burdenoff/microfe-bigconsole 2026.1001.2 → 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.
@@ -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,EAAiB,MAAM,OAAO,CAAC;AAC/C,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;CACtB;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;AAED,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,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;AAsID,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAwFzC,CAAC;AAEH,eAAe,UAAU,CAAC"}
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 { memo as e, useMemo as t } from "react";
2
- import { jsx as n, jsxs as r } from "react/jsx-runtime";
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 i(e) {
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 a = [
13
- "status",
14
- "state",
15
- "column",
16
- "stage",
17
- "category",
18
- "phase",
19
- "group"
20
- ], o = [
21
- "title",
22
- "name",
23
- "summary",
24
- "label",
25
- "identifier",
26
- "id"
27
- ];
28
- function s(e) {
29
- return typeof e == "string" && e.length > 0 ? e : void 0;
30
- }
31
- function c(e) {
32
- let t = (e) => e.filter((e) => !!e && typeof e == "object" && !Array.isArray(e));
33
- if (Array.isArray(e)) return t(e);
34
- if (e && typeof e == "object") {
35
- let n = e;
36
- for (let e of [
37
- "tasks",
38
- "rows",
39
- "items",
40
- "data",
41
- "nodes",
42
- "results",
43
- "records"
44
- ]) if (Array.isArray(n[e])) return t(n[e]);
45
- let r = Object.values(n).find((e) => Array.isArray(e));
46
- if (Array.isArray(r)) return t(r);
47
- }
48
- return [];
49
- }
50
- function l(e, t) {
51
- for (let n of t) if (n && n in e) return n;
52
- }
53
- function u(e) {
54
- let t = s(e)?.toLowerCase();
55
- if (t) {
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
- }, [l, u]);
119
- return l.length ? /* @__PURE__ */ n("div", {
120
- className: "flex h-full p-2 gap-2 overflow-x-auto",
121
- children: l.map((e) => {
122
- let t = f[e.id] || [];
123
- return /* @__PURE__ */ r("div", {
124
- className: "flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg",
125
- children: [/* @__PURE__ */ r("div", {
126
- className: "flex items-center gap-2 p-2 border-b border-border-default",
127
- children: [
128
- /* @__PURE__ */ n("div", {
129
- className: "w-2 h-2 rounded-full",
130
- style: { backgroundColor: e.color || "var(--color-text-muted)" }
131
- }),
132
- /* @__PURE__ */ n("span", {
133
- className: "text-xs font-medium text-text-primary truncate",
134
- children: e.title
135
- }),
136
- /* @__PURE__ */ n("span", {
137
- className: "ml-auto text-xs text-text-muted",
138
- children: t.length
139
- })
140
- ]
141
- }), /* @__PURE__ */ r("div", {
142
- className: "flex-1 p-2 space-y-2 overflow-y-auto",
143
- children: [t.map((e) => /* @__PURE__ */ r("div", {
144
- className: `
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
- ${o ? "cursor-pointer hover:border-action-primary-bg" : ""}
108
+ ${d ? "cursor-pointer hover:border-action-primary-bg" : ""}
147
109
  transition-colors
148
110
  `,
149
- onClick: () => o?.(e),
150
- children: [/* @__PURE__ */ n("p", {
151
- className: "text-xs font-medium text-text-primary line-clamp-2",
152
- children: e.title
153
- }), /* @__PURE__ */ r("div", {
154
- className: "flex items-center gap-1 mt-2 flex-wrap",
155
- children: [e.priority && /* @__PURE__ */ n("span", {
156
- className: `px-1.5 py-0.5 text-[10px] rounded ${i(e.priority)}`,
157
- children: e.priority
158
- }), e.assignee && /* @__PURE__ */ n("span", {
159
- className: "px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text",
160
- children: e.assignee
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
- }, e.id);
169
- })
170
- }) : /* @__PURE__ */ n("div", {
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__ */ r("div", {
156
+ children: /* @__PURE__ */ c("div", {
173
157
  className: "text-center",
174
- children: [/* @__PURE__ */ n("svg", {
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__ */ n("path", {
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__ */ n("p", { children: s("bigconsole.widget.kanban.emptyState", "No board data") })]
169
+ }), /* @__PURE__ */ s("p", { children: f("bigconsole.widget.kanban.emptyState", "No board data") })]
186
170
  })
187
171
  });
188
172
  });
189
173
  //#endregion
190
- export { f as default };
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":";;;AAkDA,SAAS,EAAiB,GAA2B;AACnD,SAAQ,GAAR;EACE,KAAK,OACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,MACH,QAAO;EACT,QACE,QAAO;;;AAQb,IAAM,IAA0B;CAAC;CAAU;CAAS;CAAU;CAAS;CAAY;CAAS;CAAQ,EAC9F,IAAyB;CAAC;CAAS;CAAQ;CAAW;CAAS;CAAc;CAAK;AAExF,SAAS,EAAS,GAAoC;AACpD,QAAO,OAAO,KAAU,YAAY,EAAM,SAAS,IAAI,IAAQ,KAAA;;AAIjE,SAAS,EAAO,GAA0C;CACxD,IAAM,KAAe,MACnB,EAAI,QAAQ,MAAoC,CAAC,CAAC,KAAK,OAAO,KAAM,YAAY,CAAC,MAAM,QAAQ,EAAE,CAAC;AAEpG,KAAI,MAAM,QAAQ,EAAK,CAAE,QAAO,EAAY,EAAK;AAEjD,KAAI,KAAQ,OAAO,KAAS,UAAU;EACpC,IAAM,IAAM;AACZ,OAAK,IAAM,KAAO;GAAC;GAAS;GAAQ;GAAS;GAAQ;GAAS;GAAW;GAAU,CACjF,KAAI,MAAM,QAAQ,EAAI,GAAK,CAAE,QAAO,EAAY,EAAI,GAAkB;EAExE,IAAM,IAAa,OAAO,OAAO,EAAI,CAAC,MAAM,MAAM,MAAM,QAAQ,EAAE,CAAC;AACnE,MAAI,MAAM,QAAQ,EAAW,CAAE,QAAO,EAAY,EAAW;;AAE/D,QAAO,EAAE;;AAIX,SAAS,EAAa,GAA8B,GAAkD;AACpG,MAAK,IAAM,KAAO,EAChB,KAAI,KAAO,KAAO,EAAK,QAAO;;AAKlC,SAAS,EAAkB,GAAwC;CACjE,IAAM,IAAI,EAAS,EAAM,EAAE,aAAa;AACnC,QACL;MAAI,MAAM,YAAY,MAAM,OAAQ,QAAO;AAC3C,MAAI,MAAM,YAAY,MAAM,SAAU,QAAO;AAC7C,MAAI,MAAM,MAAO,QAAO;;;AAa1B,SAAS,EACP,GACA,GACkD;AAElD,KAAI,KAAQ,OAAO,KAAS,YAAY,CAAC,MAAM,QAAQ,EAAK,IAAI,MAAM,QAAS,EAAoB,QAAQ,EAAE;EAC3G,IAAM,IAAI;AACV,SAAO;GAAE,SAAS,EAAE,WAAW,EAAE;GAAE,OAAO,EAAE,SAAS,EAAE;GAAE;;CAG3D,IAAM,IAAO,EAAO,EAAK;AACzB,KAAI,EAAK,WAAW,EAAG,QAAO;EAAE,SAAS,EAAE;EAAE,OAAO,EAAE;EAAE;CAExD,IAAM,IAAS,EAAK,IACd,IAAc,EAAa,GAAQ;EACvC,EAAS,GAAQ,YAAY;EAC7B,EAAS,GAAQ,WAAW;EAC5B,EAAS,GAAQ,YAAY;EAC7B,GAAG;EACH,EAAS,GAAQ,WAAW;EAC5B,EAAS,GAAQ,WAAW;EAC7B,CAAC;AACF,KAAI,CAAC,EAAa,QAAO;EAAE,SAAS,EAAE;EAAE,OAAO,EAAE;EAAE;CAEnD,IAAM,IAAa,EAAa,GAAQ;EACtC,EAAS,GAAQ,eAAe;EAChC,EAAS,GAAQ,WAAW;EAC5B,GAAG;EACH,EAAS,GAAQ,WAAW;EAC7B,CAAC,EAEI,IAAuC,EAAE,EACzC,IAAwB,EAAE,EAC1B,IAAsB,EAAK,KAAK,GAAK,MAAM;EAC/C,IAAM,IAAY,EAAI,IAChB,IAAS,KAAa,QAAQ,MAAc,KAAK,kBAAkB,OAAO,EAAU;AAC1F,EAAM,KAAU,MACd,EAAa,KAAU,GACvB,EAAY,KAAK,EAAO;EAE1B,IAAM,IAAQ,EAAI,MAAM,EAAI,cAAc,EAAI,KACxC,IAAS,MAAM,QAAQ,EAAI,OAAO,GAAG,EAAI,OAAO,QAAQ,MAAmB,OAAO,KAAM,SAAS,GAAG,KAAA;AAC1G,SAAO;GACL,IAAI,KAAS,OAAuB,QAAQ,MAAxB,OAAO,EAAM;GACjC,OAAO,KAAc,EAAI,MAAe,OAAO,OAAO,EAAI,GAAY,GAAG,QAAQ,IAAI;GACrF;GACA,UAAU,EAAS,EAAI,SAAS;GAChC,UAAU,EAAkB,EAAI,YAAY,EAAI,cAAc;GAC9D,QAAQ,KAAU,EAAO,SAAS,IAAI,IAAS,KAAA;GAChD;GACD;AAGF,QAAO;EAAE,SADuB,EAAY,KAAK,OAAQ;GAAE;GAAI,OAAO,EAAa,MAAO;GAAI,EAAE;EAC9E;EAAO;;AAO3B,IAAa,IAAkC,EAAK,SAAoB,EAAE,WAAQ,SAAM,YAAS,QAAK;CACpG,IAAM,IAAS,EAAO,QAChB,EAAE,YAAS,aAAU,QAAc,EAAgB,GAAM,EAAO,EAAE,CAAC,GAAM,EAAO,CAAC,EAGjF,IAAgB,QAAc;EAClC,IAAM,IAAwC,EAAE;AAIhD,SAHA,EAAQ,SAAS,MAAQ;AACvB,KAAQ,EAAI,MAAM,EAAM,QAAQ,MAAM,EAAE,WAAW,EAAI,GAAG;IAC1D,EACK;IACN,CAAC,GAAS,EAAM,CAAC;AAoBpB,QAlBK,EAAQ,SAmBX,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAc,EAAc,EAAO,OAAO,EAAE;AAElD,UACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAO,SAAS,2BAA2B;OACrE,CAAA;MACF,kBAAC,QAAD;OAAM,WAAU;iBAAkD,EAAO;OAAa,CAAA;MACtF,kBAAC,QAAD;OAAM,WAAU;iBAAmC,EAAY;OAAc,CAAA;MACzE;QAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAY,KAAK,MAChB,kBAAC,OAAD;MAEE,WAAW;;wBAEL,IAAU,kDAAkD,GAAG;;;MAGrE,eAAe,IAAU,EAAK;gBAPhC,CASE,kBAAC,KAAD;OAAG,WAAU;iBAAsD,EAAK;OAAU,CAAA,EAElF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAW,qCAAqC,EAAiB,EAAK,SAAS;kBAClF,EAAK;QACD,CAAA,EAER,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAK;QACD,CAAA,CAEL;SACF;QAtBC,EAAK,GAsBN,CACN,EAED,EAAY,WAAW,KACtB,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,oCAAoC,WAAW;MAChD,CAAA,CAEF;OACF;MA9CI,EAAO,GA8CX;IAER;EACE,CAAA,GAvEJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;IAAyC,MAAK;IAAO,QAAO;IAAe,SAAQ;cAChG,kBAAC,QAAD;KACE,eAAc;KACd,gBAAe;KACf,aAAa;KACb,GAAE;KACF,CAAA;IACE,CAAA,EACN,kBAAC,KAAD,EAAA,UAAI,EAAE,uCAAuC,gBAAgB,EAAK,CAAA,CAC9D;;EACF,CAAA;EA6DV"}
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;AAEtC,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAElE,cAAc,cAAc,CAAC;AAE7B,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAG7C,CAAC;AAEH,eAAe,YAAY,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 "./KanbanView.js";
2
- import { memo as t } from "react";
3
- import { jsx as n } from "react/jsx-runtime";
4
- import { useI18n as r } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
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 i = t(function(t) {
7
- let { t: i } = r();
8
- return /* @__PURE__ */ n(e, {
9
- ...t,
10
- t: i
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 { i as default };
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 {...props} t={t} />;\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;;;AAcA,IAAa,IAAsC,EAAK,SAAsB,GAAO;CACnF,IAAM,EAAE,SAAM,GAAS;AACvB,QAAO,kBAAC,GAAD;EAAY,GAAI;EAAU;EAAK,CAAA;EACtC"}
1
+ {"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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.1001.2",
3
+ "version": "2026.1001.3",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [