@burdenoff/microfe-planmagnet 2026.1002.1 → 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/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/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 +4 -4
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { useTr as n } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { ErrorLine as r } from "../controls/ConfirmButton.js";
|
|
4
|
+
import { enumLabel as i } from "../controls/WorkItemControls.js";
|
|
5
|
+
import { detailPath as a } from "../utils/rowNavigation.js";
|
|
6
|
+
import { PRODUCT_HIERARCHY as o } from "../product/hierarchyOperations.js";
|
|
7
|
+
import { allNodeKeys as s, buildProductHierarchy as c, epicKey as l, featureKey as u, filterProductHierarchy as d, hierarchyTotals as f, lineKey as p, productKey as m } from "../product/hierarchy.js";
|
|
8
|
+
import { ProductFeatureStatusBadge as h } from "../product/ProductFeatureControls.js";
|
|
9
|
+
import { useDeferredValue as g, useMemo as _, useState as v } from "react";
|
|
10
|
+
import { Archive as y, ChevronDown as b, ChevronRight as x, ChevronsDownUp as S, ChevronsUpDown as C, CircleDot as w, Layers as T, ListTree as E, Loader2 as D, Milestone as O, Package as k, Puzzle as A } from "lucide-react";
|
|
11
|
+
import { useQuery as j } from "@apollo/client/react";
|
|
12
|
+
import { jsx as M, jsxs as N } from "react/jsx-runtime";
|
|
13
|
+
import { Badge as P, Button as F, Card as I, CardContent as L, Input as R } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/planmagnet/pages/ProductsProductHierarchyPagePage.tsx
|
|
15
|
+
function z(e) {
|
|
16
|
+
return `/tasks/task-detail-page?taskId=${encodeURIComponent(e)}`;
|
|
17
|
+
}
|
|
18
|
+
function B(e) {
|
|
19
|
+
let t = /* @__PURE__ */ new Set();
|
|
20
|
+
for (let n of e) {
|
|
21
|
+
t.add(p(n.line));
|
|
22
|
+
for (let e of n.products) t.add(m(e.id));
|
|
23
|
+
}
|
|
24
|
+
return t;
|
|
25
|
+
}
|
|
26
|
+
function V() {
|
|
27
|
+
let i = n(), { workspaceId: l } = e(), u = t(), [m, h] = v(!1), [b, x] = v(""), w = g(b), [O, k] = v(null), { data: A, previousData: P, loading: V, error: H } = j(o, {
|
|
28
|
+
variables: {
|
|
29
|
+
workspaceId: l ?? "",
|
|
30
|
+
lineFilter: m ? void 0 : { isActive: !0 },
|
|
31
|
+
productFilter: m ? void 0 : { isActive: !0 },
|
|
32
|
+
includeArchivedFeatures: m
|
|
33
|
+
},
|
|
34
|
+
skip: !l
|
|
35
|
+
}), U = A ?? P, G = _(() => U ? c(U.planmagnetProductLines.items, U.planmagnetProducts.items) : [], [U]), K = _(() => d(G, w), [G, w]), q = O ?? B(G), J = (e) => q.has(e), Y = (e) => k((t) => {
|
|
36
|
+
let n = new Set(t ?? B(G));
|
|
37
|
+
return n.has(e) ? n.delete(e) : n.add(e), n;
|
|
38
|
+
}), X = (e) => {
|
|
39
|
+
x(e);
|
|
40
|
+
let { expand: t } = d(G, e);
|
|
41
|
+
t.size > 0 && k((e) => new Set([...e ?? B(G), ...t]));
|
|
42
|
+
};
|
|
43
|
+
if (V && !U) return /* @__PURE__ */ M("div", {
|
|
44
|
+
className: "flex items-center justify-center p-12",
|
|
45
|
+
children: /* @__PURE__ */ M(D, { className: "h-8 w-8 animate-spin text-muted-foreground" })
|
|
46
|
+
});
|
|
47
|
+
if (H && !U) return /* @__PURE__ */ M("div", {
|
|
48
|
+
className: "p-6 text-destructive",
|
|
49
|
+
children: i("planmagnet.productHierarchy.errors.load", "Failed to load the product hierarchy: {{message}}", { message: H.message })
|
|
50
|
+
});
|
|
51
|
+
let Z = f(K.groups), Q = !!U && U.planmagnetProductLines.totalCount > U.planmagnetProductLines.items.length, $ = !!U && U.planmagnetProducts.totalCount > U.planmagnetProducts.items.length;
|
|
52
|
+
return /* @__PURE__ */ N("div", {
|
|
53
|
+
className: "space-y-6 p-6",
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ N("div", {
|
|
56
|
+
className: "flex flex-wrap items-start justify-between gap-4",
|
|
57
|
+
children: [/* @__PURE__ */ N("div", { children: [/* @__PURE__ */ N("h1", {
|
|
58
|
+
className: "flex items-center gap-2 text-2xl font-semibold",
|
|
59
|
+
children: [/* @__PURE__ */ M(E, { className: "h-6 w-6" }), i("planmagnet.productHierarchy.title", "Product hierarchy")]
|
|
60
|
+
}), /* @__PURE__ */ M("p", {
|
|
61
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
62
|
+
children: i("planmagnet.productHierarchy.subtitle", "Product lines › products › features › epics › stories, as linked on each record’s page. Nothing here is inferred.")
|
|
63
|
+
})] }), /* @__PURE__ */ N("div", {
|
|
64
|
+
className: "flex flex-wrap items-center gap-2",
|
|
65
|
+
children: [/* @__PURE__ */ N(F, {
|
|
66
|
+
size: "sm",
|
|
67
|
+
variant: "outline",
|
|
68
|
+
onClick: () => u("/products/product-lines-page"),
|
|
69
|
+
"data-testid": "product-hierarchy-manage-lines",
|
|
70
|
+
children: [/* @__PURE__ */ M(T, { className: "mr-1 h-4 w-4" }), i("planmagnet.productHierarchy.manageLines", "Product lines")]
|
|
71
|
+
}), /* @__PURE__ */ N(F, {
|
|
72
|
+
size: "sm",
|
|
73
|
+
variant: m ? "default" : "outline",
|
|
74
|
+
"aria-pressed": m,
|
|
75
|
+
onClick: () => h((e) => !e),
|
|
76
|
+
"data-testid": "product-hierarchy-toggle-archived",
|
|
77
|
+
children: [/* @__PURE__ */ M(y, { className: "mr-1 h-4 w-4" }), i("planmagnet.productHierarchy.showArchived", "Show archived")]
|
|
78
|
+
})]
|
|
79
|
+
})]
|
|
80
|
+
}),
|
|
81
|
+
/* @__PURE__ */ N("div", {
|
|
82
|
+
className: "flex flex-wrap items-center gap-2",
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ M(R, {
|
|
85
|
+
"aria-label": i("planmagnet.productHierarchy.search", "Search the hierarchy"),
|
|
86
|
+
placeholder: i("planmagnet.productHierarchy.searchPlaceholder", "Search lines, products, features, epics, stories…"),
|
|
87
|
+
value: b,
|
|
88
|
+
onChange: (e) => X(e.target.value),
|
|
89
|
+
className: "max-w-sm",
|
|
90
|
+
"data-testid": "product-hierarchy-search"
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ N(F, {
|
|
93
|
+
size: "sm",
|
|
94
|
+
variant: "ghost",
|
|
95
|
+
onClick: () => k(s(G)),
|
|
96
|
+
"data-testid": "product-hierarchy-expand-all",
|
|
97
|
+
children: [/* @__PURE__ */ M(C, { className: "mr-1 h-4 w-4" }), i("planmagnet.productHierarchy.expandAll", "Expand all")]
|
|
98
|
+
}),
|
|
99
|
+
/* @__PURE__ */ N(F, {
|
|
100
|
+
size: "sm",
|
|
101
|
+
variant: "ghost",
|
|
102
|
+
onClick: () => k(/* @__PURE__ */ new Set()),
|
|
103
|
+
"data-testid": "product-hierarchy-collapse-all",
|
|
104
|
+
children: [/* @__PURE__ */ M(S, { className: "mr-1 h-4 w-4" }), i("planmagnet.productHierarchy.collapseAll", "Collapse all")]
|
|
105
|
+
})
|
|
106
|
+
]
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ M("p", {
|
|
109
|
+
className: "text-xs text-muted-foreground",
|
|
110
|
+
"data-testid": "product-hierarchy-totals",
|
|
111
|
+
children: i("planmagnet.productHierarchy.totals", "{{lines}} line(s) · {{products}} product(s) · {{features}} feature(s) · {{epics}} epic(s) · {{stories}} story/item(s)", {
|
|
112
|
+
lines: Z.lines,
|
|
113
|
+
products: Z.products,
|
|
114
|
+
features: Z.features,
|
|
115
|
+
epics: Z.epics,
|
|
116
|
+
stories: Z.children
|
|
117
|
+
})
|
|
118
|
+
}),
|
|
119
|
+
H && /* @__PURE__ */ M(r, { message: i("planmagnet.productHierarchy.errors.refresh", "The hierarchy could not be refreshed: {{message}}", { message: H.message }) }),
|
|
120
|
+
(Q || $) && /* @__PURE__ */ M("p", {
|
|
121
|
+
className: "text-xs text-muted-foreground",
|
|
122
|
+
children: i("planmagnet.productHierarchy.truncated", "Only the first 100 product lines and 100 products are shown.")
|
|
123
|
+
}),
|
|
124
|
+
K.groups.length === 0 ? /* @__PURE__ */ M(I, { children: /* @__PURE__ */ M(L, {
|
|
125
|
+
className: "py-12 text-center text-sm text-muted-foreground",
|
|
126
|
+
children: w.trim() ? i("planmagnet.productHierarchy.emptySearch", "Nothing in the hierarchy matches.") : i("planmagnet.productHierarchy.empty", "No product lines or products yet. Create a product line, then put products in it from each product’s page.")
|
|
127
|
+
}) }) : /* @__PURE__ */ M(I, { children: /* @__PURE__ */ M(L, {
|
|
128
|
+
className: "pt-4",
|
|
129
|
+
children: /* @__PURE__ */ M("ul", {
|
|
130
|
+
className: "space-y-1",
|
|
131
|
+
"data-testid": "product-hierarchy-tree",
|
|
132
|
+
children: K.groups.map((e) => /* @__PURE__ */ M(W, {
|
|
133
|
+
group: e,
|
|
134
|
+
isOpen: J,
|
|
135
|
+
toggle: Y,
|
|
136
|
+
onOpenProduct: (e) => u(a("/products/product-detail-page", e)),
|
|
137
|
+
onOpenFeature: (e) => u(a("/products/product-feature-detail-page", e)),
|
|
138
|
+
onOpenWorkItem: (e) => u(z(e))
|
|
139
|
+
}, p(e.line)))
|
|
140
|
+
})
|
|
141
|
+
}) })
|
|
142
|
+
]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function H({ nodeKey: e, label: t, expandable: r, isOpen: i, toggle: a, children: o }) {
|
|
146
|
+
let s = n(), c = i(e);
|
|
147
|
+
return /* @__PURE__ */ N("div", {
|
|
148
|
+
className: "flex min-w-0 items-start gap-2 py-1 text-sm",
|
|
149
|
+
children: [r ? /* @__PURE__ */ M("button", {
|
|
150
|
+
type: "button",
|
|
151
|
+
className: "shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted",
|
|
152
|
+
"aria-expanded": c,
|
|
153
|
+
"aria-label": c ? s("planmagnet.productHierarchy.collapseNamed", "Collapse {{name}}", { name: t }) : s("planmagnet.productHierarchy.expandNamed", "Expand {{name}}", { name: t }),
|
|
154
|
+
onClick: () => a(e),
|
|
155
|
+
children: M(c ? b : x, { className: "h-4 w-4" })
|
|
156
|
+
}) : /* @__PURE__ */ M("span", {
|
|
157
|
+
className: "w-5 shrink-0",
|
|
158
|
+
"aria-hidden": "true"
|
|
159
|
+
}), /* @__PURE__ */ M("div", {
|
|
160
|
+
className: "flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-0.5",
|
|
161
|
+
children: o
|
|
162
|
+
})]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
var U = "ml-3 space-y-1 border-l pl-3";
|
|
166
|
+
function W({ group: e, ...t }) {
|
|
167
|
+
let r = n(), i = p(e.line), a = e.line?.name ?? r("planmagnet.productHierarchy.noLine", "No product line");
|
|
168
|
+
return /* @__PURE__ */ N("li", {
|
|
169
|
+
"data-testid": "hierarchy-line",
|
|
170
|
+
children: [/* @__PURE__ */ N(H, {
|
|
171
|
+
nodeKey: i,
|
|
172
|
+
label: a,
|
|
173
|
+
expandable: e.products.length > 0,
|
|
174
|
+
isOpen: t.isOpen,
|
|
175
|
+
toggle: t.toggle,
|
|
176
|
+
children: [
|
|
177
|
+
e.line ? /* @__PURE__ */ M(T, { className: "h-4 w-4 shrink-0 text-primary" }) : /* @__PURE__ */ M(k, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
178
|
+
/* @__PURE__ */ M("span", {
|
|
179
|
+
className: e.line ? "truncate font-semibold" : "truncate font-semibold text-muted-foreground",
|
|
180
|
+
children: a
|
|
181
|
+
}),
|
|
182
|
+
e.line?.archivedAt && /* @__PURE__ */ M(P, {
|
|
183
|
+
variant: "outline",
|
|
184
|
+
className: "text-xs",
|
|
185
|
+
children: r("planmagnet.productHierarchy.archived", "Archived")
|
|
186
|
+
}),
|
|
187
|
+
/* @__PURE__ */ M("span", {
|
|
188
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
189
|
+
children: r("planmagnet.productHierarchy.productCount", "{{count}} product(s)", { count: e.products.length })
|
|
190
|
+
})
|
|
191
|
+
]
|
|
192
|
+
}), t.isOpen(i) && e.products.length > 0 && /* @__PURE__ */ M("ul", {
|
|
193
|
+
className: U,
|
|
194
|
+
children: e.products.map((e) => /* @__PURE__ */ M(G, {
|
|
195
|
+
product: e,
|
|
196
|
+
...t
|
|
197
|
+
}, e.id))
|
|
198
|
+
})]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function G({ product: e, ...t }) {
|
|
202
|
+
let r = n(), a = m(e.id);
|
|
203
|
+
return /* @__PURE__ */ N("li", {
|
|
204
|
+
"data-testid": "hierarchy-product",
|
|
205
|
+
children: [/* @__PURE__ */ N(H, {
|
|
206
|
+
nodeKey: a,
|
|
207
|
+
label: e.name,
|
|
208
|
+
expandable: e.features.length > 0,
|
|
209
|
+
isOpen: t.isOpen,
|
|
210
|
+
toggle: t.toggle,
|
|
211
|
+
children: [
|
|
212
|
+
/* @__PURE__ */ M(k, { className: "h-4 w-4 shrink-0 text-primary" }),
|
|
213
|
+
/* @__PURE__ */ M("button", {
|
|
214
|
+
type: "button",
|
|
215
|
+
className: "min-w-0 truncate text-left font-medium hover:underline",
|
|
216
|
+
onClick: () => t.onOpenProduct(e.id),
|
|
217
|
+
children: e.name
|
|
218
|
+
}),
|
|
219
|
+
/* @__PURE__ */ M(P, {
|
|
220
|
+
variant: "secondary",
|
|
221
|
+
className: "text-xs",
|
|
222
|
+
children: i(e.lifecycleStage)
|
|
223
|
+
}),
|
|
224
|
+
e.archivedAt && /* @__PURE__ */ M(P, {
|
|
225
|
+
variant: "outline",
|
|
226
|
+
className: "text-xs",
|
|
227
|
+
children: r("planmagnet.productHierarchy.archived", "Archived")
|
|
228
|
+
}),
|
|
229
|
+
/* @__PURE__ */ M("span", {
|
|
230
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
231
|
+
children: r("planmagnet.productHierarchy.featureCount", "{{count}} feature(s)", { count: e.features.length })
|
|
232
|
+
})
|
|
233
|
+
]
|
|
234
|
+
}), t.isOpen(a) && e.features.length > 0 && /* @__PURE__ */ M("ul", {
|
|
235
|
+
className: U,
|
|
236
|
+
children: e.features.map((e) => /* @__PURE__ */ M(K, {
|
|
237
|
+
feature: e,
|
|
238
|
+
...t
|
|
239
|
+
}, e.id))
|
|
240
|
+
})]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function K({ feature: e, ...t }) {
|
|
244
|
+
let r = n(), i = u(e.id);
|
|
245
|
+
return /* @__PURE__ */ N("li", {
|
|
246
|
+
"data-testid": "hierarchy-feature",
|
|
247
|
+
children: [/* @__PURE__ */ N(H, {
|
|
248
|
+
nodeKey: i,
|
|
249
|
+
label: e.name,
|
|
250
|
+
expandable: e.epics.length > 0,
|
|
251
|
+
isOpen: t.isOpen,
|
|
252
|
+
toggle: t.toggle,
|
|
253
|
+
children: [
|
|
254
|
+
/* @__PURE__ */ M(A, { className: "h-4 w-4 shrink-0 text-primary" }),
|
|
255
|
+
/* @__PURE__ */ M("button", {
|
|
256
|
+
type: "button",
|
|
257
|
+
className: "min-w-0 truncate text-left hover:underline",
|
|
258
|
+
onClick: () => t.onOpenFeature(e.id),
|
|
259
|
+
children: e.name
|
|
260
|
+
}),
|
|
261
|
+
/* @__PURE__ */ M(h, { status: e.status }),
|
|
262
|
+
e.archivedAt && /* @__PURE__ */ M(P, {
|
|
263
|
+
variant: "outline",
|
|
264
|
+
className: "text-xs",
|
|
265
|
+
children: r("planmagnet.productHierarchy.archived", "Archived")
|
|
266
|
+
}),
|
|
267
|
+
/* @__PURE__ */ M("span", {
|
|
268
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
269
|
+
children: r("planmagnet.productHierarchy.epicCount", "{{count}} epic(s)", { count: e.epics.length })
|
|
270
|
+
})
|
|
271
|
+
]
|
|
272
|
+
}), t.isOpen(i) && e.epics.length > 0 && /* @__PURE__ */ M("ul", {
|
|
273
|
+
className: U,
|
|
274
|
+
children: e.epics.map((e) => /* @__PURE__ */ M(q, {
|
|
275
|
+
epic: e,
|
|
276
|
+
...t
|
|
277
|
+
}, e.id))
|
|
278
|
+
})]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function q({ epic: e, ...t }) {
|
|
282
|
+
let r = n(), a = l(e.id);
|
|
283
|
+
return /* @__PURE__ */ N("li", {
|
|
284
|
+
"data-testid": "hierarchy-epic",
|
|
285
|
+
children: [/* @__PURE__ */ N(H, {
|
|
286
|
+
nodeKey: a,
|
|
287
|
+
label: `${e.key} ${e.title}`,
|
|
288
|
+
expandable: e.children.length > 0,
|
|
289
|
+
isOpen: t.isOpen,
|
|
290
|
+
toggle: t.toggle,
|
|
291
|
+
children: [
|
|
292
|
+
/* @__PURE__ */ M(O, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
293
|
+
/* @__PURE__ */ N("button", {
|
|
294
|
+
type: "button",
|
|
295
|
+
className: "min-w-0 truncate text-left hover:underline",
|
|
296
|
+
onClick: () => t.onOpenWorkItem(e.id),
|
|
297
|
+
children: [/* @__PURE__ */ M("span", {
|
|
298
|
+
className: "mr-2 text-muted-foreground",
|
|
299
|
+
children: e.key
|
|
300
|
+
}), e.title]
|
|
301
|
+
}),
|
|
302
|
+
/* @__PURE__ */ N("span", {
|
|
303
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
304
|
+
children: [
|
|
305
|
+
i(e.status),
|
|
306
|
+
" ·",
|
|
307
|
+
" ",
|
|
308
|
+
r("planmagnet.productHierarchy.storyCount", "{{count}} story/item(s)", { count: e.children.length })
|
|
309
|
+
]
|
|
310
|
+
})
|
|
311
|
+
]
|
|
312
|
+
}), t.isOpen(a) && e.children.length > 0 && /* @__PURE__ */ M("ul", {
|
|
313
|
+
className: U,
|
|
314
|
+
children: e.children.map((e) => /* @__PURE__ */ M("li", {
|
|
315
|
+
"data-testid": "hierarchy-story",
|
|
316
|
+
children: /* @__PURE__ */ N(H, {
|
|
317
|
+
nodeKey: `item:${e.id}`,
|
|
318
|
+
label: e.title,
|
|
319
|
+
expandable: !1,
|
|
320
|
+
isOpen: t.isOpen,
|
|
321
|
+
toggle: t.toggle,
|
|
322
|
+
children: [
|
|
323
|
+
/* @__PURE__ */ M(w, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
324
|
+
/* @__PURE__ */ N("button", {
|
|
325
|
+
type: "button",
|
|
326
|
+
className: "min-w-0 truncate text-left hover:underline",
|
|
327
|
+
onClick: () => t.onOpenWorkItem(e.id),
|
|
328
|
+
children: [/* @__PURE__ */ M("span", {
|
|
329
|
+
className: "mr-2 text-muted-foreground",
|
|
330
|
+
children: e.key
|
|
331
|
+
}), e.title]
|
|
332
|
+
}),
|
|
333
|
+
/* @__PURE__ */ N("span", {
|
|
334
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
335
|
+
children: [
|
|
336
|
+
i(e.type),
|
|
337
|
+
" · ",
|
|
338
|
+
i(e.status)
|
|
339
|
+
]
|
|
340
|
+
})
|
|
341
|
+
]
|
|
342
|
+
})
|
|
343
|
+
}, e.id))
|
|
344
|
+
})]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
//#endregion
|
|
348
|
+
export { V as ProductsProductHierarchyPagePage };
|
|
349
|
+
|
|
350
|
+
//# sourceMappingURL=ProductsProductHierarchyPagePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductsProductHierarchyPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsProductHierarchyPagePage.tsx"],"sourcesContent":["import { useDeferredValue, useMemo, useState, type ReactNode } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Button, Card, CardContent, Input } from '@burdenoff/fe-libs/ui';\nimport {\n Archive,\n ChevronDown,\n ChevronRight,\n ChevronsDownUp,\n ChevronsUpDown,\n CircleDot,\n Layers,\n ListTree,\n Loader2,\n Milestone,\n Package,\n Puzzle,\n} from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { PRODUCT_HIERARCHY } from '../product/hierarchyOperations';\nimport { ProductFeatureStatusBadge } from '../product/ProductFeatureControls';\nimport {\n allNodeKeys,\n buildProductHierarchy,\n epicKey,\n featureKey,\n filterProductHierarchy,\n hierarchyTotals,\n lineKey,\n productKey,\n type HierarchyEpic,\n type HierarchyFeature,\n type HierarchyGroup,\n type HierarchyLineRef,\n type HierarchyProduct,\n} from '../product/hierarchy';\n\n/**\n * Product hierarchy (PRM-01): Product line > Product > Feature > Epic > Story, end to end, read from what people\n * linked — nothing is inferred. Products in no line sit under \"No product line\". Every level collapses; search\n * narrows the tree to what matches and opens the path to each match; \"Show archived\" brings back archived lines,\n * products and features. Each name opens that record's own page, where the links are managed.\n */\n\ninterface HierarchyData {\n planmagnetProductLines: { items: HierarchyLineRef[]; totalCount: number };\n planmagnetProducts: { items: HierarchyProduct[]; totalCount: number };\n}\n\nfunction taskPath(id: string) {\n return `/tasks/task-detail-page?taskId=${encodeURIComponent(id)}`;\n}\n\n/** Lines and products open by default; features (their epics) and epics (their stories) on demand. */\nfunction defaultOpenKeys(groups: readonly HierarchyGroup[]): Set<string> {\n const keys = new Set<string>();\n for (const group of groups) {\n keys.add(lineKey(group.line));\n for (const product of group.products) keys.add(productKey(product.id));\n }\n return keys;\n}\n\nexport function ProductsProductHierarchyPagePage() {\n const tr = useTr();\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [showArchived, setShowArchived] = useState(false);\n const [search, setSearch] = useState('');\n const deferredSearch = useDeferredValue(search);\n // null until the person expands or collapses something: the default view follows the data as it loads.\n const [openKeys, setOpenKeys] = useState<Set<string> | null>(null);\n\n const { data, previousData, loading, error } = useQuery<HierarchyData>(PRODUCT_HIERARCHY, {\n variables: {\n workspaceId: workspaceId ?? '',\n // Omitted filters list active and archived records alike.\n lineFilter: showArchived ? undefined : { isActive: true },\n productFilter: showArchived ? undefined : { isActive: true },\n includeArchivedFeatures: showArchived,\n },\n skip: !workspaceId,\n });\n const source = data ?? previousData;\n\n const groups = useMemo(\n () =>\n source\n ? buildProductHierarchy(\n source.planmagnetProductLines.items,\n source.planmagnetProducts.items\n )\n : [],\n [source]\n );\n const filtered = useMemo(\n () => filterProductHierarchy(groups, deferredSearch),\n [groups, deferredSearch]\n );\n const open = openKeys ?? defaultOpenKeys(groups);\n const isOpen = (key: string) => open.has(key);\n const toggle = (key: string) =>\n setOpenKeys((prev) => {\n const next = new Set(prev ?? defaultOpenKeys(groups));\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n });\n\n const onSearch = (value: string) => {\n setSearch(value);\n // Open the path to every match as the person types, keeping whatever they opened themselves.\n const { expand } = filterProductHierarchy(groups, value);\n if (expand.size > 0) {\n setOpenKeys((prev) => new Set([...(prev ?? defaultOpenKeys(groups)), ...expand]));\n }\n };\n\n // Only before the first load: a refetch must not blank the page (it unmounts every component on it).\n if (loading && !source) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error && !source) {\n return (\n <div className=\"p-6 text-destructive\">\n {tr(\n 'planmagnet.productHierarchy.errors.load',\n 'Failed to load the product hierarchy: {{message}}',\n { message: error.message }\n )}\n </div>\n );\n }\n\n const totals = hierarchyTotals(filtered.groups);\n const linesTruncated =\n !!source &&\n source.planmagnetProductLines.totalCount > source.planmagnetProductLines.items.length;\n const productsTruncated =\n !!source && source.planmagnetProducts.totalCount > source.planmagnetProducts.items.length;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div>\n <h1 className=\"flex items-center gap-2 text-2xl font-semibold\">\n <ListTree className=\"h-6 w-6\" />\n {tr('planmagnet.productHierarchy.title', 'Product hierarchy')}\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.productHierarchy.subtitle',\n 'Product lines › products › features › epics › stories, as linked on each record’s page. Nothing here is inferred.'\n )}\n </p>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate('/products/product-lines-page')}\n data-testid=\"product-hierarchy-manage-lines\"\n >\n <Layers className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productHierarchy.manageLines', 'Product lines')}\n </Button>\n <Button\n size=\"sm\"\n variant={showArchived ? 'default' : 'outline'}\n aria-pressed={showArchived}\n onClick={() => setShowArchived((v) => !v)}\n data-testid=\"product-hierarchy-toggle-archived\"\n >\n <Archive className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productHierarchy.showArchived', 'Show archived')}\n </Button>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <Input\n aria-label={tr('planmagnet.productHierarchy.search', 'Search the hierarchy')}\n placeholder={tr(\n 'planmagnet.productHierarchy.searchPlaceholder',\n 'Search lines, products, features, epics, stories…'\n )}\n value={search}\n onChange={(e) => onSearch(e.target.value)}\n className=\"max-w-sm\"\n data-testid=\"product-hierarchy-search\"\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setOpenKeys(allNodeKeys(groups))}\n data-testid=\"product-hierarchy-expand-all\"\n >\n <ChevronsUpDown className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productHierarchy.expandAll', 'Expand all')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setOpenKeys(new Set())}\n data-testid=\"product-hierarchy-collapse-all\"\n >\n <ChevronsDownUp className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productHierarchy.collapseAll', 'Collapse all')}\n </Button>\n </div>\n\n <p className=\"text-xs text-muted-foreground\" data-testid=\"product-hierarchy-totals\">\n {tr(\n 'planmagnet.productHierarchy.totals',\n '{{lines}} line(s) · {{products}} product(s) · {{features}} feature(s) · {{epics}} epic(s) · {{stories}} story/item(s)',\n {\n lines: totals.lines,\n products: totals.products,\n features: totals.features,\n epics: totals.epics,\n stories: totals.children,\n }\n )}\n </p>\n\n {error && (\n <ErrorLine\n message={tr(\n 'planmagnet.productHierarchy.errors.refresh',\n 'The hierarchy could not be refreshed: {{message}}',\n { message: error.message }\n )}\n />\n )}\n {(linesTruncated || productsTruncated) && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productHierarchy.truncated',\n 'Only the first 100 product lines and 100 products are shown.'\n )}\n </p>\n )}\n\n {filtered.groups.length === 0 ? (\n <Card>\n <CardContent className=\"py-12 text-center text-sm text-muted-foreground\">\n {deferredSearch.trim()\n ? tr('planmagnet.productHierarchy.emptySearch', 'Nothing in the hierarchy matches.')\n : tr(\n 'planmagnet.productHierarchy.empty',\n 'No product lines or products yet. Create a product line, then put products in it from each product’s page.'\n )}\n </CardContent>\n </Card>\n ) : (\n <Card>\n <CardContent className=\"pt-4\">\n <ul className=\"space-y-1\" data-testid=\"product-hierarchy-tree\">\n {filtered.groups.map((group) => (\n <LineNode\n key={lineKey(group.line)}\n group={group}\n isOpen={isOpen}\n toggle={toggle}\n onOpenProduct={(id) => navigate(detailPath('/products/product-detail-page', id))}\n onOpenFeature={(id) =>\n navigate(detailPath('/products/product-feature-detail-page', id))\n }\n onOpenWorkItem={(id) => navigate(taskPath(id))}\n />\n ))}\n </ul>\n </CardContent>\n </Card>\n )}\n </div>\n );\n}\n\n// ─── Tree nodes ─────────────────────────────────────────────────────────────\n\ninterface TreeProps {\n isOpen: (key: string) => boolean;\n toggle: (key: string) => void;\n onOpenProduct: (id: string) => void;\n onOpenFeature: (id: string) => void;\n onOpenWorkItem: (id: string) => void;\n}\n\n/**\n * One row: an expand toggle (or a spacer for a leaf), then the row's own content. The content wraps rather than\n * squeezing: on a narrow screen the badges and counts move under the name instead of truncating it to a letter.\n */\nfunction TreeRow({\n nodeKey,\n label,\n expandable,\n isOpen,\n toggle,\n children,\n}: {\n nodeKey: string;\n label: string;\n expandable: boolean;\n isOpen: (key: string) => boolean;\n toggle: (key: string) => void;\n children: ReactNode;\n}) {\n const tr = useTr();\n const open = isOpen(nodeKey);\n return (\n <div className=\"flex min-w-0 items-start gap-2 py-1 text-sm\">\n {expandable ? (\n <button\n type=\"button\"\n className=\"shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted\"\n aria-expanded={open}\n aria-label={\n open\n ? tr('planmagnet.productHierarchy.collapseNamed', 'Collapse {{name}}', {\n name: label,\n })\n : tr('planmagnet.productHierarchy.expandNamed', 'Expand {{name}}', { name: label })\n }\n onClick={() => toggle(nodeKey)}\n >\n {open ? <ChevronDown className=\"h-4 w-4\" /> : <ChevronRight className=\"h-4 w-4\" />}\n </button>\n ) : (\n <span className=\"w-5 shrink-0\" aria-hidden=\"true\" />\n )}\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-0.5\">{children}</div>\n </div>\n );\n}\n\nconst childListClass = 'ml-3 space-y-1 border-l pl-3';\n\nfunction LineNode({ group, ...tree }: TreeProps & { group: HierarchyGroup }) {\n const tr = useTr();\n const key = lineKey(group.line);\n const name = group.line?.name ?? tr('planmagnet.productHierarchy.noLine', 'No product line');\n return (\n <li data-testid=\"hierarchy-line\">\n <TreeRow\n nodeKey={key}\n label={name}\n expandable={group.products.length > 0}\n isOpen={tree.isOpen}\n toggle={tree.toggle}\n >\n {group.line ? (\n <Layers className=\"h-4 w-4 shrink-0 text-primary\" />\n ) : (\n <Package className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n )}\n <span\n className={\n group.line ? 'truncate font-semibold' : 'truncate font-semibold text-muted-foreground'\n }\n >\n {name}\n </span>\n {group.line?.archivedAt && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {tr('planmagnet.productHierarchy.archived', 'Archived')}\n </Badge>\n )}\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {tr('planmagnet.productHierarchy.productCount', '{{count}} product(s)', {\n count: group.products.length,\n })}\n </span>\n </TreeRow>\n {tree.isOpen(key) && group.products.length > 0 && (\n <ul className={childListClass}>\n {group.products.map((product) => (\n <ProductNode key={product.id} product={product} {...tree} />\n ))}\n </ul>\n )}\n </li>\n );\n}\n\nfunction ProductNode({ product, ...tree }: TreeProps & { product: HierarchyProduct }) {\n const tr = useTr();\n const key = productKey(product.id);\n return (\n <li data-testid=\"hierarchy-product\">\n <TreeRow\n nodeKey={key}\n label={product.name}\n expandable={product.features.length > 0}\n isOpen={tree.isOpen}\n toggle={tree.toggle}\n >\n <Package className=\"h-4 w-4 shrink-0 text-primary\" />\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left font-medium hover:underline\"\n onClick={() => tree.onOpenProduct(product.id)}\n >\n {product.name}\n </button>\n <Badge variant=\"secondary\" className=\"text-xs\">\n {enumLabel(product.lifecycleStage)}\n </Badge>\n {product.archivedAt && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {tr('planmagnet.productHierarchy.archived', 'Archived')}\n </Badge>\n )}\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {tr('planmagnet.productHierarchy.featureCount', '{{count}} feature(s)', {\n count: product.features.length,\n })}\n </span>\n </TreeRow>\n {tree.isOpen(key) && product.features.length > 0 && (\n <ul className={childListClass}>\n {product.features.map((feature) => (\n <FeatureNode key={feature.id} feature={feature} {...tree} />\n ))}\n </ul>\n )}\n </li>\n );\n}\n\nfunction FeatureNode({ feature, ...tree }: TreeProps & { feature: HierarchyFeature }) {\n const tr = useTr();\n const key = featureKey(feature.id);\n return (\n <li data-testid=\"hierarchy-feature\">\n <TreeRow\n nodeKey={key}\n label={feature.name}\n expandable={feature.epics.length > 0}\n isOpen={tree.isOpen}\n toggle={tree.toggle}\n >\n <Puzzle className=\"h-4 w-4 shrink-0 text-primary\" />\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => tree.onOpenFeature(feature.id)}\n >\n {feature.name}\n </button>\n <ProductFeatureStatusBadge status={feature.status} />\n {feature.archivedAt && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {tr('planmagnet.productHierarchy.archived', 'Archived')}\n </Badge>\n )}\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {tr('planmagnet.productHierarchy.epicCount', '{{count}} epic(s)', {\n count: feature.epics.length,\n })}\n </span>\n </TreeRow>\n {tree.isOpen(key) && feature.epics.length > 0 && (\n <ul className={childListClass}>\n {feature.epics.map((epic) => (\n <EpicNode key={epic.id} epic={epic} {...tree} />\n ))}\n </ul>\n )}\n </li>\n );\n}\n\nfunction EpicNode({ epic, ...tree }: TreeProps & { epic: HierarchyEpic }) {\n const tr = useTr();\n const key = epicKey(epic.id);\n return (\n <li data-testid=\"hierarchy-epic\">\n <TreeRow\n nodeKey={key}\n label={`${epic.key} ${epic.title}`}\n expandable={epic.children.length > 0}\n isOpen={tree.isOpen}\n toggle={tree.toggle}\n >\n <Milestone className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => tree.onOpenWorkItem(epic.id)}\n >\n <span className=\"mr-2 text-muted-foreground\">{epic.key}</span>\n {epic.title}\n </button>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {enumLabel(epic.status)} ·{' '}\n {tr('planmagnet.productHierarchy.storyCount', '{{count}} story/item(s)', {\n count: epic.children.length,\n })}\n </span>\n </TreeRow>\n {tree.isOpen(key) && epic.children.length > 0 && (\n <ul className={childListClass}>\n {epic.children.map((child) => (\n <li key={child.id} data-testid=\"hierarchy-story\">\n <TreeRow\n nodeKey={`item:${child.id}`}\n label={child.title}\n expandable={false}\n isOpen={tree.isOpen}\n toggle={tree.toggle}\n >\n <CircleDot className=\"h-3.5 w-3.5 shrink-0 text-muted-foreground\" />\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => tree.onOpenWorkItem(child.id)}\n >\n <span className=\"mr-2 text-muted-foreground\">{child.key}</span>\n {child.title}\n </button>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {enumLabel(child.type)} · {enumLabel(child.status)}\n </span>\n </TreeRow>\n </li>\n ))}\n </ul>\n )}\n </li>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductsProductLinesPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsProductLinesPagePage.tsx"],"names":[],"mappings":"AA8CA,wBAAgB,4BAA4B,4CAiP3C"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { PersonName as n } from "../people/People.js";
|
|
3
|
+
import { useTr as r } from "../../shared/hooks/useTr.js";
|
|
4
|
+
import { ErrorLine as i } from "../controls/ConfirmButton.js";
|
|
5
|
+
import { PRODUCT_LINES_PAGE as a } from "../product/hierarchyOperations.js";
|
|
6
|
+
import { ProductLineActions as o, ProductLineFormDialog as s, ProductLineProducts as c } from "../product/ProductLineControls.js";
|
|
7
|
+
import { useDeferredValue as l, useState as u } from "react";
|
|
8
|
+
import { Archive as d, Layers as f, ListTree as p, Loader2 as m } from "lucide-react";
|
|
9
|
+
import { useQuery as h } from "@apollo/client/react";
|
|
10
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
11
|
+
import { Badge as v, Button as y, Card as b, CardContent as x, CardHeader as S, CardTitle as C, Input as w } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
//#region src/planmagnet/pages/ProductsProductLinesPagePage.tsx
|
|
13
|
+
function T() {
|
|
14
|
+
let T = r(), { workspaceId: E } = e(), D = t(), [O, k] = u(!1), [A, j] = u(""), M = l(A.trim()), [N, P] = u(null), { data: F, previousData: I, loading: L, error: R } = h(a, {
|
|
15
|
+
variables: {
|
|
16
|
+
workspaceId: E ?? "",
|
|
17
|
+
filter: {
|
|
18
|
+
...O ? {} : { isActive: !0 },
|
|
19
|
+
...M ? { search: M } : {}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
skip: !E
|
|
23
|
+
}), z = (F ?? I)?.planmagnetProductLines;
|
|
24
|
+
if (L && !z) return /* @__PURE__ */ g("div", {
|
|
25
|
+
className: "flex items-center justify-center p-12",
|
|
26
|
+
children: /* @__PURE__ */ g(m, { className: "h-8 w-8 animate-spin text-muted-foreground" })
|
|
27
|
+
});
|
|
28
|
+
if (R && !z) return /* @__PURE__ */ g("div", {
|
|
29
|
+
className: "p-6 text-destructive",
|
|
30
|
+
children: T("planmagnet.productLines.errors.loadPage", "Failed to load product lines: {{message}}", { message: R.message })
|
|
31
|
+
});
|
|
32
|
+
let B = z?.items ?? [], V = z?.totalCount ?? 0;
|
|
33
|
+
return /* @__PURE__ */ _("div", {
|
|
34
|
+
className: "space-y-6 p-6",
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ _("div", {
|
|
37
|
+
className: "flex flex-wrap items-start justify-between gap-4",
|
|
38
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ _("h1", {
|
|
39
|
+
className: "flex items-center gap-2 text-2xl font-semibold",
|
|
40
|
+
children: [/* @__PURE__ */ g(f, { className: "h-6 w-6" }), T("planmagnet.productLines.title", "Product lines")]
|
|
41
|
+
}), /* @__PURE__ */ g("p", {
|
|
42
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
43
|
+
children: T("planmagnet.productLines.subtitle", "Group related products into lines. A product belongs to at most one line; the line never owns it.")
|
|
44
|
+
})] }), /* @__PURE__ */ _("div", {
|
|
45
|
+
className: "flex flex-wrap items-center gap-2",
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ _(y, {
|
|
48
|
+
size: "sm",
|
|
49
|
+
variant: "outline",
|
|
50
|
+
onClick: () => D("/products/product-hierarchy-page"),
|
|
51
|
+
"data-testid": "product-lines-view-hierarchy",
|
|
52
|
+
children: [/* @__PURE__ */ g(p, { className: "mr-1 h-4 w-4" }), T("planmagnet.productLines.actions.viewHierarchy", "View hierarchy")]
|
|
53
|
+
}),
|
|
54
|
+
/* @__PURE__ */ _(y, {
|
|
55
|
+
size: "sm",
|
|
56
|
+
variant: O ? "default" : "outline",
|
|
57
|
+
"aria-pressed": O,
|
|
58
|
+
onClick: () => k((e) => !e),
|
|
59
|
+
"data-testid": "product-lines-toggle-archived",
|
|
60
|
+
children: [/* @__PURE__ */ g(d, { className: "mr-1 h-4 w-4" }), T("planmagnet.productLines.actions.showArchived", "Show archived")]
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ g(s, {})
|
|
63
|
+
]
|
|
64
|
+
})]
|
|
65
|
+
}),
|
|
66
|
+
/* @__PURE__ */ _("div", {
|
|
67
|
+
className: "flex flex-wrap items-center gap-3",
|
|
68
|
+
children: [/* @__PURE__ */ g(w, {
|
|
69
|
+
"aria-label": T("planmagnet.productLines.search", "Search product lines"),
|
|
70
|
+
placeholder: T("planmagnet.productLines.searchPlaceholder", "Search by name or description…"),
|
|
71
|
+
value: A,
|
|
72
|
+
onChange: (e) => j(e.target.value),
|
|
73
|
+
className: "max-w-xs",
|
|
74
|
+
"data-testid": "product-lines-search"
|
|
75
|
+
}), /* @__PURE__ */ g("p", {
|
|
76
|
+
className: "text-sm text-muted-foreground",
|
|
77
|
+
"data-testid": "product-lines-count",
|
|
78
|
+
children: V === 1 ? T("planmagnet.productLines.countOne", "1 product line") : T("planmagnet.productLines.count", "{{count}} product lines", { count: V })
|
|
79
|
+
})]
|
|
80
|
+
}),
|
|
81
|
+
N && /* @__PURE__ */ _("div", {
|
|
82
|
+
role: "status",
|
|
83
|
+
className: "flex items-start justify-between gap-3 rounded-md bg-muted p-3 text-sm",
|
|
84
|
+
"data-testid": "product-lines-notice",
|
|
85
|
+
children: [/* @__PURE__ */ g("span", { children: N }), /* @__PURE__ */ g("button", {
|
|
86
|
+
type: "button",
|
|
87
|
+
className: "shrink-0 text-xs text-muted-foreground hover:underline",
|
|
88
|
+
onClick: () => P(null),
|
|
89
|
+
children: T("planmagnet.productLines.dismiss", "Dismiss")
|
|
90
|
+
})]
|
|
91
|
+
}),
|
|
92
|
+
R && /* @__PURE__ */ g(i, { message: T("planmagnet.productLines.errors.refresh", "The list could not be refreshed: {{message}}", { message: R.message }) }),
|
|
93
|
+
B.length === 0 ? /* @__PURE__ */ g(b, { children: /* @__PURE__ */ g(x, {
|
|
94
|
+
className: "py-12 text-center text-sm text-muted-foreground",
|
|
95
|
+
children: M ? T("planmagnet.productLines.emptySearch", "No product line matches your search.") : O ? T("planmagnet.productLines.emptyAll", "No product lines yet. Create one to group related products.") : T("planmagnet.productLines.empty", "No active product lines. Create one to group related products, or show archived lines.")
|
|
96
|
+
}) }) : /* @__PURE__ */ g("div", {
|
|
97
|
+
className: "grid gap-4 lg:grid-cols-2",
|
|
98
|
+
children: B.map((e) => /* @__PURE__ */ _(b, {
|
|
99
|
+
"data-testid": "product-line-card",
|
|
100
|
+
children: [/* @__PURE__ */ g(S, {
|
|
101
|
+
className: "space-y-3",
|
|
102
|
+
children: /* @__PURE__ */ _("div", {
|
|
103
|
+
className: "flex flex-wrap items-start justify-between gap-3",
|
|
104
|
+
children: [/* @__PURE__ */ _("div", {
|
|
105
|
+
className: "min-w-0",
|
|
106
|
+
children: [/* @__PURE__ */ _(C, {
|
|
107
|
+
className: "flex items-center gap-2 text-base",
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ g(f, { className: "h-4 w-4 shrink-0 text-primary" }),
|
|
110
|
+
/* @__PURE__ */ g("span", {
|
|
111
|
+
className: "truncate",
|
|
112
|
+
children: e.name
|
|
113
|
+
}),
|
|
114
|
+
e.archivedAt && /* @__PURE__ */ g(v, {
|
|
115
|
+
variant: "outline",
|
|
116
|
+
className: "text-xs",
|
|
117
|
+
children: T("planmagnet.productLines.archived", "Archived")
|
|
118
|
+
})
|
|
119
|
+
]
|
|
120
|
+
}), /* @__PURE__ */ _("p", {
|
|
121
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
122
|
+
children: [
|
|
123
|
+
T("planmagnet.productLines.owner", "Owner"),
|
|
124
|
+
":",
|
|
125
|
+
" ",
|
|
126
|
+
e.ownerId ? /* @__PURE__ */ g(n, { id: e.ownerId }) : T("planmagnet.productLines.fields.noOwner", "No owner")
|
|
127
|
+
]
|
|
128
|
+
})]
|
|
129
|
+
}), /* @__PURE__ */ g(o, {
|
|
130
|
+
line: e,
|
|
131
|
+
productCount: e.products.length,
|
|
132
|
+
onArchived: (e) => P(O ? T("planmagnet.productLines.notice.archived", "“{{name}}” is archived.", { name: e }) : T("planmagnet.productLines.notice.archivedHidden", "“{{name}}” is archived and hidden from this list — turn on “Show archived” to see it. Its products stay in it.", { name: e })),
|
|
133
|
+
onDeleted: (e) => P(T("planmagnet.productLines.notice.deleted", "“{{name}}” was deleted. Its products are kept and now show under “No product line”.", { name: e }))
|
|
134
|
+
})]
|
|
135
|
+
})
|
|
136
|
+
}), /* @__PURE__ */ _(x, {
|
|
137
|
+
className: "space-y-4",
|
|
138
|
+
children: [
|
|
139
|
+
/* @__PURE__ */ g("p", {
|
|
140
|
+
className: "text-sm text-muted-foreground",
|
|
141
|
+
children: e.description || T("planmagnet.productLines.noDescription", "No description.")
|
|
142
|
+
}),
|
|
143
|
+
e.archivedAt && /* @__PURE__ */ g("p", {
|
|
144
|
+
className: "text-xs text-muted-foreground",
|
|
145
|
+
children: T("planmagnet.productLines.archivedNote", "Archived: its products stay in it, but it takes no new ones.")
|
|
146
|
+
}),
|
|
147
|
+
/* @__PURE__ */ g(c, {
|
|
148
|
+
line: e,
|
|
149
|
+
products: e.products
|
|
150
|
+
})
|
|
151
|
+
]
|
|
152
|
+
})]
|
|
153
|
+
}, e.id))
|
|
154
|
+
}),
|
|
155
|
+
z?.pageInfo.hasNextPage && /* @__PURE__ */ g("p", {
|
|
156
|
+
className: "text-xs text-muted-foreground",
|
|
157
|
+
children: T("planmagnet.productLines.truncated", "Showing the first {{shown}} of {{total}} product lines — search to narrow the list.", {
|
|
158
|
+
shown: B.length,
|
|
159
|
+
total: V
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { T as ProductsProductLinesPagePage };
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=ProductsProductLinesPagePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductsProductLinesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsProductLinesPagePage.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { Archive, Layers, ListTree, Loader2 } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport {\n ProductLineActions,\n ProductLineFormDialog,\n ProductLineProducts,\n type ProductLineProductRef,\n type ProductLineValues,\n} from '../product/ProductLineControls';\nimport { PRODUCT_LINES_PAGE } from '../product/hierarchyOperations';\n\n/**\n * Product lines (PRM-01): the top of the product hierarchy — Product line > Product > Feature > Epic > Story. Lists\n * lines with their products (search and \"show archived\" run on the server), and creates, edits, archives and\n * deletes them. A line never owns its products: archiving keeps them in it, deleting only takes them out of it.\n * Nothing is inferred — every product starts in no line until someone puts it in one.\n */\n\ninterface ProductLineNode extends ProductLineValues {\n createdAt: string;\n updatedAt: string;\n products: ProductLineProductRef[];\n}\n\ninterface ProductLinesData {\n planmagnetProductLines: {\n items: ProductLineNode[];\n pageInfo: { hasNextPage: boolean };\n totalCount: number;\n };\n}\n\nexport function ProductsProductLinesPagePage() {\n const tr = useTr();\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [showArchived, setShowArchived] = useState(false);\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n // What the last archive/delete did — its card may already be gone from the list.\n const [notice, setNotice] = useState<string | null>(null);\n\n const { data, previousData, loading, error } = useQuery<ProductLinesData>(PRODUCT_LINES_PAGE, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: {\n // isActive omitted lists active and archived lines alike.\n ...(showArchived ? {} : { isActive: true }),\n ...(deferred ? { search: deferred } : {}),\n },\n },\n skip: !workspaceId,\n });\n // A new search or the archived toggle keeps the current rows (and the search box) on screen meanwhile.\n const list = (data ?? previousData)?.planmagnetProductLines;\n\n // Only before the first load: a refetch must not blank the page (it unmounts every component on it).\n if (loading && !list) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error && !list) {\n return (\n <div className=\"p-6 text-destructive\">\n {tr(\n 'planmagnet.productLines.errors.loadPage',\n 'Failed to load product lines: {{message}}',\n {\n message: error.message,\n }\n )}\n </div>\n );\n }\n\n const lines = list?.items ?? [];\n const totalCount = list?.totalCount ?? 0;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div>\n <h1 className=\"flex items-center gap-2 text-2xl font-semibold\">\n <Layers className=\"h-6 w-6\" />\n {tr('planmagnet.productLines.title', 'Product lines')}\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.productLines.subtitle',\n 'Group related products into lines. A product belongs to at most one line; the line never owns it.'\n )}\n </p>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate('/products/product-hierarchy-page')}\n data-testid=\"product-lines-view-hierarchy\"\n >\n <ListTree className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productLines.actions.viewHierarchy', 'View hierarchy')}\n </Button>\n <Button\n size=\"sm\"\n variant={showArchived ? 'default' : 'outline'}\n aria-pressed={showArchived}\n onClick={() => setShowArchived((v) => !v)}\n data-testid=\"product-lines-toggle-archived\"\n >\n <Archive className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productLines.actions.showArchived', 'Show archived')}\n </Button>\n <ProductLineFormDialog />\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <Input\n aria-label={tr('planmagnet.productLines.search', 'Search product lines')}\n placeholder={tr(\n 'planmagnet.productLines.searchPlaceholder',\n 'Search by name or description…'\n )}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"max-w-xs\"\n data-testid=\"product-lines-search\"\n />\n <p className=\"text-sm text-muted-foreground\" data-testid=\"product-lines-count\">\n {totalCount === 1\n ? tr('planmagnet.productLines.countOne', '1 product line')\n : tr('planmagnet.productLines.count', '{{count}} product lines', { count: totalCount })}\n </p>\n </div>\n\n {notice && (\n <div\n role=\"status\"\n className=\"flex items-start justify-between gap-3 rounded-md bg-muted p-3 text-sm\"\n data-testid=\"product-lines-notice\"\n >\n <span>{notice}</span>\n <button\n type=\"button\"\n className=\"shrink-0 text-xs text-muted-foreground hover:underline\"\n onClick={() => setNotice(null)}\n >\n {tr('planmagnet.productLines.dismiss', 'Dismiss')}\n </button>\n </div>\n )}\n\n {error && (\n <ErrorLine\n message={tr(\n 'planmagnet.productLines.errors.refresh',\n 'The list could not be refreshed: {{message}}',\n {\n message: error.message,\n }\n )}\n />\n )}\n\n {lines.length === 0 ? (\n <Card>\n <CardContent className=\"py-12 text-center text-sm text-muted-foreground\">\n {deferred\n ? tr('planmagnet.productLines.emptySearch', 'No product line matches your search.')\n : showArchived\n ? tr(\n 'planmagnet.productLines.emptyAll',\n 'No product lines yet. Create one to group related products.'\n )\n : tr(\n 'planmagnet.productLines.empty',\n 'No active product lines. Create one to group related products, or show archived lines.'\n )}\n </CardContent>\n </Card>\n ) : (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n {lines.map((line) => (\n <Card key={line.id} data-testid=\"product-line-card\">\n <CardHeader className=\"space-y-3\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Layers className=\"h-4 w-4 shrink-0 text-primary\" />\n <span className=\"truncate\">{line.name}</span>\n {line.archivedAt && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {tr('planmagnet.productLines.archived', 'Archived')}\n </Badge>\n )}\n </CardTitle>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {tr('planmagnet.productLines.owner', 'Owner')}:{' '}\n {line.ownerId ? (\n <PersonName id={line.ownerId} />\n ) : (\n tr('planmagnet.productLines.fields.noOwner', 'No owner')\n )}\n </p>\n </div>\n <ProductLineActions\n line={line}\n productCount={line.products.length}\n onArchived={(name) =>\n setNotice(\n showArchived\n ? tr(\n 'planmagnet.productLines.notice.archived',\n '“{{name}}” is archived.',\n {\n name,\n }\n )\n : tr(\n 'planmagnet.productLines.notice.archivedHidden',\n '“{{name}}” is archived and hidden from this list — turn on “Show archived” to see it. Its products stay in it.',\n { name }\n )\n )\n }\n onDeleted={(name) =>\n setNotice(\n tr(\n 'planmagnet.productLines.notice.deleted',\n '“{{name}}” was deleted. Its products are kept and now show under “No product line”.',\n { name }\n )\n )\n }\n />\n </div>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <p className=\"text-sm text-muted-foreground\">\n {line.description ||\n tr('planmagnet.productLines.noDescription', 'No description.')}\n </p>\n {line.archivedAt && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productLines.archivedNote',\n 'Archived: its products stay in it, but it takes no new ones.'\n )}\n </p>\n )}\n <ProductLineProducts line={line} products={line.products} />\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {list?.pageInfo.hasNextPage && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productLines.truncated',\n 'Showing the first {{shown}} of {{total}} product lines — search to narrow the list.',\n { shown: lines.length, total: totalCount }\n )}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsProductsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsProductsPagePage.tsx"],"names":[],"mappings":"AA+EA,wBAAgB,wBAAwB,
|
|
1
|
+
{"version":3,"file":"ProductsProductsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsProductsPagePage.tsx"],"names":[],"mappings":"AA+EA,wBAAgB,wBAAwB,4CAmMvC"}
|
|
@@ -195,7 +195,7 @@ function E() {
|
|
|
195
195
|
children: e.featureRequests.length
|
|
196
196
|
}), /* @__PURE__ */ _("p", {
|
|
197
197
|
className: "text-xs text-muted-foreground",
|
|
198
|
-
children: [/* @__PURE__ */ g(p, { className: "inline h-3 w-3 mr-0.5" }), D("planmagnet.products.shared.
|
|
198
|
+
children: [/* @__PURE__ */ g(p, { className: "inline h-3 w-3 mr-0.5" }), D("planmagnet.products.shared.featureRequests", "Feature requests")]
|
|
199
199
|
})]
|
|
200
200
|
})
|
|
201
201
|
]
|