@burdenoff/microfe-planmagnet 2026.925.9 → 2026.925.10
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/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.js +82 -85
- package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.js +1 -1
- package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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,2CA+GtE"}
|
|
@@ -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 [
|
|
163
|
+
function P({ comment: e, mine: i, onError: a }) {
|
|
164
|
+
let [s, b] = r(!1), [x, S] = r(e.body), C = a, w = {
|
|
164
165
|
refetchQueries: ["WorkItemComments"],
|
|
165
166
|
awaitRefetchQueries: !0
|
|
166
|
-
}, [T, E] = o(O, w), [D,
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
e.editedAt
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
})
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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 = { 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 {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 = { 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 </li>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SprintControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/sprints/SprintControls.tsx"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC;AACxF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAQ3D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAI/D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD;AAsCD,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,UAAU,EACV,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,
|
|
1
|
+
{"version":3,"file":"SprintControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/sprints/SprintControls.tsx"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC;AACxF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAQ3D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAI/D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD;AAsCD,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,UAAU,EACV,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,kDA2DA;AAsFD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAQD,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,SAAS,EACT,MAAM,EACN,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB,2CA2EA;AAqKD,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,2CAuKvF"}
|
|
@@ -61,7 +61,7 @@ function H({ sprintId: e, sprintName: n, status: o }) {
|
|
|
61
61
|
refetchQueries: L,
|
|
62
62
|
awaitRefetchQueries: !0
|
|
63
63
|
}, [l, d] = u(R, s), [m, h] = u(z, s), [g, _] = u(B, s), [v, y] = r(null), b = d.loading || h.loading || _.loading, x = F(o);
|
|
64
|
-
if (x.length === 0) return null;
|
|
64
|
+
if (x.length === 0) return v ? /* @__PURE__ */ f(V, { message: v }) : null;
|
|
65
65
|
let S = async (n) => {
|
|
66
66
|
y(null);
|
|
67
67
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SprintControls.js","names":[],"sources":["../../../src/planmagnet/sprints/SprintControls.tsx"],"sourcesContent":["import { useDeferredValue, 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 Badge,\n Button,\n Checkbox,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, Play, Plus, X, XCircle } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Sprint lifecycle + membership controls.\n *\n * The buttons mirror the service's transition table exactly\n * (wspace-planmagnet-svc sprint.service.ts `TRANSITIONS`):\n * start PLANNING -> ACTIVE\n * complete ACTIVE | REVIEW -> COMPLETED\n * cancel PLANNING|ACTIVE|REVIEW -> CANCELLED\n * A COMPLETED or CANCELLED sprint is finished: no transitions, and its work\n * items can no longer be added or removed. The server enforces all of this;\n * the UI only hides what would be refused, and still surfaces a refusal (a\n * concurrent change can make a shown button stale).\n */\n\nexport type SprintStatus = 'PLANNING' | 'ACTIVE' | 'REVIEW' | 'COMPLETED' | 'CANCELLED';\nexport type SprintAction = 'start' | 'complete' | 'cancel';\n\nconst ALLOWED: Record<SprintAction, readonly SprintStatus[]> = {\n start: ['PLANNING'],\n complete: ['ACTIVE', 'REVIEW'],\n cancel: ['PLANNING', 'ACTIVE', 'REVIEW'],\n};\n\nexport function sprintActionsFor(status: string): SprintAction[] {\n return (Object.keys(ALLOWED) as SprintAction[]).filter((action) =>\n (ALLOWED[action] as readonly string[]).includes(status)\n );\n}\n\nexport function isSprintFinished(status: string): boolean {\n return status === 'COMPLETED' || status === 'CANCELLED';\n}\n\n/** Every query that renders sprint status or membership. */\nconst SPRINT_QUERIES = ['GetSprints', 'GetSprintDetail', 'GetSprintForEdit', 'GetDashboardData'];\n\nconst START_SPRINT = gql`\n mutation SprintControlsStart($id: ID!) {\n planmagnetStartSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst COMPLETE_SPRINT = gql`\n mutation SprintControlsComplete($id: ID!) {\n planmagnetCompleteSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst CANCEL_SPRINT = gql`\n mutation SprintControlsCancel($id: ID!) {\n planmagnetCancelSprint(id: $id) {\n id\n status\n }\n }\n`;\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 SprintLifecycleActions({\n sprintId,\n sprintName,\n status,\n}: {\n sprintId: string;\n sprintName: string;\n status: string;\n}) {\n const options = { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true };\n const [start, startState] = useMutation(START_SPRINT, options);\n const [complete, completeState] = useMutation(COMPLETE_SPRINT, options);\n const [cancel, cancelState] = useMutation(CANCEL_SPRINT, options);\n const [error, setError] = useState<string | null>(null);\n\n const busy = startState.loading || completeState.loading || cancelState.loading;\n const actions = sprintActionsFor(status);\n if (actions.length === 0) return null;\n\n const run = async (mutate: typeof start) => {\n setError(null);\n try {\n await throwOnError(mutate({ variables: { id: sprintId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be updated.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\" data-testid=\"sprint-lifecycle-actions\">\n <div className=\"flex gap-2\">\n {actions.includes('start') && (\n <Button size=\"sm\" onClick={() => run(start)} disabled={busy}>\n <Play className=\"mr-1 h-4 w-4\" />\n Start sprint\n </Button>\n )}\n {actions.includes('complete') && (\n <ConfirmButton\n label=\"Complete sprint\"\n icon={<CheckCircle2 className=\"mr-1 h-4 w-4\" />}\n title={`Complete ${sprintName}?`}\n description=\"The sprint is closed and its work items can no longer be changed from it. This cannot be undone.\"\n confirmLabel=\"Complete sprint\"\n disabled={busy}\n onConfirm={() => run(complete)}\n />\n )}\n {actions.includes('cancel') && (\n <ConfirmButton\n label=\"Cancel sprint\"\n variant=\"outline\"\n icon={<XCircle className=\"mr-1 h-4 w-4\" />}\n title={`Cancel ${sprintName}?`}\n description=\"The sprint is abandoned and cannot be restarted. Its work items stay in it for the record. This cannot be undone.\"\n confirmLabel=\"Cancel sprint\"\n destructive\n disabled={busy}\n onConfirm={() => run(cancel)}\n />\n )}\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nfunction ConfirmButton({\n label,\n icon,\n title,\n description,\n confirmLabel,\n onConfirm,\n disabled,\n destructive = false,\n variant = 'default',\n}: {\n label: string;\n icon: React.ReactNode;\n title: string;\n description: string;\n confirmLabel: string;\n onConfirm: () => void;\n disabled: boolean;\n destructive?: boolean;\n variant?: 'default' | 'outline';\n}) {\n return (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button size=\"sm\" variant={variant} disabled={disabled}>\n {icon}\n {label}\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription>{description}</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n onClick={onConfirm}\n className={\n destructive\n ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'\n : ''\n }\n >\n {confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n\n// ─── Work items in the sprint ──────────────────────────────────────────────\n\nconst ADD_ITEMS = gql`\n mutation SprintControlsAddItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetAddWorkItemsToSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst REMOVE_ITEMS = gql`\n mutation SprintControlsRemoveItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetRemoveWorkItemsFromSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst PROJECT_WORK_ITEMS = gql`\n query SprintControlsCandidates($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n totalCount\n items {\n id\n key\n title\n status\n storyPoints\n sprintId\n }\n }\n }\n`;\n\nexport interface SprintWorkItem {\n id: string;\n key: string;\n title?: string | null;\n status: string;\n storyPoints: number | null;\n}\n\ninterface CandidateItem extends SprintWorkItem {\n sprintId: string | null;\n}\n\nconst statusLabel = (status: string) => status.toLowerCase().replace(/_/g, ' ');\n\nexport function SprintWorkItemsPanel({\n sprintId,\n projectId,\n status,\n items,\n}: {\n sprintId: string;\n projectId: string;\n status: string;\n items: SprintWorkItem[];\n}) {\n const finished = isSprintFinished(status);\n const [remove, removeState] = useMutation(REMOVE_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (workItemId: string) => {\n setError(null);\n setRemovingId(workItemId);\n try {\n await throwOnError(\n remove({ variables: { id: sprintId, input: { workItemIds: [workItemId] } } })\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work item could not be removed.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"sprint-work-items\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {items.length} work item{items.length === 1 ? '' : 's'}\n </p>\n {!finished && <AddWorkItemsDialog sprintId={sprintId} projectId={projectId} />}\n </div>\n {finished && (\n <p className=\"text-xs text-muted-foreground\">\n This sprint is {statusLabel(status)}; its work items can no longer be changed.\n </p>\n )}\n {error && <ErrorLine message={error} />}\n {items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No work items in this sprint yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {items.map((item) => (\n <li\n key={item.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <span className=\"min-w-0 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n <span className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {statusLabel(item.status)}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">{item.storyPoints ?? 0} pts</span>\n {!finished && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${item.key} from sprint`}\n disabled={removeState.loading}\n onClick={() => handleRemove(item.id)}\n >\n {removingId === item.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n )}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddWorkItemsDialog({ sprintId, projectId }: { sprintId: string; projectId: string }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const deferredSearch = useDeferredValue(search.trim());\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetWorkItems: { totalCount: number; items: CandidateItem[] };\n }>(PROJECT_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { projectId, ...(deferredSearch ? { search: deferredSearch } : {}) },\n },\n skip: !open || !workspaceId,\n fetchPolicy: 'network-only',\n });\n const [add, addState] = useMutation(ADD_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const candidates = (data?.planmagnetWorkItems.items ?? []).filter(\n (item) => item.sprintId !== sprintId\n );\n const totalCount = data?.planmagnetWorkItems.totalCount ?? 0;\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setSearch('');\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await throwOnError(\n add({ variables: { id: sprintId, input: { workItemIds: [...selected] } } })\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work items could not be added.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add work items\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add work items</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Work items from this sprint's project. An item already in another sprint moves\n here.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search by title\"\n aria-label=\"Search work items\"\n />\n <div className=\"max-h-80 overflow-y-auto space-y-1\" data-testid=\"sprint-add-candidates\">\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n {deferredSearch\n ? 'No matching work items.'\n : 'Every work item in this project is already in this sprint.'}\n </p>\n )}\n {candidates.map((item) => {\n const inputId = `sprint-add-${item.id}`;\n return (\n <label\n key={item.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(item.id)}\n onCheckedChange={(checked) => toggle(item.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n {item.sprintId && (\n <span className=\"text-xs text-muted-foreground shrink-0\">in another sprint</span>\n )}\n <Badge variant=\"secondary\" className=\"text-xs capitalize shrink-0\">\n {statusLabel(item.status)}\n </Badge>\n </label>\n );\n })}\n {totalCount > 100 && (\n <p className=\"text-xs text-muted-foreground pt-2\">\n Showing the first 100 of {totalCount}. Search to narrow the list.\n </p>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={handleAdd}\n disabled={selected.size === 0 || addState.loading}\n data-testid=\"sprint-add-confirm\"\n >\n {addState.loading\n ? 'Adding…'\n : `Add ${selected.size} work item${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── New sprint ─────────────────────────────────────────────────────────────\n\nconst CREATE_SPRINT = gql`\n mutation SprintControlsCreate($input: PlanmagnetCreateSprintInput!) {\n planmagnetCreateSprint(input: $input) {\n id\n }\n }\n`;\nconst SPRINT_PROJECTS = gql`\n query SprintControlsProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n }\n }\n`;\n\nconst EMPTY_FORM = { name: '', projectId: '', startDate: '', endDate: '', goal: '', capacity: '' };\n\nexport function NewSprintDialog({ onCreated }: { onCreated: (sprintId: string) => void }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n\n const { data: projectsData } = useQuery<{\n planmagnetProjects: { items: Array<{ id: string; name: string }> };\n }>(SPRINT_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const projects = projectsData?.planmagnetProjects.items ?? [];\n const projectId = form.projectId || projects[0]?.id || '';\n\n const [create, { loading }] = useMutation<{ planmagnetCreateSprint: { id: string } }>(\n CREATE_SPRINT,\n { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true }\n );\n\n const capacity = form.capacity.trim() === '' ? undefined : Number(form.capacity);\n const invalidCapacity = capacity !== undefined && (!Number.isInteger(capacity) || capacity < 0);\n const invalidDates = Boolean(form.startDate && form.endDate && form.endDate < form.startDate);\n const canSubmit =\n form.name.trim() !== '' && projectId !== '' && !invalidCapacity && !invalidDates && !loading;\n\n const handleCreate = async () => {\n setError(null);\n try {\n const result = await throwOnError(\n create({\n variables: {\n input: {\n name: form.name.trim(),\n projectId,\n startDate: form.startDate || undefined,\n endDate: form.endDate || undefined,\n goal: form.goal.trim() || undefined,\n capacity,\n },\n },\n })\n );\n const id = result.data?.planmagnetCreateSprint.id;\n setForm(EMPTY_FORM);\n setOpen(false);\n if (id) onCreated(id);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be created.');\n }\n };\n\n const field = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\">\n <Plus className=\"mr-1 h-4 w-4\" />\n New sprint\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New sprint</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n The sprint starts in Planning. Start it when it is ready.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"new-sprint-name\" className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id=\"new-sprint-name\"\n className=\"mt-1\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <select\n id=\"new-sprint-project\"\n className={field}\n value={projectId}\n onChange={(e) => setForm({ ...form, projectId: e.target.value })}\n >\n {projects.length === 0 && <option value=\"\">No projects</option>}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"new-sprint-start\" className=\"text-sm font-medium\">\n Start date\n </label>\n <Input\n id=\"new-sprint-start\"\n type=\"date\"\n className=\"mt-1\"\n value={form.startDate}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-end\" className=\"text-sm font-medium\">\n End date\n </label>\n <Input\n id=\"new-sprint-end\"\n type=\"date\"\n className=\"mt-1\"\n value={form.endDate}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n {invalidDates && <ErrorLine message=\"The end date is before the start date.\" />}\n <div>\n <label htmlFor=\"new-sprint-capacity\" className=\"text-sm font-medium\">\n Capacity (story points)\n </label>\n <Input\n id=\"new-sprint-capacity\"\n type=\"number\"\n min={0}\n className=\"mt-1\"\n value={form.capacity}\n onChange={(e) => setForm({ ...form, capacity: e.target.value })}\n />\n </div>\n {invalidCapacity && <ErrorLine message=\"Capacity must be a whole number, 0 or more.\" />}\n <div>\n <label htmlFor=\"new-sprint-goal\" className=\"text-sm font-medium\">\n Goal\n </label>\n <Input\n id=\"new-sprint-goal\"\n className=\"mt-1\"\n value={form.goal}\n onChange={(e) => setForm({ ...form, goal: e.target.value })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button onClick={handleCreate} disabled={!canSubmit} data-testid=\"new-sprint-submit\">\n {loading ? 'Creating…' : 'Create sprint'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SprintControls.js","names":[],"sources":["../../../src/planmagnet/sprints/SprintControls.tsx"],"sourcesContent":["import { useDeferredValue, 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 Badge,\n Button,\n Checkbox,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, Play, Plus, X, XCircle } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Sprint lifecycle + membership controls.\n *\n * The buttons mirror the service's transition table exactly\n * (wspace-planmagnet-svc sprint.service.ts `TRANSITIONS`):\n * start PLANNING -> ACTIVE\n * complete ACTIVE | REVIEW -> COMPLETED\n * cancel PLANNING|ACTIVE|REVIEW -> CANCELLED\n * A COMPLETED or CANCELLED sprint is finished: no transitions, and its work\n * items can no longer be added or removed. The server enforces all of this;\n * the UI only hides what would be refused, and still surfaces a refusal (a\n * concurrent change can make a shown button stale).\n */\n\nexport type SprintStatus = 'PLANNING' | 'ACTIVE' | 'REVIEW' | 'COMPLETED' | 'CANCELLED';\nexport type SprintAction = 'start' | 'complete' | 'cancel';\n\nconst ALLOWED: Record<SprintAction, readonly SprintStatus[]> = {\n start: ['PLANNING'],\n complete: ['ACTIVE', 'REVIEW'],\n cancel: ['PLANNING', 'ACTIVE', 'REVIEW'],\n};\n\nexport function sprintActionsFor(status: string): SprintAction[] {\n return (Object.keys(ALLOWED) as SprintAction[]).filter((action) =>\n (ALLOWED[action] as readonly string[]).includes(status)\n );\n}\n\nexport function isSprintFinished(status: string): boolean {\n return status === 'COMPLETED' || status === 'CANCELLED';\n}\n\n/** Every query that renders sprint status or membership. */\nconst SPRINT_QUERIES = ['GetSprints', 'GetSprintDetail', 'GetSprintForEdit', 'GetDashboardData'];\n\nconst START_SPRINT = gql`\n mutation SprintControlsStart($id: ID!) {\n planmagnetStartSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst COMPLETE_SPRINT = gql`\n mutation SprintControlsComplete($id: ID!) {\n planmagnetCompleteSprint(id: $id) {\n id\n status\n }\n }\n`;\nconst CANCEL_SPRINT = gql`\n mutation SprintControlsCancel($id: ID!) {\n planmagnetCancelSprint(id: $id) {\n id\n status\n }\n }\n`;\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 SprintLifecycleActions({\n sprintId,\n sprintName,\n status,\n}: {\n sprintId: string;\n sprintName: string;\n status: string;\n}) {\n const options = { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true };\n const [start, startState] = useMutation(START_SPRINT, options);\n const [complete, completeState] = useMutation(COMPLETE_SPRINT, options);\n const [cancel, cancelState] = useMutation(CANCEL_SPRINT, options);\n const [error, setError] = useState<string | null>(null);\n\n const busy = startState.loading || completeState.loading || cancelState.loading;\n const actions = sprintActionsFor(status);\n // A refused transition still refetches; if the sprint turned out to be\n // finished there are no buttons left, but the reason must stay visible.\n if (actions.length === 0) return error ? <ErrorLine message={error} /> : null;\n\n const run = async (mutate: typeof start) => {\n setError(null);\n try {\n await throwOnError(mutate({ variables: { id: sprintId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be updated.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\" data-testid=\"sprint-lifecycle-actions\">\n <div className=\"flex gap-2\">\n {actions.includes('start') && (\n <Button size=\"sm\" onClick={() => run(start)} disabled={busy}>\n <Play className=\"mr-1 h-4 w-4\" />\n Start sprint\n </Button>\n )}\n {actions.includes('complete') && (\n <ConfirmButton\n label=\"Complete sprint\"\n icon={<CheckCircle2 className=\"mr-1 h-4 w-4\" />}\n title={`Complete ${sprintName}?`}\n description=\"The sprint is closed and its work items can no longer be changed from it. This cannot be undone.\"\n confirmLabel=\"Complete sprint\"\n disabled={busy}\n onConfirm={() => run(complete)}\n />\n )}\n {actions.includes('cancel') && (\n <ConfirmButton\n label=\"Cancel sprint\"\n variant=\"outline\"\n icon={<XCircle className=\"mr-1 h-4 w-4\" />}\n title={`Cancel ${sprintName}?`}\n description=\"The sprint is abandoned and cannot be restarted. Its work items stay in it for the record. This cannot be undone.\"\n confirmLabel=\"Cancel sprint\"\n destructive\n disabled={busy}\n onConfirm={() => run(cancel)}\n />\n )}\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nfunction ConfirmButton({\n label,\n icon,\n title,\n description,\n confirmLabel,\n onConfirm,\n disabled,\n destructive = false,\n variant = 'default',\n}: {\n label: string;\n icon: React.ReactNode;\n title: string;\n description: string;\n confirmLabel: string;\n onConfirm: () => void;\n disabled: boolean;\n destructive?: boolean;\n variant?: 'default' | 'outline';\n}) {\n return (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button size=\"sm\" variant={variant} disabled={disabled}>\n {icon}\n {label}\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription>{description}</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n onClick={onConfirm}\n className={\n destructive\n ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'\n : ''\n }\n >\n {confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n\n// ─── Work items in the sprint ──────────────────────────────────────────────\n\nconst ADD_ITEMS = gql`\n mutation SprintControlsAddItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetAddWorkItemsToSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst REMOVE_ITEMS = gql`\n mutation SprintControlsRemoveItems($id: ID!, $input: PlanmagnetSprintWorkItemsInput!) {\n planmagnetRemoveWorkItemsFromSprint(id: $id, input: $input) {\n id\n }\n }\n`;\nconst PROJECT_WORK_ITEMS = gql`\n query SprintControlsCandidates($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n totalCount\n items {\n id\n key\n title\n status\n storyPoints\n sprintId\n }\n }\n }\n`;\n\nexport interface SprintWorkItem {\n id: string;\n key: string;\n title?: string | null;\n status: string;\n storyPoints: number | null;\n}\n\ninterface CandidateItem extends SprintWorkItem {\n sprintId: string | null;\n}\n\nconst statusLabel = (status: string) => status.toLowerCase().replace(/_/g, ' ');\n\nexport function SprintWorkItemsPanel({\n sprintId,\n projectId,\n status,\n items,\n}: {\n sprintId: string;\n projectId: string;\n status: string;\n items: SprintWorkItem[];\n}) {\n const finished = isSprintFinished(status);\n const [remove, removeState] = useMutation(REMOVE_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (workItemId: string) => {\n setError(null);\n setRemovingId(workItemId);\n try {\n await throwOnError(\n remove({ variables: { id: sprintId, input: { workItemIds: [workItemId] } } })\n );\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work item could not be removed.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"sprint-work-items\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {items.length} work item{items.length === 1 ? '' : 's'}\n </p>\n {!finished && <AddWorkItemsDialog sprintId={sprintId} projectId={projectId} />}\n </div>\n {finished && (\n <p className=\"text-xs text-muted-foreground\">\n This sprint is {statusLabel(status)}; its work items can no longer be changed.\n </p>\n )}\n {error && <ErrorLine message={error} />}\n {items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No work items in this sprint yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {items.map((item) => (\n <li\n key={item.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <span className=\"min-w-0 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n <span className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {statusLabel(item.status)}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">{item.storyPoints ?? 0} pts</span>\n {!finished && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${item.key} from sprint`}\n disabled={removeState.loading}\n onClick={() => handleRemove(item.id)}\n >\n {removingId === item.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n )}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddWorkItemsDialog({ sprintId, projectId }: { sprintId: string; projectId: string }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const deferredSearch = useDeferredValue(search.trim());\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetWorkItems: { totalCount: number; items: CandidateItem[] };\n }>(PROJECT_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { projectId, ...(deferredSearch ? { search: deferredSearch } : {}) },\n },\n skip: !open || !workspaceId,\n fetchPolicy: 'network-only',\n });\n const [add, addState] = useMutation(ADD_ITEMS, {\n refetchQueries: SPRINT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const candidates = (data?.planmagnetWorkItems.items ?? []).filter(\n (item) => item.sprintId !== sprintId\n );\n const totalCount = data?.planmagnetWorkItems.totalCount ?? 0;\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setSearch('');\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await throwOnError(\n add({ variables: { id: sprintId, input: { workItemIds: [...selected] } } })\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The work items could not be added.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add work items\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add work items</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Work items from this sprint's project. An item already in another sprint moves\n here.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search by title\"\n aria-label=\"Search work items\"\n />\n <div className=\"max-h-80 overflow-y-auto space-y-1\" data-testid=\"sprint-add-candidates\">\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n {deferredSearch\n ? 'No matching work items.'\n : 'Every work item in this project is already in this sprint.'}\n </p>\n )}\n {candidates.map((item) => {\n const inputId = `sprint-add-${item.id}`;\n return (\n <label\n key={item.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(item.id)}\n onCheckedChange={(checked) => toggle(item.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground mr-2\">{item.key}</span>\n {item.title}\n </span>\n {item.sprintId && (\n <span className=\"text-xs text-muted-foreground shrink-0\">in another sprint</span>\n )}\n <Badge variant=\"secondary\" className=\"text-xs capitalize shrink-0\">\n {statusLabel(item.status)}\n </Badge>\n </label>\n );\n })}\n {totalCount > 100 && (\n <p className=\"text-xs text-muted-foreground pt-2\">\n Showing the first 100 of {totalCount}. Search to narrow the list.\n </p>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={handleAdd}\n disabled={selected.size === 0 || addState.loading}\n data-testid=\"sprint-add-confirm\"\n >\n {addState.loading\n ? 'Adding…'\n : `Add ${selected.size} work item${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── New sprint ─────────────────────────────────────────────────────────────\n\nconst CREATE_SPRINT = gql`\n mutation SprintControlsCreate($input: PlanmagnetCreateSprintInput!) {\n planmagnetCreateSprint(input: $input) {\n id\n }\n }\n`;\nconst SPRINT_PROJECTS = gql`\n query SprintControlsProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n }\n }\n`;\n\nconst EMPTY_FORM = { name: '', projectId: '', startDate: '', endDate: '', goal: '', capacity: '' };\n\nexport function NewSprintDialog({ onCreated }: { onCreated: (sprintId: string) => void }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(EMPTY_FORM);\n const [error, setError] = useState<string | null>(null);\n\n const { data: projectsData } = useQuery<{\n planmagnetProjects: { items: Array<{ id: string; name: string }> };\n }>(SPRINT_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const projects = projectsData?.planmagnetProjects.items ?? [];\n const projectId = form.projectId || projects[0]?.id || '';\n\n const [create, { loading }] = useMutation<{ planmagnetCreateSprint: { id: string } }>(\n CREATE_SPRINT,\n { refetchQueries: SPRINT_QUERIES, awaitRefetchQueries: true }\n );\n\n const capacity = form.capacity.trim() === '' ? undefined : Number(form.capacity);\n const invalidCapacity = capacity !== undefined && (!Number.isInteger(capacity) || capacity < 0);\n const invalidDates = Boolean(form.startDate && form.endDate && form.endDate < form.startDate);\n const canSubmit =\n form.name.trim() !== '' && projectId !== '' && !invalidCapacity && !invalidDates && !loading;\n\n const handleCreate = async () => {\n setError(null);\n try {\n const result = await throwOnError(\n create({\n variables: {\n input: {\n name: form.name.trim(),\n projectId,\n startDate: form.startDate || undefined,\n endDate: form.endDate || undefined,\n goal: form.goal.trim() || undefined,\n capacity,\n },\n },\n })\n );\n const id = result.data?.planmagnetCreateSprint.id;\n setForm(EMPTY_FORM);\n setOpen(false);\n if (id) onCreated(id);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The sprint could not be created.');\n }\n };\n\n const field = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\">\n <Plus className=\"mr-1 h-4 w-4\" />\n New sprint\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New sprint</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n The sprint starts in Planning. Start it when it is ready.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"new-sprint-name\" className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id=\"new-sprint-name\"\n className=\"mt-1\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-project\" className=\"text-sm font-medium\">\n Project\n </label>\n <select\n id=\"new-sprint-project\"\n className={field}\n value={projectId}\n onChange={(e) => setForm({ ...form, projectId: e.target.value })}\n >\n {projects.length === 0 && <option value=\"\">No projects</option>}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"new-sprint-start\" className=\"text-sm font-medium\">\n Start date\n </label>\n <Input\n id=\"new-sprint-start\"\n type=\"date\"\n className=\"mt-1\"\n value={form.startDate}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"new-sprint-end\" className=\"text-sm font-medium\">\n End date\n </label>\n <Input\n id=\"new-sprint-end\"\n type=\"date\"\n className=\"mt-1\"\n value={form.endDate}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n {invalidDates && <ErrorLine message=\"The end date is before the start date.\" />}\n <div>\n <label htmlFor=\"new-sprint-capacity\" className=\"text-sm font-medium\">\n Capacity (story points)\n </label>\n <Input\n id=\"new-sprint-capacity\"\n type=\"number\"\n min={0}\n className=\"mt-1\"\n value={form.capacity}\n onChange={(e) => setForm({ ...form, capacity: e.target.value })}\n />\n </div>\n {invalidCapacity && <ErrorLine message=\"Capacity must be a whole number, 0 or more.\" />}\n <div>\n <label htmlFor=\"new-sprint-goal\" className=\"text-sm font-medium\">\n Goal\n </label>\n <Input\n id=\"new-sprint-goal\"\n className=\"mt-1\"\n value={form.goal}\n onChange={(e) => setForm({ ...form, goal: e.target.value })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button onClick={handleCreate} disabled={!canSubmit} data-testid=\"new-sprint-submit\">\n {loading ? 'Creating…' : 'Create sprint'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|