@burdenoff/microfe-bigconsole 2026.922.1 → 2026.922.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,78 +1,78 @@
1
1
  import e from "../../../hooks/useWidgetComments.js";
2
- import { useCallback as t, useState as n } from "react";
3
- import { Check as r, CornerDownRight as i, Loader2 as a, MessageSquare as o, RotateCcw as s, Send as c, Trash2 as l, X as u } from "lucide-react";
4
- import { useI18n as d } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
5
- import { jsx as f, jsxs as p } from "react/jsx-runtime";
2
+ import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
3
+ import { Check as a, CornerDownRight as o, Loader2 as s, MessageSquare as c, RotateCcw as l, Send as u, Trash2 as d, X as f } from "lucide-react";
4
+ import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
5
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
6
6
  //#region src/bigconsole/components/widgets/comments/WidgetComments.tsx
7
- function m(e, t) {
7
+ function g(e, t) {
8
8
  return e("bigconsole.widget.comments.authorLabel", "User {{id}}", { id: t.slice(0, 6) });
9
9
  }
10
- function h(e) {
10
+ function _(e) {
11
11
  let t = new Date(e);
12
12
  return Number.isNaN(t.getTime()) ? "" : new Intl.DateTimeFormat(void 0, {
13
13
  dateStyle: "medium",
14
14
  timeStyle: "short"
15
15
  }).format(t);
16
16
  }
17
- var g = ({ comment: e, isReply: t, busy: n, onResolve: a, onDelete: o, onReply: c }) => {
18
- let { t: u } = d(), g = !!e.resolvedAt, _ = e.content.text ?? e.content.markdown ?? "";
19
- return /* @__PURE__ */ f("div", {
17
+ var v = ({ comment: e, isReply: t, busy: n, onResolve: r, onDelete: i, onReply: s }) => {
18
+ let { t: c } = p(), u = !!e.resolvedAt, f = e.content.text ?? e.content.markdown ?? "";
19
+ return /* @__PURE__ */ m("div", {
20
20
  className: t ? "pl-6" : "",
21
- children: /* @__PURE__ */ p("div", {
21
+ children: /* @__PURE__ */ h("div", {
22
22
  className: "rounded-md border border-[var(--color-border-default)] bg-bg-surface p-2.5",
23
23
  children: [
24
- /* @__PURE__ */ p("div", {
24
+ /* @__PURE__ */ h("div", {
25
25
  className: "flex items-center justify-between gap-2",
26
- children: [/* @__PURE__ */ f("span", {
26
+ children: [/* @__PURE__ */ m("span", {
27
27
  className: "text-xs font-medium text-text-primary",
28
- children: m(u, e.authorId)
29
- }), /* @__PURE__ */ f("span", {
28
+ children: g(c, e.authorId)
29
+ }), /* @__PURE__ */ m("span", {
30
30
  className: "text-[10px] text-text-muted tabular-nums",
31
- children: h(e.createdAt)
31
+ children: _(e.createdAt)
32
32
  })]
33
33
  }),
34
- /* @__PURE__ */ f("p", {
34
+ /* @__PURE__ */ m("p", {
35
35
  className: "mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary",
36
- children: _
36
+ children: f
37
37
  }),
38
- /* @__PURE__ */ p("div", {
38
+ /* @__PURE__ */ h("div", {
39
39
  className: "mt-1.5 flex items-center gap-2",
40
40
  children: [
41
- g && /* @__PURE__ */ p("span", {
41
+ u && /* @__PURE__ */ h("span", {
42
42
  className: "inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-status-success-bg text-status-success-text",
43
43
  children: [
44
- /* @__PURE__ */ f(r, { className: "h-3 w-3" }),
44
+ /* @__PURE__ */ m(a, { className: "h-3 w-3" }),
45
45
  " ",
46
- u("bigconsole.widget.comments.resolvedBadge", "Resolved")
46
+ c("bigconsole.widget.comments.resolvedBadge", "Resolved")
47
47
  ]
48
48
  }),
49
- c && /* @__PURE__ */ p("button", {
49
+ s && /* @__PURE__ */ h("button", {
50
50
  type: "button",
51
51
  disabled: n,
52
- onClick: () => c(e.id),
52
+ onClick: () => s(e.id),
53
53
  className: "inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50",
54
54
  children: [
55
- /* @__PURE__ */ f(i, { className: "h-3 w-3" }),
55
+ /* @__PURE__ */ m(o, { className: "h-3 w-3" }),
56
56
  " ",
57
- u("bigconsole.widget.comments.reply", "Reply")
57
+ c("bigconsole.widget.comments.reply", "Reply")
58
58
  ]
59
59
  }),
60
- /* @__PURE__ */ p("button", {
60
+ /* @__PURE__ */ h("button", {
61
61
  type: "button",
62
62
  disabled: n,
63
- onClick: () => a(e.id, !g),
63
+ onClick: () => r(e.id, !u),
64
64
  className: "inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50",
65
- children: [f(g ? s : r, { className: "h-3 w-3" }), g ? u("bigconsole.widget.comments.reopen", "Reopen") : u("bigconsole.widget.comments.resolve", "Resolve")]
65
+ children: [m(u ? l : a, { className: "h-3 w-3" }), u ? c("bigconsole.widget.comments.reopen", "Reopen") : c("bigconsole.widget.comments.resolve", "Resolve")]
66
66
  }),
67
- /* @__PURE__ */ p("button", {
67
+ /* @__PURE__ */ h("button", {
68
68
  type: "button",
69
69
  disabled: n,
70
- onClick: () => o(e.id),
70
+ onClick: () => i(e.id),
71
71
  className: "inline-flex items-center gap-1 text-[11px] text-status-error-text hover:opacity-80 disabled:opacity-50",
72
72
  children: [
73
- /* @__PURE__ */ f(l, { className: "h-3 w-3" }),
73
+ /* @__PURE__ */ m(d, { className: "h-3 w-3" }),
74
74
  " ",
75
- u("bigconsole.widget.comments.delete", "Delete")
75
+ c("bigconsole.widget.comments.delete", "Delete")
76
76
  ]
77
77
  })
78
78
  ]
@@ -80,138 +80,148 @@ var g = ({ comment: e, isReply: t, busy: n, onResolve: a, onDelete: o, onReply:
80
80
  ]
81
81
  })
82
82
  });
83
- }, _ = ({ placeholder: e, busy: t, autoFocus: r, onSubmit: i, onCancel: o }) => {
84
- let { t: s } = d(), [l, m] = n(""), h = async () => {
85
- let e = l.trim();
86
- e && await i(e) !== !1 && m("");
83
+ }, y = ({ placeholder: e, busy: t, autoFocus: n, onSubmit: r, onCancel: a }) => {
84
+ let { t: o } = p(), [c, l] = i(""), d = async () => {
85
+ let e = c.trim();
86
+ e && await r(e) !== !1 && l("");
87
87
  };
88
- return /* @__PURE__ */ p("div", {
88
+ return /* @__PURE__ */ h("div", {
89
89
  className: "flex items-end gap-2",
90
- children: [/* @__PURE__ */ f("textarea", {
91
- autoFocus: r,
92
- value: l,
93
- onChange: (e) => m(e.target.value),
90
+ children: [/* @__PURE__ */ m("textarea", {
91
+ autoFocus: n,
92
+ value: c,
93
+ onChange: (e) => l(e.target.value),
94
94
  onKeyDown: (e) => {
95
- e.stopPropagation(), (e.metaKey || e.ctrlKey) && e.key === "Enter" && (e.preventDefault(), h());
95
+ e.stopPropagation(), (e.metaKey || e.ctrlKey) && e.key === "Enter" && (e.preventDefault(), d());
96
96
  },
97
97
  rows: 2,
98
98
  placeholder: e,
99
99
  className: "flex-1 resize-none rounded-md border border-[var(--color-border-default)] bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-action-primary-bg"
100
- }), /* @__PURE__ */ p("div", {
100
+ }), /* @__PURE__ */ h("div", {
101
101
  className: "flex flex-col gap-1",
102
- children: [/* @__PURE__ */ f("button", {
102
+ children: [/* @__PURE__ */ m("button", {
103
103
  type: "button",
104
- disabled: t || !l.trim(),
105
- onClick: () => void h(),
106
- title: s("bigconsole.widget.comments.postTitle", "Post (⌘/Ctrl + Enter)"),
104
+ disabled: t || !c.trim(),
105
+ onClick: () => void d(),
106
+ title: o("bigconsole.widget.comments.postTitle", "Post (⌘/Ctrl + Enter)"),
107
107
  className: "inline-flex items-center justify-center rounded-md bg-action-primary-bg p-1.5 text-action-primary-text hover:opacity-90 disabled:opacity-50",
108
- children: t ? /* @__PURE__ */ f(a, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ f(c, { className: "h-4 w-4" })
109
- }), o && /* @__PURE__ */ f("button", {
108
+ children: t ? /* @__PURE__ */ m(s, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ m(u, { className: "h-4 w-4" })
109
+ }), a && /* @__PURE__ */ m("button", {
110
110
  type: "button",
111
- onClick: o,
111
+ onClick: a,
112
112
  className: "inline-flex items-center justify-center rounded-md border border-[var(--color-border-default)] p-1.5 text-text-muted hover:text-text-primary",
113
- title: s("bigconsole.widget.comments.cancelReplyTitle", "Cancel reply"),
114
- children: /* @__PURE__ */ f(u, { className: "h-4 w-4" })
113
+ title: o("bigconsole.widget.comments.cancelReplyTitle", "Cancel reply"),
114
+ children: /* @__PURE__ */ m(f, { className: "h-4 w-4" })
115
115
  })]
116
116
  })]
117
117
  });
118
- }, v = ({ widgetId: r, onClose: i }) => {
119
- let { t: s } = d(), { threads: c, count: l, loading: m, error: h, mutating: v, addComment: y, resolveComment: b, deleteComment: x } = e(r), [S, C] = n(null), w = t(async (e, t) => {
120
- let n = await y(e, t);
121
- return n && t && C(null), n;
122
- }, [y]);
123
- return /* @__PURE__ */ p("div", {
118
+ }, b = ({ widgetId: a, onClose: o }) => {
119
+ let { t: l } = p(), { threads: u, count: d, loading: g, error: _, mutating: b, addComment: x, resolveComment: S, deleteComment: C } = e(a), [w, T] = i(null), [E, D] = i(!1), O = r(null);
120
+ n(() => {
121
+ if (!E) return;
122
+ let e = O.current;
123
+ e?.scrollTo({
124
+ top: e.scrollHeight,
125
+ behavior: "smooth"
126
+ }), D(!1);
127
+ }, [E, u]);
128
+ let k = t(async (e, t) => {
129
+ let n = await x(e, t);
130
+ return n && (t ? T(null) : D(!0)), n;
131
+ }, [x]);
132
+ return /* @__PURE__ */ h("div", {
124
133
  className: "absolute inset-0 z-40 flex flex-col bg-bg-surface/98 backdrop-blur-sm",
125
134
  role: "dialog",
126
- "aria-label": s("bigconsole.widget.comments.dialogAriaLabel", "Widget comments"),
135
+ "aria-label": l("bigconsole.widget.comments.dialogAriaLabel", "Widget comments"),
127
136
  tabIndex: -1,
128
137
  onClick: (e) => e.stopPropagation(),
129
138
  onKeyDown: (e) => {
130
- e.key === "Escape" && (e.stopPropagation(), i());
139
+ e.key === "Escape" && (e.stopPropagation(), o());
131
140
  },
132
141
  children: [
133
- /* @__PURE__ */ p("div", {
142
+ /* @__PURE__ */ h("div", {
134
143
  className: "flex min-w-0 items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] bg-bg-surface px-3 py-2 sm:px-4",
135
- children: [/* @__PURE__ */ p("h3", {
144
+ children: [/* @__PURE__ */ h("h3", {
136
145
  className: "flex min-w-0 items-center gap-2 text-sm font-semibold leading-tight tracking-tight text-text-primary",
137
146
  children: [
138
- /* @__PURE__ */ f(o, {
147
+ /* @__PURE__ */ m(c, {
139
148
  "aria-hidden": "true",
140
149
  className: "h-icon-sm w-icon-sm shrink-0 text-text-secondary"
141
150
  }),
142
- s("bigconsole.widget.comments.title", "Comments"),
143
- /* @__PURE__ */ f("span", {
151
+ l("bigconsole.widget.comments.title", "Comments"),
152
+ /* @__PURE__ */ m("span", {
144
153
  className: "rounded-full bg-bg-sunken px-1.5 py-0.5 text-[10px] text-text-secondary tabular-nums",
145
- children: l
154
+ children: d
146
155
  })
147
156
  ]
148
- }), /* @__PURE__ */ f("button", {
157
+ }), /* @__PURE__ */ m("button", {
149
158
  type: "button",
150
- onClick: i,
151
- "aria-label": s("bigconsole.widget.comments.closeAriaLabel", "Close comments"),
159
+ onClick: o,
160
+ "aria-label": l("bigconsole.widget.comments.closeAriaLabel", "Close comments"),
152
161
  className: "shrink-0 rounded-button p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface",
153
- children: /* @__PURE__ */ f(u, { className: "h-4 w-4" })
162
+ children: /* @__PURE__ */ m(f, { className: "h-4 w-4" })
154
163
  })]
155
164
  }),
156
- /* @__PURE__ */ p("div", {
165
+ /* @__PURE__ */ h("div", {
166
+ ref: O,
157
167
  className: "flex-1 space-y-3 overflow-auto p-3",
158
168
  children: [
159
- m && c.length === 0 && /* @__PURE__ */ f("div", {
169
+ g && u.length === 0 && /* @__PURE__ */ m("div", {
160
170
  className: "flex items-center justify-center py-8 text-text-muted",
161
- children: /* @__PURE__ */ f(a, { className: "h-5 w-5 animate-spin" })
171
+ children: /* @__PURE__ */ m(s, { className: "h-5 w-5 animate-spin" })
162
172
  }),
163
- h && !m && /* @__PURE__ */ f("p", {
173
+ _ && !g && /* @__PURE__ */ m("p", {
164
174
  role: "alert",
165
175
  className: "rounded-input border border-[var(--color-status-error-border)] bg-status-error-bg-subtle px-2.5 py-2 text-xs text-[var(--color-status-error-text)]",
166
- children: s("bigconsole.widget.comments.loadError", "Couldn’t load comments.")
176
+ children: l("bigconsole.widget.comments.loadError", "Couldn’t load comments.")
167
177
  }),
168
- !m && !h && c.length === 0 && /* @__PURE__ */ f("p", {
178
+ !g && !_ && u.length === 0 && /* @__PURE__ */ m("p", {
169
179
  className: "py-8 text-center text-sm text-text-muted",
170
- children: s("bigconsole.widget.comments.empty", "No comments yet. Start the conversation.")
180
+ children: l("bigconsole.widget.comments.empty", "No comments yet. Start the conversation.")
171
181
  }),
172
- c.map(({ comment: e, replies: t }) => /* @__PURE__ */ p("div", {
182
+ u.map(({ comment: e, replies: t }) => /* @__PURE__ */ h("div", {
173
183
  className: "space-y-2",
174
184
  children: [
175
- /* @__PURE__ */ f(g, {
185
+ /* @__PURE__ */ m(v, {
176
186
  comment: e,
177
- busy: v,
178
- onResolve: (e, t) => void b(e, t),
179
- onDelete: (e) => void x(e),
180
- onReply: (e) => C((t) => t === e ? null : e)
187
+ busy: b,
188
+ onResolve: (e, t) => void S(e, t),
189
+ onDelete: (e) => void C(e),
190
+ onReply: (e) => T((t) => t === e ? null : e)
181
191
  }),
182
- t.map((e) => /* @__PURE__ */ f(g, {
192
+ t.map((e) => /* @__PURE__ */ m(v, {
183
193
  comment: e,
184
194
  isReply: !0,
185
- busy: v,
186
- onResolve: (e, t) => void b(e, t),
187
- onDelete: (e) => void x(e)
195
+ busy: b,
196
+ onResolve: (e, t) => void S(e, t),
197
+ onDelete: (e) => void C(e)
188
198
  }, e.id)),
189
- S === e.id && /* @__PURE__ */ f("div", {
199
+ w === e.id && /* @__PURE__ */ m("div", {
190
200
  className: "pl-6",
191
- children: /* @__PURE__ */ f(_, {
192
- placeholder: s("bigconsole.widget.comments.replyPlaceholder", "Write a reply…"),
193
- busy: v,
201
+ children: /* @__PURE__ */ m(y, {
202
+ placeholder: l("bigconsole.widget.comments.replyPlaceholder", "Write a reply…"),
203
+ busy: b,
194
204
  autoFocus: !0,
195
- onSubmit: (t) => w(t, e.id),
196
- onCancel: () => C(null)
205
+ onSubmit: (t) => k(t, e.id),
206
+ onCancel: () => T(null)
197
207
  })
198
208
  })
199
209
  ]
200
210
  }, e.id))
201
211
  ]
202
212
  }),
203
- /* @__PURE__ */ f("div", {
213
+ /* @__PURE__ */ m("div", {
204
214
  className: "border-t border-[var(--color-border-default)] p-3",
205
- children: /* @__PURE__ */ f(_, {
206
- placeholder: s("bigconsole.widget.comments.addPlaceholder", "Add a comment…"),
207
- busy: v,
208
- onSubmit: (e) => w(e)
215
+ children: /* @__PURE__ */ m(y, {
216
+ placeholder: l("bigconsole.widget.comments.addPlaceholder", "Add a comment…"),
217
+ busy: b,
218
+ onSubmit: (e) => k(e)
209
219
  })
210
220
  })
211
221
  ]
212
222
  });
213
223
  };
214
224
  //#endregion
215
- export { v as WidgetComments };
225
+ export { b as WidgetComments };
216
226
 
217
227
  //# sourceMappingURL=WidgetComments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"WidgetComments.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/comments/WidgetComments.tsx"],"sourcesContent":["/**\n * WidgetComments\n *\n * Collaborative comment thread for a single widget, rendered as a slide-over\n * panel inside the widget container. Supports posting top-level comments and\n * threaded replies, resolving/reopening, and (soft) deleting. Data flows through\n * useWidgetComments → the federated EntityComment API.\n */\n\nimport { type FC, useState, useCallback } from 'react';\nimport { MessageSquare, Send, Check, RotateCcw, Trash2, X, CornerDownRight, Loader2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWidgetComments, type WidgetCommentThread } from '../../../hooks/useWidgetComments';\nimport type { EntityCommentFieldsFragment } from '../../../../generated/wspace-operations';\n\nexport interface WidgetCommentsProps {\n widgetId: string;\n /** Close the panel. */\n onClose: () => void;\n}\n\nfunction authorLabel(\n t: (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n ) => string,\n authorId: string\n): string {\n // We only have an opaque actor id here; show a short, stable handle.\n return t('bigconsole.widget.comments.authorLabel', 'User {{id}}', { id: authorId.slice(0, 6) });\n}\n\nfunction formatWhen(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '';\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(d);\n}\n\ninterface CommentItemProps {\n comment: EntityCommentFieldsFragment;\n isReply?: boolean;\n busy: boolean;\n onResolve: (id: string, resolved: boolean) => void;\n onDelete: (id: string) => void;\n onReply?: (id: string) => void;\n}\n\nconst CommentItem: FC<CommentItemProps> = ({ comment, isReply, busy, onResolve, onDelete, onReply }) => {\n const { t } = useI18n();\n const resolved = !!comment.resolvedAt;\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <div className={isReply ? 'pl-6' : ''}>\n <div className=\"rounded-md border border-[var(--color-border-default)] bg-bg-surface p-2.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium text-text-primary\">{authorLabel(t, comment.authorId)}</span>\n <span className=\"text-[10px] text-text-muted tabular-nums\">{formatWhen(comment.createdAt)}</span>\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">{body}</p>\n <div className=\"mt-1.5 flex items-center gap-2\">\n {resolved && (\n <span className=\"inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-status-success-bg text-status-success-text\">\n <Check className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.resolvedBadge', 'Resolved')}\n </span>\n )}\n {onReply && (\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onReply(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n <CornerDownRight className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.reply', 'Reply')}\n </button>\n )}\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onResolve(comment.id, !resolved)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n {resolved ? <RotateCcw className=\"h-3 w-3\" /> : <Check className=\"h-3 w-3\" />}\n {resolved\n ? t('bigconsole.widget.comments.reopen', 'Reopen')\n : t('bigconsole.widget.comments.resolve', 'Resolve')}\n </button>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onDelete(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-status-error-text hover:opacity-80 disabled:opacity-50\"\n >\n <Trash2 className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.delete', 'Delete')}\n </button>\n </div>\n </div>\n </div>\n );\n};\n\ninterface ComposerProps {\n placeholder: string;\n busy: boolean;\n autoFocus?: boolean;\n /** Returns false when the post failed — the draft is kept so it isn't lost. */\n onSubmit: (text: string) => boolean | void | Promise<boolean | void>;\n onCancel?: () => void;\n}\n\nconst Composer: FC<ComposerProps> = ({ placeholder, busy, autoFocus, onSubmit, onCancel }) => {\n const { t } = useI18n();\n const [text, setText] = useState('');\n const submit = async () => {\n const v = text.trim();\n if (!v) return;\n // Only clear once the post succeeds; on failure the user keeps their draft.\n const ok = await onSubmit(v);\n if (ok !== false) setText('');\n };\n return (\n <div className=\"flex items-end gap-2\">\n <textarea\n autoFocus={autoFocus}\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n // The composer lives inside an edit-mode widget, whose keyboard\n // handler consumes Space to select the widget. Keep text editing\n // events within the textarea so spaces and other input are intact.\n e.stopPropagation();\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault();\n void submit();\n }\n }}\n rows={2}\n placeholder={placeholder}\n className=\"flex-1 resize-none rounded-md border border-[var(--color-border-default)] bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n />\n <div className=\"flex flex-col gap-1\">\n <button\n type=\"button\"\n disabled={busy || !text.trim()}\n onClick={() => void submit()}\n title={t('bigconsole.widget.comments.postTitle', 'Post (⌘/Ctrl + Enter)')}\n className=\"inline-flex items-center justify-center rounded-md bg-action-primary-bg p-1.5 text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Send className=\"h-4 w-4\" />}\n </button>\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"inline-flex items-center justify-center rounded-md border border-[var(--color-border-default)] p-1.5 text-text-muted hover:text-text-primary\"\n title={t('bigconsole.widget.comments.cancelReplyTitle', 'Cancel reply')}\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n </div>\n );\n};\n\nexport const WidgetComments: FC<WidgetCommentsProps> = ({ widgetId, onClose }) => {\n const { t } = useI18n();\n const { threads, count, loading, error, mutating, addComment, resolveComment, deleteComment } =\n useWidgetComments(widgetId);\n const [replyTo, setReplyTo] = useState<string | null>(null);\n\n const handleAdd = useCallback(\n async (text: string, parentId?: string): Promise<boolean> => {\n const ok = await addComment(text, parentId);\n if (ok && parentId) setReplyTo(null);\n return ok;\n },\n [addComment]\n );\n\n return (\n <div\n className=\"absolute inset-0 z-40 flex flex-col bg-bg-surface/98 backdrop-blur-sm\"\n role=\"dialog\"\n aria-label={t('bigconsole.widget.comments.dialogAriaLabel', 'Widget comments')}\n tabIndex={-1}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n onClose();\n }\n }}\n >\n {/*\n Header. Matches the widget card's own title band exactly — same padding,\n same seam, same semibold title — so opening comments does not change the\n card's header grammar mid-flight (BOFF-5636).\n */}\n <div className=\"flex min-w-0 items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] bg-bg-surface px-3 py-2 sm:px-4\">\n <h3 className=\"flex min-w-0 items-center gap-2 text-sm font-semibold leading-tight tracking-tight text-text-primary\">\n <MessageSquare aria-hidden=\"true\" className=\"h-icon-sm w-icon-sm shrink-0 text-text-secondary\" />\n {t('bigconsole.widget.comments.title', 'Comments')}\n <span className=\"rounded-full bg-bg-sunken px-1.5 py-0.5 text-[10px] text-text-secondary tabular-nums\">\n {count}\n </span>\n </h3>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={t('bigconsole.widget.comments.closeAriaLabel', 'Close comments')}\n className=\"shrink-0 rounded-button p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Thread list */}\n <div className=\"flex-1 space-y-3 overflow-auto p-3\">\n {loading && threads.length === 0 && (\n <div className=\"flex items-center justify-center py-8 text-text-muted\">\n <Loader2 className=\"h-5 w-5 animate-spin\" />\n </div>\n )}\n {error && !loading && (\n <p\n role=\"alert\"\n className=\"rounded-input border border-[var(--color-status-error-border)] bg-status-error-bg-subtle px-2.5 py-2 text-xs text-[var(--color-status-error-text)]\"\n >\n {t('bigconsole.widget.comments.loadError', 'Couldn’t load comments.')}\n </p>\n )}\n {!loading && !error && threads.length === 0 && (\n <p className=\"py-8 text-center text-sm text-text-muted\">\n {t('bigconsole.widget.comments.empty', 'No comments yet. Start the conversation.')}\n </p>\n )}\n {threads.map(({ comment, replies }: WidgetCommentThread) => (\n <div key={comment.id} className=\"space-y-2\">\n <CommentItem\n comment={comment}\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n onReply={(id) => setReplyTo((cur) => (cur === id ? null : id))}\n />\n {replies.map((reply) => (\n <CommentItem\n key={reply.id}\n comment={reply}\n isReply\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n />\n ))}\n {replyTo === comment.id && (\n <div className=\"pl-6\">\n <Composer\n placeholder={t('bigconsole.widget.comments.replyPlaceholder', 'Write a reply…')}\n busy={mutating}\n autoFocus\n onSubmit={(text) => handleAdd(text, comment.id)}\n onCancel={() => setReplyTo(null)}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n\n {/* New top-level comment */}\n <div className=\"border-t border-[var(--color-border-default)] p-3\">\n <Composer\n placeholder={t('bigconsole.widget.comments.addPlaceholder', 'Add a comment…')}\n busy={mutating}\n onSubmit={(text) => handleAdd(text)}\n />\n </div>\n </div>\n );\n};\n\nexport default WidgetComments;\n"],"mappings":"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"}
1
+ {"version":3,"file":"WidgetComments.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/comments/WidgetComments.tsx"],"sourcesContent":["/**\n * WidgetComments\n *\n * Collaborative comment thread for a single widget, rendered as a slide-over\n * panel inside the widget container. Supports posting top-level comments and\n * threaded replies, resolving/reopening, and (soft) deleting. Data flows through\n * useWidgetComments → the federated EntityComment API.\n */\n\nimport { type FC, useState, useCallback, useEffect, useRef } from 'react';\nimport { MessageSquare, Send, Check, RotateCcw, Trash2, X, CornerDownRight, Loader2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWidgetComments, type WidgetCommentThread } from '../../../hooks/useWidgetComments';\nimport type { EntityCommentFieldsFragment } from '../../../../generated/wspace-operations';\n\nexport interface WidgetCommentsProps {\n widgetId: string;\n /** Close the panel. */\n onClose: () => void;\n}\n\nfunction authorLabel(\n t: (\n key: string,\n fallbackOrParams?: string | Record<string, string | number>,\n params?: Record<string, string | number>\n ) => string,\n authorId: string\n): string {\n // We only have an opaque actor id here; show a short, stable handle.\n return t('bigconsole.widget.comments.authorLabel', 'User {{id}}', { id: authorId.slice(0, 6) });\n}\n\nfunction formatWhen(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return '';\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(d);\n}\n\ninterface CommentItemProps {\n comment: EntityCommentFieldsFragment;\n isReply?: boolean;\n busy: boolean;\n onResolve: (id: string, resolved: boolean) => void;\n onDelete: (id: string) => void;\n onReply?: (id: string) => void;\n}\n\nconst CommentItem: FC<CommentItemProps> = ({ comment, isReply, busy, onResolve, onDelete, onReply }) => {\n const { t } = useI18n();\n const resolved = !!comment.resolvedAt;\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <div className={isReply ? 'pl-6' : ''}>\n <div className=\"rounded-md border border-[var(--color-border-default)] bg-bg-surface p-2.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium text-text-primary\">{authorLabel(t, comment.authorId)}</span>\n <span className=\"text-[10px] text-text-muted tabular-nums\">{formatWhen(comment.createdAt)}</span>\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">{body}</p>\n <div className=\"mt-1.5 flex items-center gap-2\">\n {resolved && (\n <span className=\"inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-status-success-bg text-status-success-text\">\n <Check className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.resolvedBadge', 'Resolved')}\n </span>\n )}\n {onReply && (\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onReply(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n <CornerDownRight className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.reply', 'Reply')}\n </button>\n )}\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onResolve(comment.id, !resolved)}\n className=\"inline-flex items-center gap-1 text-[11px] text-text-muted hover:text-text-primary disabled:opacity-50\"\n >\n {resolved ? <RotateCcw className=\"h-3 w-3\" /> : <Check className=\"h-3 w-3\" />}\n {resolved\n ? t('bigconsole.widget.comments.reopen', 'Reopen')\n : t('bigconsole.widget.comments.resolve', 'Resolve')}\n </button>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => onDelete(comment.id)}\n className=\"inline-flex items-center gap-1 text-[11px] text-status-error-text hover:opacity-80 disabled:opacity-50\"\n >\n <Trash2 className=\"h-3 w-3\" /> {t('bigconsole.widget.comments.delete', 'Delete')}\n </button>\n </div>\n </div>\n </div>\n );\n};\n\ninterface ComposerProps {\n placeholder: string;\n busy: boolean;\n autoFocus?: boolean;\n /** Returns false when the post failed — the draft is kept so it isn't lost. */\n onSubmit: (text: string) => boolean | void | Promise<boolean | void>;\n onCancel?: () => void;\n}\n\nconst Composer: FC<ComposerProps> = ({ placeholder, busy, autoFocus, onSubmit, onCancel }) => {\n const { t } = useI18n();\n const [text, setText] = useState('');\n const submit = async () => {\n const v = text.trim();\n if (!v) return;\n // Only clear once the post succeeds; on failure the user keeps their draft.\n const ok = await onSubmit(v);\n if (ok !== false) setText('');\n };\n return (\n <div className=\"flex items-end gap-2\">\n <textarea\n autoFocus={autoFocus}\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n // The composer lives inside an edit-mode widget, whose keyboard\n // handler consumes Space to select the widget. Keep text editing\n // events within the textarea so spaces and other input are intact.\n e.stopPropagation();\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault();\n void submit();\n }\n }}\n rows={2}\n placeholder={placeholder}\n className=\"flex-1 resize-none rounded-md border border-[var(--color-border-default)] bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n />\n <div className=\"flex flex-col gap-1\">\n <button\n type=\"button\"\n disabled={busy || !text.trim()}\n onClick={() => void submit()}\n title={t('bigconsole.widget.comments.postTitle', 'Post (⌘/Ctrl + Enter)')}\n className=\"inline-flex items-center justify-center rounded-md bg-action-primary-bg p-1.5 text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Send className=\"h-4 w-4\" />}\n </button>\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"inline-flex items-center justify-center rounded-md border border-[var(--color-border-default)] p-1.5 text-text-muted hover:text-text-primary\"\n title={t('bigconsole.widget.comments.cancelReplyTitle', 'Cancel reply')}\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n </div>\n );\n};\n\nexport const WidgetComments: FC<WidgetCommentsProps> = ({ widgetId, onClose }) => {\n const { t } = useI18n();\n const { threads, count, loading, error, mutating, addComment, resolveComment, deleteComment } =\n useWidgetComments(widgetId);\n const [replyTo, setReplyTo] = useState<string | null>(null);\n const [scrollToNewest, setScrollToNewest] = useState(false);\n const threadListRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!scrollToNewest) return;\n\n const threadList = threadListRef.current;\n threadList?.scrollTo({ top: threadList.scrollHeight, behavior: 'smooth' });\n setScrollToNewest(false);\n }, [scrollToNewest, threads]);\n\n const handleAdd = useCallback(\n async (text: string, parentId?: string): Promise<boolean> => {\n const ok = await addComment(text, parentId);\n if (ok) {\n if (parentId) setReplyTo(null);\n else setScrollToNewest(true);\n }\n return ok;\n },\n [addComment]\n );\n\n return (\n <div\n className=\"absolute inset-0 z-40 flex flex-col bg-bg-surface/98 backdrop-blur-sm\"\n role=\"dialog\"\n aria-label={t('bigconsole.widget.comments.dialogAriaLabel', 'Widget comments')}\n tabIndex={-1}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n onClose();\n }\n }}\n >\n {/*\n Header. Matches the widget card's own title band exactly — same padding,\n same seam, same semibold title — so opening comments does not change the\n card's header grammar mid-flight (BOFF-5636).\n */}\n <div className=\"flex min-w-0 items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] bg-bg-surface px-3 py-2 sm:px-4\">\n <h3 className=\"flex min-w-0 items-center gap-2 text-sm font-semibold leading-tight tracking-tight text-text-primary\">\n <MessageSquare aria-hidden=\"true\" className=\"h-icon-sm w-icon-sm shrink-0 text-text-secondary\" />\n {t('bigconsole.widget.comments.title', 'Comments')}\n <span className=\"rounded-full bg-bg-sunken px-1.5 py-0.5 text-[10px] text-text-secondary tabular-nums\">\n {count}\n </span>\n </h3>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label={t('bigconsole.widget.comments.closeAriaLabel', 'Close comments')}\n className=\"shrink-0 rounded-button p-1 text-text-muted hover:bg-bg-sunken hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Thread list */}\n <div ref={threadListRef} className=\"flex-1 space-y-3 overflow-auto p-3\">\n {loading && threads.length === 0 && (\n <div className=\"flex items-center justify-center py-8 text-text-muted\">\n <Loader2 className=\"h-5 w-5 animate-spin\" />\n </div>\n )}\n {error && !loading && (\n <p\n role=\"alert\"\n className=\"rounded-input border border-[var(--color-status-error-border)] bg-status-error-bg-subtle px-2.5 py-2 text-xs text-[var(--color-status-error-text)]\"\n >\n {t('bigconsole.widget.comments.loadError', 'Couldn’t load comments.')}\n </p>\n )}\n {!loading && !error && threads.length === 0 && (\n <p className=\"py-8 text-center text-sm text-text-muted\">\n {t('bigconsole.widget.comments.empty', 'No comments yet. Start the conversation.')}\n </p>\n )}\n {threads.map(({ comment, replies }: WidgetCommentThread) => (\n <div key={comment.id} className=\"space-y-2\">\n <CommentItem\n comment={comment}\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n onReply={(id) => setReplyTo((cur) => (cur === id ? null : id))}\n />\n {replies.map((reply) => (\n <CommentItem\n key={reply.id}\n comment={reply}\n isReply\n busy={mutating}\n onResolve={(id, r) => void resolveComment(id, r)}\n onDelete={(id) => void deleteComment(id)}\n />\n ))}\n {replyTo === comment.id && (\n <div className=\"pl-6\">\n <Composer\n placeholder={t('bigconsole.widget.comments.replyPlaceholder', 'Write a reply…')}\n busy={mutating}\n autoFocus\n onSubmit={(text) => handleAdd(text, comment.id)}\n onCancel={() => setReplyTo(null)}\n />\n </div>\n )}\n </div>\n ))}\n </div>\n\n {/* New top-level comment */}\n <div className=\"border-t border-[var(--color-border-default)] p-3\">\n <Composer\n placeholder={t('bigconsole.widget.comments.addPlaceholder', 'Add a comment…')}\n busy={mutating}\n onSubmit={(text) => handleAdd(text)}\n />\n </div>\n </div>\n );\n};\n\nexport default WidgetComments;\n"],"mappings":"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"}
@@ -1,22 +1,31 @@
1
1
  import { useCreateEntityCommentMutation as e, useDeleteEntityCommentMutation as t, useGetEntityCommentsQuery as n, useResolveEntityCommentMutation as r } from "../../generated/wspace-operations.js";
2
- import { useCallback as i, useMemo as a } from "react";
2
+ import { useCallback as i, useMemo as a, useState as o } from "react";
3
3
  //#region src/bigconsole/hooks/useWidgetComments.ts
4
- var o = "bigconsole-widget";
5
- function s(s, c = !0) {
6
- let { data: l, loading: u, error: d, refetch: f } = n({
4
+ var s = "bigconsole-widget";
5
+ function c(c, l = !0) {
6
+ let { data: u, loading: d, error: f, refetch: p } = n({
7
7
  variables: { input: {
8
- entityId: s ?? "",
9
- entityType: o,
8
+ entityId: c ?? "",
9
+ entityType: s,
10
10
  take: 100,
11
11
  sortBy: "createdAt",
12
12
  sortOrder: "asc"
13
13
  } },
14
- skip: !c || !s,
14
+ skip: !l || !c,
15
15
  fetchPolicy: "cache-and-network",
16
16
  notifyOnNetworkStatusChange: !0
17
- }), [p, m] = e(), [h, g] = r(), [_, v] = t(), y = a(() => l?.getEntityComments.items ?? [], [l]), b = l?.getEntityComments.total ?? y.length, x = a(() => {
18
- let e = y.filter((e) => !e.parentCommentId), t = /* @__PURE__ */ new Map();
19
- for (let e of y) if (e.parentCommentId) {
17
+ }), [m, h] = e(), [g, _] = r(), [v, y] = t(), [b, x] = o([]), S = a(() => {
18
+ let e = /* @__PURE__ */ new Map();
19
+ for (let t of u?.getEntityComments.items ?? []) e.set(t.id, t);
20
+ for (let t of b) t.entityId === c && e.set(t.id, t);
21
+ return [...e.values()].sort((e, t) => e.createdAt.localeCompare(t.createdAt));
22
+ }, [
23
+ b,
24
+ u,
25
+ c
26
+ ]), C = u?.getEntityComments.total ?? S.length, w = a(() => {
27
+ let e = S.filter((e) => !e.parentCommentId), t = /* @__PURE__ */ new Map();
28
+ for (let e of S) if (e.parentCommentId) {
20
29
  let n = t.get(e.parentCommentId) ?? [];
21
30
  n.push(e), t.set(e.parentCommentId, n);
22
31
  }
@@ -24,58 +33,60 @@ function s(s, c = !0) {
24
33
  comment: e,
25
34
  replies: t.get(e.id) ?? []
26
35
  }));
27
- }, [y]), S = i(async (e, t) => {
36
+ }, [S]), T = i(async (e, t) => {
28
37
  let n = e.trim();
29
- if (!n || !s) return !1;
38
+ if (!n || !c) return !1;
30
39
  try {
31
- return (await p({ variables: { input: {
32
- entityId: s,
33
- entityType: "bigconsole-widget",
40
+ let e = (await m({ variables: { input: {
41
+ entityId: c,
42
+ entityType: s,
34
43
  content: { text: n },
35
44
  contentType: "TEXT",
36
45
  ...t ? { parentCommentId: t } : {}
37
- } } })).data?.createEntityComment ? (await f(), !0) : !1;
46
+ } } })).data?.createEntityComment;
47
+ return e ? (x((t) => t.some((t) => t.id === e.id) ? t : [...t, e]), await p(), !0) : !1;
38
48
  } catch {
39
49
  return !1;
40
50
  }
41
51
  }, [
52
+ m,
42
53
  p,
43
- f,
44
- s
45
- ]), C = i(async (e, t) => {
54
+ c
55
+ ]), E = i(async (e, t) => {
46
56
  try {
47
- return (await h({ variables: { input: {
57
+ let n = (await g({ variables: { input: {
48
58
  id: e,
49
59
  resolved: t
50
- } } })).data?.resolveEntityComment ? (await f(), !0) : !1;
60
+ } } })).data?.resolveEntityComment;
61
+ return n ? (x((e) => e.map((e) => e.id === n.id ? n : e)), await p(), !0) : !1;
51
62
  } catch {
52
63
  return !1;
53
64
  }
54
- }, [h, f]), w = i(async (e) => {
65
+ }, [g, p]), D = i(async (e) => {
55
66
  try {
56
- return (await _({ variables: { input: {
67
+ return (await v({ variables: { input: {
57
68
  id: e,
58
69
  soft: !0
59
- } } })).data?.deleteEntityComment ? (await f(), !0) : !1;
70
+ } } })).data?.deleteEntityComment ? (x((t) => t.filter((t) => t.id !== e)), await p(), !0) : !1;
60
71
  } catch {
61
72
  return !1;
62
73
  }
63
- }, [_, f]);
74
+ }, [v, p]);
64
75
  return {
65
- threads: x,
66
- count: b,
67
- loading: u,
68
- error: d,
69
- mutating: m.loading || g.loading || v.loading,
70
- addComment: S,
71
- resolveComment: C,
72
- deleteComment: w,
76
+ threads: w,
77
+ count: C,
78
+ loading: d,
79
+ error: f,
80
+ mutating: h.loading || _.loading || y.loading,
81
+ addComment: T,
82
+ resolveComment: E,
83
+ deleteComment: D,
73
84
  refetch: i(() => {
74
- f();
75
- }, [f])
85
+ p();
86
+ }, [p])
76
87
  };
77
88
  }
78
89
  //#endregion
79
- export { o as WIDGET_COMMENT_ENTITY_TYPE, s as default };
90
+ export { s as WIDGET_COMMENT_ENTITY_TYPE, c as default };
80
91
 
81
92
  //# sourceMappingURL=useWidgetComments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useWidgetComments.js","names":[],"sources":["../../../src/bigconsole/hooks/useWidgetComments.ts"],"sourcesContent":["/**\n * useWidgetComments Hook\n *\n * Comment thread for a single widget, backed by the federated polymorphic\n * EntityComment API (module-conversations-svc) with `entityType` =\n * `bigconsole-widget`. Returns the flat list plus a parent/child threaded\n * view, and create / resolve / delete actions that refetch on success.\n *\n * The query fragment is intentionally slim (no nested replies/reactions/files)\n * to stay under the gateway cost-limit; threads are assembled here from\n * `parentCommentId`.\n */\n\nimport { useCallback, useMemo } from 'react';\nimport {\n useGetEntityCommentsQuery,\n useCreateEntityCommentMutation,\n useResolveEntityCommentMutation,\n useDeleteEntityCommentMutation,\n} from '../../generated/wspace-operations';\nimport type { EntityCommentFieldsFragment } from '../../generated/wspace-operations';\n\n/** Entity type used to scope comments to a BigConsole widget. */\nexport const WIDGET_COMMENT_ENTITY_TYPE = 'bigconsole-widget';\n\n/** A top-level comment with its (one level of) replies attached. */\nexport interface WidgetCommentThread {\n comment: EntityCommentFieldsFragment;\n replies: EntityCommentFieldsFragment[];\n}\n\nexport interface UseWidgetCommentsResult {\n threads: WidgetCommentThread[];\n /** Total comment count (top-level + replies), excluding deleted. */\n count: number;\n loading: boolean;\n error?: Error;\n mutating: boolean;\n addComment: (text: string, parentCommentId?: string) => Promise<boolean>;\n resolveComment: (id: string, resolved: boolean) => Promise<boolean>;\n deleteComment: (id: string) => Promise<boolean>;\n refetch: () => void;\n}\n\nexport function useWidgetComments(widgetId: string | undefined, enabled = true): UseWidgetCommentsResult {\n const skip = !enabled || !widgetId;\n\n const { data, loading, error, refetch } = useGetEntityCommentsQuery({\n variables: {\n input: {\n entityId: widgetId ?? '',\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n take: 100,\n sortBy: 'createdAt',\n sortOrder: 'asc',\n },\n },\n skip,\n fetchPolicy: 'cache-and-network',\n notifyOnNetworkStatusChange: true,\n });\n\n const [createComment, createState] = useCreateEntityCommentMutation();\n const [resolveCommentMutation, resolveState] = useResolveEntityCommentMutation();\n const [deleteCommentMutation, deleteState] = useDeleteEntityCommentMutation();\n\n const items: EntityCommentFieldsFragment[] = useMemo(() => data?.getEntityComments.items ?? [], [data]);\n // Use the server-reported total for the count (items is capped at the page size).\n const total = data?.getEntityComments.total ?? items.length;\n\n // Reassemble a flat list into top-level threads + their direct replies.\n const threads = useMemo<WidgetCommentThread[]>(() => {\n const roots = items.filter((c) => !c.parentCommentId);\n const repliesByParent = new Map<string, EntityCommentFieldsFragment[]>();\n for (const c of items) {\n if (c.parentCommentId) {\n const list = repliesByParent.get(c.parentCommentId) ?? [];\n list.push(c);\n repliesByParent.set(c.parentCommentId, list);\n }\n }\n return roots.map((comment) => ({ comment, replies: repliesByParent.get(comment.id) ?? [] }));\n }, [items]);\n\n // All actions catch their own errors and resolve to a boolean (never reject),\n // so callers can `await` for success/failure without unhandled rejections and\n // can keep the user's draft when a post fails.\n const addComment = useCallback(\n async (text: string, parentCommentId?: string): Promise<boolean> => {\n const body = text.trim();\n if (!body || !widgetId) return false;\n try {\n const res = await createComment({\n variables: {\n input: {\n entityId: widgetId,\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n content: { text: body },\n contentType: 'TEXT',\n ...(parentCommentId ? { parentCommentId } : {}),\n },\n },\n });\n if (res.data?.createEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [createComment, refetch, widgetId]\n );\n\n const resolveComment = useCallback(\n async (id: string, resolved: boolean): Promise<boolean> => {\n try {\n const res = await resolveCommentMutation({ variables: { input: { id, resolved } } });\n if (res.data?.resolveEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [resolveCommentMutation, refetch]\n );\n\n const deleteComment = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const res = await deleteCommentMutation({ variables: { input: { id, soft: true } } });\n if (res.data?.deleteEntityComment) {\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [deleteCommentMutation, refetch]\n );\n\n return {\n threads,\n count: total,\n loading,\n error: error as Error | undefined,\n mutating: createState.loading || resolveState.loading || deleteState.loading,\n addComment,\n resolveComment,\n deleteComment,\n refetch: useCallback(() => {\n void refetch();\n }, [refetch]),\n };\n}\n\nexport default useWidgetComments;\n"],"mappings":";;;AAuBA,IAAa,IAA6B;AAqB1C,SAAgB,EAAkB,GAA8B,IAAU,IAA+B;CAGvG,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EAA0B;EAClE,WAAW,EACT,OAAO;GACL,UAAU,KAAY;GACtB,YAAY;GACZ,MAAM;GACN,QAAQ;GACR,WAAW;GACZ,EACF;EACD,MAZW,CAAC,KAAW,CAAC;EAaxB,aAAa;EACb,6BAA6B;EAC9B,CAAC,EAEI,CAAC,GAAe,KAAe,GAAgC,EAC/D,CAAC,GAAwB,KAAgB,GAAiC,EAC1E,CAAC,GAAuB,KAAe,GAAgC,EAEvE,IAAuC,QAAc,GAAM,kBAAkB,SAAS,EAAE,EAAE,CAAC,EAAK,CAAC,EAEjG,IAAQ,GAAM,kBAAkB,SAAS,EAAM,QAG/C,IAAU,QAAqC;EACnD,IAAM,IAAQ,EAAM,QAAQ,MAAM,CAAC,EAAE,gBAAgB,EAC/C,oBAAkB,IAAI,KAA4C;AACxE,OAAK,IAAM,KAAK,EACd,KAAI,EAAE,iBAAiB;GACrB,IAAM,IAAO,EAAgB,IAAI,EAAE,gBAAgB,IAAI,EAAE;AAEzD,GADA,EAAK,KAAK,EAAE,EACZ,EAAgB,IAAI,EAAE,iBAAiB,EAAK;;AAGhD,SAAO,EAAM,KAAK,OAAa;GAAE;GAAS,SAAS,EAAgB,IAAI,EAAQ,GAAG,IAAI,EAAE;GAAE,EAAE;IAC3F,CAAC,EAAM,CAAC,EAKL,IAAa,EACjB,OAAO,GAAc,MAA+C;EAClE,IAAM,IAAO,EAAK,MAAM;AACxB,MAAI,CAAC,KAAQ,CAAC,EAAU,QAAO;AAC/B,MAAI;AAgBF,WAfY,MAAM,EAAc,EAC9B,WAAW,EACT,OAAO;IACL,UAAU;IACV,YAAA;IACA,SAAS,EAAE,MAAM,GAAM;IACvB,aAAa;IACb,GAAI,IAAkB,EAAE,oBAAiB,GAAG,EAAE;IAC/C,EACF,EACF,CAAC,EACM,MAAM,uBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX;EAAC;EAAe;EAAS;EAAS,CACnC,EAEK,IAAiB,EACrB,OAAO,GAAY,MAAwC;AACzD,MAAI;AAMF,WALY,MAAM,EAAuB,EAAE,WAAW,EAAE,OAAO;IAAE;IAAI;IAAU,EAAE,EAAE,CAAC,EAC5E,MAAM,wBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX,CAAC,GAAwB,EAAQ,CAClC,EAEK,IAAgB,EACpB,OAAO,MAAiC;AACtC,MAAI;AAMF,WALY,MAAM,EAAsB,EAAE,WAAW,EAAE,OAAO;IAAE;IAAI,MAAM;IAAM,EAAE,EAAE,CAAC,EAC7E,MAAM,uBACZ,MAAM,GAAS,EACR,MAEF;UACD;AACN,UAAO;;IAGX,CAAC,GAAuB,EAAQ,CACjC;AAED,QAAO;EACL;EACA,OAAO;EACP;EACO;EACP,UAAU,EAAY,WAAW,EAAa,WAAW,EAAY;EACrE;EACA;EACA;EACA,SAAS,QAAkB;AACpB,MAAS;KACb,CAAC,EAAQ,CAAC;EACd"}
1
+ {"version":3,"file":"useWidgetComments.js","names":[],"sources":["../../../src/bigconsole/hooks/useWidgetComments.ts"],"sourcesContent":["/**\n * useWidgetComments Hook\n *\n * Comment thread for a single widget, backed by the federated polymorphic\n * EntityComment API (module-conversations-svc) with `entityType` =\n * `bigconsole-widget`. Returns the flat list plus a parent/child threaded\n * view, and create / resolve / delete actions that refetch on success. Created\n * comments are retained locally after the refetch so the newest comment stays\n * visible even when the server page is full.\n *\n * The query fragment is intentionally slim (no nested replies/reactions/files)\n * to stay under the gateway cost-limit; threads are assembled here from\n * `parentCommentId`.\n */\n\nimport { useCallback, useMemo, useState } from 'react';\nimport {\n useGetEntityCommentsQuery,\n useCreateEntityCommentMutation,\n useResolveEntityCommentMutation,\n useDeleteEntityCommentMutation,\n} from '../../generated/wspace-operations';\nimport type { EntityCommentFieldsFragment } from '../../generated/wspace-operations';\n\n/** Entity type used to scope comments to a BigConsole widget. */\nexport const WIDGET_COMMENT_ENTITY_TYPE = 'bigconsole-widget';\n\n/** A top-level comment with its (one level of) replies attached. */\nexport interface WidgetCommentThread {\n comment: EntityCommentFieldsFragment;\n replies: EntityCommentFieldsFragment[];\n}\n\nexport interface UseWidgetCommentsResult {\n threads: WidgetCommentThread[];\n /** Total comment count (top-level + replies), excluding deleted. */\n count: number;\n loading: boolean;\n error?: Error;\n mutating: boolean;\n addComment: (text: string, parentCommentId?: string) => Promise<boolean>;\n resolveComment: (id: string, resolved: boolean) => Promise<boolean>;\n deleteComment: (id: string) => Promise<boolean>;\n refetch: () => void;\n}\n\nexport function useWidgetComments(widgetId: string | undefined, enabled = true): UseWidgetCommentsResult {\n const skip = !enabled || !widgetId;\n\n const { data, loading, error, refetch } = useGetEntityCommentsQuery({\n variables: {\n input: {\n entityId: widgetId ?? '',\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n take: 100,\n sortBy: 'createdAt',\n sortOrder: 'asc',\n },\n },\n skip,\n fetchPolicy: 'cache-and-network',\n notifyOnNetworkStatusChange: true,\n });\n\n const [createComment, createState] = useCreateEntityCommentMutation();\n const [resolveCommentMutation, resolveState] = useResolveEntityCommentMutation();\n const [deleteCommentMutation, deleteState] = useDeleteEntityCommentMutation();\n const [createdComments, setCreatedComments] = useState<EntityCommentFieldsFragment[]>([]);\n\n // Keep comments returned by this session's create mutation alongside the\n // server page. The page is oldest-first, so a refetch after its 100-comment\n // limit is reached does not otherwise include a newly created comment.\n const items: EntityCommentFieldsFragment[] = useMemo(() => {\n const commentsById = new Map<string, EntityCommentFieldsFragment>();\n for (const comment of data?.getEntityComments.items ?? []) {\n commentsById.set(comment.id, comment);\n }\n for (const comment of createdComments) {\n if (comment.entityId === widgetId) commentsById.set(comment.id, comment);\n }\n return [...commentsById.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }, [createdComments, data, widgetId]);\n // Use the server-reported total for the count (items is capped at the page size).\n const total = data?.getEntityComments.total ?? items.length;\n\n // Reassemble a flat list into top-level threads + their direct replies.\n const threads = useMemo<WidgetCommentThread[]>(() => {\n const roots = items.filter((c) => !c.parentCommentId);\n const repliesByParent = new Map<string, EntityCommentFieldsFragment[]>();\n for (const c of items) {\n if (c.parentCommentId) {\n const list = repliesByParent.get(c.parentCommentId) ?? [];\n list.push(c);\n repliesByParent.set(c.parentCommentId, list);\n }\n }\n return roots.map((comment) => ({ comment, replies: repliesByParent.get(comment.id) ?? [] }));\n }, [items]);\n\n // All actions catch their own errors and resolve to a boolean (never reject),\n // so callers can `await` for success/failure without unhandled rejections and\n // can keep the user's draft when a post fails.\n const addComment = useCallback(\n async (text: string, parentCommentId?: string): Promise<boolean> => {\n const body = text.trim();\n if (!body || !widgetId) return false;\n try {\n const res = await createComment({\n variables: {\n input: {\n entityId: widgetId,\n entityType: WIDGET_COMMENT_ENTITY_TYPE,\n content: { text: body },\n contentType: 'TEXT',\n ...(parentCommentId ? { parentCommentId } : {}),\n },\n },\n });\n const comment = res.data?.createEntityComment;\n if (comment) {\n setCreatedComments((current) =>\n current.some((existing) => existing.id === comment.id) ? current : [...current, comment]\n );\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [createComment, refetch, widgetId]\n );\n\n const resolveComment = useCallback(\n async (id: string, resolved: boolean): Promise<boolean> => {\n try {\n const res = await resolveCommentMutation({ variables: { input: { id, resolved } } });\n const comment = res.data?.resolveEntityComment;\n if (comment) {\n setCreatedComments((current) => current.map((existing) => (existing.id === comment.id ? comment : existing)));\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [resolveCommentMutation, refetch]\n );\n\n const deleteComment = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n const res = await deleteCommentMutation({ variables: { input: { id, soft: true } } });\n if (res.data?.deleteEntityComment) {\n setCreatedComments((current) => current.filter((comment) => comment.id !== id));\n await refetch();\n return true;\n }\n return false;\n } catch {\n return false;\n }\n },\n [deleteCommentMutation, refetch]\n );\n\n return {\n threads,\n count: total,\n loading,\n error: error as Error | undefined,\n mutating: createState.loading || resolveState.loading || deleteState.loading,\n addComment,\n resolveComment,\n deleteComment,\n refetch: useCallback(() => {\n void refetch();\n }, [refetch]),\n };\n}\n\nexport default useWidgetComments;\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.922.1",
3
+ "version": "2026.922.2",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [