@burdenoff/microfe-planmagnet 2026.927.3 → 2026.927.4
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/controls/FeatureControls.d.ts +26 -0
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/FeatureControls.js +318 -0
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -0
- package/dist/planmagnet/controls/MilestoneControls.d.ts +17 -0
- package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/MilestoneControls.js +200 -0
- package/dist/planmagnet/controls/MilestoneControls.js.map +1 -0
- package/dist/planmagnet/controls/ObjectiveControls.d.ts +31 -0
- package/dist/planmagnet/controls/ObjectiveControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/ObjectiveControls.js +296 -0
- package/dist/planmagnet/controls/ObjectiveControls.js.map +1 -0
- package/dist/planmagnet/controls/Pickers.d.ts +26 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -0
- package/dist/planmagnet/controls/Pickers.js +171 -0
- package/dist/planmagnet/controls/Pickers.js.map +1 -0
- package/dist/planmagnet/controls/ProjectControls.js +7 -7
- package/dist/planmagnet/controls/ReleaseControls.d.ts +14 -0
- package/dist/planmagnet/controls/ReleaseControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/ReleaseControls.js +108 -0
- package/dist/planmagnet/controls/ReleaseControls.js.map +1 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +186 -152
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.js +63 -56
- package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js +229 -214
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js +142 -114
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditReleasePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditReleasePagePage.js +134 -105
- package/dist/planmagnet/pages/ProductsEditReleasePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +102 -108
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +119 -94
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +107 -106
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js +2 -0
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +6 -6
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +55 -63
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +4 -4
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +123 -128
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +139 -107
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +173 -126
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.js +1 -0
- package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +23 -23
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature request controls (BOFF-7951/7963). Feature requests were read-only: no vote, no status change after
|
|
3
|
+
* creation, no edit or delete — so every UI-created request stayed at 0 votes and its creation status, and the
|
|
4
|
+
* Declined/Duplicate tabs could only ever be empty.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FEATURE_STATUSES: readonly ["SUBMITTED", "UNDER_REVIEW", "PLANNED", "IN_PROGRESS", "COMPLETED", "DECLINED", "DUPLICATE"];
|
|
7
|
+
/** Vote for a request, or take your vote back. */
|
|
8
|
+
export declare function FeatureVoteButton({ featureId, voted }: {
|
|
9
|
+
featureId: string;
|
|
10
|
+
voted: boolean;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export interface FeatureValues {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description: string | null;
|
|
16
|
+
status: string;
|
|
17
|
+
businessValue: number | null;
|
|
18
|
+
effortEstimate: string | null;
|
|
19
|
+
projectId: string | null;
|
|
20
|
+
workItemId: string | null;
|
|
21
|
+
}
|
|
22
|
+
/** Status select, Edit and Delete for the feature detail header. */
|
|
23
|
+
export declare function FeatureHeaderActions({ feature }: {
|
|
24
|
+
feature: FeatureValues;
|
|
25
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=FeatureControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"AAuBA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,wGAQnB,CAAC;AA+CX,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,2CA6B5F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA2E3E"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { throwOnError as t } from "../utils/mutationResult.js";
|
|
3
|
+
import { ProjectSelect as n, WorkItemSearchSelect as r } from "./Pickers.js";
|
|
4
|
+
import { ConfirmButton as i, ErrorLine as a } from "./ConfirmButton.js";
|
|
5
|
+
import { parseIntField as o } from "../utils/formValidation.js";
|
|
6
|
+
import { enumLabel as s } from "./WorkItemControls.js";
|
|
7
|
+
import { detailPath as c } from "../utils/rowNavigation.js";
|
|
8
|
+
import { useState as l } from "react";
|
|
9
|
+
import { BarChart3 as u, Pencil as d, ThumbsUp as f, Trash2 as p } from "lucide-react";
|
|
10
|
+
import { gql as m } from "@apollo/client";
|
|
11
|
+
import { useMutation as h } from "@apollo/client/react";
|
|
12
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
13
|
+
import { Button as v, Input as y, ResponsiveDialog as b, ResponsiveDialogContent as x, ResponsiveDialogDescription as S, ResponsiveDialogFooter as C, ResponsiveDialogHeader as w, ResponsiveDialogTitle as T, ResponsiveDialogTrigger as E } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/planmagnet/controls/FeatureControls.tsx
|
|
15
|
+
var D = [
|
|
16
|
+
"SUBMITTED",
|
|
17
|
+
"UNDER_REVIEW",
|
|
18
|
+
"PLANNED",
|
|
19
|
+
"IN_PROGRESS",
|
|
20
|
+
"COMPLETED",
|
|
21
|
+
"DECLINED",
|
|
22
|
+
"DUPLICATE"
|
|
23
|
+
], O = [
|
|
24
|
+
"GetFeatureRequest",
|
|
25
|
+
"ListFeatureRequests",
|
|
26
|
+
"ListFeatureRequestsForProduct",
|
|
27
|
+
"ListFeatureRequestsForProducts",
|
|
28
|
+
"ListAllFeaturesAnalytics",
|
|
29
|
+
"GetFeatureRequestAnalytics",
|
|
30
|
+
"ReleaseCustomerRequests"
|
|
31
|
+
], k = m`
|
|
32
|
+
mutation FeatureControlsVote($id: ID!) {
|
|
33
|
+
planmagnetVoteOnFeature(featureRequestId: $id) {
|
|
34
|
+
id
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
`, A = m`
|
|
38
|
+
mutation FeatureControlsUnvote($id: ID!) {
|
|
39
|
+
planmagnetRemoveFeatureVote(featureRequestId: $id)
|
|
40
|
+
}
|
|
41
|
+
`, j = m`
|
|
42
|
+
mutation FeatureControlsUpdate($id: ID!, $input: PlanmagnetUpdateFeatureRequestInput!) {
|
|
43
|
+
planmagnetUpdateFeatureRequest(id: $id, input: $input) {
|
|
44
|
+
id
|
|
45
|
+
title
|
|
46
|
+
description
|
|
47
|
+
status
|
|
48
|
+
businessValue
|
|
49
|
+
effortEstimate
|
|
50
|
+
projectId
|
|
51
|
+
workItemId
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`, M = m`
|
|
55
|
+
mutation FeatureControlsDelete($id: ID!) {
|
|
56
|
+
planmagnetDeleteFeatureRequest(id: $id)
|
|
57
|
+
}
|
|
58
|
+
`, N = {
|
|
59
|
+
refetchQueries: O,
|
|
60
|
+
awaitRefetchQueries: !0
|
|
61
|
+
};
|
|
62
|
+
function P({ featureId: e, voted: n }) {
|
|
63
|
+
let [r, i] = h(k, N), [o, s] = h(A, N), [c, u] = l(null), d = i.loading || s.loading, p = async () => {
|
|
64
|
+
u(null);
|
|
65
|
+
try {
|
|
66
|
+
await t((n ? o : r)({ variables: { id: e } }));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
u(e instanceof Error ? e.message : "Your vote could not be saved.");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return /* @__PURE__ */ _("div", {
|
|
72
|
+
className: "flex flex-col items-end gap-1",
|
|
73
|
+
children: [/* @__PURE__ */ _(v, {
|
|
74
|
+
size: "sm",
|
|
75
|
+
variant: n ? "default" : "outline",
|
|
76
|
+
"data-testid": "feature-vote",
|
|
77
|
+
"aria-pressed": n,
|
|
78
|
+
disabled: d,
|
|
79
|
+
onClick: () => void p(),
|
|
80
|
+
children: [/* @__PURE__ */ g(f, { className: "mr-1 h-4 w-4" }), n ? "Voted" : "Vote"]
|
|
81
|
+
}), c && /* @__PURE__ */ g(a, { message: c })]
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function F({ feature: n }) {
|
|
85
|
+
let r = e(), [o, d] = h(j, N), [f, m] = h(M, { refetchQueries: O.filter((e) => e !== "GetFeatureRequest") }), [y, b] = l(null), x = d.loading || m.loading, S = D.includes(n.status) ? D : [n.status, ...D], C = async (e) => {
|
|
86
|
+
b(null);
|
|
87
|
+
try {
|
|
88
|
+
await t(o({ variables: {
|
|
89
|
+
id: n.id,
|
|
90
|
+
input: { status: e }
|
|
91
|
+
} }));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
b(e instanceof Error ? e.message : "The status could not be changed.");
|
|
94
|
+
}
|
|
95
|
+
}, w = async () => {
|
|
96
|
+
b(null);
|
|
97
|
+
try {
|
|
98
|
+
await t(f({ variables: { id: n.id } })), r("/products/features-page");
|
|
99
|
+
} catch (e) {
|
|
100
|
+
b(e instanceof Error ? e.message : "The request could not be deleted.");
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return /* @__PURE__ */ _("div", {
|
|
104
|
+
className: "flex flex-col items-end gap-2",
|
|
105
|
+
children: [/* @__PURE__ */ _("div", {
|
|
106
|
+
className: "flex flex-wrap items-center justify-end gap-2",
|
|
107
|
+
children: [
|
|
108
|
+
/* @__PURE__ */ g("select", {
|
|
109
|
+
"aria-label": "Status",
|
|
110
|
+
"data-testid": "feature-status",
|
|
111
|
+
className: "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60",
|
|
112
|
+
value: n.status,
|
|
113
|
+
disabled: x,
|
|
114
|
+
onChange: (e) => {
|
|
115
|
+
e.target.value !== n.status && C(e.target.value);
|
|
116
|
+
},
|
|
117
|
+
children: S.map((e) => /* @__PURE__ */ g("option", {
|
|
118
|
+
value: e,
|
|
119
|
+
children: s(e)
|
|
120
|
+
}, e))
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ g(I, { feature: n }),
|
|
123
|
+
/* @__PURE__ */ _(v, {
|
|
124
|
+
size: "sm",
|
|
125
|
+
variant: "outline",
|
|
126
|
+
"data-testid": "feature-analytics",
|
|
127
|
+
onClick: () => r(c("/products/feature-analytics-page", n.id)),
|
|
128
|
+
children: [/* @__PURE__ */ g(u, { className: "mr-1 h-4 w-4" }), "Analytics"]
|
|
129
|
+
}),
|
|
130
|
+
/* @__PURE__ */ g(i, {
|
|
131
|
+
label: "Delete",
|
|
132
|
+
variant: "outline",
|
|
133
|
+
icon: /* @__PURE__ */ g(p, { className: "mr-1 h-4 w-4" }),
|
|
134
|
+
title: `Delete "${n.title}"?`,
|
|
135
|
+
description: "The request and its votes are deleted. This cannot be undone here.",
|
|
136
|
+
confirmLabel: "Delete request",
|
|
137
|
+
destructive: !0,
|
|
138
|
+
disabled: x,
|
|
139
|
+
onConfirm: () => void w()
|
|
140
|
+
})
|
|
141
|
+
]
|
|
142
|
+
}), y && /* @__PURE__ */ g(a, { message: y })]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function I({ feature: e }) {
|
|
146
|
+
let i = {
|
|
147
|
+
title: e.title,
|
|
148
|
+
description: e.description ?? "",
|
|
149
|
+
businessValue: e.businessValue == null ? "" : String(e.businessValue),
|
|
150
|
+
effortEstimate: e.effortEstimate ?? "",
|
|
151
|
+
projectId: e.projectId ?? "",
|
|
152
|
+
workItemId: e.workItemId ?? ""
|
|
153
|
+
}, [s, c] = l(!1), [u, f] = l(i), [p, m] = l(null), [D, O] = h(j, N), k = o(u.businessValue, {
|
|
154
|
+
min: 0,
|
|
155
|
+
max: 100
|
|
156
|
+
}), A = u.title.trim() !== "" && !k.invalid && !O.loading, M = async () => {
|
|
157
|
+
m(null);
|
|
158
|
+
let n = (e) => e.trim() ? e.trim() : null, r = {
|
|
159
|
+
title: u.title.trim(),
|
|
160
|
+
description: n(u.description),
|
|
161
|
+
businessValue: k.value ?? null,
|
|
162
|
+
effortEstimate: n(u.effortEstimate),
|
|
163
|
+
projectId: u.projectId || null,
|
|
164
|
+
workItemId: u.workItemId || null
|
|
165
|
+
}, a = {
|
|
166
|
+
title: i.title,
|
|
167
|
+
description: n(i.description),
|
|
168
|
+
businessValue: i.businessValue === "" ? null : Number(i.businessValue),
|
|
169
|
+
effortEstimate: n(i.effortEstimate),
|
|
170
|
+
projectId: i.projectId || null,
|
|
171
|
+
workItemId: i.workItemId || null
|
|
172
|
+
}, o = Object.fromEntries(Object.entries(r).filter(([e, t]) => t !== a[e]));
|
|
173
|
+
if (Object.keys(o).length === 0) {
|
|
174
|
+
c(!1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
await t(D({ variables: {
|
|
179
|
+
id: e.id,
|
|
180
|
+
input: o
|
|
181
|
+
} })), c(!1);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
m(e instanceof Error ? e.message : "The request could not be saved.");
|
|
184
|
+
}
|
|
185
|
+
}, P = "text-sm font-medium";
|
|
186
|
+
return /* @__PURE__ */ _(b, {
|
|
187
|
+
open: s,
|
|
188
|
+
onOpenChange: (e) => {
|
|
189
|
+
c(e), e ? f(i) : m(null);
|
|
190
|
+
},
|
|
191
|
+
children: [/* @__PURE__ */ g(E, {
|
|
192
|
+
asChild: !0,
|
|
193
|
+
children: /* @__PURE__ */ _(v, {
|
|
194
|
+
size: "sm",
|
|
195
|
+
variant: "outline",
|
|
196
|
+
"data-testid": "feature-edit",
|
|
197
|
+
children: [/* @__PURE__ */ g(d, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
198
|
+
})
|
|
199
|
+
}), /* @__PURE__ */ _(x, {
|
|
200
|
+
className: "max-w-lg",
|
|
201
|
+
children: [
|
|
202
|
+
/* @__PURE__ */ _(w, { children: [/* @__PURE__ */ g(T, { children: "Edit feature request" }), /* @__PURE__ */ g(S, { children: "Link the request to the work item that delivers it so releases can show who is waiting on it." })] }),
|
|
203
|
+
/* @__PURE__ */ _("div", {
|
|
204
|
+
className: "space-y-3",
|
|
205
|
+
children: [
|
|
206
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
207
|
+
htmlFor: "feature-edit-title",
|
|
208
|
+
className: P,
|
|
209
|
+
children: "Title"
|
|
210
|
+
}), /* @__PURE__ */ g(y, {
|
|
211
|
+
id: "feature-edit-title",
|
|
212
|
+
className: "mt-1",
|
|
213
|
+
value: u.title,
|
|
214
|
+
onChange: (e) => f({
|
|
215
|
+
...u,
|
|
216
|
+
title: e.target.value
|
|
217
|
+
})
|
|
218
|
+
})] }),
|
|
219
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
220
|
+
htmlFor: "feature-edit-description",
|
|
221
|
+
className: P,
|
|
222
|
+
children: "Description"
|
|
223
|
+
}), /* @__PURE__ */ g("textarea", {
|
|
224
|
+
id: "feature-edit-description",
|
|
225
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]",
|
|
226
|
+
value: u.description,
|
|
227
|
+
onChange: (e) => f({
|
|
228
|
+
...u,
|
|
229
|
+
description: e.target.value
|
|
230
|
+
})
|
|
231
|
+
})] }),
|
|
232
|
+
/* @__PURE__ */ _("div", {
|
|
233
|
+
className: "grid grid-cols-2 gap-3",
|
|
234
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
235
|
+
htmlFor: "feature-edit-value",
|
|
236
|
+
className: P,
|
|
237
|
+
children: "Business value (0–100)"
|
|
238
|
+
}), /* @__PURE__ */ g(y, {
|
|
239
|
+
id: "feature-edit-value",
|
|
240
|
+
type: "number",
|
|
241
|
+
min: 0,
|
|
242
|
+
max: 100,
|
|
243
|
+
className: "mt-1",
|
|
244
|
+
value: u.businessValue,
|
|
245
|
+
"aria-invalid": k.invalid,
|
|
246
|
+
onChange: (e) => f({
|
|
247
|
+
...u,
|
|
248
|
+
businessValue: e.target.value
|
|
249
|
+
})
|
|
250
|
+
})] }), /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
251
|
+
htmlFor: "feature-edit-effort",
|
|
252
|
+
className: P,
|
|
253
|
+
children: "Effort"
|
|
254
|
+
}), /* @__PURE__ */ g(y, {
|
|
255
|
+
id: "feature-edit-effort",
|
|
256
|
+
className: "mt-1",
|
|
257
|
+
placeholder: "e.g. M",
|
|
258
|
+
value: u.effortEstimate,
|
|
259
|
+
onChange: (e) => f({
|
|
260
|
+
...u,
|
|
261
|
+
effortEstimate: e.target.value
|
|
262
|
+
})
|
|
263
|
+
})] })]
|
|
264
|
+
}),
|
|
265
|
+
k.invalid && /* @__PURE__ */ g(a, { message: "Business value is a whole number from 0 to 100." }),
|
|
266
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
267
|
+
htmlFor: "feature-edit-project",
|
|
268
|
+
className: P,
|
|
269
|
+
children: "Project"
|
|
270
|
+
}), /* @__PURE__ */ g("div", {
|
|
271
|
+
className: "mt-1",
|
|
272
|
+
children: /* @__PURE__ */ g(n, {
|
|
273
|
+
id: "feature-edit-project",
|
|
274
|
+
value: u.projectId,
|
|
275
|
+
noneLabel: "No project",
|
|
276
|
+
onChange: (e) => f({
|
|
277
|
+
...u,
|
|
278
|
+
projectId: e,
|
|
279
|
+
workItemId: ""
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})] }),
|
|
283
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
284
|
+
htmlFor: "feature-edit-workitem",
|
|
285
|
+
className: P,
|
|
286
|
+
children: "Delivered by work item"
|
|
287
|
+
}), /* @__PURE__ */ g("div", {
|
|
288
|
+
className: "mt-1",
|
|
289
|
+
children: /* @__PURE__ */ g(r, {
|
|
290
|
+
id: "feature-edit-workitem",
|
|
291
|
+
projectId: u.projectId || void 0,
|
|
292
|
+
value: u.workItemId,
|
|
293
|
+
onChange: (e) => f({
|
|
294
|
+
...u,
|
|
295
|
+
workItemId: e
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
})] }),
|
|
299
|
+
p && /* @__PURE__ */ g(a, { message: p })
|
|
300
|
+
]
|
|
301
|
+
}),
|
|
302
|
+
/* @__PURE__ */ _(C, { children: [/* @__PURE__ */ g(v, {
|
|
303
|
+
variant: "outline",
|
|
304
|
+
onClick: () => c(!1),
|
|
305
|
+
children: "Cancel"
|
|
306
|
+
}), /* @__PURE__ */ g(v, {
|
|
307
|
+
onClick: () => void M(),
|
|
308
|
+
disabled: !A,
|
|
309
|
+
children: O.loading ? "Saving…" : "Save"
|
|
310
|
+
})] })
|
|
311
|
+
]
|
|
312
|
+
})]
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
export { F as FeatureHeaderActions, P as FeatureVoteButton };
|
|
317
|
+
|
|
318
|
+
//# sourceMappingURL=FeatureControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureControls.js","names":[],"sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { BarChart3, Pencil, ThumbsUp, Trash2 } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError } from '../utils/mutationResult';\nimport { parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { ProjectSelect, WorkItemSearchSelect } from './Pickers';\nimport { enumLabel } from './WorkItemControls';\n\n/**\n * Feature request controls (BOFF-7951/7963). Feature requests were read-only: no vote, no status change after\n * creation, no edit or delete — so every UI-created request stayed at 0 votes and its creation status, and the\n * Declined/Duplicate tabs could only ever be empty.\n */\n\nexport const FEATURE_STATUSES = [\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n] as const;\n\n/** Every query that shows a feature request, its votes or feature counts. */\nconst FEATURE_QUERIES = [\n 'GetFeatureRequest',\n 'ListFeatureRequests',\n 'ListFeatureRequestsForProduct',\n 'ListFeatureRequestsForProducts',\n 'ListAllFeaturesAnalytics',\n 'GetFeatureRequestAnalytics',\n 'ReleaseCustomerRequests',\n];\n\nconst VOTE = gql`\n mutation FeatureControlsVote($id: ID!) {\n planmagnetVoteOnFeature(featureRequestId: $id) {\n id\n }\n }\n`;\nconst UNVOTE = gql`\n mutation FeatureControlsUnvote($id: ID!) {\n planmagnetRemoveFeatureVote(featureRequestId: $id)\n }\n`;\nconst UPDATE = gql`\n mutation FeatureControlsUpdate($id: ID!, $input: PlanmagnetUpdateFeatureRequestInput!) {\n planmagnetUpdateFeatureRequest(id: $id, input: $input) {\n id\n title\n description\n status\n businessValue\n effortEstimate\n projectId\n workItemId\n }\n }\n`;\nconst DELETE = gql`\n mutation FeatureControlsDelete($id: ID!) {\n planmagnetDeleteFeatureRequest(id: $id)\n }\n`;\n\nconst options = { refetchQueries: FEATURE_QUERIES, awaitRefetchQueries: true };\n\n/** Vote for a request, or take your vote back. */\nexport function FeatureVoteButton({ featureId, voted }: { featureId: string; voted: boolean }) {\n const [vote, voteState] = useMutation(VOTE, options);\n const [unvote, unvoteState] = useMutation(UNVOTE, options);\n const [error, setError] = useState<string | null>(null);\n const busy = voteState.loading || unvoteState.loading;\n const toggle = async () => {\n setError(null);\n try {\n await throwOnError((voted ? unvote : vote)({ variables: { id: featureId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Your vote could not be saved.');\n }\n };\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <Button\n size=\"sm\"\n variant={voted ? 'default' : 'outline'}\n data-testid=\"feature-vote\"\n aria-pressed={voted}\n disabled={busy}\n onClick={() => void toggle()}\n >\n <ThumbsUp className=\"mr-1 h-4 w-4\" />\n {voted ? 'Voted' : 'Vote'}\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nexport interface FeatureValues {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n projectId: string | null;\n workItemId: string | null;\n}\n\n/** Status select, Edit and Delete for the feature detail header. */\nexport function FeatureHeaderActions({ feature }: { feature: FeatureValues }) {\n const navigate = usePlanMagnetNavigate();\n const [update, updateState] = useMutation(UPDATE, options);\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: FEATURE_QUERIES.filter((q) => q !== 'GetFeatureRequest'),\n });\n const [error, setError] = useState<string | null>(null);\n const busy = updateState.loading || removeState.loading;\n const statuses = (FEATURE_STATUSES as readonly string[]).includes(feature.status)\n ? FEATURE_STATUSES\n : [feature.status, ...FEATURE_STATUSES];\n\n const onStatus = async (status: string) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: feature.id, input: { status } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: feature.id } }));\n navigate('/products/features-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The request could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Status\"\n data-testid=\"feature-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={feature.status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== feature.status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <FeatureEditDialog feature={feature} />\n {/* BOFF-7983: the analytics page existed with no way in. */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"feature-analytics\"\n onClick={() => navigate(detailPath('/products/feature-analytics-page', feature.id))}\n >\n <BarChart3 className=\"mr-1 h-4 w-4\" />\n Analytics\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete \"${feature.title}\"?`}\n description=\"The request and its votes are deleted. This cannot be undone here.\"\n confirmLabel=\"Delete request\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Edit a request's fields, including the project and the work item it rides on (the release readiness card needs\n * that link, and no UI could set it — BOFF-7966). Only changed fields are sent; an emptied field is cleared.\n */\nfunction FeatureEditDialog({ feature }: { feature: FeatureValues }) {\n const initial = {\n title: feature.title,\n description: feature.description ?? '',\n businessValue: feature.businessValue == null ? '' : String(feature.businessValue),\n effortEstimate: feature.effortEstimate ?? '',\n projectId: feature.projectId ?? '',\n workItemId: feature.workItemId ?? '',\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation(UPDATE, options);\n const businessValue = parseIntField(form.businessValue, { min: 0, max: 100 });\n const canSave = form.title.trim() !== '' && !businessValue.invalid && !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\n const next = {\n title: form.title.trim(),\n description: text(form.description),\n businessValue: businessValue.value ?? null,\n effortEstimate: text(form.effortEstimate),\n projectId: form.projectId || null,\n workItemId: form.workItemId || null,\n };\n const before = {\n title: initial.title,\n description: text(initial.description),\n businessValue: initial.businessValue === '' ? null : Number(initial.businessValue),\n effortEstimate: text(initial.effortEstimate),\n projectId: initial.projectId || null,\n workItemId: initial.workItemId || null,\n };\n const input = Object.fromEntries(\n Object.entries(next).filter(([k, v]) => v !== before[k as keyof typeof before])\n );\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: feature.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The request could not be saved.');\n }\n };\n\n const label = 'text-sm font-medium';\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) setForm(initial);\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"feature-edit\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit feature request</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Link the request to the work item that delivers it so releases can show who is waiting\n on it.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"feature-edit-title\" className={label}>\n Title\n </label>\n <Input\n id=\"feature-edit-title\"\n className=\"mt-1\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-description\" className={label}>\n Description\n </label>\n <textarea\n id=\"feature-edit-description\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"feature-edit-value\" className={label}>\n Business value (0–100)\n </label>\n <Input\n id=\"feature-edit-value\"\n type=\"number\"\n min={0}\n max={100}\n className=\"mt-1\"\n value={form.businessValue}\n aria-invalid={businessValue.invalid}\n onChange={(e) => setForm({ ...form, businessValue: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-effort\" className={label}>\n Effort\n </label>\n <Input\n id=\"feature-edit-effort\"\n className=\"mt-1\"\n placeholder=\"e.g. M\"\n value={form.effortEstimate}\n onChange={(e) => setForm({ ...form, effortEstimate: e.target.value })}\n />\n </div>\n </div>\n {businessValue.invalid && (\n <ErrorLine message=\"Business value is a whole number from 0 to 100.\" />\n )}\n <div>\n <label htmlFor=\"feature-edit-project\" className={label}>\n Project\n </label>\n <div className=\"mt-1\">\n <ProjectSelect\n id=\"feature-edit-project\"\n value={form.projectId}\n noneLabel=\"No project\"\n onChange={(projectId) => setForm({ ...form, projectId, workItemId: '' })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"feature-edit-workitem\" className={label}>\n Delivered by work item\n </label>\n <div className=\"mt-1\">\n <WorkItemSearchSelect\n id=\"feature-edit-workitem\"\n projectId={form.projectId || undefined}\n value={form.workItemId}\n onChange={(workItemId) => setForm({ ...form, workItemId })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void save()} disabled={!canSave}>\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milestone controls (BOFF-7953/7963). A milestone was created Pending and could never move: no status control,
|
|
3
|
+
* no edit, no delete — so the In progress / Completed / Missed / Cancelled tabs could only be empty, and the
|
|
4
|
+
* "Completed" date stayed "—". Completing now also records the date (service, 2026-09-27).
|
|
5
|
+
*/
|
|
6
|
+
export declare const MILESTONE_STATUSES: readonly ["PENDING", "IN_PROGRESS", "COMPLETED", "MISSED", "CANCELLED"];
|
|
7
|
+
export interface MilestoneValues {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string | null;
|
|
11
|
+
status: string;
|
|
12
|
+
dueDate: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function MilestoneHeaderActions({ milestone }: {
|
|
15
|
+
milestone: MilestoneValues;
|
|
16
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=MilestoneControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MilestoneControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/MilestoneControls.tsx"],"names":[],"mappings":"AAoBA;;;;GAIG;AAEH,eAAO,MAAM,kBAAkB,yEAMrB,CAAC;AA2BX,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,eAAe,CAAA;CAAE,2CAoEnF"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { throwOnError as t } from "../utils/mutationResult.js";
|
|
3
|
+
import { ConfirmButton as n, ErrorLine as r } from "./ConfirmButton.js";
|
|
4
|
+
import { enumLabel as i } from "./WorkItemControls.js";
|
|
5
|
+
import { useState as a } from "react";
|
|
6
|
+
import { Pencil as o, Trash2 as s } from "lucide-react";
|
|
7
|
+
import { gql as c } from "@apollo/client";
|
|
8
|
+
import { useMutation as l } from "@apollo/client/react";
|
|
9
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
10
|
+
import { Button as f, Input as p, ResponsiveDialog as m, ResponsiveDialogContent as h, ResponsiveDialogDescription as g, ResponsiveDialogFooter as _, ResponsiveDialogHeader as v, ResponsiveDialogTitle as y, ResponsiveDialogTrigger as b } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/planmagnet/controls/MilestoneControls.tsx
|
|
12
|
+
var x = [
|
|
13
|
+
"PENDING",
|
|
14
|
+
"IN_PROGRESS",
|
|
15
|
+
"COMPLETED",
|
|
16
|
+
"MISSED",
|
|
17
|
+
"CANCELLED"
|
|
18
|
+
], S = [
|
|
19
|
+
"GetMilestonesForDetail",
|
|
20
|
+
"GetMilestones",
|
|
21
|
+
"GetDashboardData",
|
|
22
|
+
"GetAnalyticsData"
|
|
23
|
+
], C = c`
|
|
24
|
+
mutation MilestoneControlsUpdate($id: ID!, $input: PlanmagnetUpdateMilestoneInput!) {
|
|
25
|
+
planmagnetUpdateMilestone(id: $id, input: $input) {
|
|
26
|
+
id
|
|
27
|
+
name
|
|
28
|
+
description
|
|
29
|
+
status
|
|
30
|
+
dueDate
|
|
31
|
+
completedAt
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`, w = c`
|
|
35
|
+
mutation MilestoneControlsDelete($id: ID!) {
|
|
36
|
+
planmagnetDeleteMilestone(id: $id)
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
function T({ milestone: o }) {
|
|
40
|
+
let c = e(), [f, p] = l(C, {
|
|
41
|
+
refetchQueries: S,
|
|
42
|
+
awaitRefetchQueries: !0
|
|
43
|
+
}), [m, h] = l(w, { refetchQueries: S.filter((e) => e !== "GetMilestonesForDetail") }), [g, _] = a(null), v = p.loading || h.loading, y = x.includes(o.status) ? x : [o.status, ...x], b = async (e) => {
|
|
44
|
+
_(null);
|
|
45
|
+
try {
|
|
46
|
+
await t(f({ variables: {
|
|
47
|
+
id: o.id,
|
|
48
|
+
input: { status: e }
|
|
49
|
+
} }));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
_(e instanceof Error ? e.message : "The status could not be changed.");
|
|
52
|
+
}
|
|
53
|
+
}, T = async () => {
|
|
54
|
+
_(null);
|
|
55
|
+
try {
|
|
56
|
+
await t(m({ variables: { id: o.id } })), c("/projects/milestones-page");
|
|
57
|
+
} catch (e) {
|
|
58
|
+
_(e instanceof Error ? e.message : "The milestone could not be deleted.");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return /* @__PURE__ */ d("div", {
|
|
62
|
+
className: "flex flex-col items-end gap-2",
|
|
63
|
+
children: [/* @__PURE__ */ d("div", {
|
|
64
|
+
className: "flex flex-wrap items-center justify-end gap-2",
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ u("select", {
|
|
67
|
+
"aria-label": "Status",
|
|
68
|
+
"data-testid": "milestone-status",
|
|
69
|
+
className: "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60",
|
|
70
|
+
value: o.status,
|
|
71
|
+
disabled: v,
|
|
72
|
+
onChange: (e) => {
|
|
73
|
+
e.target.value !== o.status && b(e.target.value);
|
|
74
|
+
},
|
|
75
|
+
children: y.map((e) => /* @__PURE__ */ u("option", {
|
|
76
|
+
value: e,
|
|
77
|
+
children: i(e)
|
|
78
|
+
}, e))
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ u(D, { milestone: o }),
|
|
81
|
+
/* @__PURE__ */ u(n, {
|
|
82
|
+
label: "Delete",
|
|
83
|
+
variant: "outline",
|
|
84
|
+
icon: /* @__PURE__ */ u(s, { className: "mr-1 h-4 w-4" }),
|
|
85
|
+
title: `Delete ${o.name}?`,
|
|
86
|
+
description: "The milestone is deleted. This cannot be undone here.",
|
|
87
|
+
confirmLabel: "Delete milestone",
|
|
88
|
+
destructive: !0,
|
|
89
|
+
disabled: v,
|
|
90
|
+
onConfirm: () => void T()
|
|
91
|
+
})
|
|
92
|
+
]
|
|
93
|
+
}), g && /* @__PURE__ */ u(r, { message: g })]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
var E = (e) => e ? e.slice(0, 10) : "";
|
|
97
|
+
function D({ milestone: e }) {
|
|
98
|
+
let n = {
|
|
99
|
+
name: e.name,
|
|
100
|
+
description: e.description ?? "",
|
|
101
|
+
dueDate: E(e.dueDate)
|
|
102
|
+
}, [i, s] = a(!1), [c, x] = a(n), [w, T] = a(null), [D, O] = l(C, {
|
|
103
|
+
refetchQueries: S,
|
|
104
|
+
awaitRefetchQueries: !0
|
|
105
|
+
}), k = async () => {
|
|
106
|
+
T(null);
|
|
107
|
+
let r = {};
|
|
108
|
+
if (c.name.trim() !== n.name && (r.name = c.name.trim()), c.description.trim() !== n.description.trim() && (r.description = c.description.trim() || null), c.dueDate !== n.dueDate && (r.dueDate = c.dueDate || null), Object.keys(r).length === 0) {
|
|
109
|
+
s(!1);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await t(D({ variables: {
|
|
114
|
+
id: e.id,
|
|
115
|
+
input: r
|
|
116
|
+
} })), s(!1);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
T(e instanceof Error ? e.message : "The milestone could not be saved.");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return /* @__PURE__ */ d(m, {
|
|
122
|
+
open: i,
|
|
123
|
+
onOpenChange: (e) => {
|
|
124
|
+
s(e), e ? x(n) : T(null);
|
|
125
|
+
},
|
|
126
|
+
children: [/* @__PURE__ */ u(b, {
|
|
127
|
+
asChild: !0,
|
|
128
|
+
children: /* @__PURE__ */ d(f, {
|
|
129
|
+
size: "sm",
|
|
130
|
+
variant: "outline",
|
|
131
|
+
"data-testid": "milestone-edit",
|
|
132
|
+
children: [/* @__PURE__ */ u(o, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
133
|
+
})
|
|
134
|
+
}), /* @__PURE__ */ d(h, {
|
|
135
|
+
className: "max-w-lg",
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ d(v, { children: [/* @__PURE__ */ u(y, { children: "Edit milestone" }), /* @__PURE__ */ u(g, { children: "Change the name, description or due date." })] }),
|
|
138
|
+
/* @__PURE__ */ d("div", {
|
|
139
|
+
className: "space-y-3",
|
|
140
|
+
children: [
|
|
141
|
+
/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ u("label", {
|
|
142
|
+
htmlFor: "milestone-edit-name",
|
|
143
|
+
className: "text-sm font-medium",
|
|
144
|
+
children: "Name"
|
|
145
|
+
}), /* @__PURE__ */ u(p, {
|
|
146
|
+
id: "milestone-edit-name",
|
|
147
|
+
className: "mt-1",
|
|
148
|
+
value: c.name,
|
|
149
|
+
onChange: (e) => x({
|
|
150
|
+
...c,
|
|
151
|
+
name: e.target.value
|
|
152
|
+
})
|
|
153
|
+
})] }),
|
|
154
|
+
/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ u("label", {
|
|
155
|
+
htmlFor: "milestone-edit-due",
|
|
156
|
+
className: "text-sm font-medium",
|
|
157
|
+
children: "Due date"
|
|
158
|
+
}), /* @__PURE__ */ u(p, {
|
|
159
|
+
id: "milestone-edit-due",
|
|
160
|
+
type: "date",
|
|
161
|
+
className: "mt-1",
|
|
162
|
+
value: c.dueDate,
|
|
163
|
+
onChange: (e) => x({
|
|
164
|
+
...c,
|
|
165
|
+
dueDate: e.target.value
|
|
166
|
+
})
|
|
167
|
+
})] }),
|
|
168
|
+
/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ u("label", {
|
|
169
|
+
htmlFor: "milestone-edit-description",
|
|
170
|
+
className: "text-sm font-medium",
|
|
171
|
+
children: "Description"
|
|
172
|
+
}), /* @__PURE__ */ u("textarea", {
|
|
173
|
+
id: "milestone-edit-description",
|
|
174
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]",
|
|
175
|
+
value: c.description,
|
|
176
|
+
onChange: (e) => x({
|
|
177
|
+
...c,
|
|
178
|
+
description: e.target.value
|
|
179
|
+
})
|
|
180
|
+
})] }),
|
|
181
|
+
w && /* @__PURE__ */ u(r, { message: w })
|
|
182
|
+
]
|
|
183
|
+
}),
|
|
184
|
+
/* @__PURE__ */ d(_, { children: [/* @__PURE__ */ u(f, {
|
|
185
|
+
variant: "outline",
|
|
186
|
+
onClick: () => s(!1),
|
|
187
|
+
children: "Cancel"
|
|
188
|
+
}), /* @__PURE__ */ u(f, {
|
|
189
|
+
onClick: () => void k(),
|
|
190
|
+
disabled: c.name.trim() === "" || O.loading,
|
|
191
|
+
children: O.loading ? "Saving…" : "Save"
|
|
192
|
+
})] })
|
|
193
|
+
]
|
|
194
|
+
})]
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
export { T as MilestoneHeaderActions };
|
|
199
|
+
|
|
200
|
+
//# sourceMappingURL=MilestoneControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MilestoneControls.js","names":[],"sources":["../../../src/planmagnet/controls/MilestoneControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { enumLabel } from './WorkItemControls';\n\n/**\n * Milestone controls (BOFF-7953/7963). A milestone was created Pending and could never move: no status control,\n * no edit, no delete — so the In progress / Completed / Missed / Cancelled tabs could only be empty, and the\n * \"Completed\" date stayed \"—\". Completing now also records the date (service, 2026-09-27).\n */\n\nexport const MILESTONE_STATUSES = [\n 'PENDING',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'MISSED',\n 'CANCELLED',\n] as const;\n\nconst MILESTONE_QUERIES = [\n 'GetMilestonesForDetail',\n 'GetMilestones',\n 'GetDashboardData',\n 'GetAnalyticsData',\n];\n\nconst UPDATE = gql`\n mutation MilestoneControlsUpdate($id: ID!, $input: PlanmagnetUpdateMilestoneInput!) {\n planmagnetUpdateMilestone(id: $id, input: $input) {\n id\n name\n description\n status\n dueDate\n completedAt\n }\n }\n`;\nconst DELETE = gql`\n mutation MilestoneControlsDelete($id: ID!) {\n planmagnetDeleteMilestone(id: $id)\n }\n`;\n\nexport interface MilestoneValues {\n id: string;\n name: string;\n description: string | null;\n status: string;\n dueDate: string | null;\n}\n\nexport function MilestoneHeaderActions({ milestone }: { milestone: MilestoneValues }) {\n const navigate = usePlanMagnetNavigate();\n const [update, updateState] = useMutation(UPDATE, {\n refetchQueries: MILESTONE_QUERIES,\n awaitRefetchQueries: true,\n });\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: MILESTONE_QUERIES.filter((q) => q !== 'GetMilestonesForDetail'),\n });\n const [error, setError] = useState<string | null>(null);\n const busy = updateState.loading || removeState.loading;\n const statuses = (MILESTONE_STATUSES as readonly string[]).includes(milestone.status)\n ? MILESTONE_STATUSES\n : [milestone.status, ...MILESTONE_STATUSES];\n\n const onStatus = async (status: string) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: milestone.id, input: { status } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: milestone.id } }));\n navigate('/projects/milestones-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The milestone could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Status\"\n data-testid=\"milestone-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={milestone.status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== milestone.status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <MilestoneEditDialog milestone={milestone} />\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete ${milestone.name}?`}\n description=\"The milestone is deleted. This cannot be undone here.\"\n confirmLabel=\"Delete milestone\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst day = (iso: string | null) => (iso ? iso.slice(0, 10) : '');\n\n/** Name, description and due date. Only changed fields are sent; an emptied field is cleared. */\nfunction MilestoneEditDialog({ milestone }: { milestone: MilestoneValues }) {\n const initial = {\n name: milestone.name,\n description: milestone.description ?? '',\n dueDate: day(milestone.dueDate),\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation(UPDATE, {\n refetchQueries: MILESTONE_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const save = async () => {\n setError(null);\n const input: Record<string, unknown> = {};\n if (form.name.trim() !== initial.name) input.name = form.name.trim();\n if (form.description.trim() !== initial.description.trim())\n input.description = form.description.trim() || null;\n if (form.dueDate !== initial.dueDate) input.dueDate = form.dueDate || null;\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: milestone.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The milestone could not be saved.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) setForm(initial);\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"milestone-edit\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit milestone</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Change the name, description or due date.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"milestone-edit-name\" className=\"text-sm font-medium\">\n Name\n </label>\n <Input\n id=\"milestone-edit-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=\"milestone-edit-due\" className=\"text-sm font-medium\">\n Due date\n </label>\n <Input\n id=\"milestone-edit-due\"\n type=\"date\"\n className=\"mt-1\"\n value={form.dueDate}\n onChange={(e) => setForm({ ...form, dueDate: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"milestone-edit-description\" className=\"text-sm font-medium\">\n Description\n </label>\n <textarea\n id=\"milestone-edit-description\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void save()} disabled={form.name.trim() === '' || state.loading}>\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|