@burdenoff/microfe-planmagnet 2026.930.23 → 2026.930.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/index.js +6 -5
  2. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  3. package/dist/planmagnet/PlanMagnetRoot.js +154 -147
  4. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  6. package/dist/planmagnet/PlanMagnetRoutes.js +66 -62
  7. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  8. package/dist/planmagnet/comments/CommentThread.d.ts +8 -1
  9. package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
  10. package/dist/planmagnet/comments/CommentThread.js +188 -181
  11. package/dist/planmagnet/comments/CommentThread.js.map +1 -1
  12. package/dist/planmagnet/comments/TargetComments.d.ts +10 -2
  13. package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
  14. package/dist/planmagnet/comments/TargetComments.js +10 -8
  15. package/dist/planmagnet/comments/TargetComments.js.map +1 -1
  16. package/dist/planmagnet/comments/WorkItemComments.d.ts +7 -1
  17. package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
  18. package/dist/planmagnet/comments/WorkItemComments.js +5 -4
  19. package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
  20. package/dist/planmagnet/controls/FeatureControls.d.ts +11 -0
  21. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  22. package/dist/planmagnet/controls/FeatureControls.js +305 -135
  23. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  24. package/dist/planmagnet/controls/Pickers.d.ts +18 -0
  25. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  26. package/dist/planmagnet/controls/Pickers.js +78 -1
  27. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  28. package/dist/planmagnet/controls/PrioritizationControls.d.ts +72 -0
  29. package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -0
  30. package/dist/planmagnet/controls/PrioritizationControls.js +246 -0
  31. package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -0
  32. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts +8 -0
  33. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts.map +1 -0
  34. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js +217 -0
  35. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js.map +1 -0
  36. package/dist/planmagnet/docs/DocDiffView.d.ts +26 -0
  37. package/dist/planmagnet/docs/DocDiffView.d.ts.map +1 -0
  38. package/dist/planmagnet/docs/DocDiffView.js +52 -0
  39. package/dist/planmagnet/docs/DocDiffView.js.map +1 -0
  40. package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts +13 -0
  41. package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts.map +1 -0
  42. package/dist/planmagnet/docs/DocSuggestionsPanel.js +308 -0
  43. package/dist/planmagnet/docs/DocSuggestionsPanel.js.map +1 -0
  44. package/dist/planmagnet/docs/DocVersionCompare.d.ts +7 -0
  45. package/dist/planmagnet/docs/DocVersionCompare.d.ts.map +1 -0
  46. package/dist/planmagnet/docs/DocVersionCompare.js +105 -0
  47. package/dist/planmagnet/docs/DocVersionCompare.js.map +1 -0
  48. package/dist/planmagnet/hooks/usePresence.d.ts +39 -0
  49. package/dist/planmagnet/hooks/usePresence.d.ts.map +1 -0
  50. package/dist/planmagnet/hooks/usePresence.js +146 -0
  51. package/dist/planmagnet/hooks/usePresence.js.map +1 -0
  52. package/dist/planmagnet/index.d.ts +1 -0
  53. package/dist/planmagnet/index.d.ts.map +1 -1
  54. package/dist/planmagnet/index.js +1 -0
  55. package/dist/planmagnet/links/CodeLinksPanel.d.ts +22 -0
  56. package/dist/planmagnet/links/CodeLinksPanel.d.ts.map +1 -0
  57. package/dist/planmagnet/links/CodeLinksPanel.js +94 -0
  58. package/dist/planmagnet/links/CodeLinksPanel.js.map +1 -0
  59. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  60. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +488 -217
  61. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  62. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  63. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +131 -125
  64. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  65. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  66. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +183 -99
  67. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  68. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  69. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +113 -97
  70. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  71. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts +2 -0
  72. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts.map +1 -0
  73. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js +363 -0
  74. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js.map +1 -0
  75. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  76. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +96 -79
  77. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  78. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  79. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +144 -128
  80. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  81. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  82. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +205 -196
  83. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  84. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  85. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +194 -160
  86. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  87. package/dist/planmagnet/presence/PresenceAvatars.d.ts +16 -0
  88. package/dist/planmagnet/presence/PresenceAvatars.d.ts.map +1 -0
  89. package/dist/planmagnet/presence/PresenceAvatars.js +56 -0
  90. package/dist/planmagnet/presence/PresenceAvatars.js.map +1 -0
  91. package/dist/planmagnet/product/ProductControls.d.ts +33 -0
  92. package/dist/planmagnet/product/ProductControls.d.ts.map +1 -1
  93. package/dist/planmagnet/product/ProductControls.js +593 -268
  94. package/dist/planmagnet/product/ProductControls.js.map +1 -1
  95. package/dist/planmagnet/product/routes.d.ts +10 -0
  96. package/dist/planmagnet/product/routes.d.ts.map +1 -0
  97. package/dist/planmagnet/product/routes.js +6 -0
  98. package/dist/planmagnet/product/routes.js.map +1 -0
  99. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts +10 -0
  100. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
  101. package/dist/planmagnet/roadmaps/RoadmapControls.js +235 -184
  102. package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
  103. package/dist/planmagnet/utils/graphqlErrors.d.ts +6 -0
  104. package/dist/planmagnet/utils/graphqlErrors.d.ts.map +1 -1
  105. package/dist/planmagnet/utils/graphqlErrors.js +5 -2
  106. package/dist/planmagnet/utils/graphqlErrors.js.map +1 -1
  107. package/dist/planmagnet/utils/lineDiff.d.ts +21 -0
  108. package/dist/planmagnet/utils/lineDiff.d.ts.map +1 -0
  109. package/dist/planmagnet/utils/lineDiff.js +36 -0
  110. package/dist/planmagnet/utils/lineDiff.js.map +1 -0
  111. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CommentThread.js","names":[],"sources":["../../../src/planmagnet/comments/CommentThread.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { DocumentNode } from 'graphql';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, ChevronDown, ChevronRight, Reply, RotateCcw, Trash2 } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName, usePersonProfile } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { MentionComposer, MentionText } from './MentionComposer';\n\n/**\n * The comment thread engine shared by every commentable surface in this MFE: a work item (CWF-02, via\n * `WorkItemComments.tsx`) and a project/doc (CWF-12, via `TargetComments.tsx`). Modeled on\n * `comments/RoadmapComments.tsx`'s list/add/author-only-edit/delete structure, extended with:\n *\n * - Threading (CWF-02): `PlanmagnetWorkItemComment`/`PlanmagnetComment.replies` are ONE level deep by contract —\n * the server reparents \"a reply to a reply\" onto ITS OWN top-level parent. So a Reply action lives on both a\n * top-level comment and on one of its replies, but both always post with `parentId = <the top-level id>`\n * (`replyTargetId` below), matching what the server would do anyway — never a reply's own id.\n * - Resolution (CWF-14): a top-level-only OPEN/RESOLVED toggle, not restricted to the comment's author. A\n * resolved thread collapses to a one-line summary that can be expanded back out.\n * - Mentions (CWF-13): every composer here (add / reply / edit) is a `MentionComposer`; every rendered body is\n * `MentionText`, so a `@[Name](actorId)` token always shows as a highlighted name, never raw markup.\n * - A caller-agnostic core: `query`/`add`/`edit`/`remove`/`resolve` are passed in per surface (different\n * operation names, same field shapes — see `graphql/wspace.graphql`'s `PlanmagnetWorkItemComment` vs\n * `PlanmagnetComment` doc comments: \"everything here means exactly what it means on\" the other type).\n *\n * The status filter (Open/Resolved/All) is applied CLIENT-SIDE over the loaded page: none of\n * `planmagnetWorkItemComments`/`planmagnetProjectComments`/`planmagnetDocComments` take a status argument, only\n * `pagination` — same constraint the pre-existing pagination already lived with.\n */\n\nconst PAGE = 30;\nconst MAX_LENGTH = 10_000; // Every Add*CommentInput: \"1 to 10,000 characters after trimming\"\n\ntype CommentStatus = 'OPEN' | 'RESOLVED';\n\nexport interface ThreadComment {\n id: string;\n authorId: string;\n body: string;\n createdAt: string;\n editedAt: string | null;\n parentId: string | null;\n status: CommentStatus;\n resolvedBy: string | null;\n resolvedAt: string | null;\n mentions: string[];\n replies: ThreadComment[];\n}\n\ninterface ThreadList {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: ThreadComment[];\n}\n\ntype ThreadQueryData = Record<string, ThreadList>;\n\nexport interface CommentThreadProps {\n /** What this thread is attached to, used only in accessible copy (\"Add a comment on this task…\"). */\n targetNoun: string;\n /** Seeds every form-control id this instance renders, so two threads on the same page never collide. */\n idPrefix: string;\n query: {\n document: DocumentNode;\n /** The response field this query's data lands under (`planmagnetWorkItemComments`, …). */\n dataKey: string;\n variables: Record<string, unknown>;\n skip?: boolean;\n };\n add: {\n document: DocumentNode;\n buildVariables: (body: string, parentId: string | null) => Record<string, unknown>;\n };\n edit: DocumentNode;\n remove: DocumentNode;\n resolve: DocumentNode;\n /** Apollo operation names to refetch after any write here (this thread's own query, plus e.g. an Activity\n * panel on the same page that also shows COMMENTED events). */\n refetchQueries: string[];\n /** Reports the thread's live top-level count up to the caller, for a \"Comments (n)\" title. */\n onTotalCount?: (count: number) => void;\n}\n\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nexport function CommentThread({\n targetNoun,\n idPrefix,\n query,\n add,\n edit,\n remove,\n resolve,\n refetchQueries,\n onTotalCount,\n}: CommentThreadProps) {\n const { currentUser, workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n const [filter, setFilter] = useState<'OPEN' | 'RESOLVED' | 'ALL'>('ALL');\n\n const mutationRefetch = { refetchQueries, awaitRefetchQueries: true };\n\n const {\n data,\n loading,\n error: loadError,\n fetchMore,\n } = useQuery<ThreadQueryData>(query.document, {\n variables: { ...query.variables, pagination: { first: PAGE } },\n skip: query.skip,\n });\n const [addComment, addState] = useMutation(add.document, mutationRefetch);\n\n const thread = data?.[query.dataKey];\n // The server returns TOP-LEVEL comments here; each carries its own (bounded, unpaginated) `replies`.\n const topLevel = thread?.items ?? [];\n const visible = filter === 'ALL' ? topLevel : topLevel.filter((c) => c.status === filter);\n const openCount = topLevel.filter((c) => c.status === 'OPEN').length;\n const resolvedCount = topLevel.length - openCount;\n\n const trimmed = draft.trim();\n const tooLong = trimmed.length > MAX_LENGTH;\n\n useEffect(() => {\n if (thread) onTotalCount?.(thread.totalCount);\n }, [thread, onTotalCount]);\n\n const post = async (body: string, parentId: string | null): Promise<boolean> => {\n setError(null);\n try {\n await throwOnError(addComment({ variables: add.buildVariables(body, parentId) }));\n return true;\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The comment could not be posted.')\n );\n return false;\n }\n };\n\n const submitTop = async () => {\n if (await post(trimmed, null)) setDraft('');\n };\n\n const loadMore = async () => {\n const after = thread?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { ...query.variables, pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n [query.dataKey]: {\n ...fetchMoreResult[query.dataKey],\n items: [...prev[query.dataKey].items, ...fetchMoreResult[query.dataKey].items],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-4\" data-testid=\"comment-thread\">\n {error && <ErrorLine message={error} />}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading comments…</p>}\n {loadError && <ErrorLine message={`Comments could not be loaded: ${loadError.message}`} />}\n\n {thread && topLevel.length > 0 && (\n <div\n role=\"group\"\n aria-label=\"Filter comments by resolution\"\n data-testid=\"comment-filter\"\n className=\"flex flex-wrap gap-1\"\n >\n {(\n [\n ['OPEN', `Open (${openCount})`],\n ['RESOLVED', `Resolved (${resolvedCount})`],\n ['ALL', `All (${topLevel.length})`],\n ] as const\n ).map(([mode, label]) => (\n <Button\n key={mode}\n type=\"button\"\n size=\"sm\"\n variant={filter === mode ? 'default' : 'outline'}\n aria-pressed={filter === mode}\n className=\"h-7 px-2.5 text-xs\"\n onClick={() => setFilter(mode)}\n >\n {label}\n </Button>\n ))}\n </div>\n )}\n\n {thread && visible.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n {topLevel.length === 0 ? 'No comments yet.' : 'No comments match this filter.'}\n </p>\n )}\n\n <ol className=\"space-y-3\">\n {visible.map((comment) => (\n <TopLevelThread\n key={comment.id}\n comment={comment}\n currentUserId={currentUser?.id}\n editMutation={edit}\n removeMutation={remove}\n resolveMutation={resolve}\n addMutation={add.document}\n buildReplyVariables={(body) => add.buildVariables(body, comment.id)}\n mutationRefetch={mutationRefetch}\n onError={setError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n ))}\n </ol>\n\n {thread?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${topLevel.length} of ${thread.totalCount})`}\n </Button>\n )}\n\n <div className=\"space-y-2 border-t border-border-default pt-4\">\n <label htmlFor={`${idPrefix}-add`} className=\"sr-only\">\n Add a comment\n </label>\n <MentionComposer\n id={`${idPrefix}-add`}\n ariaLabel={`Add a comment on this ${targetNoun}`}\n placeholder=\"Add a comment…\"\n className=\"min-h-[72px]\"\n value={draft}\n onChange={setDraft}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {tooLong && <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />}\n <div className=\"flex justify-end\">\n <Button\n size=\"sm\"\n onClick={() => void submitTop()}\n disabled={!trimmed || tooLong || addState.loading}\n data-testid=\"comment-post\"\n >\n {addState.loading ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\ninterface RowMutationProps {\n editMutation: DocumentNode;\n removeMutation: DocumentNode;\n mutationRefetch: { refetchQueries: string[]; awaitRefetchQueries: boolean };\n onError: (message: string | null) => void;\n idPrefix: string;\n workspaceId?: string;\n authToken?: string;\n apiGatewayUrl?: string;\n}\n\n/**\n * One top-level comment: header, body, its replies (indented), a Reply action, and — top-level only — the\n * Resolve/Reopen toggle. A resolved thread starts collapsed to a one-line summary; expand/collapse is local UI\n * state that does not reset when Resolve/Reopen flips (so reopening mid-review doesn't yank the view around).\n */\nfunction TopLevelThread({\n comment,\n currentUserId,\n editMutation,\n removeMutation,\n resolveMutation,\n addMutation,\n buildReplyVariables,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n}: RowMutationProps & {\n comment: ThreadComment;\n currentUserId: string | undefined;\n resolveMutation: DocumentNode;\n addMutation: DocumentNode;\n buildReplyVariables: (body: string) => Record<string, unknown>;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(comment.body);\n const [expanded, setExpanded] = useState(comment.status !== 'RESOLVED');\n const [replyOpen, setReplyOpen] = useState(false);\n const [replyDraft, setReplyDraft] = useState('');\n const [replyingToName, setReplyingToName] = useState<string | null>(null);\n\n const authorProfile = usePersonProfile(comment.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n const [resolveComment, resolveState] = useMutation(resolveMutation, mutationRefetch);\n const [reply, replyState] = useMutation(addMutation, mutationRefetch);\n\n const mine = comment.authorId === currentUserId;\n const resolved = comment.status === 'RESOLVED';\n const editTooLong = text.trim().length > MAX_LENGTH;\n const replyTooLong = replyDraft.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: comment.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: comment.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be deleted.'));\n }\n };\n\n const toggleResolve = async () => {\n onError(null);\n try {\n await throwOnError(resolveComment({ variables: { id: comment.id, resolved: !resolved } }));\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not resolve comments.'\n : mutationErrorMessage(e, 'The resolution could not be changed.')\n );\n }\n };\n\n const openReply = (name: string, mentionId?: string, mentionName?: string) => {\n setReplyingToName(name);\n setReplyDraft(mentionId && mentionName ? `@[${mentionName}](${mentionId}) ` : '');\n setReplyOpen(true);\n setExpanded(true);\n };\n\n const submitReply = async () => {\n const body = replyDraft.trim();\n if (!body) return;\n onError(null);\n try {\n await throwOnError(reply({ variables: buildReplyVariables(body) }));\n setReplyDraft('');\n setReplyOpen(false);\n setReplyingToName(null);\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The reply could not be posted.')\n );\n }\n };\n\n const rowId = `${idPrefix}-comment-${comment.id}`;\n\n return (\n <li\n className=\"rounded-md border border-border-default p-3 space-y-2\"\n data-testid=\"comment-thread-item\"\n >\n {resolved && !expanded ? (\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"min-w-0 truncate text-sm text-muted-foreground\">\n <CheckCircle2\n className=\"mr-1 inline h-3.5 w-3.5 text-status-success-text\"\n aria-hidden=\"true\"\n />\n Resolved by <PersonName id={comment.resolvedBy} />\n {comment.resolvedAt ? ` · ${when(comment.resolvedAt)}` : ''}\n {comment.replies.length > 0\n ? ` · ${comment.replies.length} repl${comment.replies.length === 1 ? 'y' : 'ies'}`\n : ''}\n </p>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 shrink-0 px-2 text-xs\"\n aria-expanded={false}\n aria-controls={rowId}\n onClick={() => setExpanded(true)}\n >\n <ChevronRight className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Expand\n </Button>\n </div>\n ) : (\n <div id={rowId} className=\"space-y-2\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={comment.authorId} />\n </span>{' '}\n · {when(comment.createdAt)}\n {comment.editedAt && (\n <span title={`Edited ${when(comment.editedAt)}`}> · edited</span>\n )}\n {resolved && (\n <span className=\"ml-1 inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolved\n </span>\n )}\n </span>\n <span className=\"flex shrink-0 items-center gap-1\">\n {mine && !editing && (\n <>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete comment\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this comment?\"\n description=\"It is removed from the discussion for everyone, replies included. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </>\n )}\n <Button\n size=\"sm\"\n variant={resolved ? 'outline' : 'ghost'}\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-resolve\"\n disabled={resolveState.loading}\n onClick={() => void toggleResolve()}\n >\n {resolved ? (\n <>\n <RotateCcw className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reopen\n </>\n ) : (\n <>\n <CheckCircle2 className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolve\n </>\n )}\n </Button>\n {resolved && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n aria-expanded\n aria-controls={rowId}\n onClick={() => setExpanded(false)}\n >\n <ChevronDown className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Collapse\n </Button>\n )}\n </span>\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${rowId}-edit`}\n ariaLabel=\"Edit comment\"\n className=\"min-h-[60px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(comment.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <MentionText text={comment.body} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n openReply(\n authorProfile?.displayName ?? 'this comment',\n comment.authorId,\n authorProfile?.displayName\n )\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n\n {comment.replies.length > 0 && (\n <ol className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {comment.replies.map((r) => (\n <ReplyItem\n key={r.id}\n reply={r}\n currentUserId={currentUserId}\n editMutation={editMutation}\n removeMutation={removeMutation}\n mutationRefetch={mutationRefetch}\n onError={onError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n onReply={openReply}\n />\n ))}\n </ol>\n )}\n\n {replyOpen && (\n <div className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {replyingToName && (\n <p className=\"text-xs text-muted-foreground\">Replying to {replyingToName}</p>\n )}\n <MentionComposer\n id={`${rowId}-reply`}\n ariaLabel=\"Write a reply\"\n className=\"min-h-[60px]\"\n value={replyDraft}\n onChange={setReplyDraft}\n autoFocus\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {replyTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setReplyOpen(false);\n setReplyDraft('');\n setReplyingToName(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void submitReply()}\n disabled={!replyDraft.trim() || replyTooLong || replyState.loading}\n >\n {replyState.loading ? 'Posting…' : 'Reply'}\n </Button>\n </div>\n </div>\n )}\n </div>\n )}\n </li>\n );\n}\n\n/** One reply, indented under its top-level parent. Replies carry no resolution state of their own (only a\n * top-level comment does — CWF-14) and their own Reply action still targets the TOP-LEVEL thread\n * (`onReply` is the parent's `openReply`, called with this reply's own author as the \"replying to\" context). */\nfunction ReplyItem({\n reply,\n currentUserId,\n editMutation,\n removeMutation,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n onReply,\n}: RowMutationProps & {\n reply: ThreadComment;\n currentUserId: string | undefined;\n onReply: (name: string, mentionId?: string, mentionName?: string) => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(reply.body);\n const profile = usePersonProfile(reply.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n\n const mine = reply.authorId === currentUserId;\n const editTooLong = text.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: reply.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: reply.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be deleted.'));\n }\n };\n\n return (\n <li className=\"space-y-1\" data-testid=\"comment-reply-item\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={reply.authorId} />\n </span>{' '}\n · {when(reply.createdAt)}\n {reply.editedAt && <span title={`Edited ${when(reply.editedAt)}`}> · edited</span>}\n </span>\n {mine && !editing && (\n <span className=\"flex shrink-0 items-center gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete reply\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this reply?\"\n description=\"It is removed from the discussion for everyone. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </span>\n )}\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${idPrefix}-reply-${reply.id}-edit`}\n ariaLabel=\"Edit reply\"\n className=\"min-h-[56px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(reply.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <MentionText text={reply.body} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n onReply(profile?.displayName ?? 'this reply', reply.authorId, profile?.displayName)\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n </li>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CommentThread.js","names":[],"sources":["../../../src/planmagnet/comments/CommentThread.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { DocumentNode } from 'graphql';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, ChevronDown, ChevronRight, Reply, RotateCcw, Trash2 } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName, usePersonProfile } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { MentionComposer, MentionText } from './MentionComposer';\nimport { TypingLine } from '../presence/PresenceAvatars';\n\n/**\n * The comment thread engine shared by every commentable surface in this MFE: a work item (CWF-02, via\n * `WorkItemComments.tsx`) and a project/doc (CWF-12, via `TargetComments.tsx`). Modeled on\n * `comments/RoadmapComments.tsx`'s list/add/author-only-edit/delete structure, extended with:\n *\n * - Threading (CWF-02): `PlanmagnetWorkItemComment`/`PlanmagnetComment.replies` are ONE level deep by contract —\n * the server reparents \"a reply to a reply\" onto ITS OWN top-level parent. So a Reply action lives on both a\n * top-level comment and on one of its replies, but both always post with `parentId = <the top-level id>`\n * (`replyTargetId` below), matching what the server would do anyway — never a reply's own id.\n * - Resolution (CWF-14): a top-level-only OPEN/RESOLVED toggle, not restricted to the comment's author. A\n * resolved thread collapses to a one-line summary that can be expanded back out.\n * - Mentions (CWF-13): every composer here (add / reply / edit) is a `MentionComposer`; every rendered body is\n * `MentionText`, so a `@[Name](actorId)` token always shows as a highlighted name, never raw markup.\n * - A caller-agnostic core: `query`/`add`/`edit`/`remove`/`resolve` are passed in per surface (different\n * operation names, same field shapes — see `graphql/wspace.graphql`'s `PlanmagnetWorkItemComment` vs\n * `PlanmagnetComment` doc comments: \"everything here means exactly what it means on\" the other type).\n *\n * The status filter (Open/Resolved/All) is applied CLIENT-SIDE over the loaded page: none of\n * `planmagnetWorkItemComments`/`planmagnetProjectComments`/`planmagnetDocComments` take a status argument, only\n * `pagination` — same constraint the pre-existing pagination already lived with.\n */\n\nconst PAGE = 30;\nconst MAX_LENGTH = 10_000; // Every Add*CommentInput: \"1 to 10,000 characters after trimming\"\n\ntype CommentStatus = 'OPEN' | 'RESOLVED';\n\nexport interface ThreadComment {\n id: string;\n authorId: string;\n body: string;\n createdAt: string;\n editedAt: string | null;\n parentId: string | null;\n status: CommentStatus;\n resolvedBy: string | null;\n resolvedAt: string | null;\n mentions: string[];\n replies: ThreadComment[];\n}\n\ninterface ThreadList {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: ThreadComment[];\n}\n\ntype ThreadQueryData = Record<string, ThreadList>;\n\nexport interface CommentThreadProps {\n /** What this thread is attached to, used only in accessible copy (\"Add a comment on this task…\"). */\n targetNoun: string;\n /** Seeds every form-control id this instance renders, so two threads on the same page never collide. */\n idPrefix: string;\n query: {\n document: DocumentNode;\n /** The response field this query's data lands under (`planmagnetWorkItemComments`, …). */\n dataKey: string;\n variables: Record<string, unknown>;\n skip?: boolean;\n };\n add: {\n document: DocumentNode;\n buildVariables: (body: string, parentId: string | null) => Record<string, unknown>;\n };\n edit: DocumentNode;\n remove: DocumentNode;\n resolve: DocumentNode;\n /** Apollo operation names to refetch after any write here (this thread's own query, plus e.g. an Activity\n * panel on the same page that also shows COMMENTED events). */\n refetchQueries: string[];\n /** Reports the thread's live top-level count up to the caller, for a \"Comments (n)\" title. */\n onTotalCount?: (count: number) => void;\n /** CWF-24 typing presence, owned by the page (one `usePresence` call shared with its `PresenceAvatars`) and\n * threaded down here so the composers can signal typing and the thread can show who else is mid-reply. Omitted\n * entirely on a surface that has not wired presence up (renders nothing extra, same as before). */\n presence?: {\n typingActorIds: readonly string[];\n notifyTyping: () => void;\n };\n}\n\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nexport function CommentThread({\n targetNoun,\n idPrefix,\n query,\n add,\n edit,\n remove,\n resolve,\n refetchQueries,\n onTotalCount,\n presence,\n}: CommentThreadProps) {\n const { currentUser, workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n const [filter, setFilter] = useState<'OPEN' | 'RESOLVED' | 'ALL'>('ALL');\n\n const mutationRefetch = { refetchQueries, awaitRefetchQueries: true };\n\n const {\n data,\n loading,\n error: loadError,\n fetchMore,\n } = useQuery<ThreadQueryData>(query.document, {\n variables: { ...query.variables, pagination: { first: PAGE } },\n skip: query.skip,\n });\n const [addComment, addState] = useMutation(add.document, mutationRefetch);\n\n const thread = data?.[query.dataKey];\n // The server returns TOP-LEVEL comments here; each carries its own (bounded, unpaginated) `replies`.\n const topLevel = thread?.items ?? [];\n const visible = filter === 'ALL' ? topLevel : topLevel.filter((c) => c.status === filter);\n const openCount = topLevel.filter((c) => c.status === 'OPEN').length;\n const resolvedCount = topLevel.length - openCount;\n\n const trimmed = draft.trim();\n const tooLong = trimmed.length > MAX_LENGTH;\n\n useEffect(() => {\n if (thread) onTotalCount?.(thread.totalCount);\n }, [thread, onTotalCount]);\n\n const post = async (body: string, parentId: string | null): Promise<boolean> => {\n setError(null);\n try {\n await throwOnError(addComment({ variables: add.buildVariables(body, parentId) }));\n return true;\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The comment could not be posted.')\n );\n return false;\n }\n };\n\n const submitTop = async () => {\n if (await post(trimmed, null)) setDraft('');\n };\n\n const loadMore = async () => {\n const after = thread?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { ...query.variables, pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n [query.dataKey]: {\n ...fetchMoreResult[query.dataKey],\n items: [...prev[query.dataKey].items, ...fetchMoreResult[query.dataKey].items],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-4\" data-testid=\"comment-thread\">\n {error && <ErrorLine message={error} />}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading comments…</p>}\n {loadError && <ErrorLine message={`Comments could not be loaded: ${loadError.message}`} />}\n\n {thread && topLevel.length > 0 && (\n <div\n role=\"group\"\n aria-label=\"Filter comments by resolution\"\n data-testid=\"comment-filter\"\n className=\"flex flex-wrap gap-1\"\n >\n {(\n [\n ['OPEN', `Open (${openCount})`],\n ['RESOLVED', `Resolved (${resolvedCount})`],\n ['ALL', `All (${topLevel.length})`],\n ] as const\n ).map(([mode, label]) => (\n <Button\n key={mode}\n type=\"button\"\n size=\"sm\"\n variant={filter === mode ? 'default' : 'outline'}\n aria-pressed={filter === mode}\n className=\"h-7 px-2.5 text-xs\"\n onClick={() => setFilter(mode)}\n >\n {label}\n </Button>\n ))}\n </div>\n )}\n\n {thread && visible.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n {topLevel.length === 0 ? 'No comments yet.' : 'No comments match this filter.'}\n </p>\n )}\n\n <ol className=\"space-y-3\">\n {visible.map((comment) => (\n <TopLevelThread\n key={comment.id}\n comment={comment}\n currentUserId={currentUser?.id}\n editMutation={edit}\n removeMutation={remove}\n resolveMutation={resolve}\n addMutation={add.document}\n buildReplyVariables={(body) => add.buildVariables(body, comment.id)}\n mutationRefetch={mutationRefetch}\n onError={setError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n onTyping={presence?.notifyTyping}\n />\n ))}\n </ol>\n\n {thread?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${topLevel.length} of ${thread.totalCount})`}\n </Button>\n )}\n\n {presence && <TypingLine typingActorIds={presence.typingActorIds} />}\n\n <div className=\"space-y-2 border-t border-border-default pt-4\">\n <label htmlFor={`${idPrefix}-add`} className=\"sr-only\">\n Add a comment\n </label>\n <MentionComposer\n id={`${idPrefix}-add`}\n ariaLabel={`Add a comment on this ${targetNoun}`}\n placeholder=\"Add a comment…\"\n className=\"min-h-[72px]\"\n value={draft}\n onChange={(value) => {\n setDraft(value);\n presence?.notifyTyping();\n }}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {tooLong && <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />}\n <div className=\"flex justify-end\">\n <Button\n size=\"sm\"\n onClick={() => void submitTop()}\n disabled={!trimmed || tooLong || addState.loading}\n data-testid=\"comment-post\"\n >\n {addState.loading ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\ninterface RowMutationProps {\n editMutation: DocumentNode;\n removeMutation: DocumentNode;\n mutationRefetch: { refetchQueries: string[]; awaitRefetchQueries: boolean };\n onError: (message: string | null) => void;\n idPrefix: string;\n workspaceId?: string;\n authToken?: string;\n apiGatewayUrl?: string;\n}\n\n/**\n * One top-level comment: header, body, its replies (indented), a Reply action, and — top-level only — the\n * Resolve/Reopen toggle. A resolved thread starts collapsed to a one-line summary; expand/collapse is local UI\n * state that does not reset when Resolve/Reopen flips (so reopening mid-review doesn't yank the view around).\n */\nfunction TopLevelThread({\n comment,\n currentUserId,\n editMutation,\n removeMutation,\n resolveMutation,\n addMutation,\n buildReplyVariables,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n onTyping,\n}: RowMutationProps & {\n comment: ThreadComment;\n currentUserId: string | undefined;\n resolveMutation: DocumentNode;\n addMutation: DocumentNode;\n buildReplyVariables: (body: string) => Record<string, unknown>;\n /** CWF-24: signals typing while the reply box (the only composer a `TopLevelThread` instance owns) is active. */\n onTyping?: () => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(comment.body);\n const [expanded, setExpanded] = useState(comment.status !== 'RESOLVED');\n const [replyOpen, setReplyOpen] = useState(false);\n const [replyDraft, setReplyDraft] = useState('');\n const [replyingToName, setReplyingToName] = useState<string | null>(null);\n\n const authorProfile = usePersonProfile(comment.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n const [resolveComment, resolveState] = useMutation(resolveMutation, mutationRefetch);\n const [reply, replyState] = useMutation(addMutation, mutationRefetch);\n\n const mine = comment.authorId === currentUserId;\n const resolved = comment.status === 'RESOLVED';\n const editTooLong = text.trim().length > MAX_LENGTH;\n const replyTooLong = replyDraft.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: comment.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: comment.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be deleted.'));\n }\n };\n\n const toggleResolve = async () => {\n onError(null);\n try {\n await throwOnError(resolveComment({ variables: { id: comment.id, resolved: !resolved } }));\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not resolve comments.'\n : mutationErrorMessage(e, 'The resolution could not be changed.')\n );\n }\n };\n\n const openReply = (name: string, mentionId?: string, mentionName?: string) => {\n setReplyingToName(name);\n setReplyDraft(mentionId && mentionName ? `@[${mentionName}](${mentionId}) ` : '');\n setReplyOpen(true);\n setExpanded(true);\n };\n\n const submitReply = async () => {\n const body = replyDraft.trim();\n if (!body) return;\n onError(null);\n try {\n await throwOnError(reply({ variables: buildReplyVariables(body) }));\n setReplyDraft('');\n setReplyOpen(false);\n setReplyingToName(null);\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The reply could not be posted.')\n );\n }\n };\n\n const rowId = `${idPrefix}-comment-${comment.id}`;\n\n return (\n <li\n className=\"rounded-md border border-border-default p-3 space-y-2\"\n data-testid=\"comment-thread-item\"\n >\n {resolved && !expanded ? (\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"min-w-0 truncate text-sm text-muted-foreground\">\n <CheckCircle2\n className=\"mr-1 inline h-3.5 w-3.5 text-status-success-text\"\n aria-hidden=\"true\"\n />\n Resolved by <PersonName id={comment.resolvedBy} />\n {comment.resolvedAt ? ` · ${when(comment.resolvedAt)}` : ''}\n {comment.replies.length > 0\n ? ` · ${comment.replies.length} repl${comment.replies.length === 1 ? 'y' : 'ies'}`\n : ''}\n </p>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 shrink-0 px-2 text-xs\"\n aria-expanded={false}\n aria-controls={rowId}\n onClick={() => setExpanded(true)}\n >\n <ChevronRight className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Expand\n </Button>\n </div>\n ) : (\n <div id={rowId} className=\"space-y-2\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={comment.authorId} />\n </span>{' '}\n · {when(comment.createdAt)}\n {comment.editedAt && (\n <span title={`Edited ${when(comment.editedAt)}`}> · edited</span>\n )}\n {resolved && (\n <span className=\"ml-1 inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolved\n </span>\n )}\n </span>\n <span className=\"flex shrink-0 items-center gap-1\">\n {mine && !editing && (\n <>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete comment\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this comment?\"\n description=\"It is removed from the discussion for everyone, replies included. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </>\n )}\n <Button\n size=\"sm\"\n variant={resolved ? 'outline' : 'ghost'}\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-resolve\"\n disabled={resolveState.loading}\n onClick={() => void toggleResolve()}\n >\n {resolved ? (\n <>\n <RotateCcw className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reopen\n </>\n ) : (\n <>\n <CheckCircle2 className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolve\n </>\n )}\n </Button>\n {resolved && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n aria-expanded\n aria-controls={rowId}\n onClick={() => setExpanded(false)}\n >\n <ChevronDown className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Collapse\n </Button>\n )}\n </span>\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${rowId}-edit`}\n ariaLabel=\"Edit comment\"\n className=\"min-h-[60px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(comment.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <MentionText text={comment.body} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n openReply(\n authorProfile?.displayName ?? 'this comment',\n comment.authorId,\n authorProfile?.displayName\n )\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n\n {comment.replies.length > 0 && (\n <ol className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {comment.replies.map((r) => (\n <ReplyItem\n key={r.id}\n reply={r}\n currentUserId={currentUserId}\n editMutation={editMutation}\n removeMutation={removeMutation}\n mutationRefetch={mutationRefetch}\n onError={onError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n onReply={openReply}\n />\n ))}\n </ol>\n )}\n\n {replyOpen && (\n <div className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {replyingToName && (\n <p className=\"text-xs text-muted-foreground\">Replying to {replyingToName}</p>\n )}\n <MentionComposer\n id={`${rowId}-reply`}\n ariaLabel=\"Write a reply\"\n className=\"min-h-[60px]\"\n value={replyDraft}\n onChange={(value) => {\n setReplyDraft(value);\n onTyping?.();\n }}\n autoFocus\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {replyTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setReplyOpen(false);\n setReplyDraft('');\n setReplyingToName(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void submitReply()}\n disabled={!replyDraft.trim() || replyTooLong || replyState.loading}\n >\n {replyState.loading ? 'Posting…' : 'Reply'}\n </Button>\n </div>\n </div>\n )}\n </div>\n )}\n </li>\n );\n}\n\n/** One reply, indented under its top-level parent. Replies carry no resolution state of their own (only a\n * top-level comment does — CWF-14) and their own Reply action still targets the TOP-LEVEL thread\n * (`onReply` is the parent's `openReply`, called with this reply's own author as the \"replying to\" context). */\nfunction ReplyItem({\n reply,\n currentUserId,\n editMutation,\n removeMutation,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n onReply,\n}: RowMutationProps & {\n reply: ThreadComment;\n currentUserId: string | undefined;\n onReply: (name: string, mentionId?: string, mentionName?: string) => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(reply.body);\n const profile = usePersonProfile(reply.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n\n const mine = reply.authorId === currentUserId;\n const editTooLong = text.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: reply.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: reply.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be deleted.'));\n }\n };\n\n return (\n <li className=\"space-y-1\" data-testid=\"comment-reply-item\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={reply.authorId} />\n </span>{' '}\n · {when(reply.createdAt)}\n {reply.editedAt && <span title={`Edited ${when(reply.editedAt)}`}> · edited</span>}\n </span>\n {mine && !editing && (\n <span className=\"flex shrink-0 items-center gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete reply\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this reply?\"\n description=\"It is removed from the discussion for everyone. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </span>\n )}\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${idPrefix}-reply-${reply.id}-edit`}\n ariaLabel=\"Edit reply\"\n className=\"min-h-[56px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(reply.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <MentionText text={reply.body} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n onReply(profile?.displayName ?? 'this reply', reply.authorId, profile?.displayName)\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n </li>\n );\n}\n"],"mappings":"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"}
@@ -1,7 +1,15 @@
1
- export declare function ProjectComments({ projectId }: {
1
+ export interface TargetCommentsPresence {
2
+ typingActorIds: readonly string[];
3
+ notifyTyping: () => void;
4
+ }
5
+ export declare function ProjectComments({ projectId, presence, }: {
2
6
  projectId: string;
7
+ /** CWF-24: typing presence from the page's own `usePresence(targetType: 'PROJECT', ...)` call. */
8
+ presence?: TargetCommentsPresence;
3
9
  }): import("react/jsx-runtime").JSX.Element;
4
- export declare function DocComments({ docId }: {
10
+ export declare function DocComments({ docId, presence, }: {
5
11
  docId: string;
12
+ /** CWF-24: typing presence from the page's own `usePresence(targetType: 'DOC', ...)` call. */
13
+ presence?: TargetCommentsPresence;
6
14
  }): import("react/jsx-runtime").JSX.Element;
7
15
  //# sourceMappingURL=TargetComments.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TargetComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/TargetComments.tsx"],"names":[],"mappings":"AAoFA,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,2CAiCnE;AAqED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,2CAiCvD"}
1
+ {"version":3,"file":"TargetComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/TargetComments.tsx"],"names":[],"mappings":"AAoFA,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,QAAQ,GACT,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC,2CAkCA;AAqED,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC,2CAkCA"}
@@ -67,8 +67,8 @@ var u = r`
67
67
  }
68
68
  }
69
69
  `;
70
- function h({ projectId: r }) {
71
- let [h, g] = t(null);
70
+ function h({ projectId: r, presence: h }) {
71
+ let [g, _] = t(null);
72
72
  return /* @__PURE__ */ a(o, {
73
73
  "data-testid": "project-comments",
74
74
  children: [/* @__PURE__ */ i(c, { children: /* @__PURE__ */ a(l, {
@@ -76,7 +76,7 @@ function h({ projectId: r }) {
76
76
  children: [
77
77
  /* @__PURE__ */ i(n, { className: "h-4 w-4" }),
78
78
  "Comments",
79
- h === null ? "" : ` (${h})`
79
+ g === null ? "" : ` (${g})`
80
80
  ]
81
81
  }) }), /* @__PURE__ */ i(s, { children: /* @__PURE__ */ i(e, {
82
82
  targetNoun: "project",
@@ -99,7 +99,8 @@ function h({ projectId: r }) {
99
99
  remove: p,
100
100
  resolve: m,
101
101
  refetchQueries: ["ProjectComments"],
102
- onTotalCount: g
102
+ onTotalCount: _,
103
+ presence: h
103
104
  }) })]
104
105
  });
105
106
  }
@@ -165,8 +166,8 @@ var g = r`
165
166
  }
166
167
  }
167
168
  `;
168
- function x({ docId: r }) {
169
- let [u, d] = t(null);
169
+ function x({ docId: r, presence: u }) {
170
+ let [d, f] = t(null);
170
171
  return /* @__PURE__ */ a(o, {
171
172
  "data-testid": "doc-comments",
172
173
  children: [/* @__PURE__ */ i(c, { children: /* @__PURE__ */ a(l, {
@@ -174,7 +175,7 @@ function x({ docId: r }) {
174
175
  children: [
175
176
  /* @__PURE__ */ i(n, { className: "h-4 w-4" }),
176
177
  "Comments",
177
- u === null ? "" : ` (${u})`
178
+ d === null ? "" : ` (${d})`
178
179
  ]
179
180
  }) }), /* @__PURE__ */ i(s, { children: /* @__PURE__ */ i(e, {
180
181
  targetNoun: "doc",
@@ -197,7 +198,8 @@ function x({ docId: r }) {
197
198
  remove: y,
198
199
  resolve: b,
199
200
  refetchQueries: ["DocComments"],
200
- onTotalCount: d
201
+ onTotalCount: f,
202
+ presence: u
201
203
  }) })]
202
204
  });
203
205
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TargetComments.js","names":[],"sources":["../../../src/planmagnet/comments/TargetComments.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { MessageSquare } from 'lucide-react';\nimport { CommentThread } from './CommentThread';\n\n/**\n * Comments on a project or a doc (CWF-12) — the `PlanmagnetComment` type (`targetType` PROJECT | DOC), which\n * means exactly what `PlanmagnetWorkItemComment` means on a work item (see its schema doc comment): same\n * threading (CWF-02), resolution (CWF-14) and mention (CWF-13) behaviour, via the same `CommentThread` engine\n * `WorkItemComments.tsx` uses — only the GraphQL operation names differ per target.\n *\n * A viewer who can read but not write (e.g. a stakeholder-viewer role) sees the thread and gets the same\n * \"Your role can read but not comment.\" refusal `CommentThread` already shows on a FORBIDDEN post — there is no\n * separate read-only banner here; the refusal only appears once they actually try to post.\n */\n\nconst PROJECT_COMMENTS = gql`\n query ProjectComments($projectId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetProjectComments(projectId: $projectId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n }\n }\n }\n }\n`;\nconst ADD_PROJECT_COMMENT = gql`\n mutation ProjectCommentAdd($input: PlanmagnetAddProjectCommentInput!) {\n planmagnetAddProjectComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT_PROJECT_COMMENT = gql`\n mutation ProjectCommentEdit($id: ID!, $body: String!) {\n planmagnetEditProjectComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE_PROJECT_COMMENT = gql`\n mutation ProjectCommentDelete($id: ID!) {\n planmagnetDeleteProjectComment(id: $id)\n }\n`;\nconst RESOLVE_PROJECT_COMMENT = gql`\n mutation ProjectCommentResolve($id: ID!, $resolved: Boolean!) {\n planmagnetResolveProjectComment(id: $id, resolved: $resolved) {\n id\n status\n resolvedBy\n resolvedAt\n }\n }\n`;\n\nexport function ProjectComments({ projectId }: { projectId: string }) {\n const [totalCount, setTotalCount] = useState<number | null>(null);\n return (\n <Card data-testid=\"project-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Comments{totalCount !== null ? ` (${totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <CommentThread\n targetNoun=\"project\"\n idPrefix={`project-comment-${projectId}`}\n query={{\n document: PROJECT_COMMENTS,\n dataKey: 'planmagnetProjectComments',\n variables: { projectId },\n skip: !projectId,\n }}\n add={{\n document: ADD_PROJECT_COMMENT,\n buildVariables: (body, parentId) => ({ input: { projectId, body, parentId } }),\n }}\n edit={EDIT_PROJECT_COMMENT}\n remove={DELETE_PROJECT_COMMENT}\n resolve={RESOLVE_PROJECT_COMMENT}\n refetchQueries={['ProjectComments']}\n onTotalCount={setTotalCount}\n />\n </CardContent>\n </Card>\n );\n}\n\nconst DOC_COMMENTS = gql`\n query DocComments($docId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetDocComments(docId: $docId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n }\n }\n }\n }\n`;\nconst ADD_DOC_COMMENT = gql`\n mutation DocCommentAdd($input: PlanmagnetAddDocCommentInput!) {\n planmagnetAddDocComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT_DOC_COMMENT = gql`\n mutation DocCommentEdit($id: ID!, $body: String!) {\n planmagnetEditDocComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE_DOC_COMMENT = gql`\n mutation DocCommentDelete($id: ID!) {\n planmagnetDeleteDocComment(id: $id)\n }\n`;\nconst RESOLVE_DOC_COMMENT = gql`\n mutation DocCommentResolve($id: ID!, $resolved: Boolean!) {\n planmagnetResolveDocComment(id: $id, resolved: $resolved) {\n id\n status\n resolvedBy\n resolvedAt\n }\n }\n`;\n\nexport function DocComments({ docId }: { docId: string }) {\n const [totalCount, setTotalCount] = useState<number | null>(null);\n return (\n <Card data-testid=\"doc-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Comments{totalCount !== null ? ` (${totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <CommentThread\n targetNoun=\"doc\"\n idPrefix={`doc-comment-${docId}`}\n query={{\n document: DOC_COMMENTS,\n dataKey: 'planmagnetDocComments',\n variables: { docId },\n skip: !docId,\n }}\n add={{\n document: ADD_DOC_COMMENT,\n buildVariables: (body, parentId) => ({ input: { docId, body, parentId } }),\n }}\n edit={EDIT_DOC_COMMENT}\n remove={DELETE_DOC_COMMENT}\n resolve={RESOLVE_DOC_COMMENT}\n refetchQueries={['DocComments']}\n onTotalCount={setTotalCount}\n />\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;AAiBA,IAAM,IAAmB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCtB,IAAsB,CAAG;;;;;;GAOzB,IAAuB,CAAG;;;;;;;;GAS1B,IAAyB,CAAG;;;;GAK5B,IAA0B,CAAG;;;;;;;;;;AAWnC,SAAgB,EAAgB,EAAE,gBAAoC;CACpE,IAAM,CAAC,GAAY,KAAiB,EAAwB,KAAK;AACjE,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;IAC5B,MAAe,OAA4B,KAArB,KAAK,EAAW;IACrC;MACD,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GACE,YAAW;GACX,UAAU,mBAAmB;GAC7B,OAAO;IACL,UAAU;IACV,SAAS;IACT,WAAW,EAAE,cAAW;IACxB,MAAM,CAAC;IACR;GACD,KAAK;IACH,UAAU;IACV,iBAAiB,GAAM,OAAc,EAAE,OAAO;KAAE;KAAW;KAAM;KAAU,EAAE;IAC9E;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,kBAAkB;GACnC,cAAc;GACd,CAAA,EACU,CAAA,CACT;;;AAIX,IAAM,IAAe,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmClB,IAAkB,CAAG;;;;;;GAOrB,IAAmB,CAAG;;;;;;;;GAStB,IAAqB,CAAG;;;;GAKxB,IAAsB,CAAG;;;;;;;;;;AAW/B,SAAgB,EAAY,EAAE,YAA4B;CACxD,IAAM,CAAC,GAAY,KAAiB,EAAwB,KAAK;AACjE,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;IAC5B,MAAe,OAA4B,KAArB,KAAK,EAAW;IACrC;MACD,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GACE,YAAW;GACX,UAAU,eAAe;GACzB,OAAO;IACL,UAAU;IACV,SAAS;IACT,WAAW,EAAE,UAAO;IACpB,MAAM,CAAC;IACR;GACD,KAAK;IACH,UAAU;IACV,iBAAiB,GAAM,OAAc,EAAE,OAAO;KAAE;KAAO;KAAM;KAAU,EAAE;IAC1E;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,cAAc;GAC/B,cAAc;GACd,CAAA,EACU,CAAA,CACT"}
1
+ {"version":3,"file":"TargetComments.js","names":[],"sources":["../../../src/planmagnet/comments/TargetComments.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { MessageSquare } from 'lucide-react';\nimport { CommentThread } from './CommentThread';\n\n/**\n * Comments on a project or a doc (CWF-12) — the `PlanmagnetComment` type (`targetType` PROJECT | DOC), which\n * means exactly what `PlanmagnetWorkItemComment` means on a work item (see its schema doc comment): same\n * threading (CWF-02), resolution (CWF-14) and mention (CWF-13) behaviour, via the same `CommentThread` engine\n * `WorkItemComments.tsx` uses — only the GraphQL operation names differ per target.\n *\n * A viewer who can read but not write (e.g. a stakeholder-viewer role) sees the thread and gets the same\n * \"Your role can read but not comment.\" refusal `CommentThread` already shows on a FORBIDDEN post — there is no\n * separate read-only banner here; the refusal only appears once they actually try to post.\n */\n\nconst PROJECT_COMMENTS = gql`\n query ProjectComments($projectId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetProjectComments(projectId: $projectId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n }\n }\n }\n }\n`;\nconst ADD_PROJECT_COMMENT = gql`\n mutation ProjectCommentAdd($input: PlanmagnetAddProjectCommentInput!) {\n planmagnetAddProjectComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT_PROJECT_COMMENT = gql`\n mutation ProjectCommentEdit($id: ID!, $body: String!) {\n planmagnetEditProjectComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE_PROJECT_COMMENT = gql`\n mutation ProjectCommentDelete($id: ID!) {\n planmagnetDeleteProjectComment(id: $id)\n }\n`;\nconst RESOLVE_PROJECT_COMMENT = gql`\n mutation ProjectCommentResolve($id: ID!, $resolved: Boolean!) {\n planmagnetResolveProjectComment(id: $id, resolved: $resolved) {\n id\n status\n resolvedBy\n resolvedAt\n }\n }\n`;\n\nexport interface TargetCommentsPresence {\n typingActorIds: readonly string[];\n notifyTyping: () => void;\n}\n\nexport function ProjectComments({\n projectId,\n presence,\n}: {\n projectId: string;\n /** CWF-24: typing presence from the page's own `usePresence(targetType: 'PROJECT', ...)` call. */\n presence?: TargetCommentsPresence;\n}) {\n const [totalCount, setTotalCount] = useState<number | null>(null);\n return (\n <Card data-testid=\"project-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Comments{totalCount !== null ? ` (${totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <CommentThread\n targetNoun=\"project\"\n idPrefix={`project-comment-${projectId}`}\n query={{\n document: PROJECT_COMMENTS,\n dataKey: 'planmagnetProjectComments',\n variables: { projectId },\n skip: !projectId,\n }}\n add={{\n document: ADD_PROJECT_COMMENT,\n buildVariables: (body, parentId) => ({ input: { projectId, body, parentId } }),\n }}\n edit={EDIT_PROJECT_COMMENT}\n remove={DELETE_PROJECT_COMMENT}\n resolve={RESOLVE_PROJECT_COMMENT}\n refetchQueries={['ProjectComments']}\n onTotalCount={setTotalCount}\n presence={presence}\n />\n </CardContent>\n </Card>\n );\n}\n\nconst DOC_COMMENTS = gql`\n query DocComments($docId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetDocComments(docId: $docId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n }\n }\n }\n }\n`;\nconst ADD_DOC_COMMENT = gql`\n mutation DocCommentAdd($input: PlanmagnetAddDocCommentInput!) {\n planmagnetAddDocComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT_DOC_COMMENT = gql`\n mutation DocCommentEdit($id: ID!, $body: String!) {\n planmagnetEditDocComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE_DOC_COMMENT = gql`\n mutation DocCommentDelete($id: ID!) {\n planmagnetDeleteDocComment(id: $id)\n }\n`;\nconst RESOLVE_DOC_COMMENT = gql`\n mutation DocCommentResolve($id: ID!, $resolved: Boolean!) {\n planmagnetResolveDocComment(id: $id, resolved: $resolved) {\n id\n status\n resolvedBy\n resolvedAt\n }\n }\n`;\n\nexport function DocComments({\n docId,\n presence,\n}: {\n docId: string;\n /** CWF-24: typing presence from the page's own `usePresence(targetType: 'DOC', ...)` call. */\n presence?: TargetCommentsPresence;\n}) {\n const [totalCount, setTotalCount] = useState<number | null>(null);\n return (\n <Card data-testid=\"doc-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Comments{totalCount !== null ? ` (${totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <CommentThread\n targetNoun=\"doc\"\n idPrefix={`doc-comment-${docId}`}\n query={{\n document: DOC_COMMENTS,\n dataKey: 'planmagnetDocComments',\n variables: { docId },\n skip: !docId,\n }}\n add={{\n document: ADD_DOC_COMMENT,\n buildVariables: (body, parentId) => ({ input: { docId, body, parentId } }),\n }}\n edit={EDIT_DOC_COMMENT}\n remove={DELETE_DOC_COMMENT}\n resolve={RESOLVE_DOC_COMMENT}\n refetchQueries={['DocComments']}\n onTotalCount={setTotalCount}\n presence={presence}\n />\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;AAiBA,IAAM,IAAmB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCtB,IAAsB,CAAG;;;;;;GAOzB,IAAuB,CAAG;;;;;;;;GAS1B,IAAyB,CAAG;;;;GAK5B,IAA0B,CAAG;;;;;;;;;;AAgBnC,SAAgB,EAAgB,EAC9B,cACA,eAKC;CACD,IAAM,CAAC,GAAY,KAAiB,EAAwB,KAAK;AACjE,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;IAC5B,MAAe,OAA4B,KAArB,KAAK,EAAW;IACrC;MACD,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GACE,YAAW;GACX,UAAU,mBAAmB;GAC7B,OAAO;IACL,UAAU;IACV,SAAS;IACT,WAAW,EAAE,cAAW;IACxB,MAAM,CAAC;IACR;GACD,KAAK;IACH,UAAU;IACV,iBAAiB,GAAM,OAAc,EAAE,OAAO;KAAE;KAAW;KAAM;KAAU,EAAE;IAC9E;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,kBAAkB;GACnC,cAAc;GACJ;GACV,CAAA,EACU,CAAA,CACT;;;AAIX,IAAM,IAAe,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmClB,IAAkB,CAAG;;;;;;GAOrB,IAAmB,CAAG;;;;;;;;GAStB,IAAqB,CAAG;;;;GAKxB,IAAsB,CAAG;;;;;;;;;;AAW/B,SAAgB,EAAY,EAC1B,UACA,eAKC;CACD,IAAM,CAAC,GAAY,KAAiB,EAAwB,KAAK;AACjE,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;IAC5B,MAAe,OAA4B,KAArB,KAAK,EAAW;IACrC;MACD,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GACE,YAAW;GACX,UAAU,eAAe;GACzB,OAAO;IACL,UAAU;IACV,SAAS;IACT,WAAW,EAAE,UAAO;IACpB,MAAM,CAAC;IACR;GACD,KAAK;IACH,UAAU;IACV,iBAAiB,GAAM,OAAc,EAAE,OAAO;KAAE;KAAO;KAAM;KAAU,EAAE;IAC1E;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,cAAc;GAC/B,cAAc;GACJ;GACV,CAAA,EACU,CAAA,CACT"}
@@ -1,4 +1,10 @@
1
- export declare function WorkItemComments({ workItemId }: {
1
+ export interface WorkItemCommentsPresence {
2
+ typingActorIds: readonly string[];
3
+ notifyTyping: () => void;
4
+ }
5
+ export declare function WorkItemComments({ workItemId, presence, }: {
2
6
  workItemId: string;
7
+ /** CWF-24: typing presence from the page's own `usePresence(targetType: 'WORK_ITEM', ...)` call. */
8
+ presence?: WorkItemCommentsPresence;
3
9
  }): import("react/jsx-runtime").JSX.Element;
4
10
  //# sourceMappingURL=WorkItemComments.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WorkItemComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"names":[],"mappings":"AAiFA,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,2CAiCtE"}
1
+ {"version":3,"file":"WorkItemComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"names":[],"mappings":"AAiFA,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,QAAQ,GACT,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,QAAQ,CAAC,EAAE,wBAAwB,CAAC;CACrC,2CAkCA"}
@@ -67,8 +67,8 @@ var u = r`
67
67
  }
68
68
  }
69
69
  `;
70
- function h({ workItemId: r }) {
71
- let [h, g] = t(null);
70
+ function h({ workItemId: r, presence: h }) {
71
+ let [g, _] = t(null);
72
72
  return /* @__PURE__ */ a(o, {
73
73
  "data-testid": "work-item-comments",
74
74
  children: [/* @__PURE__ */ i(c, { children: /* @__PURE__ */ a(l, {
@@ -76,7 +76,7 @@ function h({ workItemId: r }) {
76
76
  children: [
77
77
  /* @__PURE__ */ i(n, { className: "h-4 w-4" }),
78
78
  "Discussion",
79
- h === null ? "" : ` (${h})`
79
+ g === null ? "" : ` (${g})`
80
80
  ]
81
81
  }) }), /* @__PURE__ */ i(s, { children: /* @__PURE__ */ i(e, {
82
82
  targetNoun: "task",
@@ -99,7 +99,8 @@ function h({ workItemId: r }) {
99
99
  remove: p,
100
100
  resolve: m,
101
101
  refetchQueries: ["WorkItemComments", "WorkItemActivity"],
102
- onTotalCount: g
102
+ onTotalCount: _,
103
+ presence: h
103
104
  }) })]
104
105
  });
105
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WorkItemComments.js","names":[],"sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { MessageSquare } from 'lucide-react';\nimport { CommentThread } from './CommentThread';\n\n/**\n * Discussion on a work item (CWF-02 threading, CWF-14 resolution, CWF-13 mentions). Anyone who can read the item\n * sees the thread; posting/replying/resolving need the item's write tier; only a comment's author can edit or\n * delete it. The engine (list, threading, resolve, mention composer) lives in `CommentThread.tsx`, shared with\n * the project and doc panels (`TargetComments.tsx`) — this file only wires the work-item-specific GraphQL\n * operations and keeps the Card chrome/title this page already had.\n */\n\nconst WORK_ITEM_COMMENTS = gql`\n query WorkItemComments($workItemId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetWorkItemComments(workItemId: $workItemId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n }\n }\n }\n }\n`;\nconst ADD_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentAdd($input: PlanmagnetAddWorkItemCommentInput!) {\n planmagnetAddWorkItemComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentEdit($id: ID!, $body: String!) {\n planmagnetEditWorkItemComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentDelete($id: ID!) {\n planmagnetDeleteWorkItemComment(id: $id)\n }\n`;\nconst RESOLVE_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentResolve($id: ID!, $resolved: Boolean!) {\n planmagnetResolveWorkItemComment(id: $id, resolved: $resolved) {\n id\n status\n resolvedBy\n resolvedAt\n }\n }\n`;\n\nexport function WorkItemComments({ workItemId }: { workItemId: string }) {\n const [totalCount, setTotalCount] = useState<number | null>(null);\n return (\n <Card data-testid=\"work-item-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Discussion{totalCount !== null ? ` (${totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <CommentThread\n targetNoun=\"task\"\n idPrefix={`work-item-comment-${workItemId}`}\n query={{\n document: WORK_ITEM_COMMENTS,\n dataKey: 'planmagnetWorkItemComments',\n variables: { workItemId },\n skip: !workItemId,\n }}\n add={{\n document: ADD_WORK_ITEM_COMMENT,\n buildVariables: (body, parentId) => ({ input: { workItemId, body, parentId } }),\n }}\n edit={EDIT_WORK_ITEM_COMMENT}\n remove={DELETE_WORK_ITEM_COMMENT}\n resolve={RESOLVE_WORK_ITEM_COMMENT}\n refetchQueries={['WorkItemComments', 'WorkItemActivity']}\n onTotalCount={setTotalCount}\n />\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;AAcA,IAAM,IAAqB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCxB,IAAwB,CAAG;;;;;;GAO3B,IAAyB,CAAG;;;;;;;;GAS5B,IAA2B,CAAG;;;;GAK9B,IAA4B,CAAG;;;;;;;;;;AAWrC,SAAgB,EAAiB,EAAE,iBAAsC;CACvE,IAAM,CAAC,GAAY,KAAiB,EAAwB,KAAK;AACjE,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;IAC1B,MAAe,OAA4B,KAArB,KAAK,EAAW;IACvC;MACD,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GACE,YAAW;GACX,UAAU,qBAAqB;GAC/B,OAAO;IACL,UAAU;IACV,SAAS;IACT,WAAW,EAAE,eAAY;IACzB,MAAM,CAAC;IACR;GACD,KAAK;IACH,UAAU;IACV,iBAAiB,GAAM,OAAc,EAAE,OAAO;KAAE;KAAY;KAAM;KAAU,EAAE;IAC/E;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,oBAAoB,mBAAmB;GACxD,cAAc;GACd,CAAA,EACU,CAAA,CACT"}
1
+ {"version":3,"file":"WorkItemComments.js","names":[],"sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { MessageSquare } from 'lucide-react';\nimport { CommentThread } from './CommentThread';\n\n/**\n * Discussion on a work item (CWF-02 threading, CWF-14 resolution, CWF-13 mentions). Anyone who can read the item\n * sees the thread; posting/replying/resolving need the item's write tier; only a comment's author can edit or\n * delete it. The engine (list, threading, resolve, mention composer) lives in `CommentThread.tsx`, shared with\n * the project and doc panels (`TargetComments.tsx`) — this file only wires the work-item-specific GraphQL\n * operations and keeps the Card chrome/title this page already had.\n */\n\nconst WORK_ITEM_COMMENTS = gql`\n query WorkItemComments($workItemId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetWorkItemComments(workItemId: $workItemId, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n }\n }\n }\n }\n`;\nconst ADD_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentAdd($input: PlanmagnetAddWorkItemCommentInput!) {\n planmagnetAddWorkItemComment(input: $input) {\n id\n }\n }\n`;\nconst EDIT_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentEdit($id: ID!, $body: String!) {\n planmagnetEditWorkItemComment(id: $id, body: $body) {\n id\n body\n editedAt\n }\n }\n`;\nconst DELETE_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentDelete($id: ID!) {\n planmagnetDeleteWorkItemComment(id: $id)\n }\n`;\nconst RESOLVE_WORK_ITEM_COMMENT = gql`\n mutation WorkItemCommentResolve($id: ID!, $resolved: Boolean!) {\n planmagnetResolveWorkItemComment(id: $id, resolved: $resolved) {\n id\n status\n resolvedBy\n resolvedAt\n }\n }\n`;\n\nexport interface WorkItemCommentsPresence {\n typingActorIds: readonly string[];\n notifyTyping: () => void;\n}\n\nexport function WorkItemComments({\n workItemId,\n presence,\n}: {\n workItemId: string;\n /** CWF-24: typing presence from the page's own `usePresence(targetType: 'WORK_ITEM', ...)` call. */\n presence?: WorkItemCommentsPresence;\n}) {\n const [totalCount, setTotalCount] = useState<number | null>(null);\n return (\n <Card data-testid=\"work-item-comments\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"h-4 w-4\" />\n Discussion{totalCount !== null ? ` (${totalCount})` : ''}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <CommentThread\n targetNoun=\"task\"\n idPrefix={`work-item-comment-${workItemId}`}\n query={{\n document: WORK_ITEM_COMMENTS,\n dataKey: 'planmagnetWorkItemComments',\n variables: { workItemId },\n skip: !workItemId,\n }}\n add={{\n document: ADD_WORK_ITEM_COMMENT,\n buildVariables: (body, parentId) => ({ input: { workItemId, body, parentId } }),\n }}\n edit={EDIT_WORK_ITEM_COMMENT}\n remove={DELETE_WORK_ITEM_COMMENT}\n resolve={RESOLVE_WORK_ITEM_COMMENT}\n refetchQueries={['WorkItemComments', 'WorkItemActivity']}\n onTotalCount={setTotalCount}\n presence={presence}\n />\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;AAcA,IAAM,IAAqB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCxB,IAAwB,CAAG;;;;;;GAO3B,IAAyB,CAAG;;;;;;;;GAS5B,IAA2B,CAAG;;;;GAK9B,IAA4B,CAAG;;;;;;;;;;AAgBrC,SAAgB,EAAiB,EAC/B,eACA,eAKC;CACD,IAAM,CAAC,GAAY,KAAiB,EAAwB,KAAK;AACjE,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;IAC1B,MAAe,OAA4B,KAArB,KAAK,EAAW;IACvC;MACD,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GACE,YAAW;GACX,UAAU,qBAAqB;GAC/B,OAAO;IACL,UAAU;IACV,SAAS;IACT,WAAW,EAAE,eAAY;IACzB,MAAM,CAAC;IACR;GACD,KAAK;IACH,UAAU;IACV,iBAAiB,GAAM,OAAc,EAAE,OAAO;KAAE;KAAY;KAAM;KAAU,EAAE;IAC/E;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,oBAAoB,mBAAmB;GACxD,cAAc;GACJ;GACV,CAAA,EACU,CAAA,CACT"}
@@ -4,6 +4,8 @@
4
4
  * Declined/Duplicate tabs could only ever be empty.
5
5
  */
6
6
  export declare const FEATURE_STATUSES: readonly ["SUBMITTED", "UNDER_REVIEW", "PLANNED", "IN_PROGRESS", "COMPLETED", "DECLINED", "DUPLICATE"];
7
+ /** Every query that shows a feature request, its votes or feature counts. */
8
+ export declare const FEATURE_QUERIES: string[];
7
9
  /** Vote for a request, or take your vote back. */
8
10
  export declare function FeatureVoteButton({ featureId, voted }: {
9
11
  featureId: string;
@@ -18,9 +20,18 @@ export interface FeatureValues {
18
20
  effortEstimate: string | null;
19
21
  projectId: string | null;
20
22
  workItemId: string | null;
23
+ /** PRM-27 — optional so existing callers (detail header actions predate PRM-27) need no change. */
24
+ moscow?: string | null;
25
+ kano?: string | null;
26
+ valueScore?: number | null;
27
+ effortScore?: number | null;
28
+ valueEffortQuadrant?: string | null;
21
29
  }
22
30
  /** Status select, Edit and Delete for the feature detail header. */
23
31
  export declare function FeatureHeaderActions({ feature }: {
24
32
  feature: FeatureValues;
25
33
  }): import("react/jsx-runtime").JSX.Element;
34
+ export declare function FeaturePrioritizationCard({ feature }: {
35
+ feature: FeatureValues;
36
+ }): import("react/jsx-runtime").JSX.Element;
26
37
  //# sourceMappingURL=FeatureControls.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"AAuBA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,wGAQnB,CAAC;AA+CX,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,2CA6B5F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA4E3E"}
1
+ {"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"AAoCA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,wGAQnB,CAAC;AAEX,6EAA6E;AAC7E,eAAO,MAAM,eAAe,UAQ3B,CAAC;AAyCF,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,2CA6B5F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA4E3E;AAyOD,wBAAgB,yBAAyB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CAwGhF"}