@burdenoff/microfe-planmagnet 2026.1001.8 → 2026.1002.2
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 +5 -0
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/wspace-operations.d.ts +1814 -401
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +5167 -54
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +172 -160
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +95 -83
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts +16 -0
- package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts.map +1 -0
- package/dist/planmagnet/comments/CommentAttachmentUpload.js +218 -0
- package/dist/planmagnet/comments/CommentAttachmentUpload.js.map +1 -0
- package/dist/planmagnet/comments/CommentAttachments.d.ts +15 -0
- package/dist/planmagnet/comments/CommentAttachments.d.ts.map +1 -0
- package/dist/planmagnet/comments/CommentAttachments.js +74 -0
- package/dist/planmagnet/comments/CommentAttachments.js.map +1 -0
- package/dist/planmagnet/comments/CommentBody.d.ts +11 -0
- package/dist/planmagnet/comments/CommentBody.d.ts.map +1 -0
- package/dist/planmagnet/comments/CommentBody.js +130 -0
- package/dist/planmagnet/comments/CommentBody.js.map +1 -0
- package/dist/planmagnet/comments/CommentThread.d.ts +7 -2
- package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
- package/dist/planmagnet/comments/CommentThread.js +288 -233
- package/dist/planmagnet/comments/CommentThread.js.map +1 -1
- package/dist/planmagnet/comments/MentionComposer.d.ts +0 -7
- package/dist/planmagnet/comments/MentionComposer.d.ts.map +1 -1
- package/dist/planmagnet/comments/MentionComposer.js +225 -86
- package/dist/planmagnet/comments/MentionComposer.js.map +1 -1
- package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/TargetComments.js +44 -6
- package/dist/planmagnet/comments/TargetComments.js.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.js +22 -3
- package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
- package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts +102 -0
- package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts.map +1 -0
- package/dist/planmagnet/comments/commentAttachmentUploadClient.js +298 -0
- package/dist/planmagnet/comments/commentAttachmentUploadClient.js.map +1 -0
- package/dist/planmagnet/comments/commentImageTokens.d.ts +5 -0
- package/dist/planmagnet/comments/commentImageTokens.d.ts.map +1 -0
- package/dist/planmagnet/comments/commentImageTokens.js +14 -0
- package/dist/planmagnet/comments/commentImageTokens.js.map +1 -0
- package/dist/planmagnet/comments/commentTokens.d.ts +31 -0
- package/dist/planmagnet/comments/commentTokens.d.ts.map +1 -0
- package/dist/planmagnet/comments/commentTokens.js +142 -0
- package/dist/planmagnet/comments/commentTokens.js.map +1 -0
- package/dist/planmagnet/comments/fileDisplay.d.ts +22 -0
- package/dist/planmagnet/comments/fileDisplay.d.ts.map +1 -0
- package/dist/planmagnet/comments/fileDisplay.js +48 -0
- package/dist/planmagnet/comments/fileDisplay.js.map +1 -0
- package/dist/planmagnet/comments/useCommentFileUrl.d.ts +22 -0
- package/dist/planmagnet/comments/useCommentFileUrl.d.ts.map +1 -0
- package/dist/planmagnet/comments/useCommentFileUrl.js +54 -0
- package/dist/planmagnet/comments/useCommentFileUrl.js.map +1 -0
- package/dist/planmagnet/controls/Pickers.d.ts +7 -2
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +10 -9
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/index.d.ts +1 -1
- package/dist/planmagnet/index.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +4 -4
- 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 +133 -131
- 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 +5 -5
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +6 -6
- 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 +105 -82
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js +174 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js +350 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js +168 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +156 -154
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts +29 -0
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts.map +1 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js +373 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js.map +1 -0
- package/dist/planmagnet/product/ProductControls.js +1 -1
- package/dist/planmagnet/product/ProductControls.js.map +1 -1
- package/dist/planmagnet/product/ProductFeatureControls.d.ts +39 -0
- package/dist/planmagnet/product/ProductFeatureControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureControls.js +315 -0
- package/dist/planmagnet/product/ProductFeatureControls.js.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts +4 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js +192 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts +52 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.js +417 -0
- package/dist/planmagnet/product/ProductLineControls.js.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts +4 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js +129 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts +4 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js +41 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js.map +1 -0
- package/dist/planmagnet/product/hierarchy.d.ts +127 -0
- package/dist/planmagnet/product/hierarchy.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchy.js +173 -0
- package/dist/planmagnet/product/hierarchy.js.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts +35 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.js +440 -0
- package/dist/planmagnet/product/hierarchyOperations.js.map +1 -0
- package/dist/planmagnet/product/refusal.d.ts +14 -0
- package/dist/planmagnet/product/refusal.d.ts.map +1 -0
- package/dist/planmagnet/product/refusal.js +10 -0
- package/dist/planmagnet/product/refusal.js.map +1 -0
- package/dist/planmagnet/product/routes.d.ts +4 -0
- package/dist/planmagnet/product/routes.d.ts.map +1 -1
- package/dist/planmagnet/product/routes.js +2 -2
- package/dist/planmagnet/product/routes.js.map +1 -1
- package/dist/shared/utils/i18n.d.ts +3 -0
- package/dist/shared/utils/i18n.d.ts.map +1 -1
- package/dist/shared/utils/i18n.js +1 -1
- package/dist/shared/utils/i18n.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { useTr as n } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { LEAVES_PAGE_CONTEXT as r, throwOnError as i } from "../utils/mutationResult.js";
|
|
4
|
+
import { ConfirmButton as a, ErrorLine as o } from "../controls/ConfirmButton.js";
|
|
5
|
+
import { AssigneeSelect as s, enumLabel as c } from "../controls/WorkItemControls.js";
|
|
6
|
+
import { detailPath as l } from "../utils/rowNavigation.js";
|
|
7
|
+
import { ARCHIVE_PRODUCT_FEATURE as u, CREATE_PRODUCT_FEATURE as d, DELETE_PRODUCT_FEATURE as f, PRODUCT_FEATURES_LIST as p, PRODUCT_FEATURE_QUERIES as m, UNARCHIVE_PRODUCT_FEATURE as h, UPDATE_PRODUCT_FEATURE as g } from "./hierarchyOperations.js";
|
|
8
|
+
import { PRODUCT_FEATURE_STATUSES as _, changedFields as v, normalizeText as y, productFeatureStatusVariant as b } from "./hierarchy.js";
|
|
9
|
+
import { refusalMessage as x } from "./refusal.js";
|
|
10
|
+
import { useState as S } from "react";
|
|
11
|
+
import { Archive as C, ArchiveRestore as w, Pencil as T, Plus as E, Puzzle as D, Trash2 as O } from "lucide-react";
|
|
12
|
+
import { useMutation as k, useQuery as A } from "@apollo/client/react";
|
|
13
|
+
import { jsx as j, jsxs as M } from "react/jsx-runtime";
|
|
14
|
+
import { Badge as N, Button as P, Input as F, ResponsiveDialog as I, ResponsiveDialogContent as L, ResponsiveDialogDescription as R, ResponsiveDialogFooter as z, ResponsiveDialogHeader as B, ResponsiveDialogTitle as V, ResponsiveDialogTrigger as H } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
//#region src/planmagnet/product/ProductFeatureControls.tsx
|
|
16
|
+
var U = "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]", W = "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60", G = {
|
|
17
|
+
refetchQueries: m,
|
|
18
|
+
awaitRefetchQueries: !0
|
|
19
|
+
};
|
|
20
|
+
function K() {
|
|
21
|
+
return n()("planmagnet.productFeatures.errors.forbidden", "Your role cannot change this product’s features.");
|
|
22
|
+
}
|
|
23
|
+
function q({ status: e }) {
|
|
24
|
+
return /* @__PURE__ */ j(N, {
|
|
25
|
+
variant: b(e),
|
|
26
|
+
className: "text-xs",
|
|
27
|
+
"data-testid": "product-feature-status",
|
|
28
|
+
children: c(e)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function J({ productId: t, feature: r, disabled: a = !1 }) {
|
|
32
|
+
let l = n(), u = K(), { currentUser: f } = e(), p = {
|
|
33
|
+
name: r?.name ?? "",
|
|
34
|
+
description: r?.description ?? "",
|
|
35
|
+
status: r?.status ?? "BACKLOG",
|
|
36
|
+
ownerId: r ? r.ownerId ?? "" : f?.id ?? ""
|
|
37
|
+
}, [m, h] = S(!1), [b, C] = S(p), [w, D] = S(null), [O, A] = k(d, G), [N, q] = k(g, G), J = A.loading || q.loading, Y = b.name.trim() !== "" && !J, X = r ? `product-feature-edit-${r.id}` : `product-feature-new-${t}`, Z = _.includes(b.status) ? _ : [b.status, ..._], Q = async () => {
|
|
38
|
+
D(null);
|
|
39
|
+
let e = {
|
|
40
|
+
name: b.name.trim(),
|
|
41
|
+
description: y(b.description),
|
|
42
|
+
status: b.status,
|
|
43
|
+
ownerId: b.ownerId || null
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
if (r) {
|
|
47
|
+
let t = v({
|
|
48
|
+
name: r.name.trim(),
|
|
49
|
+
description: y(r.description ?? ""),
|
|
50
|
+
status: r.status,
|
|
51
|
+
ownerId: r.ownerId
|
|
52
|
+
}, e);
|
|
53
|
+
Object.keys(t).length > 0 && await i(N({ variables: {
|
|
54
|
+
id: r.id,
|
|
55
|
+
input: t
|
|
56
|
+
} }));
|
|
57
|
+
} else await i(O({ variables: { input: {
|
|
58
|
+
productId: t,
|
|
59
|
+
name: e.name,
|
|
60
|
+
status: e.status,
|
|
61
|
+
...e.description ? { description: e.description } : {},
|
|
62
|
+
...e.ownerId ? { ownerId: e.ownerId } : {}
|
|
63
|
+
} } }));
|
|
64
|
+
h(!1);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
D(x(e, r ? l("planmagnet.productFeatures.errors.save", "The feature could not be saved.") : l("planmagnet.productFeatures.errors.create", "The feature could not be created."), u));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
return /* @__PURE__ */ M(I, {
|
|
70
|
+
open: m,
|
|
71
|
+
onOpenChange: (e) => {
|
|
72
|
+
h(e), e ? C(p) : D(null);
|
|
73
|
+
},
|
|
74
|
+
children: [/* @__PURE__ */ j(H, {
|
|
75
|
+
asChild: !0,
|
|
76
|
+
children: r ? /* @__PURE__ */ M(P, {
|
|
77
|
+
size: "sm",
|
|
78
|
+
variant: "outline",
|
|
79
|
+
"data-testid": "product-feature-edit",
|
|
80
|
+
children: [/* @__PURE__ */ j(T, { className: "mr-1 h-4 w-4" }), l("planmagnet.productFeatures.actions.edit", "Edit")]
|
|
81
|
+
}) : /* @__PURE__ */ M(P, {
|
|
82
|
+
size: "sm",
|
|
83
|
+
disabled: a,
|
|
84
|
+
"data-testid": "product-feature-new",
|
|
85
|
+
children: [/* @__PURE__ */ j(E, { className: "mr-1 h-4 w-4" }), l("planmagnet.productFeatures.actions.new", "New feature")]
|
|
86
|
+
})
|
|
87
|
+
}), /* @__PURE__ */ M(L, {
|
|
88
|
+
className: "max-w-lg",
|
|
89
|
+
children: [
|
|
90
|
+
/* @__PURE__ */ M(B, { children: [/* @__PURE__ */ j(V, { children: r ? l("planmagnet.productFeatures.dialog.editTitle", "Edit feature") : l("planmagnet.productFeatures.dialog.newTitle", "New feature") }), /* @__PURE__ */ j(R, { children: l("planmagnet.productFeatures.dialog.description", "A feature is a capability the team decided to build. Customer asks stay feature requests — link them to the feature that answers them.") })] }),
|
|
91
|
+
/* @__PURE__ */ M("div", {
|
|
92
|
+
className: "space-y-4",
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ M("div", { children: [/* @__PURE__ */ M("label", {
|
|
95
|
+
htmlFor: `${X}-name`,
|
|
96
|
+
className: "text-sm font-medium",
|
|
97
|
+
children: [l("planmagnet.productFeatures.fields.name", "Name"), " *"]
|
|
98
|
+
}), /* @__PURE__ */ j(F, {
|
|
99
|
+
id: `${X}-name`,
|
|
100
|
+
className: "mt-1",
|
|
101
|
+
value: b.name,
|
|
102
|
+
placeholder: l("planmagnet.productFeatures.fields.namePlaceholder", "e.g. Single sign-on"),
|
|
103
|
+
onChange: (e) => C({
|
|
104
|
+
...b,
|
|
105
|
+
name: e.target.value
|
|
106
|
+
}),
|
|
107
|
+
"data-testid": "product-feature-name"
|
|
108
|
+
})] }),
|
|
109
|
+
/* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("label", {
|
|
110
|
+
htmlFor: `${X}-description`,
|
|
111
|
+
className: "text-sm font-medium",
|
|
112
|
+
children: l("planmagnet.productFeatures.fields.description", "Description")
|
|
113
|
+
}), /* @__PURE__ */ j("textarea", {
|
|
114
|
+
id: `${X}-description`,
|
|
115
|
+
className: U,
|
|
116
|
+
value: b.description,
|
|
117
|
+
onChange: (e) => C({
|
|
118
|
+
...b,
|
|
119
|
+
description: e.target.value
|
|
120
|
+
}),
|
|
121
|
+
"data-testid": "product-feature-description"
|
|
122
|
+
})] }),
|
|
123
|
+
/* @__PURE__ */ M("div", {
|
|
124
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
125
|
+
children: [/* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("label", {
|
|
126
|
+
htmlFor: `${X}-status`,
|
|
127
|
+
className: "text-sm font-medium",
|
|
128
|
+
children: l("planmagnet.productFeatures.fields.status", "Status")
|
|
129
|
+
}), /* @__PURE__ */ j("select", {
|
|
130
|
+
id: `${X}-status`,
|
|
131
|
+
className: W,
|
|
132
|
+
value: b.status,
|
|
133
|
+
onChange: (e) => C({
|
|
134
|
+
...b,
|
|
135
|
+
status: e.target.value
|
|
136
|
+
}),
|
|
137
|
+
"data-testid": "product-feature-status-select",
|
|
138
|
+
children: Z.map((e) => /* @__PURE__ */ j("option", {
|
|
139
|
+
value: e,
|
|
140
|
+
children: c(e)
|
|
141
|
+
}, e))
|
|
142
|
+
})] }), /* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("label", {
|
|
143
|
+
htmlFor: `${X}-owner`,
|
|
144
|
+
className: "text-sm font-medium",
|
|
145
|
+
children: l("planmagnet.productFeatures.fields.owner", "Owner")
|
|
146
|
+
}), /* @__PURE__ */ j("div", {
|
|
147
|
+
className: "mt-1",
|
|
148
|
+
children: /* @__PURE__ */ j(s, {
|
|
149
|
+
id: `${X}-owner`,
|
|
150
|
+
value: b.ownerId,
|
|
151
|
+
noneLabel: l("planmagnet.productFeatures.fields.noOwner", "No owner"),
|
|
152
|
+
onChange: (e) => C({
|
|
153
|
+
...b,
|
|
154
|
+
ownerId: e
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})] })]
|
|
158
|
+
})
|
|
159
|
+
]
|
|
160
|
+
}),
|
|
161
|
+
w && /* @__PURE__ */ j(o, { message: w }),
|
|
162
|
+
/* @__PURE__ */ j(z, { children: /* @__PURE__ */ j(P, {
|
|
163
|
+
onClick: () => void Q(),
|
|
164
|
+
disabled: !Y,
|
|
165
|
+
"data-testid": "product-feature-save",
|
|
166
|
+
children: J ? l("planmagnet.productFeatures.actions.saving", "Saving…") : r ? l("planmagnet.productFeatures.actions.save", "Save") : l("planmagnet.productFeatures.actions.create", "Create feature")
|
|
167
|
+
}) })
|
|
168
|
+
]
|
|
169
|
+
})]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function Y({ feature: e, epicCount: s, requestCount: c }) {
|
|
173
|
+
let d = n(), p = K(), m = t(), [g, _] = k(u, G), [v, y] = k(h, G), [b, T] = k(f, { context: r }), [E, D] = S(null), A = _.loading || y.loading || T.loading, N = async () => {
|
|
174
|
+
D(null);
|
|
175
|
+
try {
|
|
176
|
+
e.archivedAt ? await i(v({ variables: { id: e.id } })) : await i(g({ variables: { id: e.id } }));
|
|
177
|
+
} catch (e) {
|
|
178
|
+
D(x(e, d("planmagnet.productFeatures.errors.update", "The feature could not be updated."), p));
|
|
179
|
+
}
|
|
180
|
+
}, F = async () => {
|
|
181
|
+
D(null);
|
|
182
|
+
try {
|
|
183
|
+
await i(b({ variables: { id: e.id } })), m(l("/products/product-detail-page", e.productId));
|
|
184
|
+
} catch (e) {
|
|
185
|
+
D(x(e, d("planmagnet.productFeatures.errors.delete", "The feature could not be deleted."), p));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
return /* @__PURE__ */ M("div", {
|
|
189
|
+
className: "flex flex-col items-end gap-2",
|
|
190
|
+
children: [/* @__PURE__ */ M("div", {
|
|
191
|
+
className: "flex flex-wrap justify-end gap-2",
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ j(J, {
|
|
194
|
+
productId: e.productId,
|
|
195
|
+
feature: e
|
|
196
|
+
}),
|
|
197
|
+
/* @__PURE__ */ M(P, {
|
|
198
|
+
size: "sm",
|
|
199
|
+
variant: "outline",
|
|
200
|
+
disabled: A,
|
|
201
|
+
onClick: () => void N(),
|
|
202
|
+
"data-testid": "product-feature-toggle-archive",
|
|
203
|
+
children: [e.archivedAt ? /* @__PURE__ */ j(w, { className: "mr-1 h-4 w-4" }) : /* @__PURE__ */ j(C, { className: "mr-1 h-4 w-4" }), e.archivedAt ? d("planmagnet.productFeatures.actions.unarchive", "Unarchive") : d("planmagnet.productFeatures.actions.archive", "Archive")]
|
|
204
|
+
}),
|
|
205
|
+
/* @__PURE__ */ j(a, {
|
|
206
|
+
label: d("planmagnet.productFeatures.actions.delete", "Delete"),
|
|
207
|
+
variant: "outline",
|
|
208
|
+
icon: /* @__PURE__ */ j(O, { className: "mr-1 h-4 w-4" }),
|
|
209
|
+
testId: "product-feature-delete",
|
|
210
|
+
title: d("planmagnet.productFeatures.confirmDelete.title", "Delete {{name}}?", { name: e.name }),
|
|
211
|
+
description: d("planmagnet.productFeatures.confirmDelete.description", "Its {{requests}} feature request(s) and {{epics}} epic(s) are kept — only their links to this feature are removed, and stories stay under their epics. No request or work item is deleted or changed. This cannot be undone here.", {
|
|
212
|
+
requests: c,
|
|
213
|
+
epics: s
|
|
214
|
+
}),
|
|
215
|
+
confirmLabel: d("planmagnet.productFeatures.confirmDelete.confirm", "Delete feature"),
|
|
216
|
+
destructive: !0,
|
|
217
|
+
disabled: A,
|
|
218
|
+
onConfirm: () => void F()
|
|
219
|
+
})
|
|
220
|
+
]
|
|
221
|
+
}), E && /* @__PURE__ */ j(o, { message: E })]
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function X({ productId: r, productArchived: i }) {
|
|
225
|
+
let a = n(), { workspaceId: s } = e(), c = t(), [u, d] = S(!1), { data: f, previousData: m, loading: h, error: g } = A(p, {
|
|
226
|
+
variables: {
|
|
227
|
+
workspaceId: s ?? "",
|
|
228
|
+
productId: r,
|
|
229
|
+
filter: u ? void 0 : { isActive: !0 }
|
|
230
|
+
},
|
|
231
|
+
skip: !s || !r
|
|
232
|
+
}), _ = (f ?? m)?.planmagnetProductFeatures, v = _?.items ?? [];
|
|
233
|
+
return /* @__PURE__ */ M("div", {
|
|
234
|
+
className: "space-y-3",
|
|
235
|
+
"data-testid": "product-features",
|
|
236
|
+
children: [
|
|
237
|
+
/* @__PURE__ */ M("div", {
|
|
238
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
239
|
+
children: [/* @__PURE__ */ j("p", {
|
|
240
|
+
className: "text-sm text-muted-foreground",
|
|
241
|
+
children: a("planmagnet.productFeatures.panel.subtitle", "Capabilities the team decided to build. Each links the epics that deliver it and the feature requests it answers.")
|
|
242
|
+
}), /* @__PURE__ */ M("div", {
|
|
243
|
+
className: "flex items-center gap-2",
|
|
244
|
+
children: [/* @__PURE__ */ M(P, {
|
|
245
|
+
size: "sm",
|
|
246
|
+
variant: u ? "default" : "outline",
|
|
247
|
+
"aria-pressed": u,
|
|
248
|
+
onClick: () => d((e) => !e),
|
|
249
|
+
"data-testid": "product-features-toggle-archived",
|
|
250
|
+
children: [/* @__PURE__ */ j(C, { className: "mr-1 h-4 w-4" }), a("planmagnet.productFeatures.panel.showArchived", "Show archived")]
|
|
251
|
+
}), /* @__PURE__ */ j(J, {
|
|
252
|
+
productId: r,
|
|
253
|
+
disabled: i
|
|
254
|
+
})]
|
|
255
|
+
})]
|
|
256
|
+
}),
|
|
257
|
+
i && /* @__PURE__ */ j("p", {
|
|
258
|
+
className: "text-xs text-muted-foreground",
|
|
259
|
+
children: a("planmagnet.productFeatures.panel.productArchived", "This product is archived — unarchive it to add features.")
|
|
260
|
+
}),
|
|
261
|
+
g && /* @__PURE__ */ j(o, { message: a("planmagnet.productFeatures.errors.load", "Features could not be loaded: {{message}}", { message: g.message }) }),
|
|
262
|
+
h && !_ && /* @__PURE__ */ j("p", {
|
|
263
|
+
className: "text-sm text-muted-foreground",
|
|
264
|
+
children: a("planmagnet.productFeatures.loading", "Loading features…")
|
|
265
|
+
}),
|
|
266
|
+
_ && v.length === 0 && /* @__PURE__ */ j("p", {
|
|
267
|
+
className: "py-4 text-center text-sm text-muted-foreground",
|
|
268
|
+
children: u ? a("planmagnet.productFeatures.panel.emptyAll", "This product has no features yet.") : a("planmagnet.productFeatures.panel.empty", "No active features yet. Add the first capability this product will deliver.")
|
|
269
|
+
}),
|
|
270
|
+
v.length > 0 && /* @__PURE__ */ j("ul", {
|
|
271
|
+
className: "space-y-1",
|
|
272
|
+
children: v.map((e) => /* @__PURE__ */ M("li", {
|
|
273
|
+
className: "flex flex-wrap items-center justify-between gap-2 rounded-md border p-2 text-sm",
|
|
274
|
+
children: [/* @__PURE__ */ M("button", {
|
|
275
|
+
type: "button",
|
|
276
|
+
className: "flex min-w-0 items-center gap-2 text-left font-medium hover:underline",
|
|
277
|
+
onClick: () => c(l("/products/product-feature-detail-page", e.id)),
|
|
278
|
+
children: [/* @__PURE__ */ j(D, { className: "h-4 w-4 shrink-0 text-primary" }), /* @__PURE__ */ j("span", {
|
|
279
|
+
className: "truncate",
|
|
280
|
+
children: e.name
|
|
281
|
+
})]
|
|
282
|
+
}), /* @__PURE__ */ M("div", {
|
|
283
|
+
className: "flex shrink-0 items-center gap-2",
|
|
284
|
+
children: [
|
|
285
|
+
/* @__PURE__ */ j("span", {
|
|
286
|
+
className: "text-xs text-muted-foreground",
|
|
287
|
+
children: a("planmagnet.productFeatures.panel.counts", "{{epics}} epic(s) · {{requests}} request(s)", {
|
|
288
|
+
epics: e.epics.length,
|
|
289
|
+
requests: e.featureRequests.length
|
|
290
|
+
})
|
|
291
|
+
}),
|
|
292
|
+
e.archivedAt && /* @__PURE__ */ j(N, {
|
|
293
|
+
variant: "outline",
|
|
294
|
+
className: "text-xs",
|
|
295
|
+
children: a("planmagnet.productFeatures.archived", "Archived")
|
|
296
|
+
}),
|
|
297
|
+
/* @__PURE__ */ j(q, { status: e.status })
|
|
298
|
+
]
|
|
299
|
+
})]
|
|
300
|
+
}, e.id))
|
|
301
|
+
}),
|
|
302
|
+
_?.pageInfo.hasNextPage && /* @__PURE__ */ j("p", {
|
|
303
|
+
className: "text-xs text-muted-foreground",
|
|
304
|
+
children: a("planmagnet.productFeatures.panel.truncated", "Showing the first {{shown}} of {{total}} features.", {
|
|
305
|
+
shown: v.length,
|
|
306
|
+
total: _.totalCount
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
]
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
export { Y as ProductFeatureHeaderActions, q as ProductFeatureStatusBadge, X as ProductFeaturesPanel };
|
|
314
|
+
|
|
315
|
+
//# sourceMappingURL=ProductFeatureControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductFeatureControls.js","names":[],"sources":["../../../src/planmagnet/product/ProductFeatureControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Badge,\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 { Archive, ArchiveRestore, Pencil, Plus, Puzzle, Trash2 } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { AssigneeSelect, enumLabel } from '../controls/WorkItemControls';\nimport { LEAVES_PAGE_CONTEXT, throwOnError } from '../utils/mutationResult';\nimport { detailPath } from '../utils/rowNavigation';\nimport {\n PRODUCT_FEATURE_STATUSES,\n changedFields,\n normalizeText,\n productFeatureStatusVariant,\n} from './hierarchy';\nimport {\n ARCHIVE_PRODUCT_FEATURE,\n CREATE_PRODUCT_FEATURE,\n DELETE_PRODUCT_FEATURE,\n PRODUCT_FEATURES_LIST,\n PRODUCT_FEATURE_QUERIES,\n UNARCHIVE_PRODUCT_FEATURE,\n UPDATE_PRODUCT_FEATURE,\n} from './hierarchyOperations';\nimport { refusalMessage } from './refusal';\n\n/**\n * Product features (PRM-01): what a product team decided to build — not a feature request, which is customer\n * feedback asking for something and may point at the feature that addresses it. A feature belongs to one product\n * for good, links EPIC work items (stories hang off their epics) and the requests it answers. Archiving keeps every\n * link; deleting removes only the links — never a request or a work item.\n */\n\nexport interface ProductFeatureValues {\n id: string;\n productId: string;\n name: string;\n description: string | null;\n status: string;\n ownerId: string | null;\n archivedAt: string | null;\n}\n\nconst textareaClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]';\nconst selectClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst writeOptions = { refetchQueries: PRODUCT_FEATURE_QUERIES, awaitRefetchQueries: true };\n\nfunction useForbiddenText() {\n const tr = useTr();\n return tr(\n 'planmagnet.productFeatures.errors.forbidden',\n 'Your role cannot change this product’s features.'\n );\n}\n\nexport function ProductFeatureStatusBadge({ status }: { status: string }) {\n return (\n <Badge\n variant={productFeatureStatusVariant(status)}\n className=\"text-xs\"\n data-testid=\"product-feature-status\"\n >\n {enumLabel(status)}\n </Badge>\n );\n}\n\n// ─── Create / edit ──────────────────────────────────────────────────────────\n\n/**\n * Create a feature under `productId` (no `feature`) or edit one: name, description, status and owner. An edit sends\n * only what changed; an emptied description or a cleared owner is sent as null, which clears it. Status always has\n * a value — the service refuses to clear it.\n */\nexport function ProductFeatureFormDialog({\n productId,\n feature,\n disabled = false,\n}: {\n productId: string;\n feature?: ProductFeatureValues;\n disabled?: boolean;\n}) {\n const tr = useTr();\n const forbidden = useForbiddenText();\n const { currentUser } = usePlanMagnet();\n const initial = {\n name: feature?.name ?? '',\n description: feature?.description ?? '',\n status: feature?.status ?? 'BACKLOG',\n // A new feature is owned by whoever creates it unless they pick someone else (or nobody).\n ownerId: feature ? (feature.ownerId ?? '') : (currentUser?.id ?? ''),\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [create, createState] = useMutation(CREATE_PRODUCT_FEATURE, writeOptions);\n const [update, updateState] = useMutation(UPDATE_PRODUCT_FEATURE, writeOptions);\n const saving = createState.loading || updateState.loading;\n const canSave = form.name.trim() !== '' && !saving;\n const idPrefix = feature\n ? `product-feature-edit-${feature.id}`\n : `product-feature-new-${productId}`;\n // A stored status this list does not know (newer data) stays selectable rather than silently showing another.\n const statuses: readonly string[] = (PRODUCT_FEATURE_STATUSES as readonly string[]).includes(\n form.status\n )\n ? PRODUCT_FEATURE_STATUSES\n : [form.status, ...PRODUCT_FEATURE_STATUSES];\n\n const save = async () => {\n setError(null);\n const next = {\n name: form.name.trim(),\n description: normalizeText(form.description),\n status: form.status,\n ownerId: form.ownerId || null,\n };\n try {\n if (feature) {\n const before = {\n name: feature.name.trim(),\n description: normalizeText(feature.description ?? ''),\n status: feature.status,\n ownerId: feature.ownerId,\n };\n const input = changedFields(before, next);\n if (Object.keys(input).length > 0) {\n await throwOnError(update({ variables: { id: feature.id, input } }));\n }\n } else {\n await throwOnError(\n create({\n variables: {\n input: {\n productId,\n name: next.name,\n status: next.status,\n ...(next.description ? { description: next.description } : {}),\n ...(next.ownerId ? { ownerId: next.ownerId } : {}),\n },\n },\n })\n );\n }\n setOpen(false);\n } catch (e) {\n setError(\n refusalMessage(\n e,\n feature\n ? tr('planmagnet.productFeatures.errors.save', 'The feature could not be saved.')\n : tr('planmagnet.productFeatures.errors.create', 'The feature could not be created.'),\n forbidden\n )\n );\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 {feature ? (\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-feature-edit\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productFeatures.actions.edit', 'Edit')}\n </Button>\n ) : (\n <Button size=\"sm\" disabled={disabled} data-testid=\"product-feature-new\">\n <Plus className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productFeatures.actions.new', 'New feature')}\n </Button>\n )}\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {feature\n ? tr('planmagnet.productFeatures.dialog.editTitle', 'Edit feature')\n : tr('planmagnet.productFeatures.dialog.newTitle', 'New feature')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'planmagnet.productFeatures.dialog.description',\n 'A feature is a capability the team decided to build. Customer asks stay feature requests — link them to the feature that answers them.'\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-4\">\n <div>\n <label htmlFor={`${idPrefix}-name`} className=\"text-sm font-medium\">\n {tr('planmagnet.productFeatures.fields.name', 'Name')} *\n </label>\n <Input\n id={`${idPrefix}-name`}\n className=\"mt-1\"\n value={form.name}\n placeholder={tr(\n 'planmagnet.productFeatures.fields.namePlaceholder',\n 'e.g. Single sign-on'\n )}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n data-testid=\"product-feature-name\"\n />\n </div>\n <div>\n <label htmlFor={`${idPrefix}-description`} className=\"text-sm font-medium\">\n {tr('planmagnet.productFeatures.fields.description', 'Description')}\n </label>\n <textarea\n id={`${idPrefix}-description`}\n className={textareaClass}\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n data-testid=\"product-feature-description\"\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor={`${idPrefix}-status`} className=\"text-sm font-medium\">\n {tr('planmagnet.productFeatures.fields.status', 'Status')}\n </label>\n <select\n id={`${idPrefix}-status`}\n className={selectClass}\n value={form.status}\n onChange={(e) => setForm({ ...form, status: e.target.value })}\n data-testid=\"product-feature-status-select\"\n >\n {statuses.map((status) => (\n <option key={status} value={status}>\n {enumLabel(status)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-owner`} className=\"text-sm font-medium\">\n {tr('planmagnet.productFeatures.fields.owner', 'Owner')}\n </label>\n <div className=\"mt-1\">\n <AssigneeSelect\n id={`${idPrefix}-owner`}\n value={form.ownerId}\n noneLabel={tr('planmagnet.productFeatures.fields.noOwner', 'No owner')}\n onChange={(ownerId) => setForm({ ...form, ownerId })}\n />\n </div>\n </div>\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void save()}\n disabled={!canSave}\n data-testid=\"product-feature-save\"\n >\n {saving\n ? tr('planmagnet.productFeatures.actions.saving', 'Saving…')\n : feature\n ? tr('planmagnet.productFeatures.actions.save', 'Save')\n : tr('planmagnet.productFeatures.actions.create', 'Create feature')}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Archive / unarchive / delete (feature detail header) ───────────────────\n\nexport function ProductFeatureHeaderActions({\n feature,\n epicCount,\n requestCount,\n}: {\n feature: ProductFeatureValues;\n epicCount: number;\n requestCount: number;\n}) {\n const tr = useTr();\n const forbidden = useForbiddenText();\n const navigate = usePlanMagnetNavigate();\n const [archive, archiveState] = useMutation(ARCHIVE_PRODUCT_FEATURE, writeOptions);\n const [unarchive, unarchiveState] = useMutation(UNARCHIVE_PRODUCT_FEATURE, writeOptions);\n // Leaves the page: no refetch of this (deleted) feature on the way out — the product page fetches its list on\n // mount (see utils/mutationResult.ts).\n const [remove, removeState] = useMutation(DELETE_PRODUCT_FEATURE, {\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = archiveState.loading || unarchiveState.loading || removeState.loading;\n\n const onToggleArchive = async () => {\n setError(null);\n try {\n if (feature.archivedAt) await throwOnError(unarchive({ variables: { id: feature.id } }));\n else await throwOnError(archive({ variables: { id: feature.id } }));\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr('planmagnet.productFeatures.errors.update', 'The feature could not be updated.'),\n forbidden\n )\n );\n }\n };\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: feature.id } }));\n navigate(detailPath('/products/product-detail-page', feature.productId));\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr('planmagnet.productFeatures.errors.delete', 'The feature could not be deleted.'),\n forbidden\n )\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap justify-end gap-2\">\n <ProductFeatureFormDialog productId={feature.productId} feature={feature} />\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={busy}\n onClick={() => void onToggleArchive()}\n data-testid=\"product-feature-toggle-archive\"\n >\n {feature.archivedAt ? (\n <ArchiveRestore className=\"mr-1 h-4 w-4\" />\n ) : (\n <Archive className=\"mr-1 h-4 w-4\" />\n )}\n {feature.archivedAt\n ? tr('planmagnet.productFeatures.actions.unarchive', 'Unarchive')\n : tr('planmagnet.productFeatures.actions.archive', 'Archive')}\n </Button>\n <ConfirmButton\n label={tr('planmagnet.productFeatures.actions.delete', 'Delete')}\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n testId=\"product-feature-delete\"\n title={tr('planmagnet.productFeatures.confirmDelete.title', 'Delete {{name}}?', {\n name: feature.name,\n })}\n description={tr(\n 'planmagnet.productFeatures.confirmDelete.description',\n 'Its {{requests}} feature request(s) and {{epics}} epic(s) are kept — only their links to this feature are removed, and stories stay under their epics. No request or work item is deleted or changed. This cannot be undone here.',\n { requests: requestCount, epics: epicCount }\n )}\n confirmLabel={tr('planmagnet.productFeatures.confirmDelete.confirm', 'Delete feature')}\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n// ─── A product's features (product detail) ──────────────────────────────────\n\ninterface FeatureListItem extends ProductFeatureValues {\n epics: { id: string }[];\n featureRequests: { id: string }[];\n}\n\ninterface FeatureListData {\n planmagnetProductFeatures: {\n items: FeatureListItem[];\n pageInfo: { hasNextPage: boolean };\n totalCount: number;\n };\n}\n\n/** The product page's Features section: list with status, \"Show archived\", and \"New feature\". */\nexport function ProductFeaturesPanel({\n productId,\n productArchived,\n}: {\n productId: string;\n productArchived: boolean;\n}) {\n const tr = useTr();\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [showArchived, setShowArchived] = useState(false);\n const { data, previousData, loading, error } = useQuery<FeatureListData>(PRODUCT_FEATURES_LIST, {\n variables: {\n workspaceId: workspaceId ?? '',\n productId,\n // isActive omitted lists active and archived alike.\n filter: showArchived ? undefined : { isActive: true },\n },\n skip: !workspaceId || !productId,\n });\n // Keep the rows on screen while the toggle refetches, instead of blanking the section.\n const list = (data ?? previousData)?.planmagnetProductFeatures;\n const features = list?.items ?? [];\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-features\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.panel.subtitle',\n 'Capabilities the team decided to build. Each links the epics that deliver it and the feature requests it answers.'\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant={showArchived ? 'default' : 'outline'}\n aria-pressed={showArchived}\n onClick={() => setShowArchived((v) => !v)}\n data-testid=\"product-features-toggle-archived\"\n >\n <Archive className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productFeatures.panel.showArchived', 'Show archived')}\n </Button>\n <ProductFeatureFormDialog productId={productId} disabled={productArchived} />\n </div>\n </div>\n {productArchived && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.panel.productArchived',\n 'This product is archived — unarchive it to add features.'\n )}\n </p>\n )}\n {error && (\n <ErrorLine\n message={tr(\n 'planmagnet.productFeatures.errors.load',\n 'Features could not be loaded: {{message}}',\n { message: error.message }\n )}\n />\n )}\n {loading && !list && (\n <p className=\"text-sm text-muted-foreground\">\n {tr('planmagnet.productFeatures.loading', 'Loading features…')}\n </p>\n )}\n {list && features.length === 0 && (\n <p className=\"py-4 text-center text-sm text-muted-foreground\">\n {showArchived\n ? tr('planmagnet.productFeatures.panel.emptyAll', 'This product has no features yet.')\n : tr(\n 'planmagnet.productFeatures.panel.empty',\n 'No active features yet. Add the first capability this product will deliver.'\n )}\n </p>\n )}\n {features.length > 0 && (\n <ul className=\"space-y-1\">\n {features.map((feature) => (\n <li\n key={feature.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-md border p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"flex min-w-0 items-center gap-2 text-left font-medium hover:underline\"\n onClick={() =>\n navigate(detailPath('/products/product-feature-detail-page', feature.id))\n }\n >\n <Puzzle className=\"h-4 w-4 shrink-0 text-primary\" />\n <span className=\"truncate\">{feature.name}</span>\n </button>\n <div className=\"flex shrink-0 items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.panel.counts',\n '{{epics}} epic(s) · {{requests}} request(s)',\n { epics: feature.epics.length, requests: feature.featureRequests.length }\n )}\n </span>\n {feature.archivedAt && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {tr('planmagnet.productFeatures.archived', 'Archived')}\n </Badge>\n )}\n <ProductFeatureStatusBadge status={feature.status} />\n </div>\n </li>\n ))}\n </ul>\n )}\n {list?.pageInfo.hasNextPage && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.panel.truncated',\n 'Showing the first {{shown}} of {{total}} features.',\n { shown: features.length, total: list.totalCount }\n )}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuDA,IAAM,IACJ,2FACI,IACJ,kGAEI,IAAe;CAAE,gBAAgB;CAAyB,qBAAqB;CAAM;AAE3F,SAAS,IAAmB;AAE1B,QADW,GAAO,CAEhB,+CACA,mDACD;;AAGH,SAAgB,EAA0B,EAAE,aAA8B;AACxE,QACE,kBAAC,GAAD;EACE,SAAS,EAA4B,EAAO;EAC5C,WAAU;EACV,eAAY;YAEX,EAAU,EAAO;EACZ,CAAA;;AAWZ,SAAgB,EAAyB,EACvC,cACA,YACA,cAAW,MAKV;CACD,IAAM,IAAK,GAAO,EACZ,IAAY,GAAkB,EAC9B,EAAE,mBAAgB,GAAe,EACjC,IAAU;EACd,MAAM,GAAS,QAAQ;EACvB,aAAa,GAAS,eAAe;EACrC,QAAQ,GAAS,UAAU;EAE3B,SAAS,IAAW,EAAQ,WAAW,KAAO,GAAa,MAAM;EAClE,EACK,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAM,KAAW,EAAS,EAAQ,EACnC,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAQ,KAAe,EAAY,GAAwB,EAAa,EACzE,CAAC,GAAQ,KAAe,EAAY,GAAwB,EAAa,EACzE,IAAS,EAAY,WAAW,EAAY,SAC5C,IAAU,EAAK,KAAK,MAAM,KAAK,MAAM,CAAC,GACtC,IAAW,IACb,wBAAwB,EAAQ,OAChC,uBAAuB,KAErB,IAA+B,EAA+C,SAClF,EAAK,OACN,GACG,IACA,CAAC,EAAK,QAAQ,GAAG,EAAyB,EAExC,IAAO,YAAY;AACvB,IAAS,KAAK;EACd,IAAM,IAAO;GACX,MAAM,EAAK,KAAK,MAAM;GACtB,aAAa,EAAc,EAAK,YAAY;GAC5C,QAAQ,EAAK;GACb,SAAS,EAAK,WAAW;GAC1B;AACD,MAAI;AACF,OAAI,GAAS;IAOX,IAAM,IAAQ,EANC;KACb,MAAM,EAAQ,KAAK,MAAM;KACzB,aAAa,EAAc,EAAQ,eAAe,GAAG;KACrD,QAAQ,EAAQ;KAChB,SAAS,EAAQ;KAClB,EACmC,EAAK;AACzC,IAAI,OAAO,KAAK,EAAM,CAAC,SAAS,KAC9B,MAAM,EAAa,EAAO,EAAE,WAAW;KAAE,IAAI,EAAQ;KAAI;KAAO,EAAE,CAAC,CAAC;SAGtE,OAAM,EACJ,EAAO,EACL,WAAW,EACT,OAAO;IACL;IACA,MAAM,EAAK;IACX,QAAQ,EAAK;IACb,GAAI,EAAK,cAAc,EAAE,aAAa,EAAK,aAAa,GAAG,EAAE;IAC7D,GAAI,EAAK,UAAU,EAAE,SAAS,EAAK,SAAS,GAAG,EAAE;IAClD,EACF,EACF,CAAC,CACH;AAEH,KAAQ,GAAM;WACP,GAAG;AACV,KACE,EACE,GACA,IACI,EAAG,0CAA0C,kCAAkC,GAC/E,EAAG,4CAA4C,oCAAoC,EACvF,EACD,CACF;;;AAIL,QACE,kBAAC,GAAD;EACQ;EACN,eAAe,MAAS;AAEtB,GADA,EAAQ,EAAK,EACT,IAAM,EAAQ,EAAQ,GACrB,EAAS,KAAK;;YALvB,CAQE,kBAAC,GAAD;GAAyB,SAAA;aACtB,IACC,kBAAC,GAAD;IAAQ,MAAK;IAAK,SAAQ;IAAU,eAAY;cAAhD,CACE,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAClC,EAAG,2CAA2C,OAAO,CAC/C;QAET,kBAAC,GAAD;IAAQ,MAAK;IAAe;IAAU,eAAY;cAAlD,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAChC,EAAG,0CAA0C,cAAc,CACrD;;GAEa,CAAA,EAC1B,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACG,IACG,EAAG,+CAA+C,eAAe,GACjE,EAAG,8CAA8C,cAAc,EAC7C,CAAA,EACxB,kBAAC,GAAD,EAAA,UACG,EACC,iDACA,yIACD,EAC2B,CAAA,CACP,EAAA,CAAA;IACzB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAQ,WAAU;iBAA9C,CACG,EAAG,0CAA0C,OAAO,EAAC,KAChD;UACR,kBAAC,GAAD;OACE,IAAI,GAAG,EAAS;OAChB,WAAU;OACV,OAAO,EAAK;OACZ,aAAa,EACX,qDACA,sBACD;OACD,WAAW,MAAM,EAAQ;QAAE,GAAG;QAAM,MAAM,EAAE,OAAO;QAAO,CAAC;OAC3D,eAAY;OACZ,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAe,WAAU;iBAClD,EAAG,iDAAiD,cAAc;OAC7D,CAAA,EACR,kBAAC,YAAD;OACE,IAAI,GAAG,EAAS;OAChB,WAAW;OACX,OAAO,EAAK;OACZ,WAAW,MAAM,EAAQ;QAAE,GAAG;QAAM,aAAa,EAAE,OAAO;QAAO,CAAC;OAClE,eAAY;OACZ,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAS,GAAG,EAAS;QAAU,WAAU;kBAC7C,EAAG,4CAA4C,SAAS;QACnD,CAAA,EACR,kBAAC,UAAD;QACE,IAAI,GAAG,EAAS;QAChB,WAAW;QACX,OAAO,EAAK;QACZ,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,QAAQ,EAAE,OAAO;SAAO,CAAC;QAC7D,eAAY;kBAEX,EAAS,KAAK,MACb,kBAAC,UAAD;SAAqB,OAAO;mBACzB,EAAU,EAAO;SACX,EAFI,EAEJ,CACT;QACK,CAAA,CACL,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAS,GAAG,EAAS;QAAS,WAAU;kBAC5C,EAAG,2CAA2C,QAAQ;QACjD,CAAA,EACR,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SACE,IAAI,GAAG,EAAS;SAChB,OAAO,EAAK;SACZ,WAAW,EAAG,6CAA6C,WAAW;SACtE,WAAW,MAAY,EAAQ;UAAE,GAAG;UAAM;UAAS,CAAC;SACpD,CAAA;QACE,CAAA,CACF,EAAA,CAAA,CACF;;MACF;;IACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA;IACvC,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;KACE,eAAe,KAAK,GAAM;KAC1B,UAAU,CAAC;KACX,eAAY;eAEX,IACG,EAAG,6CAA6C,UAAU,GAC1D,IACE,EAAG,2CAA2C,OAAO,GACrD,EAAG,6CAA6C,iBAAiB;KAChE,CAAA,EACc,CAAA;IACD;KACT;;;AAMvB,SAAgB,EAA4B,EAC1C,YACA,cACA,mBAKC;CACD,IAAM,IAAK,GAAO,EACZ,IAAY,GAAkB,EAC9B,IAAW,GAAuB,EAClC,CAAC,GAAS,KAAgB,EAAY,GAAyB,EAAa,EAC5E,CAAC,GAAW,KAAkB,EAAY,GAA2B,EAAa,EAGlF,CAAC,GAAQ,KAAe,EAAY,GAAwB,EAChE,SAAS,GACV,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAO,EAAa,WAAW,EAAe,WAAW,EAAY,SAErE,IAAkB,YAAY;AAClC,IAAS,KAAK;AACd,MAAI;AACF,GAAI,EAAQ,aAAY,MAAM,EAAa,EAAU,EAAE,WAAW,EAAE,IAAI,EAAQ,IAAI,EAAE,CAAC,CAAC,GACnF,MAAM,EAAa,EAAQ,EAAE,WAAW,EAAE,IAAI,EAAQ,IAAI,EAAE,CAAC,CAAC;WAC5D,GAAG;AACV,KACE,EACE,GACA,EAAG,4CAA4C,oCAAoC,EACnF,EACD,CACF;;IAIC,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AAEF,GADA,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,EAAQ,IAAI,EAAE,CAAC,CAAC,EAC7D,EAAS,EAAW,iCAAiC,EAAQ,UAAU,CAAC;WACjE,GAAG;AACV,KACE,EACE,GACA,EAAG,4CAA4C,oCAAoC,EACnF,EACD,CACF;;;AAIL,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KAA0B,WAAW,EAAQ;KAAoB;KAAW,CAAA;IAC5E,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,UAAU;KACV,eAAe,KAAK,GAAiB;KACrC,eAAY;eALd,CAOG,EAAQ,aACP,kBAAC,GAAD,EAAgB,WAAU,gBAAiB,CAAA,GAE3C,kBAAC,GAAD,EAAS,WAAU,gBAAiB,CAAA,EAErC,EAAQ,aACL,EAAG,gDAAgD,YAAY,GAC/D,EAAG,8CAA8C,UAAU,CACxD;;IACT,kBAAC,GAAD;KACE,OAAO,EAAG,6CAA6C,SAAS;KAChE,SAAQ;KACR,MAAM,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA;KACzC,QAAO;KACP,OAAO,EAAG,kDAAkD,oBAAoB,EAC9E,MAAM,EAAQ,MACf,CAAC;KACF,aAAa,EACX,wDACA,qOACA;MAAE,UAAU;MAAc,OAAO;MAAW,CAC7C;KACD,cAAc,EAAG,oDAAoD,iBAAiB;KACtF,aAAA;KACA,UAAU;KACV,iBAAiB,KAAK,GAAU;KAChC,CAAA;IACE;MACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC;;;AAoBV,SAAgB,EAAqB,EACnC,cACA,sBAIC;CACD,IAAM,IAAK,GAAO,EACZ,EAAE,mBAAgB,GAAe,EACjC,IAAW,GAAuB,EAClC,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,EAAE,SAAM,iBAAc,YAAS,aAAU,EAA0B,GAAuB;EAC9F,WAAW;GACT,aAAa,KAAe;GAC5B;GAEA,QAAQ,IAAe,KAAA,IAAY,EAAE,UAAU,IAAM;GACtD;EACD,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EAEI,KAAQ,KAAQ,IAAe,2BAC/B,IAAW,GAAM,SAAS,EAAE;AAElC,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,eAAY;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,6CACA,oHACD;KACC,CAAA,EACJ,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,MAAK;MACL,SAAS,IAAe,YAAY;MACpC,gBAAc;MACd,eAAe,GAAiB,MAAM,CAAC,EAAE;MACzC,eAAY;gBALd,CAOE,kBAAC,GAAD,EAAS,WAAU,gBAAiB,CAAA,EACnC,EAAG,iDAAiD,gBAAgB,CAC9D;SACT,kBAAC,GAAD;MAAqC;MAAW,UAAU;MAAmB,CAAA,CACzE;OACF;;GACL,KACC,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,oDACA,2DACD;IACC,CAAA;GAEL,KACC,kBAAC,GAAD,EACE,SAAS,EACP,0CACA,6CACA,EAAE,SAAS,EAAM,SAAS,CAC3B,EACD,CAAA;GAEH,KAAW,CAAC,KACX,kBAAC,KAAD;IAAG,WAAU;cACV,EAAG,sCAAsC,oBAAoB;IAC5D,CAAA;GAEL,KAAQ,EAAS,WAAW,KAC3B,kBAAC,KAAD;IAAG,WAAU;cACV,IACG,EAAG,6CAA6C,oCAAoC,GACpF,EACE,0CACA,8EACD;IACH,CAAA;GAEL,EAAS,SAAS,KACjB,kBAAC,MAAD;IAAI,WAAU;cACX,EAAS,KAAK,MACb,kBAAC,MAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,UAAD;MACE,MAAK;MACL,WAAU;MACV,eACE,EAAS,EAAW,yCAAyC,EAAQ,GAAG,CAAC;gBAJ7E,CAOE,kBAAC,GAAD,EAAQ,WAAU,iCAAkC,CAAA,EACpD,kBAAC,QAAD;OAAM,WAAU;iBAAY,EAAQ;OAAY,CAAA,CACzC;SACT,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EACC,2CACA,+CACA;SAAE,OAAO,EAAQ,MAAM;SAAQ,UAAU,EAAQ,gBAAgB;SAAQ,CAC1E;QACI,CAAA;OACN,EAAQ,cACP,kBAAC,GAAD;QAAO,SAAQ;QAAU,WAAU;kBAChC,EAAG,uCAAuC,WAAW;QAChD,CAAA;OAEV,kBAAC,GAAD,EAA2B,QAAQ,EAAQ,QAAU,CAAA;OACjD;QACH;OA5BE,EAAQ,GA4BV,CACL;IACC,CAAA;GAEN,GAAM,SAAS,eACd,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,8CACA,sDACA;KAAE,OAAO,EAAS;KAAQ,OAAO,EAAK;KAAY,CACnD;IACC,CAAA;GAEF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductFeatureLinkCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/product/ProductFeatureLinkCard.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,sBAAsB,CAAC,EAAE,gBAAgB,EAAE,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,2CAsQxF"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { useTr as n } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { throwOnError as r } from "../utils/mutationResult.js";
|
|
4
|
+
import { ErrorLine as i } from "../controls/ConfirmButton.js";
|
|
5
|
+
import { detailPath as a } from "../utils/rowNavigation.js";
|
|
6
|
+
import { ProductSelect as o } from "../controls/Pickers.js";
|
|
7
|
+
import { FEATURE_REQUEST_FEATURE_OPTIONS as s, FEATURE_REQUEST_PRODUCT_FEATURE as c, LINK_FEATURE_REQUEST_TO_PRODUCT_FEATURE as l, PRODUCT_FEATURE_QUERIES as u, UNLINK_FEATURE_REQUEST_FROM_PRODUCT_FEATURE as d } from "./hierarchyOperations.js";
|
|
8
|
+
import { refusalMessage as f } from "./refusal.js";
|
|
9
|
+
import { ProductFeatureStatusBadge as p } from "./ProductFeatureControls.js";
|
|
10
|
+
import { useState as m } from "react";
|
|
11
|
+
import { Link2 as h, Puzzle as g, X as _ } from "lucide-react";
|
|
12
|
+
import { useMutation as v, useQuery as y } from "@apollo/client/react";
|
|
13
|
+
import { jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
14
|
+
import { Badge as S, Button as C, Card as w, CardContent as T, CardHeader as E, CardTitle as D } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
//#region src/planmagnet/product/ProductFeatureLinkCard.tsx
|
|
16
|
+
var O = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60";
|
|
17
|
+
function k({ featureRequestId: k }) {
|
|
18
|
+
let A = n(), { workspaceId: j } = e(), M = t(), [N, P] = m(!1), [F, I] = m(""), [L, R] = m(""), [z, B] = m(null), V = y(c, {
|
|
19
|
+
variables: { id: k },
|
|
20
|
+
skip: !k
|
|
21
|
+
}), H = V.data?.planmagnetFeatureRequest ?? null, U = H?.productFeature ?? null, W = H?.productId ?? F, G = y(s, {
|
|
22
|
+
variables: {
|
|
23
|
+
workspaceId: j ?? "",
|
|
24
|
+
productId: W
|
|
25
|
+
},
|
|
26
|
+
skip: !N || !j || !W
|
|
27
|
+
}), K = {
|
|
28
|
+
refetchQueries: u,
|
|
29
|
+
awaitRefetchQueries: !0
|
|
30
|
+
}, [q, J] = v(l, K), [Y, X] = v(d, K), Z = A("planmagnet.productFeatures.errors.forbiddenLinks", "Your role cannot change what this feature is linked to."), Q = (G.data?.planmagnetProductFeatures.items ?? []).filter((e) => e.id !== U?.id), $ = () => {
|
|
31
|
+
P(!1), I(""), R("");
|
|
32
|
+
}, ee = async () => {
|
|
33
|
+
if (L) {
|
|
34
|
+
B(null);
|
|
35
|
+
try {
|
|
36
|
+
await r(q({ variables: {
|
|
37
|
+
featureRequestId: k,
|
|
38
|
+
productFeatureId: L
|
|
39
|
+
} })), $();
|
|
40
|
+
} catch (e) {
|
|
41
|
+
B(f(e, A("planmagnet.productFeatures.requestCard.errors.link", "The request could not be linked to the feature."), Z));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, te = async () => {
|
|
45
|
+
if (U) {
|
|
46
|
+
B(null);
|
|
47
|
+
try {
|
|
48
|
+
await r(Y({ variables: {
|
|
49
|
+
featureRequestId: k,
|
|
50
|
+
productFeatureId: U.id
|
|
51
|
+
} }));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
B(f(e, A("planmagnet.productFeatures.requestCard.errors.unlink", "The request could not be unlinked from the feature."), Z));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return /* @__PURE__ */ x(w, {
|
|
58
|
+
"data-testid": "feature-request-product-feature",
|
|
59
|
+
children: [/* @__PURE__ */ b(E, { children: /* @__PURE__ */ b(D, { children: A("planmagnet.productFeatures.requestCard.title", "Product feature") }) }), /* @__PURE__ */ x(T, {
|
|
60
|
+
className: "space-y-3 text-sm",
|
|
61
|
+
children: [
|
|
62
|
+
V.error && /* @__PURE__ */ b(i, { message: A("planmagnet.productFeatures.requestCard.errors.load", "The product feature could not be loaded: {{message}}", { message: V.error.message }) }),
|
|
63
|
+
V.loading && !V.data && /* @__PURE__ */ b("p", {
|
|
64
|
+
className: "text-muted-foreground",
|
|
65
|
+
children: A("planmagnet.productFeatures.loading", "Loading features…")
|
|
66
|
+
}),
|
|
67
|
+
U ? /* @__PURE__ */ x("div", {
|
|
68
|
+
className: "flex items-start justify-between gap-2",
|
|
69
|
+
children: [/* @__PURE__ */ x("button", {
|
|
70
|
+
type: "button",
|
|
71
|
+
className: "min-w-0 text-left hover:underline",
|
|
72
|
+
onClick: () => M(a("/products/product-feature-detail-page", U.id)),
|
|
73
|
+
"data-testid": "feature-request-product-feature-link",
|
|
74
|
+
children: [/* @__PURE__ */ x("span", {
|
|
75
|
+
className: "flex items-center gap-2 font-medium",
|
|
76
|
+
children: [/* @__PURE__ */ b(g, { className: "h-4 w-4 shrink-0 text-primary" }), /* @__PURE__ */ b("span", {
|
|
77
|
+
className: "truncate",
|
|
78
|
+
children: U.name
|
|
79
|
+
})]
|
|
80
|
+
}), U.product && /* @__PURE__ */ b("span", {
|
|
81
|
+
className: "text-xs text-muted-foreground",
|
|
82
|
+
children: A("planmagnet.productFeatures.requestCard.inProduct", "in {{name}}", { name: U.product.name })
|
|
83
|
+
})]
|
|
84
|
+
}), /* @__PURE__ */ x("div", {
|
|
85
|
+
className: "flex shrink-0 items-center gap-2",
|
|
86
|
+
children: [
|
|
87
|
+
U.archivedAt && /* @__PURE__ */ b(S, {
|
|
88
|
+
variant: "outline",
|
|
89
|
+
className: "text-xs",
|
|
90
|
+
children: A("planmagnet.productFeatures.archived", "Archived")
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ b(p, { status: U.status }),
|
|
93
|
+
/* @__PURE__ */ b(C, {
|
|
94
|
+
size: "icon",
|
|
95
|
+
variant: "ghost",
|
|
96
|
+
"aria-label": A("planmagnet.productFeatures.requestCard.unlink", "Unlink the product feature"),
|
|
97
|
+
disabled: X.loading,
|
|
98
|
+
onClick: () => void te(),
|
|
99
|
+
"data-testid": "feature-request-product-feature-unlink",
|
|
100
|
+
children: /* @__PURE__ */ b(_, { className: "h-4 w-4" })
|
|
101
|
+
})
|
|
102
|
+
]
|
|
103
|
+
})]
|
|
104
|
+
}) : H && !N && /* @__PURE__ */ b("p", {
|
|
105
|
+
className: "text-muted-foreground",
|
|
106
|
+
children: A("planmagnet.productFeatures.requestCard.none", "Not linked to a product feature yet — link the feature that will answer this request.")
|
|
107
|
+
}),
|
|
108
|
+
z && /* @__PURE__ */ b(i, { message: z }),
|
|
109
|
+
H && N && /* @__PURE__ */ x("div", {
|
|
110
|
+
className: "space-y-2",
|
|
111
|
+
children: [
|
|
112
|
+
H.productId === null && /* @__PURE__ */ x("div", {
|
|
113
|
+
className: "space-y-1",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ b("label", {
|
|
116
|
+
htmlFor: "feature-request-pick-product",
|
|
117
|
+
className: "text-xs font-medium",
|
|
118
|
+
children: A("planmagnet.productFeatures.requestCard.product", "Product")
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ b(o, {
|
|
121
|
+
id: "feature-request-pick-product",
|
|
122
|
+
value: F,
|
|
123
|
+
noneLabel: A("planmagnet.productFeatures.requestCard.chooseProduct", "Choose a product"),
|
|
124
|
+
onChange: (e) => {
|
|
125
|
+
I(e), R("");
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
/* @__PURE__ */ b("p", {
|
|
129
|
+
className: "text-xs text-muted-foreground",
|
|
130
|
+
children: A("planmagnet.productFeatures.requestCard.noProductHint", "This request has no product, so any product’s feature can answer it. Linking does not set its product.")
|
|
131
|
+
})
|
|
132
|
+
]
|
|
133
|
+
}),
|
|
134
|
+
/* @__PURE__ */ b("label", {
|
|
135
|
+
htmlFor: "feature-request-pick-feature",
|
|
136
|
+
className: "text-xs font-medium",
|
|
137
|
+
children: A("planmagnet.productFeatures.requestCard.feature", "Feature")
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ x("select", {
|
|
140
|
+
id: "feature-request-pick-feature",
|
|
141
|
+
className: O,
|
|
142
|
+
value: L,
|
|
143
|
+
disabled: !W,
|
|
144
|
+
onChange: (e) => R(e.target.value),
|
|
145
|
+
"data-testid": "feature-request-pick-feature",
|
|
146
|
+
children: [/* @__PURE__ */ b("option", {
|
|
147
|
+
value: "",
|
|
148
|
+
children: G.loading && !G.data ? A("planmagnet.productFeatures.loading", "Loading features…") : A("planmagnet.productFeatures.requestCard.chooseFeature", "Choose a feature")
|
|
149
|
+
}), Q.map((e) => /* @__PURE__ */ b("option", {
|
|
150
|
+
value: e.id,
|
|
151
|
+
children: e.name
|
|
152
|
+
}, e.id))]
|
|
153
|
+
}),
|
|
154
|
+
G.data && Q.length === 0 && /* @__PURE__ */ b("p", {
|
|
155
|
+
className: "text-xs text-muted-foreground",
|
|
156
|
+
children: A("planmagnet.productFeatures.requestCard.noFeatures", "This product has no other active feature. Add one from the product’s page.")
|
|
157
|
+
}),
|
|
158
|
+
G.error && /* @__PURE__ */ b(i, { message: G.error.message }),
|
|
159
|
+
/* @__PURE__ */ x("div", {
|
|
160
|
+
className: "flex justify-end gap-2",
|
|
161
|
+
children: [/* @__PURE__ */ b(C, {
|
|
162
|
+
size: "sm",
|
|
163
|
+
variant: "ghost",
|
|
164
|
+
onClick: $,
|
|
165
|
+
children: A("planmagnet.productFeatures.actions.cancel", "Cancel")
|
|
166
|
+
}), /* @__PURE__ */ b(C, {
|
|
167
|
+
size: "sm",
|
|
168
|
+
disabled: !L || J.loading,
|
|
169
|
+
onClick: () => void ee(),
|
|
170
|
+
"data-testid": "feature-request-product-feature-save",
|
|
171
|
+
children: A("planmagnet.productFeatures.requestCard.link", "Link")
|
|
172
|
+
})]
|
|
173
|
+
})
|
|
174
|
+
]
|
|
175
|
+
}),
|
|
176
|
+
H && !N && /* @__PURE__ */ x(C, {
|
|
177
|
+
size: "sm",
|
|
178
|
+
variant: "outline",
|
|
179
|
+
onClick: () => {
|
|
180
|
+
B(null), P(!0);
|
|
181
|
+
},
|
|
182
|
+
"data-testid": "feature-request-product-feature-choose",
|
|
183
|
+
children: [/* @__PURE__ */ b(h, { className: "mr-1 h-4 w-4" }), U ? A("planmagnet.productFeatures.requestCard.change", "Change feature") : A("planmagnet.productFeatures.requestCard.choose", "Link a feature")]
|
|
184
|
+
})
|
|
185
|
+
]
|
|
186
|
+
})]
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
export { k as ProductFeatureLinkCard };
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=ProductFeatureLinkCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductFeatureLinkCard.js","names":[],"sources":["../../../src/planmagnet/product/ProductFeatureLinkCard.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Badge, Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { Link2, Puzzle, X } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { ProductSelect } from '../controls/Pickers';\nimport { throwOnError } from '../utils/mutationResult';\nimport { detailPath } from '../utils/rowNavigation';\nimport {\n FEATURE_REQUEST_FEATURE_OPTIONS,\n FEATURE_REQUEST_PRODUCT_FEATURE,\n LINK_FEATURE_REQUEST_TO_PRODUCT_FEATURE,\n PRODUCT_FEATURE_QUERIES,\n UNLINK_FEATURE_REQUEST_FROM_PRODUCT_FEATURE,\n} from './hierarchyOperations';\nimport { ProductFeatureStatusBadge } from './ProductFeatureControls';\nimport { refusalMessage } from './refusal';\n\n/**\n * A feature request's product feature (PRM-01): the capability the team is building that answers this request.\n * The request stays a request — linking never converts it, and never sets or changes its product. A request with a\n * product may only point at a feature of that product, so its features are the only ones offered; a request with\n * no product picks a product first, then one of its features. Linking a request that already points at a feature\n * moves it (a request points at one feature at most).\n */\n\ninterface LinkedFeature {\n id: string;\n name: string;\n status: string;\n archivedAt: string | null;\n product: { id: string; name: string } | null;\n}\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nexport function ProductFeatureLinkCard({ featureRequestId }: { featureRequestId: string }) {\n const tr = useTr();\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [choosing, setChoosing] = useState(false);\n const [pickedProductId, setPickedProductId] = useState('');\n const [featureId, setFeatureId] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const current = useQuery<{\n planmagnetFeatureRequest: {\n id: string;\n productId: string | null;\n productFeature: LinkedFeature | null;\n } | null;\n }>(FEATURE_REQUEST_PRODUCT_FEATURE, {\n variables: { id: featureRequestId },\n skip: !featureRequestId,\n });\n const request = current.data?.planmagnetFeatureRequest ?? null;\n const linked = request?.productFeature ?? null;\n // A request with a product can only point at that product's features.\n const productId = request?.productId ?? pickedProductId;\n\n const options = useQuery<{\n planmagnetProductFeatures: {\n items: { id: string; name: string; status: string }[];\n totalCount: number;\n };\n }>(FEATURE_REQUEST_FEATURE_OPTIONS, {\n variables: { workspaceId: workspaceId ?? '', productId },\n skip: !choosing || !workspaceId || !productId,\n });\n const writeOptions = { refetchQueries: PRODUCT_FEATURE_QUERIES, awaitRefetchQueries: true };\n const [link, linkState] = useMutation(LINK_FEATURE_REQUEST_TO_PRODUCT_FEATURE, writeOptions);\n const [unlink, unlinkState] = useMutation(\n UNLINK_FEATURE_REQUEST_FROM_PRODUCT_FEATURE,\n writeOptions\n );\n const forbidden = tr(\n 'planmagnet.productFeatures.errors.forbiddenLinks',\n 'Your role cannot change what this feature is linked to.'\n );\n const featureOptions = (options.data?.planmagnetProductFeatures.items ?? []).filter(\n (f) => f.id !== linked?.id\n );\n\n const closeChooser = () => {\n setChoosing(false);\n setPickedProductId('');\n setFeatureId('');\n };\n\n const onLink = async () => {\n if (!featureId) return;\n setError(null);\n try {\n await throwOnError(link({ variables: { featureRequestId, productFeatureId: featureId } }));\n closeChooser();\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr(\n 'planmagnet.productFeatures.requestCard.errors.link',\n 'The request could not be linked to the feature.'\n ),\n forbidden\n )\n );\n }\n };\n\n const onUnlink = async () => {\n if (!linked) return;\n setError(null);\n try {\n await throwOnError(unlink({ variables: { featureRequestId, productFeatureId: linked.id } }));\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr(\n 'planmagnet.productFeatures.requestCard.errors.unlink',\n 'The request could not be unlinked from the feature.'\n ),\n forbidden\n )\n );\n }\n };\n\n return (\n <Card data-testid=\"feature-request-product-feature\">\n <CardHeader>\n <CardTitle>\n {tr('planmagnet.productFeatures.requestCard.title', 'Product feature')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n {current.error && (\n <ErrorLine\n message={tr(\n 'planmagnet.productFeatures.requestCard.errors.load',\n 'The product feature could not be loaded: {{message}}',\n { message: current.error.message }\n )}\n />\n )}\n {current.loading && !current.data && (\n <p className=\"text-muted-foreground\">\n {tr('planmagnet.productFeatures.loading', 'Loading features…')}\n </p>\n )}\n {linked ? (\n <div className=\"flex items-start justify-between gap-2\">\n <button\n type=\"button\"\n className=\"min-w-0 text-left hover:underline\"\n onClick={() =>\n navigate(detailPath('/products/product-feature-detail-page', linked.id))\n }\n data-testid=\"feature-request-product-feature-link\"\n >\n <span className=\"flex items-center gap-2 font-medium\">\n <Puzzle className=\"h-4 w-4 shrink-0 text-primary\" />\n <span className=\"truncate\">{linked.name}</span>\n </span>\n {linked.product && (\n <span className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.productFeatures.requestCard.inProduct', 'in {{name}}', {\n name: linked.product.name,\n })}\n </span>\n )}\n </button>\n <div className=\"flex shrink-0 items-center gap-2\">\n {linked.archivedAt && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {tr('planmagnet.productFeatures.archived', 'Archived')}\n </Badge>\n )}\n <ProductFeatureStatusBadge status={linked.status} />\n <Button\n size=\"icon\"\n variant=\"ghost\"\n aria-label={tr(\n 'planmagnet.productFeatures.requestCard.unlink',\n 'Unlink the product feature'\n )}\n disabled={unlinkState.loading}\n onClick={() => void onUnlink()}\n data-testid=\"feature-request-product-feature-unlink\"\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ) : (\n request &&\n !choosing && (\n <p className=\"text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requestCard.none',\n 'Not linked to a product feature yet — link the feature that will answer this request.'\n )}\n </p>\n )\n )}\n {error && <ErrorLine message={error} />}\n {request && choosing && (\n <div className=\"space-y-2\">\n {request.productId === null && (\n <div className=\"space-y-1\">\n <label htmlFor=\"feature-request-pick-product\" className=\"text-xs font-medium\">\n {tr('planmagnet.productFeatures.requestCard.product', 'Product')}\n </label>\n <ProductSelect\n id=\"feature-request-pick-product\"\n value={pickedProductId}\n noneLabel={tr(\n 'planmagnet.productFeatures.requestCard.chooseProduct',\n 'Choose a product'\n )}\n onChange={(next) => {\n setPickedProductId(next);\n setFeatureId('');\n }}\n />\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requestCard.noProductHint',\n 'This request has no product, so any product’s feature can answer it. Linking does not set its product.'\n )}\n </p>\n </div>\n )}\n <label htmlFor=\"feature-request-pick-feature\" className=\"text-xs font-medium\">\n {tr('planmagnet.productFeatures.requestCard.feature', 'Feature')}\n </label>\n <select\n id=\"feature-request-pick-feature\"\n className={selectClass}\n value={featureId}\n disabled={!productId}\n onChange={(e) => setFeatureId(e.target.value)}\n data-testid=\"feature-request-pick-feature\"\n >\n <option value=\"\">\n {options.loading && !options.data\n ? tr('planmagnet.productFeatures.loading', 'Loading features…')\n : tr('planmagnet.productFeatures.requestCard.chooseFeature', 'Choose a feature')}\n </option>\n {featureOptions.map((f) => (\n <option key={f.id} value={f.id}>\n {f.name}\n </option>\n ))}\n </select>\n {options.data && featureOptions.length === 0 && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requestCard.noFeatures',\n 'This product has no other active feature. Add one from the product’s page.'\n )}\n </p>\n )}\n {options.error && <ErrorLine message={options.error.message} />}\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"ghost\" onClick={closeChooser}>\n {tr('planmagnet.productFeatures.actions.cancel', 'Cancel')}\n </Button>\n <Button\n size=\"sm\"\n disabled={!featureId || linkState.loading}\n onClick={() => void onLink()}\n data-testid=\"feature-request-product-feature-save\"\n >\n {tr('planmagnet.productFeatures.requestCard.link', 'Link')}\n </Button>\n </div>\n </div>\n )}\n {request && !choosing && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setError(null);\n setChoosing(true);\n }}\n data-testid=\"feature-request-product-feature-choose\"\n >\n <Link2 className=\"mr-1 h-4 w-4\" />\n {linked\n ? tr('planmagnet.productFeatures.requestCard.change', 'Change feature')\n : tr('planmagnet.productFeatures.requestCard.choose', 'Link a feature')}\n </Button>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|