@burdenoff/microfe-planmagnet 2026.925.16 → 2026.925.18
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/generated/wspace-types.d.ts +58 -0
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/node_modules/@apollo/client/errors/ServerError.js +17 -0
- package/dist/node_modules/@apollo/client/errors/ServerError.js.map +1 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +67 -62
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +104 -117
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/releases/ReleaseGatesCard.d.ts +7 -0
- package/dist/planmagnet/releases/ReleaseGatesCard.d.ts.map +1 -1
- package/dist/planmagnet/releases/ReleaseGatesCard.js +1 -1
- package/dist/planmagnet/releases/ReleaseGatesCard.js.map +1 -1
- package/dist/planmagnet/releases/ReleaseReadinessCard.d.ts +7 -0
- package/dist/planmagnet/releases/ReleaseReadinessCard.d.ts.map +1 -0
- package/dist/planmagnet/releases/ReleaseReadinessCard.js +391 -0
- package/dist/planmagnet/releases/ReleaseReadinessCard.js.map +1 -0
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts +13 -0
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts.map +1 -0
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.js +189 -0
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { CombinedGraphQLErrors as e } from "../../node_modules/@apollo/client/errors/CombinedGraphQLErrors.js";
|
|
2
|
+
import { ServerError as t } from "../../node_modules/@apollo/client/errors/ServerError.js";
|
|
3
|
+
import { usePlanMagnet as n, usePlanMagnetNavigate as r } from "../providers/PlanMagnetProvider.js";
|
|
4
|
+
import { RELEASE_GATES as i } from "./ReleaseGatesCard.js";
|
|
5
|
+
import { useState as a } from "react";
|
|
6
|
+
import { AlertTriangle as o, CheckCircle2 as s, Clock as c, Link2 as l, ShieldAlert as u, Users as d, Wallet as f } from "lucide-react";
|
|
7
|
+
import { gql as p } from "@apollo/client";
|
|
8
|
+
import { useQuery as m } from "@apollo/client/react";
|
|
9
|
+
import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
10
|
+
import { Card as v, CardContent as y, CardHeader as b, CardTitle as x } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/planmagnet/releases/ReleaseReadinessCard.tsx
|
|
12
|
+
var S = p`
|
|
13
|
+
query ReleaseReadiness($releaseId: ID!) {
|
|
14
|
+
planmagnetReleaseReadiness(releaseId: $releaseId) {
|
|
15
|
+
scope
|
|
16
|
+
totalItems
|
|
17
|
+
doneItems
|
|
18
|
+
openWorkItemIds
|
|
19
|
+
blockers {
|
|
20
|
+
workItem {
|
|
21
|
+
id
|
|
22
|
+
key
|
|
23
|
+
title
|
|
24
|
+
status
|
|
25
|
+
}
|
|
26
|
+
waitingOn {
|
|
27
|
+
id
|
|
28
|
+
key
|
|
29
|
+
title
|
|
30
|
+
status
|
|
31
|
+
projectId
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
`, C = p`
|
|
37
|
+
query ReleaseBudgets($workspaceId: ID!, $projectId: String!) {
|
|
38
|
+
planmagnetBudgets(
|
|
39
|
+
workspaceId: $workspaceId
|
|
40
|
+
filter: { projectId: $projectId }
|
|
41
|
+
pagination: { first: 50 }
|
|
42
|
+
) {
|
|
43
|
+
items {
|
|
44
|
+
id
|
|
45
|
+
name
|
|
46
|
+
status
|
|
47
|
+
currency
|
|
48
|
+
plannedAmount
|
|
49
|
+
spentAmount
|
|
50
|
+
remainingAmount
|
|
51
|
+
approvedAt
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`, w = p`
|
|
56
|
+
query ReleaseRisks($workspaceId: ID!, $projectId: String!) {
|
|
57
|
+
planmagnetRisks(
|
|
58
|
+
workspaceId: $workspaceId
|
|
59
|
+
filter: { projectId: $projectId }
|
|
60
|
+
pagination: { first: 100 }
|
|
61
|
+
) {
|
|
62
|
+
items {
|
|
63
|
+
id
|
|
64
|
+
title
|
|
65
|
+
severity
|
|
66
|
+
status
|
|
67
|
+
riskScore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`, T = p`
|
|
72
|
+
query ReleaseCustomerRequests($workspaceId: ID!, $projectId: String!) {
|
|
73
|
+
planmagnetFeatureRequests(
|
|
74
|
+
workspaceId: $workspaceId
|
|
75
|
+
filter: { projectId: $projectId }
|
|
76
|
+
pagination: { first: 100 }
|
|
77
|
+
) {
|
|
78
|
+
items {
|
|
79
|
+
id
|
|
80
|
+
title
|
|
81
|
+
status
|
|
82
|
+
voteCount
|
|
83
|
+
workItemId
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`, E = new Set([
|
|
88
|
+
"RESOLVED",
|
|
89
|
+
"ACCEPTED",
|
|
90
|
+
"CLOSED"
|
|
91
|
+
]), D = new Set([
|
|
92
|
+
"COMPLETED",
|
|
93
|
+
"DECLINED",
|
|
94
|
+
"DUPLICATE"
|
|
95
|
+
]), O = 1440 * 60 * 1e3;
|
|
96
|
+
function k(n) {
|
|
97
|
+
return e.is(n) ? n.errors.some((e) => e.extensions?.code === "FORBIDDEN") : t.is(n) ? n.statusCode === 403 || /"code"\s*:\s*"FORBIDDEN"/.test(n.bodyText) : !1;
|
|
98
|
+
}
|
|
99
|
+
function A({ icon: e, title: t, testId: n, loading: r, error: i, children: a }) {
|
|
100
|
+
let o = a;
|
|
101
|
+
return i ? o = /* @__PURE__ */ g("p", {
|
|
102
|
+
className: "text-sm text-muted-foreground",
|
|
103
|
+
children: k(i) ? "Your role cannot see this." : `Could not be loaded: ${i instanceof Error ? i.message : "unknown error"}`
|
|
104
|
+
}) : r && (o = /* @__PURE__ */ g("p", {
|
|
105
|
+
className: "text-sm text-muted-foreground",
|
|
106
|
+
children: "Loading…"
|
|
107
|
+
})), /* @__PURE__ */ _("section", {
|
|
108
|
+
className: "space-y-1.5",
|
|
109
|
+
"data-testid": n,
|
|
110
|
+
children: [/* @__PURE__ */ _("h3", {
|
|
111
|
+
className: "flex items-center gap-2 text-sm font-medium",
|
|
112
|
+
children: [e, t]
|
|
113
|
+
}), o]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
var j = (e, t) => {
|
|
117
|
+
try {
|
|
118
|
+
return new Intl.NumberFormat(void 0, {
|
|
119
|
+
style: "currency",
|
|
120
|
+
currency: t
|
|
121
|
+
}).format(e);
|
|
122
|
+
} catch {
|
|
123
|
+
return `${e.toFixed(2)} ${t}`;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
function M({ releaseId: e, projectId: t, releaseStatus: p, plannedDate: k }) {
|
|
127
|
+
let { workspaceId: M } = n(), [N] = a(() => Date.now()), P = r(), F = {
|
|
128
|
+
variables: {
|
|
129
|
+
workspaceId: M,
|
|
130
|
+
projectId: t
|
|
131
|
+
},
|
|
132
|
+
skip: !M || !t
|
|
133
|
+
}, I = m(S, { variables: { releaseId: e } }), L = m(i, { variables: { id: e } }), R = m(C, F), z = m(w, F), B = m(T, F), V = p === "RELEASED", H = I.data?.planmagnetReleaseReadiness, U = (L.data?.planmagnetRelease?.qualityGates ?? []).filter((e) => e.status !== "PASSED" && e.status !== "WAIVED"), W = R.data?.planmagnetBudgets.items ?? [], G = (z.data?.planmagnetRisks.items ?? []).filter((e) => !E.has(e.status)).sort((e, t) => t.riskScore - e.riskScore), K = new Set(H?.openWorkItemIds ?? []), q = new Set((H?.blockers ?? []).map((e) => e.workItem.id)), J = (B.data?.planmagnetFeatureRequests.items ?? []).filter((e) => e.workItemId && K.has(e.workItemId) && !D.has(e.status)), Y = k && !V ? Math.floor((N - new Date(k).getTime()) / O) : null, X = [];
|
|
134
|
+
Y !== null && Y > 0 && X.push(`${Y} day${Y === 1 ? "" : "s"} past its planned date`), H && H.blockers.length > 0 && X.push(`${H.blockers.length} blocked item${H.blockers.length === 1 ? "" : "s"}`), H && H.openWorkItemIds.length > 0 && X.push(`${H.openWorkItemIds.length} unfinished item${H.openWorkItemIds.length === 1 ? "" : "s"}`), U.length > 0 && X.push(`${U.length} gate${U.length === 1 ? "" : "s"} not passed`), W.filter((e) => e.remainingAmount < 0 || e.status === "EXCEEDED").length > 0 && X.push("over budget");
|
|
135
|
+
let Z = !I.loading && !L.loading;
|
|
136
|
+
return /* @__PURE__ */ _(v, {
|
|
137
|
+
"data-testid": "release-readiness",
|
|
138
|
+
children: [/* @__PURE__ */ g(b, { children: /* @__PURE__ */ g(x, { children: "What stands between this release and shipping" }) }), /* @__PURE__ */ _(y, {
|
|
139
|
+
className: "space-y-5",
|
|
140
|
+
children: [
|
|
141
|
+
V ? /* @__PURE__ */ _("p", {
|
|
142
|
+
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
143
|
+
children: [/* @__PURE__ */ g(s, { className: "h-4 w-4" }), " Released."]
|
|
144
|
+
}) : Z && X.length === 0 && H && H.scope !== "NONE" ? /* @__PURE__ */ _("p", {
|
|
145
|
+
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
146
|
+
"data-testid": "readiness-headline",
|
|
147
|
+
children: [/* @__PURE__ */ g(s, { className: "h-4 w-4" }), " Nothing is holding this release."]
|
|
148
|
+
}) : Z && X.length > 0 ? /* @__PURE__ */ _("p", {
|
|
149
|
+
className: "flex items-center gap-2 text-sm text-status-warning-text",
|
|
150
|
+
"data-testid": "readiness-headline",
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ g(o, { className: "h-4 w-4 shrink-0" }),
|
|
153
|
+
" Held by: ",
|
|
154
|
+
X.join(", "),
|
|
155
|
+
"."
|
|
156
|
+
]
|
|
157
|
+
}) : null,
|
|
158
|
+
/* @__PURE__ */ g(A, {
|
|
159
|
+
icon: /* @__PURE__ */ g(l, { className: "h-4 w-4" }),
|
|
160
|
+
title: "Work and blockers",
|
|
161
|
+
testId: "readiness-work",
|
|
162
|
+
loading: I.loading && !I.data,
|
|
163
|
+
error: I.error,
|
|
164
|
+
children: H && /* @__PURE__ */ _("div", {
|
|
165
|
+
className: "space-y-2",
|
|
166
|
+
children: [
|
|
167
|
+
/* @__PURE__ */ g("p", {
|
|
168
|
+
className: "text-sm text-muted-foreground",
|
|
169
|
+
children: H.scope === "NONE" ? "This release has no work items and no project, so there is nothing to track." : H.scope === "PROJECTS" ? `No work items are assigned to this release, so everything open in its project counts: ${H.totalItems} open.` : `${H.doneItems} of ${H.totalItems} work items done.`
|
|
170
|
+
}),
|
|
171
|
+
H.blockers.length === 0 && H.scope !== "NONE" && /* @__PURE__ */ g("p", {
|
|
172
|
+
className: "text-sm text-muted-foreground",
|
|
173
|
+
children: "No item is waiting on unfinished work."
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ g("ul", {
|
|
176
|
+
className: "space-y-1.5",
|
|
177
|
+
children: H.blockers.map((e) => /* @__PURE__ */ _("li", {
|
|
178
|
+
className: "rounded-md border p-2 text-sm",
|
|
179
|
+
"data-testid": "readiness-blocker",
|
|
180
|
+
children: [
|
|
181
|
+
/* @__PURE__ */ _("button", {
|
|
182
|
+
type: "button",
|
|
183
|
+
className: "font-medium hover:underline text-left",
|
|
184
|
+
onClick: () => P(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.workItem.id)}`),
|
|
185
|
+
children: [
|
|
186
|
+
e.workItem.key,
|
|
187
|
+
" ",
|
|
188
|
+
e.workItem.title
|
|
189
|
+
]
|
|
190
|
+
}),
|
|
191
|
+
/* @__PURE__ */ g("span", {
|
|
192
|
+
className: "text-muted-foreground",
|
|
193
|
+
children: " waits on "
|
|
194
|
+
}),
|
|
195
|
+
e.waitingOn.map((e, n) => /* @__PURE__ */ _("span", { children: [
|
|
196
|
+
n > 0 && ", ",
|
|
197
|
+
/* @__PURE__ */ _("button", {
|
|
198
|
+
type: "button",
|
|
199
|
+
className: "hover:underline",
|
|
200
|
+
onClick: () => P(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
|
|
201
|
+
children: [
|
|
202
|
+
e.key,
|
|
203
|
+
" ",
|
|
204
|
+
e.title
|
|
205
|
+
]
|
|
206
|
+
}),
|
|
207
|
+
" ",
|
|
208
|
+
/* @__PURE__ */ _("span", {
|
|
209
|
+
className: "text-xs text-muted-foreground",
|
|
210
|
+
children: [
|
|
211
|
+
"(",
|
|
212
|
+
e.status.toLowerCase().replace("_", " "),
|
|
213
|
+
t && e.projectId && e.projectId !== t ? ", another project" : "",
|
|
214
|
+
")"
|
|
215
|
+
]
|
|
216
|
+
})
|
|
217
|
+
] }, e.id))
|
|
218
|
+
]
|
|
219
|
+
}, e.workItem.id))
|
|
220
|
+
})
|
|
221
|
+
]
|
|
222
|
+
})
|
|
223
|
+
}),
|
|
224
|
+
/* @__PURE__ */ g(A, {
|
|
225
|
+
icon: /* @__PURE__ */ g(u, { className: "h-4 w-4" }),
|
|
226
|
+
title: "Approvals",
|
|
227
|
+
testId: "readiness-approvals",
|
|
228
|
+
loading: L.loading && !L.data,
|
|
229
|
+
error: L.error,
|
|
230
|
+
children: /* @__PURE__ */ g("p", {
|
|
231
|
+
className: "text-sm text-muted-foreground",
|
|
232
|
+
children: U.length === 0 ? "No quality gate is waiting for a decision." : `Waiting on: ${U.map((e) => `${e.name} (${e.status.toLowerCase().replace("_", " ")})`).join(", ")}.`
|
|
233
|
+
})
|
|
234
|
+
}),
|
|
235
|
+
t ? /* @__PURE__ */ _(h, { children: [
|
|
236
|
+
/* @__PURE__ */ g(A, {
|
|
237
|
+
icon: /* @__PURE__ */ g(f, { className: "h-4 w-4" }),
|
|
238
|
+
title: "Budget",
|
|
239
|
+
testId: "readiness-budget",
|
|
240
|
+
loading: R.loading && !R.data,
|
|
241
|
+
error: R.error,
|
|
242
|
+
children: W.length === 0 ? /* @__PURE__ */ g("p", {
|
|
243
|
+
className: "text-sm text-muted-foreground",
|
|
244
|
+
children: "This release's project has no budget."
|
|
245
|
+
}) : /* @__PURE__ */ g("ul", {
|
|
246
|
+
className: "space-y-1",
|
|
247
|
+
children: W.map((e) => {
|
|
248
|
+
let t = e.remainingAmount < 0;
|
|
249
|
+
return /* @__PURE__ */ _("li", {
|
|
250
|
+
className: "text-sm",
|
|
251
|
+
"data-testid": "readiness-budget-row",
|
|
252
|
+
children: [
|
|
253
|
+
/* @__PURE__ */ g("span", {
|
|
254
|
+
className: "font-medium",
|
|
255
|
+
children: e.name
|
|
256
|
+
}),
|
|
257
|
+
":",
|
|
258
|
+
" ",
|
|
259
|
+
j(e.spentAmount, e.currency),
|
|
260
|
+
" of ",
|
|
261
|
+
j(e.plannedAmount, e.currency),
|
|
262
|
+
" ",
|
|
263
|
+
"spent",
|
|
264
|
+
t ? /* @__PURE__ */ _("span", {
|
|
265
|
+
className: "text-status-error-text",
|
|
266
|
+
children: [
|
|
267
|
+
" ",
|
|
268
|
+
"— over by ",
|
|
269
|
+
j(-e.remainingAmount, e.currency)
|
|
270
|
+
]
|
|
271
|
+
}) : /* @__PURE__ */ _("span", {
|
|
272
|
+
className: "text-muted-foreground",
|
|
273
|
+
children: [
|
|
274
|
+
" ",
|
|
275
|
+
"— ",
|
|
276
|
+
j(e.remainingAmount, e.currency),
|
|
277
|
+
" left"
|
|
278
|
+
]
|
|
279
|
+
}),
|
|
280
|
+
!e.approvedAt && /* @__PURE__ */ g("span", {
|
|
281
|
+
className: "text-status-warning-text",
|
|
282
|
+
children: " · not approved"
|
|
283
|
+
})
|
|
284
|
+
]
|
|
285
|
+
}, e.id);
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
}),
|
|
289
|
+
/* @__PURE__ */ g(A, {
|
|
290
|
+
icon: /* @__PURE__ */ g(o, { className: "h-4 w-4" }),
|
|
291
|
+
title: "Open risks",
|
|
292
|
+
testId: "readiness-risks",
|
|
293
|
+
loading: z.loading && !z.data,
|
|
294
|
+
error: z.error,
|
|
295
|
+
children: G.length === 0 ? /* @__PURE__ */ g("p", {
|
|
296
|
+
className: "text-sm text-muted-foreground",
|
|
297
|
+
children: "No open risks on this release's project."
|
|
298
|
+
}) : /* @__PURE__ */ _("ul", {
|
|
299
|
+
className: "space-y-1",
|
|
300
|
+
children: [G.slice(0, 5).map((e) => /* @__PURE__ */ _("li", {
|
|
301
|
+
className: "text-sm",
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ g("span", {
|
|
304
|
+
className: "font-medium",
|
|
305
|
+
children: e.title
|
|
306
|
+
}),
|
|
307
|
+
" ",
|
|
308
|
+
/* @__PURE__ */ _("span", {
|
|
309
|
+
className: "text-xs text-muted-foreground",
|
|
310
|
+
children: [
|
|
311
|
+
"(",
|
|
312
|
+
e.severity.toLowerCase(),
|
|
313
|
+
", score ",
|
|
314
|
+
e.riskScore,
|
|
315
|
+
",",
|
|
316
|
+
" ",
|
|
317
|
+
e.status.toLowerCase(),
|
|
318
|
+
")"
|
|
319
|
+
]
|
|
320
|
+
})
|
|
321
|
+
]
|
|
322
|
+
}, e.id)), G.length > 5 && /* @__PURE__ */ _("li", {
|
|
323
|
+
className: "text-xs text-muted-foreground",
|
|
324
|
+
children: [
|
|
325
|
+
"and ",
|
|
326
|
+
G.length - 5,
|
|
327
|
+
" more"
|
|
328
|
+
]
|
|
329
|
+
})]
|
|
330
|
+
})
|
|
331
|
+
}),
|
|
332
|
+
/* @__PURE__ */ g(A, {
|
|
333
|
+
icon: /* @__PURE__ */ g(d, { className: "h-4 w-4" }),
|
|
334
|
+
title: "Customer requests riding on unfinished work",
|
|
335
|
+
testId: "readiness-requests",
|
|
336
|
+
loading: B.loading && !B.data,
|
|
337
|
+
error: B.error,
|
|
338
|
+
children: J.length === 0 ? /* @__PURE__ */ g("p", {
|
|
339
|
+
className: "text-sm text-muted-foreground",
|
|
340
|
+
children: "No open customer request is tied to unfinished work in this release."
|
|
341
|
+
}) : /* @__PURE__ */ g("ul", {
|
|
342
|
+
className: "space-y-1",
|
|
343
|
+
children: J.map((e) => /* @__PURE__ */ _("li", {
|
|
344
|
+
className: "text-sm",
|
|
345
|
+
"data-testid": "readiness-request",
|
|
346
|
+
children: [
|
|
347
|
+
/* @__PURE__ */ g("span", {
|
|
348
|
+
className: "font-medium",
|
|
349
|
+
children: e.title
|
|
350
|
+
}),
|
|
351
|
+
" ",
|
|
352
|
+
/* @__PURE__ */ _("span", {
|
|
353
|
+
className: "text-xs text-muted-foreground",
|
|
354
|
+
children: [
|
|
355
|
+
"(",
|
|
356
|
+
e.voteCount,
|
|
357
|
+
" vote",
|
|
358
|
+
e.voteCount === 1 ? "" : "s",
|
|
359
|
+
")"
|
|
360
|
+
]
|
|
361
|
+
}),
|
|
362
|
+
e.workItemId && q.has(e.workItemId) && /* @__PURE__ */ g("span", {
|
|
363
|
+
className: "text-status-error-text",
|
|
364
|
+
children: " — its work is blocked"
|
|
365
|
+
})
|
|
366
|
+
]
|
|
367
|
+
}, e.id))
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
] }) : /* @__PURE__ */ g("p", {
|
|
371
|
+
className: "text-sm text-muted-foreground",
|
|
372
|
+
children: "This release has no project, so budget, risks and customer requests cannot be shown."
|
|
373
|
+
}),
|
|
374
|
+
k && Y !== null && /* @__PURE__ */ _("p", {
|
|
375
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
376
|
+
children: [
|
|
377
|
+
/* @__PURE__ */ g(c, { className: "h-3.5 w-3.5" }),
|
|
378
|
+
"Planned for ",
|
|
379
|
+
new Date(k).toLocaleDateString(),
|
|
380
|
+
Y > 0 ? ` (${Y} day${Y === 1 ? "" : "s"} ago)` : "",
|
|
381
|
+
"."
|
|
382
|
+
]
|
|
383
|
+
})
|
|
384
|
+
]
|
|
385
|
+
})]
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
export { M as ReleaseReadinessCard };
|
|
390
|
+
|
|
391
|
+
//# sourceMappingURL=ReleaseReadinessCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReleaseReadinessCard.js","names":[],"sources":["../../../src/planmagnet/releases/ReleaseReadinessCard.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { CombinedGraphQLErrors, ServerError } from '@apollo/client/errors';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n CheckCircle2,\n Link2,\n Wallet,\n Users,\n ShieldAlert,\n Clock,\n} from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { RELEASE_GATES } from './ReleaseGatesCard';\n\n/**\n * Release overview (P1.4): what stands between this release and shipping, in\n * one place: unfinished and dependency-blocked work, gates not yet passed,\n * the budget position of its project, its open risks, and the customer\n * requests that ride on its unfinished work.\n *\n * Each part is read through its OWN gated query. Budgets, risks and feature\n * requests are separate RBAC resources (a product manager can read releases\n * but not budgets), so a part the viewer may not see says so, instead of the\n * release leaking it or the whole card failing.\n */\n\nconst READINESS = gql`\n query ReleaseReadiness($releaseId: ID!) {\n planmagnetReleaseReadiness(releaseId: $releaseId) {\n scope\n totalItems\n doneItems\n openWorkItemIds\n blockers {\n workItem {\n id\n key\n title\n status\n }\n waitingOn {\n id\n key\n title\n status\n projectId\n }\n }\n }\n }\n`;\nconst BUDGETS = gql`\n query ReleaseBudgets($workspaceId: ID!, $projectId: String!) {\n planmagnetBudgets(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 50 }\n ) {\n items {\n id\n name\n status\n currency\n plannedAmount\n spentAmount\n remainingAmount\n approvedAt\n }\n }\n }\n`;\nconst RISKS = gql`\n query ReleaseRisks($workspaceId: ID!, $projectId: String!) {\n planmagnetRisks(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n title\n severity\n status\n riskScore\n }\n }\n }\n`;\nconst REQUESTS = gql`\n query ReleaseCustomerRequests($workspaceId: ID!, $projectId: String!) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n title\n status\n voteCount\n workItemId\n }\n }\n }\n`;\n\ninterface ItemRef {\n id: string;\n key: string;\n title: string;\n status: string;\n projectId?: string;\n}\ninterface Readiness {\n scope: 'WORK_ITEMS' | 'PROJECTS' | 'NONE';\n totalItems: number;\n doneItems: number;\n openWorkItemIds: string[];\n blockers: { workItem: ItemRef; waitingOn: ItemRef[] }[];\n}\ninterface Budget {\n id: string;\n name: string;\n status: string;\n currency: string;\n plannedAmount: number;\n spentAmount: number;\n remainingAmount: number;\n approvedAt: string | null;\n}\ninterface Risk {\n id: string;\n title: string;\n severity: string;\n status: string;\n riskScore: number;\n}\ninterface Request {\n id: string;\n title: string;\n status: string;\n voteCount: number;\n workItemId: string | null;\n}\n\nconst CLOSED_RISK = new Set(['RESOLVED', 'ACCEPTED', 'CLOSED']);\nconst CLOSED_REQUEST = new Set(['COMPLETED', 'DECLINED', 'DUPLICATE']);\nconst DAY = 24 * 60 * 60 * 1000;\n\n/**\n * Is this the gateway refusing the viewer's role? The gateway answers an RBAC\n * denial with HTTP 403 (400 inside a batch) and `extensions.code: FORBIDDEN`.\n * Depending on the fetch path Apollo surfaces that as GraphQL errors or as a\n * ServerError carrying the body, so both are read.\n */\nfunction denied(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'FORBIDDEN');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 403 || /\"code\"\\s*:\\s*\"FORBIDDEN\"/.test(error.bodyText);\n }\n return false;\n}\n\n/** One part of the overview: its own title, and its own loading/denied/error state. */\nfunction Part({\n icon,\n title,\n testId,\n loading,\n error,\n children,\n}: {\n icon: ReactNode;\n title: string;\n testId: string;\n loading: boolean;\n error: unknown;\n children: ReactNode;\n}) {\n let body = children;\n if (error) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {denied(error)\n ? 'Your role cannot see this.'\n : `Could not be loaded: ${error instanceof Error ? error.message : 'unknown error'}`}\n </p>\n );\n } else if (loading) {\n body = <p className=\"text-sm text-muted-foreground\">Loading…</p>;\n }\n return (\n <section className=\"space-y-1.5\" data-testid={testId}>\n <h3 className=\"flex items-center gap-2 text-sm font-medium\">\n {icon}\n {title}\n </h3>\n {body}\n </section>\n );\n}\n\nconst money = (amount: number, currency: string) => {\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);\n } catch {\n return `${amount.toFixed(2)} ${currency}`;\n }\n};\n\nexport function ReleaseReadinessCard({\n releaseId,\n projectId,\n releaseStatus,\n plannedDate,\n}: {\n releaseId: string;\n projectId: string | null;\n releaseStatus: string;\n plannedDate: string | null;\n}) {\n const { workspaceId } = usePlanMagnet();\n // Read once: lateness is a property of the page view, not of each render.\n const [now] = useState(() => Date.now());\n const navigate = usePlanMagnetNavigate();\n const byProject = { variables: { workspaceId, projectId }, skip: !workspaceId || !projectId };\n\n const readiness = useQuery<{ planmagnetReleaseReadiness: Readiness }>(READINESS, {\n variables: { releaseId },\n });\n const gates = useQuery<{\n planmagnetRelease: { qualityGates: { id: string; name: string; status: string }[] } | null;\n }>(RELEASE_GATES, { variables: { id: releaseId } });\n const budgets = useQuery<{ planmagnetBudgets: { items: Budget[] } }>(BUDGETS, byProject);\n const risks = useQuery<{ planmagnetRisks: { items: Risk[] } }>(RISKS, byProject);\n const requests = useQuery<{ planmagnetFeatureRequests: { items: Request[] } }>(\n REQUESTS,\n byProject\n );\n\n const shipped = releaseStatus === 'RELEASED';\n const r = readiness.data?.planmagnetReleaseReadiness;\n const openGates = (gates.data?.planmagnetRelease?.qualityGates ?? []).filter(\n (g) => g.status !== 'PASSED' && g.status !== 'WAIVED'\n );\n const budgetRows = budgets.data?.planmagnetBudgets.items ?? [];\n const openRisks = (risks.data?.planmagnetRisks.items ?? [])\n .filter((risk) => !CLOSED_RISK.has(risk.status))\n .sort((a, b) => b.riskScore - a.riskScore);\n const openIds = new Set(r?.openWorkItemIds ?? []);\n const blockedIds = new Set((r?.blockers ?? []).map((b) => b.workItem.id));\n const riding = (requests.data?.planmagnetFeatureRequests.items ?? []).filter(\n (req) => req.workItemId && openIds.has(req.workItemId) && !CLOSED_REQUEST.has(req.status)\n );\n\n const daysLate =\n plannedDate && !shipped ? Math.floor((now - new Date(plannedDate).getTime()) / DAY) : null;\n\n // The headline lists every reason, not just the first one found.\n const reasons: string[] = [];\n if (daysLate !== null && daysLate > 0)\n reasons.push(`${daysLate} day${daysLate === 1 ? '' : 's'} past its planned date`);\n if (r && r.blockers.length > 0)\n reasons.push(`${r.blockers.length} blocked item${r.blockers.length === 1 ? '' : 's'}`);\n if (r && r.openWorkItemIds.length > 0)\n reasons.push(\n `${r.openWorkItemIds.length} unfinished item${r.openWorkItemIds.length === 1 ? '' : 's'}`\n );\n if (openGates.length > 0)\n reasons.push(`${openGates.length} gate${openGates.length === 1 ? '' : 's'} not passed`);\n const overBudget = budgetRows.filter((b) => b.remainingAmount < 0 || b.status === 'EXCEEDED');\n if (overBudget.length > 0) reasons.push('over budget');\n const settled = !readiness.loading && !gates.loading;\n\n return (\n <Card data-testid=\"release-readiness\">\n <CardHeader>\n <CardTitle>What stands between this release and shipping</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {shipped ? (\n <p className=\"flex items-center gap-2 text-sm text-status-success-text\">\n <CheckCircle2 className=\"h-4 w-4\" /> Released.\n </p>\n ) : settled && reasons.length === 0 && r && r.scope !== 'NONE' ? (\n <p\n className=\"flex items-center gap-2 text-sm text-status-success-text\"\n data-testid=\"readiness-headline\"\n >\n <CheckCircle2 className=\"h-4 w-4\" /> Nothing is holding this release.\n </p>\n ) : settled && reasons.length > 0 ? (\n <p\n className=\"flex items-center gap-2 text-sm text-status-warning-text\"\n data-testid=\"readiness-headline\"\n >\n <AlertTriangle className=\"h-4 w-4 shrink-0\" /> Held by: {reasons.join(', ')}.\n </p>\n ) : null}\n\n <Part\n icon={<Link2 className=\"h-4 w-4\" />}\n title=\"Work and blockers\"\n testId=\"readiness-work\"\n loading={readiness.loading && !readiness.data}\n error={readiness.error}\n >\n {r && (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">\n {r.scope === 'NONE'\n ? 'This release has no work items and no project, so there is nothing to track.'\n : r.scope === 'PROJECTS'\n ? `No work items are assigned to this release, so everything open in its project counts: ${r.totalItems} open.`\n : `${r.doneItems} of ${r.totalItems} work items done.`}\n </p>\n {r.blockers.length === 0 && r.scope !== 'NONE' && (\n <p className=\"text-sm text-muted-foreground\">\n No item is waiting on unfinished work.\n </p>\n )}\n <ul className=\"space-y-1.5\">\n {r.blockers.map((b) => (\n <li\n key={b.workItem.id}\n className=\"rounded-md border p-2 text-sm\"\n data-testid=\"readiness-blocker\"\n >\n <button\n type=\"button\"\n className=\"font-medium hover:underline text-left\"\n onClick={() =>\n navigate(\n `/tasks/task-detail-page?taskId=${encodeURIComponent(b.workItem.id)}`\n )\n }\n >\n {b.workItem.key} {b.workItem.title}\n </button>\n <span className=\"text-muted-foreground\"> waits on </span>\n {b.waitingOn.map((w, i) => (\n <span key={w.id}>\n {i > 0 && ', '}\n <button\n type=\"button\"\n className=\"hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(w.id)}`)\n }\n >\n {w.key} {w.title}\n </button>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({w.status.toLowerCase().replace('_', ' ')}\n {projectId && w.projectId && w.projectId !== projectId\n ? ', another project'\n : ''}\n )\n </span>\n </span>\n ))}\n </li>\n ))}\n </ul>\n </div>\n )}\n </Part>\n\n <Part\n icon={<ShieldAlert className=\"h-4 w-4\" />}\n title=\"Approvals\"\n testId=\"readiness-approvals\"\n loading={gates.loading && !gates.data}\n error={gates.error}\n >\n <p className=\"text-sm text-muted-foreground\">\n {openGates.length === 0\n ? 'No quality gate is waiting for a decision.'\n : `Waiting on: ${openGates.map((g) => `${g.name} (${g.status.toLowerCase().replace('_', ' ')})`).join(', ')}.`}\n </p>\n </Part>\n\n {projectId ? (\n <>\n <Part\n icon={<Wallet className=\"h-4 w-4\" />}\n title=\"Budget\"\n testId=\"readiness-budget\"\n loading={budgets.loading && !budgets.data}\n error={budgets.error}\n >\n {budgetRows.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n This release's project has no budget.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {budgetRows.map((b) => {\n const over = b.remainingAmount < 0;\n return (\n <li key={b.id} className=\"text-sm\" data-testid=\"readiness-budget-row\">\n <span className=\"font-medium\">{b.name}</span>:{' '}\n {money(b.spentAmount, b.currency)} of {money(b.plannedAmount, b.currency)}{' '}\n spent\n {over ? (\n <span className=\"text-status-error-text\">\n {' '}\n — over by {money(-b.remainingAmount, b.currency)}\n </span>\n ) : (\n <span className=\"text-muted-foreground\">\n {' '}\n — {money(b.remainingAmount, b.currency)} left\n </span>\n )}\n {!b.approvedAt && (\n <span className=\"text-status-warning-text\"> · not approved</span>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </Part>\n\n <Part\n icon={<AlertTriangle className=\"h-4 w-4\" />}\n title=\"Open risks\"\n testId=\"readiness-risks\"\n loading={risks.loading && !risks.data}\n error={risks.error}\n >\n {openRisks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No open risks on this release's project.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {openRisks.slice(0, 5).map((risk) => (\n <li key={risk.id} className=\"text-sm\">\n <span className=\"font-medium\">{risk.title}</span>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({risk.severity.toLowerCase()}, score {risk.riskScore},{' '}\n {risk.status.toLowerCase()})\n </span>\n </li>\n ))}\n {openRisks.length > 5 && (\n <li className=\"text-xs text-muted-foreground\">\n and {openRisks.length - 5} more\n </li>\n )}\n </ul>\n )}\n </Part>\n\n <Part\n icon={<Users className=\"h-4 w-4\" />}\n title=\"Customer requests riding on unfinished work\"\n testId=\"readiness-requests\"\n loading={requests.loading && !requests.data}\n error={requests.error}\n >\n {riding.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No open customer request is tied to unfinished work in this release.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {riding.map((req) => (\n <li key={req.id} className=\"text-sm\" data-testid=\"readiness-request\">\n <span className=\"font-medium\">{req.title}</span>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({req.voteCount} vote{req.voteCount === 1 ? '' : 's'})\n </span>\n {req.workItemId && blockedIds.has(req.workItemId) && (\n <span className=\"text-status-error-text\"> — its work is blocked</span>\n )}\n </li>\n ))}\n </ul>\n )}\n </Part>\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n This release has no project, so budget, risks and customer requests cannot be shown.\n </p>\n )}\n\n {plannedDate && daysLate !== null && (\n <p className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <Clock className=\"h-3.5 w-3.5\" />\n Planned for {new Date(plannedDate).toLocaleDateString()}\n {daysLate > 0 ? ` (${daysLate} day${daysLate === 1 ? '' : 's'} ago)` : ''}.\n </p>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ReleaseWorkItem {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
status: string;
|
|
5
|
+
type: string;
|
|
6
|
+
priority: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function ReleaseWorkItemsCard({ releaseId, projectId, items, }: {
|
|
9
|
+
releaseId: string;
|
|
10
|
+
projectId: string | null;
|
|
11
|
+
items: ReleaseWorkItem[];
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=ReleaseWorkItemsCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReleaseWorkItemsCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/releases/ReleaseWorkItemsCard.tsx"],"names":[],"mappings":"AAkDA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,KAAK,GACN,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B,2CAiKA"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { throwOnError as n } from "../utils/mutationResult.js";
|
|
3
|
+
import { useDeferredValue as r, useState as i } from "react";
|
|
4
|
+
import { Plus as a, X as o } from "lucide-react";
|
|
5
|
+
import { gql as s } from "@apollo/client";
|
|
6
|
+
import { useMutation as c, useQuery as l } from "@apollo/client/react";
|
|
7
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
8
|
+
import { Badge as f, Button as p, Card as m, CardContent as h, CardHeader as g, CardTitle as _, Input as v } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/planmagnet/releases/ReleaseWorkItemsCard.tsx
|
|
10
|
+
var y = s`
|
|
11
|
+
query ReleaseWorkItemCandidates($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {
|
|
12
|
+
planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {
|
|
13
|
+
items {
|
|
14
|
+
id
|
|
15
|
+
key
|
|
16
|
+
title
|
|
17
|
+
status
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`, b = s`
|
|
22
|
+
mutation ReleaseAddWorkItems($releaseId: ID!, $workItemIds: [ID!]!) {
|
|
23
|
+
planmagnetAddWorkItemsToRelease(releaseId: $releaseId, workItemIds: $workItemIds) {
|
|
24
|
+
id
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`, x = s`
|
|
28
|
+
mutation ReleaseRemoveWorkItems($releaseId: ID!, $workItemIds: [ID!]!) {
|
|
29
|
+
planmagnetRemoveWorkItemsFromRelease(releaseId: $releaseId, workItemIds: $workItemIds) {
|
|
30
|
+
id
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
function S({ releaseId: s, projectId: S, items: C }) {
|
|
35
|
+
let { workspaceId: w } = e(), T = t(), [E, D] = i(!1), [O, k] = i(""), [A, j] = i(null), M = r(O.trim()), N = l(y, {
|
|
36
|
+
variables: {
|
|
37
|
+
workspaceId: w,
|
|
38
|
+
filter: {
|
|
39
|
+
projectId: S,
|
|
40
|
+
...M ? { search: M } : {}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
skip: !E || !w || !S
|
|
44
|
+
}), P = {
|
|
45
|
+
refetchQueries: ["GetRelease", "ReleaseReadiness"],
|
|
46
|
+
awaitRefetchQueries: !0
|
|
47
|
+
}, [F, I] = c(b, P), [L, R] = c(x, P), z = new Set(C.map((e) => e.id)), B = (N.data?.planmagnetWorkItems.items ?? []).filter((e) => !z.has(e.id)), V = async (e, t) => {
|
|
48
|
+
j(null);
|
|
49
|
+
try {
|
|
50
|
+
await n(e());
|
|
51
|
+
} catch (e) {
|
|
52
|
+
j(e instanceof Error ? e.message : t);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return /* @__PURE__ */ d(m, {
|
|
56
|
+
"data-testid": "release-work-items",
|
|
57
|
+
children: [/* @__PURE__ */ d(g, {
|
|
58
|
+
className: "flex flex-row items-center justify-between space-y-0",
|
|
59
|
+
children: [/* @__PURE__ */ d(_, { children: [
|
|
60
|
+
"Work Items (",
|
|
61
|
+
C.length,
|
|
62
|
+
")"
|
|
63
|
+
] }), S && !E && /* @__PURE__ */ d(p, {
|
|
64
|
+
size: "sm",
|
|
65
|
+
variant: "outline",
|
|
66
|
+
onClick: () => D(!0),
|
|
67
|
+
"data-testid": "release-items-add",
|
|
68
|
+
children: [/* @__PURE__ */ u(a, { className: "mr-1 h-4 w-4" }), " Add work items"]
|
|
69
|
+
})]
|
|
70
|
+
}), /* @__PURE__ */ d(h, {
|
|
71
|
+
className: "space-y-2",
|
|
72
|
+
children: [
|
|
73
|
+
A && /* @__PURE__ */ u("p", {
|
|
74
|
+
role: "alert",
|
|
75
|
+
className: "text-sm text-destructive",
|
|
76
|
+
children: A
|
|
77
|
+
}),
|
|
78
|
+
C.length === 0 && /* @__PURE__ */ u("p", {
|
|
79
|
+
className: "text-sm text-muted-foreground",
|
|
80
|
+
children: "No work items are in this release yet, so its overview and release notes cover everything in its project."
|
|
81
|
+
}),
|
|
82
|
+
C.map((e) => /* @__PURE__ */ d("div", {
|
|
83
|
+
className: "flex items-center justify-between gap-2 rounded-md border p-3",
|
|
84
|
+
"data-testid": "release-item",
|
|
85
|
+
children: [/* @__PURE__ */ d("button", {
|
|
86
|
+
type: "button",
|
|
87
|
+
className: "min-w-0 text-left",
|
|
88
|
+
onClick: () => T(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
|
|
89
|
+
children: [/* @__PURE__ */ u("p", {
|
|
90
|
+
className: "truncate text-sm font-medium hover:underline",
|
|
91
|
+
children: e.title
|
|
92
|
+
}), /* @__PURE__ */ d("p", {
|
|
93
|
+
className: "text-xs capitalize text-muted-foreground",
|
|
94
|
+
children: [
|
|
95
|
+
e.type.toLowerCase(),
|
|
96
|
+
" · ",
|
|
97
|
+
e.priority.toLowerCase(),
|
|
98
|
+
" priority"
|
|
99
|
+
]
|
|
100
|
+
})]
|
|
101
|
+
}), /* @__PURE__ */ d("span", {
|
|
102
|
+
className: "flex shrink-0 items-center gap-1",
|
|
103
|
+
children: [/* @__PURE__ */ u(f, {
|
|
104
|
+
variant: "outline",
|
|
105
|
+
className: "text-xs capitalize",
|
|
106
|
+
children: e.status.toLowerCase().replace("_", " ")
|
|
107
|
+
}), /* @__PURE__ */ u(p, {
|
|
108
|
+
size: "icon",
|
|
109
|
+
variant: "ghost",
|
|
110
|
+
"aria-label": `Remove ${e.title} from this release`,
|
|
111
|
+
disabled: R.loading,
|
|
112
|
+
onClick: () => V(() => L({ variables: {
|
|
113
|
+
releaseId: s,
|
|
114
|
+
workItemIds: [e.id]
|
|
115
|
+
} }), "The work item could not be removed."),
|
|
116
|
+
children: /* @__PURE__ */ u(o, { className: "h-4 w-4" })
|
|
117
|
+
})]
|
|
118
|
+
})]
|
|
119
|
+
}, e.id)),
|
|
120
|
+
E && /* @__PURE__ */ d("div", {
|
|
121
|
+
className: "space-y-2 border-t pt-3",
|
|
122
|
+
children: [
|
|
123
|
+
/* @__PURE__ */ d("div", {
|
|
124
|
+
className: "flex gap-2",
|
|
125
|
+
children: [/* @__PURE__ */ u(v, {
|
|
126
|
+
"aria-label": "Find work items in this release's project",
|
|
127
|
+
placeholder: "Find work items in this project",
|
|
128
|
+
value: O,
|
|
129
|
+
onChange: (e) => k(e.target.value)
|
|
130
|
+
}), /* @__PURE__ */ u(p, {
|
|
131
|
+
size: "sm",
|
|
132
|
+
variant: "ghost",
|
|
133
|
+
onClick: () => {
|
|
134
|
+
D(!1), k("");
|
|
135
|
+
},
|
|
136
|
+
children: "Done"
|
|
137
|
+
})]
|
|
138
|
+
}),
|
|
139
|
+
N.loading && !N.data && /* @__PURE__ */ u("p", {
|
|
140
|
+
className: "text-sm text-muted-foreground",
|
|
141
|
+
children: "Loading…"
|
|
142
|
+
}),
|
|
143
|
+
N.error && /* @__PURE__ */ d("p", {
|
|
144
|
+
role: "alert",
|
|
145
|
+
className: "text-sm text-destructive",
|
|
146
|
+
children: ["Work items could not be loaded: ", N.error.message]
|
|
147
|
+
}),
|
|
148
|
+
N.data && B.length === 0 && /* @__PURE__ */ u("p", {
|
|
149
|
+
className: "text-sm text-muted-foreground",
|
|
150
|
+
children: "No other work items match."
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ u("ul", {
|
|
153
|
+
className: "space-y-1",
|
|
154
|
+
children: B.map((e) => /* @__PURE__ */ d("li", {
|
|
155
|
+
className: "flex items-center justify-between gap-2 text-sm",
|
|
156
|
+
children: [/* @__PURE__ */ d("span", {
|
|
157
|
+
className: "min-w-0 truncate",
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ u("span", {
|
|
160
|
+
className: "text-muted-foreground",
|
|
161
|
+
children: e.key
|
|
162
|
+
}),
|
|
163
|
+
" ",
|
|
164
|
+
e.title
|
|
165
|
+
]
|
|
166
|
+
}), /* @__PURE__ */ u(p, {
|
|
167
|
+
size: "sm",
|
|
168
|
+
variant: "outline",
|
|
169
|
+
className: "h-7 shrink-0 text-xs",
|
|
170
|
+
disabled: I.loading,
|
|
171
|
+
"data-testid": "release-item-candidate-add",
|
|
172
|
+
onClick: () => V(() => F({ variables: {
|
|
173
|
+
releaseId: s,
|
|
174
|
+
workItemIds: [e.id]
|
|
175
|
+
} }), "The work item could not be added."),
|
|
176
|
+
children: "Add"
|
|
177
|
+
})]
|
|
178
|
+
}, e.id))
|
|
179
|
+
})
|
|
180
|
+
]
|
|
181
|
+
})
|
|
182
|
+
]
|
|
183
|
+
})]
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { S as ReleaseWorkItemsCard };
|
|
188
|
+
|
|
189
|
+
//# sourceMappingURL=ReleaseWorkItemsCard.js.map
|