@burdenoff/microfe-planmagnet 2026.925.17 → 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 +15 -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 +127 -126
- 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 +102 -122
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/releases/ReleaseReadinessCard.d.ts.map +1 -1
- package/dist/planmagnet/releases/ReleaseReadinessCard.js +90 -89
- package/dist/planmagnet/releases/ReleaseReadinessCard.js.map +1 -1
- 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
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { CombinedGraphQLErrors as e } from "../../node_modules/@apollo/client/errors/CombinedGraphQLErrors.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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";
|
|
10
11
|
//#region src/planmagnet/releases/ReleaseReadinessCard.tsx
|
|
11
|
-
var
|
|
12
|
+
var S = p`
|
|
12
13
|
query ReleaseReadiness($releaseId: ID!) {
|
|
13
14
|
planmagnetReleaseReadiness(releaseId: $releaseId) {
|
|
14
15
|
scope
|
|
@@ -32,7 +33,7 @@ var x = f`
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
`,
|
|
36
|
+
`, C = p`
|
|
36
37
|
query ReleaseBudgets($workspaceId: ID!, $projectId: String!) {
|
|
37
38
|
planmagnetBudgets(
|
|
38
39
|
workspaceId: $workspaceId
|
|
@@ -51,7 +52,7 @@ var x = f`
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
`,
|
|
55
|
+
`, w = p`
|
|
55
56
|
query ReleaseRisks($workspaceId: ID!, $projectId: String!) {
|
|
56
57
|
planmagnetRisks(
|
|
57
58
|
workspaceId: $workspaceId
|
|
@@ -67,7 +68,7 @@ var x = f`
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
`,
|
|
71
|
+
`, T = p`
|
|
71
72
|
query ReleaseCustomerRequests($workspaceId: ID!, $projectId: String!) {
|
|
72
73
|
planmagnetFeatureRequests(
|
|
73
74
|
workspaceId: $workspaceId
|
|
@@ -83,36 +84,36 @@ var x = f`
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
|
-
`,
|
|
87
|
+
`, E = new Set([
|
|
87
88
|
"RESOLVED",
|
|
88
89
|
"ACCEPTED",
|
|
89
90
|
"CLOSED"
|
|
90
|
-
]),
|
|
91
|
+
]), D = new Set([
|
|
91
92
|
"COMPLETED",
|
|
92
93
|
"DECLINED",
|
|
93
94
|
"DUPLICATE"
|
|
94
|
-
]),
|
|
95
|
-
function
|
|
96
|
-
return e.is(
|
|
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;
|
|
97
98
|
}
|
|
98
|
-
function
|
|
99
|
+
function A({ icon: e, title: t, testId: n, loading: r, error: i, children: a }) {
|
|
99
100
|
let o = a;
|
|
100
|
-
return i ? o = /* @__PURE__ */
|
|
101
|
+
return i ? o = /* @__PURE__ */ g("p", {
|
|
101
102
|
className: "text-sm text-muted-foreground",
|
|
102
|
-
children:
|
|
103
|
-
}) : r && (o = /* @__PURE__ */
|
|
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", {
|
|
104
105
|
className: "text-sm text-muted-foreground",
|
|
105
106
|
children: "Loading…"
|
|
106
|
-
})), /* @__PURE__ */
|
|
107
|
+
})), /* @__PURE__ */ _("section", {
|
|
107
108
|
className: "space-y-1.5",
|
|
108
109
|
"data-testid": n,
|
|
109
|
-
children: [/* @__PURE__ */
|
|
110
|
+
children: [/* @__PURE__ */ _("h3", {
|
|
110
111
|
className: "flex items-center gap-2 text-sm font-medium",
|
|
111
112
|
children: [e, t]
|
|
112
113
|
}), o]
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
|
-
var
|
|
116
|
+
var j = (e, t) => {
|
|
116
117
|
try {
|
|
117
118
|
return new Intl.NumberFormat(void 0, {
|
|
118
119
|
style: "currency",
|
|
@@ -122,62 +123,62 @@ var A = (e, t) => {
|
|
|
122
123
|
return `${e.toFixed(2)} ${t}`;
|
|
123
124
|
}
|
|
124
125
|
};
|
|
125
|
-
function
|
|
126
|
-
let { workspaceId: M } =
|
|
126
|
+
function M({ releaseId: e, projectId: t, releaseStatus: p, plannedDate: k }) {
|
|
127
|
+
let { workspaceId: M } = n(), [N] = a(() => Date.now()), P = r(), F = {
|
|
127
128
|
variables: {
|
|
128
129
|
workspaceId: M,
|
|
129
|
-
projectId:
|
|
130
|
+
projectId: t
|
|
130
131
|
},
|
|
131
|
-
skip: !M || !
|
|
132
|
-
}, I =
|
|
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 = [];
|
|
133
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");
|
|
134
135
|
let Z = !I.loading && !L.loading;
|
|
135
|
-
return /* @__PURE__ */
|
|
136
|
+
return /* @__PURE__ */ _(v, {
|
|
136
137
|
"data-testid": "release-readiness",
|
|
137
|
-
children: [/* @__PURE__ */
|
|
138
|
+
children: [/* @__PURE__ */ g(b, { children: /* @__PURE__ */ g(x, { children: "What stands between this release and shipping" }) }), /* @__PURE__ */ _(y, {
|
|
138
139
|
className: "space-y-5",
|
|
139
140
|
children: [
|
|
140
|
-
V ? /* @__PURE__ */
|
|
141
|
+
V ? /* @__PURE__ */ _("p", {
|
|
141
142
|
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
142
|
-
children: [/* @__PURE__ */
|
|
143
|
-
}) : Z && X.length === 0 && H && H.scope !== "NONE" ? /* @__PURE__ */
|
|
143
|
+
children: [/* @__PURE__ */ g(s, { className: "h-4 w-4" }), " Released."]
|
|
144
|
+
}) : Z && X.length === 0 && H && H.scope !== "NONE" ? /* @__PURE__ */ _("p", {
|
|
144
145
|
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
145
146
|
"data-testid": "readiness-headline",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
147
|
-
}) : Z && X.length > 0 ? /* @__PURE__ */
|
|
147
|
+
children: [/* @__PURE__ */ g(s, { className: "h-4 w-4" }), " Nothing is holding this release."]
|
|
148
|
+
}) : Z && X.length > 0 ? /* @__PURE__ */ _("p", {
|
|
148
149
|
className: "flex items-center gap-2 text-sm text-status-warning-text",
|
|
149
150
|
"data-testid": "readiness-headline",
|
|
150
151
|
children: [
|
|
151
|
-
/* @__PURE__ */
|
|
152
|
+
/* @__PURE__ */ g(o, { className: "h-4 w-4 shrink-0" }),
|
|
152
153
|
" Held by: ",
|
|
153
154
|
X.join(", "),
|
|
154
155
|
"."
|
|
155
156
|
]
|
|
156
157
|
}) : null,
|
|
157
|
-
/* @__PURE__ */
|
|
158
|
-
icon: /* @__PURE__ */
|
|
158
|
+
/* @__PURE__ */ g(A, {
|
|
159
|
+
icon: /* @__PURE__ */ g(l, { className: "h-4 w-4" }),
|
|
159
160
|
title: "Work and blockers",
|
|
160
161
|
testId: "readiness-work",
|
|
161
162
|
loading: I.loading && !I.data,
|
|
162
163
|
error: I.error,
|
|
163
|
-
children: H && /* @__PURE__ */
|
|
164
|
+
children: H && /* @__PURE__ */ _("div", {
|
|
164
165
|
className: "space-y-2",
|
|
165
166
|
children: [
|
|
166
|
-
/* @__PURE__ */
|
|
167
|
+
/* @__PURE__ */ g("p", {
|
|
167
168
|
className: "text-sm text-muted-foreground",
|
|
168
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.`
|
|
169
170
|
}),
|
|
170
|
-
H.blockers.length === 0 && H.scope !== "NONE" && /* @__PURE__ */
|
|
171
|
+
H.blockers.length === 0 && H.scope !== "NONE" && /* @__PURE__ */ g("p", {
|
|
171
172
|
className: "text-sm text-muted-foreground",
|
|
172
173
|
children: "No item is waiting on unfinished work."
|
|
173
174
|
}),
|
|
174
|
-
/* @__PURE__ */
|
|
175
|
+
/* @__PURE__ */ g("ul", {
|
|
175
176
|
className: "space-y-1.5",
|
|
176
|
-
children: H.blockers.map((e) => /* @__PURE__ */
|
|
177
|
+
children: H.blockers.map((e) => /* @__PURE__ */ _("li", {
|
|
177
178
|
className: "rounded-md border p-2 text-sm",
|
|
178
179
|
"data-testid": "readiness-blocker",
|
|
179
180
|
children: [
|
|
180
|
-
/* @__PURE__ */
|
|
181
|
+
/* @__PURE__ */ _("button", {
|
|
181
182
|
type: "button",
|
|
182
183
|
className: "font-medium hover:underline text-left",
|
|
183
184
|
onClick: () => P(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.workItem.id)}`),
|
|
@@ -187,13 +188,13 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
187
188
|
e.workItem.title
|
|
188
189
|
]
|
|
189
190
|
}),
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
+
/* @__PURE__ */ g("span", {
|
|
191
192
|
className: "text-muted-foreground",
|
|
192
193
|
children: " waits on "
|
|
193
194
|
}),
|
|
194
|
-
e.waitingOn.map((e,
|
|
195
|
-
|
|
196
|
-
/* @__PURE__ */
|
|
195
|
+
e.waitingOn.map((e, n) => /* @__PURE__ */ _("span", { children: [
|
|
196
|
+
n > 0 && ", ",
|
|
197
|
+
/* @__PURE__ */ _("button", {
|
|
197
198
|
type: "button",
|
|
198
199
|
className: "hover:underline",
|
|
199
200
|
onClick: () => P(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
|
|
@@ -204,12 +205,12 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
204
205
|
]
|
|
205
206
|
}),
|
|
206
207
|
" ",
|
|
207
|
-
/* @__PURE__ */
|
|
208
|
+
/* @__PURE__ */ _("span", {
|
|
208
209
|
className: "text-xs text-muted-foreground",
|
|
209
210
|
children: [
|
|
210
211
|
"(",
|
|
211
212
|
e.status.toLowerCase().replace("_", " "),
|
|
212
|
-
|
|
213
|
+
t && e.projectId && e.projectId !== t ? ", another project" : "",
|
|
213
214
|
")"
|
|
214
215
|
]
|
|
215
216
|
})
|
|
@@ -220,63 +221,63 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
220
221
|
]
|
|
221
222
|
})
|
|
222
223
|
}),
|
|
223
|
-
/* @__PURE__ */
|
|
224
|
-
icon: /* @__PURE__ */
|
|
224
|
+
/* @__PURE__ */ g(A, {
|
|
225
|
+
icon: /* @__PURE__ */ g(u, { className: "h-4 w-4" }),
|
|
225
226
|
title: "Approvals",
|
|
226
227
|
testId: "readiness-approvals",
|
|
227
228
|
loading: L.loading && !L.data,
|
|
228
229
|
error: L.error,
|
|
229
|
-
children: /* @__PURE__ */
|
|
230
|
+
children: /* @__PURE__ */ g("p", {
|
|
230
231
|
className: "text-sm text-muted-foreground",
|
|
231
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(", ")}.`
|
|
232
233
|
})
|
|
233
234
|
}),
|
|
234
|
-
|
|
235
|
-
/* @__PURE__ */
|
|
236
|
-
icon: /* @__PURE__ */
|
|
235
|
+
t ? /* @__PURE__ */ _(h, { children: [
|
|
236
|
+
/* @__PURE__ */ g(A, {
|
|
237
|
+
icon: /* @__PURE__ */ g(f, { className: "h-4 w-4" }),
|
|
237
238
|
title: "Budget",
|
|
238
239
|
testId: "readiness-budget",
|
|
239
240
|
loading: R.loading && !R.data,
|
|
240
241
|
error: R.error,
|
|
241
|
-
children: W.length === 0 ? /* @__PURE__ */
|
|
242
|
+
children: W.length === 0 ? /* @__PURE__ */ g("p", {
|
|
242
243
|
className: "text-sm text-muted-foreground",
|
|
243
244
|
children: "This release's project has no budget."
|
|
244
|
-
}) : /* @__PURE__ */
|
|
245
|
+
}) : /* @__PURE__ */ g("ul", {
|
|
245
246
|
className: "space-y-1",
|
|
246
247
|
children: W.map((e) => {
|
|
247
248
|
let t = e.remainingAmount < 0;
|
|
248
|
-
return /* @__PURE__ */
|
|
249
|
+
return /* @__PURE__ */ _("li", {
|
|
249
250
|
className: "text-sm",
|
|
250
251
|
"data-testid": "readiness-budget-row",
|
|
251
252
|
children: [
|
|
252
|
-
/* @__PURE__ */
|
|
253
|
+
/* @__PURE__ */ g("span", {
|
|
253
254
|
className: "font-medium",
|
|
254
255
|
children: e.name
|
|
255
256
|
}),
|
|
256
257
|
":",
|
|
257
258
|
" ",
|
|
258
|
-
|
|
259
|
+
j(e.spentAmount, e.currency),
|
|
259
260
|
" of ",
|
|
260
|
-
|
|
261
|
+
j(e.plannedAmount, e.currency),
|
|
261
262
|
" ",
|
|
262
263
|
"spent",
|
|
263
|
-
t ? /* @__PURE__ */
|
|
264
|
+
t ? /* @__PURE__ */ _("span", {
|
|
264
265
|
className: "text-status-error-text",
|
|
265
266
|
children: [
|
|
266
267
|
" ",
|
|
267
268
|
"— over by ",
|
|
268
|
-
|
|
269
|
+
j(-e.remainingAmount, e.currency)
|
|
269
270
|
]
|
|
270
|
-
}) : /* @__PURE__ */
|
|
271
|
+
}) : /* @__PURE__ */ _("span", {
|
|
271
272
|
className: "text-muted-foreground",
|
|
272
273
|
children: [
|
|
273
274
|
" ",
|
|
274
275
|
"— ",
|
|
275
|
-
|
|
276
|
+
j(e.remainingAmount, e.currency),
|
|
276
277
|
" left"
|
|
277
278
|
]
|
|
278
279
|
}),
|
|
279
|
-
!e.approvedAt && /* @__PURE__ */
|
|
280
|
+
!e.approvedAt && /* @__PURE__ */ g("span", {
|
|
280
281
|
className: "text-status-warning-text",
|
|
281
282
|
children: " · not approved"
|
|
282
283
|
})
|
|
@@ -285,26 +286,26 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
285
286
|
})
|
|
286
287
|
})
|
|
287
288
|
}),
|
|
288
|
-
/* @__PURE__ */
|
|
289
|
-
icon: /* @__PURE__ */
|
|
289
|
+
/* @__PURE__ */ g(A, {
|
|
290
|
+
icon: /* @__PURE__ */ g(o, { className: "h-4 w-4" }),
|
|
290
291
|
title: "Open risks",
|
|
291
292
|
testId: "readiness-risks",
|
|
292
293
|
loading: z.loading && !z.data,
|
|
293
294
|
error: z.error,
|
|
294
|
-
children: G.length === 0 ? /* @__PURE__ */
|
|
295
|
+
children: G.length === 0 ? /* @__PURE__ */ g("p", {
|
|
295
296
|
className: "text-sm text-muted-foreground",
|
|
296
297
|
children: "No open risks on this release's project."
|
|
297
|
-
}) : /* @__PURE__ */
|
|
298
|
+
}) : /* @__PURE__ */ _("ul", {
|
|
298
299
|
className: "space-y-1",
|
|
299
|
-
children: [G.slice(0, 5).map((e) => /* @__PURE__ */
|
|
300
|
+
children: [G.slice(0, 5).map((e) => /* @__PURE__ */ _("li", {
|
|
300
301
|
className: "text-sm",
|
|
301
302
|
children: [
|
|
302
|
-
/* @__PURE__ */
|
|
303
|
+
/* @__PURE__ */ g("span", {
|
|
303
304
|
className: "font-medium",
|
|
304
305
|
children: e.title
|
|
305
306
|
}),
|
|
306
307
|
" ",
|
|
307
|
-
/* @__PURE__ */
|
|
308
|
+
/* @__PURE__ */ _("span", {
|
|
308
309
|
className: "text-xs text-muted-foreground",
|
|
309
310
|
children: [
|
|
310
311
|
"(",
|
|
@@ -318,7 +319,7 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
318
319
|
]
|
|
319
320
|
})
|
|
320
321
|
]
|
|
321
|
-
}, e.id)), G.length > 5 && /* @__PURE__ */
|
|
322
|
+
}, e.id)), G.length > 5 && /* @__PURE__ */ _("li", {
|
|
322
323
|
className: "text-xs text-muted-foreground",
|
|
323
324
|
children: [
|
|
324
325
|
"and ",
|
|
@@ -328,27 +329,27 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
328
329
|
})]
|
|
329
330
|
})
|
|
330
331
|
}),
|
|
331
|
-
/* @__PURE__ */
|
|
332
|
-
icon: /* @__PURE__ */
|
|
332
|
+
/* @__PURE__ */ g(A, {
|
|
333
|
+
icon: /* @__PURE__ */ g(d, { className: "h-4 w-4" }),
|
|
333
334
|
title: "Customer requests riding on unfinished work",
|
|
334
335
|
testId: "readiness-requests",
|
|
335
336
|
loading: B.loading && !B.data,
|
|
336
337
|
error: B.error,
|
|
337
|
-
children: J.length === 0 ? /* @__PURE__ */
|
|
338
|
+
children: J.length === 0 ? /* @__PURE__ */ g("p", {
|
|
338
339
|
className: "text-sm text-muted-foreground",
|
|
339
340
|
children: "No open customer request is tied to unfinished work in this release."
|
|
340
|
-
}) : /* @__PURE__ */
|
|
341
|
+
}) : /* @__PURE__ */ g("ul", {
|
|
341
342
|
className: "space-y-1",
|
|
342
|
-
children: J.map((e) => /* @__PURE__ */
|
|
343
|
+
children: J.map((e) => /* @__PURE__ */ _("li", {
|
|
343
344
|
className: "text-sm",
|
|
344
345
|
"data-testid": "readiness-request",
|
|
345
346
|
children: [
|
|
346
|
-
/* @__PURE__ */
|
|
347
|
+
/* @__PURE__ */ g("span", {
|
|
347
348
|
className: "font-medium",
|
|
348
349
|
children: e.title
|
|
349
350
|
}),
|
|
350
351
|
" ",
|
|
351
|
-
/* @__PURE__ */
|
|
352
|
+
/* @__PURE__ */ _("span", {
|
|
352
353
|
className: "text-xs text-muted-foreground",
|
|
353
354
|
children: [
|
|
354
355
|
"(",
|
|
@@ -358,7 +359,7 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
358
359
|
")"
|
|
359
360
|
]
|
|
360
361
|
}),
|
|
361
|
-
e.workItemId && q.has(e.workItemId) && /* @__PURE__ */
|
|
362
|
+
e.workItemId && q.has(e.workItemId) && /* @__PURE__ */ g("span", {
|
|
362
363
|
className: "text-status-error-text",
|
|
363
364
|
children: " — its work is blocked"
|
|
364
365
|
})
|
|
@@ -366,16 +367,16 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
366
367
|
}, e.id))
|
|
367
368
|
})
|
|
368
369
|
})
|
|
369
|
-
] }) : /* @__PURE__ */
|
|
370
|
+
] }) : /* @__PURE__ */ g("p", {
|
|
370
371
|
className: "text-sm text-muted-foreground",
|
|
371
372
|
children: "This release has no project, so budget, risks and customer requests cannot be shown."
|
|
372
373
|
}),
|
|
373
|
-
|
|
374
|
+
k && Y !== null && /* @__PURE__ */ _("p", {
|
|
374
375
|
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
375
376
|
children: [
|
|
376
|
-
/* @__PURE__ */
|
|
377
|
+
/* @__PURE__ */ g(c, { className: "h-3.5 w-3.5" }),
|
|
377
378
|
"Planned for ",
|
|
378
|
-
new Date(
|
|
379
|
+
new Date(k).toLocaleDateString(),
|
|
379
380
|
Y > 0 ? ` (${Y} day${Y === 1 ? "" : "s"} ago)` : "",
|
|
380
381
|
"."
|
|
381
382
|
]
|
|
@@ -385,6 +386,6 @@ function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
|
385
386
|
});
|
|
386
387
|
}
|
|
387
388
|
//#endregion
|
|
388
|
-
export {
|
|
389
|
+
export { M as ReleaseReadinessCard };
|
|
389
390
|
|
|
390
391
|
//# sourceMappingURL=ReleaseReadinessCard.js.map
|
|
@@ -1 +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 } 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\nfunction denied(error: unknown): boolean {\n return (\n CombinedGraphQLErrors.is(error) && error.errors.some((e) => e.extensions?.code === 'FORBIDDEN')\n );\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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"}
|
|
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"}
|