@burdenoff/microfe-planmagnet 2026.1001.6 → 2026.1001.8

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.
Files changed (94) hide show
  1. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoutes.js +10 -6
  3. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  4. package/dist/planmagnet/automation/automationRuleTypes.d.ts +31 -19
  5. package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -1
  6. package/dist/planmagnet/automation/automationRuleTypes.js +4 -8
  7. package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -1
  8. package/dist/planmagnet/controls/FeatureControls.d.ts +18 -1
  9. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/FeatureControls.js +259 -202
  11. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  12. package/dist/planmagnet/controls/FeedbackControls.d.ts +70 -0
  13. package/dist/planmagnet/controls/FeedbackControls.d.ts.map +1 -0
  14. package/dist/planmagnet/controls/FeedbackControls.js +153 -0
  15. package/dist/planmagnet/controls/FeedbackControls.js.map +1 -0
  16. package/dist/planmagnet/controls/SyncControls.d.ts.map +1 -1
  17. package/dist/planmagnet/controls/SyncControls.js +1 -0
  18. package/dist/planmagnet/controls/SyncControls.js.map +1 -1
  19. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts +47 -0
  20. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts.map +1 -0
  21. package/dist/planmagnet/controls/WebhookSubscriptionControls.js +542 -0
  22. package/dist/planmagnet/controls/WebhookSubscriptionControls.js.map +1 -0
  23. package/dist/planmagnet/dashboards/DashboardUI.js +7 -7
  24. package/dist/planmagnet/dashboards/cards/RiskSeverityCard.js +4 -4
  25. package/dist/planmagnet/forecast/DeliveryForecastCard.js +8 -8
  26. package/dist/planmagnet/forecast/ProjectOutlookCard.js +10 -10
  27. package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +9 -9
  28. package/dist/planmagnet/forecast/ResourceForecastCard.js +6 -6
  29. package/dist/planmagnet/hooks/useLiveRefresh.d.ts +33 -2
  30. package/dist/planmagnet/hooks/useLiveRefresh.d.ts.map +1 -1
  31. package/dist/planmagnet/hooks/useLiveRefresh.js +71 -42
  32. package/dist/planmagnet/hooks/useLiveRefresh.js.map +1 -1
  33. package/dist/planmagnet/integrations/syncTypes.d.ts +2 -1
  34. package/dist/planmagnet/integrations/syncTypes.d.ts.map +1 -1
  35. package/dist/planmagnet/integrations/syncTypes.js +19 -9
  36. package/dist/planmagnet/integrations/syncTypes.js.map +1 -1
  37. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +8 -8
  38. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +23 -23
  39. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  40. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +340 -248
  41. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  42. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.d.ts.map +1 -1
  43. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js +172 -167
  44. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js.map +1 -1
  45. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.d.ts.map +1 -1
  46. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +92 -83
  47. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js.map +1 -1
  48. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts +2 -0
  49. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts.map +1 -0
  50. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js +175 -0
  51. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js.map +1 -0
  52. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  53. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +151 -126
  54. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  55. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  56. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +136 -129
  57. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  58. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  59. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +149 -119
  60. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  61. package/dist/planmagnet/pages/ProductsProductsPagePage.js +13 -13
  62. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
  63. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +153 -117
  64. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
  65. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  66. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +173 -138
  67. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  68. package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +9 -9
  69. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +8 -8
  70. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +12 -12
  71. package/dist/planmagnet/pages/ReportsReportsPagePage.js +23 -23
  72. package/dist/planmagnet/pages/TasksMyDashboardPagePage.js +18 -18
  73. package/dist/planmagnet/pages/TasksMyWorkPagePage.js +10 -10
  74. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +5 -5
  75. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -8
  76. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +6 -6
  77. package/dist/planmagnet/pages/analytics/MetricsCard.js +6 -6
  78. package/dist/planmagnet/pages/analytics/QualityMetricsCard.js +6 -6
  79. package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +8 -8
  80. package/dist/planmagnet/pages/analytics/StatusFlowCard.js +7 -7
  81. package/dist/planmagnet/pages/analytics/ThroughputCard.js +7 -7
  82. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +7 -7
  83. package/dist/planmagnet/people/People.d.ts +14 -0
  84. package/dist/planmagnet/people/People.d.ts.map +1 -1
  85. package/dist/planmagnet/people/People.js +36 -22
  86. package/dist/planmagnet/people/People.js.map +1 -1
  87. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.d.ts.map +1 -1
  88. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +171 -153
  89. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js.map +1 -1
  90. package/dist/planmagnet/webhooks/webhookTypes.d.ts +54 -0
  91. package/dist/planmagnet/webhooks/webhookTypes.d.ts.map +1 -0
  92. package/dist/planmagnet/webhooks/webhookTypes.js +17 -0
  93. package/dist/planmagnet/webhooks/webhookTypes.js.map +1 -0
  94. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"FeatureControls.js","names":[],"sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\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, ThumbsUp, 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 { parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { ProjectSelect, WorkItemSearchSelect } from './Pickers';\nimport { enumLabel } from './WorkItemControls';\nimport {\n PrioritizationScoreFields,\n QuadrantBadge,\n RiceScoreFields,\n kanoLabel,\n moscowLabel,\n prioritizationScoresValid,\n riceInputsValid,\n riceScoreMissingHint,\n type PrioritizationFormValues,\n type RiceFormValues,\n} from './PrioritizationControls';\n\n/**\n * Feature request controls (BOFF-7951/7963). Feature requests were read-only: no vote, no status change after\n * creation, no edit or delete — so every UI-created request stayed at 0 votes and its creation status, and the\n * Declined/Duplicate tabs could only ever be empty.\n */\n\nexport const FEATURE_STATUSES = [\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n] as const;\n\n/** Every query that shows a feature request, its votes or feature counts. */\nexport const FEATURE_QUERIES = [\n 'GetFeatureRequest',\n 'ListFeatureRequests',\n 'ListFeatureRequestsForProduct',\n 'ListFeatureRequestsForProducts',\n 'ListAllFeaturesAnalytics',\n 'GetFeatureRequestAnalytics',\n 'ReleaseCustomerRequests',\n];\n\nconst VOTE = gql`\n mutation FeatureControlsVote($id: ID!) {\n planmagnetVoteOnFeature(featureRequestId: $id) {\n id\n }\n }\n`;\nconst UNVOTE = gql`\n mutation FeatureControlsUnvote($id: ID!) {\n planmagnetRemoveFeatureVote(featureRequestId: $id)\n }\n`;\nconst UPDATE = gql`\n mutation FeatureControlsUpdate($id: ID!, $input: PlanmagnetUpdateFeatureRequestInput!) {\n planmagnetUpdateFeatureRequest(id: $id, input: $input) {\n id\n title\n description\n status\n businessValue\n effortEstimate\n projectId\n workItemId\n moscow\n kano\n valueScore\n effortScore\n valueEffortQuadrant\n reach\n impact\n confidence\n riceScore\n }\n }\n`;\nconst DELETE = gql`\n mutation FeatureControlsDelete($id: ID!) {\n planmagnetDeleteFeatureRequest(id: $id)\n }\n`;\n\nconst options = { refetchQueries: FEATURE_QUERIES, awaitRefetchQueries: true };\n\n/** Vote for a request, or take your vote back. */\nexport function FeatureVoteButton({ featureId, voted }: { featureId: string; voted: boolean }) {\n const [vote, voteState] = useMutation(VOTE, options);\n const [unvote, unvoteState] = useMutation(UNVOTE, options);\n const [error, setError] = useState<string | null>(null);\n const busy = voteState.loading || unvoteState.loading;\n const toggle = async () => {\n setError(null);\n try {\n await throwOnError((voted ? unvote : vote)({ variables: { id: featureId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Your vote could not be saved.');\n }\n };\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <Button\n size=\"sm\"\n variant={voted ? 'default' : 'outline'}\n data-testid=\"feature-vote\"\n aria-pressed={voted}\n disabled={busy}\n onClick={() => void toggle()}\n >\n <ThumbsUp className=\"mr-1 h-4 w-4\" />\n {voted ? 'Voted' : 'Vote'}\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nexport interface FeatureValues {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n projectId: string | null;\n workItemId: string | null;\n /** PRM-27 — optional so existing callers (detail header actions predate PRM-27) need no change. */\n moscow?: string | null;\n kano?: string | null;\n valueScore?: number | null;\n effortScore?: number | null;\n valueEffortQuadrant?: string | null;\n /** PRM-26 — optional for the same reason as the PRM-27 fields above. */\n reach?: number | null;\n impact?: number | null;\n confidence?: number | null;\n riceScore?: number | null;\n}\n\n/** Status select, Edit and Delete for the feature detail header. */\nexport function FeatureHeaderActions({ feature }: { feature: FeatureValues }) {\n const navigate = usePlanMagnetNavigate();\n const [update, updateState] = useMutation(UPDATE, options);\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: FEATURE_QUERIES.filter((q) => q !== 'GetFeatureRequest'),\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = updateState.loading || removeState.loading;\n const statuses = (FEATURE_STATUSES as readonly string[]).includes(feature.status)\n ? FEATURE_STATUSES\n : [feature.status, ...FEATURE_STATUSES];\n\n const onStatus = async (status: string) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: feature.id, input: { status } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: feature.id } }));\n navigate('/products/features-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The request could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Status\"\n data-testid=\"feature-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={feature.status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== feature.status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <FeatureEditDialog feature={feature} />\n {/* BOFF-7983: the analytics page existed with no way in. */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"feature-analytics\"\n onClick={() => navigate(detailPath('/products/feature-analytics-page', feature.id))}\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 \"${feature.title}\"?`}\n description=\"The request and its votes are deleted. This cannot be undone here.\"\n confirmLabel=\"Delete request\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Edit a request's fields, including the project and the work item it rides on (the release readiness card needs\n * that link, and no UI could set it — BOFF-7966). Only changed fields are sent; an emptied field is cleared.\n */\nfunction FeatureEditDialog({ feature }: { feature: FeatureValues }) {\n const initial = {\n title: feature.title,\n description: feature.description ?? '',\n businessValue: feature.businessValue == null ? '' : String(feature.businessValue),\n effortEstimate: feature.effortEstimate ?? '',\n projectId: feature.projectId ?? '',\n workItemId: feature.workItemId ?? '',\n };\n const initialPrioritization: PrioritizationFormValues = {\n moscow: feature.moscow ?? '',\n kano: feature.kano ?? '',\n valueScore: feature.valueScore == null ? '' : String(feature.valueScore),\n effortScore: feature.effortScore == null ? '' : String(feature.effortScore),\n };\n const initialRice: RiceFormValues = {\n reach: feature.reach == null ? '' : String(feature.reach),\n impact: feature.impact == null ? '' : String(feature.impact),\n confidence: feature.confidence == null ? '' : String(feature.confidence),\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [prioritization, setPrioritization] =\n useState<PrioritizationFormValues>(initialPrioritization);\n const [rice, setRice] = useState<RiceFormValues>(initialRice);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation(UPDATE, options);\n const businessValue = parseIntField(form.businessValue, { min: 0, max: 100 });\n const canSave =\n form.title.trim() !== '' &&\n !businessValue.invalid &&\n prioritizationScoresValid(prioritization) &&\n riceInputsValid(rice) &&\n !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\n const value = parseIntField(prioritization.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(prioritization.effortScore, { min: 1, max: 10 });\n const reach = parseIntField(rice.reach, { min: 1, max: 10 });\n const impact = parseIntField(rice.impact, { min: 1, max: 10 });\n const confidence = parseIntField(rice.confidence, { min: 1, max: 10 });\n const next = {\n title: form.title.trim(),\n description: text(form.description),\n businessValue: businessValue.value ?? null,\n effortEstimate: text(form.effortEstimate),\n projectId: form.projectId || null,\n workItemId: form.workItemId || null,\n moscow: prioritization.moscow || null,\n kano: prioritization.kano || null,\n valueScore: value.value ?? null,\n effortScore: effort.value ?? null,\n reach: reach.value ?? null,\n impact: impact.value ?? null,\n confidence: confidence.value ?? null,\n };\n const before = {\n title: initial.title,\n description: text(initial.description),\n businessValue: initial.businessValue === '' ? null : Number(initial.businessValue),\n effortEstimate: text(initial.effortEstimate),\n projectId: initial.projectId || null,\n workItemId: initial.workItemId || null,\n moscow: initialPrioritization.moscow || null,\n kano: initialPrioritization.kano || null,\n valueScore:\n initialPrioritization.valueScore === '' ? null : Number(initialPrioritization.valueScore),\n effortScore:\n initialPrioritization.effortScore === '' ? null : Number(initialPrioritization.effortScore),\n reach: initialRice.reach === '' ? null : Number(initialRice.reach),\n impact: initialRice.impact === '' ? null : Number(initialRice.impact),\n confidence: initialRice.confidence === '' ? null : Number(initialRice.confidence),\n };\n const input = Object.fromEntries(\n Object.entries(next).filter(([k, v]) => v !== before[k as keyof typeof before])\n );\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: feature.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The request could not be saved.'));\n }\n };\n\n const label = 'text-sm font-medium';\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n setForm(initial);\n setPrioritization(initialPrioritization);\n setRice(initialRice);\n } else {\n setError(null);\n }\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"feature-edit\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit feature request</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Link the request to the work item that delivers it so releases can show who is waiting\n on it.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"feature-edit-title\" className={label}>\n Title\n </label>\n <Input\n id=\"feature-edit-title\"\n className=\"mt-1\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-description\" className={label}>\n Description\n </label>\n <textarea\n id=\"feature-edit-description\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"feature-edit-value\" className={label}>\n Business value (0–100)\n </label>\n <Input\n id=\"feature-edit-value\"\n type=\"number\"\n min={0}\n max={100}\n className=\"mt-1\"\n value={form.businessValue}\n aria-invalid={businessValue.invalid}\n onChange={(e) => setForm({ ...form, businessValue: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-effort\" className={label}>\n Effort\n </label>\n <Input\n id=\"feature-edit-effort\"\n className=\"mt-1\"\n placeholder=\"e.g. M\"\n value={form.effortEstimate}\n onChange={(e) => setForm({ ...form, effortEstimate: e.target.value })}\n />\n </div>\n </div>\n {businessValue.invalid && (\n <ErrorLine message=\"Business value is a whole number from 0 to 100.\" />\n )}\n <div>\n <label htmlFor=\"feature-edit-project\" className={label}>\n Project\n </label>\n <div className=\"mt-1\">\n <ProjectSelect\n id=\"feature-edit-project\"\n value={form.projectId}\n noneLabel=\"No project\"\n onChange={(projectId) => setForm({ ...form, projectId, workItemId: '' })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"feature-edit-workitem\" className={label}>\n Delivered by work item\n </label>\n <div className=\"mt-1\">\n <WorkItemSearchSelect\n id=\"feature-edit-workitem\"\n projectId={form.projectId || undefined}\n value={form.workItemId}\n onChange={(workItemId) => setForm({ ...form, workItemId })}\n />\n </div>\n </div>\n {/* PRM-27: MoSCoW, Kano and Value-vs-Effort scoring. The quadrant itself is server-computed — this\n dialog shows the value last read back from the server, not a live client-side guess; the feature\n detail page's own Prioritization card refreshes with the saved value once this dialog closes\n (GetFeatureRequest is in FEATURE_QUERIES, awaited before the dialog closes). */}\n <div className=\"border-t pt-3\">\n <p className={label}>Prioritization</p>\n <div className=\"mt-1 flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Current quadrant:</span>\n <QuadrantBadge quadrant={feature.valueEffortQuadrant ?? null} />\n </div>\n <div className=\"mt-2\">\n <PrioritizationScoreFields\n idPrefix=\"feature-edit\"\n values={prioritization}\n onChange={setPrioritization}\n />\n </div>\n {/* PRM-26: RICE, next to the value/effort scores above. */}\n <div className=\"mt-3 border-t pt-3\">\n <p className={label}>RICE</p>\n <div className=\"mt-1 flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Current RICE score:</span>\n <span className=\"text-sm font-medium\" data-testid=\"feature-edit-rice-score\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n <div className=\"mt-2\">\n <RiceScoreFields\n idPrefix=\"feature-edit\"\n values={rice}\n onChange={setRice}\n effortScoreMissing={\n parseIntField(prioritization.effortScore, { min: 1, max: 10 }).value == null\n }\n />\n </div>\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 {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n/**\n * The feature detail page's own Prioritization card (PRM-27): shows the current MoSCoW/Kano/scores and the\n * server-computed quadrant, with an inline \"Edit\" toggle for the four inputs. Everything shown outside the\n * editing form — including the quadrant badge — is read straight from the `feature` prop rather than mirrored\n * into local state, so it stays correct whichever of this card's own save or the sibling `FeatureEditDialog`'s\n * save last refreshed `GetFeatureRequest` (both awaited their refetch before returning/closing).\n */\ninterface UpdateFeaturePrioritizationResult {\n planmagnetUpdateFeatureRequest: { valueEffortQuadrant: string | null } | null;\n}\n\nexport function FeaturePrioritizationCard({ feature }: { feature: FeatureValues }) {\n const initial: PrioritizationFormValues = {\n moscow: feature.moscow ?? '',\n kano: feature.kano ?? '',\n valueScore: feature.valueScore == null ? '' : String(feature.valueScore),\n effortScore: feature.effortScore == null ? '' : String(feature.effortScore),\n };\n const initialRice: RiceFormValues = {\n reach: feature.reach == null ? '' : String(feature.reach),\n impact: feature.impact == null ? '' : String(feature.impact),\n confidence: feature.confidence == null ? '' : String(feature.confidence),\n };\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState<PrioritizationFormValues>(initial);\n const [rice, setRice] = useState<RiceFormValues>(initialRice);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation<UpdateFeaturePrioritizationResult>(UPDATE, options);\n\n const startEdit = () => {\n setForm(initial);\n setRice(initialRice);\n setError(null);\n setEditing(true);\n };\n\n const save = async () => {\n setError(null);\n const value = parseIntField(form.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(form.effortScore, { min: 1, max: 10 });\n const reach = parseIntField(rice.reach, { min: 1, max: 10 });\n const impact = parseIntField(rice.impact, { min: 1, max: 10 });\n const confidence = parseIntField(rice.confidence, { min: 1, max: 10 });\n const input = {\n moscow: form.moscow || null,\n kano: form.kano || null,\n valueScore: value.value ?? null,\n effortScore: effort.value ?? null,\n reach: reach.value ?? null,\n impact: impact.value ?? null,\n confidence: confidence.value ?? null,\n };\n try {\n // This mutation's own `options` also carries `awaitRefetchQueries: true` over `GetFeatureRequest`, so by\n // the time this resolves the `feature` prop has already refreshed with the new quadrant — read it straight\n // from there (below) rather than mirroring the response into local state, which a sibling editor's own save\n // (FeatureEditDialog, same refetch) would then leave stale.\n await throwOnError(update({ variables: { id: feature.id, input } }));\n setEditing(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The prioritization could not be saved.'));\n }\n };\n\n return (\n <Card data-testid=\"feature-prioritization-card\">\n <CardHeader className=\"flex flex-row items-center justify-between gap-2\">\n <CardTitle>Prioritization</CardTitle>\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"feature-prioritization-edit\"\n onClick={startEdit}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {!editing ? (\n <>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Quadrant:</span>\n <QuadrantBadge quadrant={feature.valueEffortQuadrant ?? null} />\n </div>\n <div className=\"grid grid-cols-2 gap-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">MoSCoW</span>\n <span className=\"font-medium\">{moscowLabel(feature.moscow)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Kano</span>\n <span className=\"font-medium\">{kanoLabel(feature.kano)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Value score</span>\n <span className=\"font-medium\">{feature.valueScore ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Effort score</span>\n <span className=\"font-medium\">{feature.effortScore ?? 'Not set'}</span>\n </div>\n </div>\n {/* PRM-26: RICE, next to the value/effort scores above. */}\n <div className=\"border-t pt-3\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-muted-foreground\">RICE score</span>\n <span className=\"font-medium\" data-testid=\"feature-rice-score\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n {feature.riceScore == null && (\n <p\n className=\"text-xs text-muted-foreground mt-0.5\"\n data-testid=\"feature-rice-missing-hint\"\n >\n {riceScoreMissingHint(feature)}\n </p>\n )}\n <div className=\"grid grid-cols-3 gap-2 text-sm mt-2\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reach</span>\n <span className=\"font-medium\">{feature.reach ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Impact</span>\n <span className=\"font-medium\">{feature.impact ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Confidence</span>\n <span className=\"font-medium\">{feature.confidence ?? 'Not set'}</span>\n </div>\n </div>\n </div>\n </>\n ) : (\n <>\n <PrioritizationScoreFields idPrefix=\"feature-prio\" values={form} onChange={setForm} />\n <div className=\"border-t pt-3\">\n <p className=\"text-sm font-medium mb-1\">RICE</p>\n <RiceScoreFields\n idPrefix=\"feature-prio\"\n values={rice}\n onChange={setRice}\n effortScoreMissing={\n parseIntField(form.effortScore, { min: 1, max: 10 }).value == null\n }\n />\n </div>\n {error && <ErrorLine message={error} />}\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(false)}>\n Cancel\n </Button>\n <Button\n size=\"sm\"\n data-testid=\"feature-prioritization-save\"\n disabled={\n state.loading || !prioritizationScoresValid(form) || !riceInputsValid(rice)\n }\n onClick={() => void save()}\n >\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"FeatureControls.js","names":[],"sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\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, ThumbsUp, 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 { parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { ProjectSelect, WorkItemSearchSelect } from './Pickers';\nimport { enumLabel } from './WorkItemControls';\nimport {\n PrioritizationScoreFields,\n QuadrantBadge,\n RiceScoreFields,\n kanoLabel,\n moscowLabel,\n prioritizationScoresValid,\n riceInputsValid,\n riceScoreMissingHint,\n type PrioritizationFormValues,\n type RiceFormValues,\n} from './PrioritizationControls';\nimport {\n AnonymousSubmissionCheckbox,\n FeedbackFields,\n type FeedbackFormValues,\n} from './FeedbackControls';\n\n/**\n * Feature request controls (BOFF-7951/7963). Feature requests were read-only: no vote, no status change after\n * creation, no edit or delete — so every UI-created request stayed at 0 votes and its creation status, and the\n * Declined/Duplicate tabs could only ever be empty.\n */\n\nexport const FEATURE_STATUSES = [\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n] as const;\n\n/** Every query that shows a feature request, its votes or feature counts. */\nexport const FEATURE_QUERIES = [\n 'GetFeatureRequest',\n 'ListFeatureRequests',\n 'ListFeatureRequestsForProduct',\n 'ListFeatureRequestsForProducts',\n 'ListAllFeaturesAnalytics',\n 'GetFeatureRequestAnalytics',\n 'ReleaseCustomerRequests',\n];\n\nconst VOTE = gql`\n mutation FeatureControlsVote($id: ID!, $input: PlanmagnetVoteOnFeatureInput) {\n planmagnetVoteOnFeature(featureRequestId: $id, input: $input) {\n id\n isAnonymous\n }\n }\n`;\nconst UNVOTE = gql`\n mutation FeatureControlsUnvote($id: ID!) {\n planmagnetRemoveFeatureVote(featureRequestId: $id)\n }\n`;\nconst UPDATE = gql`\n mutation FeatureControlsUpdate($id: ID!, $input: PlanmagnetUpdateFeatureRequestInput!) {\n planmagnetUpdateFeatureRequest(id: $id, input: $input) {\n id\n title\n description\n status\n businessValue\n effortEstimate\n projectId\n workItemId\n moscow\n kano\n valueScore\n effortScore\n valueEffortQuadrant\n reach\n impact\n confidence\n riceScore\n rating\n category\n isAnonymous\n submittedBy\n }\n }\n`;\nconst DELETE = gql`\n mutation FeatureControlsDelete($id: ID!) {\n planmagnetDeleteFeatureRequest(id: $id)\n }\n`;\n\nconst options = { refetchQueries: FEATURE_QUERIES, awaitRefetchQueries: true };\n\n/**\n * Vote for a request, or take your vote back. CWF-15: an unvoted caller can choose \"Vote anonymously\" (its own\n * `FeatureVote.isAnonymous`, independent of the parent request's own anonymity) before casting — the checkbox\n * only makes sense while deciding how to vote, so it disappears once a vote exists (unvoting needs no choice).\n *\n * `voted` (from the parent's `votes.some(v => v.voterId === currentUser.id)`) is unreliable right after an\n * anonymous vote: the schema redacts `FeatureVote.voterId` to `\"anonymous\"` for a non-moderator caller's own\n * anonymous vote (no self carve-out), so once `FEATURE_QUERIES` refetches, that comparison flips back to\n * false even though the vote exists. Track this session's own vote/unvote outcome locally and prefer it over\n * the (possibly-redacted) server-derived value, so the button and the anonymous checkbox stay correct. The\n * caller remounts this component on feature change (`key={feature.id}`) rather than this resetting the\n * override itself in an effect, matching React's own \"adjust state when a prop changes\" guidance — no\n * cascading-render setState-in-effect, and no one-render-behind reset.\n */\nexport function FeatureVoteButton({ featureId, voted }: { featureId: string; voted: boolean }) {\n const [vote, voteState] = useMutation(VOTE, options);\n const [unvote, unvoteState] = useMutation(UNVOTE, options);\n const [error, setError] = useState<string | null>(null);\n const [anonymous, setAnonymous] = useState(false);\n const [localVoted, setLocalVoted] = useState<boolean | null>(null);\n const effectiveVoted = localVoted ?? voted;\n const busy = voteState.loading || unvoteState.loading;\n const toggle = async () => {\n setError(null);\n try {\n if (effectiveVoted) {\n await throwOnError(unvote({ variables: { id: featureId } }));\n setLocalVoted(false);\n } else {\n await throwOnError(\n vote({ variables: { id: featureId, input: { isAnonymous: anonymous } } })\n );\n setLocalVoted(true);\n setAnonymous(false);\n }\n } catch (e) {\n setError(mutationErrorMessage(e, 'Your vote could not be saved.'));\n }\n };\n return (\n <div className=\"flex flex-col items-end gap-1\">\n {!effectiveVoted && (\n <AnonymousSubmissionCheckbox\n idPrefix=\"feature-vote\"\n checked={anonymous}\n onChange={setAnonymous}\n disabled={busy}\n label=\"Vote anonymously\"\n hint={null}\n />\n )}\n <Button\n size=\"sm\"\n variant={effectiveVoted ? 'default' : 'outline'}\n data-testid=\"feature-vote\"\n aria-pressed={effectiveVoted}\n disabled={busy}\n onClick={() => void toggle()}\n >\n <ThumbsUp className=\"mr-1 h-4 w-4\" />\n {effectiveVoted ? 'Voted' : 'Vote'}\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nexport interface FeatureValues {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n projectId: string | null;\n workItemId: string | null;\n /** PRM-27 — optional so existing callers (detail header actions predate PRM-27) need no change. */\n moscow?: string | null;\n kano?: string | null;\n valueScore?: number | null;\n effortScore?: number | null;\n valueEffortQuadrant?: string | null;\n /** PRM-26 — optional for the same reason as the PRM-27 fields above. */\n reach?: number | null;\n impact?: number | null;\n confidence?: number | null;\n riceScore?: number | null;\n /** CWF-15 — optional for the same \"predates this pass\" reason as the PRM-26/27 fields above. */\n rating?: number | null;\n category?: string | null;\n isAnonymous?: boolean;\n}\n\n/** Status select, Edit and Delete for the feature detail header. */\nexport function FeatureHeaderActions({ feature }: { feature: FeatureValues }) {\n const navigate = usePlanMagnetNavigate();\n const [update, updateState] = useMutation(UPDATE, options);\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: FEATURE_QUERIES.filter((q) => q !== 'GetFeatureRequest'),\n context: LEAVES_PAGE_CONTEXT,\n });\n const [error, setError] = useState<string | null>(null);\n const busy = updateState.loading || removeState.loading;\n const statuses = (FEATURE_STATUSES as readonly string[]).includes(feature.status)\n ? FEATURE_STATUSES\n : [feature.status, ...FEATURE_STATUSES];\n\n const onStatus = async (status: string) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: feature.id, input: { status } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: feature.id } }));\n navigate('/products/features-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The request could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Status\"\n data-testid=\"feature-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={feature.status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== feature.status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <FeatureEditDialog feature={feature} />\n {/* BOFF-7983: the analytics page existed with no way in. */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"feature-analytics\"\n onClick={() => navigate(detailPath('/products/feature-analytics-page', feature.id))}\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 \"${feature.title}\"?`}\n description=\"The request and its votes are deleted. This cannot be undone here.\"\n confirmLabel=\"Delete request\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Edit a request's fields, including the project and the work item it rides on (the release readiness card needs\n * that link, and no UI could set it — BOFF-7966). Only changed fields are sent; an emptied field is cleared.\n */\nfunction FeatureEditDialog({ feature }: { feature: FeatureValues }) {\n const initial = {\n title: feature.title,\n description: feature.description ?? '',\n businessValue: feature.businessValue == null ? '' : String(feature.businessValue),\n effortEstimate: feature.effortEstimate ?? '',\n projectId: feature.projectId ?? '',\n workItemId: feature.workItemId ?? '',\n };\n const initialPrioritization: PrioritizationFormValues = {\n moscow: feature.moscow ?? '',\n kano: feature.kano ?? '',\n valueScore: feature.valueScore == null ? '' : String(feature.valueScore),\n effortScore: feature.effortScore == null ? '' : String(feature.effortScore),\n };\n const initialRice: RiceFormValues = {\n reach: feature.reach == null ? '' : String(feature.reach),\n impact: feature.impact == null ? '' : String(feature.impact),\n confidence: feature.confidence == null ? '' : String(feature.confidence),\n };\n // CWF-15.\n const initialFeedback: FeedbackFormValues = {\n rating: feature.rating == null ? '' : String(feature.rating),\n category: feature.category ?? '',\n };\n const initialAnonymous = feature.isAnonymous ?? false;\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [prioritization, setPrioritization] =\n useState<PrioritizationFormValues>(initialPrioritization);\n const [rice, setRice] = useState<RiceFormValues>(initialRice);\n const [feedback, setFeedback] = useState<FeedbackFormValues>(initialFeedback);\n const [anonymous, setAnonymous] = useState(initialAnonymous);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation(UPDATE, options);\n const businessValue = parseIntField(form.businessValue, { min: 0, max: 100 });\n const rating = parseIntField(feedback.rating, { min: 1, max: 5 });\n const canSave =\n form.title.trim() !== '' &&\n !businessValue.invalid &&\n !rating.invalid &&\n prioritizationScoresValid(prioritization) &&\n riceInputsValid(rice) &&\n !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\n const value = parseIntField(prioritization.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(prioritization.effortScore, { min: 1, max: 10 });\n const reach = parseIntField(rice.reach, { min: 1, max: 10 });\n const impact = parseIntField(rice.impact, { min: 1, max: 10 });\n const confidence = parseIntField(rice.confidence, { min: 1, max: 10 });\n const next = {\n title: form.title.trim(),\n description: text(form.description),\n businessValue: businessValue.value ?? null,\n effortEstimate: text(form.effortEstimate),\n projectId: form.projectId || null,\n workItemId: form.workItemId || null,\n moscow: prioritization.moscow || null,\n kano: prioritization.kano || null,\n valueScore: value.value ?? null,\n effortScore: effort.value ?? null,\n reach: reach.value ?? null,\n impact: impact.value ?? null,\n confidence: confidence.value ?? null,\n rating: rating.value ?? null,\n category: text(feedback.category),\n isAnonymous: anonymous,\n };\n const before = {\n title: initial.title,\n description: text(initial.description),\n businessValue: initial.businessValue === '' ? null : Number(initial.businessValue),\n effortEstimate: text(initial.effortEstimate),\n projectId: initial.projectId || null,\n workItemId: initial.workItemId || null,\n moscow: initialPrioritization.moscow || null,\n kano: initialPrioritization.kano || null,\n valueScore:\n initialPrioritization.valueScore === '' ? null : Number(initialPrioritization.valueScore),\n rating: initialFeedback.rating === '' ? null : Number(initialFeedback.rating),\n category: text(initialFeedback.category),\n isAnonymous: initialAnonymous,\n effortScore:\n initialPrioritization.effortScore === '' ? null : Number(initialPrioritization.effortScore),\n reach: initialRice.reach === '' ? null : Number(initialRice.reach),\n impact: initialRice.impact === '' ? null : Number(initialRice.impact),\n confidence: initialRice.confidence === '' ? null : Number(initialRice.confidence),\n };\n const input = Object.fromEntries(\n Object.entries(next).filter(([k, v]) => v !== before[k as keyof typeof before])\n );\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: feature.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The request could not be saved.'));\n }\n };\n\n const label = 'text-sm font-medium';\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n setForm(initial);\n setPrioritization(initialPrioritization);\n setRice(initialRice);\n setFeedback(initialFeedback);\n setAnonymous(initialAnonymous);\n } else {\n setError(null);\n }\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"feature-edit\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit feature request</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Link the request to the work item that delivers it so releases can show who is waiting\n on it.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"feature-edit-title\" className={label}>\n Title\n </label>\n <Input\n id=\"feature-edit-title\"\n className=\"mt-1\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-description\" className={label}>\n Description\n </label>\n <textarea\n id=\"feature-edit-description\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"feature-edit-value\" className={label}>\n Business value (0–100)\n </label>\n <Input\n id=\"feature-edit-value\"\n type=\"number\"\n min={0}\n max={100}\n className=\"mt-1\"\n value={form.businessValue}\n aria-invalid={businessValue.invalid}\n onChange={(e) => setForm({ ...form, businessValue: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-effort\" className={label}>\n Effort\n </label>\n <Input\n id=\"feature-edit-effort\"\n className=\"mt-1\"\n placeholder=\"e.g. M\"\n value={form.effortEstimate}\n onChange={(e) => setForm({ ...form, effortEstimate: e.target.value })}\n />\n </div>\n </div>\n {businessValue.invalid && (\n <ErrorLine message=\"Business value is a whole number from 0 to 100.\" />\n )}\n <div>\n <label htmlFor=\"feature-edit-project\" className={label}>\n Project\n </label>\n <div className=\"mt-1\">\n <ProjectSelect\n id=\"feature-edit-project\"\n value={form.projectId}\n noneLabel=\"No project\"\n onChange={(projectId) => setForm({ ...form, projectId, workItemId: '' })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"feature-edit-workitem\" className={label}>\n Delivered by work item\n </label>\n <div className=\"mt-1\">\n <WorkItemSearchSelect\n id=\"feature-edit-workitem\"\n projectId={form.projectId || undefined}\n value={form.workItemId}\n onChange={(workItemId) => setForm({ ...form, workItemId })}\n />\n </div>\n </div>\n {/* PRM-27: MoSCoW, Kano and Value-vs-Effort scoring. The quadrant itself is server-computed — this\n dialog shows the value last read back from the server, not a live client-side guess; the feature\n detail page's own Prioritization card refreshes with the saved value once this dialog closes\n (GetFeatureRequest is in FEATURE_QUERIES, awaited before the dialog closes). */}\n <div className=\"border-t pt-3\">\n <p className={label}>Prioritization</p>\n <div className=\"mt-1 flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Current quadrant:</span>\n <QuadrantBadge quadrant={feature.valueEffortQuadrant ?? null} />\n </div>\n <div className=\"mt-2\">\n <PrioritizationScoreFields\n idPrefix=\"feature-edit\"\n values={prioritization}\n onChange={setPrioritization}\n />\n </div>\n {/* PRM-26: RICE, next to the value/effort scores above. */}\n <div className=\"mt-3 border-t pt-3\">\n <p className={label}>RICE</p>\n <div className=\"mt-1 flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Current RICE score:</span>\n <span className=\"text-sm font-medium\" data-testid=\"feature-edit-rice-score\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n <div className=\"mt-2\">\n <RiceScoreFields\n idPrefix=\"feature-edit\"\n values={rice}\n onChange={setRice}\n effortScoreMissing={\n parseIntField(prioritization.effortScore, { min: 1, max: 10 }).value == null\n }\n />\n </div>\n </div>\n </div>\n {/* CWF-15: structured feedback — rating, category and the \"submit anonymously\" flag. */}\n <div className=\"border-t pt-3\">\n <p className={label}>Feedback</p>\n <div className=\"mt-2\">\n <FeedbackFields idPrefix=\"feature-edit\" values={feedback} onChange={setFeedback} />\n {rating.invalid && <ErrorLine message=\"Rating is a whole number from 1 to 5.\" />}\n </div>\n <div className=\"mt-3\">\n <AnonymousSubmissionCheckbox\n idPrefix=\"feature-edit\"\n checked={anonymous}\n onChange={setAnonymous}\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 {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n/**\n * The feature detail page's own Prioritization card (PRM-27): shows the current MoSCoW/Kano/scores and the\n * server-computed quadrant, with an inline \"Edit\" toggle for the four inputs. Everything shown outside the\n * editing form — including the quadrant badge — is read straight from the `feature` prop rather than mirrored\n * into local state, so it stays correct whichever of this card's own save or the sibling `FeatureEditDialog`'s\n * save last refreshed `GetFeatureRequest` (both awaited their refetch before returning/closing).\n */\ninterface UpdateFeaturePrioritizationResult {\n planmagnetUpdateFeatureRequest: { valueEffortQuadrant: string | null } | null;\n}\n\nexport function FeaturePrioritizationCard({ feature }: { feature: FeatureValues }) {\n const initial: PrioritizationFormValues = {\n moscow: feature.moscow ?? '',\n kano: feature.kano ?? '',\n valueScore: feature.valueScore == null ? '' : String(feature.valueScore),\n effortScore: feature.effortScore == null ? '' : String(feature.effortScore),\n };\n const initialRice: RiceFormValues = {\n reach: feature.reach == null ? '' : String(feature.reach),\n impact: feature.impact == null ? '' : String(feature.impact),\n confidence: feature.confidence == null ? '' : String(feature.confidence),\n };\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState<PrioritizationFormValues>(initial);\n const [rice, setRice] = useState<RiceFormValues>(initialRice);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation<UpdateFeaturePrioritizationResult>(UPDATE, options);\n\n const startEdit = () => {\n setForm(initial);\n setRice(initialRice);\n setError(null);\n setEditing(true);\n };\n\n const save = async () => {\n setError(null);\n const value = parseIntField(form.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(form.effortScore, { min: 1, max: 10 });\n const reach = parseIntField(rice.reach, { min: 1, max: 10 });\n const impact = parseIntField(rice.impact, { min: 1, max: 10 });\n const confidence = parseIntField(rice.confidence, { min: 1, max: 10 });\n const input = {\n moscow: form.moscow || null,\n kano: form.kano || null,\n valueScore: value.value ?? null,\n effortScore: effort.value ?? null,\n reach: reach.value ?? null,\n impact: impact.value ?? null,\n confidence: confidence.value ?? null,\n };\n try {\n // This mutation's own `options` also carries `awaitRefetchQueries: true` over `GetFeatureRequest`, so by\n // the time this resolves the `feature` prop has already refreshed with the new quadrant — read it straight\n // from there (below) rather than mirroring the response into local state, which a sibling editor's own save\n // (FeatureEditDialog, same refetch) would then leave stale.\n await throwOnError(update({ variables: { id: feature.id, input } }));\n setEditing(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The prioritization could not be saved.'));\n }\n };\n\n return (\n <Card data-testid=\"feature-prioritization-card\">\n <CardHeader className=\"flex flex-row items-center justify-between gap-2\">\n <CardTitle>Prioritization</CardTitle>\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"feature-prioritization-edit\"\n onClick={startEdit}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {!editing ? (\n <>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Quadrant:</span>\n <QuadrantBadge quadrant={feature.valueEffortQuadrant ?? null} />\n </div>\n <div className=\"grid grid-cols-2 gap-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">MoSCoW</span>\n <span className=\"font-medium\">{moscowLabel(feature.moscow)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Kano</span>\n <span className=\"font-medium\">{kanoLabel(feature.kano)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Value score</span>\n <span className=\"font-medium\">{feature.valueScore ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Effort score</span>\n <span className=\"font-medium\">{feature.effortScore ?? 'Not set'}</span>\n </div>\n </div>\n {/* PRM-26: RICE, next to the value/effort scores above. */}\n <div className=\"border-t pt-3\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-muted-foreground\">RICE score</span>\n <span className=\"font-medium\" data-testid=\"feature-rice-score\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n {feature.riceScore == null && (\n <p\n className=\"text-xs text-muted-foreground mt-0.5\"\n data-testid=\"feature-rice-missing-hint\"\n >\n {riceScoreMissingHint(feature)}\n </p>\n )}\n <div className=\"grid grid-cols-3 gap-2 text-sm mt-2\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reach</span>\n <span className=\"font-medium\">{feature.reach ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Impact</span>\n <span className=\"font-medium\">{feature.impact ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Confidence</span>\n <span className=\"font-medium\">{feature.confidence ?? 'Not set'}</span>\n </div>\n </div>\n </div>\n </>\n ) : (\n <>\n <PrioritizationScoreFields idPrefix=\"feature-prio\" values={form} onChange={setForm} />\n <div className=\"border-t pt-3\">\n <p className=\"text-sm font-medium mb-1\">RICE</p>\n <RiceScoreFields\n idPrefix=\"feature-prio\"\n values={rice}\n onChange={setRice}\n effortScoreMissing={\n parseIntField(form.effortScore, { min: 1, max: 10 }).value == null\n }\n />\n </div>\n {error && <ErrorLine message={error} />}\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(false)}>\n Cancel\n </Button>\n <Button\n size=\"sm\"\n data-testid=\"feature-prioritization-save\"\n disabled={\n state.loading || !prioritizationScoresValid(form) || !riceInputsValid(rice)\n }\n onClick={() => void save()}\n >\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * CWF-15 structured feedback: a 1-5 rating and a freeform category on a feature request, an opt-in "submit
3
+ * anonymously" on the request itself and (independently) on a vote. Shared by the feature request create/edit
4
+ * form (FeatureControls.tsx / ProductsNewFeaturePagePage.tsx), the feature detail and list pages, and the vote
5
+ * button, so the same controls and the same "what does this literal value mean" rendering are used everywhere.
6
+ *
7
+ * `rating`/`category` mirror `PrioritizationScoreFields`' shape convention elsewhere in this file family: '' on
8
+ * the form means "not set", converted to `null`/omitted at the mutation call site, never sent as an empty string
9
+ * (the schema's `category`/`rating` are `String`/`Int`, not "" is a value).
10
+ */
11
+ /** `FeatureRequest.category` has no fixed vocabulary server-side (any non-empty string is accepted) — these are
12
+ * just the suggestions offered in the field's `<datalist>`, taken from the schema's own doc comment example
13
+ * ("bug", "ui", "performance") plus two more common ones. Free text is always allowed alongside them. */
14
+ export declare const FEEDBACK_CATEGORY_SUGGESTIONS: readonly ["bug", "ui", "performance", "feature", "documentation"];
15
+ export interface FeedbackFormValues {
16
+ /** '' = not set, else a whole number 1-5 as text. */
17
+ rating: string;
18
+ /** '' = not set. */
19
+ category: string;
20
+ }
21
+ export declare const EMPTY_FEEDBACK_FORM: FeedbackFormValues;
22
+ /** An editable 1-5 star picker. Clicking the already-selected star clears the rating (CWF-15's `rating` is
23
+ * nullable) rather than requiring a separate "clear" control. */
24
+ export declare function RatingStars({ idPrefix, value, onChange, disabled, }: {
25
+ idPrefix: string;
26
+ /** '' = not set, else '1'..'5'. */
27
+ value: string;
28
+ onChange: (next: string) => void;
29
+ disabled?: boolean;
30
+ }): import("react/jsx-runtime").JSX.Element;
31
+ /** Read-only 1-5 stars for the detail/list pages — `rating == null` renders "Not rated" text, never a row of
32
+ * empty stars (which would look identical to an explicit 0, a value this field never holds). */
33
+ export declare function RatingDisplay({ rating }: {
34
+ rating: number | null | undefined;
35
+ }): import("react/jsx-runtime").JSX.Element;
36
+ /** The freeform category input, with a `<datalist>` of suggestions. */
37
+ export declare function CategoryField({ idPrefix, value, onChange, disabled, }: {
38
+ idPrefix: string;
39
+ value: string;
40
+ onChange: (next: string) => void;
41
+ disabled?: boolean;
42
+ }): import("react/jsx-runtime").JSX.Element;
43
+ /** A category badge for the detail/list pages — renders nothing when unset, same convention
44
+ * `QuadrantBadge`'s `hideWhenUnscored` uses for an unset score elsewhere in this file family. */
45
+ export declare function CategoryBadge({ category }: {
46
+ category: string | null | undefined;
47
+ }): import("react/jsx-runtime").JSX.Element | null;
48
+ /** Rating + category together — the two CWF-15 fields the create/edit form and the detail page both show. Kept
49
+ * separate from the "submit anonymously" checkbox below: a vote uses the checkbox alone, with no rating or
50
+ * category of its own. */
51
+ export declare function FeedbackFields({ idPrefix, values, onChange, disabled, }: {
52
+ idPrefix: string;
53
+ values: FeedbackFormValues;
54
+ onChange: (next: FeedbackFormValues) => void;
55
+ disabled?: boolean;
56
+ }): import("react/jsx-runtime").JSX.Element;
57
+ /** The "submit/vote anonymously" checkbox (CWF-15's `isAnonymous`), reused for both a feature request's own
58
+ * `isAnonymous` (create/edit) and a vote's independent `isAnonymous` (cast where the vote is cast) — the two
59
+ * flags are unrelated server-side (a named request can still receive an anonymous vote, and vice versa), so
60
+ * each call site holds its own boolean rather than sharing one. */
61
+ export declare function AnonymousSubmissionCheckbox({ idPrefix, checked, onChange, disabled, label, hint, }: {
62
+ idPrefix: string;
63
+ checked: boolean;
64
+ onChange: (next: boolean) => void;
65
+ disabled?: boolean;
66
+ label?: string;
67
+ /** `null` renders no hint line at all (e.g. the vote button, which is too compact for one). */
68
+ hint?: string | null;
69
+ }): import("react/jsx-runtime").JSX.Element;
70
+ //# sourceMappingURL=FeedbackControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeedbackControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeedbackControls.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH;;0GAE0G;AAC1G,eAAO,MAAM,6BAA6B,mEAMhC,CAAC;AAEX,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,mBAAmB,EAAE,kBAAiD,CAAC;AAIpF;kEACkE;AAClE,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAgB,GACjB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,2CA8DA;AAED;iGACiG;AACjG,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,2CAmB9E;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAgB,GACjB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,2CAwBA;AAED;kGACkG;AAClG,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,kDAOlF;AAED;;2BAE2B;AAC3B,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAgB,GACjB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,QAAQ,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,2CAiBA;AAED;;;oEAGoE;AACpE,wBAAgB,2BAA2B,CAAC,EAC1C,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAgB,EAChB,KAA4B,EAC5B,IAAwE,GACzE,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,2CAgBA"}
@@ -0,0 +1,153 @@
1
+ import { useRef as e } from "react";
2
+ import { Star as t } from "lucide-react";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
+ import { Badge as i, Checkbox as a, Input as o } from "@burdenoff/fe-libs/ui";
5
+ //#region src/planmagnet/controls/FeedbackControls.tsx
6
+ var s = [
7
+ "bug",
8
+ "ui",
9
+ "performance",
10
+ "feature",
11
+ "documentation"
12
+ ], c = {
13
+ rating: "",
14
+ category: ""
15
+ }, l = [
16
+ 1,
17
+ 2,
18
+ 3,
19
+ 4,
20
+ 5
21
+ ];
22
+ function u({ idPrefix: i, value: a, onChange: o, disabled: s = !1 }) {
23
+ let c = a === "" ? 0 : Number(a), u = e([]), d = c === 0 ? 0 : c - 1, f = (e, t) => {
24
+ let n = Math.min(l.length - 1, Math.max(0, e + t));
25
+ o(String(l[n])), u.current[n]?.focus();
26
+ };
27
+ return /* @__PURE__ */ r("div", { children: [/* @__PURE__ */ n("span", {
28
+ className: "text-sm font-medium",
29
+ id: `${i}-rating-label`,
30
+ children: "Rating"
31
+ }), /* @__PURE__ */ r("div", {
32
+ role: "radiogroup",
33
+ "aria-labelledby": `${i}-rating-label`,
34
+ className: "mt-1 flex items-center gap-1",
35
+ "data-testid": "feedback-rating-stars",
36
+ children: [l.map((e, r) => /* @__PURE__ */ n("button", {
37
+ ref: (e) => {
38
+ u.current[r] = e;
39
+ },
40
+ type: "button",
41
+ role: "radio",
42
+ "aria-checked": c === e,
43
+ "aria-label": `${e} star${e === 1 ? "" : "s"}`,
44
+ disabled: s,
45
+ tabIndex: r === d ? 0 : -1,
46
+ "data-testid": `feedback-rating-star-${e}`,
47
+ onClick: () => o(c === e ? "" : String(e)),
48
+ onKeyDown: (e) => {
49
+ s || (e.key === "ArrowRight" || e.key === "ArrowDown" ? (e.preventDefault(), f(r, 1)) : (e.key === "ArrowLeft" || e.key === "ArrowUp") && (e.preventDefault(), f(r, -1)));
50
+ },
51
+ className: "rounded p-0.5 disabled:opacity-60",
52
+ children: /* @__PURE__ */ n(t, {
53
+ className: `h-5 w-5 ${e <= c ? "fill-current text-primary" : "text-muted-foreground"}`,
54
+ "aria-hidden": "true"
55
+ })
56
+ }, e)), /* @__PURE__ */ n("span", {
57
+ className: "ml-1 text-xs text-muted-foreground",
58
+ children: c === 0 ? "Not set" : `${c}/5`
59
+ })]
60
+ })] });
61
+ }
62
+ function d({ rating: e }) {
63
+ return e == null ? /* @__PURE__ */ n("span", {
64
+ className: "text-sm text-muted-foreground",
65
+ children: "Not rated"
66
+ }) : /* @__PURE__ */ n("span", {
67
+ className: "inline-flex items-center gap-0.5",
68
+ "data-testid": "feedback-rating-display",
69
+ "aria-label": `${e} out of 5 stars`,
70
+ children: l.map((r) => /* @__PURE__ */ n(t, {
71
+ className: `h-3.5 w-3.5 ${r <= e ? "fill-current text-primary" : "text-muted-foreground"}`,
72
+ "aria-hidden": "true"
73
+ }, r))
74
+ });
75
+ }
76
+ function f({ idPrefix: e, value: t, onChange: i, disabled: a = !1 }) {
77
+ let c = `${e}-category-suggestions`;
78
+ return /* @__PURE__ */ r("div", { children: [
79
+ /* @__PURE__ */ n("label", {
80
+ htmlFor: `${e}-category`,
81
+ className: "text-sm font-medium",
82
+ children: "Category"
83
+ }),
84
+ /* @__PURE__ */ n(o, {
85
+ id: `${e}-category`,
86
+ "data-testid": "feedback-category",
87
+ list: c,
88
+ className: "mt-1",
89
+ placeholder: "e.g. bug, ui, performance",
90
+ value: t,
91
+ disabled: a,
92
+ onChange: (e) => i(e.target.value)
93
+ }),
94
+ /* @__PURE__ */ n("datalist", {
95
+ id: c,
96
+ children: s.map((e) => /* @__PURE__ */ n("option", { value: e }, e))
97
+ })
98
+ ] });
99
+ }
100
+ function p({ category: e }) {
101
+ return e ? /* @__PURE__ */ n(i, {
102
+ variant: "outline",
103
+ className: "text-xs",
104
+ "data-testid": "feedback-category-badge",
105
+ children: e
106
+ }) : null;
107
+ }
108
+ function m({ idPrefix: e, values: t, onChange: i, disabled: a = !1 }) {
109
+ return /* @__PURE__ */ r("div", {
110
+ className: "space-y-3",
111
+ "data-testid": "feedback-fields",
112
+ children: [/* @__PURE__ */ n(u, {
113
+ idPrefix: e,
114
+ value: t.rating,
115
+ onChange: (e) => i({
116
+ ...t,
117
+ rating: e
118
+ }),
119
+ disabled: a
120
+ }), /* @__PURE__ */ n(f, {
121
+ idPrefix: e,
122
+ value: t.category,
123
+ onChange: (e) => i({
124
+ ...t,
125
+ category: e
126
+ }),
127
+ disabled: a
128
+ })]
129
+ });
130
+ }
131
+ function h({ idPrefix: e, checked: t, onChange: i, disabled: o = !1, label: s = "Submit anonymously", hint: c = "Hides your name from everyone except moderators handling abuse." }) {
132
+ return /* @__PURE__ */ r("div", {
133
+ className: "space-y-0.5",
134
+ children: [/* @__PURE__ */ r("label", {
135
+ htmlFor: `${e}-anonymous`,
136
+ className: "flex items-center gap-2 text-sm",
137
+ children: [/* @__PURE__ */ n(a, {
138
+ id: `${e}-anonymous`,
139
+ "data-testid": "feedback-anonymous-checkbox",
140
+ checked: t,
141
+ onCheckedChange: (e) => i(e === !0),
142
+ disabled: o
143
+ }), s]
144
+ }), c && /* @__PURE__ */ n("p", {
145
+ className: "pl-6 text-xs text-muted-foreground",
146
+ children: c
147
+ })]
148
+ });
149
+ }
150
+ //#endregion
151
+ export { h as AnonymousSubmissionCheckbox, p as CategoryBadge, c as EMPTY_FEEDBACK_FORM, m as FeedbackFields, d as RatingDisplay };
152
+
153
+ //# sourceMappingURL=FeedbackControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeedbackControls.js","names":[],"sources":["../../../src/planmagnet/controls/FeedbackControls.tsx"],"sourcesContent":["import { useRef } from 'react';\nimport { Badge, Checkbox, Input } from '@burdenoff/fe-libs/ui';\nimport { Star } from 'lucide-react';\n\n/**\n * CWF-15 structured feedback: a 1-5 rating and a freeform category on a feature request, an opt-in \"submit\n * anonymously\" on the request itself and (independently) on a vote. Shared by the feature request create/edit\n * form (FeatureControls.tsx / ProductsNewFeaturePagePage.tsx), the feature detail and list pages, and the vote\n * button, so the same controls and the same \"what does this literal value mean\" rendering are used everywhere.\n *\n * `rating`/`category` mirror `PrioritizationScoreFields`' shape convention elsewhere in this file family: '' on\n * the form means \"not set\", converted to `null`/omitted at the mutation call site, never sent as an empty string\n * (the schema's `category`/`rating` are `String`/`Int`, not \"\" is a value).\n */\n\n/** `FeatureRequest.category` has no fixed vocabulary server-side (any non-empty string is accepted) — these are\n * just the suggestions offered in the field's `<datalist>`, taken from the schema's own doc comment example\n * (\"bug\", \"ui\", \"performance\") plus two more common ones. Free text is always allowed alongside them. */\nexport const FEEDBACK_CATEGORY_SUGGESTIONS = [\n 'bug',\n 'ui',\n 'performance',\n 'feature',\n 'documentation',\n] as const;\n\nexport interface FeedbackFormValues {\n /** '' = not set, else a whole number 1-5 as text. */\n rating: string;\n /** '' = not set. */\n category: string;\n}\n\nexport const EMPTY_FEEDBACK_FORM: FeedbackFormValues = { rating: '', category: '' };\n\nconst RATING_VALUES = [1, 2, 3, 4, 5] as const;\n\n/** An editable 1-5 star picker. Clicking the already-selected star clears the rating (CWF-15's `rating` is\n * nullable) rather than requiring a separate \"clear\" control. */\nexport function RatingStars({\n idPrefix,\n value,\n onChange,\n disabled = false,\n}: {\n idPrefix: string;\n /** '' = not set, else '1'..'5'. */\n value: string;\n onChange: (next: string) => void;\n disabled?: boolean;\n}) {\n const selected = value === '' ? 0 : Number(value);\n // ARIA radiogroup keyboard pattern (matches the native `<input type=\"radio\">` groups elsewhere in this MFE,\n // e.g. SyncControls/SprintControls): one tab stop for the whole group (roving tabindex on the checked star,\n // or the first star when none is set), Left/Up and Right/Down both move focus AND select, same as a native\n // radio group's arrow-key behavior.\n const buttonRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const focusIndex = selected === 0 ? 0 : selected - 1;\n const moveFocus = (fromIndex: number, delta: number) => {\n const nextIndex = Math.min(RATING_VALUES.length - 1, Math.max(0, fromIndex + delta));\n onChange(String(RATING_VALUES[nextIndex]));\n buttonRefs.current[nextIndex]?.focus();\n };\n return (\n <div>\n <span className=\"text-sm font-medium\" id={`${idPrefix}-rating-label`}>\n Rating\n </span>\n <div\n role=\"radiogroup\"\n aria-labelledby={`${idPrefix}-rating-label`}\n className=\"mt-1 flex items-center gap-1\"\n data-testid=\"feedback-rating-stars\"\n >\n {RATING_VALUES.map((star, index) => (\n <button\n key={star}\n ref={(el) => {\n buttonRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected === star}\n aria-label={`${star} star${star === 1 ? '' : 's'}`}\n disabled={disabled}\n tabIndex={index === focusIndex ? 0 : -1}\n data-testid={`feedback-rating-star-${star}`}\n onClick={() => onChange(selected === star ? '' : String(star))}\n onKeyDown={(e) => {\n if (disabled) return;\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n moveFocus(index, 1);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n moveFocus(index, -1);\n }\n }}\n className=\"rounded p-0.5 disabled:opacity-60\"\n >\n <Star\n className={`h-5 w-5 ${star <= selected ? 'fill-current text-primary' : 'text-muted-foreground'}`}\n aria-hidden=\"true\"\n />\n </button>\n ))}\n <span className=\"ml-1 text-xs text-muted-foreground\">\n {selected === 0 ? 'Not set' : `${selected}/5`}\n </span>\n </div>\n </div>\n );\n}\n\n/** Read-only 1-5 stars for the detail/list pages — `rating == null` renders \"Not rated\" text, never a row of\n * empty stars (which would look identical to an explicit 0, a value this field never holds). */\nexport function RatingDisplay({ rating }: { rating: number | null | undefined }) {\n if (rating == null) {\n return <span className=\"text-sm text-muted-foreground\">Not rated</span>;\n }\n return (\n <span\n className=\"inline-flex items-center gap-0.5\"\n data-testid=\"feedback-rating-display\"\n aria-label={`${rating} out of 5 stars`}\n >\n {RATING_VALUES.map((star) => (\n <Star\n key={star}\n className={`h-3.5 w-3.5 ${star <= rating ? 'fill-current text-primary' : 'text-muted-foreground'}`}\n aria-hidden=\"true\"\n />\n ))}\n </span>\n );\n}\n\n/** The freeform category input, with a `<datalist>` of suggestions. */\nexport function CategoryField({\n idPrefix,\n value,\n onChange,\n disabled = false,\n}: {\n idPrefix: string;\n value: string;\n onChange: (next: string) => void;\n disabled?: boolean;\n}) {\n const listId = `${idPrefix}-category-suggestions`;\n return (\n <div>\n <label htmlFor={`${idPrefix}-category`} className=\"text-sm font-medium\">\n Category\n </label>\n <Input\n id={`${idPrefix}-category`}\n data-testid=\"feedback-category\"\n list={listId}\n className=\"mt-1\"\n placeholder=\"e.g. bug, ui, performance\"\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n />\n <datalist id={listId}>\n {FEEDBACK_CATEGORY_SUGGESTIONS.map((c) => (\n <option key={c} value={c} />\n ))}\n </datalist>\n </div>\n );\n}\n\n/** A category badge for the detail/list pages — renders nothing when unset, same convention\n * `QuadrantBadge`'s `hideWhenUnscored` uses for an unset score elsewhere in this file family. */\nexport function CategoryBadge({ category }: { category: string | null | undefined }) {\n if (!category) return null;\n return (\n <Badge variant=\"outline\" className=\"text-xs\" data-testid=\"feedback-category-badge\">\n {category}\n </Badge>\n );\n}\n\n/** Rating + category together — the two CWF-15 fields the create/edit form and the detail page both show. Kept\n * separate from the \"submit anonymously\" checkbox below: a vote uses the checkbox alone, with no rating or\n * category of its own. */\nexport function FeedbackFields({\n idPrefix,\n values,\n onChange,\n disabled = false,\n}: {\n idPrefix: string;\n values: FeedbackFormValues;\n onChange: (next: FeedbackFormValues) => void;\n disabled?: boolean;\n}) {\n return (\n <div className=\"space-y-3\" data-testid=\"feedback-fields\">\n <RatingStars\n idPrefix={idPrefix}\n value={values.rating}\n onChange={(rating) => onChange({ ...values, rating })}\n disabled={disabled}\n />\n <CategoryField\n idPrefix={idPrefix}\n value={values.category}\n onChange={(category) => onChange({ ...values, category })}\n disabled={disabled}\n />\n </div>\n );\n}\n\n/** The \"submit/vote anonymously\" checkbox (CWF-15's `isAnonymous`), reused for both a feature request's own\n * `isAnonymous` (create/edit) and a vote's independent `isAnonymous` (cast where the vote is cast) — the two\n * flags are unrelated server-side (a named request can still receive an anonymous vote, and vice versa), so\n * each call site holds its own boolean rather than sharing one. */\nexport function AnonymousSubmissionCheckbox({\n idPrefix,\n checked,\n onChange,\n disabled = false,\n label = 'Submit anonymously',\n hint = 'Hides your name from everyone except moderators handling abuse.',\n}: {\n idPrefix: string;\n checked: boolean;\n onChange: (next: boolean) => void;\n disabled?: boolean;\n label?: string;\n /** `null` renders no hint line at all (e.g. the vote button, which is too compact for one). */\n hint?: string | null;\n}) {\n return (\n <div className=\"space-y-0.5\">\n <label htmlFor={`${idPrefix}-anonymous`} className=\"flex items-center gap-2 text-sm\">\n <Checkbox\n id={`${idPrefix}-anonymous`}\n data-testid=\"feedback-anonymous-checkbox\"\n checked={checked}\n onCheckedChange={(next) => onChange(next === true)}\n disabled={disabled}\n />\n {label}\n </label>\n {hint && <p className=\"pl-6 text-xs text-muted-foreground\">{hint}</p>}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SyncControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/SyncControls.tsx"],"names":[],"mappings":"AAgCA,OAAO,EASL,KAAK,kBAAkB,EACvB,KAAK,SAAS,EAEd,KAAK,aAAa,EAClB,KAAK,aAAa,EAEnB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;GAOG;AAEH,eAAO,MAAM,iBAAiB,UAAgB,CAAC;AAC/C,eAAO,MAAM,mBAAmB,UAA8C,CAAC;AAc/E,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAM7D;AAQD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAMhE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAMjE;AAED,8GAA8G;AAC9G,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,GAAS,EACT,SAAmC,EACnC,MAAM,GACP,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CAwBA;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,2CAyBlE;AAmBD;;yBAEyB;AACzB,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,QAAQ,EACR,IAAW,GACZ,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC3C,IAAI,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;CACzB,2CAuCA;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,2CA4BzE;AAmBD,oGAAoG;AACpG,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,2CA6CvF;AAYD;4FAC4F;AAC5F,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CA+ClE;AAQD,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,KAAK;AACL,0GAA0G;AAC1G,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,2CAmCA;AAcD;;;uDAGuD;AACvD,wBAAgB,sBAAsB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,2CAyD5E;AAED,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAK5E,CAAC;AA2BF,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,aAAa,CAAC;IACzB,gBAAgB,EAAE,kBAAkB,CAAC;IACrC,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,2CA+S9D"}
1
+ {"version":3,"file":"SyncControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/SyncControls.tsx"],"names":[],"mappings":"AAgCA,OAAO,EASL,KAAK,kBAAkB,EACvB,KAAK,SAAS,EAEd,KAAK,aAAa,EAClB,KAAK,aAAa,EAEnB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;GAOG;AAEH,eAAO,MAAM,iBAAiB,UAAgB,CAAC;AAC/C,eAAO,MAAM,mBAAmB,UAA8C,CAAC;AAc/E,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAM7D;AAQD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAMhE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAMjE;AAED,8GAA8G;AAC9G,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,GAAS,EACT,SAAmC,EACnC,MAAM,GACP,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CAwBA;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,2CAyBlE;AAmBD;;yBAEyB;AACzB,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,QAAQ,EACR,IAAW,GACZ,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC3C,IAAI,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;CACzB,2CAuCA;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,2CA4BzE;AAmBD,oGAAoG;AACpG,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,2CA6CvF;AAYD;4FAC4F;AAC5F,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CA+ClE;AAQD,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,KAAK;AACL,0GAA0G;AAC1G,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,2CAmCA;AAcD;;;uDAGuD;AACvD,wBAAgB,sBAAsB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,2CAyD5E;AAED,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAQ5E,CAAC;AA2BF,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,aAAa,CAAC;IACzB,gBAAgB,EAAE,kBAAkB,CAAC;IACrC,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,2CA+S9D"}
@@ -348,6 +348,7 @@ function Y({ conflictId: r }) {
348
348
  var pe = {
349
349
  SOURCE_WINS: "PlanMagnet wins — the local work item overwrites the provider on a detected conflict.",
350
350
  TARGET_WINS: "Provider wins — the external tool overwrites PlanMagnet on a detected conflict.",
351
+ MERGE: "Merge — keep this project's title and description, take the remote status, priority, labels and due date.",
351
352
  MANUAL: "Flag and skip — a conflict is recorded and left alone until resolved here."
352
353
  }, me = "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm", he = w`
353
354
  query SyncControlsConnection($id: ID!) {