@burdenoff/microfe-planmagnet 2026.927.4 → 2026.927.5

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 (137) hide show
  1. package/dist/generated/global-types.d.ts +9 -1
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-operations.d.ts +0 -12
  4. package/dist/generated/wspace-operations.d.ts.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  6. package/dist/planmagnet/PlanMagnetRoutes.js +14 -10
  7. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  8. package/dist/planmagnet/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
  9. package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +149 -127
  10. package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
  11. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  12. package/dist/planmagnet/controls/FeatureControls.js +142 -139
  13. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  14. package/dist/planmagnet/controls/FeatureTriage.d.ts +21 -0
  15. package/dist/planmagnet/controls/FeatureTriage.d.ts.map +1 -0
  16. package/dist/planmagnet/controls/FeatureTriage.js +398 -0
  17. package/dist/planmagnet/controls/FeatureTriage.js.map +1 -0
  18. package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -1
  19. package/dist/planmagnet/controls/MilestoneControls.js +76 -73
  20. package/dist/planmagnet/controls/MilestoneControls.js.map +1 -1
  21. package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
  22. package/dist/planmagnet/controls/ProjectControls.js +47 -44
  23. package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
  24. package/dist/planmagnet/controls/ReleaseControls.d.ts.map +1 -1
  25. package/dist/planmagnet/controls/ReleaseControls.js +47 -44
  26. package/dist/planmagnet/controls/ReleaseControls.js.map +1 -1
  27. package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
  28. package/dist/planmagnet/controls/WorkItemControls.js +68 -65
  29. package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
  30. package/dist/planmagnet/hooks/useCounts.d.ts +39 -0
  31. package/dist/planmagnet/hooks/useCounts.d.ts.map +1 -0
  32. package/dist/planmagnet/hooks/useCounts.js +209 -0
  33. package/dist/planmagnet/hooks/useCounts.js.map +1 -0
  34. package/dist/planmagnet/import/exportColumns.d.ts +52 -0
  35. package/dist/planmagnet/import/exportColumns.d.ts.map +1 -0
  36. package/dist/planmagnet/import/exportColumns.js +81 -0
  37. package/dist/planmagnet/import/exportColumns.js.map +1 -0
  38. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
  39. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +255 -242
  40. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  41. package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  42. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +199 -160
  43. package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
  44. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  45. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +147 -144
  46. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  47. package/dist/planmagnet/pages/GoalsGoalsPagePage.d.ts.map +1 -1
  48. package/dist/planmagnet/pages/GoalsGoalsPagePage.js +2 -7
  49. package/dist/planmagnet/pages/GoalsGoalsPagePage.js.map +1 -1
  50. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.d.ts.map +1 -1
  51. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js +173 -173
  52. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js.map +1 -1
  53. package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
  54. package/dist/planmagnet/pages/ProductsEditProductPagePage.js +120 -117
  55. package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
  56. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.d.ts.map +1 -1
  57. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +97 -117
  58. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
  59. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  60. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +168 -103
  61. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  62. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  63. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +117 -103
  64. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  65. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  66. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +4 -8
  67. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  68. package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
  69. package/dist/planmagnet/pages/ProductsProductsPagePage.js +85 -90
  70. package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
  71. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
  72. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +12 -2
  73. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
  74. package/dist/planmagnet/pages/ProductsReleasesPagePage.d.ts.map +1 -1
  75. package/dist/planmagnet/pages/ProductsReleasesPagePage.js +2 -2
  76. package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
  77. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
  78. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +48 -71
  79. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
  80. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
  81. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +17 -13
  82. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
  83. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.d.ts.map +1 -1
  84. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js +1 -1
  85. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js.map +1 -1
  86. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +6 -6
  87. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  88. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +11 -8
  89. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  90. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +4 -4
  91. package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
  92. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +1 -1
  93. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
  94. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  95. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +152 -139
  96. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  97. package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
  98. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +75 -74
  99. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
  100. package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts +7 -0
  101. package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
  102. package/dist/planmagnet/pages/ReportsReportsPagePage.js +39 -201
  103. package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
  104. package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
  105. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +118 -99
  106. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
  107. package/dist/planmagnet/pages/TasksImportPagePage.d.ts +2 -0
  108. package/dist/planmagnet/pages/TasksImportPagePage.d.ts.map +1 -0
  109. package/dist/planmagnet/pages/TasksImportPagePage.js +309 -0
  110. package/dist/planmagnet/pages/TasksImportPagePage.js.map +1 -0
  111. package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
  112. package/dist/planmagnet/pages/TasksTasksPagePage.js +110 -104
  113. package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
  114. package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
  115. package/dist/planmagnet/pages/TeamTeamPagePage.js +99 -177
  116. package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
  117. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
  118. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -4
  119. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
  120. package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts.map +1 -1
  121. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js +10 -4
  122. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js.map +1 -1
  123. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
  124. package/dist/planmagnet/roadmaps/RoadmapControls.js +151 -138
  125. package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
  126. package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
  127. package/dist/planmagnet/sprints/SprintControls.js +32 -30
  128. package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
  129. package/dist/planmagnet/utils/mutationResult.d.ts +10 -0
  130. package/dist/planmagnet/utils/mutationResult.d.ts.map +1 -1
  131. package/dist/planmagnet/utils/mutationResult.js +2 -1
  132. package/dist/planmagnet/utils/mutationResult.js.map +1 -1
  133. package/dist/planmagnet/utils/rowNavigation.d.ts +2 -0
  134. package/dist/planmagnet/utils/rowNavigation.d.ts.map +1 -1
  135. package/dist/planmagnet/utils/rowNavigation.js +9 -8
  136. package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
  137. 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 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 } from '../utils/mutationResult';\nimport { parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { ProjectSelect, WorkItemSearchSelect } from './Pickers';\nimport { enumLabel } from './WorkItemControls';\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. */\nconst 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 }\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}\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 });\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 [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\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 = form.title.trim() !== '' && !businessValue.invalid && !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\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 };\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 };\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(e instanceof Error ? e.message : '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) setForm(initial);\n else setError(null);\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 {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"],"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 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 { parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { ProjectSelect, WorkItemSearchSelect } from './Pickers';\nimport { enumLabel } from './WorkItemControls';\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. */\nconst 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 }\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}\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 [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\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 = form.title.trim() !== '' && !businessValue.invalid && !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\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 };\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 };\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(e instanceof Error ? e.message : '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) setForm(initial);\n else setError(null);\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 {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"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export declare const DECLINE_REASON_MAX = 2000;
2
+ export interface TriageFeature {
3
+ id: string;
4
+ title: string;
5
+ status: string;
6
+ projectId?: string | null;
7
+ snoozedUntil?: string | null;
8
+ }
9
+ /** True while the request waits for a decision (accept, decline and merge apply). */
10
+ export declare const isOpenRequest: (status: string) => boolean;
11
+ /** A snooze that has not passed yet. */
12
+ export declare const isSnoozed: (snoozedUntil: string | null | undefined, now: number) => boolean;
13
+ /**
14
+ * The four triage actions for an undecided request. Snooze applies only while it waits for triage (SUBMITTED);
15
+ * a snoozed request offers "Back to triage" instead.
16
+ */
17
+ export declare function FeatureTriageActions({ feature, now }: {
18
+ feature: TriageFeature;
19
+ now: number;
20
+ }): import("react/jsx-runtime").JSX.Element | null;
21
+ //# sourceMappingURL=FeatureTriage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureTriage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureTriage.tsx"],"names":[],"mappings":"AAyFA,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAGvC,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,qFAAqF;AACrF,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,YAA0B,CAAC;AACvE,wCAAwC;AACxC,eAAO,MAAM,SAAS,GAAI,cAAc,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,MAAM,YACpB,CAAC;AAuZ3D;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,kDAoB7F"}
@@ -0,0 +1,398 @@
1
+ import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
2
+ import { throwOnError as t } from "../utils/mutationResult.js";
3
+ import { ProjectSelect as n } from "./Pickers.js";
4
+ import { ErrorLine as r } from "./ConfirmButton.js";
5
+ import { useDeferredValue as i, useState as a } from "react";
6
+ import { AlarmClock as o, Check as s, GitMerge as c, Sun as l, X as u } from "lucide-react";
7
+ import { gql as d } from "@apollo/client";
8
+ import { useMutation as f, useQuery as p } from "@apollo/client/react";
9
+ import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
10
+ import { Button as _, Input as v, ResponsiveDialog as y, ResponsiveDialogContent as b, ResponsiveDialogDescription as x, ResponsiveDialogFooter as S, ResponsiveDialogHeader as C, ResponsiveDialogTitle as w, ResponsiveDialogTrigger as T } from "@burdenoff/fe-libs/ui";
11
+ //#region src/planmagnet/controls/FeatureTriage.tsx
12
+ var E = {
13
+ refetchQueries: [
14
+ "GetFeatureRequest",
15
+ "ListFeatureRequests",
16
+ "CountsFeatures",
17
+ "ListFeatureRequestsForProduct",
18
+ "ListFeatureRequestsForProducts",
19
+ "ListAllFeaturesAnalytics",
20
+ "ReleaseCustomerRequests"
21
+ ],
22
+ awaitRefetchQueries: !0
23
+ }, D = d`
24
+ mutation FeatureTriageAccept($id: ID!, $input: PlanmagnetAcceptFeatureRequestInput) {
25
+ planmagnetAcceptFeatureRequest(id: $id, input: $input) {
26
+ id
27
+ status
28
+ }
29
+ }
30
+ `, O = d`
31
+ mutation FeatureTriageDecline($id: ID!, $reason: String!) {
32
+ planmagnetDeclineFeatureRequest(id: $id, reason: $reason) {
33
+ id
34
+ status
35
+ declineReason
36
+ }
37
+ }
38
+ `, k = d`
39
+ mutation FeatureTriageSnooze($id: ID!, $until: DateTime) {
40
+ planmagnetSnoozeFeatureRequest(id: $id, until: $until) {
41
+ id
42
+ snoozedUntil
43
+ }
44
+ }
45
+ `, A = d`
46
+ mutation FeatureTriageMerge($id: ID!, $intoId: ID!) {
47
+ planmagnetMergeFeatureRequest(id: $id, intoId: $intoId) {
48
+ id
49
+ voteCount
50
+ }
51
+ }
52
+ `, j = d`
53
+ query FeatureTriageMergeCandidates($workspaceId: ID!, $filter: PlanmagnetFeatureRequestFilter) {
54
+ planmagnetFeatureRequests(
55
+ workspaceId: $workspaceId
56
+ filter: $filter
57
+ pagination: { first: 20 }
58
+ ) {
59
+ items {
60
+ id
61
+ title
62
+ status
63
+ voteCount
64
+ }
65
+ totalCount
66
+ }
67
+ }
68
+ `, M = 2e3, N = ["SUBMITTED", "UNDER_REVIEW"], P = (e) => N.includes(e), F = (e, t) => !!e && new Date(e).getTime() > t, I = (e, t) => e instanceof Error ? e.message : t;
69
+ function L({ trigger: e, testId: t, title: n, description: i, children: a, confirmLabel: o, canConfirm: s, busy: c, onConfirm: l, error: u, onOpenChange: d, open: f }) {
70
+ return /* @__PURE__ */ g(y, {
71
+ open: f,
72
+ onOpenChange: d,
73
+ children: [/* @__PURE__ */ h(T, {
74
+ asChild: !0,
75
+ children: /* @__PURE__ */ h(_, {
76
+ size: "sm",
77
+ variant: "outline",
78
+ "data-testid": t,
79
+ onClick: (e) => e.stopPropagation(),
80
+ onKeyDown: (e) => e.stopPropagation(),
81
+ children: e
82
+ })
83
+ }), /* @__PURE__ */ g(b, {
84
+ className: "max-w-lg",
85
+ children: [
86
+ /* @__PURE__ */ g(C, { children: [/* @__PURE__ */ h(w, { children: n }), /* @__PURE__ */ h(x, { children: i })] }),
87
+ /* @__PURE__ */ g("div", {
88
+ className: "space-y-3",
89
+ children: [a, u && /* @__PURE__ */ h(r, { message: u })]
90
+ }),
91
+ /* @__PURE__ */ g(S, { children: [/* @__PURE__ */ h(_, {
92
+ variant: "outline",
93
+ onClick: () => d(!1),
94
+ children: "Cancel"
95
+ }), /* @__PURE__ */ h(_, {
96
+ onClick: l,
97
+ disabled: !s || c,
98
+ children: c ? "Saving…" : o
99
+ })] })
100
+ ]
101
+ })]
102
+ });
103
+ }
104
+ function R({ feature: e }) {
105
+ let [r, i] = a(!1), [o, c] = a("PLANNED"), [l, u] = a(e.projectId ?? ""), [d, p] = a(null), [_, v] = f(D, E), y = async () => {
106
+ p(null);
107
+ try {
108
+ await t(_({ variables: {
109
+ id: e.id,
110
+ input: {
111
+ status: o,
112
+ ...l && l !== e.projectId && { projectId: l }
113
+ }
114
+ } })), i(!1);
115
+ } catch (e) {
116
+ p(I(e, "The request could not be accepted."));
117
+ }
118
+ };
119
+ return /* @__PURE__ */ g(L, {
120
+ open: r,
121
+ onOpenChange: (e) => {
122
+ i(e), e || p(null);
123
+ },
124
+ testId: "triage-accept",
125
+ trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(s, { className: "mr-1 h-4 w-4" }), "Accept"] }),
126
+ title: `Accept "${e.title}"`,
127
+ description: "Accepting takes it out of triage. Planned means it will be built; Under review means a closer look first.",
128
+ confirmLabel: "Accept",
129
+ canConfirm: !0,
130
+ busy: v.loading,
131
+ onConfirm: () => void y(),
132
+ error: d,
133
+ children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
134
+ htmlFor: `triage-accept-status-${e.id}`,
135
+ className: "text-sm font-medium",
136
+ children: "Accept as"
137
+ }), /* @__PURE__ */ g("select", {
138
+ id: `triage-accept-status-${e.id}`,
139
+ className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
140
+ value: o,
141
+ onChange: (e) => c(e.target.value),
142
+ children: [/* @__PURE__ */ h("option", {
143
+ value: "PLANNED",
144
+ children: "Planned"
145
+ }), e.status !== "UNDER_REVIEW" && /* @__PURE__ */ h("option", {
146
+ value: "UNDER_REVIEW",
147
+ children: "Under review"
148
+ })]
149
+ })] }), /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
150
+ htmlFor: `triage-accept-project-${e.id}`,
151
+ className: "text-sm font-medium",
152
+ children: "Project that will deliver it"
153
+ }), /* @__PURE__ */ h("div", {
154
+ className: "mt-1",
155
+ children: /* @__PURE__ */ h(n, {
156
+ id: `triage-accept-project-${e.id}`,
157
+ value: l,
158
+ noneLabel: "No project yet",
159
+ onChange: u
160
+ })
161
+ })] })]
162
+ });
163
+ }
164
+ function z({ feature: e }) {
165
+ let [n, r] = a(!1), [i, o] = a(""), [s, c] = a(null), [l, d] = f(O, E), p = i.trim(), _ = p.length > M, v = async () => {
166
+ c(null);
167
+ try {
168
+ await t(l({ variables: {
169
+ id: e.id,
170
+ reason: p
171
+ } })), r(!1), o("");
172
+ } catch (e) {
173
+ c(I(e, "The request could not be declined."));
174
+ }
175
+ };
176
+ return /* @__PURE__ */ h(L, {
177
+ open: n,
178
+ onOpenChange: (e) => {
179
+ r(e), e || c(null);
180
+ },
181
+ testId: "triage-decline",
182
+ trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(u, { className: "mr-1 h-4 w-4" }), "Decline"] }),
183
+ title: `Decline "${e.title}"`,
184
+ description: "The reason is kept on the request, so anyone who asks later can see why.",
185
+ confirmLabel: "Decline",
186
+ canConfirm: p !== "" && !_,
187
+ busy: d.loading,
188
+ onConfirm: () => void v(),
189
+ error: s ?? (_ ? "A decline reason can be at most 2000 characters." : null),
190
+ children: /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("label", {
191
+ htmlFor: `triage-decline-reason-${e.id}`,
192
+ className: "text-sm font-medium",
193
+ children: "Reason"
194
+ }), /* @__PURE__ */ h("textarea", {
195
+ id: `triage-decline-reason-${e.id}`,
196
+ className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[88px]",
197
+ value: i,
198
+ "aria-invalid": _,
199
+ onChange: (e) => o(e.target.value)
200
+ })] })
201
+ });
202
+ }
203
+ function B(e) {
204
+ let t = (t) => {
205
+ let n = new Date(e + t * 864e5);
206
+ return n.setHours(9, 0, 0, 0), n;
207
+ };
208
+ return [
209
+ {
210
+ label: "Tomorrow",
211
+ until: t(1)
212
+ },
213
+ {
214
+ label: "In 3 days",
215
+ until: t(3)
216
+ },
217
+ {
218
+ label: "Next week",
219
+ until: t(7)
220
+ },
221
+ {
222
+ label: "In a month",
223
+ until: t(30)
224
+ }
225
+ ];
226
+ }
227
+ function V({ feature: e }) {
228
+ let [n, r] = a(!1), [i, s] = a([]), [c, l] = a(0), [u, d] = a(null), [p, _] = f(k, E), v = async () => {
229
+ d(null);
230
+ let n = i[c];
231
+ if (n) try {
232
+ await t(p({ variables: {
233
+ id: e.id,
234
+ until: n.until.toISOString()
235
+ } })), r(!1);
236
+ } catch (e) {
237
+ d(I(e, "The request could not be snoozed."));
238
+ }
239
+ };
240
+ return /* @__PURE__ */ h(L, {
241
+ open: n,
242
+ onOpenChange: (e) => {
243
+ r(e), e ? (s(B(Date.now())), l(0)) : d(null);
244
+ },
245
+ testId: "triage-snooze",
246
+ trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(o, { className: "mr-1 h-4 w-4" }), "Snooze"] }),
247
+ title: `Snooze "${e.title}"`,
248
+ description: "It leaves the triage queue and comes back on its own. It stays undecided meanwhile.",
249
+ confirmLabel: "Snooze",
250
+ canConfirm: i.length > 0,
251
+ busy: _.loading,
252
+ onConfirm: () => void v(),
253
+ error: u,
254
+ children: /* @__PURE__ */ g("fieldset", {
255
+ className: "space-y-2",
256
+ children: [/* @__PURE__ */ h("legend", {
257
+ className: "text-sm font-medium",
258
+ children: "Bring it back"
259
+ }), i.map((t, n) => /* @__PURE__ */ g("label", {
260
+ className: "flex items-center gap-2 text-sm",
261
+ children: [
262
+ /* @__PURE__ */ h("input", {
263
+ type: "radio",
264
+ name: `triage-snooze-${e.id}`,
265
+ checked: c === n,
266
+ onChange: () => l(n)
267
+ }),
268
+ t.label,
269
+ /* @__PURE__ */ g("span", {
270
+ className: "text-muted-foreground",
271
+ children: [
272
+ "(",
273
+ t.until.toLocaleDateString(void 0, {
274
+ weekday: "short",
275
+ day: "numeric",
276
+ month: "short"
277
+ }),
278
+ ")"
279
+ ]
280
+ })
281
+ ]
282
+ }, t.label))]
283
+ })
284
+ });
285
+ }
286
+ function H({ feature: n }) {
287
+ let { workspaceId: r } = e(), [o, s] = a(!1), [l, u] = a(""), [d, _] = a(""), [y, b] = a(null), x = i(l.trim()), [S, C] = f(A, E), { data: w, loading: T } = p(j, {
288
+ variables: {
289
+ workspaceId: r ?? "",
290
+ filter: x ? { search: x } : {}
291
+ },
292
+ skip: !r || !o
293
+ }), D = (w?.planmagnetFeatureRequests.items ?? []).filter((e) => e.id !== n.id && e.status !== "DUPLICATE"), O = async () => {
294
+ b(null);
295
+ try {
296
+ await t(S({ variables: {
297
+ id: n.id,
298
+ intoId: d
299
+ } })), s(!1);
300
+ } catch (e) {
301
+ b(I(e, "The requests could not be merged."));
302
+ }
303
+ };
304
+ return /* @__PURE__ */ h(L, {
305
+ open: o,
306
+ onOpenChange: (e) => {
307
+ s(e), e ? (u(""), _("")) : b(null);
308
+ },
309
+ testId: "triage-merge",
310
+ trigger: /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(c, { className: "mr-1 h-4 w-4" }), "Merge"] }),
311
+ title: `Merge "${n.title}" into its original`,
312
+ description: "It becomes a duplicate of the original and its votes move there (someone who voted on both keeps one vote).",
313
+ confirmLabel: "Merge",
314
+ canConfirm: d !== "",
315
+ busy: C.loading,
316
+ onConfirm: () => void O(),
317
+ error: y,
318
+ children: /* @__PURE__ */ g("div", {
319
+ className: "space-y-1",
320
+ children: [
321
+ /* @__PURE__ */ h("label", {
322
+ htmlFor: `triage-merge-into-${n.id}`,
323
+ className: "text-sm font-medium",
324
+ children: "Original request"
325
+ }),
326
+ /* @__PURE__ */ h(v, {
327
+ "aria-label": "Search feature requests",
328
+ placeholder: "Search by title",
329
+ value: l,
330
+ onChange: (e) => u(e.target.value)
331
+ }),
332
+ /* @__PURE__ */ g("select", {
333
+ id: `triage-merge-into-${n.id}`,
334
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
335
+ value: d,
336
+ onChange: (e) => _(e.target.value),
337
+ children: [/* @__PURE__ */ h("option", {
338
+ value: "",
339
+ children: T && !w ? "Searching…" : D.length ? "Choose the original" : "No match"
340
+ }), D.map((e) => /* @__PURE__ */ g("option", {
341
+ value: e.id,
342
+ children: [
343
+ e.title,
344
+ " (",
345
+ e.voteCount,
346
+ " vote",
347
+ e.voteCount === 1 ? "" : "s",
348
+ ")"
349
+ ]
350
+ }, e.id))]
351
+ })
352
+ ]
353
+ })
354
+ });
355
+ }
356
+ function U({ feature: e }) {
357
+ let [n, i] = f(k, E), [o, s] = a(null), c = async () => {
358
+ s(null);
359
+ try {
360
+ await t(n({ variables: {
361
+ id: e.id,
362
+ until: null
363
+ } }));
364
+ } catch (e) {
365
+ s(I(e, "The request could not be woken."));
366
+ }
367
+ };
368
+ return /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ g(_, {
369
+ size: "sm",
370
+ variant: "outline",
371
+ "data-testid": "triage-wake",
372
+ disabled: i.loading,
373
+ onClick: (e) => {
374
+ e.stopPropagation(), c();
375
+ },
376
+ children: [/* @__PURE__ */ h(l, { className: "mr-1 h-4 w-4" }), "Back to triage"]
377
+ }), o && /* @__PURE__ */ h(r, { message: o })] });
378
+ }
379
+ function W({ feature: e, now: t }) {
380
+ if (!P(e.status)) return null;
381
+ let n = F(e.snoozedUntil, t), r = (e) => e.stopPropagation();
382
+ return /* @__PURE__ */ g("div", {
383
+ className: "flex flex-wrap items-center gap-2",
384
+ "data-testid": "triage-actions",
385
+ onClick: r,
386
+ onKeyDown: r,
387
+ children: [
388
+ /* @__PURE__ */ h(R, { feature: e }),
389
+ /* @__PURE__ */ h(z, { feature: e }),
390
+ e.status === "SUBMITTED" && h(n ? U : V, { feature: e }),
391
+ /* @__PURE__ */ h(H, { feature: e })
392
+ ]
393
+ });
394
+ }
395
+ //#endregion
396
+ export { W as FeatureTriageActions, P as isOpenRequest, F as isSnoozed };
397
+
398
+ //# sourceMappingURL=FeatureTriage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureTriage.js","names":[],"sources":["../../../src/planmagnet/controls/FeatureTriage.tsx"],"sourcesContent":["import { useDeferredValue, useState, type ReactNode } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } 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 { AlarmClock, Check, GitMerge, Sun, X } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ErrorLine } from './ConfirmButton';\nimport { ProjectSelect } from './Pickers';\n\n/**\n * Triage for incoming feature requests (BUILD_PLAN P1.3): accept, decline with a reason, snooze, merge into the\n * original. Linear's triage is these four plus commenting. The feedback UI had none of them — a request could only\n * have its status flipped, so a decline carried no reason and a duplicate kept its votes away from the original.\n */\n\n/** Every query that shows a feature request or feature counts. */\nconst TRIAGE_REFETCH = [\n 'GetFeatureRequest',\n 'ListFeatureRequests',\n 'CountsFeatures',\n 'ListFeatureRequestsForProduct',\n 'ListFeatureRequestsForProducts',\n 'ListAllFeaturesAnalytics',\n 'ReleaseCustomerRequests',\n];\nconst options = { refetchQueries: TRIAGE_REFETCH, awaitRefetchQueries: true };\n\nconst ACCEPT = gql`\n mutation FeatureTriageAccept($id: ID!, $input: PlanmagnetAcceptFeatureRequestInput) {\n planmagnetAcceptFeatureRequest(id: $id, input: $input) {\n id\n status\n }\n }\n`;\nconst DECLINE = gql`\n mutation FeatureTriageDecline($id: ID!, $reason: String!) {\n planmagnetDeclineFeatureRequest(id: $id, reason: $reason) {\n id\n status\n declineReason\n }\n }\n`;\nconst SNOOZE = gql`\n mutation FeatureTriageSnooze($id: ID!, $until: DateTime) {\n planmagnetSnoozeFeatureRequest(id: $id, until: $until) {\n id\n snoozedUntil\n }\n }\n`;\nconst MERGE = gql`\n mutation FeatureTriageMerge($id: ID!, $intoId: ID!) {\n planmagnetMergeFeatureRequest(id: $id, intoId: $intoId) {\n id\n voteCount\n }\n }\n`;\nconst MERGE_CANDIDATES = gql`\n query FeatureTriageMergeCandidates($workspaceId: ID!, $filter: PlanmagnetFeatureRequestFilter) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: $filter\n pagination: { first: 20 }\n ) {\n items {\n id\n title\n status\n voteCount\n }\n totalCount\n }\n }\n`;\n\nexport const DECLINE_REASON_MAX = 2000;\nconst OPEN = ['SUBMITTED', 'UNDER_REVIEW'];\n\nexport interface TriageFeature {\n id: string;\n title: string;\n status: string;\n projectId?: string | null;\n snoozedUntil?: string | null;\n}\n\n/** True while the request waits for a decision (accept, decline and merge apply). */\nexport const isOpenRequest = (status: string) => OPEN.includes(status);\n/** A snooze that has not passed yet. */\nexport const isSnoozed = (snoozedUntil: string | null | undefined, now: number) =>\n !!snoozedUntil && new Date(snoozedUntil).getTime() > now;\n\nconst errorText = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\n\n/** A dialog with one action; the trigger never propagates to a clickable row it sits in. */\nfunction ActionDialog({\n trigger,\n testId,\n title,\n description,\n children,\n confirmLabel,\n canConfirm,\n busy,\n onConfirm,\n error,\n onOpenChange,\n open,\n}: {\n trigger: ReactNode;\n testId: string;\n title: string;\n description: string;\n children: ReactNode;\n confirmLabel: string;\n canConfirm: boolean;\n busy: boolean;\n onConfirm: () => void;\n error: string | null;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid={testId}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => e.stopPropagation()}\n >\n {trigger}\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n {children}\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={onConfirm} disabled={!canConfirm || busy}>\n {busy ? 'Saving…' : confirmLabel}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction AcceptDialog({ feature }: { feature: TriageFeature }) {\n const [open, setOpen] = useState(false);\n const [status, setStatus] = useState<'PLANNED' | 'UNDER_REVIEW'>('PLANNED');\n const [projectId, setProjectId] = useState(feature.projectId ?? '');\n const [error, setError] = useState<string | null>(null);\n const [accept, state] = useMutation(ACCEPT, options);\n const submit = async () => {\n setError(null);\n try {\n await throwOnError(\n accept({\n variables: {\n id: feature.id,\n input: { status, ...(projectId && projectId !== feature.projectId && { projectId }) },\n },\n })\n );\n setOpen(false);\n } catch (e) {\n setError(errorText(e, 'The request could not be accepted.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n testId=\"triage-accept\"\n trigger={\n <>\n <Check className=\"mr-1 h-4 w-4\" />\n Accept\n </>\n }\n title={`Accept \"${feature.title}\"`}\n description=\"Accepting takes it out of triage. Planned means it will be built; Under review means a closer look first.\"\n confirmLabel=\"Accept\"\n canConfirm\n busy={state.loading}\n onConfirm={() => void submit()}\n error={error}\n >\n <div>\n <label htmlFor={`triage-accept-status-${feature.id}`} className=\"text-sm font-medium\">\n Accept as\n </label>\n <select\n id={`triage-accept-status-${feature.id}`}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={status}\n onChange={(e) => setStatus(e.target.value as 'PLANNED' | 'UNDER_REVIEW')}\n >\n <option value=\"PLANNED\">Planned</option>\n {feature.status !== 'UNDER_REVIEW' && <option value=\"UNDER_REVIEW\">Under review</option>}\n </select>\n </div>\n <div>\n <label htmlFor={`triage-accept-project-${feature.id}`} className=\"text-sm font-medium\">\n Project that will deliver it\n </label>\n <div className=\"mt-1\">\n <ProjectSelect\n id={`triage-accept-project-${feature.id}`}\n value={projectId}\n noneLabel=\"No project yet\"\n onChange={setProjectId}\n />\n </div>\n </div>\n </ActionDialog>\n );\n}\n\nfunction DeclineDialog({ feature }: { feature: TriageFeature }) {\n const [open, setOpen] = useState(false);\n const [reason, setReason] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [decline, state] = useMutation(DECLINE, options);\n const trimmed = reason.trim();\n const tooLong = trimmed.length > DECLINE_REASON_MAX;\n const submit = async () => {\n setError(null);\n try {\n await throwOnError(decline({ variables: { id: feature.id, reason: trimmed } }));\n setOpen(false);\n setReason('');\n } catch (e) {\n setError(errorText(e, 'The request could not be declined.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) setError(null);\n }}\n testId=\"triage-decline\"\n trigger={\n <>\n <X className=\"mr-1 h-4 w-4\" />\n Decline\n </>\n }\n title={`Decline \"${feature.title}\"`}\n description=\"The reason is kept on the request, so anyone who asks later can see why.\"\n confirmLabel=\"Decline\"\n canConfirm={trimmed !== '' && !tooLong}\n busy={state.loading}\n onConfirm={() => void submit()}\n error={\n error ??\n (tooLong ? `A decline reason can be at most ${DECLINE_REASON_MAX} characters.` : null)\n }\n >\n <div>\n <label htmlFor={`triage-decline-reason-${feature.id}`} className=\"text-sm font-medium\">\n Reason\n </label>\n <textarea\n id={`triage-decline-reason-${feature.id}`}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[88px]\"\n value={reason}\n aria-invalid={tooLong}\n onChange={(e) => setReason(e.target.value)}\n />\n </div>\n </ActionDialog>\n );\n}\n\n/** Snooze presets are computed when the dialog opens, never during render. */\nfunction snoozePresets(from: number) {\n const day = 86_400_000;\n const morning = (days: number) => {\n const d = new Date(from + days * day);\n d.setHours(9, 0, 0, 0);\n return d;\n };\n return [\n { label: 'Tomorrow', until: morning(1) },\n { label: 'In 3 days', until: morning(3) },\n { label: 'Next week', until: morning(7) },\n { label: 'In a month', until: morning(30) },\n ];\n}\n\nfunction SnoozeDialog({ feature }: { feature: TriageFeature }) {\n const [open, setOpen] = useState(false);\n const [presets, setPresets] = useState<{ label: string; until: Date }[]>([]);\n const [choice, setChoice] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [snooze, state] = useMutation(SNOOZE, options);\n const submit = async () => {\n setError(null);\n const preset = presets[choice];\n if (!preset) return;\n try {\n await throwOnError(\n snooze({ variables: { id: feature.id, until: preset.until.toISOString() } })\n );\n setOpen(false);\n } catch (e) {\n setError(errorText(e, 'The request could not be snoozed.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n setPresets(snoozePresets(Date.now()));\n setChoice(0);\n } else setError(null);\n }}\n testId=\"triage-snooze\"\n trigger={\n <>\n <AlarmClock className=\"mr-1 h-4 w-4\" />\n Snooze\n </>\n }\n title={`Snooze \"${feature.title}\"`}\n description=\"It leaves the triage queue and comes back on its own. It stays undecided meanwhile.\"\n confirmLabel=\"Snooze\"\n canConfirm={presets.length > 0}\n busy={state.loading}\n onConfirm={() => void submit()}\n error={error}\n >\n <fieldset className=\"space-y-2\">\n <legend className=\"text-sm font-medium\">Bring it back</legend>\n {presets.map((p, i) => (\n <label key={p.label} className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n name={`triage-snooze-${feature.id}`}\n checked={choice === i}\n onChange={() => setChoice(i)}\n />\n {p.label}\n <span className=\"text-muted-foreground\">\n (\n {p.until.toLocaleDateString(undefined, {\n weekday: 'short',\n day: 'numeric',\n month: 'short',\n })}\n )\n </span>\n </label>\n ))}\n </fieldset>\n </ActionDialog>\n );\n}\n\nfunction MergeDialog({ feature }: { feature: TriageFeature }) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [intoId, setIntoId] = useState('');\n const [error, setError] = useState<string | null>(null);\n const deferred = useDeferredValue(search.trim());\n const [merge, state] = useMutation(MERGE, options);\n const { data, loading } = useQuery<{\n planmagnetFeatureRequests: {\n items: { id: string; title: string; status: string; voteCount: number }[];\n totalCount: number;\n };\n }>(MERGE_CANDIDATES, {\n variables: { workspaceId: workspaceId ?? '', filter: deferred ? { search: deferred } : {} },\n skip: !workspaceId || !open,\n });\n // A request cannot merge into itself, and a duplicate is not an original.\n const candidates = (data?.planmagnetFeatureRequests.items ?? []).filter(\n (f) => f.id !== feature.id && f.status !== 'DUPLICATE'\n );\n const submit = async () => {\n setError(null);\n try {\n await throwOnError(merge({ variables: { id: feature.id, intoId } }));\n setOpen(false);\n } catch (e) {\n setError(errorText(e, 'The requests could not be merged.'));\n }\n };\n return (\n <ActionDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n setSearch('');\n setIntoId('');\n } else setError(null);\n }}\n testId=\"triage-merge\"\n trigger={\n <>\n <GitMerge className=\"mr-1 h-4 w-4\" />\n Merge\n </>\n }\n title={`Merge \"${feature.title}\" into its original`}\n description=\"It becomes a duplicate of the original and its votes move there (someone who voted on both keeps one vote).\"\n confirmLabel=\"Merge\"\n canConfirm={intoId !== ''}\n busy={state.loading}\n onConfirm={() => void submit()}\n error={error}\n >\n <div className=\"space-y-1\">\n <label htmlFor={`triage-merge-into-${feature.id}`} className=\"text-sm font-medium\">\n Original request\n </label>\n <Input\n aria-label=\"Search feature requests\"\n placeholder=\"Search by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={`triage-merge-into-${feature.id}`}\n className=\"w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={intoId}\n onChange={(e) => setIntoId(e.target.value)}\n >\n <option value=\"\">\n {loading && !data\n ? 'Searching…'\n : candidates.length\n ? 'Choose the original'\n : 'No match'}\n </option>\n {candidates.map((f) => (\n <option key={f.id} value={f.id}>\n {f.title} ({f.voteCount} vote{f.voteCount === 1 ? '' : 's'})\n </option>\n ))}\n </select>\n </div>\n </ActionDialog>\n );\n}\n\nfunction WakeButton({ feature }: { feature: TriageFeature }) {\n const [snooze, state] = useMutation(SNOOZE, options);\n const [error, setError] = useState<string | null>(null);\n const wake = async () => {\n setError(null);\n try {\n await throwOnError(snooze({ variables: { id: feature.id, until: null } }));\n } catch (e) {\n setError(errorText(e, 'The request could not be woken.'));\n }\n };\n return (\n <>\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"triage-wake\"\n disabled={state.loading}\n onClick={(e) => {\n e.stopPropagation();\n void wake();\n }}\n >\n <Sun className=\"mr-1 h-4 w-4\" />\n Back to triage\n </Button>\n {error && <ErrorLine message={error} />}\n </>\n );\n}\n\n/**\n * The four triage actions for an undecided request. Snooze applies only while it waits for triage (SUBMITTED);\n * a snoozed request offers \"Back to triage\" instead.\n */\nexport function FeatureTriageActions({ feature, now }: { feature: TriageFeature; now: number }) {\n if (!isOpenRequest(feature.status)) return null;\n const snoozed = isSnoozed(feature.snoozedUntil, now);\n // React events bubble through portals: without this, a click or Enter inside a dialog reached the clickable\n // list row around these actions and opened the request instead.\n const contain = (e: { stopPropagation: () => void }) => e.stopPropagation();\n return (\n <div\n className=\"flex flex-wrap items-center gap-2\"\n data-testid=\"triage-actions\"\n onClick={contain}\n onKeyDown={contain}\n >\n <AcceptDialog feature={feature} />\n <DeclineDialog feature={feature} />\n {feature.status === 'SUBMITTED' &&\n (snoozed ? <WakeButton feature={feature} /> : <SnoozeDialog feature={feature} />)}\n <MergeDialog feature={feature} />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"MilestoneControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/MilestoneControls.tsx"],"names":[],"mappings":"AAoBA;;;;GAIG;AAEH,eAAO,MAAM,kBAAkB,yEAMrB,CAAC;AA2BX,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,eAAe,CAAA;CAAE,2CAoEnF"}
1
+ {"version":3,"file":"MilestoneControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/MilestoneControls.tsx"],"names":[],"mappings":"AAoBA;;;;GAIG;AAEH,eAAO,MAAM,kBAAkB,yEAMrB,CAAC;AA2BX,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,eAAe,CAAA;CAAE,2CAqEnF"}