@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.
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js +109 -99
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js.map +1 -1
- package/dist/bigconsole/hooks/useWidgetComments.js +47 -36
- package/dist/bigconsole/hooks/useWidgetComments.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,78 +1,78 @@
|
|
|
1
1
|
import e from "../../../hooks/useWidgetComments.js";
|
|
2
|
-
import { useCallback as t,
|
|
3
|
-
import { Check as
|
|
4
|
-
import { useI18n as
|
|
5
|
-
import { jsx as
|
|
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
|
|
7
|
+
function g(e, t) {
|
|
8
8
|
return e("bigconsole.widget.comments.authorLabel", "User {{id}}", { id: t.slice(0, 6) });
|
|
9
9
|
}
|
|
10
|
-
function
|
|
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
|
|
18
|
-
let { t:
|
|
19
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
24
|
+
/* @__PURE__ */ h("div", {
|
|
25
25
|
className: "flex items-center justify-between gap-2",
|
|
26
|
-
children: [/* @__PURE__ */
|
|
26
|
+
children: [/* @__PURE__ */ m("span", {
|
|
27
27
|
className: "text-xs font-medium text-text-primary",
|
|
28
|
-
children:
|
|
29
|
-
}), /* @__PURE__ */
|
|
28
|
+
children: g(c, e.authorId)
|
|
29
|
+
}), /* @__PURE__ */ m("span", {
|
|
30
30
|
className: "text-[10px] text-text-muted tabular-nums",
|
|
31
|
-
children:
|
|
31
|
+
children: _(e.createdAt)
|
|
32
32
|
})]
|
|
33
33
|
}),
|
|
34
|
-
/* @__PURE__ */
|
|
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__ */
|
|
38
|
+
/* @__PURE__ */ h("div", {
|
|
39
39
|
className: "mt-1.5 flex items-center gap-2",
|
|
40
40
|
children: [
|
|
41
|
-
|
|
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__ */
|
|
44
|
+
/* @__PURE__ */ m(a, { className: "h-3 w-3" }),
|
|
45
45
|
" ",
|
|
46
|
-
|
|
46
|
+
c("bigconsole.widget.comments.resolvedBadge", "Resolved")
|
|
47
47
|
]
|
|
48
48
|
}),
|
|
49
|
-
|
|
49
|
+
s && /* @__PURE__ */ h("button", {
|
|
50
50
|
type: "button",
|
|
51
51
|
disabled: n,
|
|
52
|
-
onClick: () =>
|
|
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__ */
|
|
55
|
+
/* @__PURE__ */ m(o, { className: "h-3 w-3" }),
|
|
56
56
|
" ",
|
|
57
|
-
|
|
57
|
+
c("bigconsole.widget.comments.reply", "Reply")
|
|
58
58
|
]
|
|
59
59
|
}),
|
|
60
|
-
/* @__PURE__ */
|
|
60
|
+
/* @__PURE__ */ h("button", {
|
|
61
61
|
type: "button",
|
|
62
62
|
disabled: n,
|
|
63
|
-
onClick: () =>
|
|
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: [
|
|
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__ */
|
|
67
|
+
/* @__PURE__ */ h("button", {
|
|
68
68
|
type: "button",
|
|
69
69
|
disabled: n,
|
|
70
|
-
onClick: () =>
|
|
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__ */
|
|
73
|
+
/* @__PURE__ */ m(d, { className: "h-3 w-3" }),
|
|
74
74
|
" ",
|
|
75
|
-
|
|
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
|
-
},
|
|
84
|
-
let { t:
|
|
85
|
-
let e =
|
|
86
|
-
e && await
|
|
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__ */
|
|
88
|
+
return /* @__PURE__ */ h("div", {
|
|
89
89
|
className: "flex items-end gap-2",
|
|
90
|
-
children: [/* @__PURE__ */
|
|
91
|
-
autoFocus:
|
|
92
|
-
value:
|
|
93
|
-
onChange: (e) =>
|
|
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(),
|
|
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__ */
|
|
100
|
+
}), /* @__PURE__ */ h("div", {
|
|
101
101
|
className: "flex flex-col gap-1",
|
|
102
|
-
children: [/* @__PURE__ */
|
|
102
|
+
children: [/* @__PURE__ */ m("button", {
|
|
103
103
|
type: "button",
|
|
104
|
-
disabled: t || !
|
|
105
|
-
onClick: () => void
|
|
106
|
-
title:
|
|
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__ */
|
|
109
|
-
}),
|
|
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:
|
|
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:
|
|
114
|
-
children: /* @__PURE__ */ f
|
|
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
|
-
},
|
|
119
|
-
let { t:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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":
|
|
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(),
|
|
139
|
+
e.key === "Escape" && (e.stopPropagation(), o());
|
|
131
140
|
},
|
|
132
141
|
children: [
|
|
133
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
-
|
|
143
|
-
/* @__PURE__ */
|
|
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:
|
|
154
|
+
children: d
|
|
146
155
|
})
|
|
147
156
|
]
|
|
148
|
-
}), /* @__PURE__ */
|
|
157
|
+
}), /* @__PURE__ */ m("button", {
|
|
149
158
|
type: "button",
|
|
150
|
-
onClick:
|
|
151
|
-
"aria-label":
|
|
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
|
|
162
|
+
children: /* @__PURE__ */ m(f, { className: "h-4 w-4" })
|
|
154
163
|
})]
|
|
155
164
|
}),
|
|
156
|
-
/* @__PURE__ */
|
|
165
|
+
/* @__PURE__ */ h("div", {
|
|
166
|
+
ref: O,
|
|
157
167
|
className: "flex-1 space-y-3 overflow-auto p-3",
|
|
158
168
|
children: [
|
|
159
|
-
|
|
169
|
+
g && u.length === 0 && /* @__PURE__ */ m("div", {
|
|
160
170
|
className: "flex items-center justify-center py-8 text-text-muted",
|
|
161
|
-
children: /* @__PURE__ */
|
|
171
|
+
children: /* @__PURE__ */ m(s, { className: "h-5 w-5 animate-spin" })
|
|
162
172
|
}),
|
|
163
|
-
|
|
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:
|
|
176
|
+
children: l("bigconsole.widget.comments.loadError", "Couldn’t load comments.")
|
|
167
177
|
}),
|
|
168
|
-
!
|
|
178
|
+
!g && !_ && u.length === 0 && /* @__PURE__ */ m("p", {
|
|
169
179
|
className: "py-8 text-center text-sm text-text-muted",
|
|
170
|
-
children:
|
|
180
|
+
children: l("bigconsole.widget.comments.empty", "No comments yet. Start the conversation.")
|
|
171
181
|
}),
|
|
172
|
-
|
|
182
|
+
u.map(({ comment: e, replies: t }) => /* @__PURE__ */ h("div", {
|
|
173
183
|
className: "space-y-2",
|
|
174
184
|
children: [
|
|
175
|
-
/* @__PURE__ */
|
|
185
|
+
/* @__PURE__ */ m(v, {
|
|
176
186
|
comment: e,
|
|
177
|
-
busy:
|
|
178
|
-
onResolve: (e, t) => void
|
|
179
|
-
onDelete: (e) => void
|
|
180
|
-
onReply: (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__ */
|
|
192
|
+
t.map((e) => /* @__PURE__ */ m(v, {
|
|
183
193
|
comment: e,
|
|
184
194
|
isReply: !0,
|
|
185
|
-
busy:
|
|
186
|
-
onResolve: (e, t) => void
|
|
187
|
-
onDelete: (e) => void
|
|
195
|
+
busy: b,
|
|
196
|
+
onResolve: (e, t) => void S(e, t),
|
|
197
|
+
onDelete: (e) => void C(e)
|
|
188
198
|
}, e.id)),
|
|
189
|
-
|
|
199
|
+
w === e.id && /* @__PURE__ */ m("div", {
|
|
190
200
|
className: "pl-6",
|
|
191
|
-
children: /* @__PURE__ */
|
|
192
|
-
placeholder:
|
|
193
|
-
busy:
|
|
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) =>
|
|
196
|
-
onCancel: () =>
|
|
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__ */
|
|
213
|
+
/* @__PURE__ */ m("div", {
|
|
204
214
|
className: "border-t border-[var(--color-border-default)] p-3",
|
|
205
|
-
children: /* @__PURE__ */
|
|
206
|
-
placeholder:
|
|
207
|
-
busy:
|
|
208
|
-
onSubmit: (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 {
|
|
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
|
|
5
|
-
function
|
|
6
|
-
let { data:
|
|
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:
|
|
9
|
-
entityType:
|
|
8
|
+
entityId: c ?? "",
|
|
9
|
+
entityType: s,
|
|
10
10
|
take: 100,
|
|
11
11
|
sortBy: "createdAt",
|
|
12
12
|
sortOrder: "asc"
|
|
13
13
|
} },
|
|
14
|
-
skip: !
|
|
14
|
+
skip: !l || !c,
|
|
15
15
|
fetchPolicy: "cache-and-network",
|
|
16
16
|
notifyOnNetworkStatusChange: !0
|
|
17
|
-
}), [
|
|
18
|
-
let e =
|
|
19
|
-
for (let
|
|
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
|
-
}, [
|
|
36
|
+
}, [S]), T = i(async (e, t) => {
|
|
28
37
|
let n = e.trim();
|
|
29
|
-
if (!n || !
|
|
38
|
+
if (!n || !c) return !1;
|
|
30
39
|
try {
|
|
31
|
-
|
|
32
|
-
entityId:
|
|
33
|
-
entityType:
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
]), C = i(async (e, t) => {
|
|
54
|
+
c
|
|
55
|
+
]), E = i(async (e, t) => {
|
|
46
56
|
try {
|
|
47
|
-
|
|
57
|
+
let n = (await g({ variables: { input: {
|
|
48
58
|
id: e,
|
|
49
59
|
resolved: t
|
|
50
|
-
} } })).data?.resolveEntityComment
|
|
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
|
-
}, [
|
|
65
|
+
}, [g, p]), D = i(async (e) => {
|
|
55
66
|
try {
|
|
56
|
-
return (await
|
|
67
|
+
return (await v({ variables: { input: {
|
|
57
68
|
id: e,
|
|
58
69
|
soft: !0
|
|
59
|
-
} } })).data?.deleteEntityComment ? (await
|
|
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
|
-
}, [
|
|
74
|
+
}, [v, p]);
|
|
64
75
|
return {
|
|
65
|
-
threads:
|
|
66
|
-
count:
|
|
67
|
-
loading:
|
|
68
|
-
error:
|
|
69
|
-
mutating:
|
|
70
|
-
addComment:
|
|
71
|
-
resolveComment:
|
|
72
|
-
deleteComment:
|
|
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
|
-
|
|
75
|
-
}, [
|
|
85
|
+
p();
|
|
86
|
+
}, [p])
|
|
76
87
|
};
|
|
77
88
|
}
|
|
78
89
|
//#endregion
|
|
79
|
-
export {
|
|
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
|
|
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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"}
|