@burdenoff/microfe-planmagnet 2026.928.6 → 2026.928.8
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 +5 -0
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +15 -0
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/planmagnet/automation/AutomationRuleFields.d.ts.map +1 -1
- package/dist/planmagnet/automation/AutomationRuleFields.js +13 -12
- package/dist/planmagnet/automation/AutomationRuleFields.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +7 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +45 -8
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +74 -59
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +153 -63
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +138 -112
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js +77 -70
- package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,13 +3,13 @@ import { detailPath as n, stopRowActivation as r } from "../utils/rowNavigation.
|
|
|
3
3
|
import { ApplyTemplateDialog as i, NewTemplateFromProjectDialog as a } from "../create/CreateDialogs.js";
|
|
4
4
|
import { TemplateDeleteButton as o } from "../controls/TemplateControls.js";
|
|
5
5
|
import { useDeferredValue as s, useState as c } from "react";
|
|
6
|
-
import { LayoutTemplate as l, Pencil as u,
|
|
7
|
-
import { gql as
|
|
8
|
-
import { useQuery as
|
|
9
|
-
import { jsx as
|
|
10
|
-
import { Badge as
|
|
6
|
+
import { LayoutTemplate as l, Pencil as u, Plus as d, Search as f } from "lucide-react";
|
|
7
|
+
import { gql as p } from "@apollo/client";
|
|
8
|
+
import { useQuery as m } from "@apollo/client/react";
|
|
9
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
10
|
+
import { Badge as _, Button as v, EmptyState as y, Input as b } from "@burdenoff/fe-libs/ui";
|
|
11
11
|
//#region src/planmagnet/pages/TemplatesTemplatesPagePage.tsx
|
|
12
|
-
var
|
|
12
|
+
var x = p`
|
|
13
13
|
query TemplatesList(
|
|
14
14
|
$workspaceId: ID!
|
|
15
15
|
$filter: PlanmagnetProjectTemplateFilter
|
|
@@ -37,27 +37,27 @@ var b = f`
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
`,
|
|
41
|
-
function
|
|
42
|
-
let { workspaceId:
|
|
43
|
-
...
|
|
44
|
-
...
|
|
45
|
-
}, { data:
|
|
40
|
+
`, S = 50;
|
|
41
|
+
function C() {
|
|
42
|
+
let { workspaceId: p } = e(), C = t(), [w, T] = c(""), [E, D] = c(!1), [O, k] = c(!1), A = s(w.trim()), j = {
|
|
43
|
+
...A ? { search: A } : {},
|
|
44
|
+
...E ? { isPublished: !0 } : {}
|
|
45
|
+
}, { data: M, loading: N, error: P, fetchMore: F } = m(x, {
|
|
46
46
|
variables: {
|
|
47
|
-
workspaceId:
|
|
48
|
-
filter:
|
|
49
|
-
pagination: { first:
|
|
47
|
+
workspaceId: p ?? "",
|
|
48
|
+
filter: j,
|
|
49
|
+
pagination: { first: S }
|
|
50
50
|
},
|
|
51
|
-
skip: !
|
|
51
|
+
skip: !p,
|
|
52
52
|
fetchPolicy: "cache-and-network"
|
|
53
|
-
}),
|
|
54
|
-
let e =
|
|
53
|
+
}), I = M?.planmagnetProjectTemplates, L = I?.items ?? [], R = A !== "" || E, z = !!I && L.length === 0 && !R, B = async () => {
|
|
54
|
+
let e = I?.pageInfo.endCursor;
|
|
55
55
|
if (e) {
|
|
56
|
-
|
|
56
|
+
k(!0);
|
|
57
57
|
try {
|
|
58
|
-
await
|
|
58
|
+
await F({
|
|
59
59
|
variables: { pagination: {
|
|
60
|
-
first:
|
|
60
|
+
first: S,
|
|
61
61
|
after: e
|
|
62
62
|
} },
|
|
63
63
|
updateQuery: (e, { fetchMoreResult: t }) => ({ planmagnetProjectTemplates: {
|
|
@@ -66,99 +66,106 @@ function S() {
|
|
|
66
66
|
} })
|
|
67
67
|
});
|
|
68
68
|
} finally {
|
|
69
|
-
|
|
69
|
+
k(!1);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
-
return /* @__PURE__ */
|
|
73
|
+
return /* @__PURE__ */ g("div", {
|
|
74
74
|
className: "space-y-6 p-6",
|
|
75
75
|
"data-testid": "templates-page",
|
|
76
76
|
children: [
|
|
77
|
-
/* @__PURE__ */
|
|
77
|
+
/* @__PURE__ */ g("div", {
|
|
78
78
|
className: "flex flex-wrap items-center justify-between gap-3",
|
|
79
|
-
children: [/* @__PURE__ */
|
|
79
|
+
children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("h1", {
|
|
80
80
|
className: "text-2xl font-semibold",
|
|
81
81
|
children: "Project templates"
|
|
82
|
-
}), /* @__PURE__ */
|
|
82
|
+
}), /* @__PURE__ */ h("p", {
|
|
83
83
|
className: "text-sm text-muted-foreground",
|
|
84
|
-
children:
|
|
85
|
-
})] }), /* @__PURE__ */
|
|
84
|
+
children: I ? `${I.totalCount} template${I.totalCount === 1 ? "" : "s"}` : "Reusable project configurations — capture one from a project, or start a project from one"
|
|
85
|
+
})] }), !z && /* @__PURE__ */ h(a, { onCreated: (e) => C(n("/templates/edit-template-page", e)) })]
|
|
86
86
|
}),
|
|
87
|
-
/* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ g("div", {
|
|
88
88
|
className: "flex flex-wrap items-center gap-3",
|
|
89
|
-
children: [/* @__PURE__ */
|
|
89
|
+
children: [/* @__PURE__ */ g("div", {
|
|
90
90
|
className: "relative w-full max-w-sm",
|
|
91
|
-
children: [/* @__PURE__ */
|
|
91
|
+
children: [/* @__PURE__ */ h(f, { className: "absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ h(b, {
|
|
92
92
|
"aria-label": "Search templates",
|
|
93
93
|
placeholder: "Search by name or description",
|
|
94
94
|
className: "pl-8",
|
|
95
|
-
value:
|
|
96
|
-
onChange: (e) =>
|
|
95
|
+
value: w,
|
|
96
|
+
onChange: (e) => T(e.target.value)
|
|
97
97
|
})]
|
|
98
|
-
}), /* @__PURE__ */
|
|
98
|
+
}), /* @__PURE__ */ h("button", {
|
|
99
99
|
type: "button",
|
|
100
|
-
"aria-pressed":
|
|
101
|
-
onClick: () =>
|
|
102
|
-
className: `px-3 py-1.5 text-sm rounded-md ${
|
|
100
|
+
"aria-pressed": E,
|
|
101
|
+
onClick: () => D((e) => !e),
|
|
102
|
+
className: `px-3 py-1.5 text-sm rounded-md ${E ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
|
|
103
103
|
children: "Published only"
|
|
104
104
|
})]
|
|
105
105
|
}),
|
|
106
|
-
|
|
106
|
+
P && /* @__PURE__ */ g("p", {
|
|
107
107
|
role: "alert",
|
|
108
108
|
className: "text-sm text-destructive",
|
|
109
|
-
children: ["Templates could not be loaded: ",
|
|
109
|
+
children: ["Templates could not be loaded: ", P.message]
|
|
110
110
|
}),
|
|
111
|
-
|
|
111
|
+
N && !M && /* @__PURE__ */ h("p", {
|
|
112
112
|
className: "text-sm text-muted-foreground",
|
|
113
113
|
children: "Loading templates…"
|
|
114
114
|
}),
|
|
115
|
-
|
|
116
|
-
icon: /* @__PURE__ */
|
|
117
|
-
title:
|
|
115
|
+
I && L.length === 0 && /* @__PURE__ */ h(y, {
|
|
116
|
+
icon: /* @__PURE__ */ h(l, { className: "h-8 w-8 text-muted-foreground" }),
|
|
117
|
+
title: R ? "No templates match" : "No templates yet",
|
|
118
118
|
description: "Save an existing project's setup as a template, then start new projects from it in one step.",
|
|
119
|
-
action:
|
|
119
|
+
action: z ? /* @__PURE__ */ h(a, {
|
|
120
|
+
onCreated: (e) => C(n("/templates/edit-template-page", e)),
|
|
121
|
+
trigger: /* @__PURE__ */ g(v, {
|
|
122
|
+
size: "sm",
|
|
123
|
+
"data-testid": "new-template-empty-trigger",
|
|
124
|
+
children: [/* @__PURE__ */ h(d, { className: "mr-1 h-4 w-4" }), "New template"]
|
|
125
|
+
})
|
|
126
|
+
}) : void 0
|
|
120
127
|
}),
|
|
121
|
-
/* @__PURE__ */
|
|
128
|
+
/* @__PURE__ */ h("ul", {
|
|
122
129
|
className: "grid grid-cols-1 gap-3 md:grid-cols-2",
|
|
123
|
-
children:
|
|
130
|
+
children: L.map((e) => /* @__PURE__ */ g("li", {
|
|
124
131
|
"data-testid": "template-row",
|
|
125
132
|
className: "flex flex-col gap-2 rounded-md border p-3",
|
|
126
133
|
children: [
|
|
127
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ g("div", {
|
|
128
135
|
className: "flex items-start justify-between gap-2",
|
|
129
|
-
children: [/* @__PURE__ */
|
|
136
|
+
children: [/* @__PURE__ */ g("div", {
|
|
130
137
|
className: "min-w-0",
|
|
131
|
-
children: [/* @__PURE__ */
|
|
138
|
+
children: [/* @__PURE__ */ h("p", {
|
|
132
139
|
className: "truncate text-sm font-medium",
|
|
133
140
|
children: e.name
|
|
134
|
-
}), /* @__PURE__ */
|
|
141
|
+
}), /* @__PURE__ */ h("p", {
|
|
135
142
|
className: "truncate text-xs text-muted-foreground",
|
|
136
143
|
children: e.description ?? "No description"
|
|
137
144
|
})]
|
|
138
|
-
}), /* @__PURE__ */
|
|
145
|
+
}), /* @__PURE__ */ g("div", {
|
|
139
146
|
className: "flex shrink-0 gap-1",
|
|
140
|
-
children: [e.isBuiltIn && /* @__PURE__ */
|
|
147
|
+
children: [e.isBuiltIn && /* @__PURE__ */ h(_, {
|
|
141
148
|
variant: "secondary",
|
|
142
149
|
children: "Built-in"
|
|
143
|
-
}), /* @__PURE__ */
|
|
150
|
+
}), /* @__PURE__ */ h(_, {
|
|
144
151
|
variant: e.isPublished ? "success" : "outline",
|
|
145
152
|
children: e.isPublished ? "Published" : "Draft"
|
|
146
153
|
})]
|
|
147
154
|
})]
|
|
148
155
|
}),
|
|
149
|
-
/* @__PURE__ */
|
|
156
|
+
/* @__PURE__ */ g("div", {
|
|
150
157
|
className: "flex flex-wrap items-center gap-1",
|
|
151
|
-
children: [e.category && /* @__PURE__ */
|
|
158
|
+
children: [e.category && /* @__PURE__ */ h(_, {
|
|
152
159
|
variant: "outline",
|
|
153
160
|
className: "text-xs",
|
|
154
161
|
children: e.category
|
|
155
|
-
}), e.tags.map((e) => /* @__PURE__ */
|
|
162
|
+
}), e.tags.map((e) => /* @__PURE__ */ h(_, {
|
|
156
163
|
variant: "outline",
|
|
157
164
|
className: "text-xs",
|
|
158
165
|
children: e
|
|
159
166
|
}, e))]
|
|
160
167
|
}),
|
|
161
|
-
/* @__PURE__ */
|
|
168
|
+
/* @__PURE__ */ g("p", {
|
|
162
169
|
className: "text-xs text-muted-foreground",
|
|
163
170
|
children: [
|
|
164
171
|
"Used ",
|
|
@@ -167,42 +174,42 @@ function S() {
|
|
|
167
174
|
e.usageCount === 1 ? "" : "s"
|
|
168
175
|
]
|
|
169
176
|
}),
|
|
170
|
-
/* @__PURE__ */
|
|
177
|
+
/* @__PURE__ */ g("div", {
|
|
171
178
|
className: "flex items-center justify-between gap-2 border-t pt-2",
|
|
172
179
|
onClick: r,
|
|
173
|
-
children: [/* @__PURE__ */
|
|
180
|
+
children: [/* @__PURE__ */ g("div", {
|
|
174
181
|
className: "flex gap-2",
|
|
175
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ g(v, {
|
|
176
183
|
size: "sm",
|
|
177
184
|
variant: "outline",
|
|
178
|
-
onClick: () =>
|
|
185
|
+
onClick: () => C(n("/templates/edit-template-page", e.id)),
|
|
179
186
|
"data-testid": "template-edit",
|
|
180
|
-
children: [/* @__PURE__ */
|
|
181
|
-
}), /* @__PURE__ */
|
|
187
|
+
children: [/* @__PURE__ */ h(u, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
188
|
+
}), /* @__PURE__ */ h(o, {
|
|
182
189
|
templateId: e.id,
|
|
183
190
|
name: e.name,
|
|
184
191
|
isBuiltIn: e.isBuiltIn
|
|
185
192
|
})]
|
|
186
|
-
}), /* @__PURE__ */
|
|
193
|
+
}), /* @__PURE__ */ h(i, {
|
|
187
194
|
templateId: e.id,
|
|
188
195
|
templateName: e.name,
|
|
189
|
-
onApplied: (e) =>
|
|
196
|
+
onApplied: (e) => C(n("/projects/project-detail-page", e))
|
|
190
197
|
})]
|
|
191
198
|
})
|
|
192
199
|
]
|
|
193
200
|
}, e.id))
|
|
194
201
|
}),
|
|
195
|
-
|
|
202
|
+
I?.pageInfo.hasNextPage && /* @__PURE__ */ h(v, {
|
|
196
203
|
size: "sm",
|
|
197
204
|
variant: "outline",
|
|
198
|
-
onClick: () => void
|
|
199
|
-
disabled:
|
|
200
|
-
children:
|
|
205
|
+
onClick: () => void B(),
|
|
206
|
+
disabled: O,
|
|
207
|
+
children: O ? "Loading…" : `Show more (${L.length} of ${I.totalCount})`
|
|
201
208
|
})
|
|
202
209
|
]
|
|
203
210
|
});
|
|
204
211
|
}
|
|
205
212
|
//#endregion
|
|
206
|
-
export {
|
|
213
|
+
export { C as TemplatesTemplatesPagePage };
|
|
207
214
|
|
|
208
215
|
//# sourceMappingURL=TemplatesTemplatesPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemplatesTemplatesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Button, EmptyState, Input } from '@burdenoff/fe-libs/ui';\nimport { LayoutTemplate, Pencil, Search } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ApplyTemplateDialog, NewTemplateFromProjectDialog } from '../create/CreateDialogs';\nimport { TemplateDeleteButton } from '../controls/TemplateControls';\nimport { detailPath, stopRowActivation } from '../utils/rowNavigation';\n\n/**\n * Project templates browse surface (BOFF-8047, capability register \"Project templates\": model/API/authz were\n * already real and prod-verified — `planmagnetProjectTemplates`/Create/Update/Delete/Apply/CreateFromProject —\n * gate 3 (UI) was the only gap; \"No UI anywhere\" until now.\n */\n\nconst TEMPLATES = gql`\n query TemplatesList(\n $workspaceId: ID!\n $filter: PlanmagnetProjectTemplateFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetProjectTemplates(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n ) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n name\n description\n category\n isBuiltIn\n isPublished\n tags\n usageCount\n }\n }\n }\n`;\n\ninterface TemplateRow {\n id: string;\n name: string;\n description: string | null;\n category: string | null;\n isBuiltIn: boolean;\n isPublished: boolean;\n tags: string[];\n usageCount: number;\n}\n\nconst PAGE = 50;\n\nexport function TemplatesTemplatesPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [search, setSearch] = useState('');\n const [publishedOnly, setPublishedOnly] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n const term = useDeferredValue(search.trim());\n\n const filter = {\n ...(term ? { search: term } : {}),\n ...(publishedOnly ? { isPublished: true } : {}),\n };\n\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjectTemplates: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: TemplateRow[];\n };\n }>(TEMPLATES, {\n variables: { workspaceId: workspaceId ?? '', filter, pagination: { first: PAGE } },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n const list = data?.planmagnetProjectTemplates;\n const templates = list?.items ?? [];\n\n const loadMore = async () => {\n const after = list?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjectTemplates: {\n ...fetchMoreResult.planmagnetProjectTemplates,\n items: [\n ...prev.planmagnetProjectTemplates.items,\n ...fetchMoreResult.planmagnetProjectTemplates.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"templates-page\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Project templates</h1>\n <p className=\"text-sm text-muted-foreground\">\n {list\n ? `${list.totalCount} template${list.totalCount === 1 ? '' : 's'}`\n : 'Reusable project configurations — capture one from a project, or start a project from one'}\n </p>\n </div>\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground\" />\n <Input\n aria-label=\"Search templates\"\n placeholder=\"Search by name or description\"\n className=\"pl-8\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n </div>\n <button\n type=\"button\"\n aria-pressed={publishedOnly}\n onClick={() => setPublishedOnly((v) => !v)}\n className={`px-3 py-1.5 text-sm rounded-md ${publishedOnly ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'}`}\n >\n Published only\n </button>\n </div>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Templates could not be loaded: {error.message}\n </p>\n )}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading templates…</p>}\n {list && templates.length === 0 && (\n <EmptyState\n icon={<LayoutTemplate className=\"h-8 w-8 text-muted-foreground\" />}\n title={term || publishedOnly ? 'No templates match' : 'No templates yet'}\n description=\"Save an existing project's setup as a template, then start new projects from it in one step.\"\n action={\n !(term || publishedOnly) ? (\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n />\n ) : undefined\n }\n />\n )}\n\n <ul className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n {templates.map((template) => (\n <li\n key={template.id}\n data-testid=\"template-row\"\n className=\"flex flex-col gap-2 rounded-md border p-3\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium\">{template.name}</p>\n <p className=\"truncate text-xs text-muted-foreground\">\n {template.description ?? 'No description'}\n </p>\n </div>\n <div className=\"flex shrink-0 gap-1\">\n {template.isBuiltIn && <Badge variant=\"secondary\">Built-in</Badge>}\n <Badge variant={template.isPublished ? 'success' : 'outline'}>\n {template.isPublished ? 'Published' : 'Draft'}\n </Badge>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-1\">\n {template.category && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {template.category}\n </Badge>\n )}\n {template.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Used {template.usageCount} time{template.usageCount === 1 ? '' : 's'}\n </p>\n <div\n className=\"flex items-center justify-between gap-2 border-t pt-2\"\n onClick={stopRowActivation}\n >\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/templates/edit-template-page', template.id))}\n data-testid=\"template-edit\"\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <TemplateDeleteButton\n templateId={template.id}\n name={template.name}\n isBuiltIn={template.isBuiltIn}\n />\n </div>\n <ApplyTemplateDialog\n templateId={template.id}\n templateName={template.name}\n onApplied={(projectId) =>\n navigate(detailPath('/projects/project-detail-page', projectId))\n }\n />\n </div>\n </li>\n ))}\n </ul>\n {list?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${templates.length} of ${list.totalCount})`}\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TemplatesTemplatesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Button, EmptyState, Input } from '@burdenoff/fe-libs/ui';\nimport { LayoutTemplate, Pencil, Plus, Search } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ApplyTemplateDialog, NewTemplateFromProjectDialog } from '../create/CreateDialogs';\nimport { TemplateDeleteButton } from '../controls/TemplateControls';\nimport { detailPath, stopRowActivation } from '../utils/rowNavigation';\n\n/**\n * Project templates browse surface (BOFF-8047, capability register \"Project templates\": model/API/authz were\n * already real and prod-verified — `planmagnetProjectTemplates`/Create/Update/Delete/Apply/CreateFromProject —\n * gate 3 (UI) was the only gap; \"No UI anywhere\" until now.\n */\n\nconst TEMPLATES = gql`\n query TemplatesList(\n $workspaceId: ID!\n $filter: PlanmagnetProjectTemplateFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetProjectTemplates(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n ) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n name\n description\n category\n isBuiltIn\n isPublished\n tags\n usageCount\n }\n }\n }\n`;\n\ninterface TemplateRow {\n id: string;\n name: string;\n description: string | null;\n category: string | null;\n isBuiltIn: boolean;\n isPublished: boolean;\n tags: string[];\n usageCount: number;\n}\n\nconst PAGE = 50;\n\nexport function TemplatesTemplatesPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [search, setSearch] = useState('');\n const [publishedOnly, setPublishedOnly] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n const term = useDeferredValue(search.trim());\n\n const filter = {\n ...(term ? { search: term } : {}),\n ...(publishedOnly ? { isPublished: true } : {}),\n };\n\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjectTemplates: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: TemplateRow[];\n };\n }>(TEMPLATES, {\n variables: { workspaceId: workspaceId ?? '', filter, pagination: { first: PAGE } },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n\n const list = data?.planmagnetProjectTemplates;\n const templates = list?.items ?? [];\n const isFiltered = term !== '' || publishedOnly;\n // The EmptyState below only offers its own \"New template\" action once the list is confirmed empty AND\n // unfiltered (same gate as its `title`/`description`). Whenever that action is showing, drop the header's\n // button instead of duplicating it (BOFF-8047 prod verification: both rendered at once, sharing\n // data-testid=\"new-template-trigger\"). Keep the header's button for every OTHER state — loading, a\n // populated list, or a filtered-to-empty list, where the EmptyState offers no action of its own.\n const showsEmptyStateAction = !!list && templates.length === 0 && !isFiltered;\n\n const loadMore = async () => {\n const after = list?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjectTemplates: {\n ...fetchMoreResult.planmagnetProjectTemplates,\n items: [\n ...prev.planmagnetProjectTemplates.items,\n ...fetchMoreResult.planmagnetProjectTemplates.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"templates-page\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Project templates</h1>\n <p className=\"text-sm text-muted-foreground\">\n {list\n ? `${list.totalCount} template${list.totalCount === 1 ? '' : 's'}`\n : 'Reusable project configurations — capture one from a project, or start a project from one'}\n </p>\n </div>\n {!showsEmptyStateAction && (\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n />\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground\" />\n <Input\n aria-label=\"Search templates\"\n placeholder=\"Search by name or description\"\n className=\"pl-8\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n </div>\n <button\n type=\"button\"\n aria-pressed={publishedOnly}\n onClick={() => setPublishedOnly((v) => !v)}\n className={`px-3 py-1.5 text-sm rounded-md ${publishedOnly ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted'}`}\n >\n Published only\n </button>\n </div>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Templates could not be loaded: {error.message}\n </p>\n )}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading templates…</p>}\n {list && templates.length === 0 && (\n <EmptyState\n icon={<LayoutTemplate className=\"h-8 w-8 text-muted-foreground\" />}\n title={isFiltered ? 'No templates match' : 'No templates yet'}\n description=\"Save an existing project's setup as a template, then start new projects from it in one step.\"\n action={\n showsEmptyStateAction ? (\n <NewTemplateFromProjectDialog\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n trigger={\n <Button size=\"sm\" data-testid=\"new-template-empty-trigger\">\n <Plus className=\"mr-1 h-4 w-4\" />\n New template\n </Button>\n }\n />\n ) : undefined\n }\n />\n )}\n\n <ul className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n {templates.map((template) => (\n <li\n key={template.id}\n data-testid=\"template-row\"\n className=\"flex flex-col gap-2 rounded-md border p-3\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium\">{template.name}</p>\n <p className=\"truncate text-xs text-muted-foreground\">\n {template.description ?? 'No description'}\n </p>\n </div>\n <div className=\"flex shrink-0 gap-1\">\n {template.isBuiltIn && <Badge variant=\"secondary\">Built-in</Badge>}\n <Badge variant={template.isPublished ? 'success' : 'outline'}>\n {template.isPublished ? 'Published' : 'Draft'}\n </Badge>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-1\">\n {template.category && (\n <Badge variant=\"outline\" className=\"text-xs\">\n {template.category}\n </Badge>\n )}\n {template.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Used {template.usageCount} time{template.usageCount === 1 ? '' : 's'}\n </p>\n <div\n className=\"flex items-center justify-between gap-2 border-t pt-2\"\n onClick={stopRowActivation}\n >\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/templates/edit-template-page', template.id))}\n data-testid=\"template-edit\"\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <TemplateDeleteButton\n templateId={template.id}\n name={template.name}\n isBuiltIn={template.isBuiltIn}\n />\n </div>\n <ApplyTemplateDialog\n templateId={template.id}\n templateName={template.name}\n onApplied={(projectId) =>\n navigate(detailPath('/projects/project-detail-page', projectId))\n }\n />\n </div>\n </li>\n ))}\n </ul>\n {list?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${templates.length} of ${list.totalCount})`}\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|