@burdenoff/microfe-planmagnet 2026.928.7 → 2026.928.9
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/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +18 -14
- package/dist/planmagnet/PlanMagnetRoutes.js.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/WorkItemControls.d.ts +21 -0
- package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.js +34 -15
- package/dist/planmagnet/controls/WorkItemControls.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/ReportsReportsPagePage.d.ts +0 -7
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +517 -39
- package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksMyWorkPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksMyWorkPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksMyWorkPagePage.js +257 -0
- package/dist/planmagnet/pages/TasksMyWorkPagePage.js.map +1 -0
- 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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { WORK_ITEM_PRIORITIES as n, WorkItemAssigneeSelect as r, WorkItemDueDateInput as i, WorkItemEnumSelect as a, WorkItemStatusSelect as o } from "../controls/WorkItemControls.js";
|
|
3
|
+
import { rowLinkProps as s, stopRowActivation as c } from "../utils/rowNavigation.js";
|
|
4
|
+
import { useState as l } from "react";
|
|
5
|
+
import { CalendarClock as u, ListChecks as d } from "lucide-react";
|
|
6
|
+
import { gql as f } from "@apollo/client";
|
|
7
|
+
import { useQuery as p } from "@apollo/client/react";
|
|
8
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
9
|
+
import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { Badge as _, Card as v, CardContent as y, Switch as b } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/planmagnet/pages/TasksMyWorkPagePage.tsx
|
|
12
|
+
var x = f`
|
|
13
|
+
query GetMyWorkItems(
|
|
14
|
+
$workspaceId: ID!
|
|
15
|
+
$filter: PlanmagnetMyWorkItemFilter
|
|
16
|
+
$pagination: PlanmagnetPaginationInput
|
|
17
|
+
) {
|
|
18
|
+
planmagnetMyWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {
|
|
19
|
+
items {
|
|
20
|
+
id
|
|
21
|
+
key
|
|
22
|
+
title
|
|
23
|
+
status
|
|
24
|
+
priority
|
|
25
|
+
dueDate
|
|
26
|
+
storyPoints
|
|
27
|
+
isOverdue
|
|
28
|
+
dueGroup
|
|
29
|
+
assigneeId
|
|
30
|
+
project {
|
|
31
|
+
id
|
|
32
|
+
name
|
|
33
|
+
}
|
|
34
|
+
sprint {
|
|
35
|
+
id
|
|
36
|
+
name
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
pageInfo {
|
|
40
|
+
hasNextPage
|
|
41
|
+
endCursor
|
|
42
|
+
}
|
|
43
|
+
totalCount
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`, S = 50, C = [
|
|
47
|
+
"OVERDUE",
|
|
48
|
+
"DUE_TODAY",
|
|
49
|
+
"DUE_THIS_WEEK",
|
|
50
|
+
"DUE_LATER",
|
|
51
|
+
"NO_DUE_DATE"
|
|
52
|
+
], w = {
|
|
53
|
+
OVERDUE: "destructive",
|
|
54
|
+
DUE_TODAY: "warning",
|
|
55
|
+
DUE_THIS_WEEK: "info",
|
|
56
|
+
DUE_LATER: "secondary",
|
|
57
|
+
NO_DUE_DATE: "secondary"
|
|
58
|
+
};
|
|
59
|
+
function T(e, t) {
|
|
60
|
+
switch (t) {
|
|
61
|
+
case "OVERDUE": return e("planmagnet.myWork.group.overdue", "Overdue");
|
|
62
|
+
case "DUE_TODAY": return e("planmagnet.myWork.group.dueToday", "Due today");
|
|
63
|
+
case "DUE_THIS_WEEK": return e("planmagnet.myWork.group.dueThisWeek", "Due this week");
|
|
64
|
+
case "DUE_LATER": return e("planmagnet.myWork.group.dueLater", "Due later");
|
|
65
|
+
default: return e("planmagnet.myWork.group.noDueDate", "No due date");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function E() {
|
|
69
|
+
let { workspaceId: f } = e(), E = t(), { t: D } = g(), [O, k] = l(!1), [A, j] = l(!1), { data: M, loading: N, error: P, fetchMore: F } = p(x, {
|
|
70
|
+
variables: {
|
|
71
|
+
workspaceId: f,
|
|
72
|
+
filter: { includeClosed: O },
|
|
73
|
+
pagination: { first: S }
|
|
74
|
+
},
|
|
75
|
+
skip: !f
|
|
76
|
+
}), I = async () => {
|
|
77
|
+
let e = M?.planmagnetMyWorkItems?.pageInfo?.endCursor;
|
|
78
|
+
if (e) {
|
|
79
|
+
j(!0);
|
|
80
|
+
try {
|
|
81
|
+
await F({
|
|
82
|
+
variables: { pagination: {
|
|
83
|
+
first: S,
|
|
84
|
+
after: e
|
|
85
|
+
} },
|
|
86
|
+
updateQuery: (e, { fetchMoreResult: t }) => ({ planmagnetMyWorkItems: {
|
|
87
|
+
...t.planmagnetMyWorkItems,
|
|
88
|
+
items: [...e.planmagnetMyWorkItems.items, ...t.planmagnetMyWorkItems.items]
|
|
89
|
+
} })
|
|
90
|
+
});
|
|
91
|
+
} finally {
|
|
92
|
+
j(!1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
if (N && !M) return /* @__PURE__ */ m("div", {
|
|
97
|
+
className: "flex items-center justify-center p-12",
|
|
98
|
+
children: /* @__PURE__ */ m("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
|
|
99
|
+
});
|
|
100
|
+
if (P) return /* @__PURE__ */ h("div", {
|
|
101
|
+
className: "p-6 text-destructive",
|
|
102
|
+
children: [
|
|
103
|
+
D("planmagnet.myWork.errorLoading", "My work could not be loaded"),
|
|
104
|
+
": ",
|
|
105
|
+
P.message
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
let L = M?.planmagnetMyWorkItems?.items ?? [], R = M?.planmagnetMyWorkItems?.totalCount ?? L.length, z = M?.planmagnetMyWorkItems?.pageInfo?.hasNextPage ?? !1, B = C.map((e) => ({
|
|
109
|
+
group: e,
|
|
110
|
+
items: L.filter((t) => t.dueGroup === e)
|
|
111
|
+
})).filter((e) => e.items.length > 0);
|
|
112
|
+
return /* @__PURE__ */ h("div", {
|
|
113
|
+
className: "space-y-4 p-6",
|
|
114
|
+
children: [/* @__PURE__ */ h("div", {
|
|
115
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
116
|
+
children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("h1", {
|
|
117
|
+
className: "text-2xl font-semibold flex items-center gap-2",
|
|
118
|
+
children: [/* @__PURE__ */ m(d, { className: "h-6 w-6" }), D("planmagnet.myWork.title", "My Work")]
|
|
119
|
+
}), /* @__PURE__ */ m("p", {
|
|
120
|
+
className: "text-sm text-muted-foreground",
|
|
121
|
+
"data-testid": "my-work-total",
|
|
122
|
+
children: D("planmagnet.myWork.subtitle", "{{count}} item(s) assigned to you across every project", { count: R })
|
|
123
|
+
})] }), /* @__PURE__ */ h("label", {
|
|
124
|
+
className: "flex items-center gap-2 text-sm text-muted-foreground",
|
|
125
|
+
children: [/* @__PURE__ */ m(b, {
|
|
126
|
+
checked: O,
|
|
127
|
+
onCheckedChange: k,
|
|
128
|
+
"aria-label": D("planmagnet.myWork.includeClosed", "Show completed"),
|
|
129
|
+
"data-testid": "my-work-include-closed"
|
|
130
|
+
}), D("planmagnet.myWork.includeClosed", "Show completed")]
|
|
131
|
+
})]
|
|
132
|
+
}), L.length === 0 ? /* @__PURE__ */ m(v, { children: /* @__PURE__ */ h(y, {
|
|
133
|
+
className: "flex flex-col items-center gap-2 py-12 text-center",
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ m(u, { className: "h-8 w-8 text-muted-foreground" }),
|
|
136
|
+
/* @__PURE__ */ m("p", {
|
|
137
|
+
className: "text-sm font-medium",
|
|
138
|
+
children: D("planmagnet.myWork.empty", "No work assigned to you")
|
|
139
|
+
}),
|
|
140
|
+
/* @__PURE__ */ m("p", {
|
|
141
|
+
className: "text-xs text-muted-foreground",
|
|
142
|
+
children: O ? D("planmagnet.myWork.emptyHintClosed", "Nothing is assigned to you in any project yet.") : D("planmagnet.myWork.emptyHint", "Nothing open is assigned to you. Toggle \"Show completed\" to see recently finished work.")
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
}) }) : /* @__PURE__ */ h("div", {
|
|
146
|
+
className: "space-y-6",
|
|
147
|
+
children: [B.map(({ group: e, items: t }) => /* @__PURE__ */ h("section", {
|
|
148
|
+
className: "space-y-2",
|
|
149
|
+
"data-testid": `my-work-group-${e}`,
|
|
150
|
+
children: [/* @__PURE__ */ h("div", {
|
|
151
|
+
className: "flex items-center gap-2",
|
|
152
|
+
children: [/* @__PURE__ */ m(_, {
|
|
153
|
+
variant: w[e],
|
|
154
|
+
children: T(D, e)
|
|
155
|
+
}), /* @__PURE__ */ h("span", {
|
|
156
|
+
className: "text-xs text-muted-foreground",
|
|
157
|
+
children: [
|
|
158
|
+
t.length,
|
|
159
|
+
" ",
|
|
160
|
+
t.length === 1 ? "item" : "items"
|
|
161
|
+
]
|
|
162
|
+
})]
|
|
163
|
+
}), /* @__PURE__ */ m("div", {
|
|
164
|
+
className: "space-y-2",
|
|
165
|
+
children: t.map((e) => /* @__PURE__ */ m(v, {
|
|
166
|
+
...s(() => E(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`), e.title),
|
|
167
|
+
children: /* @__PURE__ */ h(y, {
|
|
168
|
+
className: "flex flex-wrap items-center gap-3 p-3",
|
|
169
|
+
children: [/* @__PURE__ */ h("div", {
|
|
170
|
+
className: "min-w-[12rem] flex-1 space-y-1",
|
|
171
|
+
children: [/* @__PURE__ */ m("p", {
|
|
172
|
+
className: "text-sm font-medium leading-tight",
|
|
173
|
+
children: e.title
|
|
174
|
+
}), /* @__PURE__ */ h("div", {
|
|
175
|
+
className: "flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground",
|
|
176
|
+
children: [
|
|
177
|
+
/* @__PURE__ */ m("span", { children: e.key }),
|
|
178
|
+
e.project && /* @__PURE__ */ m(_, {
|
|
179
|
+
variant: "outline",
|
|
180
|
+
children: e.project.name
|
|
181
|
+
}),
|
|
182
|
+
e.sprint && /* @__PURE__ */ m(_, {
|
|
183
|
+
variant: "secondary",
|
|
184
|
+
children: e.sprint.name
|
|
185
|
+
}),
|
|
186
|
+
e.dueDate && /* @__PURE__ */ h("span", {
|
|
187
|
+
className: e.isOverdue ? "text-destructive font-medium" : void 0,
|
|
188
|
+
children: ["Due ", new Date(e.dueDate).toLocaleDateString()]
|
|
189
|
+
}),
|
|
190
|
+
e.storyPoints != null && /* @__PURE__ */ h("span", { children: [e.storyPoints, " pts"] })
|
|
191
|
+
]
|
|
192
|
+
})]
|
|
193
|
+
}), /* @__PURE__ */ h("div", {
|
|
194
|
+
className: "flex flex-wrap items-center gap-2",
|
|
195
|
+
onClick: c,
|
|
196
|
+
onKeyDown: (e) => e.stopPropagation(),
|
|
197
|
+
children: [
|
|
198
|
+
e.project ? /* @__PURE__ */ m(o, {
|
|
199
|
+
workItemId: e.id,
|
|
200
|
+
projectId: e.project.id,
|
|
201
|
+
value: e.status
|
|
202
|
+
}) : /* @__PURE__ */ m(a, {
|
|
203
|
+
workItemId: e.id,
|
|
204
|
+
field: "status",
|
|
205
|
+
value: e.status,
|
|
206
|
+
options: [
|
|
207
|
+
"BACKLOG",
|
|
208
|
+
"TODO",
|
|
209
|
+
"IN_PROGRESS",
|
|
210
|
+
"IN_REVIEW",
|
|
211
|
+
"TESTING",
|
|
212
|
+
"DONE",
|
|
213
|
+
"CANCELLED"
|
|
214
|
+
],
|
|
215
|
+
label: "Status"
|
|
216
|
+
}),
|
|
217
|
+
/* @__PURE__ */ m(a, {
|
|
218
|
+
workItemId: e.id,
|
|
219
|
+
field: "priority",
|
|
220
|
+
value: e.priority,
|
|
221
|
+
options: n,
|
|
222
|
+
label: "Priority"
|
|
223
|
+
}),
|
|
224
|
+
/* @__PURE__ */ m(r, {
|
|
225
|
+
workItemId: e.id,
|
|
226
|
+
assigneeId: e.assigneeId
|
|
227
|
+
}),
|
|
228
|
+
/* @__PURE__ */ m(i, {
|
|
229
|
+
workItemId: e.id,
|
|
230
|
+
dueDate: e.dueDate
|
|
231
|
+
})
|
|
232
|
+
]
|
|
233
|
+
})]
|
|
234
|
+
})
|
|
235
|
+
}, e.id))
|
|
236
|
+
})]
|
|
237
|
+
}, e)), z && /* @__PURE__ */ m("div", {
|
|
238
|
+
className: "flex justify-center",
|
|
239
|
+
children: /* @__PURE__ */ m("button", {
|
|
240
|
+
type: "button",
|
|
241
|
+
onClick: () => void I(),
|
|
242
|
+
disabled: A,
|
|
243
|
+
className: "px-3 py-1.5 text-sm rounded-md bg-muted text-foreground hover:bg-muted/80 disabled:opacity-50",
|
|
244
|
+
"data-testid": "my-work-load-more",
|
|
245
|
+
children: A ? D("planmagnet.myWork.loadingMore", "Loading…") : D("planmagnet.myWork.loadMore", "Load more ({{shown}} of {{total}})", {
|
|
246
|
+
shown: L.length,
|
|
247
|
+
total: R
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
})]
|
|
251
|
+
})]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
export { E as TasksMyWorkPagePage };
|
|
256
|
+
|
|
257
|
+
//# sourceMappingURL=TasksMyWorkPagePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TasksMyWorkPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksMyWorkPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Card, CardContent, Switch } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { CalendarClock, ListChecks } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps, stopRowActivation } from '../utils/rowNavigation';\nimport {\n WORK_ITEM_PRIORITIES,\n WorkItemAssigneeSelect,\n WorkItemDueDateInput,\n WorkItemEnumSelect,\n WorkItemStatusSelect,\n} from '../controls/WorkItemControls';\n\n/**\n * My Work (BOFF-8048): the caller's own assigned work items across every project in the workspace, grouped by\n * due-date bucket (overdue first) and sorted the way `planmagnetMyWorkItems` returns them — server-computed, so\n * two clients (or two loads of the same client) never disagree on the grouping. See §3.2 of the BOFF-8048 design.\n *\n * Identity is implicit: there is no `actorId` argument on the query (it reads `x-actor-id` like every other\n * self-filtered list here), so there is nothing for this page to pass or fake.\n *\n * NOTE: `planmagnetMyWorkItems`, and the `isOverdue`/`dueGroup` fields on WorkItem, ship with the BOFF-8048 backend\n * PR. Until that PR deploys and the Hive CDN reconciles, this page's query will 400 in a live environment and\n * `bun run validate:schema` will fail on it locally (graphql/wspace.graphql has not been re-pulled with the new\n * fields yet) — expected, not a bug in this page. Re-run `schema-pull.sh` after the backend ships.\n */\n\nconst GET_MY_WORK_ITEMS = gql`\n query GetMyWorkItems(\n $workspaceId: ID!\n $filter: PlanmagnetMyWorkItemFilter\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetMyWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: $pagination) {\n items {\n id\n key\n title\n status\n priority\n dueDate\n storyPoints\n isOverdue\n dueGroup\n assigneeId\n project {\n id\n name\n }\n sprint {\n id\n name\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\nconst PAGE_SIZE = 50;\n\ninterface MyWorkItemNode {\n id: string;\n key: string;\n title: string;\n status: string;\n priority: string;\n dueDate: string | null;\n storyPoints: number | null;\n isOverdue: boolean;\n dueGroup: 'OVERDUE' | 'DUE_TODAY' | 'DUE_THIS_WEEK' | 'DUE_LATER' | 'NO_DUE_DATE';\n assigneeId: string | null;\n project: { id: string; name: string } | null;\n sprint: { id: string; name: string } | null;\n}\n\ninterface MyWorkData {\n planmagnetMyWorkItems: {\n items: MyWorkItemNode[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n}\n\n// Server order already IS the group order (bucketRank, then due date, then priority, then key) — this list only\n// says how to label/style each bucket and which buckets to render as sections, never re-sorts.\nconst DUE_GROUP_ORDER = [\n 'OVERDUE',\n 'DUE_TODAY',\n 'DUE_THIS_WEEK',\n 'DUE_LATER',\n 'NO_DUE_DATE',\n] as const;\n\nconst DUE_GROUP_BADGE: Record<\n (typeof DUE_GROUP_ORDER)[number],\n 'destructive' | 'warning' | 'info' | 'secondary'\n> = {\n OVERDUE: 'destructive',\n DUE_TODAY: 'warning',\n DUE_THIS_WEEK: 'info',\n DUE_LATER: 'secondary',\n NO_DUE_DATE: 'secondary',\n};\n\nfunction dueGroupLabel(t: (key: string, fallback: string) => string, group: string): string {\n switch (group) {\n case 'OVERDUE':\n return t('planmagnet.myWork.group.overdue', 'Overdue');\n case 'DUE_TODAY':\n return t('planmagnet.myWork.group.dueToday', 'Due today');\n case 'DUE_THIS_WEEK':\n return t('planmagnet.myWork.group.dueThisWeek', 'Due this week');\n case 'DUE_LATER':\n return t('planmagnet.myWork.group.dueLater', 'Due later');\n default:\n return t('planmagnet.myWork.group.noDueDate', 'No due date');\n }\n}\n\nexport function TasksMyWorkPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { t } = useI18n();\n const [includeClosed, setIncludeClosed] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n\n const { data, loading, error, fetchMore } = useQuery<MyWorkData>(GET_MY_WORK_ITEMS, {\n variables: { workspaceId, filter: { includeClosed }, pagination: { first: PAGE_SIZE } },\n skip: !workspaceId,\n });\n\n const loadMore = async () => {\n const after = data?.planmagnetMyWorkItems?.pageInfo?.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { first: PAGE_SIZE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetMyWorkItems: {\n ...fetchMoreResult.planmagnetMyWorkItems,\n items: [\n ...prev.planmagnetMyWorkItems.items,\n ...fetchMoreResult.planmagnetMyWorkItems.items,\n ],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n // Only before the first load: a refetch (toggling \"Show completed\", or an inline edit's own list refetch) must\n // not blank the page — that would also wipe out any row-level refusal message still showing (BOFF-8043).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"p-6 text-destructive\">\n {t('planmagnet.myWork.errorLoading', 'My work could not be loaded')}: {error.message}\n </div>\n );\n }\n\n const items = data?.planmagnetMyWorkItems?.items ?? [];\n const total = data?.planmagnetMyWorkItems?.totalCount ?? items.length;\n const hasMore = data?.planmagnetMyWorkItems?.pageInfo?.hasNextPage ?? false;\n\n const groups = DUE_GROUP_ORDER.map((group) => ({\n group,\n items: items.filter((item) => item.dueGroup === group),\n })).filter((g) => g.items.length > 0);\n\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <ListChecks className=\"h-6 w-6\" />\n {t('planmagnet.myWork.title', 'My Work')}\n </h1>\n <p className=\"text-sm text-muted-foreground\" data-testid=\"my-work-total\">\n {t(\n 'planmagnet.myWork.subtitle',\n '{{count}} item(s) assigned to you across every project',\n {\n count: total,\n }\n )}\n </p>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Switch\n checked={includeClosed}\n onCheckedChange={setIncludeClosed}\n aria-label={t('planmagnet.myWork.includeClosed', 'Show completed')}\n data-testid=\"my-work-include-closed\"\n />\n {t('planmagnet.myWork.includeClosed', 'Show completed')}\n </label>\n </div>\n\n {items.length === 0 ? (\n <Card>\n <CardContent className=\"flex flex-col items-center gap-2 py-12 text-center\">\n <CalendarClock className=\"h-8 w-8 text-muted-foreground\" />\n <p className=\"text-sm font-medium\">\n {t('planmagnet.myWork.empty', 'No work assigned to you')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {includeClosed\n ? t(\n 'planmagnet.myWork.emptyHintClosed',\n 'Nothing is assigned to you in any project yet.'\n )\n : t(\n 'planmagnet.myWork.emptyHint',\n 'Nothing open is assigned to you. Toggle \"Show completed\" to see recently finished work.'\n )}\n </p>\n </CardContent>\n </Card>\n ) : (\n <div className=\"space-y-6\">\n {groups.map(({ group, items: groupItems }) => (\n <section key={group} className=\"space-y-2\" data-testid={`my-work-group-${group}`}>\n <div className=\"flex items-center gap-2\">\n <Badge variant={DUE_GROUP_BADGE[group]}>{dueGroupLabel(t, group)}</Badge>\n <span className=\"text-xs text-muted-foreground\">\n {groupItems.length} {groupItems.length === 1 ? 'item' : 'items'}\n </span>\n </div>\n <div className=\"space-y-2\">\n {groupItems.map((item) => (\n <Card\n key={item.id}\n {...rowLinkProps(\n () =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(item.id)}`),\n item.title\n )}\n >\n <CardContent className=\"flex flex-wrap items-center gap-3 p-3\">\n <div className=\"min-w-[12rem] flex-1 space-y-1\">\n <p className=\"text-sm font-medium leading-tight\">{item.title}</p>\n <div className=\"flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground\">\n <span>{item.key}</span>\n {item.project && <Badge variant=\"outline\">{item.project.name}</Badge>}\n {item.sprint && <Badge variant=\"secondary\">{item.sprint.name}</Badge>}\n {item.dueDate && (\n <span\n className={\n item.isOverdue ? 'text-destructive font-medium' : undefined\n }\n >\n Due {new Date(item.dueDate).toLocaleDateString()}\n </span>\n )}\n {item.storyPoints != null && <span>{item.storyPoints} pts</span>}\n </div>\n </div>\n <div\n className=\"flex flex-wrap items-center gap-2\"\n onClick={stopRowActivation}\n onKeyDown={(e) => e.stopPropagation()}\n >\n {item.project ? (\n <WorkItemStatusSelect\n workItemId={item.id}\n projectId={item.project.id}\n value={item.status}\n />\n ) : (\n <WorkItemEnumSelect\n workItemId={item.id}\n field=\"status\"\n value={item.status}\n options={[\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n ]}\n label=\"Status\"\n />\n )}\n <WorkItemEnumSelect\n workItemId={item.id}\n field=\"priority\"\n value={item.priority}\n options={WORK_ITEM_PRIORITIES}\n label=\"Priority\"\n />\n <WorkItemAssigneeSelect workItemId={item.id} assigneeId={item.assigneeId} />\n <WorkItemDueDateInput workItemId={item.id} dueDate={item.dueDate} />\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n </section>\n ))}\n {hasMore && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n onClick={() => void loadMore()}\n disabled={loadingMore}\n className=\"px-3 py-1.5 text-sm rounded-md bg-muted text-foreground hover:bg-muted/80 disabled:opacity-50\"\n data-testid=\"my-work-load-more\"\n >\n {loadingMore\n ? t('planmagnet.myWork.loadingMore', 'Loading…')\n : t('planmagnet.myWork.loadMore', 'Load more ({{shown}} of {{total}})', {\n shown: items.length,\n total,\n })}\n </button>\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemplatesTemplatesPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,0BAA0B,
|
|
1
|
+
{"version":3,"file":"TemplatesTemplatesPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TemplatesTemplatesPagePage.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,0BAA0B,4CAqMzC"}
|
|
@@ -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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"}
|