@burdenoff/microfe-planmagnet 2026.928.10 → 2026.928.11
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/automation/AutomationRuleFields.d.ts.map +1 -1
- package/dist/planmagnet/automation/AutomationRuleFields.js +18 -18
- package/dist/planmagnet/automation/AutomationRuleFields.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js +25 -13
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -124
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +166 -128
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
- package/dist/planmagnet/utils/formValidation.d.ts +11 -0
- package/dist/planmagnet/utils/formValidation.d.ts.map +1 -1
- package/dist/planmagnet/utils/formValidation.js +13 -1
- package/dist/planmagnet/utils/formValidation.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
2
|
import { throwOnError as n } from "../utils/mutationResult.js";
|
|
3
3
|
import { WORK_ITEM_PRIORITIES as r } from "../controls/WorkItemControls.js";
|
|
4
|
-
import {
|
|
5
|
-
import { useMemo as
|
|
6
|
-
import { ArrowLeft as
|
|
7
|
-
import { gql as
|
|
8
|
-
import { useMutation as
|
|
9
|
-
import { jsx as
|
|
10
|
-
import { Button as
|
|
4
|
+
import { estimateHoursText as i, parseEstimateHours as a, parseIntField as o } from "../utils/formValidation.js";
|
|
5
|
+
import { useMemo as s, useState as c } from "react";
|
|
6
|
+
import { ArrowLeft as l, Save as u } from "lucide-react";
|
|
7
|
+
import { gql as d } from "@apollo/client";
|
|
8
|
+
import { useMutation as f, useQuery as p } from "@apollo/client/react";
|
|
9
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
10
|
+
import { Button as g, Card as _, CardContent as v, CardHeader as y, CardTitle as b, Input as x } from "@burdenoff/fe-libs/ui";
|
|
11
11
|
//#region src/planmagnet/pages/TasksEditTaskPagePage.tsx
|
|
12
|
-
var
|
|
12
|
+
var S = d`
|
|
13
13
|
query GetWorkItem($workItemId: ID!) {
|
|
14
14
|
planmagnetWorkItem(id: $workItemId) {
|
|
15
15
|
id
|
|
@@ -20,6 +20,7 @@ var b = l`
|
|
|
20
20
|
priority
|
|
21
21
|
key
|
|
22
22
|
storyPoints
|
|
23
|
+
estimateMinutes
|
|
23
24
|
dueDate
|
|
24
25
|
labels
|
|
25
26
|
tags
|
|
@@ -27,7 +28,7 @@ var b = l`
|
|
|
27
28
|
updatedAt
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
`,
|
|
31
|
+
`, C = d`
|
|
31
32
|
mutation UpdateWorkItem($workItemId: ID!, $input: PlanmagnetUpdateWorkItemInput!) {
|
|
32
33
|
planmagnetUpdateWorkItem(id: $workItemId, input: $input) {
|
|
33
34
|
id
|
|
@@ -37,17 +38,18 @@ var b = l`
|
|
|
37
38
|
status
|
|
38
39
|
priority
|
|
39
40
|
storyPoints
|
|
41
|
+
estimateMinutes
|
|
40
42
|
dueDate
|
|
41
43
|
labels
|
|
42
44
|
updatedAt
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
`;
|
|
46
|
-
function
|
|
47
|
-
let { workspaceId:
|
|
48
|
-
variables: { workItemId:
|
|
49
|
-
skip: !
|
|
50
|
-
}), [
|
|
48
|
+
function w() {
|
|
49
|
+
let { workspaceId: d } = e(), w = new URLSearchParams(window.location.search).get("taskId") ?? "", T = t(), { data: E, loading: D, error: O } = p(S, {
|
|
50
|
+
variables: { workItemId: w },
|
|
51
|
+
skip: !d || !w
|
|
52
|
+
}), [k, { loading: A }] = f(C, {
|
|
51
53
|
refetchQueries: [
|
|
52
54
|
"GetWorkItems",
|
|
53
55
|
"GetWorkItemDetail",
|
|
@@ -55,154 +57,157 @@ function S() {
|
|
|
55
57
|
"ProjectBoard"
|
|
56
58
|
],
|
|
57
59
|
awaitRefetchQueries: !0
|
|
58
|
-
}), [
|
|
59
|
-
title:
|
|
60
|
-
description:
|
|
61
|
-
priority:
|
|
62
|
-
type:
|
|
63
|
-
storyPoints:
|
|
64
|
-
labels: (
|
|
65
|
-
dueDate:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
...
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!
|
|
72
|
-
|
|
60
|
+
}), [j, M] = c(null), [N, P] = c(!1), F = E?.planmagnetWorkItem, I = s(() => ({
|
|
61
|
+
title: F?.title ?? "",
|
|
62
|
+
description: F?.description ?? "",
|
|
63
|
+
priority: F?.priority ?? "MEDIUM",
|
|
64
|
+
type: F?.type ?? "STORY",
|
|
65
|
+
storyPoints: F?.storyPoints?.toString() ?? "",
|
|
66
|
+
labels: (F?.labels ?? []).join(", "),
|
|
67
|
+
dueDate: F?.dueDate ? String(F.dueDate).slice(0, 10) : "",
|
|
68
|
+
estimateHours: i(F?.estimateMinutes)
|
|
69
|
+
}), [F]), [L, R] = c({}), z = {
|
|
70
|
+
...I,
|
|
71
|
+
...L
|
|
72
|
+
}, B = (e) => R(e), V = o(z.storyPoints, { min: 0 }), H = a(z.estimateHours), U = async () => {
|
|
73
|
+
if (!d || !w || V.invalid || H.invalid) return;
|
|
74
|
+
if (!z.title.trim()) {
|
|
75
|
+
M("A task needs a title.");
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
75
78
|
let e = (e) => e.split(",").map((e) => e.trim()).filter(Boolean), t = {
|
|
76
|
-
title:
|
|
77
|
-
description:
|
|
78
|
-
priority:
|
|
79
|
-
type:
|
|
80
|
-
storyPoints:
|
|
81
|
-
labels: e(
|
|
82
|
-
dueDate:
|
|
79
|
+
title: z.title.trim(),
|
|
80
|
+
description: z.description.trim() || null,
|
|
81
|
+
priority: z.priority,
|
|
82
|
+
type: z.type,
|
|
83
|
+
storyPoints: V.value ?? null,
|
|
84
|
+
labels: e(z.labels),
|
|
85
|
+
dueDate: z.dueDate || null,
|
|
86
|
+
estimateMinutes: H.minutes ?? null
|
|
83
87
|
}, r = {
|
|
84
|
-
title:
|
|
85
|
-
description:
|
|
86
|
-
priority:
|
|
87
|
-
type:
|
|
88
|
-
storyPoints:
|
|
89
|
-
labels: e(
|
|
90
|
-
dueDate:
|
|
88
|
+
title: I.title,
|
|
89
|
+
description: I.description.trim() || null,
|
|
90
|
+
priority: I.priority,
|
|
91
|
+
type: I.type,
|
|
92
|
+
storyPoints: I.storyPoints === "" ? null : Number(I.storyPoints),
|
|
93
|
+
labels: e(I.labels),
|
|
94
|
+
dueDate: I.dueDate || null,
|
|
95
|
+
estimateMinutes: a(I.estimateHours).minutes ?? null
|
|
91
96
|
}, i = Object.fromEntries(Object.entries(t).filter(([e, t]) => JSON.stringify(t) !== JSON.stringify(r[e])));
|
|
92
|
-
if (
|
|
93
|
-
|
|
97
|
+
if (M(null), P(!1), Object.keys(i).length === 0) {
|
|
98
|
+
P(!0);
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
96
101
|
try {
|
|
97
|
-
await n(
|
|
98
|
-
workItemId:
|
|
102
|
+
await n(k({ variables: {
|
|
103
|
+
workItemId: w,
|
|
99
104
|
input: i
|
|
100
|
-
} })),
|
|
105
|
+
} })), P(!0);
|
|
101
106
|
} catch (e) {
|
|
102
|
-
|
|
107
|
+
M(e instanceof Error ? e.message : "The task could not be saved.");
|
|
103
108
|
}
|
|
104
109
|
};
|
|
105
|
-
if (
|
|
110
|
+
if (D && !E) return /* @__PURE__ */ m("div", {
|
|
106
111
|
className: "flex items-center justify-center p-12",
|
|
107
|
-
children: /* @__PURE__ */
|
|
112
|
+
children: /* @__PURE__ */ m("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
|
|
108
113
|
});
|
|
109
|
-
if (
|
|
114
|
+
if (O) return /* @__PURE__ */ h("div", {
|
|
110
115
|
className: "p-6 text-destructive",
|
|
111
|
-
children: ["Error: ",
|
|
116
|
+
children: ["Error: ", O.message]
|
|
112
117
|
});
|
|
113
|
-
if (!
|
|
118
|
+
if (!E?.planmagnetWorkItem) return /* @__PURE__ */ m("div", {
|
|
114
119
|
className: "p-6 text-muted-foreground",
|
|
115
120
|
children: "Task not found."
|
|
116
121
|
});
|
|
117
|
-
let
|
|
118
|
-
return /* @__PURE__ */
|
|
122
|
+
let W = E.planmagnetWorkItem;
|
|
123
|
+
return /* @__PURE__ */ h("div", {
|
|
119
124
|
className: "space-y-6 p-6 max-w-4xl",
|
|
120
125
|
children: [
|
|
121
|
-
/* @__PURE__ */
|
|
126
|
+
/* @__PURE__ */ h("div", {
|
|
122
127
|
className: "flex items-center justify-between",
|
|
123
|
-
children: [/* @__PURE__ */
|
|
128
|
+
children: [/* @__PURE__ */ h("div", {
|
|
124
129
|
className: "flex items-center gap-3",
|
|
125
|
-
children: [/* @__PURE__ */
|
|
130
|
+
children: [/* @__PURE__ */ m(g, {
|
|
126
131
|
variant: "ghost",
|
|
127
132
|
size: "sm",
|
|
128
133
|
"aria-label": "Back to the task",
|
|
129
|
-
onClick: () =>
|
|
130
|
-
children: /* @__PURE__ */
|
|
131
|
-
}), /* @__PURE__ */
|
|
134
|
+
onClick: () => T(`/tasks/task-detail-page?taskId=${encodeURIComponent(w)}`),
|
|
135
|
+
children: /* @__PURE__ */ m(l, { className: "h-4 w-4" })
|
|
136
|
+
}), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("h1", {
|
|
132
137
|
className: "text-2xl font-semibold",
|
|
133
138
|
children: "Edit Task"
|
|
134
|
-
}), /* @__PURE__ */
|
|
139
|
+
}), /* @__PURE__ */ m("p", {
|
|
135
140
|
className: "text-sm text-muted-foreground",
|
|
136
|
-
children:
|
|
141
|
+
children: W.title
|
|
137
142
|
})] })]
|
|
138
|
-
}), /* @__PURE__ */
|
|
143
|
+
}), /* @__PURE__ */ h(g, {
|
|
139
144
|
size: "sm",
|
|
140
|
-
onClick:
|
|
141
|
-
disabled:
|
|
142
|
-
children: [/* @__PURE__ */
|
|
145
|
+
onClick: U,
|
|
146
|
+
disabled: A || V.invalid || H.invalid,
|
|
147
|
+
children: [/* @__PURE__ */ m(u, { className: "mr-1 h-4 w-4" }), A ? "Saving..." : "Save Changes"]
|
|
143
148
|
})]
|
|
144
149
|
}),
|
|
145
|
-
|
|
150
|
+
N && /* @__PURE__ */ m("div", {
|
|
146
151
|
role: "status",
|
|
147
152
|
className: "p-3 bg-muted text-status-success-text rounded-md text-sm",
|
|
148
153
|
children: "Changes saved."
|
|
149
154
|
}),
|
|
150
|
-
|
|
155
|
+
j && /* @__PURE__ */ m("div", {
|
|
151
156
|
role: "alert",
|
|
152
157
|
className: "p-3 bg-destructive/10 text-destructive rounded-md text-sm",
|
|
153
|
-
children:
|
|
158
|
+
children: j
|
|
154
159
|
}),
|
|
155
|
-
/* @__PURE__ */
|
|
160
|
+
/* @__PURE__ */ h(_, { children: [/* @__PURE__ */ m(y, { children: /* @__PURE__ */ m(b, { children: "Task Details" }) }), /* @__PURE__ */ h(v, {
|
|
156
161
|
className: "space-y-4",
|
|
157
162
|
children: [
|
|
158
|
-
/* @__PURE__ */
|
|
163
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
|
|
159
164
|
className: "text-sm font-medium",
|
|
160
165
|
children: "Title"
|
|
161
|
-
}), /* @__PURE__ */
|
|
162
|
-
value:
|
|
163
|
-
onChange: (e) =>
|
|
164
|
-
...
|
|
166
|
+
}), /* @__PURE__ */ m(x, {
|
|
167
|
+
value: z.title,
|
|
168
|
+
onChange: (e) => B({
|
|
169
|
+
...z,
|
|
165
170
|
title: e.target.value
|
|
166
171
|
}),
|
|
167
172
|
className: "mt-1"
|
|
168
173
|
})] }),
|
|
169
|
-
/* @__PURE__ */
|
|
174
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
|
|
170
175
|
className: "text-sm font-medium",
|
|
171
176
|
children: "Description"
|
|
172
|
-
}), /* @__PURE__ */
|
|
177
|
+
}), /* @__PURE__ */ m("textarea", {
|
|
173
178
|
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[100px]",
|
|
174
|
-
value:
|
|
175
|
-
onChange: (e) =>
|
|
176
|
-
...
|
|
179
|
+
value: z.description,
|
|
180
|
+
onChange: (e) => B({
|
|
181
|
+
...z,
|
|
177
182
|
description: e.target.value
|
|
178
183
|
})
|
|
179
184
|
})] }),
|
|
180
|
-
/* @__PURE__ */
|
|
185
|
+
/* @__PURE__ */ h("div", {
|
|
181
186
|
className: "grid grid-cols-3 gap-4",
|
|
182
187
|
children: [
|
|
183
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
|
|
184
189
|
className: "text-sm font-medium",
|
|
185
190
|
children: "Priority"
|
|
186
|
-
}), /* @__PURE__ */
|
|
191
|
+
}), /* @__PURE__ */ m("select", {
|
|
187
192
|
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
188
|
-
value:
|
|
189
|
-
onChange: (e) =>
|
|
190
|
-
...
|
|
193
|
+
value: z.priority,
|
|
194
|
+
onChange: (e) => B({
|
|
195
|
+
...z,
|
|
191
196
|
priority: e.target.value
|
|
192
197
|
}),
|
|
193
|
-
children: r.map((e) => /* @__PURE__ */
|
|
198
|
+
children: r.map((e) => /* @__PURE__ */ m("option", {
|
|
194
199
|
value: e,
|
|
195
200
|
children: e.toLowerCase()
|
|
196
201
|
}, e))
|
|
197
202
|
})] }),
|
|
198
|
-
/* @__PURE__ */
|
|
203
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
|
|
199
204
|
className: "text-sm font-medium",
|
|
200
205
|
children: "Type"
|
|
201
|
-
}), /* @__PURE__ */
|
|
206
|
+
}), /* @__PURE__ */ m("select", {
|
|
202
207
|
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
203
|
-
value:
|
|
204
|
-
onChange: (e) =>
|
|
205
|
-
...
|
|
208
|
+
value: z.type,
|
|
209
|
+
onChange: (e) => B({
|
|
210
|
+
...z,
|
|
206
211
|
type: e.target.value
|
|
207
212
|
}),
|
|
208
213
|
children: [
|
|
@@ -211,31 +216,31 @@ function S() {
|
|
|
211
216
|
"TASK",
|
|
212
217
|
"EPIC",
|
|
213
218
|
"SUB_TASK"
|
|
214
|
-
].map((e) => /* @__PURE__ */
|
|
219
|
+
].map((e) => /* @__PURE__ */ m("option", {
|
|
215
220
|
value: e,
|
|
216
221
|
children: e.toLowerCase().replace("_", " ")
|
|
217
222
|
}, e))
|
|
218
223
|
})] }),
|
|
219
|
-
/* @__PURE__ */
|
|
220
|
-
/* @__PURE__ */
|
|
224
|
+
/* @__PURE__ */ h("div", { children: [
|
|
225
|
+
/* @__PURE__ */ m("label", {
|
|
221
226
|
htmlFor: "task-story-points",
|
|
222
227
|
className: "text-sm font-medium",
|
|
223
228
|
children: "Story Points"
|
|
224
229
|
}),
|
|
225
|
-
/* @__PURE__ */
|
|
230
|
+
/* @__PURE__ */ m(x, {
|
|
226
231
|
id: "task-story-points",
|
|
227
232
|
type: "number",
|
|
228
233
|
min: 0,
|
|
229
234
|
step: 1,
|
|
230
|
-
value:
|
|
231
|
-
onChange: (e) =>
|
|
232
|
-
...
|
|
235
|
+
value: z.storyPoints,
|
|
236
|
+
onChange: (e) => B({
|
|
237
|
+
...z,
|
|
233
238
|
storyPoints: e.target.value
|
|
234
239
|
}),
|
|
235
|
-
"aria-invalid":
|
|
240
|
+
"aria-invalid": V.invalid,
|
|
236
241
|
className: "mt-1"
|
|
237
242
|
}),
|
|
238
|
-
|
|
243
|
+
V.invalid && /* @__PURE__ */ m("p", {
|
|
239
244
|
role: "alert",
|
|
240
245
|
className: "mt-1 text-xs text-destructive",
|
|
241
246
|
children: "Story points must be a whole number, 0 or more."
|
|
@@ -243,30 +248,63 @@ function S() {
|
|
|
243
248
|
] })
|
|
244
249
|
]
|
|
245
250
|
}),
|
|
246
|
-
/* @__PURE__ */
|
|
247
|
-
className: "grid grid-cols-
|
|
248
|
-
children: [
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
children:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
251
|
+
/* @__PURE__ */ h("div", {
|
|
252
|
+
className: "grid grid-cols-3 gap-4",
|
|
253
|
+
children: [
|
|
254
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
|
|
255
|
+
className: "text-sm font-medium",
|
|
256
|
+
children: "Due Date"
|
|
257
|
+
}), /* @__PURE__ */ m(x, {
|
|
258
|
+
type: "date",
|
|
259
|
+
value: z.dueDate,
|
|
260
|
+
onChange: (e) => B({
|
|
261
|
+
...z,
|
|
262
|
+
dueDate: e.target.value
|
|
263
|
+
}),
|
|
264
|
+
className: "mt-1"
|
|
265
|
+
})] }),
|
|
266
|
+
/* @__PURE__ */ h("div", { children: [
|
|
267
|
+
/* @__PURE__ */ m("label", {
|
|
268
|
+
htmlFor: "task-estimate-hours",
|
|
269
|
+
className: "text-sm font-medium",
|
|
270
|
+
children: "Estimate (hours)"
|
|
271
|
+
}),
|
|
272
|
+
/* @__PURE__ */ m(x, {
|
|
273
|
+
id: "task-estimate-hours",
|
|
274
|
+
type: "number",
|
|
275
|
+
min: 0,
|
|
276
|
+
step: .25,
|
|
277
|
+
value: z.estimateHours,
|
|
278
|
+
onChange: (e) => B({
|
|
279
|
+
...z,
|
|
280
|
+
estimateHours: e.target.value
|
|
281
|
+
}),
|
|
282
|
+
"aria-invalid": H.invalid,
|
|
283
|
+
"aria-describedby": "task-estimate-hint",
|
|
284
|
+
className: "mt-1"
|
|
285
|
+
}),
|
|
286
|
+
H.invalid ? /* @__PURE__ */ m("p", {
|
|
287
|
+
role: "alert",
|
|
288
|
+
className: "mt-1 text-xs text-destructive",
|
|
289
|
+
children: "The estimate must be a number of hours, 0 or more."
|
|
290
|
+
}) : /* @__PURE__ */ m("p", {
|
|
291
|
+
id: "task-estimate-hint",
|
|
292
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
293
|
+
children: "Used as the task's duration on the critical path."
|
|
294
|
+
})
|
|
295
|
+
] }),
|
|
296
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("label", {
|
|
297
|
+
className: "text-sm font-medium",
|
|
298
|
+
children: "Labels"
|
|
299
|
+
}), /* @__PURE__ */ m(x, {
|
|
300
|
+
value: z.labels,
|
|
301
|
+
onChange: (e) => B({
|
|
302
|
+
...z,
|
|
303
|
+
labels: e.target.value
|
|
304
|
+
}),
|
|
305
|
+
className: "mt-1"
|
|
306
|
+
})] })
|
|
307
|
+
]
|
|
270
308
|
})
|
|
271
309
|
]
|
|
272
310
|
})] })
|
|
@@ -274,6 +312,6 @@ function S() {
|
|
|
274
312
|
});
|
|
275
313
|
}
|
|
276
314
|
//#endregion
|
|
277
|
-
export {
|
|
315
|
+
export { w as TasksEditTaskPagePage };
|
|
278
316
|
|
|
279
317
|
//# sourceMappingURL=TasksEditTaskPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TasksEditTaskPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksEditTaskPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState, useMemo } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery, useMutation } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Save, ArrowLeft } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { parseIntField } from '../utils/formValidation';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WORK_ITEM_PRIORITIES } from '../controls/WorkItemControls';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItem($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n title\n description\n type\n status\n priority\n key\n storyPoints\n dueDate\n labels\n tags\n createdAt\n updatedAt\n }\n }\n`;\n\nconst UPDATE_WORK_ITEM = gql`\n mutation UpdateWorkItem($workItemId: ID!, $input: PlanmagnetUpdateWorkItemInput!) {\n planmagnetUpdateWorkItem(id: $workItemId, input: $input) {\n id\n title\n description\n type\n status\n priority\n storyPoints\n dueDate\n labels\n updatedAt\n }\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n storyPoints: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport function TasksEditTaskPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from the route params.\n // For now we use a placeholder that the host shell should provide.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n const navigate = usePlanMagnetNavigate();\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n const [updateWorkItem, { loading: saving }] = useMutation(UPDATE_WORK_ITEM, {\n refetchQueries: ['GetWorkItems', 'GetWorkItemDetail', 'GetWorkItemCounts', 'ProjectBoard'],\n awaitRefetchQueries: true,\n });\n const [saveError, setSaveError] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n\n const workItem = data?.planmagnetWorkItem;\n\n const defaultForm = useMemo(\n () => ({\n title: workItem?.title ?? '',\n description: workItem?.description ?? '',\n priority: workItem?.priority ?? 'MEDIUM',\n type: workItem?.type ?? 'STORY',\n storyPoints: workItem?.storyPoints?.toString() ?? '',\n labels: (workItem?.labels ?? []).join(', '),\n // A date input needs YYYY-MM-DD; the API returns a full ISO timestamp.\n dueDate: workItem?.dueDate ? String(workItem.dueDate).slice(0, 10) : '',\n }),\n [workItem]\n );\n\n const [formOverrides, setFormOverrides] = useState<Record<string, string>>({});\n const form = { ...defaultForm, ...formOverrides };\n const setForm = (updates: Record<string, string>) => setFormOverrides(updates);\n\n // Story points are a whole number, 0 or more (BOFF-7833); the service refuses anything else.\n const storyPoints = parseIntField(form.storyPoints, { min: 0 });\n\n // BOFF-7979: only changed fields are sent; an emptied field is cleared (it used to be omitted, so the old value\n // stayed); success is confirmed (there was no message at all).\n const handleSave = async () => {\n if (!workspaceId || !workItemId || storyPoints.invalid) return;\n if (!form.title.trim()) {\n setSaveError('A task needs a title.');\n return;\n }\n const labelList = (v: string) =>\n v\n .split(',')\n .map((l) => l.trim())\n .filter(Boolean);\n const next: Record<string, unknown> = {\n title: form.title.trim(),\n description: form.description.trim() || null,\n priority: form.priority,\n type: form.type,\n storyPoints: storyPoints.value ?? null,\n labels: labelList(form.labels),\n dueDate: form.dueDate || null,\n };\n const before: Record<string, unknown> = {\n title: defaultForm.title,\n description: defaultForm.description.trim() || null,\n priority: defaultForm.priority,\n type: defaultForm.type,\n storyPoints: defaultForm.storyPoints === '' ? null : Number(defaultForm.storyPoints),\n labels: labelList(defaultForm.labels),\n dueDate: defaultForm.dueDate || null,\n };\n const input = Object.fromEntries(\n Object.entries(next).filter(([k, v]) => JSON.stringify(v) !== JSON.stringify(before[k]))\n );\n setSaveError(null);\n setSaved(false);\n if (Object.keys(input).length === 0) {\n setSaved(true);\n return;\n }\n try {\n await throwOnError(updateWorkItem({ variables: { workItemId, input } }));\n setSaved(true);\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'The task could not be saved.');\n }\n };\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Error: {error.message}</div>;\n }\n\n if (!data?.planmagnetWorkItem) {\n return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n }\n\n const task = data.planmagnetWorkItem;\n\n return (\n <div className=\"space-y-6 p-6 max-w-4xl\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Back to the task\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(workItemId)}`)\n }\n >\n <ArrowLeft className=\"h-4 w-4\" />\n </Button>\n <div>\n <h1 className=\"text-2xl font-semibold\">Edit Task</h1>\n <p className=\"text-sm text-muted-foreground\">{task.title}</p>\n </div>\n </div>\n <Button size=\"sm\" onClick={handleSave} disabled={saving || storyPoints.invalid}>\n <Save className=\"mr-1 h-4 w-4\" />\n {saving ? 'Saving...' : 'Save Changes'}\n </Button>\n </div>\n {saved && (\n <div role=\"status\" className=\"p-3 bg-muted text-status-success-text rounded-md text-sm\">\n Changes saved.\n </div>\n )}\n {saveError && (\n <div role=\"alert\" className=\"p-3 bg-destructive/10 text-destructive rounded-md text-sm\">\n {saveError}\n </div>\n )}\n <Card>\n <CardHeader>\n <CardTitle>Task Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium\">Title</label>\n <Input\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Description</label>\n <textarea\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[100px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-3 gap-4\">\n <div>\n <label className=\"text-sm font-medium\">Priority</label>\n <select\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.priority}\n onChange={(e) => setForm({ ...form, priority: e.target.value })}\n >\n {/* Every WorkItemPriority: NONE was missing, so a NONE task showed as \"low\" (BOFF-7985). */}\n {WORK_ITEM_PRIORITIES.map((p) => (\n <option key={p} value={p}>\n {p.toLowerCase()}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className=\"text-sm font-medium\">Type</label>\n <select\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.type}\n onChange={(e) => setForm({ ...form, type: e.target.value })}\n >\n {['STORY', 'BUG', 'TASK', 'EPIC', 'SUB_TASK'].map((t) => (\n <option key={t} value={t}>\n {t.toLowerCase().replace('_', ' ')}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"task-story-points\" className=\"text-sm font-medium\">\n Story Points\n </label>\n <Input\n id=\"task-story-points\"\n type=\"number\"\n min={0}\n step={1}\n value={form.storyPoints}\n onChange={(e) => setForm({ ...form, storyPoints: e.target.value })}\n aria-invalid={storyPoints.invalid}\n className=\"mt-1\"\n />\n {storyPoints.invalid && (\n <p role=\"alert\" className=\"mt-1 text-xs text-destructive\">\n Story points must be a whole number, 0 or more.\n </p>\n )}\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"text-sm font-medium\">Due Date</label>\n <Input\n type=\"date\"\n value={form.dueDate}\n onChange={(e) => setForm({ ...form, dueDate: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Labels</label>\n <Input\n value={form.labels}\n onChange={(e) => setForm({ ...form, labels: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TasksEditTaskPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksEditTaskPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState, useMemo } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery, useMutation } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Save, ArrowLeft } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { estimateHoursText, parseEstimateHours, parseIntField } from '../utils/formValidation';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WORK_ITEM_PRIORITIES } from '../controls/WorkItemControls';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItem($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n title\n description\n type\n status\n priority\n key\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n updatedAt\n }\n }\n`;\n\nconst UPDATE_WORK_ITEM = gql`\n mutation UpdateWorkItem($workItemId: ID!, $input: PlanmagnetUpdateWorkItemInput!) {\n planmagnetUpdateWorkItem(id: $workItemId, input: $input) {\n id\n title\n description\n type\n status\n priority\n storyPoints\n estimateMinutes\n dueDate\n labels\n updatedAt\n }\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport function TasksEditTaskPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from the route params.\n // For now we use a placeholder that the host shell should provide.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n const navigate = usePlanMagnetNavigate();\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n const [updateWorkItem, { loading: saving }] = useMutation(UPDATE_WORK_ITEM, {\n refetchQueries: ['GetWorkItems', 'GetWorkItemDetail', 'GetWorkItemCounts', 'ProjectBoard'],\n awaitRefetchQueries: true,\n });\n const [saveError, setSaveError] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n\n const workItem = data?.planmagnetWorkItem;\n\n const defaultForm = useMemo(\n () => ({\n title: workItem?.title ?? '',\n description: workItem?.description ?? '',\n priority: workItem?.priority ?? 'MEDIUM',\n type: workItem?.type ?? 'STORY',\n storyPoints: workItem?.storyPoints?.toString() ?? '',\n labels: (workItem?.labels ?? []).join(', '),\n // A date input needs YYYY-MM-DD; the API returns a full ISO timestamp.\n dueDate: workItem?.dueDate ? String(workItem.dueDate).slice(0, 10) : '',\n estimateHours: estimateHoursText(workItem?.estimateMinutes),\n }),\n [workItem]\n );\n\n const [formOverrides, setFormOverrides] = useState<Record<string, string>>({});\n const form = { ...defaultForm, ...formOverrides };\n const setForm = (updates: Record<string, string>) => setFormOverrides(updates);\n\n // Story points are a whole number, 0 or more (BOFF-7833); the service refuses anything else.\n const storyPoints = parseIntField(form.storyPoints, { min: 0 });\n const estimate = parseEstimateHours(form.estimateHours);\n\n // BOFF-7979: only changed fields are sent; an emptied field is cleared (it used to be omitted, so the old value\n // stayed); success is confirmed (there was no message at all).\n const handleSave = async () => {\n if (!workspaceId || !workItemId || storyPoints.invalid || estimate.invalid) return;\n if (!form.title.trim()) {\n setSaveError('A task needs a title.');\n return;\n }\n const labelList = (v: string) =>\n v\n .split(',')\n .map((l) => l.trim())\n .filter(Boolean);\n const next: Record<string, unknown> = {\n title: form.title.trim(),\n description: form.description.trim() || null,\n priority: form.priority,\n type: form.type,\n storyPoints: storyPoints.value ?? null,\n labels: labelList(form.labels),\n dueDate: form.dueDate || null,\n estimateMinutes: estimate.minutes ?? null,\n };\n const before: Record<string, unknown> = {\n title: defaultForm.title,\n description: defaultForm.description.trim() || null,\n priority: defaultForm.priority,\n type: defaultForm.type,\n storyPoints: defaultForm.storyPoints === '' ? null : Number(defaultForm.storyPoints),\n labels: labelList(defaultForm.labels),\n dueDate: defaultForm.dueDate || null,\n // Compared through the same hours round-trip as `next`, so an untouched estimate never counts as a change.\n estimateMinutes: parseEstimateHours(defaultForm.estimateHours).minutes ?? null,\n };\n const input = Object.fromEntries(\n Object.entries(next).filter(([k, v]) => JSON.stringify(v) !== JSON.stringify(before[k]))\n );\n setSaveError(null);\n setSaved(false);\n if (Object.keys(input).length === 0) {\n setSaved(true);\n return;\n }\n try {\n await throwOnError(updateWorkItem({ variables: { workItemId, input } }));\n setSaved(true);\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'The task could not be saved.');\n }\n };\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Error: {error.message}</div>;\n }\n\n if (!data?.planmagnetWorkItem) {\n return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n }\n\n const task = data.planmagnetWorkItem;\n\n return (\n <div className=\"space-y-6 p-6 max-w-4xl\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Back to the task\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(workItemId)}`)\n }\n >\n <ArrowLeft className=\"h-4 w-4\" />\n </Button>\n <div>\n <h1 className=\"text-2xl font-semibold\">Edit Task</h1>\n <p className=\"text-sm text-muted-foreground\">{task.title}</p>\n </div>\n </div>\n <Button\n size=\"sm\"\n onClick={handleSave}\n disabled={saving || storyPoints.invalid || estimate.invalid}\n >\n <Save className=\"mr-1 h-4 w-4\" />\n {saving ? 'Saving...' : 'Save Changes'}\n </Button>\n </div>\n {saved && (\n <div role=\"status\" className=\"p-3 bg-muted text-status-success-text rounded-md text-sm\">\n Changes saved.\n </div>\n )}\n {saveError && (\n <div role=\"alert\" className=\"p-3 bg-destructive/10 text-destructive rounded-md text-sm\">\n {saveError}\n </div>\n )}\n <Card>\n <CardHeader>\n <CardTitle>Task Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <label className=\"text-sm font-medium\">Title</label>\n <Input\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Description</label>\n <textarea\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[100px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-3 gap-4\">\n <div>\n <label className=\"text-sm font-medium\">Priority</label>\n <select\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.priority}\n onChange={(e) => setForm({ ...form, priority: e.target.value })}\n >\n {/* Every WorkItemPriority: NONE was missing, so a NONE task showed as \"low\" (BOFF-7985). */}\n {WORK_ITEM_PRIORITIES.map((p) => (\n <option key={p} value={p}>\n {p.toLowerCase()}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className=\"text-sm font-medium\">Type</label>\n <select\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={form.type}\n onChange={(e) => setForm({ ...form, type: e.target.value })}\n >\n {['STORY', 'BUG', 'TASK', 'EPIC', 'SUB_TASK'].map((t) => (\n <option key={t} value={t}>\n {t.toLowerCase().replace('_', ' ')}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"task-story-points\" className=\"text-sm font-medium\">\n Story Points\n </label>\n <Input\n id=\"task-story-points\"\n type=\"number\"\n min={0}\n step={1}\n value={form.storyPoints}\n onChange={(e) => setForm({ ...form, storyPoints: e.target.value })}\n aria-invalid={storyPoints.invalid}\n className=\"mt-1\"\n />\n {storyPoints.invalid && (\n <p role=\"alert\" className=\"mt-1 text-xs text-destructive\">\n Story points must be a whole number, 0 or more.\n </p>\n )}\n </div>\n </div>\n <div className=\"grid grid-cols-3 gap-4\">\n <div>\n <label className=\"text-sm font-medium\">Due Date</label>\n <Input\n type=\"date\"\n value={form.dueDate}\n onChange={(e) => setForm({ ...form, dueDate: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n <div>\n <label htmlFor=\"task-estimate-hours\" className=\"text-sm font-medium\">\n Estimate (hours)\n </label>\n <Input\n id=\"task-estimate-hours\"\n type=\"number\"\n min={0}\n step={0.25}\n value={form.estimateHours}\n onChange={(e) => setForm({ ...form, estimateHours: e.target.value })}\n aria-invalid={estimate.invalid}\n aria-describedby=\"task-estimate-hint\"\n className=\"mt-1\"\n />\n {estimate.invalid ? (\n <p role=\"alert\" className=\"mt-1 text-xs text-destructive\">\n The estimate must be a number of hours, 0 or more.\n </p>\n ) : (\n <p id=\"task-estimate-hint\" className=\"mt-1 text-xs text-muted-foreground\">\n Used as the task's duration on the critical path.\n </p>\n )}\n </div>\n <div>\n <label className=\"text-sm font-medium\">Labels</label>\n <Input\n value={form.labels}\n onChange={(e) => setForm({ ...form, labels: e.target.value })}\n className=\"mt-1\"\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -15,4 +15,15 @@ export declare function parseIntField(raw: string, opts: {
|
|
|
15
15
|
value?: number;
|
|
16
16
|
invalid: boolean;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Parse an "estimate in hours" field into the API's `estimateMinutes` (whole minutes). Empty is "not set"
|
|
20
|
+
* (valid). Decimals are allowed (1.5 = 90 min); anything negative, non-numeric or above `maxHours` is invalid.
|
|
21
|
+
* The critical path uses this estimate as a task's duration when it has no start/due span.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseEstimateHours(raw: string, maxHours?: number): {
|
|
24
|
+
minutes?: number;
|
|
25
|
+
invalid: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** `estimateMinutes` → the hours shown in an estimate field ('' when unset); at most 2 decimals. */
|
|
28
|
+
export declare function estimateHoursText(minutes: number | null | undefined): string;
|
|
18
29
|
//# sourceMappingURL=formValidation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formValidation.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/formValidation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,yGAAyG;AACzG,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B,OAAO,CAGT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAOtC"}
|
|
1
|
+
{"version":3,"file":"formValidation.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/formValidation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,yGAAyG;AACzG,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B,OAAO,CAGT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAOtC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,SAAQ,GACf;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAMxC;AAED,oGAAoG;AACpG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAG5E"}
|
|
@@ -11,7 +11,19 @@ function t(e, t) {
|
|
|
11
11
|
invalid: !1
|
|
12
12
|
} : { invalid: !0 };
|
|
13
13
|
}
|
|
14
|
+
function n(e, t = 1e4) {
|
|
15
|
+
let n = e.trim();
|
|
16
|
+
if (n === "") return { invalid: !1 };
|
|
17
|
+
let r = Number(n);
|
|
18
|
+
return Number.isFinite(r) && r >= 0 && r <= t ? {
|
|
19
|
+
minutes: Math.round(r * 60),
|
|
20
|
+
invalid: !1
|
|
21
|
+
} : { invalid: !0 };
|
|
22
|
+
}
|
|
23
|
+
function r(e) {
|
|
24
|
+
return e == null ? "" : String(Math.round(e / 60 * 100) / 100);
|
|
25
|
+
}
|
|
14
26
|
//#endregion
|
|
15
|
-
export { e as endBeforeStart, t as parseIntField };
|
|
27
|
+
export { e as endBeforeStart, r as estimateHoursText, n as parseEstimateHours, t as parseIntField };
|
|
16
28
|
|
|
17
29
|
//# sourceMappingURL=formValidation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formValidation.js","names":[],"sources":["../../../src/planmagnet/utils/formValidation.ts"],"sourcesContent":["/**\n * Form-side checks (BOFF-7833). The service is the real gate — every rule here is also enforced there, and a\n * refusal comes back with its reason — these only let a form say so before sending.\n */\n\n/** True when both dates are set and the end (YYYY-MM-DD) is before the start. Equal days are allowed. */\nexport function endBeforeStart(\n start: string | null | undefined,\n end: string | null | undefined\n): boolean {\n if (!start || !end) return false;\n return end.slice(0, 10) < start.slice(0, 10);\n}\n\n/**\n * Parse a whole-number field. Empty is \"not set\" (valid). Anything that is not a whole number inside\n * [min, max] is invalid.\n */\nexport function parseIntField(\n raw: string,\n opts: { min: number; max?: number }\n): { value?: number; invalid: boolean } {\n const trimmed = raw.trim();\n if (trimmed === '') return { invalid: false };\n const value = Number(trimmed);\n const ok =\n Number.isInteger(value) && value >= opts.min && (opts.max === undefined || value <= opts.max);\n return ok ? { value, invalid: false } : { invalid: true };\n}\n"],"mappings":";AAMA,SAAgB,EACd,GACA,GACS;AAET,QADI,CAAC,KAAS,CAAC,IAAY,KACpB,EAAI,MAAM,GAAG,GAAG,GAAG,EAAM,MAAM,GAAG,GAAG;;AAO9C,SAAgB,EACd,GACA,GACsC;CACtC,IAAM,IAAU,EAAI,MAAM;AAC1B,KAAI,MAAY,GAAI,QAAO,EAAE,SAAS,IAAO;CAC7C,IAAM,IAAQ,OAAO,EAAQ;AAG7B,QADE,OAAO,UAAU,EAAM,IAAI,KAAS,EAAK,QAAQ,EAAK,QAAQ,KAAA,KAAa,KAAS,EAAK,OAC/E;EAAE;EAAO,SAAS;EAAO,GAAG,EAAE,SAAS,IAAM"}
|
|
1
|
+
{"version":3,"file":"formValidation.js","names":[],"sources":["../../../src/planmagnet/utils/formValidation.ts"],"sourcesContent":["/**\n * Form-side checks (BOFF-7833). The service is the real gate — every rule here is also enforced there, and a\n * refusal comes back with its reason — these only let a form say so before sending.\n */\n\n/** True when both dates are set and the end (YYYY-MM-DD) is before the start. Equal days are allowed. */\nexport function endBeforeStart(\n start: string | null | undefined,\n end: string | null | undefined\n): boolean {\n if (!start || !end) return false;\n return end.slice(0, 10) < start.slice(0, 10);\n}\n\n/**\n * Parse a whole-number field. Empty is \"not set\" (valid). Anything that is not a whole number inside\n * [min, max] is invalid.\n */\nexport function parseIntField(\n raw: string,\n opts: { min: number; max?: number }\n): { value?: number; invalid: boolean } {\n const trimmed = raw.trim();\n if (trimmed === '') return { invalid: false };\n const value = Number(trimmed);\n const ok =\n Number.isInteger(value) && value >= opts.min && (opts.max === undefined || value <= opts.max);\n return ok ? { value, invalid: false } : { invalid: true };\n}\n\n/**\n * Parse an \"estimate in hours\" field into the API's `estimateMinutes` (whole minutes). Empty is \"not set\"\n * (valid). Decimals are allowed (1.5 = 90 min); anything negative, non-numeric or above `maxHours` is invalid.\n * The critical path uses this estimate as a task's duration when it has no start/due span.\n */\nexport function parseEstimateHours(\n raw: string,\n maxHours = 10000\n): { minutes?: number; invalid: boolean } {\n const trimmed = raw.trim();\n if (trimmed === '') return { invalid: false };\n const hours = Number(trimmed);\n const ok = Number.isFinite(hours) && hours >= 0 && hours <= maxHours;\n return ok ? { minutes: Math.round(hours * 60), invalid: false } : { invalid: true };\n}\n\n/** `estimateMinutes` → the hours shown in an estimate field ('' when unset); at most 2 decimals. */\nexport function estimateHoursText(minutes: number | null | undefined): string {\n if (minutes === null || minutes === undefined) return '';\n return String(Math.round((minutes / 60) * 100) / 100);\n}\n"],"mappings":";AAMA,SAAgB,EACd,GACA,GACS;AAET,QADI,CAAC,KAAS,CAAC,IAAY,KACpB,EAAI,MAAM,GAAG,GAAG,GAAG,EAAM,MAAM,GAAG,GAAG;;AAO9C,SAAgB,EACd,GACA,GACsC;CACtC,IAAM,IAAU,EAAI,MAAM;AAC1B,KAAI,MAAY,GAAI,QAAO,EAAE,SAAS,IAAO;CAC7C,IAAM,IAAQ,OAAO,EAAQ;AAG7B,QADE,OAAO,UAAU,EAAM,IAAI,KAAS,EAAK,QAAQ,EAAK,QAAQ,KAAA,KAAa,KAAS,EAAK,OAC/E;EAAE;EAAO,SAAS;EAAO,GAAG,EAAE,SAAS,IAAM;;AAQ3D,SAAgB,EACd,GACA,IAAW,KAC6B;CACxC,IAAM,IAAU,EAAI,MAAM;AAC1B,KAAI,MAAY,GAAI,QAAO,EAAE,SAAS,IAAO;CAC7C,IAAM,IAAQ,OAAO,EAAQ;AAE7B,QADW,OAAO,SAAS,EAAM,IAAI,KAAS,KAAK,KAAS,IAChD;EAAE,SAAS,KAAK,MAAM,IAAQ,GAAG;EAAE,SAAS;EAAO,GAAG,EAAE,SAAS,IAAM;;AAIrF,SAAgB,EAAkB,GAA4C;AAE5E,QADI,KAAY,OAAsC,KAC/C,OAAO,KAAK,MAAO,IAAU,KAAM,IAAI,GAAG,IAAI"}
|