@burdenoff/microfe-planmagnet 2026.927.6 → 2026.928.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +58 -54
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/board/ProjectBoard.d.ts.map +1 -1
- package/dist/planmagnet/board/ProjectBoard.js +313 -306
- package/dist/planmagnet/board/ProjectBoard.js.map +1 -1
- package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/ProjectControls.js +47 -40
- package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.d.ts +9 -0
- package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.js +87 -59
- package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
- package/dist/planmagnet/hooks/useProjectWorkflow.d.ts +32 -0
- package/dist/planmagnet/hooks/useProjectWorkflow.d.ts.map +1 -0
- package/dist/planmagnet/hooks/useProjectWorkflow.js +44 -0
- package/dist/planmagnet/hooks/useProjectWorkflow.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +4 -4
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsWorkflowPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsWorkflowPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsWorkflowPagePage.js +261 -0
- package/dist/planmagnet/pages/ProjectsWorkflowPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +8 -9
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,15 +2,16 @@ import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/Pla
|
|
|
2
2
|
import { usePersonProfile as n } from "../people/People.js";
|
|
3
3
|
import { LEAVES_PAGE_CONTEXT as r, throwOnError as i } from "../utils/mutationResult.js";
|
|
4
4
|
import { ConfirmButton as a, ErrorLine as o } from "./ConfirmButton.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
5
|
+
import { allowedTargets as s, useProjectWorkflow as c, workflowLabel as l } from "../hooks/useProjectWorkflow.js";
|
|
6
|
+
import { useState as u } from "react";
|
|
7
|
+
import { Pencil as d, Trash2 as f } from "lucide-react";
|
|
8
|
+
import { gql as p } from "@apollo/client";
|
|
9
|
+
import { useMutation as m } from "@apollo/client/react";
|
|
10
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
11
|
+
import { Button as _ } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { useWorkspaceDirectory as v } from "@burdenoff/fe-libs/sharing";
|
|
12
13
|
//#region src/planmagnet/controls/WorkItemControls.tsx
|
|
13
|
-
var
|
|
14
|
+
var y = [
|
|
14
15
|
"BACKLOG",
|
|
15
16
|
"TODO",
|
|
16
17
|
"IN_PROGRESS",
|
|
@@ -18,18 +19,18 @@ var g = [
|
|
|
18
19
|
"TESTING",
|
|
19
20
|
"DONE",
|
|
20
21
|
"CANCELLED"
|
|
21
|
-
],
|
|
22
|
+
], b = [
|
|
22
23
|
"NONE",
|
|
23
24
|
"LOW",
|
|
24
25
|
"MEDIUM",
|
|
25
26
|
"HIGH",
|
|
26
27
|
"CRITICAL"
|
|
27
28
|
];
|
|
28
|
-
function
|
|
29
|
+
function x(e) {
|
|
29
30
|
let t = e.toLowerCase().replace(/_/g, " ");
|
|
30
31
|
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
31
32
|
}
|
|
32
|
-
var
|
|
33
|
+
var S = p`
|
|
33
34
|
mutation WorkItemControlsUpdate($id: ID!, $input: PlanmagnetUpdateWorkItemInput!) {
|
|
34
35
|
planmagnetUpdateWorkItem(id: $id, input: $input) {
|
|
35
36
|
id
|
|
@@ -38,25 +39,25 @@ var y = u`
|
|
|
38
39
|
assigneeId
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
`,
|
|
42
|
+
`, C = p`
|
|
42
43
|
mutation WorkItemControlsDelete($id: ID!) {
|
|
43
44
|
planmagnetDeleteWorkItem(id: $id)
|
|
44
45
|
}
|
|
45
|
-
`,
|
|
46
|
+
`, w = [
|
|
46
47
|
"GetWorkItems",
|
|
47
48
|
"GetWorkItemCounts",
|
|
48
49
|
"ProjectBoard",
|
|
49
50
|
"GetDashboardData"
|
|
50
|
-
],
|
|
51
|
+
], T = [
|
|
51
52
|
"GetWorkItemDetail",
|
|
52
53
|
"WorkItemActivity",
|
|
53
|
-
...
|
|
54
|
-
],
|
|
55
|
-
function
|
|
56
|
-
let [t, n] =
|
|
57
|
-
refetchQueries:
|
|
54
|
+
...w
|
|
55
|
+
], E = "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60 max-w-[12rem]";
|
|
56
|
+
function D(e) {
|
|
57
|
+
let [t, n] = m(S, {
|
|
58
|
+
refetchQueries: T,
|
|
58
59
|
awaitRefetchQueries: !0
|
|
59
|
-
}), [r, a] =
|
|
60
|
+
}), [r, a] = u(null);
|
|
60
61
|
return {
|
|
61
62
|
save: async (n) => {
|
|
62
63
|
a(null);
|
|
@@ -73,28 +74,55 @@ function w(e) {
|
|
|
73
74
|
error: r
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
|
-
function
|
|
77
|
-
let { save: a, saving: s, error: c } =
|
|
78
|
-
return /* @__PURE__ */
|
|
77
|
+
function O({ workItemId: e, field: t, value: n, options: r, label: i }) {
|
|
78
|
+
let { save: a, saving: s, error: c } = D(e), l = r.includes(n) ? r : [n, ...r];
|
|
79
|
+
return /* @__PURE__ */ g("div", {
|
|
79
80
|
className: "flex flex-col items-end gap-1",
|
|
80
|
-
children: [/* @__PURE__ */
|
|
81
|
+
children: [/* @__PURE__ */ h("select", {
|
|
81
82
|
"aria-label": i,
|
|
82
83
|
"data-testid": `task-${t}`,
|
|
83
|
-
className:
|
|
84
|
+
className: E,
|
|
84
85
|
value: n,
|
|
85
86
|
disabled: s,
|
|
86
87
|
onChange: (e) => {
|
|
87
88
|
e.target.value !== n && a({ [t]: e.target.value });
|
|
88
89
|
},
|
|
89
|
-
children: l.map((e) => /* @__PURE__ */
|
|
90
|
+
children: l.map((e) => /* @__PURE__ */ h("option", {
|
|
90
91
|
value: e,
|
|
91
|
-
children:
|
|
92
|
+
children: x(e)
|
|
92
93
|
}, e))
|
|
93
|
-
}), c && /* @__PURE__ */
|
|
94
|
+
}), c && /* @__PURE__ */ h(o, { message: c })]
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
|
-
function
|
|
97
|
-
let
|
|
97
|
+
function k({ workItemId: e, projectId: t, value: n }) {
|
|
98
|
+
let r = c(t), { save: i, saving: a, error: u } = D(e);
|
|
99
|
+
return /* @__PURE__ */ g("div", {
|
|
100
|
+
className: "flex flex-col items-end gap-1",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ h("select", {
|
|
103
|
+
"aria-label": "Status",
|
|
104
|
+
"data-testid": "task-status",
|
|
105
|
+
className: E,
|
|
106
|
+
value: n,
|
|
107
|
+
disabled: a,
|
|
108
|
+
onChange: (e) => {
|
|
109
|
+
e.target.value !== n && i({ status: e.target.value });
|
|
110
|
+
},
|
|
111
|
+
children: s(r, n, y).map((e) => /* @__PURE__ */ h("option", {
|
|
112
|
+
value: e,
|
|
113
|
+
children: l(r, e)
|
|
114
|
+
}, e))
|
|
115
|
+
}),
|
|
116
|
+
r?.enforceTransitions && /* @__PURE__ */ h("span", {
|
|
117
|
+
className: "text-[11px] text-muted-foreground",
|
|
118
|
+
children: "Only the moves this project allows"
|
|
119
|
+
}),
|
|
120
|
+
u && /* @__PURE__ */ h(o, { message: u })
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function A(t) {
|
|
125
|
+
let { workspaceId: r, authToken: i, apiGatewayUrl: a } = e(), o = v({
|
|
98
126
|
workspaceId: r ?? "",
|
|
99
127
|
authToken: i ?? void 0,
|
|
100
128
|
apiGatewayUrl: a ?? void 0
|
|
@@ -112,32 +140,32 @@ function E(t) {
|
|
|
112
140
|
fetchMore: o.fetchMore
|
|
113
141
|
};
|
|
114
142
|
}
|
|
115
|
-
function
|
|
116
|
-
let { options: i, loading: a, hasMore: o, fetchMore: s } =
|
|
117
|
-
return /* @__PURE__ */
|
|
143
|
+
function j({ id: e, value: t, onChange: n, disabled: r = !1 }) {
|
|
144
|
+
let { options: i, loading: a, hasMore: o, fetchMore: s } = A(t || null);
|
|
145
|
+
return /* @__PURE__ */ g("div", {
|
|
118
146
|
className: "flex flex-col gap-1",
|
|
119
147
|
children: [
|
|
120
|
-
/* @__PURE__ */
|
|
148
|
+
/* @__PURE__ */ g("select", {
|
|
121
149
|
id: e,
|
|
122
150
|
"aria-label": e ? void 0 : "Assignee",
|
|
123
151
|
"data-testid": "assignee-select",
|
|
124
|
-
className:
|
|
152
|
+
className: E,
|
|
125
153
|
value: t,
|
|
126
154
|
disabled: r,
|
|
127
155
|
onChange: (e) => n(e.target.value),
|
|
128
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ h("option", {
|
|
129
157
|
value: "",
|
|
130
158
|
children: "Unassigned"
|
|
131
|
-
}), i.map((e) => /* @__PURE__ */
|
|
159
|
+
}), i.map((e) => /* @__PURE__ */ h("option", {
|
|
132
160
|
value: e.id,
|
|
133
161
|
children: e.name
|
|
134
162
|
}, e.id))]
|
|
135
163
|
}),
|
|
136
|
-
a && i.length === 0 && /* @__PURE__ */
|
|
164
|
+
a && i.length === 0 && /* @__PURE__ */ h("span", {
|
|
137
165
|
className: "text-xs text-muted-foreground",
|
|
138
166
|
children: "Loading members…"
|
|
139
167
|
}),
|
|
140
|
-
o && /* @__PURE__ */
|
|
168
|
+
o && /* @__PURE__ */ h("button", {
|
|
141
169
|
type: "button",
|
|
142
170
|
className: "self-start text-xs text-primary hover:underline",
|
|
143
171
|
onClick: s,
|
|
@@ -146,56 +174,56 @@ function D({ id: e, value: t, onChange: n, disabled: r = !1 }) {
|
|
|
146
174
|
]
|
|
147
175
|
});
|
|
148
176
|
}
|
|
149
|
-
function
|
|
150
|
-
let { save: n, saving: r, error: i } =
|
|
151
|
-
return /* @__PURE__ */
|
|
177
|
+
function M({ workItemId: e, assigneeId: t }) {
|
|
178
|
+
let { save: n, saving: r, error: i } = D(e);
|
|
179
|
+
return /* @__PURE__ */ g("div", {
|
|
152
180
|
className: "flex flex-col items-end gap-1",
|
|
153
|
-
children: [/* @__PURE__ */
|
|
181
|
+
children: [/* @__PURE__ */ h(j, {
|
|
154
182
|
value: t ?? "",
|
|
155
183
|
disabled: r,
|
|
156
184
|
onChange: (e) => {
|
|
157
185
|
e !== (t ?? "") && n({ assigneeId: e || null });
|
|
158
186
|
}
|
|
159
|
-
}), i && /* @__PURE__ */
|
|
187
|
+
}), i && /* @__PURE__ */ h(o, { message: i })]
|
|
160
188
|
});
|
|
161
189
|
}
|
|
162
|
-
function
|
|
163
|
-
let
|
|
164
|
-
refetchQueries:
|
|
190
|
+
function N({ workItemId: e, title: n }) {
|
|
191
|
+
let s = t(), [c, l] = m(C, {
|
|
192
|
+
refetchQueries: w,
|
|
165
193
|
context: r
|
|
166
|
-
}), [
|
|
194
|
+
}), [p, v] = u(null), y = async () => {
|
|
167
195
|
v(null);
|
|
168
196
|
try {
|
|
169
|
-
await i(
|
|
197
|
+
await i(c({ variables: { id: e } })), s("/tasks/tasks-page");
|
|
170
198
|
} catch (e) {
|
|
171
199
|
v(e instanceof Error ? e.message : "The task could not be deleted.");
|
|
172
200
|
}
|
|
173
201
|
};
|
|
174
|
-
return /* @__PURE__ */
|
|
202
|
+
return /* @__PURE__ */ g("div", {
|
|
175
203
|
className: "flex flex-col items-end gap-2",
|
|
176
|
-
children: [/* @__PURE__ */
|
|
204
|
+
children: [/* @__PURE__ */ g("div", {
|
|
177
205
|
className: "flex gap-2",
|
|
178
|
-
children: [/* @__PURE__ */
|
|
206
|
+
children: [/* @__PURE__ */ g(_, {
|
|
179
207
|
size: "sm",
|
|
180
208
|
variant: "outline",
|
|
181
209
|
"data-testid": "task-edit",
|
|
182
|
-
onClick: () =>
|
|
183
|
-
children: [/* @__PURE__ */
|
|
184
|
-
}), /* @__PURE__ */
|
|
210
|
+
onClick: () => s(`/tasks/edit-task-page?taskId=${encodeURIComponent(e)}`),
|
|
211
|
+
children: [/* @__PURE__ */ h(d, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
212
|
+
}), /* @__PURE__ */ h(a, {
|
|
185
213
|
label: "Delete",
|
|
186
214
|
variant: "outline",
|
|
187
|
-
icon: /* @__PURE__ */ f
|
|
215
|
+
icon: /* @__PURE__ */ h(f, { className: "mr-1 h-4 w-4" }),
|
|
188
216
|
title: `Delete "${n}"?`,
|
|
189
217
|
description: "The task is deleted and disappears from boards, sprints and releases. This cannot be undone here.",
|
|
190
218
|
confirmLabel: "Delete task",
|
|
191
219
|
destructive: !0,
|
|
192
|
-
disabled:
|
|
220
|
+
disabled: l.loading,
|
|
193
221
|
onConfirm: () => void y()
|
|
194
222
|
})]
|
|
195
|
-
}),
|
|
223
|
+
}), p && /* @__PURE__ */ h(o, { message: p })]
|
|
196
224
|
});
|
|
197
225
|
}
|
|
198
226
|
//#endregion
|
|
199
|
-
export {
|
|
227
|
+
export { j as AssigneeSelect, b as WORK_ITEM_PRIORITIES, M as WorkItemAssigneeSelect, O as WorkItemEnumSelect, N as WorkItemHeaderActions, k as WorkItemStatusSelect, x as enumLabel };
|
|
200
228
|
|
|
201
229
|
//# sourceMappingURL=WorkItemControls.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkItemControls.js","names":[],"sources":["../../../src/planmagnet/controls/WorkItemControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { usePersonProfile } from '../people/People';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\n\n/**\n * Task controls (BOFF-7938, BOFF-7939). Until 2026-09-27 the task detail page was read-only: no status, priority or\n * assignee control, no Edit link (the Edit Task page existed but nothing led to it) and no Delete. A task's status\n * could only change by dragging its card on a project board, and nobody could be assigned at all — so every Team\n * page, sprint \"Team Members\" list and board card read \"Unassigned\".\n *\n * Every control saves through planmagnetUpdateWorkItem, the same path the board uses, so a status change records\n * its history (activity feed, cycle time) exactly like a drag. A refusal keeps the stored value and shows the reason.\n */\n\nexport const WORK_ITEM_STATUSES = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\nexport const WORK_ITEM_PRIORITIES = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;\n\nexport function enumLabel(value: string): string {\n const words = value.toLowerCase().replace(/_/g, ' ');\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nconst UPDATE_WORK_ITEM = gql`\n mutation WorkItemControlsUpdate($id: ID!, $input: PlanmagnetUpdateWorkItemInput!) {\n planmagnetUpdateWorkItem(id: $id, input: $input) {\n id\n status\n priority\n assigneeId\n }\n }\n`;\nconst DELETE_WORK_ITEM = gql`\n mutation WorkItemControlsDelete($id: ID!) {\n planmagnetDeleteWorkItem(id: $id)\n }\n`;\n\n/** Every query that shows a task's status, priority or assignee. */\nconst LIST_QUERIES = ['GetWorkItems', 'GetWorkItemCounts', 'ProjectBoard', 'GetDashboardData'];\nconst DETAIL_QUERIES = ['GetWorkItemDetail', 'WorkItemActivity', ...LIST_QUERIES];\n\nconst selectClass =\n 'rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60 max-w-[12rem]';\n\nfunction useWorkItemUpdate(workItemId: string) {\n const [update, state] = useMutation(UPDATE_WORK_ITEM, {\n refetchQueries: DETAIL_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const save = async (input: Record<string, unknown>) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: workItemId, input } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be updated.');\n }\n };\n return { save, saving: state.loading, error };\n}\n\n/** A status or priority select that saves on change. */\nexport function WorkItemEnumSelect({\n workItemId,\n field,\n value,\n options,\n label,\n}: {\n workItemId: string;\n field: 'status' | 'priority';\n value: string;\n options: readonly string[];\n label: string;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n // A stored value the list does not know (older data) stays selectable rather than silently showing another one.\n const all = options.includes(value) ? options : [value, ...options];\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label={label}\n data-testid={`task-${field}`}\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ [field]: e.target.value });\n }}\n >\n {all.map((option) => (\n <option key={option} value={option}>\n {enumLabel(option)}\n </option>\n ))}\n </select>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Workspace members for an assignee picker, from the fe-libs workspace directory (what the sharing picker uses; any\n * member can read it). Option values are USER ids. `planmagnetWorkspaceEditors` is not used: it needs PlanMagnet\n * admin rights, so an ordinary member would get an empty picker.\n */\nexport function useAssigneeOptions(currentId: string | null | undefined) {\n const { workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n const current = usePersonProfile(currentId);\n const options = directory.members.map((m) => ({ id: m.id, name: m.name }));\n // The current assignee may be outside the loaded page (or no longer a member): keep them selectable by name.\n if (currentId && current && !options.some((o) => o.id === currentId)) {\n options.unshift({ id: currentId, name: current.displayName });\n }\n return {\n options,\n loading: directory.loading,\n hasMore: directory.hasMore,\n fetchMore: directory.fetchMore,\n };\n}\n\n/** A plain assignee select (for forms). '' means unassigned. */\nexport function AssigneeSelect({\n id,\n value,\n onChange,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (userId: string) => void;\n disabled?: boolean;\n}) {\n const { options, loading, hasMore, fetchMore } = useAssigneeOptions(value || null);\n return (\n <div className=\"flex flex-col gap-1\">\n <select\n id={id}\n aria-label={id ? undefined : 'Assignee'}\n data-testid=\"assignee-select\"\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Unassigned</option>\n {options.map((o) => (\n <option key={o.id} value={o.id}>\n {o.name}\n </option>\n ))}\n </select>\n {loading && options.length === 0 && (\n <span className=\"text-xs text-muted-foreground\">Loading members…</span>\n )}\n {hasMore && (\n <button\n type=\"button\"\n className=\"self-start text-xs text-primary hover:underline\"\n onClick={fetchMore}\n >\n Show more members\n </button>\n )}\n </div>\n );\n}\n\n/** The task detail's assignee control: saves on change. */\nexport function WorkItemAssigneeSelect({\n workItemId,\n assigneeId,\n}: {\n workItemId: string;\n assigneeId: string | null;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <AssigneeSelect\n value={assigneeId ?? ''}\n disabled={saving}\n onChange={(next) => {\n if (next !== (assigneeId ?? '')) void save({ assigneeId: next || null });\n }}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/** Edit and Delete for the task detail header. */\nexport function WorkItemHeaderActions({\n workItemId,\n title,\n}: {\n workItemId: string;\n title: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_WORK_ITEM, {\n refetchQueries: LIST_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: workItemId } }));\n navigate('/tasks/tasks-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"task-edit\"\n onClick={() => navigate(`/tasks/edit-task-page?taskId=${encodeURIComponent(workItemId)}`)}\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 \"${title}\"?`}\n description=\"The task is deleted and disappears from boards, sprints and releases. This cannot be undone here.\"\n confirmLabel=\"Delete task\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkItemControls.js","names":[],"sources":["../../../src/planmagnet/controls/WorkItemControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { allowedTargets, useProjectWorkflow, workflowLabel } from '../hooks/useProjectWorkflow';\nimport { usePersonProfile } from '../people/People';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\n\n/**\n * Task controls (BOFF-7938, BOFF-7939). Until 2026-09-27 the task detail page was read-only: no status, priority or\n * assignee control, no Edit link (the Edit Task page existed but nothing led to it) and no Delete. A task's status\n * could only change by dragging its card on a project board, and nobody could be assigned at all — so every Team\n * page, sprint \"Team Members\" list and board card read \"Unassigned\".\n *\n * Every control saves through planmagnetUpdateWorkItem, the same path the board uses, so a status change records\n * its history (activity feed, cycle time) exactly like a drag. A refusal keeps the stored value and shows the reason.\n */\n\nexport const WORK_ITEM_STATUSES = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\nexport const WORK_ITEM_PRIORITIES = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;\n\nexport function enumLabel(value: string): string {\n const words = value.toLowerCase().replace(/_/g, ' ');\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nconst UPDATE_WORK_ITEM = gql`\n mutation WorkItemControlsUpdate($id: ID!, $input: PlanmagnetUpdateWorkItemInput!) {\n planmagnetUpdateWorkItem(id: $id, input: $input) {\n id\n status\n priority\n assigneeId\n }\n }\n`;\nconst DELETE_WORK_ITEM = gql`\n mutation WorkItemControlsDelete($id: ID!) {\n planmagnetDeleteWorkItem(id: $id)\n }\n`;\n\n/** Every query that shows a task's status, priority or assignee. */\nconst LIST_QUERIES = ['GetWorkItems', 'GetWorkItemCounts', 'ProjectBoard', 'GetDashboardData'];\nconst DETAIL_QUERIES = ['GetWorkItemDetail', 'WorkItemActivity', ...LIST_QUERIES];\n\nconst selectClass =\n 'rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60 max-w-[12rem]';\n\nfunction useWorkItemUpdate(workItemId: string) {\n const [update, state] = useMutation(UPDATE_WORK_ITEM, {\n refetchQueries: DETAIL_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const save = async (input: Record<string, unknown>) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: workItemId, input } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be updated.');\n }\n };\n return { save, saving: state.loading, error };\n}\n\n/** A status or priority select that saves on change. */\nexport function WorkItemEnumSelect({\n workItemId,\n field,\n value,\n options,\n label,\n}: {\n workItemId: string;\n field: 'status' | 'priority';\n value: string;\n options: readonly string[];\n label: string;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n // A stored value the list does not know (older data) stays selectable rather than silently showing another one.\n const all = options.includes(value) ? options : [value, ...options];\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label={label}\n data-testid={`task-${field}`}\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ [field]: e.target.value });\n }}\n >\n {all.map((option) => (\n <option key={option} value={option}>\n {enumLabel(option)}\n </option>\n ))}\n </select>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * The status select (BOFF-8043): the project's names for its statuses, and only the moves its workflow allows — the\n * server refuses anything else anyway, with the reason shown below the select.\n */\nexport function WorkItemStatusSelect({\n workItemId,\n projectId,\n value,\n}: {\n workItemId: string;\n projectId: string;\n value: string;\n}) {\n const workflow = useProjectWorkflow(projectId);\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n const options = allowedTargets(workflow, value, WORK_ITEM_STATUSES);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label=\"Status\"\n data-testid=\"task-status\"\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ status: e.target.value });\n }}\n >\n {options.map((option) => (\n <option key={option} value={option}>\n {workflowLabel(workflow, option)}\n </option>\n ))}\n </select>\n {workflow?.enforceTransitions && (\n <span className=\"text-[11px] text-muted-foreground\">\n Only the moves this project allows\n </span>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Workspace members for an assignee picker, from the fe-libs workspace directory (what the sharing picker uses; any\n * member can read it). Option values are USER ids. `planmagnetWorkspaceEditors` is not used: it needs PlanMagnet\n * admin rights, so an ordinary member would get an empty picker.\n */\nexport function useAssigneeOptions(currentId: string | null | undefined) {\n const { workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n const current = usePersonProfile(currentId);\n const options = directory.members.map((m) => ({ id: m.id, name: m.name }));\n // The current assignee may be outside the loaded page (or no longer a member): keep them selectable by name.\n if (currentId && current && !options.some((o) => o.id === currentId)) {\n options.unshift({ id: currentId, name: current.displayName });\n }\n return {\n options,\n loading: directory.loading,\n hasMore: directory.hasMore,\n fetchMore: directory.fetchMore,\n };\n}\n\n/** A plain assignee select (for forms). '' means unassigned. */\nexport function AssigneeSelect({\n id,\n value,\n onChange,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (userId: string) => void;\n disabled?: boolean;\n}) {\n const { options, loading, hasMore, fetchMore } = useAssigneeOptions(value || null);\n return (\n <div className=\"flex flex-col gap-1\">\n <select\n id={id}\n aria-label={id ? undefined : 'Assignee'}\n data-testid=\"assignee-select\"\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Unassigned</option>\n {options.map((o) => (\n <option key={o.id} value={o.id}>\n {o.name}\n </option>\n ))}\n </select>\n {loading && options.length === 0 && (\n <span className=\"text-xs text-muted-foreground\">Loading members…</span>\n )}\n {hasMore && (\n <button\n type=\"button\"\n className=\"self-start text-xs text-primary hover:underline\"\n onClick={fetchMore}\n >\n Show more members\n </button>\n )}\n </div>\n );\n}\n\n/** The task detail's assignee control: saves on change. */\nexport function WorkItemAssigneeSelect({\n workItemId,\n assigneeId,\n}: {\n workItemId: string;\n assigneeId: string | null;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <AssigneeSelect\n value={assigneeId ?? ''}\n disabled={saving}\n onChange={(next) => {\n if (next !== (assigneeId ?? '')) void save({ assigneeId: next || null });\n }}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/** Edit and Delete for the task detail header. */\nexport function WorkItemHeaderActions({\n workItemId,\n title,\n}: {\n workItemId: string;\n title: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_WORK_ITEM, {\n refetchQueries: LIST_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: workItemId } }));\n navigate('/tasks/tasks-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"task-edit\"\n onClick={() => navigate(`/tasks/edit-task-page?taskId=${encodeURIComponent(workItemId)}`)}\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 \"${title}\"?`}\n description=\"The task is deleted and disappears from boards, sprints and releases. This cannot be undone here.\"\n confirmLabel=\"Delete task\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project's workflow (BOFF-8043): the project's name for each of the seven statuses, which are hidden from new
|
|
3
|
+
* selection, and — when transitions are enforced — which moves are allowed. The server enforces the rules on every
|
|
4
|
+
* path; this hook only lets pickers offer what will be accepted and show the project's names.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PROJECT_WORKFLOW: import('graphql').DocumentNode;
|
|
7
|
+
export interface ProjectWorkflow {
|
|
8
|
+
projectId: string;
|
|
9
|
+
enforceTransitions: boolean;
|
|
10
|
+
statuses: {
|
|
11
|
+
status: string;
|
|
12
|
+
label: string;
|
|
13
|
+
hidden: boolean;
|
|
14
|
+
category: string;
|
|
15
|
+
}[];
|
|
16
|
+
transitions: {
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
export declare function useProjectWorkflow(projectId: string | null | undefined): ProjectWorkflow | null;
|
|
22
|
+
/** "In progress" for IN_PROGRESS — what a status is called when no project names it. */
|
|
23
|
+
export declare function canonicalLabel(status: string): string;
|
|
24
|
+
/** The project's name for a status (the canonical name while the workflow loads or for an unknown value). */
|
|
25
|
+
export declare function workflowLabel(workflow: ProjectWorkflow | null, status: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Statuses an item in `from` can be moved to, in canonical order, `from` first when it is not in the list (a hidden
|
|
28
|
+
* status an item already has stays shown so it is never silently replaced). Mirrors the server's rule: hidden
|
|
29
|
+
* statuses are never offered; with transitions enforced only listed moves are.
|
|
30
|
+
*/
|
|
31
|
+
export declare function allowedTargets(workflow: ProjectWorkflow | null, from: string, all: readonly string[]): string[];
|
|
32
|
+
//# sourceMappingURL=useProjectWorkflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProjectWorkflow.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/hooks/useProjectWorkflow.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,gCAiB5B,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjF,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC7C;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAM/F;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED,6GAA6G;AAC7G,wBAAgB,aAAa,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,eAAe,GAAG,IAAI,EAChC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,SAAS,MAAM,EAAE,GACrB,MAAM,EAAE,CAYV"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { gql as e } from "@apollo/client";
|
|
2
|
+
import { useQuery as t } from "@apollo/client/react";
|
|
3
|
+
//#region src/planmagnet/hooks/useProjectWorkflow.ts
|
|
4
|
+
var n = e`
|
|
5
|
+
query ProjectWorkflow($projectId: ID!) {
|
|
6
|
+
planmagnetProjectWorkflow(projectId: $projectId) {
|
|
7
|
+
projectId
|
|
8
|
+
enforceTransitions
|
|
9
|
+
statuses {
|
|
10
|
+
status
|
|
11
|
+
label
|
|
12
|
+
hidden
|
|
13
|
+
category
|
|
14
|
+
}
|
|
15
|
+
transitions {
|
|
16
|
+
from
|
|
17
|
+
to
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
function r(e) {
|
|
23
|
+
let { data: r } = t(n, {
|
|
24
|
+
variables: { projectId: e ?? "" },
|
|
25
|
+
skip: !e
|
|
26
|
+
});
|
|
27
|
+
return r?.planmagnetProjectWorkflow ?? null;
|
|
28
|
+
}
|
|
29
|
+
function i(e) {
|
|
30
|
+
let t = e.toLowerCase().replace(/_/g, " ");
|
|
31
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
32
|
+
}
|
|
33
|
+
function a(e, t) {
|
|
34
|
+
return e?.statuses.find((e) => e.status === t)?.label ?? i(t);
|
|
35
|
+
}
|
|
36
|
+
function o(e, t, n) {
|
|
37
|
+
if (!e) return n.includes(t) ? [...n] : [t, ...n];
|
|
38
|
+
let r = e.statuses.filter((e) => e.status === t || !e.hidden).filter((n) => n.status === t || !e.enforceTransitions || e.transitions.some((e) => e.from === t && e.to === n.status)).map((e) => e.status);
|
|
39
|
+
return r.includes(t) ? r : [t, ...r];
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { n as PROJECT_WORKFLOW, o as allowedTargets, i as canonicalLabel, r as useProjectWorkflow, a as workflowLabel };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=useProjectWorkflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProjectWorkflow.js","names":[],"sources":["../../../src/planmagnet/hooks/useProjectWorkflow.ts"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\n\n/**\n * A project's workflow (BOFF-8043): the project's name for each of the seven statuses, which are hidden from new\n * selection, and — when transitions are enforced — which moves are allowed. The server enforces the rules on every\n * path; this hook only lets pickers offer what will be accepted and show the project's names.\n */\n\nexport const PROJECT_WORKFLOW = gql`\n query ProjectWorkflow($projectId: ID!) {\n planmagnetProjectWorkflow(projectId: $projectId) {\n projectId\n enforceTransitions\n statuses {\n status\n label\n hidden\n category\n }\n transitions {\n from\n to\n }\n }\n }\n`;\n\nexport interface ProjectWorkflow {\n projectId: string;\n enforceTransitions: boolean;\n statuses: { status: string; label: string; hidden: boolean; category: string }[];\n transitions: { from: string; to: string }[];\n}\n\nexport function useProjectWorkflow(projectId: string | null | undefined): ProjectWorkflow | null {\n const { data } = useQuery<{ planmagnetProjectWorkflow: ProjectWorkflow }>(PROJECT_WORKFLOW, {\n variables: { projectId: projectId ?? '' },\n skip: !projectId,\n });\n return data?.planmagnetProjectWorkflow ?? null;\n}\n\n/** \"In progress\" for IN_PROGRESS — what a status is called when no project names it. */\nexport function canonicalLabel(status: string): string {\n const words = status.toLowerCase().replace(/_/g, ' ');\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\n/** The project's name for a status (the canonical name while the workflow loads or for an unknown value). */\nexport function workflowLabel(workflow: ProjectWorkflow | null, status: string): string {\n return workflow?.statuses.find((s) => s.status === status)?.label ?? canonicalLabel(status);\n}\n\n/**\n * Statuses an item in `from` can be moved to, in canonical order, `from` first when it is not in the list (a hidden\n * status an item already has stays shown so it is never silently replaced). Mirrors the server's rule: hidden\n * statuses are never offered; with transitions enforced only listed moves are.\n */\nexport function allowedTargets(\n workflow: ProjectWorkflow | null,\n from: string,\n all: readonly string[]\n): string[] {\n if (!workflow) return all.includes(from) ? [...all] : [from, ...all];\n const allowed = workflow.statuses\n .filter((s) => s.status === from || !s.hidden)\n .filter(\n (s) =>\n s.status === from ||\n !workflow.enforceTransitions ||\n workflow.transitions.some((t) => t.from === from && t.to === s.status)\n )\n .map((s) => s.status);\n return allowed.includes(from) ? allowed : [from, ...allowed];\n}\n"],"mappings":";;;AASA,IAAa,IAAmB,CAAG;;;;;;;;;;;;;;;;;;AA0BnC,SAAgB,EAAmB,GAA8D;CAC/F,IAAM,EAAE,YAAS,EAAyD,GAAkB;EAC1F,WAAW,EAAE,WAAW,KAAa,IAAI;EACzC,MAAM,CAAC;EACR,CAAC;AACF,QAAO,GAAM,6BAA6B;;AAI5C,SAAgB,EAAe,GAAwB;CACrD,IAAM,IAAQ,EAAO,aAAa,CAAC,QAAQ,MAAM,IAAI;AACrD,QAAO,EAAM,OAAO,EAAE,CAAC,aAAa,GAAG,EAAM,MAAM,EAAE;;AAIvD,SAAgB,EAAc,GAAkC,GAAwB;AACtF,QAAO,GAAU,SAAS,MAAM,MAAM,EAAE,WAAW,EAAO,EAAE,SAAS,EAAe,EAAO;;AAQ7F,SAAgB,EACd,GACA,GACA,GACU;AACV,KAAI,CAAC,EAAU,QAAO,EAAI,SAAS,EAAK,GAAG,CAAC,GAAG,EAAI,GAAG,CAAC,GAAM,GAAG,EAAI;CACpE,IAAM,IAAU,EAAS,SACtB,QAAQ,MAAM,EAAE,WAAW,KAAQ,CAAC,EAAE,OAAO,CAC7C,QACE,MACC,EAAE,WAAW,KACb,CAAC,EAAS,sBACV,EAAS,YAAY,MAAM,MAAM,EAAE,SAAS,KAAQ,EAAE,OAAO,EAAE,OAAO,CACzE,CACA,KAAK,MAAM,EAAE,OAAO;AACvB,QAAO,EAAQ,SAAS,EAAK,GAAG,IAAU,CAAC,GAAM,GAAG,EAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsProjectDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsProjectDetailPagePage.tsx"],"names":[],"mappings":"AA2EA,wBAAgB,6BAA6B,
|
|
1
|
+
{"version":3,"file":"ProjectsProjectDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsProjectDetailPagePage.tsx"],"names":[],"mappings":"AA2EA,wBAAgB,6BAA6B,4CAwV5C"}
|
|
@@ -91,7 +91,7 @@ function C() {
|
|
|
91
91
|
className: "p-6 text-muted-foreground",
|
|
92
92
|
children: "Project not found."
|
|
93
93
|
});
|
|
94
|
-
let O = D.workItems ?? [], k = D.sprints ?? [], A = D.milestones ?? [], j = k.find((e) => e.status === "ACTIVE"), M = k.filter((e) => e.status === "COMPLETED" && e.velocity != null).sort((e, t) => (t.endDate ?? "").localeCompare(e.endDate ?? ""))[0], N = n(O.map((e) => ({ status: e.status }))), P = [...O].sort((e, t) => t.createdAt.localeCompare(e.createdAt)).slice(0, 5);
|
|
94
|
+
let O = D.workItems ?? [], k = D.sprints ?? [], A = D.milestones ?? [], j = k.find((e) => e.status === "ACTIVE"), M = k.filter((e) => e.status === "COMPLETED" && e.velocity != null).sort((e, t) => (t.endDate ?? "").localeCompare(e.endDate ?? ""))[0], N = n(O.map((e) => ({ status: e.status }))), P = O.filter((e) => e.status !== "CANCELLED"), F = P.filter((e) => e.status === "DONE").length, I = [...O].sort((e, t) => t.createdAt.localeCompare(e.createdAt)).slice(0, 5);
|
|
95
95
|
return /* @__PURE__ */ h("div", {
|
|
96
96
|
className: "space-y-6 p-6",
|
|
97
97
|
children: [
|
|
@@ -137,9 +137,9 @@ function C() {
|
|
|
137
137
|
}), /* @__PURE__ */ h("p", {
|
|
138
138
|
className: "text-xl font-bold mt-1",
|
|
139
139
|
children: [
|
|
140
|
-
|
|
140
|
+
F,
|
|
141
141
|
"/",
|
|
142
|
-
|
|
142
|
+
P.length
|
|
143
143
|
]
|
|
144
144
|
})]
|
|
145
145
|
}) }),
|
|
@@ -263,7 +263,7 @@ function C() {
|
|
|
263
263
|
O.length > 5 ? " · latest 5" : ""
|
|
264
264
|
] }) }), /* @__PURE__ */ h(v, {
|
|
265
265
|
className: "space-y-2",
|
|
266
|
-
children: [
|
|
266
|
+
children: [I.map((e) => /* @__PURE__ */ h("div", {
|
|
267
267
|
...i(() => C(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`), e.key),
|
|
268
268
|
className: `flex items-center justify-between border rounded-md p-2.5 ${r}`,
|
|
269
269
|
children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("p", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsProjectDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsProjectDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { FolderKanban, CheckSquare, Users, Zap, Calendar } from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { PersonName } from '../people/People';\nimport { ProjectHeaderActions } from '../controls/ProjectControls';\nimport { workProgress } from '../hooks/useCounts';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ROW_LINK_CLASS, rowLinkProps } from '../utils/rowNavigation';\n\nconst GET_PROJECT_DETAIL = gql`\n query GetProjectDetail($id: ID!) {\n planmagnetProject(id: $id) {\n id\n name\n key\n description\n status\n methodology\n ownerId\n startDate\n endDate\n tags\n createdAt\n sprints {\n id\n name\n status\n goal\n capacity\n velocity\n startDate\n endDate\n workItems {\n id\n status\n storyPoints\n }\n }\n milestones {\n id\n name\n description\n status\n dueDate\n ownerId\n }\n workItems {\n id\n key\n title\n description\n status\n priority\n storyPoints\n assigneeId\n labels\n createdAt\n }\n }\n }\n`;\n\nconst statusColor: Record<string, string> = {\n ACTIVE: 'bg-green-100 text-green-800',\n ON_HOLD: 'bg-orange-100 text-orange-800',\n CANCELLED: 'bg-red-100 text-red-800',\n COMPLETED: 'bg-blue-100 text-blue-800',\n PLANNING: 'bg-gray-100 text-gray-800',\n ARCHIVED: 'bg-gray-100 text-gray-800',\n};\n\nexport function ProjectsProjectDetailPagePage() {\n const projectId = useEntityId('projectId');\n const navigate = usePlanMagnetNavigate();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_PROJECT_DETAIL, {\n variables: { id: projectId },\n skip: !projectId,\n });\n\n if (!projectId)\n return (\n <div className=\"p-6 text-muted-foreground\">No project selected. Open one from the list.</div>\n );\n // Only before the first load: a refetch must not blank the page (it\n // unmounts every component on it and loses their state).\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 if (error) return <div className=\"p-6 text-red-500\">Error loading project: {error.message}</div>;\n\n const project = data?.planmagnetProject;\n if (!project) return <div className=\"p-6 text-muted-foreground\">Project not found.</div>;\n\n const projectWorkItems = project.workItems ?? [];\n const projectSprints = project.sprints ?? [];\n const projectMilestones = project.milestones ?? [];\n const activeSprint = projectSprints.find((s: { status: string }) => s.status === 'ACTIVE');\n // Velocity exists only for a COMPLETED sprint (BOFF-7944: this tile read the running sprint's velocity, which is\n // never set, so it always showed 0). Show the latest completed sprint's instead.\n const lastCompleted = projectSprints\n .filter(\n (s: { status: string; velocity: number | null }) =>\n s.status === 'COMPLETED' && s.velocity != null\n )\n .sort((a: { endDate: string | null }, b: { endDate: string | null }) =>\n (b.endDate ?? '').localeCompare(a.endDate ?? '')\n )[0];\n // Done over everything not cancelled (BOFF-7989), labelled as tasks — it sat under \"Duration\" as if it were time.\n const tasksDonePct = workProgress(\n projectWorkItems.map((wi: { status: string }) => ({ status: wi.status }))\n );\n // Newest first: the list is ordered oldest-first, so slice(0, 5) showed the five OLDEST items (BOFF-7993).\n const latestWorkItems = [...projectWorkItems]\n .sort((a: { createdAt: string }, b: { createdAt: string }) =>\n b.createdAt.localeCompare(a.createdAt)\n )\n .slice(0, 5);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <FolderKanban className=\"h-5 w-5 text-primary\" />\n <h1 className=\"text-2xl font-semibold\">{project.name}</h1>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${statusColor[project.status] ?? 'bg-gray-100'}`}\n >\n {project.status.toLowerCase().replace('_', ' ')}\n </span>\n </div>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {project.description ?? 'No description'}\n </p>\n </div>\n <div className=\"flex flex-col items-end gap-2\">\n {/* Status, Edit and Delete (BOFF-7940): a project could never leave PLANNING from the UI. */}\n <ProjectHeaderActions\n projectId={project.id}\n name={project.name}\n status={project.status}\n />\n <Badge variant=\"outline\" className=\"capitalize\">\n {project.methodology.toLowerCase()}\n </Badge>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <CheckSquare className=\"h-4 w-4\" />\n Work Items\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {completedTasks}/{totalTasks}\n </p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Zap className=\"h-4 w-4\" />\n Sprints\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectSprints.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Calendar className=\"h-4 w-4\" />\n Milestones\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectMilestones.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Users className=\"h-4 w-4\" />\n Owner\n </div>\n <p className=\"text-sm font-bold mt-1 truncate\">\n <PersonName id={project.ownerId} />\n </p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Timeline</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex justify-between text-sm mb-2\">\n <span className=\"text-muted-foreground\">Tasks done</span>\n <span className=\"font-medium\">\n {tasksDonePct === null ? 'No open tasks' : `${tasksDonePct}%`}\n </span>\n </div>\n <div className=\"w-full h-4 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full\"\n style={{ width: `${tasksDonePct ?? 0}%` }}\n />\n </div>\n <div className=\"flex justify-between text-xs text-muted-foreground mt-2\">\n <span>\n {project.startDate ? new Date(project.startDate).toLocaleDateString() : 'Not set'}\n </span>\n <span>\n {project.endDate ? new Date(project.endDate).toLocaleDateString() : 'Not set'}\n </span>\n </div>\n </CardContent>\n </Card>\n\n {activeSprint && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Zap className=\"h-4 w-4 text-primary\" />\n Active Sprint\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex justify-between\">\n <span className=\"font-medium\">{activeSprint.name}</span>\n <Badge>Active</Badge>\n </div>\n {activeSprint.goal && (\n <p className=\"text-sm text-muted-foreground\">{activeSprint.goal}</p>\n )}\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems\n ?.filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n )}\n /\n {activeSprint.workItems?.reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n ) ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Points</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems?.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length ?? 0}\n /{activeSprint.workItems?.length ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Tasks</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {lastCompleted ? `${lastCompleted.velocity} pts` : '—'}\n </p>\n <p className=\"text-xs text-muted-foreground\">Velocity of the last sprint</p>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>\n Work items ({projectWorkItems.length})\n {projectWorkItems.length > 5 ? ' · latest 5' : ''}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {latestWorkItems.map(\n (task: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n labels: string[];\n }) => (\n <div\n key={task.id}\n {...rowLinkProps(\n () =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(task.id)}`),\n task.key\n )}\n className={`flex items-center justify-between border rounded-md p-2.5 ${ROW_LINK_CLASS}`}\n >\n <div>\n <p className=\"text-sm font-medium\">\n {task.key} <span className=\"font-normal\">{task.title}</span>\n </p>\n <div className=\"flex gap-1 mt-1\">\n {task.labels.map((l: string) => (\n <span key={l} className=\"text-[10px] px-1.5 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {task.storyPoints ?? 0} pts\n </span>\n <Badge variant=\"secondary\" className=\"capitalize text-xs\">\n {task.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {projectWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">No work items yet.</p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Milestones ({projectMilestones.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {projectMilestones.map(\n (ms: { id: string; name: string; status: string; dueDate: string | null }) => (\n <div key={ms.id} className=\"border-b pb-2 last:border-0\">\n <div className=\"flex justify-between\">\n <span className=\"text-sm font-medium\">{ms.name}</span>\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground mt-1\">\n <Calendar className=\"h-3 w-3\" />\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : 'No due date'}\n </div>\n </div>\n )\n )}\n {projectMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">No milestones.</p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Owner</span>\n <span>\n <PersonName id={project.ownerId} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span className=\"font-mono\">{project.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Methodology</span>\n <span className=\"capitalize\">{project.methodology.toLowerCase()}</span>\n </div>\n {project.tags.length > 0 && (\n <div className=\"flex justify-between items-start\">\n <span className=\"text-muted-foreground\">Tags</span>\n <div className=\"flex gap-1 flex-wrap justify-end\">\n {project.tags.map((tag: string) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProjectsProjectDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsProjectDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { FolderKanban, CheckSquare, Users, Zap, Calendar } from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { PersonName } from '../people/People';\nimport { ProjectHeaderActions } from '../controls/ProjectControls';\nimport { workProgress } from '../hooks/useCounts';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ROW_LINK_CLASS, rowLinkProps } from '../utils/rowNavigation';\n\nconst GET_PROJECT_DETAIL = gql`\n query GetProjectDetail($id: ID!) {\n planmagnetProject(id: $id) {\n id\n name\n key\n description\n status\n methodology\n ownerId\n startDate\n endDate\n tags\n createdAt\n sprints {\n id\n name\n status\n goal\n capacity\n velocity\n startDate\n endDate\n workItems {\n id\n status\n storyPoints\n }\n }\n milestones {\n id\n name\n description\n status\n dueDate\n ownerId\n }\n workItems {\n id\n key\n title\n description\n status\n priority\n storyPoints\n assigneeId\n labels\n createdAt\n }\n }\n }\n`;\n\nconst statusColor: Record<string, string> = {\n ACTIVE: 'bg-green-100 text-green-800',\n ON_HOLD: 'bg-orange-100 text-orange-800',\n CANCELLED: 'bg-red-100 text-red-800',\n COMPLETED: 'bg-blue-100 text-blue-800',\n PLANNING: 'bg-gray-100 text-gray-800',\n ARCHIVED: 'bg-gray-100 text-gray-800',\n};\n\nexport function ProjectsProjectDetailPagePage() {\n const projectId = useEntityId('projectId');\n const navigate = usePlanMagnetNavigate();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_PROJECT_DETAIL, {\n variables: { id: projectId },\n skip: !projectId,\n });\n\n if (!projectId)\n return (\n <div className=\"p-6 text-muted-foreground\">No project selected. Open one from the list.</div>\n );\n // Only before the first load: a refetch must not blank the page (it\n // unmounts every component on it and loses their state).\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 if (error) return <div className=\"p-6 text-red-500\">Error loading project: {error.message}</div>;\n\n const project = data?.planmagnetProject;\n if (!project) return <div className=\"p-6 text-muted-foreground\">Project not found.</div>;\n\n const projectWorkItems = project.workItems ?? [];\n const projectSprints = project.sprints ?? [];\n const projectMilestones = project.milestones ?? [];\n const activeSprint = projectSprints.find((s: { status: string }) => s.status === 'ACTIVE');\n // Velocity exists only for a COMPLETED sprint (BOFF-7944: this tile read the running sprint's velocity, which is\n // never set, so it always showed 0). Show the latest completed sprint's instead.\n const lastCompleted = projectSprints\n .filter(\n (s: { status: string; velocity: number | null }) =>\n s.status === 'COMPLETED' && s.velocity != null\n )\n .sort((a: { endDate: string | null }, b: { endDate: string | null }) =>\n (b.endDate ?? '').localeCompare(a.endDate ?? '')\n )[0];\n // Done over everything not cancelled (BOFF-7989), labelled as tasks — it sat under \"Duration\" as if it were time.\n const tasksDonePct = workProgress(\n projectWorkItems.map((wi: { status: string }) => ({ status: wi.status }))\n );\n // The Work Items tile counts the same way as tasksDonePct: done over everything not cancelled (BOFF-7989).\n // project.workItems is every non-deleted item (no page cap), so these are exact.\n const liveWorkItems = projectWorkItems.filter(\n (wi: { status: string }) => wi.status !== 'CANCELLED'\n );\n const doneWorkItems = liveWorkItems.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length;\n // Newest first: the list is ordered oldest-first, so slice(0, 5) showed the five OLDEST items (BOFF-7993).\n const latestWorkItems = [...projectWorkItems]\n .sort((a: { createdAt: string }, b: { createdAt: string }) =>\n b.createdAt.localeCompare(a.createdAt)\n )\n .slice(0, 5);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <FolderKanban className=\"h-5 w-5 text-primary\" />\n <h1 className=\"text-2xl font-semibold\">{project.name}</h1>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${statusColor[project.status] ?? 'bg-gray-100'}`}\n >\n {project.status.toLowerCase().replace('_', ' ')}\n </span>\n </div>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {project.description ?? 'No description'}\n </p>\n </div>\n <div className=\"flex flex-col items-end gap-2\">\n {/* Status, Edit and Delete (BOFF-7940): a project could never leave PLANNING from the UI. */}\n <ProjectHeaderActions\n projectId={project.id}\n name={project.name}\n status={project.status}\n />\n <Badge variant=\"outline\" className=\"capitalize\">\n {project.methodology.toLowerCase()}\n </Badge>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <CheckSquare className=\"h-4 w-4\" />\n Work Items\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {doneWorkItems}/{liveWorkItems.length}\n </p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Zap className=\"h-4 w-4\" />\n Sprints\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectSprints.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Calendar className=\"h-4 w-4\" />\n Milestones\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectMilestones.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Users className=\"h-4 w-4\" />\n Owner\n </div>\n <p className=\"text-sm font-bold mt-1 truncate\">\n <PersonName id={project.ownerId} />\n </p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Timeline</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex justify-between text-sm mb-2\">\n <span className=\"text-muted-foreground\">Tasks done</span>\n <span className=\"font-medium\">\n {tasksDonePct === null ? 'No open tasks' : `${tasksDonePct}%`}\n </span>\n </div>\n <div className=\"w-full h-4 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full\"\n style={{ width: `${tasksDonePct ?? 0}%` }}\n />\n </div>\n <div className=\"flex justify-between text-xs text-muted-foreground mt-2\">\n <span>\n {project.startDate ? new Date(project.startDate).toLocaleDateString() : 'Not set'}\n </span>\n <span>\n {project.endDate ? new Date(project.endDate).toLocaleDateString() : 'Not set'}\n </span>\n </div>\n </CardContent>\n </Card>\n\n {activeSprint && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Zap className=\"h-4 w-4 text-primary\" />\n Active Sprint\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex justify-between\">\n <span className=\"font-medium\">{activeSprint.name}</span>\n <Badge>Active</Badge>\n </div>\n {activeSprint.goal && (\n <p className=\"text-sm text-muted-foreground\">{activeSprint.goal}</p>\n )}\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems\n ?.filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n )}\n /\n {activeSprint.workItems?.reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n ) ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Points</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems?.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length ?? 0}\n /{activeSprint.workItems?.length ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Tasks</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {lastCompleted ? `${lastCompleted.velocity} pts` : '—'}\n </p>\n <p className=\"text-xs text-muted-foreground\">Velocity of the last sprint</p>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>\n Work items ({projectWorkItems.length})\n {projectWorkItems.length > 5 ? ' · latest 5' : ''}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {latestWorkItems.map(\n (task: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n labels: string[];\n }) => (\n <div\n key={task.id}\n {...rowLinkProps(\n () =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(task.id)}`),\n task.key\n )}\n className={`flex items-center justify-between border rounded-md p-2.5 ${ROW_LINK_CLASS}`}\n >\n <div>\n <p className=\"text-sm font-medium\">\n {task.key} <span className=\"font-normal\">{task.title}</span>\n </p>\n <div className=\"flex gap-1 mt-1\">\n {task.labels.map((l: string) => (\n <span key={l} className=\"text-[10px] px-1.5 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {task.storyPoints ?? 0} pts\n </span>\n <Badge variant=\"secondary\" className=\"capitalize text-xs\">\n {task.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {projectWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">No work items yet.</p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Milestones ({projectMilestones.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {projectMilestones.map(\n (ms: { id: string; name: string; status: string; dueDate: string | null }) => (\n <div key={ms.id} className=\"border-b pb-2 last:border-0\">\n <div className=\"flex justify-between\">\n <span className=\"text-sm font-medium\">{ms.name}</span>\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground mt-1\">\n <Calendar className=\"h-3 w-3\" />\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : 'No due date'}\n </div>\n </div>\n )\n )}\n {projectMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">No milestones.</p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Owner</span>\n <span>\n <PersonName id={project.ownerId} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span className=\"font-mono\">{project.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Methodology</span>\n <span className=\"capitalize\">{project.methodology.toLowerCase()}</span>\n </div>\n {project.tags.length > 0 && (\n <div className=\"flex justify-between items-start\">\n <span className=\"text-muted-foreground\">Tags</span>\n <div className=\"flex gap-1 flex-wrap justify-end\">\n {project.tags.map((tag: string) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectsWorkflowPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsWorkflowPagePage.tsx"],"names":[],"mappings":"AAsDA,wBAAgB,wBAAwB,4CAwDvC"}
|