@burdenoff/microfe-planmagnet 2026.930.23 → 2026.930.25
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/index.js +6 -5
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +154 -147
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +66 -62
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/comments/CommentThread.d.ts +8 -1
- package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
- package/dist/planmagnet/comments/CommentThread.js +188 -181
- package/dist/planmagnet/comments/CommentThread.js.map +1 -1
- package/dist/planmagnet/comments/TargetComments.d.ts +10 -2
- package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/TargetComments.js +10 -8
- package/dist/planmagnet/comments/TargetComments.js.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.d.ts +7 -1
- package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.js +5 -4
- package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts +11 -0
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.js +305 -135
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +18 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +78 -1
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.d.ts +72 -0
- package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/PrioritizationControls.js +246 -0
- package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -0
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts +8 -0
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts.map +1 -0
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js +217 -0
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js.map +1 -0
- package/dist/planmagnet/docs/DocDiffView.d.ts +26 -0
- package/dist/planmagnet/docs/DocDiffView.d.ts.map +1 -0
- package/dist/planmagnet/docs/DocDiffView.js +52 -0
- package/dist/planmagnet/docs/DocDiffView.js.map +1 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts +13 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts.map +1 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.js +308 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.js.map +1 -0
- package/dist/planmagnet/docs/DocVersionCompare.d.ts +7 -0
- package/dist/planmagnet/docs/DocVersionCompare.d.ts.map +1 -0
- package/dist/planmagnet/docs/DocVersionCompare.js +105 -0
- package/dist/planmagnet/docs/DocVersionCompare.js.map +1 -0
- package/dist/planmagnet/hooks/usePresence.d.ts +39 -0
- package/dist/planmagnet/hooks/usePresence.d.ts.map +1 -0
- package/dist/planmagnet/hooks/usePresence.js +146 -0
- package/dist/planmagnet/hooks/usePresence.js.map +1 -0
- package/dist/planmagnet/index.d.ts +1 -0
- package/dist/planmagnet/index.d.ts.map +1 -1
- package/dist/planmagnet/index.js +1 -0
- package/dist/planmagnet/links/CodeLinksPanel.d.ts +22 -0
- package/dist/planmagnet/links/CodeLinksPanel.d.ts.map +1 -0
- package/dist/planmagnet/links/CodeLinksPanel.js +94 -0
- package/dist/planmagnet/links/CodeLinksPanel.js.map +1 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +488 -217
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +131 -125
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +183 -99
- 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 +113 -97
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js +363 -0
- package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +96 -79
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +144 -128
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +205 -196
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +194 -160
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/presence/PresenceAvatars.d.ts +16 -0
- package/dist/planmagnet/presence/PresenceAvatars.d.ts.map +1 -0
- package/dist/planmagnet/presence/PresenceAvatars.js +56 -0
- package/dist/planmagnet/presence/PresenceAvatars.js.map +1 -0
- package/dist/planmagnet/product/ProductControls.d.ts +33 -0
- package/dist/planmagnet/product/ProductControls.d.ts.map +1 -1
- package/dist/planmagnet/product/ProductControls.js +593 -268
- package/dist/planmagnet/product/ProductControls.js.map +1 -1
- package/dist/planmagnet/product/routes.d.ts +10 -0
- package/dist/planmagnet/product/routes.d.ts.map +1 -0
- package/dist/planmagnet/product/routes.js +6 -0
- package/dist/planmagnet/product/routes.js.map +1 -0
- package/dist/planmagnet/roadmaps/RoadmapControls.d.ts +10 -0
- package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +235 -184
- package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
- package/dist/planmagnet/utils/graphqlErrors.d.ts +6 -0
- package/dist/planmagnet/utils/graphqlErrors.d.ts.map +1 -1
- package/dist/planmagnet/utils/graphqlErrors.js +5 -2
- package/dist/planmagnet/utils/graphqlErrors.js.map +1 -1
- package/dist/planmagnet/utils/lineDiff.d.ts +21 -0
- package/dist/planmagnet/utils/lineDiff.d.ts.map +1 -0
- package/dist/planmagnet/utils/lineDiff.js +36 -0
- package/dist/planmagnet/utils/lineDiff.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RoadmapControls.js","names":[],"sources":["../../../src/planmagnet/roadmaps/RoadmapControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { BarChart3, Pencil, Plus, Trash2 } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { endBeforeStart, parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { enumLabel } from '../controls/WorkItemControls';\n\n/**\n * Roadmap editing (BOFF-7941). Until 2026-09-27 a roadmap was a dead end: no page called\n * planmagnetCreateRoadmapItem / UpdateRoadmapItem / DeleteRoadmapItem, so a new roadmap stayed at 0 items and 0%\n * for good, and the Edit page for the roadmap itself was linked from nowhere.\n */\n\nexport const ROADMAP_ITEM_STATUSES = [\n 'PLANNED',\n 'IN_PROGRESS',\n 'AT_RISK',\n 'DELAYED',\n 'COMPLETED',\n 'CANCELLED',\n] as const;\n\n/** Now/Next/Later horizon (PRM-06). Not placed = no horizon at all (`null`), shown as its own option below. */\nexport const ROADMAP_HORIZONS = ['NOW', 'NEXT', 'LATER'] as const;\n/** Risk level recorded against a roadmap item (PRM-10). \"None\" (no risk assessed) is `null`, not a 4th value. */\nexport const ROADMAP_RISK_LEVELS = ['LOW', 'MEDIUM', 'HIGH'] as const;\n\nexport interface RoadmapItemValues {\n id: string;\n title: string;\n description: string | null;\n status: string;\n swimlane: string | null;\n startDate: string | null;\n endDate: string | null;\n /** Now/Next/Later placement (PRM-06). Null = not placed on any horizon yet. */\n horizon: string | null;\n /** Confidence this item lands as planned, 0-100 (PRM-10). Null = never recorded. */\n confidence: number | null;\n /** Risk level recorded against this item (PRM-10). Null = no risk assessed. */\n riskLevel: string | null;\n /** Free-text risk note, at most 1000 characters (PRM-10). Null = no risk assessed. */\n riskNote: string | null;\n}\n\n/** Every query that shows a roadmap's items, counts or progress — including the Now/Next/Later view's own item\n * list (`RoadmapNowNextLater.tsx`), so a placement/confidence/risk change from either view refreshes the other. */\nexport const ROADMAP_QUERIES = [\n 'GetRoadmapDetailPage',\n 'GetRoadmapDetail',\n 'ListRoadmapsPage',\n 'ListRoadmaps',\n 'GetRoadmapAnalytics',\n 'RoadmapNowNextLaterItems',\n];\n\nconst CREATE_ITEM = gql`\n mutation RoadmapControlsCreateItem($input: PlanmagnetCreateRoadmapItemInput!) {\n planmagnetCreateRoadmapItem(input: $input) {\n id\n }\n }\n`;\nconst UPDATE_ITEM = gql`\n mutation RoadmapControlsUpdateItem($id: ID!, $input: PlanmagnetUpdateRoadmapItemInput!) {\n planmagnetUpdateRoadmapItem(id: $id, input: $input) {\n id\n title\n description\n status\n swimlane\n startDate\n endDate\n horizon\n confidence\n riskLevel\n riskNote\n }\n }\n`;\nconst DELETE_ITEM = gql`\n mutation RoadmapControlsDeleteItem($id: ID!) {\n planmagnetDeleteRoadmapItem(id: $id)\n }\n`;\nconst DELETE_ROADMAP = gql`\n mutation RoadmapControlsDelete($id: ID!) {\n planmagnetDeleteRoadmap(id: $id)\n }\n`;\n\nconst day = (iso: string | null | undefined) => (iso ? iso.slice(0, 10) : '');\n\n/**\n * Add an item (no `item`) or edit one. Dates are sent only when they changed: the service checks the pair the item\n * would end up with, so re-sending untouched dates of an older item stored inverted would refuse a plain rename.\n */\nexport function RoadmapItemDialog({\n roadmapId,\n item,\n lanes,\n}: {\n roadmapId: string;\n item?: RoadmapItemValues;\n lanes: string[];\n}) {\n const initial = {\n title: item?.title ?? '',\n description: item?.description ?? '',\n status: item?.status ?? 'PLANNED',\n swimlane: item?.swimlane ?? '',\n startDate: day(item?.startDate),\n endDate: day(item?.endDate),\n horizon: item?.horizon ?? '',\n confidence: item?.confidence != null ? String(item.confidence) : '',\n riskLevel: item?.riskLevel ?? '',\n riskNote: item?.riskNote ?? '',\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const options = { refetchQueries: ROADMAP_QUERIES, awaitRefetchQueries: true };\n const [create, createState] = useMutation(CREATE_ITEM, options);\n const [update, updateState] = useMutation(UPDATE_ITEM, options);\n const saving = createState.loading || updateState.loading;\n const invalidDates = endBeforeStart(form.startDate, form.endDate);\n // Client-side bound matches the server's (0-100 whole number): a BAD_USER_INPUT round trip is avoidable, not\n // the only check — the server still enforces it (extensions.field: \"confidence\" on a refusal that slips past this).\n const confidence = parseIntField(form.confidence, { min: 0, max: 100 });\n const canSave = form.title.trim() !== '' && !invalidDates && !confidence.invalid && !saving;\n const statuses = (ROADMAP_ITEM_STATUSES as readonly string[]).includes(form.status)\n ? ROADMAP_ITEM_STATUSES\n : [form.status, ...ROADMAP_ITEM_STATUSES];\n const horizons = (ROADMAP_HORIZONS as readonly string[]).includes(form.horizon)\n ? ROADMAP_HORIZONS\n : form.horizon\n ? [form.horizon, ...ROADMAP_HORIZONS]\n : ROADMAP_HORIZONS;\n const riskLevels = (ROADMAP_RISK_LEVELS as readonly string[]).includes(form.riskLevel)\n ? ROADMAP_RISK_LEVELS\n : form.riskLevel\n ? [form.riskLevel, ...ROADMAP_RISK_LEVELS]\n : ROADMAP_RISK_LEVELS;\n const idPrefix = item ? `roadmap-item-${item.id}` : 'roadmap-item-new';\n\n const save = async () => {\n setError(null);\n const common = {\n title: form.title.trim(),\n description: form.description.trim() || null,\n status: form.status,\n swimlane: form.swimlane.trim() || null,\n };\n try {\n if (item) {\n const dates = {\n ...(form.startDate !== initial.startDate && { startDate: form.startDate || null }),\n ...(form.endDate !== initial.endDate && { endDate: form.endDate || null }),\n };\n // PRM-06/PRM-10 fields: sent only when changed, same reasoning as dates above — resending an unchanged\n // horizon/confidence/riskLevel/riskNote on a plain rename risks an unrelated refusal for no reason.\n const placement = {\n ...(form.horizon !== initial.horizon && { horizon: form.horizon || null }),\n ...(form.confidence !== initial.confidence && { confidence: confidence.value ?? null }),\n ...(form.riskLevel !== initial.riskLevel && { riskLevel: form.riskLevel || null }),\n ...(form.riskNote.trim() !== initial.riskNote.trim() && {\n riskNote: form.riskNote.trim() || null,\n }),\n };\n await throwOnError(\n update({ variables: { id: item.id, input: { ...common, ...dates, ...placement } } })\n );\n } else {\n await throwOnError(\n create({\n variables: {\n input: {\n roadmapId,\n ...common,\n startDate: form.startDate || undefined,\n endDate: form.endDate || undefined,\n horizon: form.horizon || undefined,\n confidence: confidence.value,\n riskLevel: form.riskLevel || undefined,\n riskNote: form.riskNote.trim() || undefined,\n },\n },\n })\n );\n setForm(initial);\n }\n setOpen(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The item could not be saved.'));\n }\n };\n\n const field = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) setForm(initial);\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n {item ? (\n <Button size=\"sm\" variant=\"ghost\" aria-label={`Edit ${item.title}`}>\n <Pencil className=\"h-4 w-4\" />\n </Button>\n ) : (\n <Button size=\"sm\" data-testid=\"roadmap-add-item\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add item\n </Button>\n )}\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {item ? 'Edit roadmap item' : 'Add roadmap item'}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Items in the same swimlane are grouped together on the roadmap.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor={`${idPrefix}-title`} className=\"text-sm font-medium\">\n Title\n </label>\n <Input\n id={`${idPrefix}-title`}\n className=\"mt-1\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-lane`} className=\"text-sm font-medium\">\n Swimlane\n </label>\n <Input\n id={`${idPrefix}-lane`}\n className=\"mt-1\"\n list={`${idPrefix}-lanes`}\n placeholder=\"e.g. Q4\"\n value={form.swimlane}\n onChange={(e) => setForm({ ...form, swimlane: e.target.value })}\n />\n <datalist id={`${idPrefix}-lanes`}>\n {lanes.map((lane) => (\n <option key={lane} value={lane} />\n ))}\n </datalist>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-status`} className=\"text-sm font-medium\">\n Status\n </label>\n <select\n id={`${idPrefix}-status`}\n className={field}\n value={form.status}\n onChange={(e) => setForm({ ...form, status: e.target.value })}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-start`} className=\"text-sm font-medium\">\n Start date\n </label>\n <Input\n id={`${idPrefix}-start`}\n type=\"date\"\n className=\"mt-1\"\n value={form.startDate}\n max={form.endDate || undefined}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor={`${idPrefix}-end`} className=\"text-sm font-medium\">\n End date\n </label>\n <Input\n id={`${idPrefix}-end`}\n type=\"date\"\n className=\"mt-1\"\n value={form.endDate}\n min={form.startDate || undefined}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n {invalidDates && <ErrorLine message=\"The end date is before the start date.\" />}\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-horizon`} className=\"text-sm font-medium\">\n Horizon\n </label>\n <select\n id={`${idPrefix}-horizon`}\n data-testid=\"roadmap-item-horizon\"\n className={field}\n value={form.horizon}\n onChange={(e) => setForm({ ...form, horizon: e.target.value })}\n >\n <option value=\"\">Not placed</option>\n {horizons.map((h) => (\n <option key={h} value={h}>\n {enumLabel(h)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-confidence`} className=\"text-sm font-medium\">\n Confidence (0-100)\n </label>\n <Input\n id={`${idPrefix}-confidence`}\n data-testid=\"roadmap-item-confidence\"\n type=\"number\"\n inputMode=\"numeric\"\n min={0}\n max={100}\n className=\"mt-1\"\n value={form.confidence}\n onChange={(e) => setForm({ ...form, confidence: e.target.value })}\n />\n {confidence.invalid && (\n <ErrorLine message=\"Confidence must be a whole number from 0 to 100.\" />\n )}\n </div>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-risk-level`} className=\"text-sm font-medium\">\n Risk level\n </label>\n <select\n id={`${idPrefix}-risk-level`}\n data-testid=\"roadmap-item-risk-level\"\n className={field}\n value={form.riskLevel}\n onChange={(e) => setForm({ ...form, riskLevel: e.target.value })}\n >\n <option value=\"\">None</option>\n {riskLevels.map((r) => (\n <option key={r} value={r}>\n {enumLabel(r)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-risk-note`} className=\"text-sm font-medium\">\n Risk note\n </label>\n <textarea\n id={`${idPrefix}-risk-note`}\n data-testid=\"roadmap-item-risk-note\"\n className={`${field} min-h-[56px]`}\n maxLength={1000}\n placeholder=\"Why this item is risky, and what would reduce it\"\n value={form.riskNote}\n onChange={(e) => setForm({ ...form, riskNote: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor={`${idPrefix}-description`} className=\"text-sm font-medium\">\n Description\n </label>\n <textarea\n id={`${idPrefix}-description`}\n className={`${field} min-h-[72px]`}\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void save()} disabled={!canSave}>\n {saving ? 'Saving…' : item ? 'Save item' : 'Add item'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function RemoveRoadmapItemButton({ id, title }: { id: string; title: string }) {\n const [remove, state] = useMutation(DELETE_ITEM, {\n refetchQueries: ROADMAP_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const onRemove = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The item could not be removed.');\n }\n };\n return (\n <div className=\"flex flex-col items-end\">\n <ConfirmButton\n label=\"\"\n ariaLabel={`Remove ${title}`}\n variant=\"outline\"\n icon={<Trash2 className=\"h-4 w-4\" />}\n title={`Remove \"${title}\" from the roadmap?`}\n description=\"The item is removed from this roadmap. This cannot be undone here.\"\n confirmLabel=\"Remove item\"\n destructive\n disabled={state.loading}\n onConfirm={() => void onRemove()}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/** Edit (the roadmap's own fields live on the Edit page) and Delete for the roadmap header. */\nexport function RoadmapHeaderActions({ roadmapId, name }: { roadmapId: string; name: string }) {\n const navigate = usePlanMagnetNavigate();\n const [remove, state] = useMutation(DELETE_ROADMAP, {\n refetchQueries: ['ListRoadmapsPage', 'ListRoadmaps'],\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: roadmapId } }));\n navigate('/products/roadmaps-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The roadmap could not be deleted.');\n }\n };\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"roadmap-edit\"\n // BOFF-8045: edit-product-page now edits a real Product, not a Roadmap —\n // this button owns the roadmap's own fields, so it has its own page.\n onClick={() => navigate(detailPath('/products/edit-roadmap-page', roadmapId))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n {/* BOFF-7983: the analytics page existed with no way in. */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"roadmap-analytics\"\n onClick={() => navigate(detailPath('/products/roadmap-analytics-page', roadmapId))}\n >\n <BarChart3 className=\"mr-1 h-4 w-4\" />\n Analytics\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 roadmap and its items are deleted. This cannot be undone here.\"\n confirmLabel=\"Delete roadmap\"\n destructive\n disabled={state.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RoadmapControls.js","names":[],"sources":["../../../src/planmagnet/roadmaps/RoadmapControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { BarChart3, Pencil, Plus, Trash2 } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { endBeforeStart, parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport {\n PrioritizationScoreFields,\n QuadrantBadge,\n prioritizationScoresValid,\n type PrioritizationFormValues,\n} from '../controls/PrioritizationControls';\n\n/**\n * Roadmap editing (BOFF-7941). Until 2026-09-27 a roadmap was a dead end: no page called\n * planmagnetCreateRoadmapItem / UpdateRoadmapItem / DeleteRoadmapItem, so a new roadmap stayed at 0 items and 0%\n * for good, and the Edit page for the roadmap itself was linked from nowhere.\n */\n\nexport const ROADMAP_ITEM_STATUSES = [\n 'PLANNED',\n 'IN_PROGRESS',\n 'AT_RISK',\n 'DELAYED',\n 'COMPLETED',\n 'CANCELLED',\n] as const;\n\n/** Now/Next/Later horizon (PRM-06). Not placed = no horizon at all (`null`), shown as its own option below. */\nexport const ROADMAP_HORIZONS = ['NOW', 'NEXT', 'LATER'] as const;\n/** Risk level recorded against a roadmap item (PRM-10). \"None\" (no risk assessed) is `null`, not a 4th value. */\nexport const ROADMAP_RISK_LEVELS = ['LOW', 'MEDIUM', 'HIGH'] as const;\n\nexport interface RoadmapItemValues {\n id: string;\n title: string;\n description: string | null;\n status: string;\n swimlane: string | null;\n startDate: string | null;\n endDate: string | null;\n /** Now/Next/Later placement (PRM-06). Null = not placed on any horizon yet. */\n horizon: string | null;\n /** Confidence this item lands as planned, 0-100 (PRM-10). Null = never recorded. */\n confidence: number | null;\n /** Risk level recorded against this item (PRM-10). Null = no risk assessed. */\n riskLevel: string | null;\n /** Free-text risk note, at most 1000 characters (PRM-10). Null = no risk assessed. */\n riskNote: string | null;\n /** MoSCoW classification (PRM-27). Null = never set. */\n moscow?: string | null;\n /** Kano classification (PRM-27). Null = never set. */\n kano?: string | null;\n /** Value score 1-10 (PRM-27). Null = never set. */\n valueScore?: number | null;\n /** Effort score 1-10 (PRM-27). Null = never set. */\n effortScore?: number | null;\n /** Server-computed, never sent back (PRM-27). Null unless both scores are set. */\n valueEffortQuadrant?: string | null;\n}\n\n/** Every query that shows a roadmap's items, counts or progress — including the Now/Next/Later view's own item\n * list (`RoadmapNowNextLater.tsx`), so a placement/confidence/risk change from either view refreshes the other. */\nexport const ROADMAP_QUERIES = [\n 'GetRoadmapDetailPage',\n 'GetRoadmapDetail',\n 'ListRoadmapsPage',\n 'ListRoadmaps',\n 'GetRoadmapAnalytics',\n 'RoadmapNowNextLaterItems',\n];\n\nconst CREATE_ITEM = gql`\n mutation RoadmapControlsCreateItem($input: PlanmagnetCreateRoadmapItemInput!) {\n planmagnetCreateRoadmapItem(input: $input) {\n id\n }\n }\n`;\nconst UPDATE_ITEM = gql`\n mutation RoadmapControlsUpdateItem($id: ID!, $input: PlanmagnetUpdateRoadmapItemInput!) {\n planmagnetUpdateRoadmapItem(id: $id, input: $input) {\n id\n title\n description\n status\n swimlane\n startDate\n endDate\n horizon\n confidence\n riskLevel\n riskNote\n moscow\n kano\n valueScore\n effortScore\n valueEffortQuadrant\n }\n }\n`;\nconst DELETE_ITEM = gql`\n mutation RoadmapControlsDeleteItem($id: ID!) {\n planmagnetDeleteRoadmapItem(id: $id)\n }\n`;\nconst DELETE_ROADMAP = gql`\n mutation RoadmapControlsDelete($id: ID!) {\n planmagnetDeleteRoadmap(id: $id)\n }\n`;\n\nconst day = (iso: string | null | undefined) => (iso ? iso.slice(0, 10) : '');\n\n/**\n * Add an item (no `item`) or edit one. Dates are sent only when they changed: the service checks the pair the item\n * would end up with, so re-sending untouched dates of an older item stored inverted would refuse a plain rename.\n */\nexport function RoadmapItemDialog({\n roadmapId,\n item,\n lanes,\n}: {\n roadmapId: string;\n item?: RoadmapItemValues;\n lanes: string[];\n}) {\n const initial = {\n title: item?.title ?? '',\n description: item?.description ?? '',\n status: item?.status ?? 'PLANNED',\n swimlane: item?.swimlane ?? '',\n startDate: day(item?.startDate),\n endDate: day(item?.endDate),\n horizon: item?.horizon ?? '',\n confidence: item?.confidence != null ? String(item.confidence) : '',\n riskLevel: item?.riskLevel ?? '',\n riskNote: item?.riskNote ?? '',\n };\n // PRM-27: MoSCoW/Kano/Value-Effort scoring, seeded separately from `initial` above so the diff logic below\n // reads exactly like the horizon/confidence/riskLevel/riskNote block it sits next to.\n const initialPrioritization: PrioritizationFormValues = {\n moscow: item?.moscow ?? '',\n kano: item?.kano ?? '',\n valueScore: item?.valueScore != null ? String(item.valueScore) : '',\n effortScore: item?.effortScore != null ? String(item.effortScore) : '',\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [prioritization, setPrioritization] =\n useState<PrioritizationFormValues>(initialPrioritization);\n const [error, setError] = useState<string | null>(null);\n const options = { refetchQueries: ROADMAP_QUERIES, awaitRefetchQueries: true };\n const [create, createState] = useMutation(CREATE_ITEM, options);\n const [update, updateState] = useMutation(UPDATE_ITEM, options);\n const saving = createState.loading || updateState.loading;\n const invalidDates = endBeforeStart(form.startDate, form.endDate);\n // Client-side bound matches the server's (0-100 whole number): a BAD_USER_INPUT round trip is avoidable, not\n // the only check — the server still enforces it (extensions.field: \"confidence\" on a refusal that slips past this).\n const confidence = parseIntField(form.confidence, { min: 0, max: 100 });\n const canSave =\n form.title.trim() !== '' &&\n !invalidDates &&\n !confidence.invalid &&\n prioritizationScoresValid(prioritization) &&\n !saving;\n const statuses = (ROADMAP_ITEM_STATUSES as readonly string[]).includes(form.status)\n ? ROADMAP_ITEM_STATUSES\n : [form.status, ...ROADMAP_ITEM_STATUSES];\n const horizons = (ROADMAP_HORIZONS as readonly string[]).includes(form.horizon)\n ? ROADMAP_HORIZONS\n : form.horizon\n ? [form.horizon, ...ROADMAP_HORIZONS]\n : ROADMAP_HORIZONS;\n const riskLevels = (ROADMAP_RISK_LEVELS as readonly string[]).includes(form.riskLevel)\n ? ROADMAP_RISK_LEVELS\n : form.riskLevel\n ? [form.riskLevel, ...ROADMAP_RISK_LEVELS]\n : ROADMAP_RISK_LEVELS;\n const idPrefix = item ? `roadmap-item-${item.id}` : 'roadmap-item-new';\n\n const save = async () => {\n setError(null);\n const common = {\n title: form.title.trim(),\n description: form.description.trim() || null,\n status: form.status,\n swimlane: form.swimlane.trim() || null,\n };\n const value = parseIntField(prioritization.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(prioritization.effortScore, { min: 1, max: 10 });\n try {\n if (item) {\n const dates = {\n ...(form.startDate !== initial.startDate && { startDate: form.startDate || null }),\n ...(form.endDate !== initial.endDate && { endDate: form.endDate || null }),\n };\n // PRM-06/PRM-10 fields: sent only when changed, same reasoning as dates above — resending an unchanged\n // horizon/confidence/riskLevel/riskNote on a plain rename risks an unrelated refusal for no reason.\n const placement = {\n ...(form.horizon !== initial.horizon && { horizon: form.horizon || null }),\n ...(form.confidence !== initial.confidence && { confidence: confidence.value ?? null }),\n ...(form.riskLevel !== initial.riskLevel && { riskLevel: form.riskLevel || null }),\n ...(form.riskNote.trim() !== initial.riskNote.trim() && {\n riskNote: form.riskNote.trim() || null,\n }),\n };\n // PRM-27: same \"only when changed\" rule as placement above.\n const scoring = {\n ...(prioritization.moscow !== initialPrioritization.moscow && {\n moscow: prioritization.moscow || null,\n }),\n ...(prioritization.kano !== initialPrioritization.kano && {\n kano: prioritization.kano || null,\n }),\n ...(prioritization.valueScore !== initialPrioritization.valueScore && {\n valueScore: value.value ?? null,\n }),\n ...(prioritization.effortScore !== initialPrioritization.effortScore && {\n effortScore: effort.value ?? null,\n }),\n };\n await throwOnError(\n update({\n variables: { id: item.id, input: { ...common, ...dates, ...placement, ...scoring } },\n })\n );\n } else {\n await throwOnError(\n create({\n variables: {\n input: {\n roadmapId,\n ...common,\n startDate: form.startDate || undefined,\n endDate: form.endDate || undefined,\n horizon: form.horizon || undefined,\n confidence: confidence.value,\n riskLevel: form.riskLevel || undefined,\n riskNote: form.riskNote.trim() || undefined,\n moscow: prioritization.moscow || undefined,\n kano: prioritization.kano || undefined,\n valueScore: value.value,\n effortScore: effort.value,\n },\n },\n })\n );\n setForm(initial);\n setPrioritization(initialPrioritization);\n }\n setOpen(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The item could not be saved.'));\n }\n };\n\n const field = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n setForm(initial);\n setPrioritization(initialPrioritization);\n } else {\n setError(null);\n }\n }}\n >\n <ResponsiveDialogTrigger asChild>\n {item ? (\n <Button size=\"sm\" variant=\"ghost\" aria-label={`Edit ${item.title}`}>\n <Pencil className=\"h-4 w-4\" />\n </Button>\n ) : (\n <Button size=\"sm\" data-testid=\"roadmap-add-item\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add item\n </Button>\n )}\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {item ? 'Edit roadmap item' : 'Add roadmap item'}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Items in the same swimlane are grouped together on the roadmap.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor={`${idPrefix}-title`} className=\"text-sm font-medium\">\n Title\n </label>\n <Input\n id={`${idPrefix}-title`}\n className=\"mt-1\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-lane`} className=\"text-sm font-medium\">\n Swimlane\n </label>\n <Input\n id={`${idPrefix}-lane`}\n className=\"mt-1\"\n list={`${idPrefix}-lanes`}\n placeholder=\"e.g. Q4\"\n value={form.swimlane}\n onChange={(e) => setForm({ ...form, swimlane: e.target.value })}\n />\n <datalist id={`${idPrefix}-lanes`}>\n {lanes.map((lane) => (\n <option key={lane} value={lane} />\n ))}\n </datalist>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-status`} className=\"text-sm font-medium\">\n Status\n </label>\n <select\n id={`${idPrefix}-status`}\n className={field}\n value={form.status}\n onChange={(e) => setForm({ ...form, status: e.target.value })}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-start`} className=\"text-sm font-medium\">\n Start date\n </label>\n <Input\n id={`${idPrefix}-start`}\n type=\"date\"\n className=\"mt-1\"\n value={form.startDate}\n max={form.endDate || undefined}\n onChange={(e) => setForm({ ...form, startDate: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor={`${idPrefix}-end`} className=\"text-sm font-medium\">\n End date\n </label>\n <Input\n id={`${idPrefix}-end`}\n type=\"date\"\n className=\"mt-1\"\n value={form.endDate}\n min={form.startDate || undefined}\n onChange={(e) => setForm({ ...form, endDate: e.target.value })}\n />\n </div>\n </div>\n {invalidDates && <ErrorLine message=\"The end date is before the start date.\" />}\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-horizon`} className=\"text-sm font-medium\">\n Horizon\n </label>\n <select\n id={`${idPrefix}-horizon`}\n data-testid=\"roadmap-item-horizon\"\n className={field}\n value={form.horizon}\n onChange={(e) => setForm({ ...form, horizon: e.target.value })}\n >\n <option value=\"\">Not placed</option>\n {horizons.map((h) => (\n <option key={h} value={h}>\n {enumLabel(h)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-confidence`} className=\"text-sm font-medium\">\n Confidence (0-100)\n </label>\n <Input\n id={`${idPrefix}-confidence`}\n data-testid=\"roadmap-item-confidence\"\n type=\"number\"\n inputMode=\"numeric\"\n min={0}\n max={100}\n className=\"mt-1\"\n value={form.confidence}\n onChange={(e) => setForm({ ...form, confidence: e.target.value })}\n />\n {confidence.invalid && (\n <ErrorLine message=\"Confidence must be a whole number from 0 to 100.\" />\n )}\n </div>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-risk-level`} className=\"text-sm font-medium\">\n Risk level\n </label>\n <select\n id={`${idPrefix}-risk-level`}\n data-testid=\"roadmap-item-risk-level\"\n className={field}\n value={form.riskLevel}\n onChange={(e) => setForm({ ...form, riskLevel: e.target.value })}\n >\n <option value=\"\">None</option>\n {riskLevels.map((r) => (\n <option key={r} value={r}>\n {enumLabel(r)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-risk-note`} className=\"text-sm font-medium\">\n Risk note\n </label>\n <textarea\n id={`${idPrefix}-risk-note`}\n data-testid=\"roadmap-item-risk-note\"\n className={`${field} min-h-[56px]`}\n maxLength={1000}\n placeholder=\"Why this item is risky, and what would reduce it\"\n value={form.riskNote}\n onChange={(e) => setForm({ ...form, riskNote: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor={`${idPrefix}-description`} className=\"text-sm font-medium\">\n Description\n </label>\n <textarea\n id={`${idPrefix}-description`}\n className={`${field} min-h-[72px]`}\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n {/* PRM-27: MoSCoW, Kano and Value-vs-Effort scoring. The quadrant badge shown here is the value last\n read back from the server (this item's own `valueEffortQuadrant`, never computed client-side) —\n on save the roadmap detail page's item row (which stays mounted under this dialog) refreshes with\n the freshly-computed one, since GetRoadmapDetailPage is in ROADMAP_QUERIES and is awaited before\n this dialog closes. */}\n <div className=\"border-t pt-3\">\n <p className=\"text-sm font-medium\">Prioritization</p>\n {item && (\n <div className=\"mt-1 flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Current quadrant:</span>\n <QuadrantBadge quadrant={item.valueEffortQuadrant ?? null} />\n </div>\n )}\n <div className=\"mt-2\">\n <PrioritizationScoreFields\n idPrefix={idPrefix}\n values={prioritization}\n onChange={setPrioritization}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void save()} disabled={!canSave}>\n {saving ? 'Saving…' : item ? 'Save item' : 'Add item'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function RemoveRoadmapItemButton({ id, title }: { id: string; title: string }) {\n const [remove, state] = useMutation(DELETE_ITEM, {\n refetchQueries: ROADMAP_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const onRemove = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The item could not be removed.');\n }\n };\n return (\n <div className=\"flex flex-col items-end\">\n <ConfirmButton\n label=\"\"\n ariaLabel={`Remove ${title}`}\n variant=\"outline\"\n icon={<Trash2 className=\"h-4 w-4\" />}\n title={`Remove \"${title}\" from the roadmap?`}\n description=\"The item is removed from this roadmap. This cannot be undone here.\"\n confirmLabel=\"Remove item\"\n destructive\n disabled={state.loading}\n onConfirm={() => void onRemove()}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/** Edit (the roadmap's own fields live on the Edit page) and Delete for the roadmap header. */\nexport function RoadmapHeaderActions({ roadmapId, name }: { roadmapId: string; name: string }) {\n const navigate = usePlanMagnetNavigate();\n const [remove, state] = useMutation(DELETE_ROADMAP, {\n refetchQueries: ['ListRoadmapsPage', 'ListRoadmaps'],\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: roadmapId } }));\n navigate('/products/roadmaps-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The roadmap could not be deleted.');\n }\n };\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"roadmap-edit\"\n // BOFF-8045: edit-product-page now edits a real Product, not a Roadmap —\n // this button owns the roadmap's own fields, so it has its own page.\n onClick={() => navigate(detailPath('/products/edit-roadmap-page', roadmapId))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n {/* BOFF-7983: the analytics page existed with no way in. */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"roadmap-analytics\"\n onClick={() => navigate(detailPath('/products/roadmap-analytics-page', roadmapId))}\n >\n <BarChart3 className=\"mr-1 h-4 w-4\" />\n Analytics\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 roadmap and its items are deleted. This cannot be undone here.\"\n confirmLabel=\"Delete roadmap\"\n destructive\n disabled={state.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAmCA,IAAa,IAAwB;CACnC;CACA;CACA;CACA;CACA;CACA;CACD,EAGY,IAAmB;CAAC;CAAO;CAAQ;CAAQ,EAE3C,IAAsB;CAAC;CAAO;CAAU;CAAO,EAgC/C,IAAkB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,IAAc,CAAG;;;;;;GAOjB,IAAc,CAAG;;;;;;;;;;;;;;;;;;;;;GAsBjB,IAAc,CAAG;;;;GAKjB,IAAiB,CAAG;;;;GAMpB,KAAO,MAAoC,IAAM,EAAI,MAAM,GAAG,GAAG,GAAG;AAM1E,SAAgB,EAAkB,EAChC,cACA,SACA,YAKC;CACD,IAAM,IAAU;EACd,OAAO,GAAM,SAAS;EACtB,aAAa,GAAM,eAAe;EAClC,QAAQ,GAAM,UAAU;EACxB,UAAU,GAAM,YAAY;EAC5B,WAAW,EAAI,GAAM,UAAU;EAC/B,SAAS,EAAI,GAAM,QAAQ;EAC3B,SAAS,GAAM,WAAW;EAC1B,YAAY,GAAM,cAAc,OAAiC,KAA1B,OAAO,EAAK,WAAW;EAC9D,WAAW,GAAM,aAAa;EAC9B,UAAU,GAAM,YAAY;EAC7B,EAGK,IAAkD;EACtD,QAAQ,GAAM,UAAU;EACxB,MAAM,GAAM,QAAQ;EACpB,YAAY,GAAM,cAAc,OAAiC,KAA1B,OAAO,EAAK,WAAW;EAC9D,aAAa,GAAM,eAAe,OAAkC,KAA3B,OAAO,EAAK,YAAY;EAClE,EACK,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAM,KAAW,EAAS,EAAQ,EACnC,CAAC,GAAgB,KACrB,EAAmC,EAAsB,EACrD,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAU;EAAE,gBAAgB;EAAiB,qBAAqB;EAAM,EACxE,CAAC,GAAQ,KAAe,EAAY,GAAa,EAAQ,EACzD,CAAC,GAAQ,KAAe,EAAY,GAAa,EAAQ,EACzD,IAAS,EAAY,WAAW,EAAY,SAC5C,IAAe,EAAe,EAAK,WAAW,EAAK,QAAQ,EAG3D,IAAa,EAAc,EAAK,YAAY;EAAE,KAAK;EAAG,KAAK;EAAK,CAAC,EACjE,KACJ,EAAK,MAAM,MAAM,KAAK,MACtB,CAAC,KACD,CAAC,EAAW,WACZ,EAA0B,EAAe,IACzC,CAAC,GACG,KAAY,EAA4C,SAAS,EAAK,OAAO,GAC/E,IACA,CAAC,EAAK,QAAQ,GAAG,EAAsB,EACrC,KAAY,EAAuC,SAAS,EAAK,QAAQ,GAC3E,IACA,EAAK,UACH,CAAC,EAAK,SAAS,GAAG,EAAiB,GACnC,GACA,KAAc,EAA0C,SAAS,EAAK,UAAU,GAClF,IACA,EAAK,YACH,CAAC,EAAK,WAAW,GAAG,EAAoB,GACxC,GACA,IAAW,IAAO,gBAAgB,EAAK,OAAO,oBAE9C,KAAO,YAAY;AACvB,IAAS,KAAK;EACd,IAAM,IAAS;GACb,OAAO,EAAK,MAAM,MAAM;GACxB,aAAa,EAAK,YAAY,MAAM,IAAI;GACxC,QAAQ,EAAK;GACb,UAAU,EAAK,SAAS,MAAM,IAAI;GACnC,EACK,IAAQ,EAAc,EAAe,YAAY;GAAE,KAAK;GAAG,KAAK;GAAI,CAAC,EACrE,IAAS,EAAc,EAAe,aAAa;GAAE,KAAK;GAAG,KAAK;GAAI,CAAC;AAC7E,MAAI;AACF,OAAI,GAAM;IACR,IAAM,IAAQ;KACZ,GAAI,EAAK,cAAc,EAAQ,aAAa,EAAE,WAAW,EAAK,aAAa,MAAM;KACjF,GAAI,EAAK,YAAY,EAAQ,WAAW,EAAE,SAAS,EAAK,WAAW,MAAM;KAC1E,EAGK,IAAY;KAChB,GAAI,EAAK,YAAY,EAAQ,WAAW,EAAE,SAAS,EAAK,WAAW,MAAM;KACzE,GAAI,EAAK,eAAe,EAAQ,cAAc,EAAE,YAAY,EAAW,SAAS,MAAM;KACtF,GAAI,EAAK,cAAc,EAAQ,aAAa,EAAE,WAAW,EAAK,aAAa,MAAM;KACjF,GAAI,EAAK,SAAS,MAAM,KAAK,EAAQ,SAAS,MAAM,IAAI,EACtD,UAAU,EAAK,SAAS,MAAM,IAAI,MACnC;KACF,EAEK,IAAU;KACd,GAAI,EAAe,WAAW,EAAsB,UAAU,EAC5D,QAAQ,EAAe,UAAU,MAClC;KACD,GAAI,EAAe,SAAS,EAAsB,QAAQ,EACxD,MAAM,EAAe,QAAQ,MAC9B;KACD,GAAI,EAAe,eAAe,EAAsB,cAAc,EACpE,YAAY,EAAM,SAAS,MAC5B;KACD,GAAI,EAAe,gBAAgB,EAAsB,eAAe,EACtE,aAAa,EAAO,SAAS,MAC9B;KACF;AACD,UAAM,EACJ,EAAO,EACL,WAAW;KAAE,IAAI,EAAK;KAAI,OAAO;MAAE,GAAG;MAAQ,GAAG;MAAO,GAAG;MAAW,GAAG;MAAS;KAAE,EACrF,CAAC,CACH;SAuBD,CArBA,MAAM,EACJ,EAAO,EACL,WAAW,EACT,OAAO;IACL;IACA,GAAG;IACH,WAAW,EAAK,aAAa,KAAA;IAC7B,SAAS,EAAK,WAAW,KAAA;IACzB,SAAS,EAAK,WAAW,KAAA;IACzB,YAAY,EAAW;IACvB,WAAW,EAAK,aAAa,KAAA;IAC7B,UAAU,EAAK,SAAS,MAAM,IAAI,KAAA;IAClC,QAAQ,EAAe,UAAU,KAAA;IACjC,MAAM,EAAe,QAAQ,KAAA;IAC7B,YAAY,EAAM;IAClB,aAAa,EAAO;IACrB,EACF,EACF,CAAC,CACH,EACD,EAAQ,EAAQ,EAChB,EAAkB,EAAsB;AAE1C,KAAQ,GAAM;WACP,GAAG;AACV,KAAS,EAAqB,GAAG,+BAA+B,CAAC;;IAI/D,IAAQ;AACd,QACE,kBAAC,GAAD;EACQ;EACN,eAAe,MAAS;AAEtB,GADA,EAAQ,EAAK,EACT,KACF,EAAQ,EAAQ,EAChB,EAAkB,EAAsB,IAExC,EAAS,KAAK;;YARpB,CAYE,kBAAC,GAAD;GAAyB,SAAA;aACtB,IACC,kBAAC,GAAD;IAAQ,MAAK;IAAK,SAAQ;IAAQ,cAAY,QAAQ,EAAK;cACzD,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;IACvB,CAAA,GAET,kBAAC,GAAD;IAAQ,MAAK;IAAK,eAAY;cAA9B,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,WAE1B;;GAEa,CAAA,EAC1B,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACG,IAAO,sBAAsB,oBACR,CAAA,EACxB,kBAAC,GAAD,EAAA,UAA6B,mEAEC,CAAA,CACP,EAAA,CAAA;IACzB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAS,WAAU;iBAAsB;OAE7D,CAAA,EACR,kBAAC,GAAD;OACE,IAAI,GAAG,EAAS;OAChB,WAAU;OACV,OAAO,EAAK;OACZ,WAAW,MAAM,EAAQ;QAAE,GAAG;QAAM,OAAO,EAAE,OAAO;QAAO,CAAC;OAC5D,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,SAAS,GAAG,EAAS;SAAQ,WAAU;mBAAsB;SAE5D,CAAA;QACR,kBAAC,GAAD;SACE,IAAI,GAAG,EAAS;SAChB,WAAU;SACV,MAAM,GAAG,EAAS;SAClB,aAAY;SACZ,OAAO,EAAK;SACZ,WAAW,MAAM,EAAQ;UAAE,GAAG;UAAM,UAAU,EAAE,OAAO;UAAO,CAAC;SAC/D,CAAA;QACF,kBAAC,YAAD;SAAU,IAAI,GAAG,EAAS;mBACvB,EAAM,KAAK,MACV,kBAAC,UAAD,EAAmB,OAAO,GAAQ,EAArB,EAAqB,CAClC;SACO,CAAA;QACP,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAS,GAAG,EAAS;QAAU,WAAU;kBAAsB;QAE9D,CAAA,EACR,kBAAC,UAAD;QACE,IAAI,GAAG,EAAS;QAChB,WAAW;QACX,OAAO,EAAK;QACZ,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,QAAQ,EAAE,OAAO;SAAO,CAAC;kBAE5D,GAAS,KAAK,MACb,kBAAC,UAAD;SAAgB,OAAO;mBACpB,EAAU,EAAE;SACN,EAFI,EAEJ,CACT;QACK,CAAA,CACL,EAAA,CAAA,CACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAS,GAAG,EAAS;QAAS,WAAU;kBAAsB;QAE7D,CAAA,EACR,kBAAC,GAAD;QACE,IAAI,GAAG,EAAS;QAChB,MAAK;QACL,WAAU;QACV,OAAO,EAAK;QACZ,KAAK,EAAK,WAAW,KAAA;QACrB,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,WAAW,EAAE,OAAO;SAAO,CAAC;QAChE,CAAA,CACE,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAS,GAAG,EAAS;QAAO,WAAU;kBAAsB;QAE3D,CAAA,EACR,kBAAC,GAAD;QACE,IAAI,GAAG,EAAS;QAChB,MAAK;QACL,WAAU;QACV,OAAO,EAAK;QACZ,KAAK,EAAK,aAAa,KAAA;QACvB,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,SAAS,EAAE,OAAO;SAAO,CAAC;QAC9D,CAAA,CACE,EAAA,CAAA,CACF;;MACL,KAAgB,kBAAC,GAAD,EAAW,SAAQ,0CAA2C,CAAA;MAC/E,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;QAAO,SAAS,GAAG,EAAS;QAAW,WAAU;kBAAsB;QAE/D,CAAA,EACR,kBAAC,UAAD;QACE,IAAI,GAAG,EAAS;QAChB,eAAY;QACZ,WAAW;QACX,OAAO,EAAK;QACZ,WAAW,MAAM,EAAQ;SAAE,GAAG;SAAM,SAAS,EAAE,OAAO;SAAO,CAAC;kBALhE,CAOE,kBAAC,UAAD;SAAQ,OAAM;mBAAG;SAAmB,CAAA,EACnC,GAAS,KAAK,MACb,kBAAC,UAAD;SAAgB,OAAO;mBACpB,EAAU,EAAE;SACN,EAFI,EAEJ,CACT,CACK;UACL,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,SAAD;SAAO,SAAS,GAAG,EAAS;SAAc,WAAU;mBAAsB;SAElE,CAAA;QACR,kBAAC,GAAD;SACE,IAAI,GAAG,EAAS;SAChB,eAAY;SACZ,MAAK;SACL,WAAU;SACV,KAAK;SACL,KAAK;SACL,WAAU;SACV,OAAO,EAAK;SACZ,WAAW,MAAM,EAAQ;UAAE,GAAG;UAAM,YAAY,EAAE,OAAO;UAAO,CAAC;SACjE,CAAA;QACD,EAAW,WACV,kBAAC,GAAD,EAAW,SAAQ,oDAAqD,CAAA;QAEtE,EAAA,CAAA,CACF;;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAc,WAAU;iBAAsB;OAElE,CAAA,EACR,kBAAC,UAAD;OACE,IAAI,GAAG,EAAS;OAChB,eAAY;OACZ,WAAW;OACX,OAAO,EAAK;OACZ,WAAW,MAAM,EAAQ;QAAE,GAAG;QAAM,WAAW,EAAE,OAAO;QAAO,CAAC;iBALlE,CAOE,kBAAC,UAAD;QAAQ,OAAM;kBAAG;QAAa,CAAA,EAC7B,GAAW,KAAK,MACf,kBAAC,UAAD;QAAgB,OAAO;kBACpB,EAAU,EAAE;QACN,EAFI,EAEJ,CACT,CACK;SACL,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAa,WAAU;iBAAsB;OAEjE,CAAA,EACR,kBAAC,YAAD;OACE,IAAI,GAAG,EAAS;OAChB,eAAY;OACZ,WAAW,GAAG,EAAM;OACpB,WAAW;OACX,aAAY;OACZ,OAAO,EAAK;OACZ,WAAW,MAAM,EAAQ;QAAE,GAAG;QAAM,UAAU,EAAE,OAAO;QAAO,CAAC;OAC/D,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;OAAO,SAAS,GAAG,EAAS;OAAe,WAAU;iBAAsB;OAEnE,CAAA,EACR,kBAAC,YAAD;OACE,IAAI,GAAG,EAAS;OAChB,WAAW,GAAG,EAAM;OACpB,OAAO,EAAK;OACZ,WAAW,MAAM,EAAQ;QAAE,GAAG;QAAM,aAAa,EAAE,OAAO;QAAO,CAAC;OAClE,CAAA,CACE,EAAA,CAAA;MAMN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,KAAD;SAAG,WAAU;mBAAsB;SAAkB,CAAA;QACpD,KACC,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAgC;UAAwB,CAAA,EACxE,kBAAC,GAAD,EAAe,UAAU,EAAK,uBAAuB,MAAQ,CAAA,CACzD;;QAER,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,GAAD;UACY;UACV,QAAQ;UACR,UAAU;UACV,CAAA;SACE,CAAA;QACF;;MACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA;MACnC;;IACN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,eAAe,EAAQ,GAAM;eAAE;KAEhD,CAAA,EACT,kBAAC,GAAD;KAAQ,eAAe,KAAK,IAAM;KAAE,UAAU,CAAC;eAC5C,IAAS,YAAY,IAAO,cAAc;KACpC,CAAA,CACc,EAAA,CAAA;IACD;KACT;;;AAIvB,SAAgB,EAAwB,EAAE,OAAI,YAAwC;CACpF,IAAM,CAAC,GAAQ,KAAS,EAAY,GAAa;EAC/C,gBAAgB;EAChB,qBAAqB;EACtB,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AACF,SAAM,EAAa,EAAO,EAAE,WAAW,EAAE,OAAI,EAAE,CAAC,CAAC;WAC1C,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,iCAAiC;;;AAG/E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GACE,OAAM;GACN,WAAW,UAAU;GACrB,SAAQ;GACR,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;GACpC,OAAO,WAAW,EAAM;GACxB,aAAY;GACZ,cAAa;GACb,aAAA;GACA,UAAU,EAAM;GAChB,iBAAiB,KAAK,GAAU;GAChC,CAAA,EACD,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC;;;AAKV,SAAgB,EAAqB,EAAE,cAAW,WAA6C;CAC7F,IAAM,IAAW,GAAuB,EAClC,CAAC,GAAQ,KAAS,EAAY,GAAgB;EAClD,gBAAgB,CAAC,oBAAoB,eAAe;EACpD,SAAS;EACV,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AAEF,GADA,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,GAAW,EAAE,CAAC,CAAC,EAC5D,EAAS,0BAA0B;WAC5B,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,oCAAoC;;;AAGlF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAY;KAGZ,eAAe,EAAS,EAAW,+BAA+B,EAAU,CAAC;eAN/E,CAQE,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,OAE5B;;IAET,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAY;KACZ,eAAe,EAAS,EAAW,oCAAoC,EAAU,CAAC;eAJpF,CAME,kBAAC,GAAD,EAAW,WAAU,gBAAiB,CAAA,EAAA,YAE/B;;IACT,kBAAC,GAAD;KACE,OAAM;KACN,SAAQ;KACR,MAAM,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA;KACzC,OAAO,UAAU,EAAK;KACtB,aAAY;KACZ,cAAa;KACb,aAAA;KACA,UAAU,EAAM;KAChB,iBAAiB,KAAK,GAAU;KAChC,CAAA;IACE;MACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC"}
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* `src/planmagnet/releases/ReleaseReadinessCard.tsx`'s local `denied()`, generalized for reuse.
|
|
6
6
|
*/
|
|
7
7
|
export declare function isForbidden(error: unknown): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Is this a write refused because the record moved on since the caller read it — a stale `expectedVersion`, a
|
|
10
|
+
* suggestion whose `baseVersion` the doc has passed, a checkout lock already held, a review already decided?
|
|
11
|
+
* `extensions.code: CONFLICT` the same way `isForbidden` reads `FORBIDDEN`, not a message-text guess.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isConflict(error: unknown): boolean;
|
|
8
14
|
/**
|
|
9
15
|
* A mutation refusal's display message, preferring the server's own field-tagged reason — a BAD_USER_INPUT
|
|
10
16
|
* (e.g. confidence outside 0..100) carries `extensions.field` naming which input field was wrong — over a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphqlErrors.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ7E"}
|
|
1
|
+
{"version":3,"file":"graphqlErrors.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQlD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ7E"}
|
|
@@ -4,7 +4,10 @@ import { ServerError as t } from "../../node_modules/@apollo/client/errors/Serve
|
|
|
4
4
|
function n(n) {
|
|
5
5
|
return e.is(n) ? n.errors.some((e) => e.extensions?.code === "FORBIDDEN") : t.is(n) ? n.statusCode === 403 || /"code"\s*:\s*"FORBIDDEN"/.test(n.bodyText) : !1;
|
|
6
6
|
}
|
|
7
|
-
function r(
|
|
7
|
+
function r(n) {
|
|
8
|
+
return e.is(n) ? n.errors.some((e) => e.extensions?.code === "CONFLICT") : t.is(n) ? n.statusCode === 409 || /"code"\s*:\s*"CONFLICT"/.test(n.bodyText) : !1;
|
|
9
|
+
}
|
|
10
|
+
function i(t, n) {
|
|
8
11
|
if (e.is(t)) {
|
|
9
12
|
let e = t.errors[0], n = e?.extensions?.field;
|
|
10
13
|
if (e && typeof n == "string" && n) return `${n}: ${e.message}`;
|
|
@@ -13,6 +16,6 @@ function r(t, n) {
|
|
|
13
16
|
return t instanceof Error ? t.message : n;
|
|
14
17
|
}
|
|
15
18
|
//#endregion
|
|
16
|
-
export { n as isForbidden,
|
|
19
|
+
export { r as isConflict, n as isForbidden, i as mutationErrorMessage };
|
|
17
20
|
|
|
18
21
|
//# sourceMappingURL=graphqlErrors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphqlErrors.js","names":[],"sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"sourcesContent":["import { CombinedGraphQLErrors, ServerError } from '@apollo/client/errors';\n\n/**\n * Is this the gateway refusing the viewer's role? The gateway answers an RBAC denial with HTTP 403 (400 inside\n * a batch) and `extensions.code: FORBIDDEN`. Depending on the fetch path Apollo surfaces that as GraphQL errors\n * or as a ServerError carrying the body, so both are read. Same detection as\n * `src/planmagnet/releases/ReleaseReadinessCard.tsx`'s local `denied()`, generalized for reuse.\n */\nexport function isForbidden(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'FORBIDDEN');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 403 || /\"code\"\\s*:\\s*\"FORBIDDEN\"/.test(error.bodyText);\n }\n return false;\n}\n\n/**\n * A mutation refusal's display message, preferring the server's own field-tagged reason — a BAD_USER_INPUT\n * (e.g. confidence outside 0..100) carries `extensions.field` naming which input field was wrong — over a\n * generic fallback.\n */\nexport function mutationErrorMessage(error: unknown, fallback: string): string {\n if (CombinedGraphQLErrors.is(error)) {\n const first = error.errors[0];\n const field = first?.extensions?.field;\n if (first && typeof field === 'string' && field) return `${field}: ${first.message}`;\n if (first?.message) return first.message;\n }\n return error instanceof Error ? error.message : fallback;\n}\n"],"mappings":";;;AAQA,SAAgB,EAAY,GAAyB;AAOnD,QANI,EAAsB,GAAG,EAAM,GAC1B,EAAM,OAAO,MAAM,MAAM,EAAE,YAAY,SAAS,YAAY,GAEjE,EAAY,GAAG,EAAM,GAChB,EAAM,eAAe,OAAO,2BAA2B,KAAK,EAAM,SAAS,GAE7E;;AAQT,SAAgB,EAAqB,GAAgB,GAA0B;AAC7E,KAAI,EAAsB,GAAG,EAAM,EAAE;EACnC,IAAM,IAAQ,EAAM,OAAO,IACrB,IAAQ,GAAO,YAAY;AACjC,MAAI,KAAS,OAAO,KAAU,YAAY,EAAO,QAAO,GAAG,EAAM,IAAI,EAAM;AAC3E,MAAI,GAAO,QAAS,QAAO,EAAM;;AAEnC,QAAO,aAAiB,QAAQ,EAAM,UAAU"}
|
|
1
|
+
{"version":3,"file":"graphqlErrors.js","names":[],"sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"sourcesContent":["import { CombinedGraphQLErrors, ServerError } from '@apollo/client/errors';\n\n/**\n * Is this the gateway refusing the viewer's role? The gateway answers an RBAC denial with HTTP 403 (400 inside\n * a batch) and `extensions.code: FORBIDDEN`. Depending on the fetch path Apollo surfaces that as GraphQL errors\n * or as a ServerError carrying the body, so both are read. Same detection as\n * `src/planmagnet/releases/ReleaseReadinessCard.tsx`'s local `denied()`, generalized for reuse.\n */\nexport function isForbidden(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'FORBIDDEN');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 403 || /\"code\"\\s*:\\s*\"FORBIDDEN\"/.test(error.bodyText);\n }\n return false;\n}\n\n/**\n * Is this a write refused because the record moved on since the caller read it — a stale `expectedVersion`, a\n * suggestion whose `baseVersion` the doc has passed, a checkout lock already held, a review already decided?\n * `extensions.code: CONFLICT` the same way `isForbidden` reads `FORBIDDEN`, not a message-text guess.\n */\nexport function isConflict(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'CONFLICT');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 409 || /\"code\"\\s*:\\s*\"CONFLICT\"/.test(error.bodyText);\n }\n return false;\n}\n\n/**\n * A mutation refusal's display message, preferring the server's own field-tagged reason — a BAD_USER_INPUT\n * (e.g. confidence outside 0..100) carries `extensions.field` naming which input field was wrong — over a\n * generic fallback.\n */\nexport function mutationErrorMessage(error: unknown, fallback: string): string {\n if (CombinedGraphQLErrors.is(error)) {\n const first = error.errors[0];\n const field = first?.extensions?.field;\n if (first && typeof field === 'string' && field) return `${field}: ${first.message}`;\n if (first?.message) return first.message;\n }\n return error instanceof Error ? error.message : fallback;\n}\n"],"mappings":";;;AAQA,SAAgB,EAAY,GAAyB;AAOnD,QANI,EAAsB,GAAG,EAAM,GAC1B,EAAM,OAAO,MAAM,MAAM,EAAE,YAAY,SAAS,YAAY,GAEjE,EAAY,GAAG,EAAM,GAChB,EAAM,eAAe,OAAO,2BAA2B,KAAK,EAAM,SAAS,GAE7E;;AAQT,SAAgB,EAAW,GAAyB;AAOlD,QANI,EAAsB,GAAG,EAAM,GAC1B,EAAM,OAAO,MAAM,MAAM,EAAE,YAAY,SAAS,WAAW,GAEhE,EAAY,GAAG,EAAM,GAChB,EAAM,eAAe,OAAO,0BAA0B,KAAK,EAAM,SAAS,GAE5E;;AAQT,SAAgB,EAAqB,GAAgB,GAA0B;AAC7E,KAAI,EAAsB,GAAG,EAAM,EAAE;EACnC,IAAM,IAAQ,EAAM,OAAO,IACrB,IAAQ,GAAO,YAAY;AACjC,MAAI,KAAS,OAAO,KAAU,YAAY,EAAO,QAAO,GAAG,EAAM,IAAI,EAAM;AAC3E,MAAI,GAAO,QAAS,QAAO,EAAM;;AAEnC,QAAO,aAAiB,QAAQ,EAAM,UAAU"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type LineDiffKind = 'ADDED' | 'REMOVED' | 'UNCHANGED';
|
|
2
|
+
export interface LineDiffHunk {
|
|
3
|
+
kind: LineDiffKind;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LineDiffResult {
|
|
7
|
+
hunks: LineDiffHunk[];
|
|
8
|
+
/** True when either side exceeded `LINE_CAP` — the hunks above are still a valid PREFIX of the real diff, same
|
|
9
|
+
* contract as the server's `PlanmagnetDocRevisionDiff.truncated`. */
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A line-based diff between two texts (CWF-18 §2's suggestion preview), computed entirely client-side with a
|
|
14
|
+
* small, dependency-free LCS — there is no server endpoint for "diff this UNSAVED proposed text against a
|
|
15
|
+
* revision" (the real `planmagnetDocRevisionDiff` only compares two already-SAVED versions of a doc, for the
|
|
16
|
+
* history panel's version compare — see `DocVersionCompare.tsx`, which uses that instead of this). Same hunk
|
|
17
|
+
* shape as the server's `PlanmagnetDocRevisionDiff` (`kind`/`text`, plus `truncated`) so both diffs render
|
|
18
|
+
* through the one shared `DocDiffHunks` component.
|
|
19
|
+
*/
|
|
20
|
+
export declare function lineDiff(fromText: string, toText: string): LineDiffResult;
|
|
21
|
+
//# sourceMappingURL=lineDiff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineDiff.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/lineDiff.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB;yEACqE;IACrE,SAAS,EAAE,OAAO,CAAC;CACpB;AASD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CA6CzE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/planmagnet/utils/lineDiff.ts
|
|
2
|
+
var e = 800;
|
|
3
|
+
function t(t, n) {
|
|
4
|
+
let r = t.split("\n"), i = n.split("\n"), a = r.length > e || i.length > e, o = r.slice(0, e), s = i.slice(0, e), c = o.length, l = s.length, u = Array.from({ length: c + 1 }, () => new Uint32Array(l + 1));
|
|
5
|
+
for (let e = c - 1; e >= 0; e--) {
|
|
6
|
+
let t = u[e], n = u[e + 1];
|
|
7
|
+
for (let r = l - 1; r >= 0; r--) t[r] = o[e] === s[r] ? n[r + 1] + 1 : Math.max(n[r], t[r + 1]);
|
|
8
|
+
}
|
|
9
|
+
let d = [], f = 0, p = 0;
|
|
10
|
+
for (; f < c && p < l;) o[f] === s[p] ? (d.push({
|
|
11
|
+
kind: "UNCHANGED",
|
|
12
|
+
text: o[f]
|
|
13
|
+
}), f++, p++) : u[f + 1][p] >= u[f][p + 1] ? (d.push({
|
|
14
|
+
kind: "REMOVED",
|
|
15
|
+
text: o[f]
|
|
16
|
+
}), f++) : (d.push({
|
|
17
|
+
kind: "ADDED",
|
|
18
|
+
text: s[p]
|
|
19
|
+
}), p++);
|
|
20
|
+
for (; f < c;) d.push({
|
|
21
|
+
kind: "REMOVED",
|
|
22
|
+
text: o[f]
|
|
23
|
+
}), f++;
|
|
24
|
+
for (; p < l;) d.push({
|
|
25
|
+
kind: "ADDED",
|
|
26
|
+
text: s[p]
|
|
27
|
+
}), p++;
|
|
28
|
+
return {
|
|
29
|
+
hunks: d,
|
|
30
|
+
truncated: a
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { t as lineDiff };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=lineDiff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineDiff.js","names":[],"sources":["../../../src/planmagnet/utils/lineDiff.ts"],"sourcesContent":["export type LineDiffKind = 'ADDED' | 'REMOVED' | 'UNCHANGED';\n\nexport interface LineDiffHunk {\n kind: LineDiffKind;\n text: string;\n}\n\nexport interface LineDiffResult {\n hunks: LineDiffHunk[];\n /** True when either side exceeded `LINE_CAP` — the hunks above are still a valid PREFIX of the real diff, same\n * contract as the server's `PlanmagnetDocRevisionDiff.truncated`. */\n truncated: boolean;\n}\n\n/**\n * Lines beyond this many (per side) are dropped before diffing. Bounds the classic O(n·m) LCS table to at most\n * `(LINE_CAP + 1)^2` cells — comfortably interactive in a browser tab — while the diff on what is kept is still a\n * correct prefix of the real one.\n */\nconst LINE_CAP = 800;\n\n/**\n * A line-based diff between two texts (CWF-18 §2's suggestion preview), computed entirely client-side with a\n * small, dependency-free LCS — there is no server endpoint for \"diff this UNSAVED proposed text against a\n * revision\" (the real `planmagnetDocRevisionDiff` only compares two already-SAVED versions of a doc, for the\n * history panel's version compare — see `DocVersionCompare.tsx`, which uses that instead of this). Same hunk\n * shape as the server's `PlanmagnetDocRevisionDiff` (`kind`/`text`, plus `truncated`) so both diffs render\n * through the one shared `DocDiffHunks` component.\n */\nexport function lineDiff(fromText: string, toText: string): LineDiffResult {\n const fromAll = fromText.split('\\n');\n const toAll = toText.split('\\n');\n const truncated = fromAll.length > LINE_CAP || toAll.length > LINE_CAP;\n const from = fromAll.slice(0, LINE_CAP);\n const to = toAll.slice(0, LINE_CAP);\n\n const n = from.length;\n const m = to.length;\n // lcs[i][j] = length of the longest common subsequence of from[i..] and to[j..].\n const lcs: Uint32Array[] = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));\n for (let i = n - 1; i >= 0; i--) {\n const row = lcs[i];\n const nextRow = lcs[i + 1];\n for (let j = m - 1; j >= 0; j--) {\n row[j] = from[i] === to[j] ? nextRow[j + 1] + 1 : Math.max(nextRow[j], row[j + 1]);\n }\n }\n\n const hunks: LineDiffHunk[] = [];\n let i = 0;\n let j = 0;\n while (i < n && j < m) {\n if (from[i] === to[j]) {\n hunks.push({ kind: 'UNCHANGED', text: from[i] });\n i++;\n j++;\n } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {\n hunks.push({ kind: 'REMOVED', text: from[i] });\n i++;\n } else {\n hunks.push({ kind: 'ADDED', text: to[j] });\n j++;\n }\n }\n while (i < n) {\n hunks.push({ kind: 'REMOVED', text: from[i] });\n i++;\n }\n while (j < m) {\n hunks.push({ kind: 'ADDED', text: to[j] });\n j++;\n }\n\n return { hunks, truncated };\n}\n"],"mappings":";AAmBA,IAAM,IAAW;AAUjB,SAAgB,EAAS,GAAkB,GAAgC;CACzE,IAAM,IAAU,EAAS,MAAM,KAAK,EAC9B,IAAQ,EAAO,MAAM,KAAK,EAC1B,IAAY,EAAQ,SAAS,KAAY,EAAM,SAAS,GACxD,IAAO,EAAQ,MAAM,GAAG,EAAS,EACjC,IAAK,EAAM,MAAM,GAAG,EAAS,EAE7B,IAAI,EAAK,QACT,IAAI,EAAG,QAEP,IAAqB,MAAM,KAAK,EAAE,QAAQ,IAAI,GAAG,QAAQ,IAAI,YAAY,IAAI,EAAE,CAAC;AACtF,MAAK,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,KAAK;EAC/B,IAAM,IAAM,EAAI,IACV,IAAU,EAAI,IAAI;AACxB,OAAK,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,IAC1B,GAAI,KAAK,EAAK,OAAO,EAAG,KAAK,EAAQ,IAAI,KAAK,IAAI,KAAK,IAAI,EAAQ,IAAI,EAAI,IAAI,GAAG;;CAItF,IAAM,IAAwB,EAAE,EAC5B,IAAI,GACJ,IAAI;AACR,QAAO,IAAI,KAAK,IAAI,GAClB,CAAI,EAAK,OAAO,EAAG,MACjB,EAAM,KAAK;EAAE,MAAM;EAAa,MAAM,EAAK;EAAI,CAAC,EAChD,KACA,OACS,EAAI,IAAI,GAAG,MAAM,EAAI,GAAG,IAAI,MACrC,EAAM,KAAK;EAAE,MAAM;EAAW,MAAM,EAAK;EAAI,CAAC,EAC9C,QAEA,EAAM,KAAK;EAAE,MAAM;EAAS,MAAM,EAAG;EAAI,CAAC,EAC1C;AAGJ,QAAO,IAAI,GAET,CADA,EAAM,KAAK;EAAE,MAAM;EAAW,MAAM,EAAK;EAAI,CAAC,EAC9C;AAEF,QAAO,IAAI,GAET,CADA,EAAM,KAAK;EAAE,MAAM;EAAS,MAAM,EAAG;EAAI,CAAC,EAC1C;AAGF,QAAO;EAAE;EAAO;EAAW"}
|