@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.
Files changed (24) hide show
  1. package/dist/generated/wspace-operations.d.ts +5 -0
  2. package/dist/generated/wspace-operations.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +15 -0
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/planmagnet/automation/AutomationRuleFields.d.ts.map +1 -1
  6. package/dist/planmagnet/automation/AutomationRuleFields.js +13 -12
  7. package/dist/planmagnet/automation/AutomationRuleFields.js.map +1 -1
  8. package/dist/planmagnet/controls/Pickers.d.ts +7 -0
  9. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/Pickers.js +45 -8
  11. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  12. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
  13. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +74 -59
  14. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
  15. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.d.ts.map +1 -1
  16. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +153 -63
  17. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js.map +1 -1
  18. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.d.ts.map +1 -1
  19. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +138 -112
  20. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js.map +1 -1
  21. package/dist/planmagnet/pages/TemplatesTemplatesPagePage.d.ts.map +1 -1
  22. package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js +77 -70
  23. package/dist/planmagnet/pages/TemplatesTemplatesPagePage.js.map +1 -1
  24. 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, Search as d } from "lucide-react";
7
- import { gql as f } from "@apollo/client";
8
- import { useQuery as p } from "@apollo/client/react";
9
- import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
- import { Badge as g, Button as _, EmptyState as v, Input as y } from "@burdenoff/fe-libs/ui";
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 b = f`
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
- `, x = 50;
41
- function S() {
42
- let { workspaceId: f } = e(), S = t(), [C, w] = c(""), [T, E] = c(!1), [D, O] = c(!1), k = s(C.trim()), A = {
43
- ...k ? { search: k } : {},
44
- ...T ? { isPublished: !0 } : {}
45
- }, { data: j, loading: M, error: N, fetchMore: P } = p(b, {
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: f ?? "",
48
- filter: A,
49
- pagination: { first: x }
47
+ workspaceId: p ?? "",
48
+ filter: j,
49
+ pagination: { first: S }
50
50
  },
51
- skip: !f,
51
+ skip: !p,
52
52
  fetchPolicy: "cache-and-network"
53
- }), F = j?.planmagnetProjectTemplates, I = F?.items ?? [], L = async () => {
54
- let e = F?.pageInfo.endCursor;
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
- O(!0);
56
+ k(!0);
57
57
  try {
58
- await P({
58
+ await F({
59
59
  variables: { pagination: {
60
- first: x,
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
- O(!1);
69
+ k(!1);
70
70
  }
71
71
  }
72
72
  };
73
- return /* @__PURE__ */ h("div", {
73
+ return /* @__PURE__ */ g("div", {
74
74
  className: "space-y-6 p-6",
75
75
  "data-testid": "templates-page",
76
76
  children: [
77
- /* @__PURE__ */ h("div", {
77
+ /* @__PURE__ */ g("div", {
78
78
  className: "flex flex-wrap items-center justify-between gap-3",
79
- children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("h1", {
79
+ children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("h1", {
80
80
  className: "text-2xl font-semibold",
81
81
  children: "Project templates"
82
- }), /* @__PURE__ */ m("p", {
82
+ }), /* @__PURE__ */ h("p", {
83
83
  className: "text-sm text-muted-foreground",
84
- children: F ? `${F.totalCount} template${F.totalCount === 1 ? "" : "s"}` : "Reusable project configurations — capture one from a project, or start a project from one"
85
- })] }), /* @__PURE__ */ m(a, { onCreated: (e) => S(n("/templates/edit-template-page", e)) })]
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__ */ h("div", {
87
+ /* @__PURE__ */ g("div", {
88
88
  className: "flex flex-wrap items-center gap-3",
89
- children: [/* @__PURE__ */ h("div", {
89
+ children: [/* @__PURE__ */ g("div", {
90
90
  className: "relative w-full max-w-sm",
91
- children: [/* @__PURE__ */ m(d, { className: "absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ m(y, {
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: C,
96
- onChange: (e) => w(e.target.value)
95
+ value: w,
96
+ onChange: (e) => T(e.target.value)
97
97
  })]
98
- }), /* @__PURE__ */ m("button", {
98
+ }), /* @__PURE__ */ h("button", {
99
99
  type: "button",
100
- "aria-pressed": T,
101
- onClick: () => E((e) => !e),
102
- className: `px-3 py-1.5 text-sm rounded-md ${T ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
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
- N && /* @__PURE__ */ h("p", {
106
+ P && /* @__PURE__ */ g("p", {
107
107
  role: "alert",
108
108
  className: "text-sm text-destructive",
109
- children: ["Templates could not be loaded: ", N.message]
109
+ children: ["Templates could not be loaded: ", P.message]
110
110
  }),
111
- M && !j && /* @__PURE__ */ m("p", {
111
+ N && !M && /* @__PURE__ */ h("p", {
112
112
  className: "text-sm text-muted-foreground",
113
113
  children: "Loading templates…"
114
114
  }),
115
- F && I.length === 0 && /* @__PURE__ */ m(v, {
116
- icon: /* @__PURE__ */ m(l, { className: "h-8 w-8 text-muted-foreground" }),
117
- title: k || T ? "No templates match" : "No templates yet",
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: k || T ? void 0 : /* @__PURE__ */ m(a, { onCreated: (e) => S(n("/templates/edit-template-page", e)) })
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__ */ m("ul", {
128
+ /* @__PURE__ */ h("ul", {
122
129
  className: "grid grid-cols-1 gap-3 md:grid-cols-2",
123
- children: I.map((e) => /* @__PURE__ */ h("li", {
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__ */ h("div", {
134
+ /* @__PURE__ */ g("div", {
128
135
  className: "flex items-start justify-between gap-2",
129
- children: [/* @__PURE__ */ h("div", {
136
+ children: [/* @__PURE__ */ g("div", {
130
137
  className: "min-w-0",
131
- children: [/* @__PURE__ */ m("p", {
138
+ children: [/* @__PURE__ */ h("p", {
132
139
  className: "truncate text-sm font-medium",
133
140
  children: e.name
134
- }), /* @__PURE__ */ m("p", {
141
+ }), /* @__PURE__ */ h("p", {
135
142
  className: "truncate text-xs text-muted-foreground",
136
143
  children: e.description ?? "No description"
137
144
  })]
138
- }), /* @__PURE__ */ h("div", {
145
+ }), /* @__PURE__ */ g("div", {
139
146
  className: "flex shrink-0 gap-1",
140
- children: [e.isBuiltIn && /* @__PURE__ */ m(g, {
147
+ children: [e.isBuiltIn && /* @__PURE__ */ h(_, {
141
148
  variant: "secondary",
142
149
  children: "Built-in"
143
- }), /* @__PURE__ */ m(g, {
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__ */ h("div", {
156
+ /* @__PURE__ */ g("div", {
150
157
  className: "flex flex-wrap items-center gap-1",
151
- children: [e.category && /* @__PURE__ */ m(g, {
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__ */ m(g, {
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__ */ h("p", {
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__ */ h("div", {
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__ */ h("div", {
180
+ children: [/* @__PURE__ */ g("div", {
174
181
  className: "flex gap-2",
175
- children: [/* @__PURE__ */ h(_, {
182
+ children: [/* @__PURE__ */ g(v, {
176
183
  size: "sm",
177
184
  variant: "outline",
178
- onClick: () => S(n("/templates/edit-template-page", e.id)),
185
+ onClick: () => C(n("/templates/edit-template-page", e.id)),
179
186
  "data-testid": "template-edit",
180
- children: [/* @__PURE__ */ m(u, { className: "mr-1 h-4 w-4" }), "Edit"]
181
- }), /* @__PURE__ */ m(o, {
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__ */ m(i, {
193
+ }), /* @__PURE__ */ h(i, {
187
194
  templateId: e.id,
188
195
  templateName: e.name,
189
- onApplied: (e) => S(n("/projects/project-detail-page", e))
196
+ onApplied: (e) => C(n("/projects/project-detail-page", e))
190
197
  })]
191
198
  })
192
199
  ]
193
200
  }, e.id))
194
201
  }),
195
- F?.pageInfo.hasNextPage && /* @__PURE__ */ m(_, {
202
+ I?.pageInfo.hasNextPage && /* @__PURE__ */ h(v, {
196
203
  size: "sm",
197
204
  variant: "outline",
198
- onClick: () => void L(),
199
- disabled: D,
200
- children: D ? "Loading…" : `Show more (${I.length} of ${F.totalCount})`
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 { S as TemplatesTemplatesPagePage };
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":";;;;;;;;;;;AAgBA,IAAM,IAAY,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCf,IAAO;AAEb,SAAgB,IAA6B;CAC3C,IAAM,EAAE,mBAAgB,GAAe,EACjC,IAAW,GAAuB,EAClC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAe,KAAoB,EAAS,GAAM,EACnD,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,IAAO,EAAiB,EAAO,MAAM,CAAC,EAEtC,IAAS;EACb,GAAI,IAAO,EAAE,QAAQ,GAAM,GAAG,EAAE;EAChC,GAAI,IAAgB,EAAE,aAAa,IAAM,GAAG,EAAE;EAC/C,EAEK,EAAE,SAAM,YAAS,UAAO,iBAAc,EAMzC,GAAW;EACZ,WAAW;GAAE,aAAa,KAAe;GAAI;GAAQ,YAAY,EAAE,OAAO,GAAM;GAAE;EAClF,MAAM,CAAC;EACP,aAAa;EACd,CAAC,EAEI,IAAO,GAAM,4BACb,IAAY,GAAM,SAAS,EAAE,EAE7B,IAAW,YAAY;EAC3B,IAAM,IAAQ,GAAM,SAAS;AACxB,SACL;KAAe,GAAK;AACpB,OAAI;AACF,UAAM,EAAU;KACd,WAAW,EAAE,YAAY;MAAE,OAAO;MAAM;MAAO,EAAE;KACjD,cAAc,GAAM,EAAE,0BAAuB,EAC3C,4BAA4B;MAC1B,GAAG,EAAgB;MACnB,OAAO,CACL,GAAG,EAAK,2BAA2B,OACnC,GAAG,EAAgB,2BAA2B,MAC/C;MACF,EACF;KACF,CAAC;aACM;AACR,MAAe,GAAM;;;;AAIzB,QACE,kBAAC,OAAD;EAAK,WAAU;EAAgB,eAAY;YAA3C;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAAyB;KAAsB,CAAA,EAC7D,kBAAC,KAAD;KAAG,WAAU;eACV,IACG,GAAG,EAAK,WAAW,WAAW,EAAK,eAAe,IAAI,KAAK,QAC3D;KACF,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD,EACE,YAAY,MAAO,EAAS,EAAW,iCAAiC,EAAG,CAAC,EAC5E,CAAA,CACE;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,2DAA4D,CAAA,EAC9E,kBAAC,GAAD;MACE,cAAW;MACX,aAAY;MACZ,WAAU;MACV,OAAO;MACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;MAC1C,CAAA,CACE;QACN,kBAAC,UAAD;KACE,MAAK;KACL,gBAAc;KACd,eAAe,GAAkB,MAAM,CAAC,EAAE;KAC1C,WAAW,kCAAkC,IAAgB,uCAAuC;eACrG;KAEQ,CAAA,CACL;;GAEL,KACC,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cAA1B,CAAqD,mCACnB,EAAM,QACpC;;GAEL,KAAW,CAAC,KAAQ,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAAsB,CAAA;GACvF,KAAQ,EAAU,WAAW,KAC5B,kBAAC,GAAD;IACE,MAAM,kBAAC,GAAD,EAAgB,WAAU,iCAAkC,CAAA;IAClE,OAAO,KAAQ,IAAgB,uBAAuB;IACtD,aAAY;IACZ,QACI,KAAQ,IAIN,KAAA,IAHF,kBAAC,GAAD,EACE,YAAY,MAAO,EAAS,EAAW,iCAAiC,EAAG,CAAC,EAC5E,CAAA;IAGN,CAAA;GAGJ,kBAAC,MAAD;IAAI,WAAU;cACX,EAAU,KAAK,MACd,kBAAC,MAAD;KAEE,eAAY;KACZ,WAAU;eAHZ;MAKE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAgC,EAAS;SAAS,CAAA,EAC/D,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAS,eAAe;SACvB,CAAA,CACA;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACG,EAAS,aAAa,kBAAC,GAAD;SAAO,SAAQ;mBAAY;SAAgB,CAAA,EAClE,kBAAC,GAAD;SAAO,SAAS,EAAS,cAAc,YAAY;mBAChD,EAAS,cAAc,cAAc;SAChC,CAAA,CACJ;UACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAS,YACR,kBAAC,GAAD;QAAO,SAAQ;QAAU,WAAU;kBAChC,EAAS;QACJ,CAAA,EAET,EAAS,KAAK,KAAK,MAClB,kBAAC,GAAD;QAAiB,SAAQ;QAAU,WAAU;kBAC1C;QACK,EAFI,EAEJ,CACR,CACE;;MACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QAA6C;QACrC,EAAS;QAAW;QAAM,EAAS,eAAe,IAAI,KAAK;QAC/D;;MACJ,kBAAC,OAAD;OACE,WAAU;OACV,SAAS;iBAFX,CAIE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SACE,MAAK;SACL,SAAQ;SACR,eAAe,EAAS,EAAW,iCAAiC,EAAS,GAAG,CAAC;SACjF,eAAY;mBAJd,CAME,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,OAE5B;YACT,kBAAC,GAAD;SACE,YAAY,EAAS;SACrB,MAAM,EAAS;SACf,WAAW,EAAS;SACpB,CAAA,CACE;WACN,kBAAC,GAAD;QACE,YAAY,EAAS;QACrB,cAAc,EAAS;QACvB,YAAY,MACV,EAAS,EAAW,iCAAiC,EAAU,CAAC;QAElE,CAAA,CACE;;MACH;OA7DE,EAAS,GA6DX,CACL;IACC,CAAA;GACJ,GAAM,SAAS,eACd,kBAAC,GAAD;IAAQ,MAAK;IAAK,SAAQ;IAAU,eAAe,KAAK,GAAU;IAAE,UAAU;cAC3E,IAAc,aAAa,cAAc,EAAU,OAAO,MAAM,EAAK,WAAW;IAC1E,CAAA;GAEP"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.928.6",
3
+ "version": "2026.928.8",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [