@burdenoff/microfe-planmagnet 2026.1001.4 → 2026.1001.6
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/controls/CompetitorControls.d.ts +71 -0
- package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/CompetitorControls.js +346 -12
- package/dist/planmagnet/controls/CompetitorControls.js.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts +5 -0
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.js +363 -218
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +15 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +61 -1
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.d.ts +36 -0
- package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.js +118 -1
- package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.js +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +67 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts.map +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +289 -282
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +126 -63
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +14 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +93 -57
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +125 -83
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +226 -126
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +162 -122
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +135 -109
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +15 -3
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +59 -53
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts +8 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js +164 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js.map +1 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts +2 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts.map +1 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js +155 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js.map +1 -0
- package/package.json +1 -1
|
@@ -16,5 +16,76 @@ export declare function CompetitorLinkCard({ featureRequestId, competitors, }: {
|
|
|
16
16
|
featureRequestId: string;
|
|
17
17
|
competitors: CompetitorNode[];
|
|
18
18
|
}): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
/**
|
|
20
|
+
* Mirrors feature.service.ts#assertCompetitorStringList / #assertCompetitorJsonField — the service's own
|
|
21
|
+
* caps for Competitor.strengths/weaknesses (20 items, 200 characters each) and .pricing/.features (20 KB of
|
|
22
|
+
* UTF-8 bytes each). Enforced here too so a refusal is rare, but the service is still the real gate: whatever
|
|
23
|
+
* message it sends back on a refusal is surfaced verbatim (see `mutationErrorMessage`), never replaced by a
|
|
24
|
+
* generic one.
|
|
25
|
+
*/
|
|
26
|
+
export declare const COMPETITOR_LIST_MAX_ITEMS = 20;
|
|
27
|
+
export declare const COMPETITOR_LIST_ITEM_MAX_LENGTH = 200;
|
|
28
|
+
export declare const COMPETITOR_JSON_FIELD_MAX_BYTES = 20000;
|
|
29
|
+
/** Actual UTF-8 byte count, not `.length` (UTF-16 code units) — the same distinction
|
|
30
|
+
* feature.service.ts#assertCompetitorJsonField draws (see its own "UTF-8 byte cap" fix). */
|
|
31
|
+
export declare function utf8ByteLength(value: string): number;
|
|
32
|
+
export declare function competitorListValid(values: string[]): boolean;
|
|
33
|
+
/** Blank rows are an editing affordance, never a meaningful entry — dropped before the value is sent. */
|
|
34
|
+
export declare function cleanStringList(values: string[]): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Add/remove editor for one of Competitor.strengths/weaknesses. A row over the per-entry character cap is
|
|
37
|
+
* flagged inline; the count against the item cap is always visible, not only once it is hit.
|
|
38
|
+
*/
|
|
39
|
+
export declare function StringListEditor({ label, values, onChange, disabled, placeholder, testId, }: {
|
|
40
|
+
label: string;
|
|
41
|
+
values: string[];
|
|
42
|
+
onChange: (next: string[]) => void;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
testId?: string;
|
|
46
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
export type JsonRowType = 'string' | 'number' | 'boolean' | 'json';
|
|
48
|
+
export interface JsonKeyValueRow {
|
|
49
|
+
key: string;
|
|
50
|
+
type: JsonRowType;
|
|
51
|
+
/** Raw text either way — parsed per `type` only at the point a row list is turned back into JSON.
|
|
52
|
+
* Ignored for `type: 'json'` rows: they write back `raw` instead (see jsonRowsToObject), never a
|
|
53
|
+
* re-parse of this displayed text. */
|
|
54
|
+
value: string;
|
|
55
|
+
/** Only present on a `type: 'json'` row: the original nested object/array/null value, exactly as
|
|
56
|
+
* loaded. This editor can only ever WRITE flat string/number/boolean values (see JsonKeyValueEditor),
|
|
57
|
+
* so a value shaped like this is carried through untouched rather than folded into a flattened string
|
|
58
|
+
* the moment any OTHER row in the same editor changes. */
|
|
59
|
+
raw?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/** A `Competitor.pricing`/`.features` value (already-saved JSON) back into editable rows. Any array/object-
|
|
62
|
+
* shaped value (not produced by this editor, since it only ever writes flat string/number/boolean values) is
|
|
63
|
+
* carried through as a `type: 'json'` row — rendered read-only (see JsonKeyValueEditor) and written back
|
|
64
|
+
* verbatim via `raw` (see jsonRowsToObject) — so the editor never discards data it cannot represent, and
|
|
65
|
+
* never silently flattens it into a string either; it only refuses to let a NEW row be shaped that way. */
|
|
66
|
+
export declare function jsonToRows(json: unknown): JsonKeyValueRow[];
|
|
67
|
+
/** A `type: 'number'` row with a real key but no usable number — blank, or non-numeric text pasted in.
|
|
68
|
+
* Used to BLOCK save (see jsonRowsValid) and to flag the row inline; never silently coerced to 0. */
|
|
69
|
+
export declare function jsonRowHasInvalidNumber(row: JsonKeyValueRow): boolean;
|
|
70
|
+
/** Trimmed keys repeated across rows — Object.fromEntries silently keeps only the last one (see
|
|
71
|
+
* jsonRowsToObject). Flagged inline in the editor so that trade-off is never invisible to whoever is
|
|
72
|
+
* editing; not a save-blocking condition. */
|
|
73
|
+
export declare function jsonDuplicateKeys(rows: JsonKeyValueRow[]): Set<string>;
|
|
74
|
+
export declare function jsonRowsToObject(rows: JsonKeyValueRow[]): Record<string, unknown> | null;
|
|
75
|
+
export declare function jsonRowsByteLength(rows: JsonKeyValueRow[]): number;
|
|
76
|
+
export declare function jsonRowsValid(rows: JsonKeyValueRow[]): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Key/value row editor for one of Competitor.pricing/.features — never free-form JSON text (no `eval`, no
|
|
79
|
+
* raw-text JSON box a typo could silently turn into a parse error at save time). Each row's value is typed
|
|
80
|
+
* (text / number / yes-no) so the object sent to the server is built from real strings/numbers/booleans, the
|
|
81
|
+
* only value shapes `PlanmagnetCreateCompetitorInput.pricing`/`.features` are documented to need.
|
|
82
|
+
*/
|
|
83
|
+
export declare function JsonKeyValueEditor({ label, rows, onChange, disabled, testId, }: {
|
|
84
|
+
label: string;
|
|
85
|
+
rows: JsonKeyValueRow[];
|
|
86
|
+
onChange: (next: JsonKeyValueRow[]) => void;
|
|
87
|
+
disabled?: boolean;
|
|
88
|
+
testId?: string;
|
|
89
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
19
90
|
export {};
|
|
20
91
|
//# sourceMappingURL=CompetitorControls.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompetitorControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/CompetitorControls.tsx"],"names":[],"mappings":"AAyBA,wBAAgB,uBAAuB,CAAC,EACtC,YAAY,EACZ,IAAI,GACL,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd,2CA6CA;AA6BD,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,cAAc,EAAE,CAAC;CAC/B,2CAyFA"}
|
|
1
|
+
{"version":3,"file":"CompetitorControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/CompetitorControls.tsx"],"names":[],"mappings":"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"}
|
|
@@ -8,20 +8,20 @@ import { Pencil as c, Plus as l, Trash2 as u, X as d } from "lucide-react";
|
|
|
8
8
|
import { gql as f } from "@apollo/client";
|
|
9
9
|
import { useMutation as p } from "@apollo/client/react";
|
|
10
10
|
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
11
|
-
import { Button as g } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
import { Button as g, Input as _ } from "@burdenoff/fe-libs/ui";
|
|
12
12
|
//#region src/planmagnet/controls/CompetitorControls.tsx
|
|
13
|
-
var
|
|
13
|
+
var v = [
|
|
14
14
|
"ListCompetitors",
|
|
15
15
|
"GetCompetitor",
|
|
16
16
|
"GetCompetitorForEdit"
|
|
17
|
-
],
|
|
17
|
+
], y = f`
|
|
18
18
|
mutation CompetitorControlsDelete($id: ID!) {
|
|
19
19
|
planmagnetDeleteCompetitor(id: $id)
|
|
20
20
|
}
|
|
21
21
|
`;
|
|
22
|
-
function
|
|
23
|
-
let d = e(), [f,
|
|
24
|
-
refetchQueries:
|
|
22
|
+
function b({ competitorId: o, name: l }) {
|
|
23
|
+
let d = e(), [f, _] = p(y, {
|
|
24
|
+
refetchQueries: v.filter((e) => e !== "GetCompetitor"),
|
|
25
25
|
context: t
|
|
26
26
|
}), [b, x] = s(null), S = async () => {
|
|
27
27
|
x(null);
|
|
@@ -49,13 +49,13 @@ function y({ competitorId: o, name: l }) {
|
|
|
49
49
|
description: "The competitor is deleted and unlinked from every feature request that referenced it. This cannot be undone here.",
|
|
50
50
|
confirmLabel: "Delete competitor",
|
|
51
51
|
destructive: !0,
|
|
52
|
-
disabled:
|
|
52
|
+
disabled: _.loading,
|
|
53
53
|
onConfirm: () => void S()
|
|
54
54
|
})]
|
|
55
55
|
}), b && /* @__PURE__ */ m(i, { message: b })]
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
-
var
|
|
58
|
+
var x = f`
|
|
59
59
|
mutation CompetitorLinkCardLink($input: PlanmagnetLinkCompetitorInput!) {
|
|
60
60
|
planmagnetLinkCompetitorToFeature(input: $input) {
|
|
61
61
|
id
|
|
@@ -67,7 +67,7 @@ var b = f`
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
`,
|
|
70
|
+
`, S = f`
|
|
71
71
|
mutation CompetitorLinkCardUnlink($input: PlanmagnetLinkCompetitorInput!) {
|
|
72
72
|
planmagnetUnlinkCompetitorFromFeature(input: $input) {
|
|
73
73
|
id
|
|
@@ -80,8 +80,8 @@ var b = f`
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
`;
|
|
83
|
-
function
|
|
84
|
-
let c = e(), [u, f] = s(""), [_, v] = p(
|
|
83
|
+
function C({ featureRequestId: t, competitors: r }) {
|
|
84
|
+
let c = e(), [u, f] = s(""), [_, v] = p(x), [y, b] = p(S), [C, w] = s(null), T = v.loading || b.loading, E = async () => {
|
|
85
85
|
if (u) {
|
|
86
86
|
w(null);
|
|
87
87
|
try {
|
|
@@ -166,7 +166,341 @@ function S({ featureRequestId: t, competitors: r }) {
|
|
|
166
166
|
]
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
|
+
var w = 2e4;
|
|
170
|
+
function T(e) {
|
|
171
|
+
return new TextEncoder().encode(e).length;
|
|
172
|
+
}
|
|
173
|
+
function E(e) {
|
|
174
|
+
return e.length <= 20 && e.every((e) => e.length <= 200);
|
|
175
|
+
}
|
|
176
|
+
function D(e) {
|
|
177
|
+
return e.map((e) => e.trim()).filter(Boolean);
|
|
178
|
+
}
|
|
179
|
+
function O({ label: e, values: t, onChange: n, disabled: r = !1, placeholder: i, testId: a }) {
|
|
180
|
+
let o = t.length >= 20;
|
|
181
|
+
return /* @__PURE__ */ h("div", {
|
|
182
|
+
className: "space-y-2",
|
|
183
|
+
"data-testid": a,
|
|
184
|
+
children: [
|
|
185
|
+
/* @__PURE__ */ h("div", {
|
|
186
|
+
className: "flex items-center justify-between",
|
|
187
|
+
children: [/* @__PURE__ */ m("label", {
|
|
188
|
+
className: "text-sm font-medium",
|
|
189
|
+
children: e
|
|
190
|
+
}), /* @__PURE__ */ h("span", {
|
|
191
|
+
className: "text-xs text-muted-foreground",
|
|
192
|
+
children: [
|
|
193
|
+
t.length,
|
|
194
|
+
"/",
|
|
195
|
+
20
|
|
196
|
+
]
|
|
197
|
+
})]
|
|
198
|
+
}),
|
|
199
|
+
t.length === 0 && /* @__PURE__ */ m("p", {
|
|
200
|
+
className: "text-xs text-muted-foreground",
|
|
201
|
+
children: "None added yet."
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ m("ul", {
|
|
204
|
+
className: "space-y-1.5",
|
|
205
|
+
children: t.map((o, s) => {
|
|
206
|
+
let c = o.length > 200;
|
|
207
|
+
return /* @__PURE__ */ h("li", {
|
|
208
|
+
className: "flex items-start gap-2",
|
|
209
|
+
children: [/* @__PURE__ */ h("div", {
|
|
210
|
+
className: "flex-1",
|
|
211
|
+
children: [/* @__PURE__ */ m(_, {
|
|
212
|
+
value: o,
|
|
213
|
+
placeholder: i,
|
|
214
|
+
disabled: r,
|
|
215
|
+
"aria-invalid": c,
|
|
216
|
+
"data-testid": a ? `${a}-input-${s}` : void 0,
|
|
217
|
+
onChange: (e) => {
|
|
218
|
+
let r = [...t];
|
|
219
|
+
r[s] = e.target.value, n(r);
|
|
220
|
+
}
|
|
221
|
+
}), c && /* @__PURE__ */ h("p", {
|
|
222
|
+
className: "mt-0.5 text-xs text-destructive",
|
|
223
|
+
children: [
|
|
224
|
+
o.length,
|
|
225
|
+
" / ",
|
|
226
|
+
200,
|
|
227
|
+
" characters — too long."
|
|
228
|
+
]
|
|
229
|
+
})]
|
|
230
|
+
}), /* @__PURE__ */ m(g, {
|
|
231
|
+
size: "sm",
|
|
232
|
+
variant: "ghost",
|
|
233
|
+
"aria-label": `Remove ${e} row ${s + 1}`,
|
|
234
|
+
disabled: r,
|
|
235
|
+
onClick: () => n(t.filter((e, t) => t !== s)),
|
|
236
|
+
children: /* @__PURE__ */ m(d, { className: "h-4 w-4" })
|
|
237
|
+
})]
|
|
238
|
+
}, s);
|
|
239
|
+
})
|
|
240
|
+
}),
|
|
241
|
+
/* @__PURE__ */ h(g, {
|
|
242
|
+
size: "sm",
|
|
243
|
+
variant: "outline",
|
|
244
|
+
disabled: r || o,
|
|
245
|
+
"data-testid": a ? `${a}-add` : void 0,
|
|
246
|
+
onClick: () => n([...t, ""]),
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ m(l, { className: "mr-1 h-4 w-4" }),
|
|
249
|
+
"Add ",
|
|
250
|
+
e.toLowerCase()
|
|
251
|
+
]
|
|
252
|
+
}),
|
|
253
|
+
o && /* @__PURE__ */ h("p", {
|
|
254
|
+
className: "text-xs text-muted-foreground",
|
|
255
|
+
children: [
|
|
256
|
+
"Up to ",
|
|
257
|
+
20,
|
|
258
|
+
" entries."
|
|
259
|
+
]
|
|
260
|
+
})
|
|
261
|
+
]
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function k(e) {
|
|
265
|
+
return !e || typeof e != "object" || Array.isArray(e) ? [] : Object.entries(e).map(([e, t]) => typeof t == "number" ? {
|
|
266
|
+
key: e,
|
|
267
|
+
type: "number",
|
|
268
|
+
value: String(t)
|
|
269
|
+
} : typeof t == "boolean" ? {
|
|
270
|
+
key: e,
|
|
271
|
+
type: "boolean",
|
|
272
|
+
value: String(t)
|
|
273
|
+
} : typeof t == "string" ? {
|
|
274
|
+
key: e,
|
|
275
|
+
type: "string",
|
|
276
|
+
value: t
|
|
277
|
+
} : {
|
|
278
|
+
key: e,
|
|
279
|
+
type: "json",
|
|
280
|
+
value: JSON.stringify(t, null, 2),
|
|
281
|
+
raw: t
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function A(e) {
|
|
285
|
+
if (e.type !== "number" || !e.key.trim()) return !1;
|
|
286
|
+
let t = e.value.trim();
|
|
287
|
+
return t === "" || Number.isNaN(Number(t));
|
|
288
|
+
}
|
|
289
|
+
function j(e) {
|
|
290
|
+
let t = /* @__PURE__ */ new Map();
|
|
291
|
+
for (let n of e) {
|
|
292
|
+
let e = n.key.trim();
|
|
293
|
+
e && t.set(e, (t.get(e) ?? 0) + 1);
|
|
294
|
+
}
|
|
295
|
+
let n = /* @__PURE__ */ new Set();
|
|
296
|
+
for (let [e, r] of t) r > 1 && n.add(e);
|
|
297
|
+
return n;
|
|
298
|
+
}
|
|
299
|
+
function M(e) {
|
|
300
|
+
let t = [];
|
|
301
|
+
for (let n of e) {
|
|
302
|
+
let e = n.key.trim();
|
|
303
|
+
if (e) if (n.type === "json") t.push([e, n.raw]);
|
|
304
|
+
else if (n.type === "number") {
|
|
305
|
+
if (A(n)) continue;
|
|
306
|
+
t.push([e, Number(n.value.trim())]);
|
|
307
|
+
} else n.type === "boolean" ? t.push([e, n.value === "true"]) : t.push([e, n.value]);
|
|
308
|
+
}
|
|
309
|
+
return t.length === 0 ? null : Object.fromEntries(t);
|
|
310
|
+
}
|
|
311
|
+
function N(e) {
|
|
312
|
+
let t = M(e);
|
|
313
|
+
return t ? T(JSON.stringify(t)) : 0;
|
|
314
|
+
}
|
|
315
|
+
function P(e) {
|
|
316
|
+
return N(e) <= 2e4 && !e.some(A);
|
|
317
|
+
}
|
|
318
|
+
function F({ label: e, rows: t, onChange: n, disabled: r = !1, testId: a }) {
|
|
319
|
+
let o = N(t), s = o > w, c = w / 1e3, u = j(t);
|
|
320
|
+
return /* @__PURE__ */ h("div", {
|
|
321
|
+
className: "space-y-2",
|
|
322
|
+
"data-testid": a,
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ h("div", {
|
|
325
|
+
className: "flex items-center justify-between",
|
|
326
|
+
children: [/* @__PURE__ */ m("label", {
|
|
327
|
+
className: "text-sm font-medium",
|
|
328
|
+
children: e
|
|
329
|
+
}), /* @__PURE__ */ h("span", {
|
|
330
|
+
className: `text-xs ${s ? "text-destructive" : "text-muted-foreground"}`,
|
|
331
|
+
children: [
|
|
332
|
+
(o / 1e3).toFixed(1),
|
|
333
|
+
" / ",
|
|
334
|
+
c,
|
|
335
|
+
" KB"
|
|
336
|
+
]
|
|
337
|
+
})]
|
|
338
|
+
}),
|
|
339
|
+
t.length === 0 && /* @__PURE__ */ m("p", {
|
|
340
|
+
className: "text-xs text-muted-foreground",
|
|
341
|
+
children: "No rows added yet."
|
|
342
|
+
}),
|
|
343
|
+
/* @__PURE__ */ m("ul", {
|
|
344
|
+
className: "space-y-1.5",
|
|
345
|
+
children: t.map((i, a) => {
|
|
346
|
+
let o = i.key.trim(), s = o !== "" && u.has(o);
|
|
347
|
+
if (i.type === "json") return /* @__PURE__ */ h("li", {
|
|
348
|
+
className: "flex items-start gap-2",
|
|
349
|
+
children: [
|
|
350
|
+
/* @__PURE__ */ m(_, {
|
|
351
|
+
className: "flex-1",
|
|
352
|
+
value: i.key,
|
|
353
|
+
readOnly: !0,
|
|
354
|
+
disabled: !0,
|
|
355
|
+
"aria-label": `${e} key ${a + 1} (nested value, view only)`
|
|
356
|
+
}),
|
|
357
|
+
/* @__PURE__ */ h("div", {
|
|
358
|
+
className: "flex-[2] space-y-1",
|
|
359
|
+
children: [/* @__PURE__ */ m("p", {
|
|
360
|
+
className: "text-xs text-muted-foreground",
|
|
361
|
+
children: "Nested value — view only, not editable here."
|
|
362
|
+
}), /* @__PURE__ */ m("textarea", {
|
|
363
|
+
className: "w-full rounded-md border border-input bg-muted px-2 py-1.5 font-mono text-xs",
|
|
364
|
+
value: i.value,
|
|
365
|
+
readOnly: !0,
|
|
366
|
+
rows: Math.min(8, i.value.split("\n").length),
|
|
367
|
+
"aria-label": `${e} value ${a + 1} (nested, view only)`
|
|
368
|
+
})]
|
|
369
|
+
}),
|
|
370
|
+
/* @__PURE__ */ m(g, {
|
|
371
|
+
size: "sm",
|
|
372
|
+
variant: "ghost",
|
|
373
|
+
"aria-label": `Remove ${e} row ${a + 1}`,
|
|
374
|
+
disabled: r,
|
|
375
|
+
onClick: () => n(t.filter((e, t) => t !== a)),
|
|
376
|
+
children: /* @__PURE__ */ m(d, { className: "h-4 w-4" })
|
|
377
|
+
})
|
|
378
|
+
]
|
|
379
|
+
}, a);
|
|
380
|
+
let c = A(i);
|
|
381
|
+
return /* @__PURE__ */ h("li", {
|
|
382
|
+
className: "flex items-start gap-2",
|
|
383
|
+
children: [
|
|
384
|
+
/* @__PURE__ */ h("div", {
|
|
385
|
+
className: "flex-1",
|
|
386
|
+
children: [/* @__PURE__ */ m(_, {
|
|
387
|
+
placeholder: "Key",
|
|
388
|
+
"aria-label": `${e} key ${a + 1}`,
|
|
389
|
+
"aria-invalid": s,
|
|
390
|
+
value: i.key,
|
|
391
|
+
disabled: r,
|
|
392
|
+
onChange: (e) => {
|
|
393
|
+
let r = [...t];
|
|
394
|
+
r[a] = {
|
|
395
|
+
...i,
|
|
396
|
+
key: e.target.value
|
|
397
|
+
}, n(r);
|
|
398
|
+
}
|
|
399
|
+
}), s && /* @__PURE__ */ h("p", {
|
|
400
|
+
className: "mt-0.5 text-xs text-status-warning-text",
|
|
401
|
+
children: [
|
|
402
|
+
"Repeated key — only the last “",
|
|
403
|
+
o,
|
|
404
|
+
"” row is saved."
|
|
405
|
+
]
|
|
406
|
+
})]
|
|
407
|
+
}),
|
|
408
|
+
/* @__PURE__ */ h("select", {
|
|
409
|
+
"aria-label": `${e} value type ${a + 1}`,
|
|
410
|
+
className: "rounded-md border border-input bg-background px-2 py-2 text-sm disabled:opacity-60",
|
|
411
|
+
value: i.type,
|
|
412
|
+
disabled: r,
|
|
413
|
+
onChange: (e) => {
|
|
414
|
+
let r = [...t];
|
|
415
|
+
r[a] = {
|
|
416
|
+
...i,
|
|
417
|
+
type: e.target.value
|
|
418
|
+
}, n(r);
|
|
419
|
+
},
|
|
420
|
+
children: [
|
|
421
|
+
/* @__PURE__ */ m("option", {
|
|
422
|
+
value: "string",
|
|
423
|
+
children: "Text"
|
|
424
|
+
}),
|
|
425
|
+
/* @__PURE__ */ m("option", {
|
|
426
|
+
value: "number",
|
|
427
|
+
children: "Number"
|
|
428
|
+
}),
|
|
429
|
+
/* @__PURE__ */ m("option", {
|
|
430
|
+
value: "boolean",
|
|
431
|
+
children: "Yes/No"
|
|
432
|
+
})
|
|
433
|
+
]
|
|
434
|
+
}),
|
|
435
|
+
/* @__PURE__ */ h("div", {
|
|
436
|
+
className: "flex-1",
|
|
437
|
+
children: [i.type === "boolean" ? /* @__PURE__ */ h("select", {
|
|
438
|
+
"aria-label": `${e} value ${a + 1}`,
|
|
439
|
+
className: "w-full rounded-md border border-input bg-background px-2 py-2 text-sm disabled:opacity-60",
|
|
440
|
+
value: i.value === "true" ? "true" : "false",
|
|
441
|
+
disabled: r,
|
|
442
|
+
onChange: (e) => {
|
|
443
|
+
let r = [...t];
|
|
444
|
+
r[a] = {
|
|
445
|
+
...i,
|
|
446
|
+
value: e.target.value
|
|
447
|
+
}, n(r);
|
|
448
|
+
},
|
|
449
|
+
children: [/* @__PURE__ */ m("option", {
|
|
450
|
+
value: "true",
|
|
451
|
+
children: "Yes"
|
|
452
|
+
}), /* @__PURE__ */ m("option", {
|
|
453
|
+
value: "false",
|
|
454
|
+
children: "No"
|
|
455
|
+
})]
|
|
456
|
+
}) : /* @__PURE__ */ m(_, {
|
|
457
|
+
type: i.type === "number" ? "number" : "text",
|
|
458
|
+
placeholder: "Value",
|
|
459
|
+
"aria-label": `${e} value ${a + 1}`,
|
|
460
|
+
"aria-invalid": c,
|
|
461
|
+
value: i.value,
|
|
462
|
+
disabled: r,
|
|
463
|
+
onChange: (e) => {
|
|
464
|
+
let r = [...t];
|
|
465
|
+
r[a] = {
|
|
466
|
+
...i,
|
|
467
|
+
value: e.target.value
|
|
468
|
+
}, n(r);
|
|
469
|
+
}
|
|
470
|
+
}), c && /* @__PURE__ */ m("p", {
|
|
471
|
+
className: "mt-0.5 text-xs text-destructive",
|
|
472
|
+
children: "Enter a number."
|
|
473
|
+
})]
|
|
474
|
+
}),
|
|
475
|
+
/* @__PURE__ */ m(g, {
|
|
476
|
+
size: "sm",
|
|
477
|
+
variant: "ghost",
|
|
478
|
+
"aria-label": `Remove ${e} row ${a + 1}`,
|
|
479
|
+
disabled: r,
|
|
480
|
+
onClick: () => n(t.filter((e, t) => t !== a)),
|
|
481
|
+
children: /* @__PURE__ */ m(d, { className: "h-4 w-4" })
|
|
482
|
+
})
|
|
483
|
+
]
|
|
484
|
+
}, a);
|
|
485
|
+
})
|
|
486
|
+
}),
|
|
487
|
+
/* @__PURE__ */ h(g, {
|
|
488
|
+
size: "sm",
|
|
489
|
+
variant: "outline",
|
|
490
|
+
disabled: r,
|
|
491
|
+
"data-testid": a ? `${a}-add` : void 0,
|
|
492
|
+
onClick: () => n([...t, {
|
|
493
|
+
key: "",
|
|
494
|
+
type: "string",
|
|
495
|
+
value: ""
|
|
496
|
+
}]),
|
|
497
|
+
children: [/* @__PURE__ */ m(l, { className: "mr-1 h-4 w-4" }), "Add row"]
|
|
498
|
+
}),
|
|
499
|
+
s && /* @__PURE__ */ m(i, { message: `${e} is larger than ${c} KB. Trim it and try again.` })
|
|
500
|
+
]
|
|
501
|
+
});
|
|
502
|
+
}
|
|
169
503
|
//#endregion
|
|
170
|
-
export {
|
|
504
|
+
export { b as CompetitorHeaderActions, C as CompetitorLinkCard, F as JsonKeyValueEditor, O as StringListEditor, D as cleanStringList, E as competitorListValid, M as jsonRowsToObject, P as jsonRowsValid, k as jsonToRows };
|
|
171
505
|
|
|
172
506
|
//# sourceMappingURL=CompetitorControls.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompetitorControls.js","names":[],"sources":["../../../src/planmagnet/controls/CompetitorControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2, X, Plus } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { CompetitorSearchSelect } from './Pickers';\n\n/**\n * Competitor controls (BOFF-8047). Competitors were `api-only` — full CRUD plus link/unlink onto a feature\n * request existed in the service, bundled with `planmagnet-feature` RBAC (CORRECTIONS.md B), but had zero\n * routes and no write UI: the feature request detail page could only ever display what was already linked.\n */\n\nconst COMPETITOR_QUERIES = ['ListCompetitors', 'GetCompetitor', 'GetCompetitorForEdit'];\n\nconst DELETE = gql`\n mutation CompetitorControlsDelete($id: ID!) {\n planmagnetDeleteCompetitor(id: $id)\n }\n`;\n\nexport function CompetitorHeaderActions({\n competitorId,\n name,\n}: {\n competitorId: string;\n name: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: COMPETITOR_QUERIES.filter((q) => q !== 'GetCompetitor'),\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: competitorId } }));\n navigate('/products/competitors-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The competitor could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"competitor-edit\"\n onClick={() => navigate(detailPath('/products/edit-competitor-page', competitorId))}\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 ${name}?`}\n description=\"The competitor is deleted and unlinked from every feature request that referenced it. This cannot be undone here.\"\n confirmLabel=\"Delete competitor\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst LINK = gql`\n mutation CompetitorLinkCardLink($input: PlanmagnetLinkCompetitorInput!) {\n planmagnetLinkCompetitorToFeature(input: $input) {\n id\n competitors {\n id\n name\n description\n website\n }\n }\n }\n`;\nconst UNLINK = gql`\n mutation CompetitorLinkCardUnlink($input: PlanmagnetLinkCompetitorInput!) {\n planmagnetUnlinkCompetitorFromFeature(input: $input) {\n id\n competitors {\n id\n name\n description\n website\n }\n }\n }\n`;\n\ninterface CompetitorNode {\n id: string;\n name: string;\n description: string | null;\n website: string | null;\n}\n\n/**\n * The feature request detail page's competitor section: list what is linked, remove any of them, and add\n * another from the workspace's competitor directory (or create a new one there).\n */\nexport function CompetitorLinkCard({\n featureRequestId,\n competitors,\n}: {\n featureRequestId: string;\n competitors: CompetitorNode[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [addCompetitorId, setAddCompetitorId] = useState('');\n const [link, linkState] = useMutation(LINK);\n const [unlink, unlinkState] = useMutation(UNLINK);\n const [error, setError] = useState<string | null>(null);\n const busy = linkState.loading || unlinkState.loading;\n\n const onAdd = async () => {\n if (!addCompetitorId) return;\n setError(null);\n try {\n await throwOnError(\n link({\n variables: { input: { featureRequestId, competitorId: addCompetitorId } },\n })\n );\n setAddCompetitorId('');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The competitor could not be linked.');\n }\n };\n const onRemove = async (competitorId: string) => {\n setError(null);\n try {\n await throwOnError(unlink({ variables: { input: { featureRequestId, competitorId } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The competitor could not be removed.');\n }\n };\n\n return (\n <div className=\"space-y-3\">\n {competitors.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No competitors linked yet.</p>\n )}\n {competitors.map((comp) => (\n <div key={comp.id} className=\"flex items-start justify-between gap-2 rounded-md border p-3\">\n <button\n type=\"button\"\n className=\"flex-1 text-left\"\n onClick={() => navigate(detailPath('/products/competitor-detail-page', comp.id))}\n >\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium hover:underline underline-offset-2\">\n {comp.name}\n </span>\n {comp.website && <span className=\"text-xs text-primary\">{comp.website}</span>}\n </div>\n {comp.description && (\n <p className=\"text-xs text-muted-foreground mt-1\">{comp.description}</p>\n )}\n </button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${comp.name} from this feature`}\n disabled={busy}\n onClick={() => void onRemove(comp.id)}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n <div className=\"flex items-start gap-2 pt-2 border-t\">\n <div className=\"flex-1\">\n <CompetitorSearchSelect\n id=\"feature-add-competitor\"\n value={addCompetitorId}\n onChange={setAddCompetitorId}\n exclude={competitors.map((c) => c.id)}\n />\n </div>\n <Button size=\"sm\" disabled={!addCompetitorId || busy} onClick={() => void onAdd()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Add\n </Button>\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => navigate('/products/create-competitor-page')}\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n New competitor\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CompetitorControls.js","names":[],"sources":["../../../src/planmagnet/controls/CompetitorControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2, X, Plus } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { CompetitorSearchSelect } from './Pickers';\n\n/**\n * Competitor controls (BOFF-8047). Competitors were `api-only` — full CRUD plus link/unlink onto a feature\n * request existed in the service, bundled with `planmagnet-feature` RBAC (CORRECTIONS.md B), but had zero\n * routes and no write UI: the feature request detail page could only ever display what was already linked.\n */\n\nconst COMPETITOR_QUERIES = ['ListCompetitors', 'GetCompetitor', 'GetCompetitorForEdit'];\n\nconst DELETE = gql`\n mutation CompetitorControlsDelete($id: ID!) {\n planmagnetDeleteCompetitor(id: $id)\n }\n`;\n\nexport function CompetitorHeaderActions({\n competitorId,\n name,\n}: {\n competitorId: string;\n name: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: COMPETITOR_QUERIES.filter((q) => q !== 'GetCompetitor'),\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: competitorId } }));\n navigate('/products/competitors-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The competitor could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"competitor-edit\"\n onClick={() => navigate(detailPath('/products/edit-competitor-page', competitorId))}\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 ${name}?`}\n description=\"The competitor is deleted and unlinked from every feature request that referenced it. This cannot be undone here.\"\n confirmLabel=\"Delete competitor\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst LINK = gql`\n mutation CompetitorLinkCardLink($input: PlanmagnetLinkCompetitorInput!) {\n planmagnetLinkCompetitorToFeature(input: $input) {\n id\n competitors {\n id\n name\n description\n website\n }\n }\n }\n`;\nconst UNLINK = gql`\n mutation CompetitorLinkCardUnlink($input: PlanmagnetLinkCompetitorInput!) {\n planmagnetUnlinkCompetitorFromFeature(input: $input) {\n id\n competitors {\n id\n name\n description\n website\n }\n }\n }\n`;\n\ninterface CompetitorNode {\n id: string;\n name: string;\n description: string | null;\n website: string | null;\n}\n\n/**\n * The feature request detail page's competitor section: list what is linked, remove any of them, and add\n * another from the workspace's competitor directory (or create a new one there).\n */\nexport function CompetitorLinkCard({\n featureRequestId,\n competitors,\n}: {\n featureRequestId: string;\n competitors: CompetitorNode[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [addCompetitorId, setAddCompetitorId] = useState('');\n const [link, linkState] = useMutation(LINK);\n const [unlink, unlinkState] = useMutation(UNLINK);\n const [error, setError] = useState<string | null>(null);\n const busy = linkState.loading || unlinkState.loading;\n\n const onAdd = async () => {\n if (!addCompetitorId) return;\n setError(null);\n try {\n await throwOnError(\n link({\n variables: { input: { featureRequestId, competitorId: addCompetitorId } },\n })\n );\n setAddCompetitorId('');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The competitor could not be linked.');\n }\n };\n const onRemove = async (competitorId: string) => {\n setError(null);\n try {\n await throwOnError(unlink({ variables: { input: { featureRequestId, competitorId } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The competitor could not be removed.');\n }\n };\n\n return (\n <div className=\"space-y-3\">\n {competitors.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No competitors linked yet.</p>\n )}\n {competitors.map((comp) => (\n <div key={comp.id} className=\"flex items-start justify-between gap-2 rounded-md border p-3\">\n <button\n type=\"button\"\n className=\"flex-1 text-left\"\n onClick={() => navigate(detailPath('/products/competitor-detail-page', comp.id))}\n >\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium hover:underline underline-offset-2\">\n {comp.name}\n </span>\n {comp.website && <span className=\"text-xs text-primary\">{comp.website}</span>}\n </div>\n {comp.description && (\n <p className=\"text-xs text-muted-foreground mt-1\">{comp.description}</p>\n )}\n </button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${comp.name} from this feature`}\n disabled={busy}\n onClick={() => void onRemove(comp.id)}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n <div className=\"flex items-start gap-2 pt-2 border-t\">\n <div className=\"flex-1\">\n <CompetitorSearchSelect\n id=\"feature-add-competitor\"\n value={addCompetitorId}\n onChange={setAddCompetitorId}\n exclude={competitors.map((c) => c.id)}\n />\n </div>\n <Button size=\"sm\" disabled={!addCompetitorId || busy} onClick={() => void onAdd()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Add\n </Button>\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => navigate('/products/create-competitor-page')}\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n New competitor\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n// ─── Strengths / weaknesses / pricing / features (PRM-31) ─────────────────────\n\n/**\n * Mirrors feature.service.ts#assertCompetitorStringList / #assertCompetitorJsonField — the service's own\n * caps for Competitor.strengths/weaknesses (20 items, 200 characters each) and .pricing/.features (20 KB of\n * UTF-8 bytes each). Enforced here too so a refusal is rare, but the service is still the real gate: whatever\n * message it sends back on a refusal is surfaced verbatim (see `mutationErrorMessage`), never replaced by a\n * generic one.\n */\nexport const COMPETITOR_LIST_MAX_ITEMS = 20;\nexport const COMPETITOR_LIST_ITEM_MAX_LENGTH = 200;\nexport const COMPETITOR_JSON_FIELD_MAX_BYTES = 20_000;\n\n/** Actual UTF-8 byte count, not `.length` (UTF-16 code units) — the same distinction\n * feature.service.ts#assertCompetitorJsonField draws (see its own \"UTF-8 byte cap\" fix). */\nexport function utf8ByteLength(value: string): number {\n return new TextEncoder().encode(value).length;\n}\n\nexport function competitorListValid(values: string[]): boolean {\n return (\n values.length <= COMPETITOR_LIST_MAX_ITEMS &&\n values.every((v) => v.length <= COMPETITOR_LIST_ITEM_MAX_LENGTH)\n );\n}\n\n/** Blank rows are an editing affordance, never a meaningful entry — dropped before the value is sent. */\nexport function cleanStringList(values: string[]): string[] {\n return values.map((v) => v.trim()).filter(Boolean);\n}\n\n/**\n * Add/remove editor for one of Competitor.strengths/weaknesses. A row over the per-entry character cap is\n * flagged inline; the count against the item cap is always visible, not only once it is hit.\n */\nexport function StringListEditor({\n label,\n values,\n onChange,\n disabled = false,\n placeholder,\n testId,\n}: {\n label: string;\n values: string[];\n onChange: (next: string[]) => void;\n disabled?: boolean;\n placeholder?: string;\n testId?: string;\n}) {\n const atMax = values.length >= COMPETITOR_LIST_MAX_ITEMS;\n return (\n <div className=\"space-y-2\" data-testid={testId}>\n <div className=\"flex items-center justify-between\">\n <label className=\"text-sm font-medium\">{label}</label>\n <span className=\"text-xs text-muted-foreground\">\n {values.length}/{COMPETITOR_LIST_MAX_ITEMS}\n </span>\n </div>\n {values.length === 0 && <p className=\"text-xs text-muted-foreground\">None added yet.</p>}\n <ul className=\"space-y-1.5\">\n {values.map((value, index) => {\n const tooLong = value.length > COMPETITOR_LIST_ITEM_MAX_LENGTH;\n return (\n <li key={index} className=\"flex items-start gap-2\">\n <div className=\"flex-1\">\n <Input\n value={value}\n placeholder={placeholder}\n disabled={disabled}\n aria-invalid={tooLong}\n data-testid={testId ? `${testId}-input-${index}` : undefined}\n onChange={(e) => {\n const next = [...values];\n next[index] = e.target.value;\n onChange(next);\n }}\n />\n {tooLong && (\n <p className=\"mt-0.5 text-xs text-destructive\">\n {value.length} / {COMPETITOR_LIST_ITEM_MAX_LENGTH} characters — too long.\n </p>\n )}\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${label} row ${index + 1}`}\n disabled={disabled}\n onClick={() => onChange(values.filter((_, i) => i !== index))}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </li>\n );\n })}\n </ul>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={disabled || atMax}\n data-testid={testId ? `${testId}-add` : undefined}\n onClick={() => onChange([...values, ''])}\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add {label.toLowerCase()}\n </Button>\n {atMax && (\n <p className=\"text-xs text-muted-foreground\">Up to {COMPETITOR_LIST_MAX_ITEMS} entries.</p>\n )}\n </div>\n );\n}\n\nexport type JsonRowType = 'string' | 'number' | 'boolean' | 'json';\n\nexport interface JsonKeyValueRow {\n key: string;\n type: JsonRowType;\n /** Raw text either way — parsed per `type` only at the point a row list is turned back into JSON.\n * Ignored for `type: 'json'` rows: they write back `raw` instead (see jsonRowsToObject), never a\n * re-parse of this displayed text. */\n value: string;\n /** Only present on a `type: 'json'` row: the original nested object/array/null value, exactly as\n * loaded. This editor can only ever WRITE flat string/number/boolean values (see JsonKeyValueEditor),\n * so a value shaped like this is carried through untouched rather than folded into a flattened string\n * the moment any OTHER row in the same editor changes. */\n raw?: unknown;\n}\n\n/** A `Competitor.pricing`/`.features` value (already-saved JSON) back into editable rows. Any array/object-\n * shaped value (not produced by this editor, since it only ever writes flat string/number/boolean values) is\n * carried through as a `type: 'json'` row — rendered read-only (see JsonKeyValueEditor) and written back\n * verbatim via `raw` (see jsonRowsToObject) — so the editor never discards data it cannot represent, and\n * never silently flattens it into a string either; it only refuses to let a NEW row be shaped that way. */\nexport function jsonToRows(json: unknown): JsonKeyValueRow[] {\n if (!json || typeof json !== 'object' || Array.isArray(json)) return [];\n return Object.entries(json as Record<string, unknown>).map(([key, value]) => {\n if (typeof value === 'number') return { key, type: 'number', value: String(value) };\n if (typeof value === 'boolean') return { key, type: 'boolean', value: String(value) };\n if (typeof value === 'string') return { key, type: 'string', value };\n // Object/array/null — not a shape this editor's own rows can produce.\n return { key, type: 'json', value: JSON.stringify(value, null, 2), raw: value };\n });\n}\n\n/** A `type: 'number'` row with a real key but no usable number — blank, or non-numeric text pasted in.\n * Used to BLOCK save (see jsonRowsValid) and to flag the row inline; never silently coerced to 0. */\nexport function jsonRowHasInvalidNumber(row: JsonKeyValueRow): boolean {\n if (row.type !== 'number' || !row.key.trim()) return false;\n const trimmed = row.value.trim();\n return trimmed === '' || Number.isNaN(Number(trimmed));\n}\n\n/** Trimmed keys repeated across rows — Object.fromEntries silently keeps only the last one (see\n * jsonRowsToObject). Flagged inline in the editor so that trade-off is never invisible to whoever is\n * editing; not a save-blocking condition. */\nexport function jsonDuplicateKeys(rows: JsonKeyValueRow[]): Set<string> {\n const counts = new Map<string, number>();\n for (const row of rows) {\n const key = row.key.trim();\n if (!key) continue;\n counts.set(key, (counts.get(key) ?? 0) + 1);\n }\n const duplicates = new Set<string>();\n for (const [key, count] of counts) if (count > 1) duplicates.add(key);\n return duplicates;\n}\n\nexport function jsonRowsToObject(rows: JsonKeyValueRow[]): Record<string, unknown> | null {\n const entries: [string, unknown][] = [];\n for (const row of rows) {\n const key = row.key.trim();\n if (!key) continue;\n if (row.type === 'json') {\n // Read-only passthrough (see jsonToRows) — never re-derived from `row.value`'s displayed JSON text,\n // so this row can't be corrupted by edits to any other row in the same editor.\n entries.push([key, row.raw]);\n } else if (row.type === 'number') {\n if (jsonRowHasInvalidNumber(row)) continue; // invalid — jsonRowsValid already blocks save on this\n entries.push([key, Number(row.value.trim())]);\n } else if (row.type === 'boolean') {\n entries.push([key, row.value === 'true']);\n } else {\n entries.push([key, row.value]);\n }\n }\n // Later rows win on a repeated key (Object.fromEntries' own semantics) — the same \"last one wins\" rule a\n // hand-written JSON object would follow. jsonDuplicateKeys() flags this inline so it's never silent.\n return entries.length === 0 ? null : Object.fromEntries(entries);\n}\n\nexport function jsonRowsByteLength(rows: JsonKeyValueRow[]): number {\n const obj = jsonRowsToObject(rows);\n return obj ? utf8ByteLength(JSON.stringify(obj)) : 0;\n}\n\nexport function jsonRowsValid(rows: JsonKeyValueRow[]): boolean {\n return (\n jsonRowsByteLength(rows) <= COMPETITOR_JSON_FIELD_MAX_BYTES &&\n !rows.some(jsonRowHasInvalidNumber)\n );\n}\n\n/**\n * Key/value row editor for one of Competitor.pricing/.features — never free-form JSON text (no `eval`, no\n * raw-text JSON box a typo could silently turn into a parse error at save time). Each row's value is typed\n * (text / number / yes-no) so the object sent to the server is built from real strings/numbers/booleans, the\n * only value shapes `PlanmagnetCreateCompetitorInput.pricing`/`.features` are documented to need.\n */\nexport function JsonKeyValueEditor({\n label,\n rows,\n onChange,\n disabled = false,\n testId,\n}: {\n label: string;\n rows: JsonKeyValueRow[];\n onChange: (next: JsonKeyValueRow[]) => void;\n disabled?: boolean;\n testId?: string;\n}) {\n const byteLength = jsonRowsByteLength(rows);\n const overCap = byteLength > COMPETITOR_JSON_FIELD_MAX_BYTES;\n const maxKb = COMPETITOR_JSON_FIELD_MAX_BYTES / 1000;\n const duplicateKeys = jsonDuplicateKeys(rows);\n return (\n <div className=\"space-y-2\" data-testid={testId}>\n <div className=\"flex items-center justify-between\">\n <label className=\"text-sm font-medium\">{label}</label>\n <span className={`text-xs ${overCap ? 'text-destructive' : 'text-muted-foreground'}`}>\n {(byteLength / 1000).toFixed(1)} / {maxKb} KB\n </span>\n </div>\n {rows.length === 0 && <p className=\"text-xs text-muted-foreground\">No rows added yet.</p>}\n <ul className=\"space-y-1.5\">\n {rows.map((row, index) => {\n const trimmedKey = row.key.trim();\n const isDuplicate = trimmedKey !== '' && duplicateKeys.has(trimmedKey);\n\n // Nested object/array value this editor cannot represent as a typed row — rendered read-only\n // (key, shape and value are all locked) so touching it can only ever remove it, never flatten\n // it into a string (see jsonToRows / jsonRowsToObject).\n if (row.type === 'json') {\n return (\n <li key={index} className=\"flex items-start gap-2\">\n <Input\n className=\"flex-1\"\n value={row.key}\n readOnly\n disabled\n aria-label={`${label} key ${index + 1} (nested value, view only)`}\n />\n <div className=\"flex-[2] space-y-1\">\n <p className=\"text-xs text-muted-foreground\">\n Nested value — view only, not editable here.\n </p>\n <textarea\n className=\"w-full rounded-md border border-input bg-muted px-2 py-1.5 font-mono text-xs\"\n value={row.value}\n readOnly\n rows={Math.min(8, row.value.split('\\n').length)}\n aria-label={`${label} value ${index + 1} (nested, view only)`}\n />\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${label} row ${index + 1}`}\n disabled={disabled}\n onClick={() => onChange(rows.filter((_, i) => i !== index))}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </li>\n );\n }\n\n const invalidNumber = jsonRowHasInvalidNumber(row);\n return (\n <li key={index} className=\"flex items-start gap-2\">\n <div className=\"flex-1\">\n <Input\n placeholder=\"Key\"\n aria-label={`${label} key ${index + 1}`}\n aria-invalid={isDuplicate}\n value={row.key}\n disabled={disabled}\n onChange={(e) => {\n const next = [...rows];\n next[index] = { ...row, key: e.target.value };\n onChange(next);\n }}\n />\n {isDuplicate && (\n <p className=\"mt-0.5 text-xs text-status-warning-text\">\n Repeated key — only the last “{trimmedKey}” row is saved.\n </p>\n )}\n </div>\n <select\n aria-label={`${label} value type ${index + 1}`}\n className=\"rounded-md border border-input bg-background px-2 py-2 text-sm disabled:opacity-60\"\n value={row.type}\n disabled={disabled}\n onChange={(e) => {\n const next = [...rows];\n next[index] = { ...row, type: e.target.value as JsonRowType };\n onChange(next);\n }}\n >\n <option value=\"string\">Text</option>\n <option value=\"number\">Number</option>\n <option value=\"boolean\">Yes/No</option>\n </select>\n <div className=\"flex-1\">\n {row.type === 'boolean' ? (\n <select\n aria-label={`${label} value ${index + 1}`}\n className=\"w-full rounded-md border border-input bg-background px-2 py-2 text-sm disabled:opacity-60\"\n value={row.value === 'true' ? 'true' : 'false'}\n disabled={disabled}\n onChange={(e) => {\n const next = [...rows];\n next[index] = { ...row, value: e.target.value };\n onChange(next);\n }}\n >\n <option value=\"true\">Yes</option>\n <option value=\"false\">No</option>\n </select>\n ) : (\n <Input\n type={row.type === 'number' ? 'number' : 'text'}\n placeholder=\"Value\"\n aria-label={`${label} value ${index + 1}`}\n aria-invalid={invalidNumber}\n value={row.value}\n disabled={disabled}\n onChange={(e) => {\n const next = [...rows];\n next[index] = { ...row, value: e.target.value };\n onChange(next);\n }}\n />\n )}\n {invalidNumber && (\n <p className=\"mt-0.5 text-xs text-destructive\">Enter a number.</p>\n )}\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove ${label} row ${index + 1}`}\n disabled={disabled}\n onClick={() => onChange(rows.filter((_, i) => i !== index))}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </li>\n );\n })}\n </ul>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={disabled}\n data-testid={testId ? `${testId}-add` : undefined}\n onClick={() => onChange([...rows, { key: '', type: 'string', value: '' }])}\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add row\n </Button>\n {overCap && (\n <ErrorLine message={`${label} is larger than ${maxKb} KB. Trim it and try again.`} />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -26,6 +26,11 @@ export interface FeatureValues {
|
|
|
26
26
|
valueScore?: number | null;
|
|
27
27
|
effortScore?: number | null;
|
|
28
28
|
valueEffortQuadrant?: string | null;
|
|
29
|
+
/** PRM-26 — optional for the same reason as the PRM-27 fields above. */
|
|
30
|
+
reach?: number | null;
|
|
31
|
+
impact?: number | null;
|
|
32
|
+
confidence?: number | null;
|
|
33
|
+
riceScore?: number | null;
|
|
29
34
|
}
|
|
30
35
|
/** Status select, Edit and Delete for the feature detail header. */
|
|
31
36
|
export declare function FeatureHeaderActions({ feature }: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"AAwCA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,wGAQnB,CAAC;AAEX,6EAA6E;AAC7E,eAAO,MAAM,eAAe,UAQ3B,CAAC;AA6CF,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,2CA6B5F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA4E3E;AA8QD,wBAAgB,yBAAyB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CAiKhF"}
|