@burdenoff/microfe-planmagnet 2026.925.9 → 2026.925.11

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.
@@ -0,0 +1,4 @@
1
+ export declare function WorkItemActivity({ workItemId }: {
2
+ workItemId: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
4
+ //# sourceMappingURL=WorkItemActivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkItemActivity.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/WorkItemActivity.tsx"],"names":[],"mappings":"AAyDA,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,2CA+CtE"}
@@ -0,0 +1,97 @@
1
+ import { PersonName as e } from "../people/People.js";
2
+ import { gql as t } from "@apollo/client";
3
+ import { useQuery as n } from "@apollo/client/react";
4
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
+ import { Card as a, CardContent as o, CardHeader as s, CardTitle as c } from "@burdenoff/fe-libs/ui";
6
+ //#region src/planmagnet/comments/WorkItemActivity.tsx
7
+ var l = t`
8
+ query WorkItemActivity($workItemId: ID!) {
9
+ planmagnetWorkItemActivity(workItemId: $workItemId, first: 50) {
10
+ totalCount
11
+ truncated
12
+ items {
13
+ id
14
+ kind
15
+ actorId
16
+ at
17
+ fromStatus
18
+ toStatus
19
+ excerpt
20
+ }
21
+ }
22
+ }
23
+ `, u = (e) => e ? e.charAt(0) + e.slice(1).toLowerCase().replace(/_/g, " ") : "";
24
+ function d(e) {
25
+ switch (e.kind) {
26
+ case "CREATED": return e.toStatus ? `Created in ${u(e.toStatus)}` : "Created";
27
+ case "STATUS_CHANGED": return `Moved from ${u(e.fromStatus)} to ${u(e.toStatus)}`;
28
+ case "COMMENTED": return `Commented: “${e.excerpt ?? ""}”`;
29
+ default: return e.kind;
30
+ }
31
+ }
32
+ function f({ workItemId: t }) {
33
+ let { data: u, loading: f, error: p } = n(l, {
34
+ variables: { workItemId: t },
35
+ skip: !t
36
+ }), m = u?.planmagnetWorkItemActivity, h = m?.items ?? [];
37
+ return /* @__PURE__ */ i(a, {
38
+ "data-testid": "work-item-activity",
39
+ children: [/* @__PURE__ */ r(s, { children: /* @__PURE__ */ r(c, { children: "Activity" }) }), /* @__PURE__ */ i(o, {
40
+ className: "space-y-3",
41
+ children: [
42
+ f && !u && /* @__PURE__ */ r("p", {
43
+ className: "text-sm text-muted-foreground",
44
+ children: "Loading activity…"
45
+ }),
46
+ p && /* @__PURE__ */ i("p", {
47
+ role: "alert",
48
+ className: "text-sm text-destructive",
49
+ children: ["Activity could not be loaded: ", p.message]
50
+ }),
51
+ /* @__PURE__ */ r("ol", {
52
+ className: "space-y-3",
53
+ children: h.map((t, n) => /* @__PURE__ */ i("li", {
54
+ className: "flex gap-3",
55
+ "data-testid": "activity-event",
56
+ children: [/* @__PURE__ */ i("div", {
57
+ className: "flex flex-col items-center",
58
+ children: [/* @__PURE__ */ r("div", { className: "h-2.5 w-2.5 rounded-full bg-primary mt-1.5" }), n < h.length - 1 && /* @__PURE__ */ r("div", { className: "w-px flex-1 bg-border" })]
59
+ }), /* @__PURE__ */ i("div", {
60
+ className: "pb-1 min-w-0",
61
+ children: [/* @__PURE__ */ r("p", {
62
+ className: "text-sm break-words",
63
+ children: d(t)
64
+ }), /* @__PURE__ */ i("p", {
65
+ className: "text-xs text-muted-foreground",
66
+ children: [
67
+ t.actorId ? /* @__PURE__ */ r(e, { id: t.actorId }) : "Unknown",
68
+ " ·",
69
+ " ",
70
+ new Date(t.at).toLocaleString()
71
+ ]
72
+ })]
73
+ })]
74
+ }, t.id))
75
+ }),
76
+ m?.truncated && /* @__PURE__ */ i("p", {
77
+ className: "text-xs text-muted-foreground",
78
+ children: [
79
+ "Showing the latest ",
80
+ h.length,
81
+ " of ",
82
+ m.totalCount,
83
+ " events."
84
+ ]
85
+ }),
86
+ /* @__PURE__ */ r("p", {
87
+ className: "text-xs text-muted-foreground",
88
+ children: "Creation, status changes and comments are recorded. Other edits are not tracked yet."
89
+ })
90
+ ]
91
+ })]
92
+ });
93
+ }
94
+ //#endregion
95
+ export { f as WorkItemActivity };
96
+
97
+ //# sourceMappingURL=WorkItemActivity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkItemActivity.js","names":[],"sources":["../../../src/planmagnet/comments/WorkItemActivity.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { PersonName } from '../people/People';\n\n/**\n * A work item's recorded history (P1.4). This card used to synthesise three\n * entries client-side, including \"Status changed to X\" attributed to whoever\n * was assigned, at the item's updatedAt. That was fabricated history. It now\n * shows only what the server has recorded: creation, status changes and\n * comments. Other edits are not recorded yet, and the card says so.\n */\n\nconst ACTIVITY = gql`\n query WorkItemActivity($workItemId: ID!) {\n planmagnetWorkItemActivity(workItemId: $workItemId, first: 50) {\n totalCount\n truncated\n items {\n id\n kind\n actorId\n at\n fromStatus\n toStatus\n excerpt\n }\n }\n }\n`;\n\ninterface ActivityEvent {\n id: string;\n kind: string;\n actorId: string | null;\n at: string;\n fromStatus: string | null;\n toStatus: string | null;\n excerpt: string | null;\n}\n\nconst statusLabel = (status: string | null) =>\n status ? status.charAt(0) + status.slice(1).toLowerCase().replace(/_/g, ' ') : '';\n\nfunction describe(event: ActivityEvent): string {\n switch (event.kind) {\n case 'CREATED':\n return event.toStatus ? `Created in ${statusLabel(event.toStatus)}` : 'Created';\n case 'STATUS_CHANGED':\n return `Moved from ${statusLabel(event.fromStatus)} to ${statusLabel(event.toStatus)}`;\n case 'COMMENTED':\n return `Commented: “${event.excerpt ?? ''}”`;\n default:\n return event.kind;\n }\n}\n\nexport function WorkItemActivity({ workItemId }: { workItemId: string }) {\n const { data, loading, error } = useQuery<{\n planmagnetWorkItemActivity: { totalCount: number; truncated: boolean; items: ActivityEvent[] };\n }>(ACTIVITY, { variables: { workItemId }, skip: !workItemId });\n const activity = data?.planmagnetWorkItemActivity;\n const events = activity?.items ?? [];\n\n return (\n <Card data-testid=\"work-item-activity\">\n <CardHeader>\n <CardTitle>Activity</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading activity…</p>}\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Activity could not be loaded: {error.message}\n </p>\n )}\n <ol className=\"space-y-3\">\n {events.map((event, i) => (\n <li key={event.id} className=\"flex gap-3\" data-testid=\"activity-event\">\n <div className=\"flex flex-col items-center\">\n <div className=\"h-2.5 w-2.5 rounded-full bg-primary mt-1.5\" />\n {i < events.length - 1 && <div className=\"w-px flex-1 bg-border\" />}\n </div>\n <div className=\"pb-1 min-w-0\">\n <p className=\"text-sm break-words\">{describe(event)}</p>\n <p className=\"text-xs text-muted-foreground\">\n {event.actorId ? <PersonName id={event.actorId} /> : 'Unknown'} ·{' '}\n {new Date(event.at).toLocaleString()}\n </p>\n </div>\n </li>\n ))}\n </ol>\n {activity?.truncated && (\n <p className=\"text-xs text-muted-foreground\">\n Showing the latest {events.length} of {activity.totalCount} events.\n </p>\n )}\n <p className=\"text-xs text-muted-foreground\">\n Creation, status changes and comments are recorded. Other edits are not tracked yet.\n </p>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;AAaA,IAAM,IAAW,CAAG;;;;;;;;;;;;;;;;GA4Bd,KAAe,MACnB,IAAS,EAAO,OAAO,EAAE,GAAG,EAAO,MAAM,EAAE,CAAC,aAAa,CAAC,QAAQ,MAAM,IAAI,GAAG;AAEjF,SAAS,EAAS,GAA8B;AAC9C,SAAQ,EAAM,MAAd;EACE,KAAK,UACH,QAAO,EAAM,WAAW,cAAc,EAAY,EAAM,SAAS,KAAK;EACxE,KAAK,iBACH,QAAO,cAAc,EAAY,EAAM,WAAW,CAAC,MAAM,EAAY,EAAM,SAAS;EACtF,KAAK,YACH,QAAO,eAAe,EAAM,WAAW,GAAG;EAC5C,QACE,QAAO,EAAM;;;AAInB,SAAgB,EAAiB,EAAE,iBAAsC;CACvE,IAAM,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAU;EAAE,WAAW,EAAE,eAAY;EAAE,MAAM,CAAC;EAAY,CAAC,EACxD,IAAW,GAAM,4BACjB,IAAS,GAAU,SAAS,EAAE;AAEpC,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,YAAoB,CAAA,EACpB,CAAA,EACb,kBAAC,GAAD;GAAa,WAAU;aAAvB;IACG,KAAW,CAAC,KAAQ,kBAAC,KAAD;KAAG,WAAU;eAAgC;KAAqB,CAAA;IACtF,KACC,kBAAC,KAAD;KAAG,MAAK;KAAQ,WAAU;eAA1B,CAAqD,kCACpB,EAAM,QACnC;;IAEN,kBAAC,MAAD;KAAI,WAAU;eACX,EAAO,KAAK,GAAO,MAClB,kBAAC,MAAD;MAAmB,WAAU;MAAa,eAAY;gBAAtD,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAK,WAAU,8CAA+C,CAAA,EAC7D,IAAI,EAAO,SAAS,KAAK,kBAAC,OAAD,EAAK,WAAU,yBAA0B,CAAA,CAC/D;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAuB,EAAS,EAAM;QAAK,CAAA,EACxD,kBAAC,KAAD;QAAG,WAAU;kBAAb;SACG,EAAM,UAAU,kBAAC,GAAD,EAAY,IAAI,EAAM,SAAW,CAAA,GAAG;SAAU;SAAG;SACjE,IAAI,KAAK,EAAM,GAAG,CAAC,gBAAgB;SAClC;UACA;SACH;QAZI,EAAM,GAYV,CACL;KACC,CAAA;IACJ,GAAU,aACT,kBAAC,KAAD;KAAG,WAAU;eAAb;MAA6C;MACvB,EAAO;MAAO;MAAK,EAAS;MAAW;MACzD;;IAEN,kBAAC,KAAD;KAAG,WAAU;eAAgC;KAEzC,CAAA;IACQ;KACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"WorkItemComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"names":[],"mappings":"AAqGA,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,2CA8GtE"}
1
+ {"version":3,"file":"WorkItemComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"names":[],"mappings":"AAqGA,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,2CAkHtE"}
@@ -59,7 +59,7 @@ function N({ workItemId: t }) {
59
59
  },
60
60
  skip: !t
61
61
  }), [k, j] = o(D, {
62
- refetchQueries: ["WorkItemComments"],
62
+ refetchQueries: ["WorkItemComments", "WorkItemActivity"],
63
63
  awaitRefetchQueries: !0
64
64
  }), N = g?.planmagnetWorkItemComments, F = N?.items ?? [], I = u.trim(), L = I.length > w;
65
65
  return /* @__PURE__ */ l(b, {
@@ -74,6 +74,7 @@ function N({ workItemId: t }) {
74
74
  }) }), /* @__PURE__ */ l(x, {
75
75
  className: "space-y-4",
76
76
  children: [
77
+ f && /* @__PURE__ */ c(M, { message: f }),
77
78
  _ && !g && /* @__PURE__ */ c("p", {
78
79
  className: "text-sm text-muted-foreground",
79
80
  children: "Loading comments…"
@@ -87,7 +88,8 @@ function N({ workItemId: t }) {
87
88
  className: "space-y-3",
88
89
  children: F.map((e) => /* @__PURE__ */ c(P, {
89
90
  comment: e,
90
- mine: e.authorId === a?.id
91
+ mine: e.authorId === a?.id,
92
+ onError: p
91
93
  }, e.id))
92
94
  }),
93
95
  N?.pageInfo.hasNextPage && /* @__PURE__ */ c(y, {
@@ -132,7 +134,6 @@ function N({ workItemId: t }) {
132
134
  onChange: (e) => d(e.target.value)
133
135
  }),
134
136
  L && /* @__PURE__ */ c(M, { message: `A comment can be at most ${w} characters.` }),
135
- f && /* @__PURE__ */ c(M, { message: f }),
136
137
  /* @__PURE__ */ c("div", {
137
138
  className: "flex justify-end",
138
139
  children: /* @__PURE__ */ c(y, {
@@ -159,99 +160,95 @@ function N({ workItemId: t }) {
159
160
  })]
160
161
  });
161
162
  }
162
- function P({ comment: e, mine: i }) {
163
- let [a, s] = r(!1), [b, x] = r(e.body), [S, C] = r(null), w = {
164
- refetchQueries: ["WorkItemComments"],
163
+ function P({ comment: e, mine: i, onError: a }) {
164
+ let [s, b] = r(!1), [x, S] = r(e.body), C = a, w = {
165
+ refetchQueries: ["WorkItemComments", "WorkItemActivity"],
165
166
  awaitRefetchQueries: !0
166
- }, [T, E] = o(O, w), [D, N] = o(k, w);
167
+ }, [T, E] = o(O, w), [D, M] = o(k, w);
167
168
  return /* @__PURE__ */ l("li", {
168
169
  className: "rounded-md border p-3 space-y-1",
169
170
  "data-testid": "comment",
170
- children: [
171
- /* @__PURE__ */ l("div", {
172
- className: "flex items-center justify-between gap-2 text-xs text-muted-foreground",
173
- children: [/* @__PURE__ */ l("span", { children: [
174
- /* @__PURE__ */ c("span", {
175
- className: "font-medium text-foreground",
176
- children: /* @__PURE__ */ c(t, { id: e.authorId })
177
- }),
178
- " ",
179
- "· ",
180
- j(e.createdAt),
181
- e.editedAt && /* @__PURE__ */ c("span", {
182
- title: `Edited ${j(e.editedAt)}`,
183
- children: " · edited"
184
- })
185
- ] }), i && !a && /* @__PURE__ */ l("span", {
186
- className: "flex gap-1",
187
- children: [/* @__PURE__ */ c(y, {
171
+ children: [/* @__PURE__ */ l("div", {
172
+ className: "flex items-center justify-between gap-2 text-xs text-muted-foreground",
173
+ children: [/* @__PURE__ */ l("span", { children: [
174
+ /* @__PURE__ */ c("span", {
175
+ className: "font-medium text-foreground",
176
+ children: /* @__PURE__ */ c(t, { id: e.authorId })
177
+ }),
178
+ " ",
179
+ "· ",
180
+ j(e.createdAt),
181
+ e.editedAt && /* @__PURE__ */ c("span", {
182
+ title: `Edited ${j(e.editedAt)}`,
183
+ children: " · edited"
184
+ })
185
+ ] }), i && !s && /* @__PURE__ */ l("span", {
186
+ className: "flex gap-1",
187
+ children: [/* @__PURE__ */ c(y, {
188
+ size: "sm",
189
+ variant: "ghost",
190
+ className: "h-6 px-2 text-xs",
191
+ onClick: () => b(!0),
192
+ children: "Edit"
193
+ }), /* @__PURE__ */ l(u, { children: [/* @__PURE__ */ c(v, {
194
+ asChild: !0,
195
+ children: /* @__PURE__ */ c(y, {
188
196
  size: "sm",
189
197
  variant: "ghost",
190
198
  className: "h-6 px-2 text-xs",
191
- onClick: () => s(!0),
192
- children: "Edit"
193
- }), /* @__PURE__ */ l(u, { children: [/* @__PURE__ */ c(v, {
194
- asChild: !0,
195
- children: /* @__PURE__ */ c(y, {
196
- size: "sm",
197
- variant: "ghost",
198
- className: "h-6 px-2 text-xs",
199
- disabled: N.loading,
200
- children: "Delete"
201
- })
202
- }), /* @__PURE__ */ l(p, { children: [/* @__PURE__ */ l(g, { children: [/* @__PURE__ */ c(_, { children: "Delete this comment?" }), /* @__PURE__ */ c(m, { children: "It is removed from the discussion for everyone." })] }), /* @__PURE__ */ l(h, { children: [/* @__PURE__ */ c(f, { children: "Keep it" }), /* @__PURE__ */ c(d, {
203
- className: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
204
- onClick: async () => {
205
- C(null);
206
- try {
207
- await n(D({ variables: { id: e.id } }));
208
- } catch (e) {
209
- C(A(e, "The comment could not be deleted."));
210
- }
211
- },
199
+ disabled: M.loading,
212
200
  children: "Delete"
213
- })] })] })] })]
214
- })]
215
- }),
216
- a ? /* @__PURE__ */ l("div", {
217
- className: "space-y-2",
218
- children: [/* @__PURE__ */ c("textarea", {
219
- "aria-label": "Edit comment",
220
- className: "w-full min-h-[60px] rounded-md border border-input bg-background px-3 py-2 text-sm",
221
- value: b,
222
- onChange: (e) => x(e.target.value)
223
- }), /* @__PURE__ */ l("div", {
224
- className: "flex justify-end gap-2",
225
- children: [/* @__PURE__ */ c(y, {
226
- size: "sm",
227
- variant: "outline",
228
- onClick: () => {
229
- s(!1), x(e.body), C(null);
230
- },
231
- children: "Cancel"
232
- }), /* @__PURE__ */ c(y, {
233
- size: "sm",
234
- onClick: async () => {
235
- C(null);
236
- try {
237
- await n(T({ variables: {
238
- id: e.id,
239
- body: b.trim()
240
- } })), s(!1);
241
- } catch (e) {
242
- C(A(e, "The comment could not be saved."));
243
- }
244
- },
245
- disabled: !b.trim() || E.loading,
246
- children: E.loading ? "Saving…" : "Save"
247
- })]
201
+ })
202
+ }), /* @__PURE__ */ l(p, { children: [/* @__PURE__ */ l(g, { children: [/* @__PURE__ */ c(_, { children: "Delete this comment?" }), /* @__PURE__ */ c(m, { children: "It is removed from the discussion for everyone." })] }), /* @__PURE__ */ l(h, { children: [/* @__PURE__ */ c(f, { children: "Keep it" }), /* @__PURE__ */ c(d, {
203
+ className: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
204
+ onClick: async () => {
205
+ C(null);
206
+ try {
207
+ await n(D({ variables: { id: e.id } }));
208
+ } catch (e) {
209
+ C(A(e, "The comment could not be deleted."));
210
+ }
211
+ },
212
+ children: "Delete"
213
+ })] })] })] })]
214
+ })]
215
+ }), s ? /* @__PURE__ */ l("div", {
216
+ className: "space-y-2",
217
+ children: [/* @__PURE__ */ c("textarea", {
218
+ "aria-label": "Edit comment",
219
+ className: "w-full min-h-[60px] rounded-md border border-input bg-background px-3 py-2 text-sm",
220
+ value: x,
221
+ onChange: (e) => S(e.target.value)
222
+ }), /* @__PURE__ */ l("div", {
223
+ className: "flex justify-end gap-2",
224
+ children: [/* @__PURE__ */ c(y, {
225
+ size: "sm",
226
+ variant: "outline",
227
+ onClick: () => {
228
+ b(!1), S(e.body), C(null);
229
+ },
230
+ children: "Cancel"
231
+ }), /* @__PURE__ */ c(y, {
232
+ size: "sm",
233
+ onClick: async () => {
234
+ C(null);
235
+ try {
236
+ await n(T({ variables: {
237
+ id: e.id,
238
+ body: x.trim()
239
+ } })), b(!1);
240
+ } catch (e) {
241
+ C(A(e, "The comment could not be saved."));
242
+ }
243
+ },
244
+ disabled: !x.trim() || E.loading,
245
+ children: E.loading ? "Saving…" : "Save"
248
246
  })]
249
- }) : /* @__PURE__ */ c("p", {
250
- className: "text-sm whitespace-pre-wrap break-words",
251
- children: e.body
252
- }),
253
- S && /* @__PURE__ */ c(M, { message: S })
254
- ]
247
+ })]
248
+ }) : /* @__PURE__ */ c("p", {
249
+ className: "text-sm whitespace-pre-wrap break-words",
250
+ children: e.body
251
+ })]
255
252
  });
256
253
  }
257
254
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"WorkItemComments.js","names":[],"sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n} from '@burdenoff/fe-libs/ui';\nimport { MessageSquare } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Discussion on a work item (P1.4). Anyone who can read the item sees the\n * thread; posting needs edit rights on work items; only a comment's author\n * can edit or delete it. The server enforces all three and says why it refused\n * (shown inline). The controls here only avoid offering what would be refused.\n */\n\nconst MAX_LENGTH = 10_000;\nconst PAGE = 50;\n\nconst COMMENTS = gql`\n query WorkItemComments($workItemId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetWorkItemComments(workItemId: $workItemId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n }\n }\n }\n`;\nconst ADD = gql`\n mutation WorkItemCommentAdd($input: PlanmagnetAddWorkItemCommentInput!) {\n planmagnetAddWorkItemComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT = gql`\n mutation WorkItemCommentEdit($id: ID!, $body: String!) {\n planmagnetEditWorkItemComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE = gql`\n mutation WorkItemCommentDelete($id: ID!) {\n planmagnetDeleteWorkItemComment(id: $id)\n }\n`;\n\ninterface Comment {\n id: string;\n authorId: string;\n body: string;\n createdAt: string;\n editedAt: string | null;\n}\n\ninterface CommentsData {\n planmagnetWorkItemComments: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: Comment[];\n };\n}\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nfunction ErrorLine({ message }: { message: string }) {\n return (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {message}\n </p>\n );\n}\n\nexport function WorkItemComments({ workItemId }: { workItemId: string }) {\n const { currentUser } = usePlanMagnet();\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const {\n data,\n loading,\n error: loadError,\n fetchMore,\n } = useQuery<CommentsData>(COMMENTS, {\n variables: { workItemId, pagination: { first: PAGE } },\n skip: !workItemId,\n });\n const refetch = { refetchQueries: ['WorkItemComments'], awaitRefetchQueries: true };\n const [add, addState] = useMutation(ADD, refetch);\n\n const thread = data?.planmagnetWorkItemComments;\n const comments = thread?.items ?? [];\n const trimmed = draft.trim();\n const tooLong = trimmed.length > MAX_LENGTH;\n\n const post = async () => {\n setError(null);\n try {\n await throwOnError(add({ variables: { input: { workItemId, body: trimmed } } }));\n setDraft('');\n } catch (e) {\n setError(messageOf(e, 'The comment could not be posted.'));\n }\n };\n\n const loadMore = async () => {\n const after = thread?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetWorkItemComments: {\n ...fetchMoreResult.planmagnetWorkItemComments,\n items: [\n ...prev.planmagnetWorkItemComments.items,\n ...fetchMoreResult.planmagnetWorkItemComments.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <Card data-testid=\"work-item-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Discussion {thread ? `(${thread.totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading comments…</p>}\n {loadError && <ErrorLine message={`Comments could not be loaded: ${loadError.message}`} />}\n {thread && comments.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No comments yet.</p>\n )}\n <ol className=\"space-y-3\">\n {comments.map((comment) => (\n <CommentRow\n key={comment.id}\n comment={comment}\n mine={comment.authorId === currentUser?.id}\n />\n ))}\n </ol>\n {thread?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={loadMore} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${comments.length} of ${thread.totalCount})`}\n </Button>\n )}\n <div className=\"space-y-2 border-t pt-4\">\n <label htmlFor={`comment-${workItemId}`} className=\"sr-only\">\n Add a comment\n </label>\n <textarea\n id={`comment-${workItemId}`}\n className=\"w-full min-h-[80px] rounded-md border border-input bg-background px-3 py-2 text-sm\"\n placeholder=\"Add a comment…\"\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n />\n {tooLong && <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />}\n {error && <ErrorLine message={error} />}\n <div className=\"flex justify-end\">\n <Button\n size=\"sm\"\n onClick={post}\n disabled={!trimmed || tooLong || addState.loading}\n data-testid=\"comment-post\"\n >\n {addState.loading ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n );\n}\n\nfunction CommentRow({ comment, mine }: { comment: Comment; mine: boolean }) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(comment.body);\n const [error, setError] = useState<string | null>(null);\n const refetch = { refetchQueries: ['WorkItemComments'], awaitRefetchQueries: true };\n const [edit, editState] = useMutation(EDIT, refetch);\n const [remove, removeState] = useMutation(DELETE, refetch);\n\n const save = async () => {\n setError(null);\n try {\n await throwOnError(edit({ variables: { id: comment.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n setError(messageOf(e, 'The comment could not be saved.'));\n }\n };\n\n return (\n <li className=\"rounded-md border p-3 space-y-1\" data-testid=\"comment\">\n <div className=\"flex items-center justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={comment.authorId} />\n </span>{' '}\n · {when(comment.createdAt)}\n {comment.editedAt && <span title={`Edited ${when(comment.editedAt)}`}> · edited</span>}\n </span>\n {mine && !editing && (\n <span className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n disabled={removeState.loading}\n >\n Delete\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete this comment?</AlertDialogTitle>\n <AlertDialogDescription>\n It is removed from the discussion for everyone.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n onClick={async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: comment.id } }));\n } catch (e) {\n setError(messageOf(e, 'The comment could not be deleted.'));\n }\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </span>\n )}\n </div>\n {editing ? (\n <div className=\"space-y-2\">\n <textarea\n aria-label=\"Edit comment\"\n className=\"w-full min-h-[60px] rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={text}\n onChange={(e) => setText(e.target.value)}\n />\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(comment.body);\n setError(null);\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" onClick={save} disabled={!text.trim() || editState.loading}>\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <p className=\"text-sm whitespace-pre-wrap break-words\">{comment.body}</p>\n )}\n {error && <ErrorLine message={error} />}\n </li>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WorkItemComments.js","names":[],"sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n} from '@burdenoff/fe-libs/ui';\nimport { MessageSquare } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Discussion on a work item (P1.4). Anyone who can read the item sees the\n * thread; posting needs edit rights on work items; only a comment's author\n * can edit or delete it. The server enforces all three and says why it refused\n * (shown inline). The controls here only avoid offering what would be refused.\n */\n\nconst MAX_LENGTH = 10_000;\nconst PAGE = 50;\n\nconst COMMENTS = gql`\n query WorkItemComments($workItemId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetWorkItemComments(workItemId: $workItemId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n }\n }\n }\n`;\nconst ADD = gql`\n mutation WorkItemCommentAdd($input: PlanmagnetAddWorkItemCommentInput!) {\n planmagnetAddWorkItemComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT = gql`\n mutation WorkItemCommentEdit($id: ID!, $body: String!) {\n planmagnetEditWorkItemComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE = gql`\n mutation WorkItemCommentDelete($id: ID!) {\n planmagnetDeleteWorkItemComment(id: $id)\n }\n`;\n\ninterface Comment {\n id: string;\n authorId: string;\n body: string;\n createdAt: string;\n editedAt: string | null;\n}\n\ninterface CommentsData {\n planmagnetWorkItemComments: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: Comment[];\n };\n}\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nfunction ErrorLine({ message }: { message: string }) {\n return (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {message}\n </p>\n );\n}\n\nexport function WorkItemComments({ workItemId }: { workItemId: string }) {\n const { currentUser } = usePlanMagnet();\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const {\n data,\n loading,\n error: loadError,\n fetchMore,\n } = useQuery<CommentsData>(COMMENTS, {\n variables: { workItemId, pagination: { first: PAGE } },\n skip: !workItemId,\n });\n const refetch = {\n refetchQueries: ['WorkItemComments', 'WorkItemActivity'],\n awaitRefetchQueries: true,\n };\n const [add, addState] = useMutation(ADD, refetch);\n\n const thread = data?.planmagnetWorkItemComments;\n const comments = thread?.items ?? [];\n const trimmed = draft.trim();\n const tooLong = trimmed.length > MAX_LENGTH;\n\n const post = async () => {\n setError(null);\n try {\n await throwOnError(add({ variables: { input: { workItemId, body: trimmed } } }));\n setDraft('');\n } catch (e) {\n setError(messageOf(e, 'The comment could not be posted.'));\n }\n };\n\n const loadMore = async () => {\n const after = thread?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetWorkItemComments: {\n ...fetchMoreResult.planmagnetWorkItemComments,\n items: [\n ...prev.planmagnetWorkItemComments.items,\n ...fetchMoreResult.planmagnetWorkItemComments.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <Card data-testid=\"work-item-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Discussion {thread ? `(${thread.totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {error && <ErrorLine message={error} />}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading comments…</p>}\n {loadError && <ErrorLine message={`Comments could not be loaded: ${loadError.message}`} />}\n {thread && comments.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No comments yet.</p>\n )}\n <ol className=\"space-y-3\">\n {comments.map((comment) => (\n <CommentRow\n key={comment.id}\n comment={comment}\n mine={comment.authorId === currentUser?.id}\n onError={setError}\n />\n ))}\n </ol>\n {thread?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={loadMore} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${comments.length} of ${thread.totalCount})`}\n </Button>\n )}\n <div className=\"space-y-2 border-t pt-4\">\n <label htmlFor={`comment-${workItemId}`} className=\"sr-only\">\n Add a comment\n </label>\n <textarea\n id={`comment-${workItemId}`}\n className=\"w-full min-h-[80px] rounded-md border border-input bg-background px-3 py-2 text-sm\"\n placeholder=\"Add a comment…\"\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n />\n {tooLong && <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />}\n <div className=\"flex justify-end\">\n <Button\n size=\"sm\"\n onClick={post}\n disabled={!trimmed || tooLong || addState.loading}\n data-testid=\"comment-post\"\n >\n {addState.loading ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n );\n}\n\n/**\n * Refusals are reported to the PANEL, not kept in the row: a refused edit or\n * delete still refetches the thread, and when the comment is gone (deleted\n * elsewhere) the row unmounts, taking a row-level message with it. Seen in the\n * prod verification: the comment vanished and the reason was never shown.\n */\nfunction CommentRow({\n comment,\n mine,\n onError,\n}: {\n comment: Comment;\n mine: boolean;\n onError: (message: string | null) => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(comment.body);\n const setError = onError;\n const refetch = {\n refetchQueries: ['WorkItemComments', 'WorkItemActivity'],\n awaitRefetchQueries: true,\n };\n const [edit, editState] = useMutation(EDIT, refetch);\n const [remove, removeState] = useMutation(DELETE, refetch);\n\n const save = async () => {\n setError(null);\n try {\n await throwOnError(edit({ variables: { id: comment.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n setError(messageOf(e, 'The comment could not be saved.'));\n }\n };\n\n return (\n <li className=\"rounded-md border p-3 space-y-1\" data-testid=\"comment\">\n <div className=\"flex items-center justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={comment.authorId} />\n </span>{' '}\n · {when(comment.createdAt)}\n {comment.editedAt && <span title={`Edited ${when(comment.editedAt)}`}> · edited</span>}\n </span>\n {mine && !editing && (\n <span className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n disabled={removeState.loading}\n >\n Delete\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete this comment?</AlertDialogTitle>\n <AlertDialogDescription>\n It is removed from the discussion for everyone.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n onClick={async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: comment.id } }));\n } catch (e) {\n setError(messageOf(e, 'The comment could not be deleted.'));\n }\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </span>\n )}\n </div>\n {editing ? (\n <div className=\"space-y-2\">\n <textarea\n aria-label=\"Edit comment\"\n className=\"w-full min-h-[60px] rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={text}\n onChange={(e) => setText(e.target.value)}\n />\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(comment.body);\n setError(null);\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" onClick={save} disabled={!text.trim() || editState.loading}>\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <p className=\"text-sm whitespace-pre-wrap break-words\">{comment.body}</p>\n )}\n </li>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TasksTaskDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"names":[],"mappings":"AA4RA,wBAAgB,uBAAuB,4CAgKtC"}
1
+ {"version":3,"file":"TasksTaskDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"names":[],"mappings":"AA6RA,wBAAgB,uBAAuB,4CA4HtC"}