@burdenoff/microfe-planmagnet 2026.927.3 → 2026.927.5
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/global-types.d.ts +9 -1
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/wspace-operations.d.ts +0 -12
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +14 -10
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +149 -127
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
- 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 +321 -0
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -0
- package/dist/planmagnet/controls/FeatureTriage.d.ts +21 -0
- package/dist/planmagnet/controls/FeatureTriage.d.ts.map +1 -0
- package/dist/planmagnet/controls/FeatureTriage.js +398 -0
- package/dist/planmagnet/controls/FeatureTriage.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 +203 -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.d.ts.map +1 -1
- package/dist/planmagnet/controls/ProjectControls.js +47 -44
- package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
- 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 +111 -0
- package/dist/planmagnet/controls/ReleaseControls.js.map +1 -0
- package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.js +68 -65
- package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
- package/dist/planmagnet/hooks/useCounts.d.ts +39 -0
- package/dist/planmagnet/hooks/useCounts.d.ts.map +1 -0
- package/dist/planmagnet/hooks/useCounts.js +209 -0
- package/dist/planmagnet/hooks/useCounts.js.map +1 -0
- package/dist/planmagnet/import/exportColumns.d.ts +52 -0
- package/dist/planmagnet/import/exportColumns.d.ts.map +1 -0
- package/dist/planmagnet/import/exportColumns.js +81 -0
- package/dist/planmagnet/import/exportColumns.js.map +1 -0
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +255 -242
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +199 -160
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +190 -153
- 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/GoalsGoalsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsGoalsPagePage.js +2 -7
- package/dist/planmagnet/pages/GoalsGoalsPagePage.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 +145 -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/ProductsFeatureAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +97 -117
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +169 -110
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +117 -103
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.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/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +4 -8
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +85 -90
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +118 -107
- 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 +4 -2
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +48 -71
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +17 -13
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js.map +1 -1
- 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/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +11 -8
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +152 -139
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +75 -74
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts +7 -0
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +39 -201
- package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +118 -99
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
- 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/TasksImportPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksImportPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksImportPagePage.js +309 -0
- package/dist/planmagnet/pages/TasksImportPagePage.js.map +1 -0
- 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 +111 -104
- package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +99 -177
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -4
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts.map +1 -1
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.js +10 -4
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +150 -137
- package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.js +32 -30
- package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
- package/dist/planmagnet/utils/mutationResult.d.ts +10 -0
- package/dist/planmagnet/utils/mutationResult.d.ts.map +1 -1
- package/dist/planmagnet/utils/mutationResult.js +2 -1
- package/dist/planmagnet/utils/mutationResult.js.map +1 -1
- package/dist/planmagnet/utils/rowNavigation.d.ts +2 -0
- package/dist/planmagnet/utils/rowNavigation.d.ts.map +1 -1
- package/dist/planmagnet/utils/rowNavigation.js +9 -8
- package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { throwOnError as t } from "../utils/mutationResult.js";
|
|
3
|
+
import { ProjectSelect as n } from "./Pickers.js";
|
|
4
|
+
import { ErrorLine as r } from "./ConfirmButton.js";
|
|
5
|
+
import { useDeferredValue as i, useState as a } from "react";
|
|
6
|
+
import { AlarmClock as o, Check as s, GitMerge as c, Sun as l, X as u } from "lucide-react";
|
|
7
|
+
import { gql as d } from "@apollo/client";
|
|
8
|
+
import { useMutation as f, useQuery as p } from "@apollo/client/react";
|
|
9
|
+
import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
10
|
+
import { Button as _, Input as v, ResponsiveDialog as y, ResponsiveDialogContent as b, ResponsiveDialogDescription as x, ResponsiveDialogFooter as S, ResponsiveDialogHeader as C, ResponsiveDialogTitle as w, ResponsiveDialogTrigger as T } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/planmagnet/controls/FeatureTriage.tsx
|
|
12
|
+
var E = {
|
|
13
|
+
refetchQueries: [
|
|
14
|
+
"GetFeatureRequest",
|
|
15
|
+
"ListFeatureRequests",
|
|
16
|
+
"CountsFeatures",
|
|
17
|
+
"ListFeatureRequestsForProduct",
|
|
18
|
+
"ListFeatureRequestsForProducts",
|
|
19
|
+
"ListAllFeaturesAnalytics",
|
|
20
|
+
"ReleaseCustomerRequests"
|
|
21
|
+
],
|
|
22
|
+
awaitRefetchQueries: !0
|
|
23
|
+
}, D = d`
|
|
24
|
+
mutation FeatureTriageAccept($id: ID!, $input: PlanmagnetAcceptFeatureRequestInput) {
|
|
25
|
+
planmagnetAcceptFeatureRequest(id: $id, input: $input) {
|
|
26
|
+
id
|
|
27
|
+
status
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`, O = d`
|
|
31
|
+
mutation FeatureTriageDecline($id: ID!, $reason: String!) {
|
|
32
|
+
planmagnetDeclineFeatureRequest(id: $id, reason: $reason) {
|
|
33
|
+
id
|
|
34
|
+
status
|
|
35
|
+
declineReason
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`, k = d`
|
|
39
|
+
mutation FeatureTriageSnooze($id: ID!, $until: DateTime) {
|
|
40
|
+
planmagnetSnoozeFeatureRequest(id: $id, until: $until) {
|
|
41
|
+
id
|
|
42
|
+
snoozedUntil
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`, A = d`
|
|
46
|
+
mutation FeatureTriageMerge($id: ID!, $intoId: ID!) {
|
|
47
|
+
planmagnetMergeFeatureRequest(id: $id, intoId: $intoId) {
|
|
48
|
+
id
|
|
49
|
+
voteCount
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`, j = d`
|
|
53
|
+
query FeatureTriageMergeCandidates($workspaceId: ID!, $filter: PlanmagnetFeatureRequestFilter) {
|
|
54
|
+
planmagnetFeatureRequests(
|
|
55
|
+
workspaceId: $workspaceId
|
|
56
|
+
filter: $filter
|
|
57
|
+
pagination: { first: 20 }
|
|
58
|
+
) {
|
|
59
|
+
items {
|
|
60
|
+
id
|
|
61
|
+
title
|
|
62
|
+
status
|
|
63
|
+
voteCount
|
|
64
|
+
}
|
|
65
|
+
totalCount
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`, M = 2e3, N = ["SUBMITTED", "UNDER_REVIEW"], P = (e) => N.includes(e), F = (e, t) => !!e && new Date(e).getTime() > t, I = (e, t) => e instanceof Error ? e.message : t;
|
|
69
|
+
function L({ trigger: e, testId: t, title: n, description: i, children: a, confirmLabel: o, canConfirm: s, busy: c, onConfirm: l, error: u, onOpenChange: d, open: f }) {
|
|
70
|
+
return /* @__PURE__ */ g(y, {
|
|
71
|
+
open: f,
|
|
72
|
+
onOpenChange: d,
|
|
73
|
+
children: [/* @__PURE__ */ h(T, {
|
|
74
|
+
asChild: !0,
|
|
75
|
+
children: /* @__PURE__ */ h(_, {
|
|
76
|
+
size: "sm",
|
|
77
|
+
variant: "outline",
|
|
78
|
+
"data-testid": t,
|
|
79
|
+
onClick: (e) => e.stopPropagation(),
|
|
80
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
81
|
+
children: e
|
|
82
|
+
})
|
|
83
|
+
}), /* @__PURE__ */ g(b, {
|
|
84
|
+
className: "max-w-lg",
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ g(C, { children: [/* @__PURE__ */ h(w, { children: n }), /* @__PURE__ */ h(x, { children: i })] }),
|
|
87
|
+
/* @__PURE__ */ g("div", {
|
|
88
|
+
className: "space-y-3",
|
|
89
|
+
children: [a, u && /* @__PURE__ */ h(r, { message: u })]
|
|
90
|
+
}),
|
|
91
|
+
/* @__PURE__ */ g(S, { children: [/* @__PURE__ */ h(_, {
|
|
92
|
+
variant: "outline",
|
|
93
|
+
onClick: () => d(!1),
|
|
94
|
+
children: "Cancel"
|
|
95
|
+
}), /* @__PURE__ */ h(_, {
|
|
96
|
+
onClick: l,
|
|
97
|
+
disabled: !s || c,
|
|
98
|
+
children: c ? "Saving…" : o
|
|
99
|
+
})] })
|
|
100
|
+
]
|
|
101
|
+
})]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function R({ feature: e }) {
|
|
105
|
+
let [r, i] = a(!1), [o, c] = a("PLANNED"), [l, u] = a(e.projectId ?? ""), [d, p] = a(null), [_, v] = f(D, E), y = async () => {
|
|
106
|
+
p(null);
|
|
107
|
+
try {
|
|
108
|
+
await t(_({ variables: {
|
|
109
|
+
id: e.id,
|
|
110
|
+
input: {
|
|
111
|
+
status: o,
|
|
112
|
+
...l && l !== e.projectId && { projectId: l }
|
|
113
|
+
}
|
|
114
|
+
} })), i(!1);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
p(I(e, "The request could not be accepted."));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return /* @__PURE__ */ g(L, {
|
|
120
|
+
open: r,
|
|
121
|
+
onOpenChange: (e) => {
|
|
122
|
+
i(e), e || p(null);
|
|
123
|
+
},
|
|
124
|
+
testId: "triage-accept",
|
|
125
|
+
trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(s, { className: "mr-1 h-4 w-4" }), "Accept"] }),
|
|
126
|
+
title: `Accept "${e.title}"`,
|
|
127
|
+
description: "Accepting takes it out of triage. Planned means it will be built; Under review means a closer look first.",
|
|
128
|
+
confirmLabel: "Accept",
|
|
129
|
+
canConfirm: !0,
|
|
130
|
+
busy: v.loading,
|
|
131
|
+
onConfirm: () => void y(),
|
|
132
|
+
error: d,
|
|
133
|
+
children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
|
|
134
|
+
htmlFor: `triage-accept-status-${e.id}`,
|
|
135
|
+
className: "text-sm font-medium",
|
|
136
|
+
children: "Accept as"
|
|
137
|
+
}), /* @__PURE__ */ g("select", {
|
|
138
|
+
id: `triage-accept-status-${e.id}`,
|
|
139
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
140
|
+
value: o,
|
|
141
|
+
onChange: (e) => c(e.target.value),
|
|
142
|
+
children: [/* @__PURE__ */ h("option", {
|
|
143
|
+
value: "PLANNED",
|
|
144
|
+
children: "Planned"
|
|
145
|
+
}), e.status !== "UNDER_REVIEW" && /* @__PURE__ */ h("option", {
|
|
146
|
+
value: "UNDER_REVIEW",
|
|
147
|
+
children: "Under review"
|
|
148
|
+
})]
|
|
149
|
+
})] }), /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
|
|
150
|
+
htmlFor: `triage-accept-project-${e.id}`,
|
|
151
|
+
className: "text-sm font-medium",
|
|
152
|
+
children: "Project that will deliver it"
|
|
153
|
+
}), /* @__PURE__ */ h("div", {
|
|
154
|
+
className: "mt-1",
|
|
155
|
+
children: /* @__PURE__ */ h(n, {
|
|
156
|
+
id: `triage-accept-project-${e.id}`,
|
|
157
|
+
value: l,
|
|
158
|
+
noneLabel: "No project yet",
|
|
159
|
+
onChange: u
|
|
160
|
+
})
|
|
161
|
+
})] })]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function z({ feature: e }) {
|
|
165
|
+
let [n, r] = a(!1), [i, o] = a(""), [s, c] = a(null), [l, d] = f(O, E), p = i.trim(), _ = p.length > M, v = async () => {
|
|
166
|
+
c(null);
|
|
167
|
+
try {
|
|
168
|
+
await t(l({ variables: {
|
|
169
|
+
id: e.id,
|
|
170
|
+
reason: p
|
|
171
|
+
} })), r(!1), o("");
|
|
172
|
+
} catch (e) {
|
|
173
|
+
c(I(e, "The request could not be declined."));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
return /* @__PURE__ */ h(L, {
|
|
177
|
+
open: n,
|
|
178
|
+
onOpenChange: (e) => {
|
|
179
|
+
r(e), e || c(null);
|
|
180
|
+
},
|
|
181
|
+
testId: "triage-decline",
|
|
182
|
+
trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(u, { className: "mr-1 h-4 w-4" }), "Decline"] }),
|
|
183
|
+
title: `Decline "${e.title}"`,
|
|
184
|
+
description: "The reason is kept on the request, so anyone who asks later can see why.",
|
|
185
|
+
confirmLabel: "Decline",
|
|
186
|
+
canConfirm: p !== "" && !_,
|
|
187
|
+
busy: d.loading,
|
|
188
|
+
onConfirm: () => void v(),
|
|
189
|
+
error: s ?? (_ ? "A decline reason can be at most 2000 characters." : null),
|
|
190
|
+
children: /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
|
|
191
|
+
htmlFor: `triage-decline-reason-${e.id}`,
|
|
192
|
+
className: "text-sm font-medium",
|
|
193
|
+
children: "Reason"
|
|
194
|
+
}), /* @__PURE__ */ h("textarea", {
|
|
195
|
+
id: `triage-decline-reason-${e.id}`,
|
|
196
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[88px]",
|
|
197
|
+
value: i,
|
|
198
|
+
"aria-invalid": _,
|
|
199
|
+
onChange: (e) => o(e.target.value)
|
|
200
|
+
})] })
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function B(e) {
|
|
204
|
+
let t = (t) => {
|
|
205
|
+
let n = new Date(e + t * 864e5);
|
|
206
|
+
return n.setHours(9, 0, 0, 0), n;
|
|
207
|
+
};
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
label: "Tomorrow",
|
|
211
|
+
until: t(1)
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
label: "In 3 days",
|
|
215
|
+
until: t(3)
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
label: "Next week",
|
|
219
|
+
until: t(7)
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: "In a month",
|
|
223
|
+
until: t(30)
|
|
224
|
+
}
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
function V({ feature: e }) {
|
|
228
|
+
let [n, r] = a(!1), [i, s] = a([]), [c, l] = a(0), [u, d] = a(null), [p, _] = f(k, E), v = async () => {
|
|
229
|
+
d(null);
|
|
230
|
+
let n = i[c];
|
|
231
|
+
if (n) try {
|
|
232
|
+
await t(p({ variables: {
|
|
233
|
+
id: e.id,
|
|
234
|
+
until: n.until.toISOString()
|
|
235
|
+
} })), r(!1);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
d(I(e, "The request could not be snoozed."));
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
return /* @__PURE__ */ h(L, {
|
|
241
|
+
open: n,
|
|
242
|
+
onOpenChange: (e) => {
|
|
243
|
+
r(e), e ? (s(B(Date.now())), l(0)) : d(null);
|
|
244
|
+
},
|
|
245
|
+
testId: "triage-snooze",
|
|
246
|
+
trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(o, { className: "mr-1 h-4 w-4" }), "Snooze"] }),
|
|
247
|
+
title: `Snooze "${e.title}"`,
|
|
248
|
+
description: "It leaves the triage queue and comes back on its own. It stays undecided meanwhile.",
|
|
249
|
+
confirmLabel: "Snooze",
|
|
250
|
+
canConfirm: i.length > 0,
|
|
251
|
+
busy: _.loading,
|
|
252
|
+
onConfirm: () => void v(),
|
|
253
|
+
error: u,
|
|
254
|
+
children: /* @__PURE__ */ g("fieldset", {
|
|
255
|
+
className: "space-y-2",
|
|
256
|
+
children: [/* @__PURE__ */ h("legend", {
|
|
257
|
+
className: "text-sm font-medium",
|
|
258
|
+
children: "Bring it back"
|
|
259
|
+
}), i.map((t, n) => /* @__PURE__ */ g("label", {
|
|
260
|
+
className: "flex items-center gap-2 text-sm",
|
|
261
|
+
children: [
|
|
262
|
+
/* @__PURE__ */ h("input", {
|
|
263
|
+
type: "radio",
|
|
264
|
+
name: `triage-snooze-${e.id}`,
|
|
265
|
+
checked: c === n,
|
|
266
|
+
onChange: () => l(n)
|
|
267
|
+
}),
|
|
268
|
+
t.label,
|
|
269
|
+
/* @__PURE__ */ g("span", {
|
|
270
|
+
className: "text-muted-foreground",
|
|
271
|
+
children: [
|
|
272
|
+
"(",
|
|
273
|
+
t.until.toLocaleDateString(void 0, {
|
|
274
|
+
weekday: "short",
|
|
275
|
+
day: "numeric",
|
|
276
|
+
month: "short"
|
|
277
|
+
}),
|
|
278
|
+
")"
|
|
279
|
+
]
|
|
280
|
+
})
|
|
281
|
+
]
|
|
282
|
+
}, t.label))]
|
|
283
|
+
})
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function H({ feature: n }) {
|
|
287
|
+
let { workspaceId: r } = e(), [o, s] = a(!1), [l, u] = a(""), [d, _] = a(""), [y, b] = a(null), x = i(l.trim()), [S, C] = f(A, E), { data: w, loading: T } = p(j, {
|
|
288
|
+
variables: {
|
|
289
|
+
workspaceId: r ?? "",
|
|
290
|
+
filter: x ? { search: x } : {}
|
|
291
|
+
},
|
|
292
|
+
skip: !r || !o
|
|
293
|
+
}), D = (w?.planmagnetFeatureRequests.items ?? []).filter((e) => e.id !== n.id && e.status !== "DUPLICATE"), O = async () => {
|
|
294
|
+
b(null);
|
|
295
|
+
try {
|
|
296
|
+
await t(S({ variables: {
|
|
297
|
+
id: n.id,
|
|
298
|
+
intoId: d
|
|
299
|
+
} })), s(!1);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
b(I(e, "The requests could not be merged."));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
return /* @__PURE__ */ h(L, {
|
|
305
|
+
open: o,
|
|
306
|
+
onOpenChange: (e) => {
|
|
307
|
+
s(e), e ? (u(""), _("")) : b(null);
|
|
308
|
+
},
|
|
309
|
+
testId: "triage-merge",
|
|
310
|
+
trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(c, { className: "mr-1 h-4 w-4" }), "Merge"] }),
|
|
311
|
+
title: `Merge "${n.title}" into its original`,
|
|
312
|
+
description: "It becomes a duplicate of the original and its votes move there (someone who voted on both keeps one vote).",
|
|
313
|
+
confirmLabel: "Merge",
|
|
314
|
+
canConfirm: d !== "",
|
|
315
|
+
busy: C.loading,
|
|
316
|
+
onConfirm: () => void O(),
|
|
317
|
+
error: y,
|
|
318
|
+
children: /* @__PURE__ */ g("div", {
|
|
319
|
+
className: "space-y-1",
|
|
320
|
+
children: [
|
|
321
|
+
/* @__PURE__ */ h("label", {
|
|
322
|
+
htmlFor: `triage-merge-into-${n.id}`,
|
|
323
|
+
className: "text-sm font-medium",
|
|
324
|
+
children: "Original request"
|
|
325
|
+
}),
|
|
326
|
+
/* @__PURE__ */ h(v, {
|
|
327
|
+
"aria-label": "Search feature requests",
|
|
328
|
+
placeholder: "Search by title",
|
|
329
|
+
value: l,
|
|
330
|
+
onChange: (e) => u(e.target.value)
|
|
331
|
+
}),
|
|
332
|
+
/* @__PURE__ */ g("select", {
|
|
333
|
+
id: `triage-merge-into-${n.id}`,
|
|
334
|
+
className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
335
|
+
value: d,
|
|
336
|
+
onChange: (e) => _(e.target.value),
|
|
337
|
+
children: [/* @__PURE__ */ h("option", {
|
|
338
|
+
value: "",
|
|
339
|
+
children: T && !w ? "Searching…" : D.length ? "Choose the original" : "No match"
|
|
340
|
+
}), D.map((e) => /* @__PURE__ */ g("option", {
|
|
341
|
+
value: e.id,
|
|
342
|
+
children: [
|
|
343
|
+
e.title,
|
|
344
|
+
" (",
|
|
345
|
+
e.voteCount,
|
|
346
|
+
" vote",
|
|
347
|
+
e.voteCount === 1 ? "" : "s",
|
|
348
|
+
")"
|
|
349
|
+
]
|
|
350
|
+
}, e.id))]
|
|
351
|
+
})
|
|
352
|
+
]
|
|
353
|
+
})
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function U({ feature: e }) {
|
|
357
|
+
let [n, i] = f(k, E), [o, s] = a(null), c = async () => {
|
|
358
|
+
s(null);
|
|
359
|
+
try {
|
|
360
|
+
await t(n({ variables: {
|
|
361
|
+
id: e.id,
|
|
362
|
+
until: null
|
|
363
|
+
} }));
|
|
364
|
+
} catch (e) {
|
|
365
|
+
s(I(e, "The request could not be woken."));
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ g(_, {
|
|
369
|
+
size: "sm",
|
|
370
|
+
variant: "outline",
|
|
371
|
+
"data-testid": "triage-wake",
|
|
372
|
+
disabled: i.loading,
|
|
373
|
+
onClick: (e) => {
|
|
374
|
+
e.stopPropagation(), c();
|
|
375
|
+
},
|
|
376
|
+
children: [/* @__PURE__ */ h(l, { className: "mr-1 h-4 w-4" }), "Back to triage"]
|
|
377
|
+
}), o && /* @__PURE__ */ h(r, { message: o })] });
|
|
378
|
+
}
|
|
379
|
+
function W({ feature: e, now: t }) {
|
|
380
|
+
if (!P(e.status)) return null;
|
|
381
|
+
let n = F(e.snoozedUntil, t), r = (e) => e.stopPropagation();
|
|
382
|
+
return /* @__PURE__ */ g("div", {
|
|
383
|
+
className: "flex flex-wrap items-center gap-2",
|
|
384
|
+
"data-testid": "triage-actions",
|
|
385
|
+
onClick: r,
|
|
386
|
+
onKeyDown: r,
|
|
387
|
+
children: [
|
|
388
|
+
/* @__PURE__ */ h(R, { feature: e }),
|
|
389
|
+
/* @__PURE__ */ h(z, { feature: e }),
|
|
390
|
+
e.status === "SUBMITTED" && h(n ? U : V, { feature: e }),
|
|
391
|
+
/* @__PURE__ */ h(H, { feature: e })
|
|
392
|
+
]
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
export { W as FeatureTriageActions, P as isOpenRequest, F as isSnoozed };
|
|
397
|
+
|
|
398
|
+
//# sourceMappingURL=FeatureTriage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureTriage.js","names":[],"sources":["../../../src/planmagnet/controls/FeatureTriage.tsx"],"sourcesContent":["import { useDeferredValue, useState, type ReactNode } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } 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 { AlarmClock, Check, GitMerge, Sun, X } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ErrorLine } from './ConfirmButton';\nimport { ProjectSelect } from './Pickers';\n\n/**\n * Triage for incoming feature requests (BUILD_PLAN P1.3): accept, decline with a reason, snooze, merge into the\n * original. Linear's triage is these four plus commenting. The feedback UI had none of them — a request could only\n * have its status flipped, so a decline carried no reason and a duplicate kept its votes away from the original.\n */\n\n/** Every query that shows a feature request or feature counts. */\nconst TRIAGE_REFETCH = [\n 'GetFeatureRequest',\n 'ListFeatureRequests',\n 'CountsFeatures',\n 'ListFeatureRequestsForProduct',\n 'ListFeatureRequestsForProducts',\n 'ListAllFeaturesAnalytics',\n 'ReleaseCustomerRequests',\n];\nconst options = { refetchQueries: TRIAGE_REFETCH, awaitRefetchQueries: true };\n\nconst ACCEPT = gql`\n mutation FeatureTriageAccept($id: ID!, $input: PlanmagnetAcceptFeatureRequestInput) {\n planmagnetAcceptFeatureRequest(id: $id, input: $input) {\n id\n status\n }\n }\n`;\nconst DECLINE = gql`\n mutation FeatureTriageDecline($id: ID!, $reason: String!) {\n planmagnetDeclineFeatureRequest(id: $id, reason: $reason) {\n id\n status\n declineReason\n }\n }\n`;\nconst SNOOZE = gql`\n mutation FeatureTriageSnooze($id: ID!, $until: DateTime) {\n planmagnetSnoozeFeatureRequest(id: $id, until: $until) {\n id\n snoozedUntil\n }\n }\n`;\nconst MERGE = gql`\n mutation FeatureTriageMerge($id: ID!, $intoId: ID!) {\n planmagnetMergeFeatureRequest(id: $id, intoId: $intoId) {\n id\n voteCount\n }\n }\n`;\nconst MERGE_CANDIDATES = gql`\n query FeatureTriageMergeCandidates($workspaceId: ID!, $filter: PlanmagnetFeatureRequestFilter) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: $filter\n pagination: { first: 20 }\n ) {\n items {\n id\n title\n status\n voteCount\n }\n totalCount\n }\n }\n`;\n\nexport const DECLINE_REASON_MAX = 2000;\nconst OPEN = ['SUBMITTED', 'UNDER_REVIEW'];\n\nexport interface TriageFeature {\n id: string;\n title: string;\n status: string;\n projectId?: string | null;\n snoozedUntil?: string | null;\n}\n\n/** True while the request waits for a decision (accept, decline and merge apply). */\nexport const isOpenRequest = (status: string) => OPEN.includes(status);\n/** A snooze that has not passed yet. */\nexport const isSnoozed = (snoozedUntil: string | null | undefined, now: number) =>\n !!snoozedUntil && new Date(snoozedUntil).getTime() > now;\n\nconst errorText = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\n\n/** A dialog with one action; the trigger never propagates to a clickable row it sits in. */\nfunction ActionDialog({\n trigger,\n testId,\n title,\n description,\n children,\n confirmLabel,\n canConfirm,\n busy,\n onConfirm,\n error,\n onOpenChange,\n open,\n}: {\n trigger: ReactNode;\n testId: string;\n title: string;\n description: string;\n children: ReactNode;\n confirmLabel: string;\n canConfirm: boolean;\n busy: boolean;\n onConfirm: () => void;\n error: string | null;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid={testId}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => e.stopPropagation()}\n >\n {trigger}\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n {children}\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={onConfirm} disabled={!canConfirm || busy}>\n {busy ? 'Saving…' : confirmLabel}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction AcceptDialog({ feature }: { feature: TriageFeature }) {\n const [open, setOpen] = useState(false);\n const [status, setStatus] = useState<'PLANNED' | 'UNDER_REVIEW'>('PLANNED');\n const [projectId, setProjectId] = useState(feature.projectId ?? '');\n const [error, setError] = useState<string | null>(null);\n const [accept, state] = useMutation(ACCEPT, options);\n const submit = async () => {\n setError(null);\n try {\n await throwOnError(\n accept({\n variables: {\n id: feature.id,\n input: { status, ...(projectId && projectId !== feature.projectId && { projectId }) },\n },\n })\n );\n setOpen(false);\n } catch (e) {\n setError(errorText(e, 'The request could not be accepted.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n testId=\"triage-accept\"\n trigger={\n <>\n <Check className=\"mr-1 h-4 w-4\" />\n Accept\n </>\n }\n title={`Accept \"${feature.title}\"`}\n description=\"Accepting takes it out of triage. Planned means it will be built; Under review means a closer look first.\"\n confirmLabel=\"Accept\"\n canConfirm\n busy={state.loading}\n onConfirm={() => void submit()}\n error={error}\n >\n <div>\n <label htmlFor={`triage-accept-status-${feature.id}`} className=\"text-sm font-medium\">\n Accept as\n </label>\n <select\n id={`triage-accept-status-${feature.id}`}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={status}\n onChange={(e) => setStatus(e.target.value as 'PLANNED' | 'UNDER_REVIEW')}\n >\n <option value=\"PLANNED\">Planned</option>\n {feature.status !== 'UNDER_REVIEW' && <option value=\"UNDER_REVIEW\">Under review</option>}\n </select>\n </div>\n <div>\n <label htmlFor={`triage-accept-project-${feature.id}`} className=\"text-sm font-medium\">\n Project that will deliver it\n </label>\n <div className=\"mt-1\">\n <ProjectSelect\n id={`triage-accept-project-${feature.id}`}\n value={projectId}\n noneLabel=\"No project yet\"\n onChange={setProjectId}\n />\n </div>\n </div>\n </ActionDialog>\n );\n}\n\nfunction DeclineDialog({ feature }: { feature: TriageFeature }) {\n const [open, setOpen] = useState(false);\n const [reason, setReason] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [decline, state] = useMutation(DECLINE, options);\n const trimmed = reason.trim();\n const tooLong = trimmed.length > DECLINE_REASON_MAX;\n const submit = async () => {\n setError(null);\n try {\n await throwOnError(decline({ variables: { id: feature.id, reason: trimmed } }));\n setOpen(false);\n setReason('');\n } catch (e) {\n setError(errorText(e, 'The request could not be declined.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n testId=\"triage-decline\"\n trigger={\n <>\n <X className=\"mr-1 h-4 w-4\" />\n Decline\n </>\n }\n title={`Decline \"${feature.title}\"`}\n description=\"The reason is kept on the request, so anyone who asks later can see why.\"\n confirmLabel=\"Decline\"\n canConfirm={trimmed !== '' && !tooLong}\n busy={state.loading}\n onConfirm={() => void submit()}\n error={\n error ??\n (tooLong ? `A decline reason can be at most ${DECLINE_REASON_MAX} characters.` : null)\n }\n >\n <div>\n <label htmlFor={`triage-decline-reason-${feature.id}`} className=\"text-sm font-medium\">\n Reason\n </label>\n <textarea\n id={`triage-decline-reason-${feature.id}`}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[88px]\"\n value={reason}\n aria-invalid={tooLong}\n onChange={(e) => setReason(e.target.value)}\n />\n </div>\n </ActionDialog>\n );\n}\n\n/** Snooze presets are computed when the dialog opens, never during render. */\nfunction snoozePresets(from: number) {\n const day = 86_400_000;\n const morning = (days: number) => {\n const d = new Date(from + days * day);\n d.setHours(9, 0, 0, 0);\n return d;\n };\n return [\n { label: 'Tomorrow', until: morning(1) },\n { label: 'In 3 days', until: morning(3) },\n { label: 'Next week', until: morning(7) },\n { label: 'In a month', until: morning(30) },\n ];\n}\n\nfunction SnoozeDialog({ feature }: { feature: TriageFeature }) {\n const [open, setOpen] = useState(false);\n const [presets, setPresets] = useState<{ label: string; until: Date }[]>([]);\n const [choice, setChoice] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [snooze, state] = useMutation(SNOOZE, options);\n const submit = async () => {\n setError(null);\n const preset = presets[choice];\n if (!preset) return;\n try {\n await throwOnError(\n snooze({ variables: { id: feature.id, until: preset.until.toISOString() } })\n );\n setOpen(false);\n } catch (e) {\n setError(errorText(e, 'The request could not be snoozed.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n setPresets(snoozePresets(Date.now()));\n setChoice(0);\n } else setError(null);\n }}\n testId=\"triage-snooze\"\n trigger={\n <>\n <AlarmClock className=\"mr-1 h-4 w-4\" />\n Snooze\n </>\n }\n title={`Snooze \"${feature.title}\"`}\n description=\"It leaves the triage queue and comes back on its own. It stays undecided meanwhile.\"\n confirmLabel=\"Snooze\"\n canConfirm={presets.length > 0}\n busy={state.loading}\n onConfirm={() => void submit()}\n error={error}\n >\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium\">Bring it back</legend>\n {presets.map((p, i) => (\n <label key={p.label} className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n name={`triage-snooze-${feature.id}`}\n checked={choice === i}\n onChange={() => setChoice(i)}\n />\n {p.label}\n <span className=\"text-muted-foreground\">\n (\n {p.until.toLocaleDateString(undefined, {\n weekday: 'short',\n day: 'numeric',\n month: 'short',\n })}\n )\n </span>\n </label>\n ))}\n </fieldset>\n </ActionDialog>\n );\n}\n\nfunction MergeDialog({ feature }: { feature: TriageFeature }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [intoId, setIntoId] = useState('');\n const [error, setError] = useState<string | null>(null);\n const deferred = useDeferredValue(search.trim());\n const [merge, state] = useMutation(MERGE, options);\n const { data, loading } = useQuery<{\n planmagnetFeatureRequests: {\n items: { id: string; title: string; status: string; voteCount: number }[];\n totalCount: number;\n };\n }>(MERGE_CANDIDATES, {\n variables: { workspaceId: workspaceId ?? '', filter: deferred ? { search: deferred } : {} },\n skip: !workspaceId || !open,\n });\n // A request cannot merge into itself, and a duplicate is not an original.\n const candidates = (data?.planmagnetFeatureRequests.items ?? []).filter(\n (f) => f.id !== feature.id && f.status !== 'DUPLICATE'\n );\n const submit = async () => {\n setError(null);\n try {\n await throwOnError(merge({ variables: { id: feature.id, intoId } }));\n setOpen(false);\n } catch (e) {\n setError(errorText(e, 'The requests could not be merged.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n setSearch('');\n setIntoId('');\n } else setError(null);\n }}\n testId=\"triage-merge\"\n trigger={\n <>\n <GitMerge className=\"mr-1 h-4 w-4\" />\n Merge\n </>\n }\n title={`Merge \"${feature.title}\" into its original`}\n description=\"It becomes a duplicate of the original and its votes move there (someone who voted on both keeps one vote).\"\n confirmLabel=\"Merge\"\n canConfirm={intoId !== ''}\n busy={state.loading}\n onConfirm={() => void submit()}\n error={error}\n >\n <div className=\"space-y-1\">\n <label htmlFor={`triage-merge-into-${feature.id}`} className=\"text-sm font-medium\">\n Original request\n </label>\n <Input\n aria-label=\"Search feature requests\"\n placeholder=\"Search by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={`triage-merge-into-${feature.id}`}\n className=\"w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={intoId}\n onChange={(e) => setIntoId(e.target.value)}\n >\n <option value=\"\">\n {loading && !data\n ? 'Searching…'\n : candidates.length\n ? 'Choose the original'\n : 'No match'}\n </option>\n {candidates.map((f) => (\n <option key={f.id} value={f.id}>\n {f.title} ({f.voteCount} vote{f.voteCount === 1 ? '' : 's'})\n </option>\n ))}\n </select>\n </div>\n </ActionDialog>\n );\n}\n\nfunction WakeButton({ feature }: { feature: TriageFeature }) {\n const [snooze, state] = useMutation(SNOOZE, options);\n const [error, setError] = useState<string | null>(null);\n const wake = async () => {\n setError(null);\n try {\n await throwOnError(snooze({ variables: { id: feature.id, until: null } }));\n } catch (e) {\n setError(errorText(e, 'The request could not be woken.'));\n }\n };\n return (\n <>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"triage-wake\"\n disabled={state.loading}\n onClick={(e) => {\n e.stopPropagation();\n void wake();\n }}\n >\n <Sun className=\"mr-1 h-4 w-4\" />\n Back to triage\n </Button>\n {error && <ErrorLine message={error} />}\n </>\n );\n}\n\n/**\n * The four triage actions for an undecided request. Snooze applies only while it waits for triage (SUBMITTED);\n * a snoozed request offers \"Back to triage\" instead.\n */\nexport function FeatureTriageActions({ feature, now }: { feature: TriageFeature; now: number }) {\n if (!isOpenRequest(feature.status)) return null;\n const snoozed = isSnoozed(feature.snoozedUntil, now);\n // React events bubble through portals: without this, a click or Enter inside a dialog reached the clickable\n // list row around these actions and opened the request instead.\n const contain = (e: { stopPropagation: () => void }) => e.stopPropagation();\n return (\n <div\n className=\"flex flex-wrap items-center gap-2\"\n data-testid=\"triage-actions\"\n onClick={contain}\n onKeyDown={contain}\n >\n <AcceptDialog feature={feature} />\n <DeclineDialog feature={feature} />\n {feature.status === 'SUBMITTED' &&\n (snoozed ? <WakeButton feature={feature} /> : <SnoozeDialog feature={feature} />)}\n <MergeDialog feature={feature} />\n </div>\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,2CAqEnF"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { LEAVES_PAGE_CONTEXT as t, throwOnError as n } from "../utils/mutationResult.js";
|
|
3
|
+
import { ConfirmButton as r, ErrorLine as i } from "./ConfirmButton.js";
|
|
4
|
+
import { enumLabel as a } from "./WorkItemControls.js";
|
|
5
|
+
import { useState as o } from "react";
|
|
6
|
+
import { Pencil as s, Trash2 as c } from "lucide-react";
|
|
7
|
+
import { gql as l } from "@apollo/client";
|
|
8
|
+
import { useMutation as u } from "@apollo/client/react";
|
|
9
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
10
|
+
import { Button as p, Input as m, ResponsiveDialog as h, ResponsiveDialogContent as g, ResponsiveDialogDescription as _, ResponsiveDialogFooter as v, ResponsiveDialogHeader as y, ResponsiveDialogTitle as b, ResponsiveDialogTrigger as x } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/planmagnet/controls/MilestoneControls.tsx
|
|
12
|
+
var S = [
|
|
13
|
+
"PENDING",
|
|
14
|
+
"IN_PROGRESS",
|
|
15
|
+
"COMPLETED",
|
|
16
|
+
"MISSED",
|
|
17
|
+
"CANCELLED"
|
|
18
|
+
], C = [
|
|
19
|
+
"GetMilestonesForDetail",
|
|
20
|
+
"GetMilestones",
|
|
21
|
+
"GetDashboardData",
|
|
22
|
+
"GetAnalyticsData"
|
|
23
|
+
], w = l`
|
|
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
|
+
`, T = l`
|
|
35
|
+
mutation MilestoneControlsDelete($id: ID!) {
|
|
36
|
+
planmagnetDeleteMilestone(id: $id)
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
function E({ milestone: s }) {
|
|
40
|
+
let l = e(), [p, m] = u(w, {
|
|
41
|
+
refetchQueries: C,
|
|
42
|
+
awaitRefetchQueries: !0
|
|
43
|
+
}), [h, g] = u(T, {
|
|
44
|
+
refetchQueries: C.filter((e) => e !== "GetMilestonesForDetail"),
|
|
45
|
+
context: t
|
|
46
|
+
}), [_, v] = o(null), y = m.loading || g.loading, b = S.includes(s.status) ? S : [s.status, ...S], x = async (e) => {
|
|
47
|
+
v(null);
|
|
48
|
+
try {
|
|
49
|
+
await n(p({ variables: {
|
|
50
|
+
id: s.id,
|
|
51
|
+
input: { status: e }
|
|
52
|
+
} }));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
v(e instanceof Error ? e.message : "The status could not be changed.");
|
|
55
|
+
}
|
|
56
|
+
}, E = async () => {
|
|
57
|
+
v(null);
|
|
58
|
+
try {
|
|
59
|
+
await n(h({ variables: { id: s.id } })), l("/projects/milestones-page");
|
|
60
|
+
} catch (e) {
|
|
61
|
+
v(e instanceof Error ? e.message : "The milestone could not be deleted.");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return /* @__PURE__ */ f("div", {
|
|
65
|
+
className: "flex flex-col items-end gap-2",
|
|
66
|
+
children: [/* @__PURE__ */ f("div", {
|
|
67
|
+
className: "flex flex-wrap items-center justify-end gap-2",
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ d("select", {
|
|
70
|
+
"aria-label": "Status",
|
|
71
|
+
"data-testid": "milestone-status",
|
|
72
|
+
className: "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60",
|
|
73
|
+
value: s.status,
|
|
74
|
+
disabled: y,
|
|
75
|
+
onChange: (e) => {
|
|
76
|
+
e.target.value !== s.status && x(e.target.value);
|
|
77
|
+
},
|
|
78
|
+
children: b.map((e) => /* @__PURE__ */ d("option", {
|
|
79
|
+
value: e,
|
|
80
|
+
children: a(e)
|
|
81
|
+
}, e))
|
|
82
|
+
}),
|
|
83
|
+
/* @__PURE__ */ d(O, { milestone: s }),
|
|
84
|
+
/* @__PURE__ */ d(r, {
|
|
85
|
+
label: "Delete",
|
|
86
|
+
variant: "outline",
|
|
87
|
+
icon: /* @__PURE__ */ d(c, { className: "mr-1 h-4 w-4" }),
|
|
88
|
+
title: `Delete ${s.name}?`,
|
|
89
|
+
description: "The milestone is deleted. This cannot be undone here.",
|
|
90
|
+
confirmLabel: "Delete milestone",
|
|
91
|
+
destructive: !0,
|
|
92
|
+
disabled: y,
|
|
93
|
+
onConfirm: () => void E()
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
}), _ && /* @__PURE__ */ d(i, { message: _ })]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
var D = (e) => e ? e.slice(0, 10) : "";
|
|
100
|
+
function O({ milestone: e }) {
|
|
101
|
+
let t = {
|
|
102
|
+
name: e.name,
|
|
103
|
+
description: e.description ?? "",
|
|
104
|
+
dueDate: D(e.dueDate)
|
|
105
|
+
}, [r, a] = o(!1), [c, l] = o(t), [S, T] = o(null), [E, O] = u(w, {
|
|
106
|
+
refetchQueries: C,
|
|
107
|
+
awaitRefetchQueries: !0
|
|
108
|
+
}), k = async () => {
|
|
109
|
+
T(null);
|
|
110
|
+
let r = {};
|
|
111
|
+
if (c.name.trim() !== t.name && (r.name = c.name.trim()), c.description.trim() !== t.description.trim() && (r.description = c.description.trim() || null), c.dueDate !== t.dueDate && (r.dueDate = c.dueDate || null), Object.keys(r).length === 0) {
|
|
112
|
+
a(!1);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await n(E({ variables: {
|
|
117
|
+
id: e.id,
|
|
118
|
+
input: r
|
|
119
|
+
} })), a(!1);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
T(e instanceof Error ? e.message : "The milestone could not be saved.");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return /* @__PURE__ */ f(h, {
|
|
125
|
+
open: r,
|
|
126
|
+
onOpenChange: (e) => {
|
|
127
|
+
a(e), e ? l(t) : T(null);
|
|
128
|
+
},
|
|
129
|
+
children: [/* @__PURE__ */ d(x, {
|
|
130
|
+
asChild: !0,
|
|
131
|
+
children: /* @__PURE__ */ f(p, {
|
|
132
|
+
size: "sm",
|
|
133
|
+
variant: "outline",
|
|
134
|
+
"data-testid": "milestone-edit",
|
|
135
|
+
children: [/* @__PURE__ */ d(s, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
136
|
+
})
|
|
137
|
+
}), /* @__PURE__ */ f(g, {
|
|
138
|
+
className: "max-w-lg",
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ f(y, { children: [/* @__PURE__ */ d(b, { children: "Edit milestone" }), /* @__PURE__ */ d(_, { children: "Change the name, description or due date." })] }),
|
|
141
|
+
/* @__PURE__ */ f("div", {
|
|
142
|
+
className: "space-y-3",
|
|
143
|
+
children: [
|
|
144
|
+
/* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("label", {
|
|
145
|
+
htmlFor: "milestone-edit-name",
|
|
146
|
+
className: "text-sm font-medium",
|
|
147
|
+
children: "Name"
|
|
148
|
+
}), /* @__PURE__ */ d(m, {
|
|
149
|
+
id: "milestone-edit-name",
|
|
150
|
+
className: "mt-1",
|
|
151
|
+
value: c.name,
|
|
152
|
+
onChange: (e) => l({
|
|
153
|
+
...c,
|
|
154
|
+
name: e.target.value
|
|
155
|
+
})
|
|
156
|
+
})] }),
|
|
157
|
+
/* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("label", {
|
|
158
|
+
htmlFor: "milestone-edit-due",
|
|
159
|
+
className: "text-sm font-medium",
|
|
160
|
+
children: "Due date"
|
|
161
|
+
}), /* @__PURE__ */ d(m, {
|
|
162
|
+
id: "milestone-edit-due",
|
|
163
|
+
type: "date",
|
|
164
|
+
className: "mt-1",
|
|
165
|
+
value: c.dueDate,
|
|
166
|
+
onChange: (e) => l({
|
|
167
|
+
...c,
|
|
168
|
+
dueDate: e.target.value
|
|
169
|
+
})
|
|
170
|
+
})] }),
|
|
171
|
+
/* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("label", {
|
|
172
|
+
htmlFor: "milestone-edit-description",
|
|
173
|
+
className: "text-sm font-medium",
|
|
174
|
+
children: "Description"
|
|
175
|
+
}), /* @__PURE__ */ d("textarea", {
|
|
176
|
+
id: "milestone-edit-description",
|
|
177
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]",
|
|
178
|
+
value: c.description,
|
|
179
|
+
onChange: (e) => l({
|
|
180
|
+
...c,
|
|
181
|
+
description: e.target.value
|
|
182
|
+
})
|
|
183
|
+
})] }),
|
|
184
|
+
S && /* @__PURE__ */ d(i, { message: S })
|
|
185
|
+
]
|
|
186
|
+
}),
|
|
187
|
+
/* @__PURE__ */ f(v, { children: [/* @__PURE__ */ d(p, {
|
|
188
|
+
variant: "outline",
|
|
189
|
+
onClick: () => a(!1),
|
|
190
|
+
children: "Cancel"
|
|
191
|
+
}), /* @__PURE__ */ d(p, {
|
|
192
|
+
onClick: () => void k(),
|
|
193
|
+
disabled: c.name.trim() === "" || O.loading,
|
|
194
|
+
children: O.loading ? "Saving…" : "Save"
|
|
195
|
+
})] })
|
|
196
|
+
]
|
|
197
|
+
})]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
export { E as MilestoneHeaderActions };
|
|
202
|
+
|
|
203
|
+
//# sourceMappingURL=MilestoneControls.js.map
|