@burdenoff/microfe-planmagnet 2026.1001.8 → 2026.1002.2

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 (143) hide show
  1. package/dist/generated/global-types.d.ts +5 -0
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-operations.d.ts +1814 -401
  4. package/dist/generated/wspace-operations.d.ts.map +1 -1
  5. package/dist/generated/wspace-types.d.ts +5167 -54
  6. package/dist/generated/wspace-types.d.ts.map +1 -1
  7. package/dist/index.js +8 -8
  8. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  9. package/dist/planmagnet/PlanMagnetRoot.js +172 -160
  10. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  11. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  12. package/dist/planmagnet/PlanMagnetRoutes.js +95 -83
  13. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  14. package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts +16 -0
  15. package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts.map +1 -0
  16. package/dist/planmagnet/comments/CommentAttachmentUpload.js +218 -0
  17. package/dist/planmagnet/comments/CommentAttachmentUpload.js.map +1 -0
  18. package/dist/planmagnet/comments/CommentAttachments.d.ts +15 -0
  19. package/dist/planmagnet/comments/CommentAttachments.d.ts.map +1 -0
  20. package/dist/planmagnet/comments/CommentAttachments.js +74 -0
  21. package/dist/planmagnet/comments/CommentAttachments.js.map +1 -0
  22. package/dist/planmagnet/comments/CommentBody.d.ts +11 -0
  23. package/dist/planmagnet/comments/CommentBody.d.ts.map +1 -0
  24. package/dist/planmagnet/comments/CommentBody.js +130 -0
  25. package/dist/planmagnet/comments/CommentBody.js.map +1 -0
  26. package/dist/planmagnet/comments/CommentThread.d.ts +7 -2
  27. package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
  28. package/dist/planmagnet/comments/CommentThread.js +288 -233
  29. package/dist/planmagnet/comments/CommentThread.js.map +1 -1
  30. package/dist/planmagnet/comments/MentionComposer.d.ts +0 -7
  31. package/dist/planmagnet/comments/MentionComposer.d.ts.map +1 -1
  32. package/dist/planmagnet/comments/MentionComposer.js +225 -86
  33. package/dist/planmagnet/comments/MentionComposer.js.map +1 -1
  34. package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
  35. package/dist/planmagnet/comments/TargetComments.js +44 -6
  36. package/dist/planmagnet/comments/TargetComments.js.map +1 -1
  37. package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
  38. package/dist/planmagnet/comments/WorkItemComments.js +22 -3
  39. package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
  40. package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts +102 -0
  41. package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts.map +1 -0
  42. package/dist/planmagnet/comments/commentAttachmentUploadClient.js +298 -0
  43. package/dist/planmagnet/comments/commentAttachmentUploadClient.js.map +1 -0
  44. package/dist/planmagnet/comments/commentImageTokens.d.ts +5 -0
  45. package/dist/planmagnet/comments/commentImageTokens.d.ts.map +1 -0
  46. package/dist/planmagnet/comments/commentImageTokens.js +14 -0
  47. package/dist/planmagnet/comments/commentImageTokens.js.map +1 -0
  48. package/dist/planmagnet/comments/commentTokens.d.ts +31 -0
  49. package/dist/planmagnet/comments/commentTokens.d.ts.map +1 -0
  50. package/dist/planmagnet/comments/commentTokens.js +142 -0
  51. package/dist/planmagnet/comments/commentTokens.js.map +1 -0
  52. package/dist/planmagnet/comments/fileDisplay.d.ts +22 -0
  53. package/dist/planmagnet/comments/fileDisplay.d.ts.map +1 -0
  54. package/dist/planmagnet/comments/fileDisplay.js +48 -0
  55. package/dist/planmagnet/comments/fileDisplay.js.map +1 -0
  56. package/dist/planmagnet/comments/useCommentFileUrl.d.ts +22 -0
  57. package/dist/planmagnet/comments/useCommentFileUrl.d.ts.map +1 -0
  58. package/dist/planmagnet/comments/useCommentFileUrl.js +54 -0
  59. package/dist/planmagnet/comments/useCommentFileUrl.js.map +1 -0
  60. package/dist/planmagnet/controls/Pickers.d.ts +7 -2
  61. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  62. package/dist/planmagnet/controls/Pickers.js +10 -9
  63. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  64. package/dist/planmagnet/index.d.ts +1 -1
  65. package/dist/planmagnet/index.d.ts.map +1 -1
  66. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +4 -4
  67. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
  68. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  69. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +133 -131
  70. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  71. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  72. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +5 -5
  73. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  74. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +6 -6
  75. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  76. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  77. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +105 -82
  78. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  79. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts +2 -0
  80. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts.map +1 -0
  81. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js +174 -0
  82. package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js.map +1 -0
  83. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts +2 -0
  84. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts.map +1 -0
  85. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js +350 -0
  86. package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js.map +1 -0
  87. package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts +2 -0
  88. package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts.map +1 -0
  89. package/dist/planmagnet/pages/ProductsProductLinesPagePage.js +168 -0
  90. package/dist/planmagnet/pages/ProductsProductLinesPagePage.js.map +1 -0
  91. package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
  92. package/dist/planmagnet/pages/ProductsProductsPagePage.js +1 -1
  93. package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
  94. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  95. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +156 -154
  96. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  97. package/dist/planmagnet/product/FeatureLinkPanels.d.ts +29 -0
  98. package/dist/planmagnet/product/FeatureLinkPanels.d.ts.map +1 -0
  99. package/dist/planmagnet/product/FeatureLinkPanels.js +373 -0
  100. package/dist/planmagnet/product/FeatureLinkPanels.js.map +1 -0
  101. package/dist/planmagnet/product/ProductControls.js +1 -1
  102. package/dist/planmagnet/product/ProductControls.js.map +1 -1
  103. package/dist/planmagnet/product/ProductFeatureControls.d.ts +39 -0
  104. package/dist/planmagnet/product/ProductFeatureControls.d.ts.map +1 -0
  105. package/dist/planmagnet/product/ProductFeatureControls.js +315 -0
  106. package/dist/planmagnet/product/ProductFeatureControls.js.map +1 -0
  107. package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts +4 -0
  108. package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts.map +1 -0
  109. package/dist/planmagnet/product/ProductFeatureLinkCard.js +192 -0
  110. package/dist/planmagnet/product/ProductFeatureLinkCard.js.map +1 -0
  111. package/dist/planmagnet/product/ProductLineControls.d.ts +52 -0
  112. package/dist/planmagnet/product/ProductLineControls.d.ts.map +1 -0
  113. package/dist/planmagnet/product/ProductLineControls.js +417 -0
  114. package/dist/planmagnet/product/ProductLineControls.js.map +1 -0
  115. package/dist/planmagnet/product/ProductTeamsPanel.d.ts +4 -0
  116. package/dist/planmagnet/product/ProductTeamsPanel.d.ts.map +1 -0
  117. package/dist/planmagnet/product/ProductTeamsPanel.js +129 -0
  118. package/dist/planmagnet/product/ProductTeamsPanel.js.map +1 -0
  119. package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts +4 -0
  120. package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts.map +1 -0
  121. package/dist/planmagnet/product/WorkItemProductFeatureChip.js +41 -0
  122. package/dist/planmagnet/product/WorkItemProductFeatureChip.js.map +1 -0
  123. package/dist/planmagnet/product/hierarchy.d.ts +127 -0
  124. package/dist/planmagnet/product/hierarchy.d.ts.map +1 -0
  125. package/dist/planmagnet/product/hierarchy.js +173 -0
  126. package/dist/planmagnet/product/hierarchy.js.map +1 -0
  127. package/dist/planmagnet/product/hierarchyOperations.d.ts +35 -0
  128. package/dist/planmagnet/product/hierarchyOperations.d.ts.map +1 -0
  129. package/dist/planmagnet/product/hierarchyOperations.js +440 -0
  130. package/dist/planmagnet/product/hierarchyOperations.js.map +1 -0
  131. package/dist/planmagnet/product/refusal.d.ts +14 -0
  132. package/dist/planmagnet/product/refusal.d.ts.map +1 -0
  133. package/dist/planmagnet/product/refusal.js +10 -0
  134. package/dist/planmagnet/product/refusal.js.map +1 -0
  135. package/dist/planmagnet/product/routes.d.ts +4 -0
  136. package/dist/planmagnet/product/routes.d.ts.map +1 -1
  137. package/dist/planmagnet/product/routes.js +2 -2
  138. package/dist/planmagnet/product/routes.js.map +1 -1
  139. package/dist/shared/utils/i18n.d.ts +3 -0
  140. package/dist/shared/utils/i18n.d.ts.map +1 -1
  141. package/dist/shared/utils/i18n.js +1 -1
  142. package/dist/shared/utils/i18n.js.map +1 -1
  143. package/package.json +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"TasksTaskDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WorkItemComments } from '../comments/WorkItemComments';\nimport { WorkItemActivity } from '../comments/WorkItemActivity';\nimport { TaskTimePanel } from '../time/TaskTimePanel';\nimport { PersonName } from '../people/People';\nimport { DocSearchSelect } from '../controls/Pickers';\nimport { CustomFieldsPanel } from '../controls/CustomFieldValuesPanel';\nimport { DependencyPanel } from '../dependency/DependencyPanel';\nimport { IssueLinksPanel } from '../links/IssueLinksPanel';\nimport { WorkItemMilestoneSelect } from '../milestones/MilestoneControls';\nimport {\n WORK_ITEM_PRIORITIES,\n WorkItemStatusSelect,\n WorkItemAssigneeSelect,\n WorkItemEnumSelect,\n WorkItemHeaderActions,\n} from '../controls/WorkItemControls';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { FileText, Link2, Plus, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { usePresence } from '../hooks/usePresence';\nimport { PresenceAvatars } from '../presence/PresenceAvatars';\nimport { WorkItemDiscussionPanel } from '../discussion/WorkItemDiscussionPanel';\nimport { CodeLinksPanel, type WorkItemCodeLink } from '../links/CodeLinksPanel';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItemDetail($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n projectId\n title\n description\n type\n status\n priority\n key\n assigneeId\n reporterId\n createdBy\n sprintId\n milestoneId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n updatedAt\n customFieldValues {\n id\n definitionId\n value\n }\n discussionConversationId\n codeLinks {\n id\n kind\n title\n url\n state\n number\n authorLogin\n mergedAt\n ref\n }\n }\n }\n`;\n\nconst GET_WORK_ITEM_DOCS = gql`\n query GetWorkItemDocs($workItemId: ID!) {\n planmagnetWorkItemDocs(workItemId: $workItemId) {\n id\n title\n status\n version\n updatedAt\n }\n }\n`;\n\nconst CREATE_DOC = gql`\n mutation CreateDocForWorkItem($input: PlanmagnetCreateDocInput!) {\n planmagnetCreateDoc(input: $input) {\n id\n }\n }\n`;\n\nconst LINK_DOC_TO_WORK_ITEM = gql`\n mutation LinkDocToWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetLinkDocToWorkItem(docId: $docId, workItemId: $workItemId) {\n id\n }\n }\n`;\n\nconst UNLINK_DOC_FROM_WORK_ITEM = gql`\n mutation UnlinkDocFromWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n createdBy: string;\n sprintId: string | null;\n milestoneId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n customFieldValues: { id: string; definitionId: string; value: unknown }[];\n discussionConversationId: string | null;\n codeLinks: WorkItemCodeLink[];\n}\n\ninterface WorkItemDocNode {\n id: string;\n title: string;\n status: string;\n version: number;\n updatedAt: string;\n}\n\nconst docStatusColor: Record<string, string> = {\n DRAFT: 'bg-gray-100 text-gray-800',\n PUBLISHED: 'bg-green-100 text-green-800',\n ARCHIVED: 'bg-gray-100 text-gray-500',\n};\n\n/**\n * Linked-docs panel (BOFF-4443): create a doc and link it to this work item\n * in one action, view currently-linked docs, and unlink one. Reads/writes go\n * through the real planmagnetWorkItemDocs / planmagnetCreateDoc /\n * planmagnetLinkDocToWorkItem / planmagnetUnlinkDocFromWorkItem GraphQL\n * fields — no fabricated data. Full doc editing (revisions, revert) lives on\n * a dedicated doc detail view; this panel is scoped to linking.\n */\nfunction LinkedDocsPanel({ workItemId }: { workItemId: string }) {\n const navigate = usePlanMagnetNavigate();\n const [isComposing, setIsComposing] = useState(false);\n // BOFF-7961: only \"New Doc\" existed, so a doc written on the Docs page could never be linked to a task.\n const [isLinking, setIsLinking] = useState(false);\n const [existingDocId, setExistingDocId] = useState('');\n const [newTitle, setNewTitle] = useState('');\n const [newContent, setNewContent] = useState('');\n\n const { data, loading, error, refetch } = useQuery<{\n planmagnetWorkItemDocs: WorkItemDocNode[];\n }>(GET_WORK_ITEM_DOCS, {\n variables: { workItemId },\n skip: !workItemId,\n });\n\n const [createDoc, { loading: creating }] = useMutation(CREATE_DOC);\n const [linkDoc, { loading: linking }] = useMutation(LINK_DOC_TO_WORK_ITEM);\n const [unlinkDoc, { loading: unlinking }] = useMutation(UNLINK_DOC_FROM_WORK_ITEM);\n // A refused mutation resolves with { error } (fe-libs errorPolicy 'all');\n // the composer used to clear itself as if the doc had been saved.\n const [actionError, setActionError] = useState<string | null>(null);\n\n const docs = data?.planmagnetWorkItemDocs ?? [];\n\n const handleCreateAndLink = async () => {\n if (!newTitle.trim() || !newContent.trim()) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n createDoc({ variables: { input: { title: newTitle.trim(), content: newContent.trim() } } })\n );\n const docId = result.data?.planmagnetCreateDoc?.id;\n if (docId) {\n await throwOnError(linkDoc({ variables: { docId, workItemId } }));\n }\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be saved.');\n return;\n }\n setNewTitle('');\n setNewContent('');\n setIsComposing(false);\n void refetch();\n };\n\n const handleLinkExisting = async () => {\n if (!existingDocId) return;\n setActionError(null);\n try {\n await throwOnError(linkDoc({ variables: { docId: existingDocId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be linked.');\n return;\n }\n setExistingDocId('');\n setIsLinking(false);\n void refetch();\n };\n\n const handleUnlink = async (docId: string) => {\n setActionError(null);\n try {\n await throwOnError(unlinkDoc({ variables: { docId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be unlinked.');\n return;\n }\n void refetch();\n };\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle>Linked Docs</CardTitle>\n {!isComposing && !isLinking && (\n <div className=\"flex gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsLinking(true)}>\n <Link2 className=\"h-4 w-4 mr-1\" />\n Link existing doc\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsComposing(true)}>\n <Plus className=\"h-4 w-4 mr-1\" />\n New Doc\n </Button>\n </div>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {isLinking && (\n <div className=\"space-y-2 rounded-md border border-input p-3\">\n <DocSearchSelect\n id=\"task-link-doc\"\n value={existingDocId}\n onChange={setExistingDocId}\n exclude={docs.map((d) => d.id)}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsLinking(false);\n setExistingDocId('');\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" disabled={!existingDocId || linking} onClick={handleLinkExisting}>\n {linking ? 'Linking...' : 'Link'}\n </Button>\n </div>\n </div>\n )}\n {isComposing && (\n <div className=\"space-y-3 rounded-md border border-input p-3\">\n <Input\n placeholder=\"Doc title\"\n value={newTitle}\n onChange={(e) => setNewTitle(e.target.value)}\n />\n <MarkdownEditor\n value={newContent}\n onChange={setNewContent}\n placeholder=\"Write the spec/doc content...\"\n minHeight={140}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsComposing(false);\n setNewTitle('');\n setNewContent('');\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n disabled={creating || !newTitle.trim() || !newContent.trim()}\n onClick={handleCreateAndLink}\n >\n {creating ? 'Creating...' : 'Create & Link'}\n </Button>\n </div>\n </div>\n )}\n\n {loading && <p className=\"text-sm text-muted-foreground\">Loading docs...</p>}\n {error && <p className=\"text-sm text-red-500\">Error: {error.message}</p>}\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {actionError}\n </p>\n )}\n {!loading && !error && docs.length === 0 && !isComposing && !isLinking && (\n <p className=\"text-sm text-muted-foreground\">No docs linked to this work item yet.</p>\n )}\n {docs.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <FileText className=\"h-4 w-4 text-muted-foreground shrink-0\" />\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n className=\"block max-w-full truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/docs/doc-detail-page?docId=${encodeURIComponent(doc.id)}`)\n }\n >\n {doc.title}\n </button>\n <p className=\"text-xs text-muted-foreground\">v{doc.version}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${docStatusColor[doc.status] ?? ''}`}\n >\n {doc.status.toLowerCase()}\n </span>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinking}\n onClick={() => handleUnlink(doc.id)}\n aria-label={`Unlink ${doc.title}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n );\n}\n\nexport function TasksTaskDetailPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from route params.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n // CWF-24: who else is looking at this task right now, and who's mid-comment.\n const presence = usePresence('WORK_ITEM', workItemId || null);\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const task = data?.planmagnetWorkItem;\n if (!task) return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{task.title}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {task.key} &middot; {task.type.toLowerCase()}\n </p>\n <PresenceAvatars others={presence.others} />\n </div>\n <WorkItemHeaderActions workItemId={task.id} title={task.title} />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{task.description ?? 'No description provided.'}</p>\n </CardContent>\n </Card>\n <CustomFieldsPanel\n workItemId={task.id}\n projectId={task.projectId}\n values={task.customFieldValues ?? []}\n />\n <LinkedDocsPanel workItemId={task.id} />\n <TaskTimePanel\n workspaceId={workspaceId}\n workItemId={task.id}\n projectId={task.projectId}\n taskLabel={`${task.key} — ${task.title}`}\n />\n <DependencyPanel\n workspaceId={workspaceId ?? ''}\n workItemId={task.id}\n projectId={task.projectId}\n />\n {/* PDM-24: semantic links (blocks / duplicates / causes / relates to), not scheduling. */}\n <IssueLinksPanel workItemId={task.id} />\n {/* CWF-26: a real group conversation in Conversations, not a comment thread of its own. */}\n <WorkItemDiscussionPanel\n workItemId={task.id}\n workItemKey={task.key}\n workItemTitle={task.title}\n assigneeId={task.assigneeId}\n discussionConversationId={task.discussionConversationId}\n />\n {/* AUT-12: PRs/commits/releases GitHub sync discovered that mention this item's key. */}\n <CodeLinksPanel codeLinks={task.codeLinks ?? []} />\n <WorkItemComments\n workItemId={task.id}\n presence={{\n typingActorIds: presence.typingActorIds,\n notifyTyping: presence.notifyTyping,\n }}\n />\n <WorkItemActivity workItemId={task.id} />\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Status</span>\n <WorkItemStatusSelect\n workItemId={task.id}\n projectId={task.projectId}\n value={task.status}\n />\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Priority</span>\n <WorkItemEnumSelect\n workItemId={task.id}\n field=\"priority\"\n value={task.priority}\n options={WORK_ITEM_PRIORITIES}\n label=\"Priority\"\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Type</span>\n <Badge variant=\"outline\" className=\"capitalize\">\n {task.type.toLowerCase()}\n </Badge>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Milestone</span>\n {/* PDM-22: a milestone of this task's project, or none. */}\n <WorkItemMilestoneSelect\n workItemId={task.id}\n projectId={task.projectId}\n milestoneId={task.milestoneId}\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Story Points</span>\n <span className=\"font-medium\">{task.storyPoints ?? 0}</span>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Assignee</span>\n {/* Members by name (BOFF-7946); choosing one assigns the task (BOFF-7938). */}\n <WorkItemAssigneeSelect workItemId={task.id} assigneeId={task.assigneeId} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reporter</span>\n {/* No PlanMagnet form sets reporterId, so a UI-created task has none; its creator reported it. */}\n <PersonName id={task.reporterId ?? task.createdBy} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span>{task.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Due Date</span>\n <span>{task.dueDate ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{task.createdAt}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{task.updatedAt}</span>\n </div>\n <div>\n <span className=\"text-muted-foreground text-xs\">Labels</span>\n <div className=\"flex gap-1 mt-1 flex-wrap\">\n {(task.labels ?? []).map((l) => (\n <span key={l} className=\"text-xs px-2 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TasksTaskDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WorkItemComments } from '../comments/WorkItemComments';\nimport { WorkItemActivity } from '../comments/WorkItemActivity';\nimport { TaskTimePanel } from '../time/TaskTimePanel';\nimport { PersonName } from '../people/People';\nimport { DocSearchSelect } from '../controls/Pickers';\nimport { CustomFieldsPanel } from '../controls/CustomFieldValuesPanel';\nimport { DependencyPanel } from '../dependency/DependencyPanel';\nimport { IssueLinksPanel } from '../links/IssueLinksPanel';\nimport { WorkItemMilestoneSelect } from '../milestones/MilestoneControls';\nimport {\n WORK_ITEM_PRIORITIES,\n WorkItemStatusSelect,\n WorkItemAssigneeSelect,\n WorkItemEnumSelect,\n WorkItemHeaderActions,\n} from '../controls/WorkItemControls';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { FileText, Link2, Plus, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { usePresence } from '../hooks/usePresence';\nimport { PresenceAvatars } from '../presence/PresenceAvatars';\nimport { WorkItemDiscussionPanel } from '../discussion/WorkItemDiscussionPanel';\nimport { CodeLinksPanel, type WorkItemCodeLink } from '../links/CodeLinksPanel';\nimport { WorkItemProductFeatureChip } from '../product/WorkItemProductFeatureChip';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItemDetail($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n projectId\n title\n description\n type\n status\n priority\n key\n assigneeId\n reporterId\n createdBy\n sprintId\n milestoneId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n updatedAt\n customFieldValues {\n id\n definitionId\n value\n }\n discussionConversationId\n codeLinks {\n id\n kind\n title\n url\n state\n number\n authorLogin\n mergedAt\n ref\n }\n }\n }\n`;\n\nconst GET_WORK_ITEM_DOCS = gql`\n query GetWorkItemDocs($workItemId: ID!) {\n planmagnetWorkItemDocs(workItemId: $workItemId) {\n id\n title\n status\n version\n updatedAt\n }\n }\n`;\n\nconst CREATE_DOC = gql`\n mutation CreateDocForWorkItem($input: PlanmagnetCreateDocInput!) {\n planmagnetCreateDoc(input: $input) {\n id\n }\n }\n`;\n\nconst LINK_DOC_TO_WORK_ITEM = gql`\n mutation LinkDocToWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetLinkDocToWorkItem(docId: $docId, workItemId: $workItemId) {\n id\n }\n }\n`;\n\nconst UNLINK_DOC_FROM_WORK_ITEM = gql`\n mutation UnlinkDocFromWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n createdBy: string;\n sprintId: string | null;\n milestoneId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n customFieldValues: { id: string; definitionId: string; value: unknown }[];\n discussionConversationId: string | null;\n codeLinks: WorkItemCodeLink[];\n}\n\ninterface WorkItemDocNode {\n id: string;\n title: string;\n status: string;\n version: number;\n updatedAt: string;\n}\n\nconst docStatusColor: Record<string, string> = {\n DRAFT: 'bg-gray-100 text-gray-800',\n PUBLISHED: 'bg-green-100 text-green-800',\n ARCHIVED: 'bg-gray-100 text-gray-500',\n};\n\n/**\n * Linked-docs panel (BOFF-4443): create a doc and link it to this work item\n * in one action, view currently-linked docs, and unlink one. Reads/writes go\n * through the real planmagnetWorkItemDocs / planmagnetCreateDoc /\n * planmagnetLinkDocToWorkItem / planmagnetUnlinkDocFromWorkItem GraphQL\n * fields — no fabricated data. Full doc editing (revisions, revert) lives on\n * a dedicated doc detail view; this panel is scoped to linking.\n */\nfunction LinkedDocsPanel({ workItemId }: { workItemId: string }) {\n const navigate = usePlanMagnetNavigate();\n const [isComposing, setIsComposing] = useState(false);\n // BOFF-7961: only \"New Doc\" existed, so a doc written on the Docs page could never be linked to a task.\n const [isLinking, setIsLinking] = useState(false);\n const [existingDocId, setExistingDocId] = useState('');\n const [newTitle, setNewTitle] = useState('');\n const [newContent, setNewContent] = useState('');\n\n const { data, loading, error, refetch } = useQuery<{\n planmagnetWorkItemDocs: WorkItemDocNode[];\n }>(GET_WORK_ITEM_DOCS, {\n variables: { workItemId },\n skip: !workItemId,\n });\n\n const [createDoc, { loading: creating }] = useMutation(CREATE_DOC);\n const [linkDoc, { loading: linking }] = useMutation(LINK_DOC_TO_WORK_ITEM);\n const [unlinkDoc, { loading: unlinking }] = useMutation(UNLINK_DOC_FROM_WORK_ITEM);\n // A refused mutation resolves with { error } (fe-libs errorPolicy 'all');\n // the composer used to clear itself as if the doc had been saved.\n const [actionError, setActionError] = useState<string | null>(null);\n\n const docs = data?.planmagnetWorkItemDocs ?? [];\n\n const handleCreateAndLink = async () => {\n if (!newTitle.trim() || !newContent.trim()) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n createDoc({ variables: { input: { title: newTitle.trim(), content: newContent.trim() } } })\n );\n const docId = result.data?.planmagnetCreateDoc?.id;\n if (docId) {\n await throwOnError(linkDoc({ variables: { docId, workItemId } }));\n }\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be saved.');\n return;\n }\n setNewTitle('');\n setNewContent('');\n setIsComposing(false);\n void refetch();\n };\n\n const handleLinkExisting = async () => {\n if (!existingDocId) return;\n setActionError(null);\n try {\n await throwOnError(linkDoc({ variables: { docId: existingDocId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be linked.');\n return;\n }\n setExistingDocId('');\n setIsLinking(false);\n void refetch();\n };\n\n const handleUnlink = async (docId: string) => {\n setActionError(null);\n try {\n await throwOnError(unlinkDoc({ variables: { docId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be unlinked.');\n return;\n }\n void refetch();\n };\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle>Linked Docs</CardTitle>\n {!isComposing && !isLinking && (\n <div className=\"flex gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsLinking(true)}>\n <Link2 className=\"h-4 w-4 mr-1\" />\n Link existing doc\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsComposing(true)}>\n <Plus className=\"h-4 w-4 mr-1\" />\n New Doc\n </Button>\n </div>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {isLinking && (\n <div className=\"space-y-2 rounded-md border border-input p-3\">\n <DocSearchSelect\n id=\"task-link-doc\"\n value={existingDocId}\n onChange={setExistingDocId}\n exclude={docs.map((d) => d.id)}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsLinking(false);\n setExistingDocId('');\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" disabled={!existingDocId || linking} onClick={handleLinkExisting}>\n {linking ? 'Linking...' : 'Link'}\n </Button>\n </div>\n </div>\n )}\n {isComposing && (\n <div className=\"space-y-3 rounded-md border border-input p-3\">\n <Input\n placeholder=\"Doc title\"\n value={newTitle}\n onChange={(e) => setNewTitle(e.target.value)}\n />\n <MarkdownEditor\n value={newContent}\n onChange={setNewContent}\n placeholder=\"Write the spec/doc content...\"\n minHeight={140}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsComposing(false);\n setNewTitle('');\n setNewContent('');\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n disabled={creating || !newTitle.trim() || !newContent.trim()}\n onClick={handleCreateAndLink}\n >\n {creating ? 'Creating...' : 'Create & Link'}\n </Button>\n </div>\n </div>\n )}\n\n {loading && <p className=\"text-sm text-muted-foreground\">Loading docs...</p>}\n {error && <p className=\"text-sm text-red-500\">Error: {error.message}</p>}\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {actionError}\n </p>\n )}\n {!loading && !error && docs.length === 0 && !isComposing && !isLinking && (\n <p className=\"text-sm text-muted-foreground\">No docs linked to this work item yet.</p>\n )}\n {docs.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <FileText className=\"h-4 w-4 text-muted-foreground shrink-0\" />\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n className=\"block max-w-full truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/docs/doc-detail-page?docId=${encodeURIComponent(doc.id)}`)\n }\n >\n {doc.title}\n </button>\n <p className=\"text-xs text-muted-foreground\">v{doc.version}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${docStatusColor[doc.status] ?? ''}`}\n >\n {doc.status.toLowerCase()}\n </span>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinking}\n onClick={() => handleUnlink(doc.id)}\n aria-label={`Unlink ${doc.title}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n );\n}\n\nexport function TasksTaskDetailPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from route params.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n // CWF-24: who else is looking at this task right now, and who's mid-comment.\n const presence = usePresence('WORK_ITEM', workItemId || null);\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const task = data?.planmagnetWorkItem;\n if (!task) return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{task.title}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {task.key} &middot; {task.type.toLowerCase()}\n </p>\n {/* PRM-01: read-only — an epic's product feature, or a story's through its epic. */}\n <WorkItemProductFeatureChip workItemId={task.id} />\n <PresenceAvatars others={presence.others} />\n </div>\n <WorkItemHeaderActions workItemId={task.id} title={task.title} />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{task.description ?? 'No description provided.'}</p>\n </CardContent>\n </Card>\n <CustomFieldsPanel\n workItemId={task.id}\n projectId={task.projectId}\n values={task.customFieldValues ?? []}\n />\n <LinkedDocsPanel workItemId={task.id} />\n <TaskTimePanel\n workspaceId={workspaceId}\n workItemId={task.id}\n projectId={task.projectId}\n taskLabel={`${task.key} — ${task.title}`}\n />\n <DependencyPanel\n workspaceId={workspaceId ?? ''}\n workItemId={task.id}\n projectId={task.projectId}\n />\n {/* PDM-24: semantic links (blocks / duplicates / causes / relates to), not scheduling. */}\n <IssueLinksPanel workItemId={task.id} />\n {/* CWF-26: a real group conversation in Conversations, not a comment thread of its own. */}\n <WorkItemDiscussionPanel\n workItemId={task.id}\n workItemKey={task.key}\n workItemTitle={task.title}\n assigneeId={task.assigneeId}\n discussionConversationId={task.discussionConversationId}\n />\n {/* AUT-12: PRs/commits/releases GitHub sync discovered that mention this item's key. */}\n <CodeLinksPanel codeLinks={task.codeLinks ?? []} />\n <WorkItemComments\n workItemId={task.id}\n presence={{\n typingActorIds: presence.typingActorIds,\n notifyTyping: presence.notifyTyping,\n }}\n />\n <WorkItemActivity workItemId={task.id} />\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Status</span>\n <WorkItemStatusSelect\n workItemId={task.id}\n projectId={task.projectId}\n value={task.status}\n />\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Priority</span>\n <WorkItemEnumSelect\n workItemId={task.id}\n field=\"priority\"\n value={task.priority}\n options={WORK_ITEM_PRIORITIES}\n label=\"Priority\"\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Type</span>\n <Badge variant=\"outline\" className=\"capitalize\">\n {task.type.toLowerCase()}\n </Badge>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Milestone</span>\n {/* PDM-22: a milestone of this task's project, or none. */}\n <WorkItemMilestoneSelect\n workItemId={task.id}\n projectId={task.projectId}\n milestoneId={task.milestoneId}\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Story Points</span>\n <span className=\"font-medium\">{task.storyPoints ?? 0}</span>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Assignee</span>\n {/* Members by name (BOFF-7946); choosing one assigns the task (BOFF-7938). */}\n <WorkItemAssigneeSelect workItemId={task.id} assigneeId={task.assigneeId} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reporter</span>\n {/* No PlanMagnet form sets reporterId, so a UI-created task has none; its creator reported it. */}\n <PersonName id={task.reporterId ?? task.createdBy} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span>{task.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Due Date</span>\n <span>{task.dueDate ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{task.createdAt}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{task.updatedAt}</span>\n </div>\n <div>\n <span className=\"text-muted-foreground text-xs\">Labels</span>\n <div className=\"flex gap-1 mt-1 flex-wrap\">\n {(task.labels ?? []).map((l) => (\n <span key={l} className=\"text-xs px-2 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface FeatureEpicChild {
2
+ id: string;
3
+ key: string;
4
+ title: string;
5
+ type: string;
6
+ status: string;
7
+ }
8
+ export interface FeatureEpic extends FeatureEpicChild {
9
+ children: FeatureEpicChild[];
10
+ }
11
+ export declare function FeatureEpicsPanel({ featureId, archived, epics, }: {
12
+ featureId: string;
13
+ archived: boolean;
14
+ epics: FeatureEpic[];
15
+ }): import("react/jsx-runtime").JSX.Element;
16
+ export interface FeatureLinkedRequest {
17
+ id: string;
18
+ title: string;
19
+ status: string;
20
+ voteCount: number;
21
+ productId: string | null;
22
+ }
23
+ export declare function FeatureRequestsPanel({ featureId, productId, archived, requests, }: {
24
+ featureId: string;
25
+ productId: string;
26
+ archived: boolean;
27
+ requests: FeatureLinkedRequest[];
28
+ }): import("react/jsx-runtime").JSX.Element;
29
+ //# sourceMappingURL=FeatureLinkPanels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureLinkPanels.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/product/FeatureLinkPanels.tsx"],"names":[],"mappings":"AAgDA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,gBAAgB;IACnD,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAUD,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,QAAQ,EACR,KAAK,GACN,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB,2CAuHA;AAoID,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAeD,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC,2CA0GA"}
@@ -0,0 +1,373 @@
1
+ import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
2
+ import { useTr as n } from "../../shared/hooks/useTr.js";
3
+ import { throwOnError as r } from "../utils/mutationResult.js";
4
+ import { ErrorLine as i } from "../controls/ConfirmButton.js";
5
+ import { enumLabel as a } from "../controls/WorkItemControls.js";
6
+ import { detailPath as o } from "../utils/rowNavigation.js";
7
+ import { LINK_EPIC_TO_PRODUCT_FEATURE as s, LINK_FEATURE_REQUEST_TO_PRODUCT_FEATURE as c, PRODUCT_FEATURE_EPIC_CANDIDATES as l, PRODUCT_FEATURE_QUERIES as u, PRODUCT_FEATURE_REQUEST_CANDIDATES as d, UNLINK_EPIC_FROM_PRODUCT_FEATURE as f, UNLINK_FEATURE_REQUEST_FROM_PRODUCT_FEATURE as p } from "./hierarchyOperations.js";
8
+ import { linkableRequests as m } from "./hierarchy.js";
9
+ import { refusalMessage as h } from "./refusal.js";
10
+ import { useDeferredValue as g, useState as _ } from "react";
11
+ import { Link2 as v, ThumbsUp as y, X as b } from "lucide-react";
12
+ import { useMutation as x, useQuery as S } from "@apollo/client/react";
13
+ import { jsx as C, jsxs as w } from "react/jsx-runtime";
14
+ import { Badge as T, Button as E, Input as D } from "@burdenoff/fe-libs/ui";
15
+ //#region src/planmagnet/product/FeatureLinkPanels.tsx
16
+ var O = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60", k = {
17
+ refetchQueries: u,
18
+ awaitRefetchQueries: !0
19
+ };
20
+ function A(e) {
21
+ return `/tasks/task-detail-page?taskId=${encodeURIComponent(e)}`;
22
+ }
23
+ function j() {
24
+ return n()("planmagnet.productFeatures.errors.forbiddenLinks", "Your role cannot change what this feature is linked to.");
25
+ }
26
+ function M({ featureId: e, archived: o, epics: s }) {
27
+ let c = n(), l = j(), u = t(), [d, p] = x(f, k), [m, g] = _(null), [v, y] = _(null), S = async (t) => {
28
+ y(null), g(t);
29
+ try {
30
+ await r(d({ variables: {
31
+ productFeatureId: e,
32
+ workItemId: t
33
+ } }));
34
+ } catch (e) {
35
+ y(h(e, c("planmagnet.productFeatures.epics.errors.unlink", "The epic could not be unlinked."), l));
36
+ } finally {
37
+ g(null);
38
+ }
39
+ };
40
+ return /* @__PURE__ */ w("div", {
41
+ className: "space-y-3",
42
+ "data-testid": "product-feature-epics",
43
+ children: [
44
+ /* @__PURE__ */ C("p", {
45
+ className: "text-sm text-muted-foreground",
46
+ children: c("planmagnet.productFeatures.epics.subtitle", "Epics that deliver this feature. Stories reach it through their epic; unlinking an epic changes nothing about the epic or its stories.")
47
+ }),
48
+ s.length === 0 ? /* @__PURE__ */ C("p", {
49
+ className: "py-2 text-center text-sm text-muted-foreground",
50
+ children: c("planmagnet.productFeatures.epics.empty", "No epics linked to this feature yet.")
51
+ }) : /* @__PURE__ */ C("ul", {
52
+ className: "space-y-2",
53
+ children: s.map((e) => /* @__PURE__ */ w("li", {
54
+ className: "rounded-md border p-2 text-sm",
55
+ "data-testid": "feature-epic",
56
+ children: [/* @__PURE__ */ w("div", {
57
+ className: "flex flex-wrap items-center justify-between gap-2",
58
+ children: [/* @__PURE__ */ w("button", {
59
+ type: "button",
60
+ className: "min-w-0 truncate text-left font-medium hover:underline",
61
+ onClick: () => u(A(e.id)),
62
+ children: [/* @__PURE__ */ C("span", {
63
+ className: "mr-2 text-muted-foreground",
64
+ children: e.key
65
+ }), e.title]
66
+ }), /* @__PURE__ */ w("div", {
67
+ className: "flex shrink-0 items-center gap-2",
68
+ children: [/* @__PURE__ */ C(T, {
69
+ variant: "outline",
70
+ className: "text-xs",
71
+ children: a(e.status)
72
+ }), /* @__PURE__ */ C(E, {
73
+ size: "sm",
74
+ variant: "ghost",
75
+ "aria-label": c("planmagnet.productFeatures.epics.unlinkNamed", "Unlink {{key}}", { key: e.key }),
76
+ disabled: p.loading,
77
+ onClick: () => void S(e.id),
78
+ "data-testid": "feature-epic-unlink",
79
+ children: m === e.id ? "…" : /* @__PURE__ */ C(b, { className: "h-4 w-4" })
80
+ })]
81
+ })]
82
+ }), e.children.length === 0 ? /* @__PURE__ */ C("p", {
83
+ className: "mt-1 pl-4 text-xs text-muted-foreground",
84
+ children: c("planmagnet.productFeatures.epics.noStories", "No stories under this epic yet.")
85
+ }) : /* @__PURE__ */ C("ul", {
86
+ className: "mt-1 space-y-1 border-l pl-4",
87
+ "data-testid": "feature-epic-stories",
88
+ children: e.children.map((e) => /* @__PURE__ */ w("li", {
89
+ className: "flex flex-wrap items-center justify-between gap-x-2 text-xs",
90
+ children: [/* @__PURE__ */ w("button", {
91
+ type: "button",
92
+ className: "min-w-0 truncate text-left hover:underline",
93
+ onClick: () => u(A(e.id)),
94
+ children: [/* @__PURE__ */ C("span", {
95
+ className: "mr-2 text-muted-foreground",
96
+ children: e.key
97
+ }), e.title]
98
+ }), /* @__PURE__ */ w("span", {
99
+ className: "shrink-0 text-muted-foreground",
100
+ children: [
101
+ a(e.type),
102
+ " · ",
103
+ a(e.status)
104
+ ]
105
+ })]
106
+ }, e.id))
107
+ })]
108
+ }, e.id))
109
+ }),
110
+ v && /* @__PURE__ */ C(i, { message: v }),
111
+ o ? /* @__PURE__ */ C("p", {
112
+ className: "text-xs text-muted-foreground",
113
+ children: c("planmagnet.productFeatures.archivedNoLinks", "This feature is archived — unarchive it to link more.")
114
+ }) : /* @__PURE__ */ C(N, {
115
+ featureId: e,
116
+ linkedIds: s.map((e) => e.id)
117
+ })
118
+ ]
119
+ });
120
+ }
121
+ function N({ featureId: t, linkedIds: a }) {
122
+ let o = n(), c = j(), { workspaceId: u } = e(), [d, f] = _(""), p = g(d.trim()), [m, y] = _(""), [b, T] = _(null), { data: A, loading: M, error: N } = S(l, {
123
+ variables: {
124
+ workspaceId: u ?? "",
125
+ filter: {
126
+ type: "EPIC",
127
+ ...p ? { search: p } : {},
128
+ ...a.length ? { excludeIds: a } : {}
129
+ }
130
+ },
131
+ skip: !u
132
+ }), [P, F] = x(s, k), I = A?.planmagnetWorkItems.items ?? [], L = A?.planmagnetWorkItems.totalCount ?? 0, R = I.find((e) => e.id === m) ?? null, z = async () => {
133
+ if (m) {
134
+ T(null);
135
+ try {
136
+ await r(P({ variables: {
137
+ productFeatureId: t,
138
+ workItemId: m
139
+ } })), y(""), f("");
140
+ } catch (e) {
141
+ T(h(e, o("planmagnet.productFeatures.epics.errors.link", "The epic could not be linked."), c));
142
+ }
143
+ }
144
+ };
145
+ return /* @__PURE__ */ w("div", {
146
+ className: "space-y-2 border-t pt-3",
147
+ "data-testid": "feature-epic-link",
148
+ children: [
149
+ /* @__PURE__ */ C("label", {
150
+ htmlFor: `feature-epic-select-${t}`,
151
+ className: "text-sm font-medium",
152
+ children: o("planmagnet.productFeatures.epics.linkLabel", "Link an epic")
153
+ }),
154
+ /* @__PURE__ */ C(D, {
155
+ "aria-label": o("planmagnet.productFeatures.epics.search", "Search epics"),
156
+ placeholder: o("planmagnet.productFeatures.epics.searchPlaceholder", "Search epics by key or title"),
157
+ value: d,
158
+ onChange: (e) => f(e.target.value)
159
+ }),
160
+ /* @__PURE__ */ w("div", {
161
+ className: "flex items-start gap-2",
162
+ children: [/* @__PURE__ */ w("select", {
163
+ id: `feature-epic-select-${t}`,
164
+ className: O,
165
+ value: m,
166
+ onChange: (e) => y(e.target.value),
167
+ "data-testid": "feature-epic-select",
168
+ children: [/* @__PURE__ */ C("option", {
169
+ value: "",
170
+ children: M && !A ? o("planmagnet.productFeatures.searching", "Searching…") : I.length ? o("planmagnet.productFeatures.epics.choose", "Choose an epic") : o("planmagnet.productFeatures.epics.noMatch", "No epic matches")
171
+ }), I.map((e) => /* @__PURE__ */ w("option", {
172
+ value: e.id,
173
+ children: [
174
+ e.key,
175
+ " — ",
176
+ e.title
177
+ ]
178
+ }, e.id))]
179
+ }), /* @__PURE__ */ w(E, {
180
+ size: "sm",
181
+ disabled: !m || F.loading,
182
+ onClick: () => void z(),
183
+ "data-testid": "feature-epic-link-confirm",
184
+ children: [/* @__PURE__ */ C(v, { className: "mr-1 h-4 w-4" }), o("planmagnet.productFeatures.epics.link", "Link")]
185
+ })]
186
+ }),
187
+ R?.productFeature && /* @__PURE__ */ C("p", {
188
+ className: "text-xs text-muted-foreground",
189
+ children: o("planmagnet.productFeatures.epics.moves", "{{key}} is linked to \"{{feature}}\" — an epic belongs to one feature, so linking moves it here.", {
190
+ key: R.key,
191
+ feature: R.productFeature.name
192
+ })
193
+ }),
194
+ N && /* @__PURE__ */ C(i, { message: o("planmagnet.productFeatures.epics.errors.load", "Epics could not be searched: {{message}}", { message: N.message }) }),
195
+ L > I.length && /* @__PURE__ */ C("p", {
196
+ className: "text-xs text-muted-foreground",
197
+ children: o("planmagnet.productFeatures.narrow", "Showing {{shown}} of {{total}} — type to narrow the list.", {
198
+ shown: I.length,
199
+ total: L
200
+ })
201
+ }),
202
+ b && /* @__PURE__ */ C(i, { message: b })
203
+ ]
204
+ });
205
+ }
206
+ function P({ featureId: e, productId: s, archived: c, requests: l }) {
207
+ let u = n(), d = j(), f = t(), [m, g] = x(p, k), [v, S] = _(null), [D, O] = _(null), A = async (t) => {
208
+ O(null), S(t);
209
+ try {
210
+ await r(m({ variables: {
211
+ featureRequestId: t,
212
+ productFeatureId: e
213
+ } }));
214
+ } catch (e) {
215
+ O(h(e, u("planmagnet.productFeatures.requests.errors.unlink", "The feature request could not be unlinked."), d));
216
+ } finally {
217
+ S(null);
218
+ }
219
+ };
220
+ return /* @__PURE__ */ w("div", {
221
+ className: "space-y-3",
222
+ "data-testid": "product-feature-requests",
223
+ children: [
224
+ /* @__PURE__ */ C("p", {
225
+ className: "text-sm text-muted-foreground",
226
+ children: u("planmagnet.productFeatures.requests.subtitle", "Customer and user feedback this feature answers. A request stays a request — linking never converts or changes it.")
227
+ }),
228
+ l.length === 0 ? /* @__PURE__ */ C("p", {
229
+ className: "py-2 text-center text-sm text-muted-foreground",
230
+ children: u("planmagnet.productFeatures.requests.empty", "No feature requests linked to this feature yet.")
231
+ }) : /* @__PURE__ */ C("ul", {
232
+ className: "space-y-1",
233
+ children: l.map((e) => /* @__PURE__ */ w("li", {
234
+ className: "flex flex-wrap items-center justify-between gap-2 rounded-md border p-2 text-sm",
235
+ "data-testid": "feature-request-row",
236
+ children: [/* @__PURE__ */ C("button", {
237
+ type: "button",
238
+ className: "min-w-0 truncate text-left hover:underline",
239
+ onClick: () => f(o("/products/feature-detail-page", e.id)),
240
+ children: e.title
241
+ }), /* @__PURE__ */ w("div", {
242
+ className: "flex shrink-0 items-center gap-2",
243
+ children: [
244
+ /* @__PURE__ */ w("span", {
245
+ className: "flex items-center gap-1 text-xs text-muted-foreground",
246
+ children: [/* @__PURE__ */ C(y, { className: "h-3 w-3" }), e.voteCount]
247
+ }),
248
+ /* @__PURE__ */ C(T, {
249
+ variant: "secondary",
250
+ className: "text-xs",
251
+ children: a(e.status)
252
+ }),
253
+ /* @__PURE__ */ C(E, {
254
+ size: "sm",
255
+ variant: "ghost",
256
+ "aria-label": u("planmagnet.productFeatures.requests.unlinkNamed", "Unlink {{title}}", { title: e.title }),
257
+ disabled: g.loading,
258
+ onClick: () => void A(e.id),
259
+ "data-testid": "feature-request-unlink",
260
+ children: v === e.id ? "…" : /* @__PURE__ */ C(b, { className: "h-4 w-4" })
261
+ })
262
+ ]
263
+ })]
264
+ }, e.id))
265
+ }),
266
+ D && /* @__PURE__ */ C(i, { message: D }),
267
+ c ? /* @__PURE__ */ C("p", {
268
+ className: "text-xs text-muted-foreground",
269
+ children: u("planmagnet.productFeatures.archivedNoLinks", "This feature is archived — unarchive it to link more.")
270
+ }) : /* @__PURE__ */ C(F, {
271
+ featureId: e,
272
+ productId: s,
273
+ linkedIds: l.map((e) => e.id)
274
+ })
275
+ ]
276
+ });
277
+ }
278
+ function F({ featureId: t, productId: a, linkedIds: o }) {
279
+ let s = n(), l = j(), { workspaceId: u } = e(), [f, p] = _(""), y = g(f.trim()), [b, T] = _(""), [A, M] = _(null), { data: N, loading: P, error: F } = S(d, {
280
+ variables: {
281
+ workspaceId: u ?? "",
282
+ sameProduct: {
283
+ productId: a,
284
+ ...y ? { search: y } : {}
285
+ },
286
+ anyProduct: y ? { search: y } : void 0
287
+ },
288
+ skip: !u
289
+ }), [I, L] = x(c, k), R = m(N?.sameProduct.items ?? [], (N?.anyProduct.items ?? []).filter((e) => e.productId === null), a, o), z = R.find((e) => e.id === b) ?? null, B = !!N && (N.sameProduct.totalCount > N.sameProduct.items.length || N.anyProduct.totalCount > N.anyProduct.items.length), V = async () => {
290
+ if (b) {
291
+ M(null);
292
+ try {
293
+ await r(I({ variables: {
294
+ featureRequestId: b,
295
+ productFeatureId: t
296
+ } })), T(""), p("");
297
+ } catch (e) {
298
+ M(h(e, s("planmagnet.productFeatures.requests.errors.link", "The feature request could not be linked."), l));
299
+ }
300
+ }
301
+ }, H = R.filter((e) => e.productId === a), U = R.filter((e) => e.productId === null);
302
+ return /* @__PURE__ */ w("div", {
303
+ className: "space-y-2 border-t pt-3",
304
+ "data-testid": "feature-request-link",
305
+ children: [
306
+ /* @__PURE__ */ C("label", {
307
+ htmlFor: `feature-request-select-${t}`,
308
+ className: "text-sm font-medium",
309
+ children: s("planmagnet.productFeatures.requests.linkLabel", "Link a feature request")
310
+ }),
311
+ /* @__PURE__ */ C(D, {
312
+ "aria-label": s("planmagnet.productFeatures.requests.search", "Search feature requests"),
313
+ placeholder: s("planmagnet.productFeatures.requests.searchPlaceholder", "Search feature requests by title"),
314
+ value: f,
315
+ onChange: (e) => p(e.target.value)
316
+ }),
317
+ /* @__PURE__ */ w("div", {
318
+ className: "flex items-start gap-2",
319
+ children: [/* @__PURE__ */ w("select", {
320
+ id: `feature-request-select-${t}`,
321
+ className: O,
322
+ value: b,
323
+ onChange: (e) => T(e.target.value),
324
+ "data-testid": "feature-request-select",
325
+ children: [
326
+ /* @__PURE__ */ C("option", {
327
+ value: "",
328
+ children: P && !N ? s("planmagnet.productFeatures.searching", "Searching…") : R.length ? s("planmagnet.productFeatures.requests.choose", "Choose a feature request") : s("planmagnet.productFeatures.requests.noMatch", "No feature request matches")
329
+ }),
330
+ H.length > 0 && /* @__PURE__ */ C("optgroup", {
331
+ label: s("planmagnet.productFeatures.requests.groupOwn", "From this product"),
332
+ children: H.map((e) => /* @__PURE__ */ C("option", {
333
+ value: e.id,
334
+ children: e.title
335
+ }, e.id))
336
+ }),
337
+ U.length > 0 && /* @__PURE__ */ C("optgroup", {
338
+ label: s("planmagnet.productFeatures.requests.groupNone", "Without a product"),
339
+ children: U.map((e) => /* @__PURE__ */ C("option", {
340
+ value: e.id,
341
+ children: e.title
342
+ }, e.id))
343
+ })
344
+ ]
345
+ }), /* @__PURE__ */ w(E, {
346
+ size: "sm",
347
+ disabled: !b || L.loading,
348
+ onClick: () => void V(),
349
+ "data-testid": "feature-request-link-confirm",
350
+ children: [/* @__PURE__ */ C(v, { className: "mr-1 h-4 w-4" }), s("planmagnet.productFeatures.requests.link", "Link")]
351
+ })]
352
+ }),
353
+ z?.productFeature && /* @__PURE__ */ C("p", {
354
+ className: "text-xs text-muted-foreground",
355
+ children: s("planmagnet.productFeatures.requests.moves", "This request points at \"{{feature}}\" — a request points at one feature, so linking moves it here.", { feature: z.productFeature.name })
356
+ }),
357
+ z && z.productId === null && /* @__PURE__ */ C("p", {
358
+ className: "text-xs text-muted-foreground",
359
+ children: s("planmagnet.productFeatures.requests.noProduct", "This request has no product. Linking does not set one.")
360
+ }),
361
+ F && /* @__PURE__ */ C(i, { message: s("planmagnet.productFeatures.requests.errors.load", "Feature requests could not be searched: {{message}}", { message: F.message }) }),
362
+ B && /* @__PURE__ */ C("p", {
363
+ className: "text-xs text-muted-foreground",
364
+ children: s("planmagnet.productFeatures.requests.narrow", "Only the first matches are listed — type to narrow the list.")
365
+ }),
366
+ A && /* @__PURE__ */ C(i, { message: A })
367
+ ]
368
+ });
369
+ }
370
+ //#endregion
371
+ export { M as FeatureEpicsPanel, P as FeatureRequestsPanel };
372
+
373
+ //# sourceMappingURL=FeatureLinkPanels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureLinkPanels.js","names":[],"sources":["../../../src/planmagnet/product/FeatureLinkPanels.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Badge, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { Link2, ThumbsUp, X } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { throwOnError } from '../utils/mutationResult';\nimport { detailPath } from '../utils/rowNavigation';\nimport { linkableRequests } from './hierarchy';\nimport {\n LINK_EPIC_TO_PRODUCT_FEATURE,\n LINK_FEATURE_REQUEST_TO_PRODUCT_FEATURE,\n PRODUCT_FEATURE_EPIC_CANDIDATES,\n PRODUCT_FEATURE_QUERIES,\n PRODUCT_FEATURE_REQUEST_CANDIDATES,\n UNLINK_EPIC_FROM_PRODUCT_FEATURE,\n UNLINK_FEATURE_REQUEST_FROM_PRODUCT_FEATURE,\n} from './hierarchyOperations';\nimport { refusalMessage } from './refusal';\n\n/**\n * The feature detail page's two link panels (PRM-01): the EPIC work items that deliver the feature (each with the\n * stories under it, through the existing parent/child relation) and the feature requests it answers. Linking and\n * unlinking only ever writes the link — the epic, its stories and the request are never changed — and a link the\n * service refuses (not an epic, another product's request, an archived feature) shows the service's own reason.\n */\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst writeOptions = { refetchQueries: PRODUCT_FEATURE_QUERIES, awaitRefetchQueries: true };\n\nfunction taskPath(id: string) {\n return `/tasks/task-detail-page?taskId=${encodeURIComponent(id)}`;\n}\n\nfunction useForbiddenText() {\n const tr = useTr();\n return tr(\n 'planmagnet.productFeatures.errors.forbiddenLinks',\n 'Your role cannot change what this feature is linked to.'\n );\n}\n\n// ─── Epics ──────────────────────────────────────────────────────────────────\n\nexport interface FeatureEpicChild {\n id: string;\n key: string;\n title: string;\n type: string;\n status: string;\n}\n\nexport interface FeatureEpic extends FeatureEpicChild {\n children: FeatureEpicChild[];\n}\n\ninterface EpicCandidate {\n id: string;\n key: string;\n title: string;\n type: string;\n productFeature: { id: string; name: string } | null;\n}\n\nexport function FeatureEpicsPanel({\n featureId,\n archived,\n epics,\n}: {\n featureId: string;\n archived: boolean;\n epics: FeatureEpic[];\n}) {\n const tr = useTr();\n const forbidden = useForbiddenText();\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_EPIC_FROM_PRODUCT_FEATURE, writeOptions);\n const [removingId, setRemovingId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const onUnlink = async (workItemId: string) => {\n setError(null);\n setRemovingId(workItemId);\n try {\n await throwOnError(unlink({ variables: { productFeatureId: featureId, workItemId } }));\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr('planmagnet.productFeatures.epics.errors.unlink', 'The epic could not be unlinked.'),\n forbidden\n )\n );\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-feature-epics\">\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.epics.subtitle',\n 'Epics that deliver this feature. Stories reach it through their epic; unlinking an epic changes nothing about the epic or its stories.'\n )}\n </p>\n {epics.length === 0 ? (\n <p className=\"py-2 text-center text-sm text-muted-foreground\">\n {tr('planmagnet.productFeatures.epics.empty', 'No epics linked to this feature yet.')}\n </p>\n ) : (\n <ul className=\"space-y-2\">\n {epics.map((epic) => (\n <li key={epic.id} className=\"rounded-md border p-2 text-sm\" data-testid=\"feature-epic\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left font-medium hover:underline\"\n onClick={() => navigate(taskPath(epic.id))}\n >\n <span className=\"mr-2 text-muted-foreground\">{epic.key}</span>\n {epic.title}\n </button>\n <div className=\"flex shrink-0 items-center gap-2\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {enumLabel(epic.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={tr(\n 'planmagnet.productFeatures.epics.unlinkNamed',\n 'Unlink {{key}}',\n {\n key: epic.key,\n }\n )}\n disabled={unlinkState.loading}\n onClick={() => void onUnlink(epic.id)}\n data-testid=\"feature-epic-unlink\"\n >\n {removingId === epic.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </div>\n {epic.children.length === 0 ? (\n <p className=\"mt-1 pl-4 text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.epics.noStories',\n 'No stories under this epic yet.'\n )}\n </p>\n ) : (\n <ul className=\"mt-1 space-y-1 border-l pl-4\" data-testid=\"feature-epic-stories\">\n {epic.children.map((child) => (\n <li\n key={child.id}\n className=\"flex flex-wrap items-center justify-between gap-x-2 text-xs\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(taskPath(child.id))}\n >\n <span className=\"mr-2 text-muted-foreground\">{child.key}</span>\n {child.title}\n </button>\n <span className=\"shrink-0 text-muted-foreground\">\n {enumLabel(child.type)} · {enumLabel(child.status)}\n </span>\n </li>\n ))}\n </ul>\n )}\n </li>\n ))}\n </ul>\n )}\n {error && <ErrorLine message={error} />}\n {archived ? (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.archivedNoLinks',\n 'This feature is archived — unarchive it to link more.'\n )}\n </p>\n ) : (\n <LinkEpicControl featureId={featureId} linkedIds={epics.map((e) => e.id)} />\n )}\n </div>\n );\n}\n\n/** Search EPIC work items only (the service refuses any other type) and link one. */\nfunction LinkEpicControl({ featureId, linkedIds }: { featureId: string; linkedIds: string[] }) {\n const tr = useTr();\n const forbidden = useForbiddenText();\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const [selectedId, setSelectedId] = useState('');\n const [error, setError] = useState<string | null>(null);\n const {\n data,\n loading,\n error: loadError,\n } = useQuery<{\n planmagnetWorkItems: { items: EpicCandidate[]; totalCount: number };\n }>(PRODUCT_FEATURE_EPIC_CANDIDATES, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: {\n type: 'EPIC',\n ...(deferred ? { search: deferred } : {}),\n ...(linkedIds.length ? { excludeIds: linkedIds } : {}),\n },\n },\n skip: !workspaceId,\n });\n const [link, linkState] = useMutation(LINK_EPIC_TO_PRODUCT_FEATURE, writeOptions);\n const candidates = data?.planmagnetWorkItems.items ?? [];\n const total = data?.planmagnetWorkItems.totalCount ?? 0;\n const selected = candidates.find((c) => c.id === selectedId) ?? null;\n\n const onLink = async () => {\n if (!selectedId) return;\n setError(null);\n try {\n await throwOnError(\n link({ variables: { productFeatureId: featureId, workItemId: selectedId } })\n );\n setSelectedId('');\n setSearch('');\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr('planmagnet.productFeatures.epics.errors.link', 'The epic could not be linked.'),\n forbidden\n )\n );\n }\n };\n\n return (\n <div className=\"space-y-2 border-t pt-3\" data-testid=\"feature-epic-link\">\n <label htmlFor={`feature-epic-select-${featureId}`} className=\"text-sm font-medium\">\n {tr('planmagnet.productFeatures.epics.linkLabel', 'Link an epic')}\n </label>\n <Input\n aria-label={tr('planmagnet.productFeatures.epics.search', 'Search epics')}\n placeholder={tr(\n 'planmagnet.productFeatures.epics.searchPlaceholder',\n 'Search epics by key or title'\n )}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <div className=\"flex items-start gap-2\">\n <select\n id={`feature-epic-select-${featureId}`}\n className={selectClass}\n value={selectedId}\n onChange={(e) => setSelectedId(e.target.value)}\n data-testid=\"feature-epic-select\"\n >\n <option value=\"\">\n {loading && !data\n ? tr('planmagnet.productFeatures.searching', 'Searching…')\n : candidates.length\n ? tr('planmagnet.productFeatures.epics.choose', 'Choose an epic')\n : tr('planmagnet.productFeatures.epics.noMatch', 'No epic matches')}\n </option>\n {candidates.map((c) => (\n <option key={c.id} value={c.id}>\n {c.key} — {c.title}\n </option>\n ))}\n </select>\n <Button\n size=\"sm\"\n disabled={!selectedId || linkState.loading}\n onClick={() => void onLink()}\n data-testid=\"feature-epic-link-confirm\"\n >\n <Link2 className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productFeatures.epics.link', 'Link')}\n </Button>\n </div>\n {selected?.productFeature && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.epics.moves',\n '{{key}} is linked to \"{{feature}}\" — an epic belongs to one feature, so linking moves it here.',\n { key: selected.key, feature: selected.productFeature.name }\n )}\n </p>\n )}\n {loadError && (\n <ErrorLine\n message={tr(\n 'planmagnet.productFeatures.epics.errors.load',\n 'Epics could not be searched: {{message}}',\n { message: loadError.message }\n )}\n />\n )}\n {total > candidates.length && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.narrow',\n 'Showing {{shown}} of {{total}} — type to narrow the list.',\n { shown: candidates.length, total }\n )}\n </p>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n// ─── Feature requests ───────────────────────────────────────────────────────\n\nexport interface FeatureLinkedRequest {\n id: string;\n title: string;\n status: string;\n voteCount: number;\n productId: string | null;\n}\n\ninterface RequestCandidate {\n id: string;\n title: string;\n status: string;\n productId: string | null;\n productFeature: { id: string; name: string } | null;\n}\n\ninterface RequestCandidateList {\n items: RequestCandidate[];\n totalCount: number;\n}\n\nexport function FeatureRequestsPanel({\n featureId,\n productId,\n archived,\n requests,\n}: {\n featureId: string;\n productId: string;\n archived: boolean;\n requests: FeatureLinkedRequest[];\n}) {\n const tr = useTr();\n const forbidden = useForbiddenText();\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(\n UNLINK_FEATURE_REQUEST_FROM_PRODUCT_FEATURE,\n writeOptions\n );\n const [removingId, setRemovingId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const onUnlink = async (featureRequestId: string) => {\n setError(null);\n setRemovingId(featureRequestId);\n try {\n await throwOnError(unlink({ variables: { featureRequestId, productFeatureId: featureId } }));\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr(\n 'planmagnet.productFeatures.requests.errors.unlink',\n 'The feature request could not be unlinked.'\n ),\n forbidden\n )\n );\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-feature-requests\">\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requests.subtitle',\n 'Customer and user feedback this feature answers. A request stays a request — linking never converts or changes it.'\n )}\n </p>\n {requests.length === 0 ? (\n <p className=\"py-2 text-center text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requests.empty',\n 'No feature requests linked to this feature yet.'\n )}\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {requests.map((request) => (\n <li\n key={request.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-md border p-2 text-sm\"\n data-testid=\"feature-request-row\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/products/feature-detail-page', request.id))}\n >\n {request.title}\n </button>\n <div className=\"flex shrink-0 items-center gap-2\">\n <span className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n <ThumbsUp className=\"h-3 w-3\" />\n {request.voteCount}\n </span>\n <Badge variant=\"secondary\" className=\"text-xs\">\n {enumLabel(request.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={tr(\n 'planmagnet.productFeatures.requests.unlinkNamed',\n 'Unlink {{title}}',\n { title: request.title }\n )}\n disabled={unlinkState.loading}\n onClick={() => void onUnlink(request.id)}\n data-testid=\"feature-request-unlink\"\n >\n {removingId === request.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </li>\n ))}\n </ul>\n )}\n {error && <ErrorLine message={error} />}\n {archived ? (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.archivedNoLinks',\n 'This feature is archived — unarchive it to link more.'\n )}\n </p>\n ) : (\n <LinkRequestControl\n featureId={featureId}\n productId={productId}\n linkedIds={requests.map((r) => r.id)}\n />\n )}\n </div>\n );\n}\n\n/**\n * Search, then link one request. Offered: this product's requests, then requests with no product — the two kinds\n * the service accepts. A request of another product is never offered (the service would refuse it: link it to a\n * feature of its own product instead), and if the service still refuses a link — say the request moved product\n * meanwhile — its reason is shown as is.\n */\nfunction LinkRequestControl({\n featureId,\n productId,\n linkedIds,\n}: {\n featureId: string;\n productId: string;\n linkedIds: string[];\n}) {\n const tr = useTr();\n const forbidden = useForbiddenText();\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const [selectedId, setSelectedId] = useState('');\n const [error, setError] = useState<string | null>(null);\n const {\n data,\n loading,\n error: loadError,\n } = useQuery<{\n sameProduct: RequestCandidateList;\n anyProduct: RequestCandidateList;\n }>(PRODUCT_FEATURE_REQUEST_CANDIDATES, {\n variables: {\n workspaceId: workspaceId ?? '',\n sameProduct: { productId, ...(deferred ? { search: deferred } : {}) },\n anyProduct: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const [link, linkState] = useMutation(LINK_FEATURE_REQUEST_TO_PRODUCT_FEATURE, writeOptions);\n const sameProduct = data?.sameProduct.items ?? [];\n // The service has no \"no product\" filter: requests without one come from the workspace-wide search, so\n // only that page is checked — and the note below says when there is more to narrow down.\n const unassigned = (data?.anyProduct.items ?? []).filter((r) => r.productId === null);\n const candidates = linkableRequests(sameProduct, unassigned, productId, linkedIds);\n const selected = candidates.find((c) => c.id === selectedId) ?? null;\n const truncated =\n !!data &&\n (data.sameProduct.totalCount > data.sameProduct.items.length ||\n data.anyProduct.totalCount > data.anyProduct.items.length);\n\n const onLink = async () => {\n if (!selectedId) return;\n setError(null);\n try {\n await throwOnError(\n link({ variables: { featureRequestId: selectedId, productFeatureId: featureId } })\n );\n setSelectedId('');\n setSearch('');\n } catch (e) {\n setError(\n refusalMessage(\n e,\n tr(\n 'planmagnet.productFeatures.requests.errors.link',\n 'The feature request could not be linked.'\n ),\n forbidden\n )\n );\n }\n };\n\n const own = candidates.filter((c) => c.productId === productId);\n const none = candidates.filter((c) => c.productId === null);\n\n return (\n <div className=\"space-y-2 border-t pt-3\" data-testid=\"feature-request-link\">\n <label htmlFor={`feature-request-select-${featureId}`} className=\"text-sm font-medium\">\n {tr('planmagnet.productFeatures.requests.linkLabel', 'Link a feature request')}\n </label>\n <Input\n aria-label={tr('planmagnet.productFeatures.requests.search', 'Search feature requests')}\n placeholder={tr(\n 'planmagnet.productFeatures.requests.searchPlaceholder',\n 'Search feature requests by title'\n )}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <div className=\"flex items-start gap-2\">\n <select\n id={`feature-request-select-${featureId}`}\n className={selectClass}\n value={selectedId}\n onChange={(e) => setSelectedId(e.target.value)}\n data-testid=\"feature-request-select\"\n >\n <option value=\"\">\n {loading && !data\n ? tr('planmagnet.productFeatures.searching', 'Searching…')\n : candidates.length\n ? tr('planmagnet.productFeatures.requests.choose', 'Choose a feature request')\n : tr('planmagnet.productFeatures.requests.noMatch', 'No feature request matches')}\n </option>\n {own.length > 0 && (\n <optgroup\n label={tr('planmagnet.productFeatures.requests.groupOwn', 'From this product')}\n >\n {own.map((c) => (\n <option key={c.id} value={c.id}>\n {c.title}\n </option>\n ))}\n </optgroup>\n )}\n {none.length > 0 && (\n <optgroup\n label={tr('planmagnet.productFeatures.requests.groupNone', 'Without a product')}\n >\n {none.map((c) => (\n <option key={c.id} value={c.id}>\n {c.title}\n </option>\n ))}\n </optgroup>\n )}\n </select>\n <Button\n size=\"sm\"\n disabled={!selectedId || linkState.loading}\n onClick={() => void onLink()}\n data-testid=\"feature-request-link-confirm\"\n >\n <Link2 className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.productFeatures.requests.link', 'Link')}\n </Button>\n </div>\n {selected?.productFeature && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requests.moves',\n 'This request points at \"{{feature}}\" — a request points at one feature, so linking moves it here.',\n { feature: selected.productFeature.name }\n )}\n </p>\n )}\n {selected && selected.productId === null && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requests.noProduct',\n 'This request has no product. Linking does not set one.'\n )}\n </p>\n )}\n {loadError && (\n <ErrorLine\n message={tr(\n 'planmagnet.productFeatures.requests.errors.load',\n 'Feature requests could not be searched: {{message}}',\n { message: loadError.message }\n )}\n />\n )}\n {truncated && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.productFeatures.requests.narrow',\n 'Only the first matches are listed — type to narrow the list.'\n )}\n </p>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -76,7 +76,7 @@ function L({ productId: e, name: n, archivedAt: s }) {
76
76
  variant: "outline",
77
77
  icon: /* @__PURE__ */ x(h, { className: "mr-1 h-4 w-4" }),
78
78
  title: `Delete ${n}?`,
79
- description: "Every linked roadmap, release and feature request stays intact — only its link to this product is cleared. This cannot be undone here.",
79
+ description: "Its features are deleted with it. Every linked roadmap, release, feature request and work item stays intact — only its link to this product or to its features is cleared — and removing its teams changes no one's access. This cannot be undone here.",
80
80
  confirmLabel: "Delete product",
81
81
  destructive: !0,
82
82
  disabled: p.loading,