@burdenoff/microfe-planmagnet 2026.928.3 → 2026.928.4
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/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +102 -84
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +103 -55
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/controls/CompetitorControls.d.ts +20 -0
- package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/CompetitorControls.js +172 -0
- package/dist/planmagnet/controls/CompetitorControls.js.map +1 -0
- package/dist/planmagnet/controls/FeatureControls.js +29 -29
- package/dist/planmagnet/controls/FeatureTriage.js +11 -11
- package/dist/planmagnet/controls/ObjectiveControls.js +24 -24
- package/dist/planmagnet/controls/Pickers.d.ts +17 -2
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +100 -9
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/PortfolioControls.d.ts +13 -0
- package/dist/planmagnet/controls/PortfolioControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/PortfolioControls.js +114 -0
- package/dist/planmagnet/controls/PortfolioControls.js.map +1 -0
- package/dist/planmagnet/controls/ProgramControls.d.ts +25 -0
- package/dist/planmagnet/controls/ProgramControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/ProgramControls.js +187 -0
- package/dist/planmagnet/controls/ProgramControls.js.map +1 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +127 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +129 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +132 -0
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -0
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +181 -0
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -0
- package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.js +242 -0
- package/dist/planmagnet/pages/PortfoliosCreatePortfolioPagePage.js.map +1 -0
- package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.js +302 -0
- package/dist/planmagnet/pages/PortfoliosEditPortfolioPagePage.js.map +1 -0
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +335 -0
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.js +154 -0
- package/dist/planmagnet/pages/PortfoliosPortfoliosPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +124 -138
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.js +221 -0
- package/dist/planmagnet/pages/ProgramsCreateProgramPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProgramsEditProgramPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProgramsEditProgramPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProgramsEditProgramPagePage.js +279 -0
- package/dist/planmagnet/pages/ProgramsEditProgramPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.js +178 -0
- package/dist/planmagnet/pages/ProgramsProgramDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProgramsProgramsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProgramsProgramsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProgramsProgramsPagePage.js +157 -0
- package/dist/planmagnet/pages/ProgramsProgramsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +130 -103
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +156 -136
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksImportPagePage.js +5 -5
- package/package.json +1 -1
|
@@ -16,11 +16,11 @@ var c = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm d
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
`;
|
|
19
|
-
function u({ id: t, value: n, onChange: r, noneLabel: s }) {
|
|
20
|
-
let { workspaceId:
|
|
21
|
-
variables: { workspaceId:
|
|
22
|
-
skip: !
|
|
23
|
-
}),
|
|
19
|
+
function u({ id: t, value: n, onChange: r, noneLabel: s, exclude: u = [] }) {
|
|
20
|
+
let { workspaceId: d } = e(), { data: f, loading: p, error: m } = i(l, {
|
|
21
|
+
variables: { workspaceId: d ?? "" },
|
|
22
|
+
skip: !d
|
|
23
|
+
}), h = (f?.planmagnetProjects.items ?? []).filter((e) => !u.includes(e.id));
|
|
24
24
|
return /* @__PURE__ */ o("select", {
|
|
25
25
|
id: t,
|
|
26
26
|
className: c,
|
|
@@ -31,11 +31,11 @@ function u({ id: t, value: n, onChange: r, noneLabel: s }) {
|
|
|
31
31
|
value: "",
|
|
32
32
|
children: s
|
|
33
33
|
}),
|
|
34
|
-
|
|
34
|
+
h.length === 0 && s === void 0 && /* @__PURE__ */ a("option", {
|
|
35
35
|
value: "",
|
|
36
|
-
children:
|
|
36
|
+
children: p && !f ? "Loading projects…" : m ? "Projects could not be loaded" : "No projects yet"
|
|
37
37
|
}),
|
|
38
|
-
|
|
38
|
+
h.map((e) => /* @__PURE__ */ a("option", {
|
|
39
39
|
value: e.id,
|
|
40
40
|
children: e.name
|
|
41
41
|
}, e.id))
|
|
@@ -165,7 +165,98 @@ function m({ id: r, value: l, onChange: u, exclude: d = [] }) {
|
|
|
165
165
|
]
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
|
+
var h = r`
|
|
169
|
+
query PickerPrograms($workspaceId: ID!) {
|
|
170
|
+
planmagnetPrograms(workspaceId: $workspaceId, pagination: { first: 100 }) {
|
|
171
|
+
items {
|
|
172
|
+
id
|
|
173
|
+
name
|
|
174
|
+
}
|
|
175
|
+
totalCount
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
function g({ id: t, value: n, onChange: r, noneLabel: s }) {
|
|
180
|
+
let { workspaceId: l } = e(), { data: u, loading: d, error: f } = i(h, {
|
|
181
|
+
variables: { workspaceId: l ?? "" },
|
|
182
|
+
skip: !l
|
|
183
|
+
}), p = u?.planmagnetPrograms.items ?? [];
|
|
184
|
+
return /* @__PURE__ */ o("select", {
|
|
185
|
+
id: t,
|
|
186
|
+
className: c,
|
|
187
|
+
value: n,
|
|
188
|
+
onChange: (e) => r(e.target.value),
|
|
189
|
+
children: [
|
|
190
|
+
s !== void 0 && /* @__PURE__ */ a("option", {
|
|
191
|
+
value: "",
|
|
192
|
+
children: s
|
|
193
|
+
}),
|
|
194
|
+
p.length === 0 && s === void 0 && /* @__PURE__ */ a("option", {
|
|
195
|
+
value: "",
|
|
196
|
+
children: d && !u ? "Loading programs…" : f ? "Programs could not be loaded" : "No programs yet"
|
|
197
|
+
}),
|
|
198
|
+
p.map((e) => /* @__PURE__ */ a("option", {
|
|
199
|
+
value: e.id,
|
|
200
|
+
children: e.name
|
|
201
|
+
}, e.id))
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
var _ = r`
|
|
206
|
+
query PickerCompetitors($workspaceId: ID!, $filter: PlanmagnetCompetitorFilter) {
|
|
207
|
+
planmagnetCompetitors(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {
|
|
208
|
+
items {
|
|
209
|
+
id
|
|
210
|
+
name
|
|
211
|
+
}
|
|
212
|
+
totalCount
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
function v({ id: r, value: l, onChange: u, exclude: d = [] }) {
|
|
217
|
+
let { workspaceId: f } = e(), [p, m] = n(""), h = t(p.trim()), { data: g, loading: v } = i(_, {
|
|
218
|
+
variables: {
|
|
219
|
+
workspaceId: f ?? "",
|
|
220
|
+
filter: h ? { search: h } : void 0
|
|
221
|
+
},
|
|
222
|
+
skip: !f
|
|
223
|
+
}), y = g?.planmagnetCompetitors.items ?? [], b = y.filter((e) => !d.includes(e.id)), x = g?.planmagnetCompetitors.totalCount ?? 0;
|
|
224
|
+
return /* @__PURE__ */ o("div", {
|
|
225
|
+
className: "space-y-1",
|
|
226
|
+
children: [
|
|
227
|
+
/* @__PURE__ */ a(s, {
|
|
228
|
+
"aria-label": "Search competitors",
|
|
229
|
+
placeholder: "Search competitors by name",
|
|
230
|
+
value: p,
|
|
231
|
+
onChange: (e) => m(e.target.value)
|
|
232
|
+
}),
|
|
233
|
+
/* @__PURE__ */ o("select", {
|
|
234
|
+
id: r,
|
|
235
|
+
className: c,
|
|
236
|
+
value: l,
|
|
237
|
+
onChange: (e) => u(e.target.value),
|
|
238
|
+
children: [/* @__PURE__ */ a("option", {
|
|
239
|
+
value: "",
|
|
240
|
+
children: v && !g ? "Searching…" : b.length ? "Choose a competitor" : "No match"
|
|
241
|
+
}), b.map((e) => /* @__PURE__ */ a("option", {
|
|
242
|
+
value: e.id,
|
|
243
|
+
children: e.name
|
|
244
|
+
}, e.id))]
|
|
245
|
+
}),
|
|
246
|
+
x > y.length && /* @__PURE__ */ o("p", {
|
|
247
|
+
className: "text-xs text-muted-foreground",
|
|
248
|
+
children: [
|
|
249
|
+
"Showing ",
|
|
250
|
+
y.length,
|
|
251
|
+
" of ",
|
|
252
|
+
x,
|
|
253
|
+
" — type to narrow the list."
|
|
254
|
+
]
|
|
255
|
+
})
|
|
256
|
+
]
|
|
257
|
+
});
|
|
258
|
+
}
|
|
168
259
|
//#endregion
|
|
169
|
-
export { m as DocSearchSelect, u as ProjectSelect, f as WorkItemSearchSelect };
|
|
260
|
+
export { v as CompetitorSearchSelect, m as DocSearchSelect, g as ProgramSelect, u as ProjectSelect, f as WorkItemSearchSelect };
|
|
170
261
|
|
|
171
262
|
//# sourceMappingURL=Pickers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pickers.js","names":[],"sources":["../../../src/planmagnet/controls/Pickers.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\n/**\n * Record pickers shared by forms and dialogs: a project select, and search-then-pick selects for work items and\n * docs. Values are record ids. Search runs on the server (never \"the first page, filtered in the browser\"), so a\n * record past the first page can still be found.\n */\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst PICKER_PROJECTS = gql`\n query PickerProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A project select. '' means \"no project\" when `noneLabel` is given. */\nexport function ProjectSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (projectId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetProjects: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PROJECTS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const projects = data?.planmagnetProjects.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {projects.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading projects…'\n : error\n ? 'Projects could not be loaded'\n : 'No projects yet'}\n </option>\n )}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_WORK_ITEMS = gql`\n query PickerWorkItems($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n key\n title\n }\n totalCount\n }\n }\n`;\n\n/**\n * Search, then pick one work item. The server matches the key, title and description. `exclude` hides items that are\n * already linked (the result says how many more exist, so an empty list never claims \"nothing left\" falsely).\n */\nexport function WorkItemSearchSelect({\n id,\n projectId,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n projectId?: string;\n value: string;\n onChange: (workItemId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetWorkItems: {\n items: { id: string; key: string; title: string }[];\n totalCount: number;\n };\n }>(PICKER_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { ...(deferred && { search: deferred }), ...(projectId && { projectId }) },\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetWorkItems.items ?? [];\n const items = found.filter((w) => !exclude.includes(w.id));\n const total = data?.planmagnetWorkItems.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search work items\"\n placeholder=\"Search by key or title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : items.length ? 'Choose a work item' : 'No match'}\n </option>\n {/* The current value stays selectable (and visible) when it is not in this page of results. */}\n {value && !items.some((w) => w.id === value) && (\n <option value={value}>Currently linked work item</option>\n )}\n {items.map((w) => (\n <option key={w.id} value={w.id}>\n {w.key} — {w.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_DOCS = gql`\n query PickerDocs($workspaceId: String, $filter: PlanmagnetDocFilter) {\n planmagnetDocs(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n title\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one doc. */\nexport function DocSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (docId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetDocs: { items: { id: string; title: string }[]; totalCount: number };\n }>(PICKER_DOCS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetDocs.items ?? [];\n const docs = found.filter((d) => !exclude.includes(d.id));\n const total = data?.planmagnetDocs.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search docs\"\n placeholder=\"Search docs by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : docs.length ? 'Choose a doc' : 'No match'}\n </option>\n {docs.map((d) => (\n <option key={d.id} value={d.id}>\n {d.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Pickers.js","names":[],"sources":["../../../src/planmagnet/controls/Pickers.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\n/**\n * Record pickers shared by forms and dialogs: a project select, and search-then-pick selects for work items and\n * docs. Values are record ids. Search runs on the server (never \"the first page, filtered in the browser\"), so a\n * record past the first page can still be found.\n */\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst PICKER_PROJECTS = gql`\n query PickerProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A project select. '' means \"no project\" when `noneLabel` is given. `exclude` hides ids already spoken for. */\nexport function ProjectSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n}: {\n id?: string;\n value: string;\n onChange: (projectId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetProjects: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PROJECTS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const projects = (data?.planmagnetProjects.items ?? []).filter((p) => !exclude.includes(p.id));\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {projects.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading projects…'\n : error\n ? 'Projects could not be loaded'\n : 'No projects yet'}\n </option>\n )}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_WORK_ITEMS = gql`\n query PickerWorkItems($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n key\n title\n }\n totalCount\n }\n }\n`;\n\n/**\n * Search, then pick one work item. The server matches the key, title and description. `exclude` hides items that are\n * already linked (the result says how many more exist, so an empty list never claims \"nothing left\" falsely).\n */\nexport function WorkItemSearchSelect({\n id,\n projectId,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n projectId?: string;\n value: string;\n onChange: (workItemId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetWorkItems: {\n items: { id: string; key: string; title: string }[];\n totalCount: number;\n };\n }>(PICKER_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { ...(deferred && { search: deferred }), ...(projectId && { projectId }) },\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetWorkItems.items ?? [];\n const items = found.filter((w) => !exclude.includes(w.id));\n const total = data?.planmagnetWorkItems.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search work items\"\n placeholder=\"Search by key or title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : items.length ? 'Choose a work item' : 'No match'}\n </option>\n {/* The current value stays selectable (and visible) when it is not in this page of results. */}\n {value && !items.some((w) => w.id === value) && (\n <option value={value}>Currently linked work item</option>\n )}\n {items.map((w) => (\n <option key={w.id} value={w.id}>\n {w.key} — {w.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_DOCS = gql`\n query PickerDocs($workspaceId: String, $filter: PlanmagnetDocFilter) {\n planmagnetDocs(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n title\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one doc. */\nexport function DocSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (docId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetDocs: { items: { id: string; title: string }[]; totalCount: number };\n }>(PICKER_DOCS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetDocs.items ?? [];\n const docs = found.filter((d) => !exclude.includes(d.id));\n const total = data?.planmagnetDocs.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search docs\"\n placeholder=\"Search docs by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : docs.length ? 'Choose a doc' : 'No match'}\n </option>\n {docs.map((d) => (\n <option key={d.id} value={d.id}>\n {d.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PROGRAMS = gql`\n query PickerPrograms($workspaceId: ID!) {\n planmagnetPrograms(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A program select. '' means \"no program\" when `noneLabel` is given. */\nexport function ProgramSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (programId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPrograms: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PROGRAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const programs = data?.planmagnetPrograms.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {programs.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading programs…'\n : error\n ? 'Programs could not be loaded'\n : 'No programs yet'}\n </option>\n )}\n {programs.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_COMPETITORS = gql`\n query PickerCompetitors($workspaceId: ID!, $filter: PlanmagnetCompetitorFilter) {\n planmagnetCompetitors(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one competitor. `exclude` hides competitors already linked. */\nexport function CompetitorSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (competitorId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetCompetitors: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_COMPETITORS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetCompetitors.items ?? [];\n const competitors = found.filter((c) => !exclude.includes(c.id));\n const total = data?.planmagnetCompetitors.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search competitors\"\n placeholder=\"Search competitors by name\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data\n ? 'Searching…'\n : competitors.length\n ? 'Choose a competitor'\n : 'No match'}\n </option>\n {competitors.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAYA,IAAM,IACJ,6FAEI,IAAkB,CAAG;;;;;;;;;;;AAa3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,cACA,aAAU,EAAE,IAOX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACpF,KAAY,GAAM,mBAAmB,SAAS,EAAE,EAAE,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC;AAC9F,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAoB,CAAG;;;;;;;;;;;;AAiB7B,SAAgB,EAAqB,EACnC,OACA,cACA,UACA,aACA,aAAU,EAAE,IAOX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAKvB,GAAmB;EACpB,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ;IAAE,GAAI,KAAY,EAAE,QAAQ,GAAU;IAAG,GAAI,KAAa,EAAE,cAAW;IAAG;GACnF;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,oBAAoB,SAAS,EAAE,EAC7C,IAAQ,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EACpD,IAAQ,GAAM,oBAAoB,cAAc;AACtD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C;KAME,kBAAC,UAAD;MAAQ,OAAM;gBACX,KAAW,CAAC,IAAO,eAAe,EAAM,SAAS,uBAAuB;MAClE,CAAA;KAER,KAAS,CAAC,EAAM,MAAM,MAAM,EAAE,OAAO,EAAM,IAC1C,kBAAC,UAAD;MAAe;gBAAO;MAAmC,CAAA;KAE1D,EAAM,KAAK,MACV,kBAAC,UAAD;MAAmB,OAAO,EAAE;gBAA5B;OACG,EAAE;OAAI;OAAI,EAAE;OACN;QAFI,EAAE,GAEN,CACT;KACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAc,CAAG;;;;;;;;;;;AAavB,SAAgB,EAAgB,EAC9B,OACA,UACA,aACA,aAAU,EAAE,IAMX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAEvB,GAAa;EACd,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ,IAAW,EAAE,QAAQ,GAAU,GAAG,KAAA;GAC3C;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,eAAe,SAAS,EAAE,EACxC,IAAO,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EACnD,IAAQ,GAAM,eAAe,cAAc;AACjD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C,CAME,kBAAC,UAAD;KAAQ,OAAM;eACX,KAAW,CAAC,IAAO,eAAe,EAAK,SAAS,iBAAiB;KAC3D,CAAA,EACR,EAAK,KAAK,MACT,kBAAC,UAAD;KAAmB,OAAO,EAAE;eACzB,EAAE;KACI,EAFI,EAAE,GAEN,CACT,CACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAkB,CAAG;;;;;;;;;;;AAa3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,gBAMC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACpF,IAAW,GAAM,mBAAmB,SAAS,EAAE;AACrD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAqB,CAAG;;;;;;;;;;;AAa9B,SAAgB,EAAuB,EACrC,OACA,UACA,aACA,aAAU,EAAE,IAMX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAEvB,GAAoB;EACrB,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ,IAAW,EAAE,QAAQ,GAAU,GAAG,KAAA;GAC3C;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,sBAAsB,SAAS,EAAE,EAC/C,IAAc,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EAC1D,IAAQ,GAAM,sBAAsB,cAAc;AACxD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C,CAME,kBAAC,UAAD;KAAQ,OAAM;eACX,KAAW,CAAC,IACT,eACA,EAAY,SACV,wBACA;KACC,CAAA,EACR,EAAY,KAAK,MAChB,kBAAC,UAAD;KAAmB,OAAO,EAAE;eACzB,EAAE;KACI,EAFI,EAAE,GAEN,CACT,CACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portfolio controls (BOFF-8047). Portfolios were `api-only` — full CRUD and the overview roll-up existed in
|
|
3
|
+
* the service (BOFF-7704, BOFF-7934) but had zero routes anywhere in the UI.
|
|
4
|
+
*/
|
|
5
|
+
export declare const PORTFOLIO_STATUSES: readonly ["ACTIVE", "ON_HOLD", "CLOSED", "ARCHIVED"];
|
|
6
|
+
/** Every StrategicAlignment value. CUSTOM requires `alignmentLabel` — the service refuses CUSTOM without one. */
|
|
7
|
+
export declare const STRATEGIC_ALIGNMENTS: readonly ["REVENUE_GROWTH", "COST_REDUCTION", "CUSTOMER_SATISFACTION", "INNOVATION", "COMPLIANCE", "OPERATIONAL_EXCELLENCE", "MARKET_EXPANSION", "RISK_MITIGATION", "CUSTOM"];
|
|
8
|
+
export declare function PortfolioHeaderActions({ portfolioId, name, status, }: {
|
|
9
|
+
portfolioId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
status: string;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=PortfolioControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PortfolioControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/PortfolioControls.tsx"],"names":[],"mappings":"AAWA;;;GAGG;AAEH,eAAO,MAAM,kBAAkB,sDAAuD,CAAC;AAEvF,iHAAiH;AACjH,eAAO,MAAM,oBAAoB,+KAUvB,CAAC;AAuBX,wBAAgB,sBAAsB,CAAC,EACrC,WAAW,EACX,IAAI,EACJ,MAAM,GACP,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,2CA+EA"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { LEAVES_PAGE_CONTEXT as t, throwOnError as n } from "../utils/mutationResult.js";
|
|
3
|
+
import { detailPath as r } from "../utils/rowNavigation.js";
|
|
4
|
+
import { ConfirmButton as i, ErrorLine as a } from "./ConfirmButton.js";
|
|
5
|
+
import { enumLabel as o } from "./WorkItemControls.js";
|
|
6
|
+
import { useState as s } from "react";
|
|
7
|
+
import { Pencil as c, Trash2 as l } from "lucide-react";
|
|
8
|
+
import { gql as u } from "@apollo/client";
|
|
9
|
+
import { useMutation as d } from "@apollo/client/react";
|
|
10
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
11
|
+
import { Button as m } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
//#region src/planmagnet/controls/PortfolioControls.tsx
|
|
13
|
+
var h = [
|
|
14
|
+
"ACTIVE",
|
|
15
|
+
"ON_HOLD",
|
|
16
|
+
"CLOSED",
|
|
17
|
+
"ARCHIVED"
|
|
18
|
+
], g = [
|
|
19
|
+
"REVENUE_GROWTH",
|
|
20
|
+
"COST_REDUCTION",
|
|
21
|
+
"CUSTOMER_SATISFACTION",
|
|
22
|
+
"INNOVATION",
|
|
23
|
+
"COMPLIANCE",
|
|
24
|
+
"OPERATIONAL_EXCELLENCE",
|
|
25
|
+
"MARKET_EXPANSION",
|
|
26
|
+
"RISK_MITIGATION",
|
|
27
|
+
"CUSTOM"
|
|
28
|
+
], _ = [
|
|
29
|
+
"ListPortfolios",
|
|
30
|
+
"GetPortfolio",
|
|
31
|
+
"GetPortfolioForEdit",
|
|
32
|
+
"GetPortfolioOverview"
|
|
33
|
+
], v = u`
|
|
34
|
+
mutation PortfolioControlsStatus($id: ID!, $input: PlanmagnetUpdatePortfolioInput!) {
|
|
35
|
+
planmagnetUpdatePortfolio(id: $id, input: $input) {
|
|
36
|
+
id
|
|
37
|
+
status
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`, y = u`
|
|
41
|
+
mutation PortfolioControlsDelete($id: ID!) {
|
|
42
|
+
planmagnetDeletePortfolio(id: $id)
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
function b({ portfolioId: u, name: g, status: b }) {
|
|
46
|
+
let x = e(), [S, C] = d(v, {
|
|
47
|
+
refetchQueries: _,
|
|
48
|
+
awaitRefetchQueries: !0
|
|
49
|
+
}), [w, T] = d(y, {
|
|
50
|
+
refetchQueries: _.filter((e) => e !== "GetPortfolio" && e !== "GetPortfolioOverview"),
|
|
51
|
+
context: t
|
|
52
|
+
}), [E, D] = s(null), O = C.loading || T.loading, k = h.includes(b) ? h : [b, ...h], A = async (e) => {
|
|
53
|
+
D(null);
|
|
54
|
+
try {
|
|
55
|
+
await n(S({ variables: {
|
|
56
|
+
id: u,
|
|
57
|
+
input: { status: e }
|
|
58
|
+
} }));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
D(e instanceof Error ? e.message : "The status could not be changed.");
|
|
61
|
+
}
|
|
62
|
+
}, j = async () => {
|
|
63
|
+
D(null);
|
|
64
|
+
try {
|
|
65
|
+
await n(w({ variables: { id: u } })), x("/products/portfolios-page");
|
|
66
|
+
} catch (e) {
|
|
67
|
+
D(e instanceof Error ? e.message : "The portfolio could not be deleted.");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return /* @__PURE__ */ p("div", {
|
|
71
|
+
className: "flex flex-col items-end gap-2",
|
|
72
|
+
children: [/* @__PURE__ */ p("div", {
|
|
73
|
+
className: "flex flex-wrap items-center justify-end gap-2",
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ f("select", {
|
|
76
|
+
"aria-label": "Portfolio status",
|
|
77
|
+
"data-testid": "portfolio-status",
|
|
78
|
+
className: "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60",
|
|
79
|
+
value: b,
|
|
80
|
+
disabled: O,
|
|
81
|
+
onChange: (e) => {
|
|
82
|
+
e.target.value !== b && A(e.target.value);
|
|
83
|
+
},
|
|
84
|
+
children: k.map((e) => /* @__PURE__ */ f("option", {
|
|
85
|
+
value: e,
|
|
86
|
+
children: o(e)
|
|
87
|
+
}, e))
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ p(m, {
|
|
90
|
+
size: "sm",
|
|
91
|
+
variant: "outline",
|
|
92
|
+
"data-testid": "portfolio-edit",
|
|
93
|
+
onClick: () => x(r("/products/edit-portfolio-page", u)),
|
|
94
|
+
children: [/* @__PURE__ */ f(c, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ f(i, {
|
|
97
|
+
label: "Delete",
|
|
98
|
+
variant: "outline",
|
|
99
|
+
icon: /* @__PURE__ */ f(l, { className: "mr-1 h-4 w-4" }),
|
|
100
|
+
title: `Delete portfolio ${g}?`,
|
|
101
|
+
description: "The portfolio is deleted. Programs and projects it groups are not deleted. This cannot be undone here.",
|
|
102
|
+
confirmLabel: "Delete portfolio",
|
|
103
|
+
destructive: !0,
|
|
104
|
+
disabled: O,
|
|
105
|
+
onConfirm: () => void j()
|
|
106
|
+
})
|
|
107
|
+
]
|
|
108
|
+
}), E && /* @__PURE__ */ f(a, { message: E })]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { h as PORTFOLIO_STATUSES, b as PortfolioHeaderActions, g as STRATEGIC_ALIGNMENTS };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=PortfolioControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PortfolioControls.js","names":[],"sources":["../../../src/planmagnet/controls/PortfolioControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { enumLabel } from './WorkItemControls';\n\n/**\n * Portfolio controls (BOFF-8047). Portfolios were `api-only` — full CRUD and the overview roll-up existed in\n * the service (BOFF-7704, BOFF-7934) but had zero routes anywhere in the UI.\n */\n\nexport const PORTFOLIO_STATUSES = ['ACTIVE', 'ON_HOLD', 'CLOSED', 'ARCHIVED'] as const;\n\n/** Every StrategicAlignment value. CUSTOM requires `alignmentLabel` — the service refuses CUSTOM without one. */\nexport const STRATEGIC_ALIGNMENTS = [\n 'REVENUE_GROWTH',\n 'COST_REDUCTION',\n 'CUSTOMER_SATISFACTION',\n 'INNOVATION',\n 'COMPLIANCE',\n 'OPERATIONAL_EXCELLENCE',\n 'MARKET_EXPANSION',\n 'RISK_MITIGATION',\n 'CUSTOM',\n] as const;\n\nconst PORTFOLIO_QUERIES = [\n 'ListPortfolios',\n 'GetPortfolio',\n 'GetPortfolioForEdit',\n 'GetPortfolioOverview',\n];\n\nconst UPDATE_STATUS = gql`\n mutation PortfolioControlsStatus($id: ID!, $input: PlanmagnetUpdatePortfolioInput!) {\n planmagnetUpdatePortfolio(id: $id, input: $input) {\n id\n status\n }\n }\n`;\nconst DELETE = gql`\n mutation PortfolioControlsDelete($id: ID!) {\n planmagnetDeletePortfolio(id: $id)\n }\n`;\n\nexport function PortfolioHeaderActions({\n portfolioId,\n name,\n status,\n}: {\n portfolioId: string;\n name: string;\n status: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [updateStatus, statusState] = useMutation(UPDATE_STATUS, {\n refetchQueries: PORTFOLIO_QUERIES,\n awaitRefetchQueries: true,\n });\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: PORTFOLIO_QUERIES.filter(\n (q) => q !== 'GetPortfolio' && q !== 'GetPortfolioOverview'\n ),\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = statusState.loading || removeState.loading;\n const statuses = (PORTFOLIO_STATUSES as readonly string[]).includes(status)\n ? PORTFOLIO_STATUSES\n : [status, ...PORTFOLIO_STATUSES];\n\n const onStatus = async (next: string) => {\n setError(null);\n try {\n await throwOnError(updateStatus({ variables: { id: portfolioId, input: { status: next } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: portfolioId } }));\n navigate('/products/portfolios-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The portfolio could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Portfolio status\"\n data-testid=\"portfolio-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"portfolio-edit\"\n onClick={() => navigate(detailPath('/products/edit-portfolio-page', portfolioId))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete portfolio ${name}?`}\n description=\"The portfolio is deleted. Programs and projects it groups are not deleted. This cannot be undone here.\"\n confirmLabel=\"Delete portfolio\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,IAAa,IAAqB;CAAC;CAAU;CAAW;CAAU;CAAW,EAGhE,IAAuB;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,IAAoB;CACxB;CACA;CACA;CACA;CACD,EAEK,IAAgB,CAAG;;;;;;;GAQnB,IAAS,CAAG;;;;;AAMlB,SAAgB,EAAuB,EACrC,gBACA,SACA,aAKC;CACD,IAAM,IAAW,GAAuB,EAClC,CAAC,GAAc,KAAe,EAAY,GAAe;EAC7D,gBAAgB;EAChB,qBAAqB;EACtB,CAAC,EACI,CAAC,GAAQ,KAAe,EAAY,GAAQ;EAChD,gBAAgB,EAAkB,QAC/B,MAAM,MAAM,kBAAkB,MAAM,uBACtC;EACD,SAAS;EACV,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAO,EAAY,WAAW,EAAY,SAC1C,IAAY,EAAyC,SAAS,EAAO,GACvE,IACA,CAAC,GAAQ,GAAG,EAAmB,EAE7B,IAAW,OAAO,MAAiB;AACvC,IAAS,KAAK;AACd,MAAI;AACF,SAAM,EAAa,EAAa,EAAE,WAAW;IAAE,IAAI;IAAa,OAAO,EAAE,QAAQ,GAAM;IAAE,EAAE,CAAC,CAAC;WACtF,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,mCAAmC;;IAG3E,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AAEF,GADA,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,GAAa,EAAE,CAAC,CAAC,EAC9D,EAAS,4BAA4B;WAC9B,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,sCAAsC;;;AAIpF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,UAAD;KACE,cAAW;KACX,eAAY;KACZ,WAAU;KACV,OAAO;KACP,UAAU;KACV,WAAW,MAAM;AACf,MAAI,EAAE,OAAO,UAAU,KAAa,EAAS,EAAE,OAAO,MAAM;;eAG7D,EAAS,KAAK,MACb,kBAAC,UAAD;MAAgB,OAAO;gBACpB,EAAU,EAAE;MACN,EAFI,EAEJ,CACT;KACK,CAAA;IACT,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAY;KACZ,eAAe,EAAS,EAAW,iCAAiC,EAAY,CAAC;eAJnF,CAME,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,OAE5B;;IACT,kBAAC,GAAD;KACE,OAAM;KACN,SAAQ;KACR,MAAM,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA;KACzC,OAAO,oBAAoB,EAAK;KAChC,aAAY;KACZ,cAAa;KACb,aAAA;KACA,UAAU;KACV,iBAAiB,KAAK,GAAU;KAChC,CAAA;IACE;MACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Program controls (BOFF-8047). Programs were `api-only` — full CRUD and project attachment existed in the
|
|
3
|
+
* service (BOFF-7704) but had zero routes anywhere in the UI. `Program.status` reuses `PlanmagnetProjectStatus`
|
|
4
|
+
* (the SDL declares it that way), so the status list is the same as a project's.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PROGRAM_STATUSES: readonly ["PLANNING", "ACTIVE", "ON_HOLD", "COMPLETED", "CANCELLED", "ARCHIVED"];
|
|
7
|
+
export declare function ProgramHeaderActions({ programId, name, status, }: {
|
|
8
|
+
programId: string;
|
|
9
|
+
name: string;
|
|
10
|
+
status: string;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* Add / remove projects on a program. `Program.projects` reads `Project.programId` (BOFF-7704); the only
|
|
14
|
+
* mutation that writes it is `planmagnetUpdateProject`, so attaching happens from the project side even
|
|
15
|
+
* though this card lives on the program.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ProgramProjectsCard({ programId, projects, }: {
|
|
18
|
+
programId: string;
|
|
19
|
+
projects: {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
status: string;
|
|
23
|
+
}[];
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=ProgramControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProgramControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/ProgramControls.tsx"],"names":[],"mappings":"AAaA;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,kFAAmB,CAAC;AAkBjD,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,IAAI,EACJ,MAAM,GACP,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,2CA6EA;AAWD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,QAAQ,GACT,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1D,2CA+EA"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { LEAVES_PAGE_CONTEXT as t, throwOnError as n } from "../utils/mutationResult.js";
|
|
3
|
+
import { detailPath as r } from "../utils/rowNavigation.js";
|
|
4
|
+
import { ConfirmButton as i, ErrorLine as a } from "./ConfirmButton.js";
|
|
5
|
+
import { ProjectSelect as o } from "./Pickers.js";
|
|
6
|
+
import { enumLabel as s } from "./WorkItemControls.js";
|
|
7
|
+
import { PROJECT_STATUSES as c } from "./ProjectControls.js";
|
|
8
|
+
import { useState as l } from "react";
|
|
9
|
+
import { Pencil as u, Trash2 as d, X as f } from "lucide-react";
|
|
10
|
+
import { gql as p } from "@apollo/client";
|
|
11
|
+
import { useMutation as m } from "@apollo/client/react";
|
|
12
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
13
|
+
import { Button as _ } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/planmagnet/controls/ProgramControls.tsx
|
|
15
|
+
var v = c, y = [
|
|
16
|
+
"ListPrograms",
|
|
17
|
+
"GetProgram",
|
|
18
|
+
"GetProgramForEdit"
|
|
19
|
+
], b = p`
|
|
20
|
+
mutation ProgramControlsStatus($id: ID!, $input: PlanmagnetUpdateProgramInput!) {
|
|
21
|
+
planmagnetUpdateProgram(id: $id, input: $input) {
|
|
22
|
+
id
|
|
23
|
+
status
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`, x = p`
|
|
27
|
+
mutation ProgramControlsDelete($id: ID!) {
|
|
28
|
+
planmagnetDeleteProgram(id: $id)
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
function S({ programId: o, name: c, status: f }) {
|
|
32
|
+
let p = e(), [S, C] = m(b, {
|
|
33
|
+
refetchQueries: y,
|
|
34
|
+
awaitRefetchQueries: !0
|
|
35
|
+
}), [w, T] = m(x, {
|
|
36
|
+
refetchQueries: y.filter((e) => e !== "GetProgram"),
|
|
37
|
+
context: t
|
|
38
|
+
}), [E, D] = l(null), O = C.loading || T.loading, k = v.includes(f) ? v : [f, ...v], A = async (e) => {
|
|
39
|
+
D(null);
|
|
40
|
+
try {
|
|
41
|
+
await n(S({ variables: {
|
|
42
|
+
id: o,
|
|
43
|
+
input: { status: e }
|
|
44
|
+
} }));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
D(e instanceof Error ? e.message : "The status could not be changed.");
|
|
47
|
+
}
|
|
48
|
+
}, j = async () => {
|
|
49
|
+
D(null);
|
|
50
|
+
try {
|
|
51
|
+
await n(w({ variables: { id: o } })), p("/products/programs-page");
|
|
52
|
+
} catch (e) {
|
|
53
|
+
D(e instanceof Error ? e.message : "The program could not be deleted.");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return /* @__PURE__ */ g("div", {
|
|
57
|
+
className: "flex flex-col items-end gap-2",
|
|
58
|
+
children: [/* @__PURE__ */ g("div", {
|
|
59
|
+
className: "flex flex-wrap items-center justify-end gap-2",
|
|
60
|
+
children: [
|
|
61
|
+
/* @__PURE__ */ h("select", {
|
|
62
|
+
"aria-label": "Program status",
|
|
63
|
+
"data-testid": "program-status",
|
|
64
|
+
className: "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60",
|
|
65
|
+
value: f,
|
|
66
|
+
disabled: O,
|
|
67
|
+
onChange: (e) => {
|
|
68
|
+
e.target.value !== f && A(e.target.value);
|
|
69
|
+
},
|
|
70
|
+
children: k.map((e) => /* @__PURE__ */ h("option", {
|
|
71
|
+
value: e,
|
|
72
|
+
children: s(e)
|
|
73
|
+
}, e))
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ g(_, {
|
|
76
|
+
size: "sm",
|
|
77
|
+
variant: "outline",
|
|
78
|
+
"data-testid": "program-edit",
|
|
79
|
+
onClick: () => p(r("/products/edit-program-page", o)),
|
|
80
|
+
children: [/* @__PURE__ */ h(u, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
81
|
+
}),
|
|
82
|
+
/* @__PURE__ */ h(i, {
|
|
83
|
+
label: "Delete",
|
|
84
|
+
variant: "outline",
|
|
85
|
+
icon: /* @__PURE__ */ h(d, { className: "mr-1 h-4 w-4" }),
|
|
86
|
+
title: `Delete program ${c}?`,
|
|
87
|
+
description: "The program is deleted. Its projects are not deleted; they are simply no longer grouped under it. This cannot be undone here.",
|
|
88
|
+
confirmLabel: "Delete program",
|
|
89
|
+
destructive: !0,
|
|
90
|
+
disabled: O,
|
|
91
|
+
onConfirm: () => void j()
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
}), E && /* @__PURE__ */ h(a, { message: E })]
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
var C = p`
|
|
98
|
+
mutation ProgramControlsAttachProject($id: ID!, $input: PlanmagnetUpdateProjectInput!) {
|
|
99
|
+
planmagnetUpdateProject(id: $id, input: $input) {
|
|
100
|
+
id
|
|
101
|
+
programId
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
function w({ programId: t, projects: i }) {
|
|
106
|
+
let c = e(), [u, d] = l(""), [p, v] = m(C, {
|
|
107
|
+
refetchQueries: [...y, "GetProjectDetail"],
|
|
108
|
+
awaitRefetchQueries: !0
|
|
109
|
+
}), [b, x] = l(null), S = v.loading, w = async () => {
|
|
110
|
+
if (u) {
|
|
111
|
+
x(null);
|
|
112
|
+
try {
|
|
113
|
+
await n(p({ variables: {
|
|
114
|
+
id: u,
|
|
115
|
+
input: { programId: t }
|
|
116
|
+
} })), d("");
|
|
117
|
+
} catch (e) {
|
|
118
|
+
x(e instanceof Error ? e.message : "The project could not be attached.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, T = async (e) => {
|
|
122
|
+
x(null);
|
|
123
|
+
try {
|
|
124
|
+
await n(p({ variables: {
|
|
125
|
+
id: e,
|
|
126
|
+
input: { programId: null }
|
|
127
|
+
} }));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
x(e instanceof Error ? e.message : "The project could not be removed.");
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return /* @__PURE__ */ g("div", {
|
|
133
|
+
className: "space-y-3",
|
|
134
|
+
children: [
|
|
135
|
+
i.length === 0 && /* @__PURE__ */ h("p", {
|
|
136
|
+
className: "text-sm text-muted-foreground",
|
|
137
|
+
children: "No projects are attached to this program yet."
|
|
138
|
+
}),
|
|
139
|
+
i.map((e) => /* @__PURE__ */ g("div", {
|
|
140
|
+
className: "flex items-center justify-between gap-2 rounded-md border p-3",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ h("button", {
|
|
143
|
+
type: "button",
|
|
144
|
+
className: "flex-1 text-left text-sm font-medium hover:underline underline-offset-2",
|
|
145
|
+
onClick: () => c(r("/projects/project-detail-page", e.id)),
|
|
146
|
+
children: e.name
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ h("span", {
|
|
149
|
+
className: "text-xs text-muted-foreground capitalize",
|
|
150
|
+
children: s(e.status)
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ h(_, {
|
|
153
|
+
size: "sm",
|
|
154
|
+
variant: "ghost",
|
|
155
|
+
"aria-label": `Remove ${e.name} from this program`,
|
|
156
|
+
disabled: S,
|
|
157
|
+
onClick: () => void T(e.id),
|
|
158
|
+
children: /* @__PURE__ */ h(f, { className: "h-4 w-4" })
|
|
159
|
+
})
|
|
160
|
+
]
|
|
161
|
+
}, e.id)),
|
|
162
|
+
/* @__PURE__ */ g("div", {
|
|
163
|
+
className: "flex items-center gap-2 pt-2 border-t",
|
|
164
|
+
children: [/* @__PURE__ */ h("div", {
|
|
165
|
+
className: "flex-1",
|
|
166
|
+
children: /* @__PURE__ */ h(o, {
|
|
167
|
+
id: "program-add-project",
|
|
168
|
+
value: u,
|
|
169
|
+
onChange: d,
|
|
170
|
+
noneLabel: "Choose a project to attach",
|
|
171
|
+
exclude: i.map((e) => e.id)
|
|
172
|
+
})
|
|
173
|
+
}), /* @__PURE__ */ h(_, {
|
|
174
|
+
size: "sm",
|
|
175
|
+
disabled: !u || S,
|
|
176
|
+
onClick: () => void w(),
|
|
177
|
+
children: "Attach"
|
|
178
|
+
})]
|
|
179
|
+
}),
|
|
180
|
+
b && /* @__PURE__ */ h(a, { message: b })
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { v as PROGRAM_STATUSES, S as ProgramHeaderActions, w as ProgramProjectsCard };
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=ProgramControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProgramControls.js","names":[],"sources":["../../../src/planmagnet/controls/ProgramControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2, X } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { enumLabel } from './WorkItemControls';\nimport { PROJECT_STATUSES } from './ProjectControls';\nimport { ProjectSelect } from './Pickers';\n\n/**\n * Program controls (BOFF-8047). Programs were `api-only` — full CRUD and project attachment existed in the\n * service (BOFF-7704) but had zero routes anywhere in the UI. `Program.status` reuses `PlanmagnetProjectStatus`\n * (the SDL declares it that way), so the status list is the same as a project's.\n */\nexport const PROGRAM_STATUSES = PROJECT_STATUSES;\n\nconst PROGRAM_QUERIES = ['ListPrograms', 'GetProgram', 'GetProgramForEdit'];\n\nconst UPDATE_STATUS = gql`\n mutation ProgramControlsStatus($id: ID!, $input: PlanmagnetUpdateProgramInput!) {\n planmagnetUpdateProgram(id: $id, input: $input) {\n id\n status\n }\n }\n`;\nconst DELETE = gql`\n mutation ProgramControlsDelete($id: ID!) {\n planmagnetDeleteProgram(id: $id)\n }\n`;\n\nexport function ProgramHeaderActions({\n programId,\n name,\n status,\n}: {\n programId: string;\n name: string;\n status: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [updateStatus, statusState] = useMutation(UPDATE_STATUS, {\n refetchQueries: PROGRAM_QUERIES,\n awaitRefetchQueries: true,\n });\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: PROGRAM_QUERIES.filter((q) => q !== 'GetProgram'),\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = statusState.loading || removeState.loading;\n const statuses = (PROGRAM_STATUSES as readonly string[]).includes(status)\n ? PROGRAM_STATUSES\n : [status, ...PROGRAM_STATUSES];\n\n const onStatus = async (next: string) => {\n setError(null);\n try {\n await throwOnError(updateStatus({ variables: { id: programId, input: { status: next } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: programId } }));\n navigate('/products/programs-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The program could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Program status\"\n data-testid=\"program-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"program-edit\"\n onClick={() => navigate(detailPath('/products/edit-program-page', programId))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete program ${name}?`}\n description=\"The program is deleted. Its projects are not deleted; they are simply no longer grouped under it. This cannot be undone here.\"\n confirmLabel=\"Delete program\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst ATTACH_PROJECT = gql`\n mutation ProgramControlsAttachProject($id: ID!, $input: PlanmagnetUpdateProjectInput!) {\n planmagnetUpdateProject(id: $id, input: $input) {\n id\n programId\n }\n }\n`;\n\n/**\n * Add / remove projects on a program. `Program.projects` reads `Project.programId` (BOFF-7704); the only\n * mutation that writes it is `planmagnetUpdateProject`, so attaching happens from the project side even\n * though this card lives on the program.\n */\nexport function ProgramProjectsCard({\n programId,\n projects,\n}: {\n programId: string;\n projects: { id: string; name: string; status: string }[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [addProjectId, setAddProjectId] = useState('');\n const [setProgram, setProgramState] = useMutation(ATTACH_PROJECT, {\n refetchQueries: [...PROGRAM_QUERIES, 'GetProjectDetail'],\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = setProgramState.loading;\n\n const onAttach = async () => {\n if (!addProjectId) return;\n setError(null);\n try {\n await throwOnError(setProgram({ variables: { id: addProjectId, input: { programId } } }));\n setAddProjectId('');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The project could not be attached.');\n }\n };\n const onDetach = async (projectId: string) => {\n setError(null);\n try {\n await throwOnError(setProgram({ variables: { id: projectId, input: { programId: null } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The project could not be removed.');\n }\n };\n\n return (\n <div className=\"space-y-3\">\n {projects.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n No projects are attached to this program yet.\n </p>\n )}\n {projects.map((project) => (\n <div\n key={project.id}\n className=\"flex items-center justify-between gap-2 rounded-md border p-3\"\n >\n <button\n type=\"button\"\n className=\"flex-1 text-left text-sm font-medium hover:underline underline-offset-2\"\n onClick={() => navigate(detailPath('/projects/project-detail-page', project.id))}\n >\n {project.name}\n </button>\n <span className=\"text-xs text-muted-foreground capitalize\">\n {enumLabel(project.status)}\n </span>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${project.name} from this program`}\n disabled={busy}\n onClick={() => void onDetach(project.id)}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n <div className=\"flex items-center gap-2 pt-2 border-t\">\n <div className=\"flex-1\">\n <ProjectSelect\n id=\"program-add-project\"\n value={addProjectId}\n onChange={setAddProjectId}\n noneLabel=\"Choose a project to attach\"\n exclude={projects.map((p) => p.id)}\n />\n </div>\n <Button size=\"sm\" disabled={!addProjectId || busy} onClick={() => void onAttach()}>\n Attach\n </Button>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CompetitorsCompetitorDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/CompetitorsCompetitorDetailPagePage.tsx"],"names":[],"mappings":"AAoCA,wBAAgB,mCAAmC,4CA0HlD"}
|