@burdenoff/microfe-planmagnet 2026.1001.8 → 2026.1002.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/global-types.d.ts +5 -0
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts +16 -0
- package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts.map +1 -0
- package/dist/planmagnet/comments/CommentAttachmentUpload.js +218 -0
- package/dist/planmagnet/comments/CommentAttachmentUpload.js.map +1 -0
- package/dist/planmagnet/comments/CommentAttachments.d.ts +15 -0
- package/dist/planmagnet/comments/CommentAttachments.d.ts.map +1 -0
- package/dist/planmagnet/comments/CommentAttachments.js +74 -0
- package/dist/planmagnet/comments/CommentAttachments.js.map +1 -0
- package/dist/planmagnet/comments/CommentBody.d.ts +11 -0
- package/dist/planmagnet/comments/CommentBody.d.ts.map +1 -0
- package/dist/planmagnet/comments/CommentBody.js +130 -0
- package/dist/planmagnet/comments/CommentBody.js.map +1 -0
- package/dist/planmagnet/comments/CommentThread.d.ts +7 -2
- package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
- package/dist/planmagnet/comments/CommentThread.js +288 -233
- package/dist/planmagnet/comments/CommentThread.js.map +1 -1
- package/dist/planmagnet/comments/MentionComposer.d.ts +0 -7
- package/dist/planmagnet/comments/MentionComposer.d.ts.map +1 -1
- package/dist/planmagnet/comments/MentionComposer.js +225 -86
- package/dist/planmagnet/comments/MentionComposer.js.map +1 -1
- package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/TargetComments.js +44 -6
- package/dist/planmagnet/comments/TargetComments.js.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.js +22 -3
- package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
- package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts +102 -0
- package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts.map +1 -0
- package/dist/planmagnet/comments/commentAttachmentUploadClient.js +298 -0
- package/dist/planmagnet/comments/commentAttachmentUploadClient.js.map +1 -0
- package/dist/planmagnet/comments/commentImageTokens.d.ts +5 -0
- package/dist/planmagnet/comments/commentImageTokens.d.ts.map +1 -0
- package/dist/planmagnet/comments/commentImageTokens.js +14 -0
- package/dist/planmagnet/comments/commentImageTokens.js.map +1 -0
- package/dist/planmagnet/comments/commentTokens.d.ts +31 -0
- package/dist/planmagnet/comments/commentTokens.d.ts.map +1 -0
- package/dist/planmagnet/comments/commentTokens.js +142 -0
- package/dist/planmagnet/comments/commentTokens.js.map +1 -0
- package/dist/planmagnet/comments/fileDisplay.d.ts +22 -0
- package/dist/planmagnet/comments/fileDisplay.d.ts.map +1 -0
- package/dist/planmagnet/comments/fileDisplay.js +48 -0
- package/dist/planmagnet/comments/fileDisplay.js.map +1 -0
- package/dist/planmagnet/comments/useCommentFileUrl.d.ts +22 -0
- package/dist/planmagnet/comments/useCommentFileUrl.d.ts.map +1 -0
- package/dist/planmagnet/comments/useCommentFileUrl.js +54 -0
- package/dist/planmagnet/comments/useCommentFileUrl.js.map +1 -0
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MentionComposer.js","names":[],"sources":["../../../src/planmagnet/comments/MentionComposer.tsx"],"sourcesContent":["import { Fragment, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react';\nimport { useWorkspaceDirectory, type WorkspaceDirectoryMember } from '@burdenoff/fe-libs/sharing';\nimport { Textarea } from '@burdenoff/fe-libs/ui';\nimport { PersonName } from '../people/People';\n\n/**\n * @-mention support (CWF-13), shared by every comment composer — the top-level \"Add a comment\" box, a reply box\n * and an edit box, on all three surfaces this MFE has comments on (task, project, doc; see CommentThread.tsx).\n *\n * A mention is stored as the literal token `@[Name](actorId)` inside the comment body — the server parses it out\n * of the raw text (`PlanmagnetComment.mentions` / `PlanmagnetWorkItemComment.mentions` in graphql/wspace.graphql\n * are populated FROM this token, read-only). There is no separate \"mention\" API call: typing \"@\", picking someone\n * and posting the comment is the whole mechanism.\n *\n * Suggestions reuse the same workspace directory the assignee picker already calls\n * (`useWorkspaceDirectory`, see `controls/WorkItemControls.tsx`'s `useAssigneeOptions`): one paged list of\n * members, filtered case-insensitively client-side by `.search(query)`. The dropdown is anchored to the\n * composer box itself, not to the caret's pixel position — a caret-coordinate \"mirror div\" measurement is the\n * textbook way to do that, but it is fragile across zoom levels, font loading and line-wrap, and not worth it for\n * a box this short; anchoring to the field is the same trade-off GitHub's and Linear's own comment composers make.\n *\n * Mention delivery: the server records `mentions` but does NOT notify anyone yet (BOFF-7927, see the schema doc\n * comment on `mentions`) — this component only inserts the token and later renders it highlighted\n * (`MentionText`), and never implies a mentioned person was paged, emailed or pushed.\n */\n\nconst MAX_SUGGESTIONS = 6;\n/** `@name` trigger: an \"@\" that starts a word (line start or preceded by whitespace), followed by up to 40\n * non-space, non-\"@\" characters up to the caret. Requiring a word boundary before \"@\" keeps this from firing\n * mid-token on something like an email address (\"ada@example.com\"). */\nconst TRIGGER_PATTERN = /(?:^|\\s)@([^\\s@]{0,40})$/;\n\nfunction findTrigger(text: string, caret: number): { start: number; query: string } | null {\n const head = text.slice(0, caret);\n const match = TRIGGER_PATTERN.exec(head);\n if (!match) return null;\n const start = match[0].startsWith('@') ? match.index : match.index + 1;\n return { start, query: match[1] };\n}\n\nexport interface MentionComposerProps {\n id: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n ariaLabel?: string;\n disabled?: boolean;\n /** Merged with the Textarea's own base styling (e.g. `\"min-h-[60px]\"` for a reply box). */\n className?: string;\n autoFocus?: boolean;\n workspaceId?: string;\n authToken?: string;\n apiGatewayUrl?: string;\n}\n\n/** A plain multi-line composer with an @-mention autocomplete overlay. Controlled, like every other composer in\n * this MFE (`value`/`onChange`) — the mention token is spliced into that same string, never a side channel. */\nexport function MentionComposer({\n id,\n value,\n onChange,\n placeholder,\n ariaLabel,\n disabled = false,\n className,\n autoFocus = false,\n workspaceId,\n authToken,\n apiGatewayUrl,\n}: MentionComposerProps) {\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n // Caret position to restore after a mention is spliced in — `value` is controlled by the parent, so the new\n // selection can only be applied once the DOM has the new text (the effect below, keyed on `value`).\n const pendingCaretRef = useRef<number | null>(null);\n const [trigger, setTrigger] = useState<{ start: number; query: string } | null>(null);\n const [highlight, setHighlight] = useState(0);\n\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken,\n apiGatewayUrl,\n });\n const { search, hasMore, fetchMore } = directory;\n\n useEffect(() => {\n search(trigger?.query ?? '');\n }, [trigger?.query, search]);\n\n useEffect(() => {\n if (pendingCaretRef.current !== null && textareaRef.current) {\n const pos = pendingCaretRef.current;\n pendingCaretRef.current = null;\n textareaRef.current.focus();\n textareaRef.current.setSelectionRange(pos, pos);\n }\n }, [value]);\n\n const options = useMemo(\n () => (trigger ? directory.members.slice(0, MAX_SUGGESTIONS) : []),\n [trigger, directory.members]\n );\n const open = trigger !== null && !disabled;\n\n // Resets the highlighted option whenever the trigger changes (new \"@query\", or it closed/opened) — done here,\n // synchronously with the same event that changed it, rather than in an effect keyed on `trigger?.query`\n // (react-hooks/set-state-in-effect: a setState called directly in an effect body cascades an extra render).\n const recomputeTrigger = (text: string, caret: number) => {\n setTrigger(findTrigger(text, caret));\n setHighlight(0);\n };\n\n const insertMention = (member: WorkspaceDirectoryMember) => {\n if (!trigger) return;\n const end = trigger.start + 1 + trigger.query.length;\n const token = `@[${member.name}](${member.id})`;\n const next = `${value.slice(0, trigger.start)}${token} ${value.slice(end)}`;\n pendingCaretRef.current = trigger.start + token.length + 1;\n setTrigger(null);\n onChange(next);\n };\n\n const listId = `${id}-mentions`;\n const activeOption = open ? options[highlight] : undefined;\n\n return (\n <div className=\"relative\">\n <Textarea\n ref={textareaRef}\n id={id}\n aria-label={ariaLabel}\n className={className}\n placeholder={placeholder}\n value={value}\n disabled={disabled}\n autoFocus={autoFocus}\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"list\"\n aria-activedescendant={activeOption ? `${listId}-${activeOption.id}` : undefined}\n onChange={(e) => {\n onChange(e.target.value);\n recomputeTrigger(e.target.value, e.target.selectionStart ?? e.target.value.length);\n }}\n onSelect={(e) => {\n const el = e.currentTarget;\n recomputeTrigger(el.value, el.selectionStart ?? el.value.length);\n }}\n onBlur={() => setTrigger(null)}\n onKeyDown={(e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (!open || options.length === 0) return;\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setHighlight((h) => (h + 1) % options.length);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setHighlight((h) => (h - 1 + options.length) % options.length);\n } else if (e.key === 'Enter' || e.key === 'Tab') {\n // `highlight` can only be stale within the bounds already guarded above (`options.length === 0`\n // returns early); still resolved defensively rather than indexing blind.\n const member = options[highlight] ?? options[0];\n if (!member) return;\n e.preventDefault();\n insertMention(member);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setTrigger(null);\n }\n }}\n />\n {open && (\n <div className=\"absolute z-20 mt-1 w-72 overflow-hidden rounded-md border border-border-default bg-bg-surface text-sm shadow-md\">\n <div\n role=\"listbox\"\n id={listId}\n aria-label=\"Workspace members\"\n data-testid=\"mention-suggestions\"\n className=\"max-h-56 overflow-y-auto p-1\"\n >\n {directory.loading && options.length === 0 && (\n <p className=\"px-2 py-1.5 text-xs text-muted-foreground\">Loading members…</p>\n )}\n {!directory.loading && options.length === 0 && (\n <p className=\"px-2 py-1.5 text-xs text-muted-foreground\">No matching members.</p>\n )}\n {options.map((member, i) => (\n <button\n key={member.id}\n type=\"button\"\n id={`${listId}-${member.id}`}\n role=\"option\"\n aria-selected={i === highlight}\n className={`flex w-full items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-left ${\n i === highlight ? 'bg-primary/10 text-primary' : 'hover:bg-muted'\n }`}\n // mousedown (not click) + preventDefault: keeps focus (and the caret we're about to restore) on\n // the textarea instead of letting the browser blur it into this button first.\n onMouseDown={(e) => {\n e.preventDefault();\n insertMention(member);\n }}\n onMouseEnter={() => setHighlight(i)}\n >\n <span className=\"truncate font-medium\">{member.name}</span>\n {member.email && (\n <span className=\"shrink-0 truncate text-xs text-muted-foreground\">\n {member.email}\n </span>\n )}\n </button>\n ))}\n </div>\n {\n // The directory loads members a page (50) at a time (`useWorkspaceDirectory`, PAGE_SIZE) and\n // `search()` only filters what's already loaded — someone outside the first page is unmentionable\n // until more is fetched. Mirrors the \"Show more members\" affordance the assignee picker and the\n // roadmap reviewer picker already wire onto this same hook (WorkItemControls' AssigneeSelect,\n // RoadmapReviewPanel).\n hasMore && (\n <button\n type=\"button\"\n className=\"w-full border-t border-border-default px-2 py-1.5 text-left text-xs text-primary hover:bg-muted hover:underline\"\n // Same mousedown/preventDefault trick as the option buttons above: fetchMore() must not blur\n // the textarea (which would close the dropdown via onBlur before the click is handled).\n onMouseDown={(e) => {\n e.preventDefault();\n fetchMore();\n }}\n >\n Show more members\n </button>\n )\n }\n </div>\n )}\n <span className=\"sr-only\" aria-live=\"polite\">\n {open\n ? activeOption\n ? `${activeOption.name} highlighted, ${options.length} member${options.length === 1 ? '' : 's'} matching`\n : directory.loading\n ? 'Loading members'\n : 'No matching members'\n : ''}\n </span>\n </div>\n );\n}\n\nconst MENTION_PATTERN = /@\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g;\n\n/** A comment body as displayed text, with `@[Name](actorId)` tokens rendered as highlighted, live-resolved names\n * instead of the raw markup (CWF-13). `actorId` drives the rendering via `PersonName` so a since-renamed member\n * shows their current name, not the one captured when the mention was typed. */\nexport function MentionText({ text, className }: { text: string; className?: string }) {\n const parts: Array<string | { name: string; actorId: string }> = [];\n let last = 0;\n for (const match of text.matchAll(MENTION_PATTERN)) {\n const index = match.index ?? 0;\n if (index > last) parts.push(text.slice(last, index));\n parts.push({ name: match[1], actorId: match[2] });\n last = index + match[0].length;\n }\n if (last < text.length) parts.push(text.slice(last));\n\n return (\n <p className={className ?? 'text-sm whitespace-pre-wrap break-words'}>\n {parts.map((part, i) =>\n typeof part === 'string' ? (\n <Fragment key={i}>{part}</Fragment>\n ) : (\n <span key={i} className=\"rounded bg-primary/10 px-1 py-0.5 font-medium text-primary\">\n @<PersonName id={part.actorId} className=\"text-primary\" />\n </span>\n )\n )}\n </p>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MentionComposer.js","names":[],"sources":["../../../src/planmagnet/comments/MentionComposer.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';\nimport { useWorkspaceDirectory, type WorkspaceDirectoryMember } from '@burdenoff/fe-libs/sharing';\nimport {\n Button,\n Input,\n Label,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { Bold, Code2, Italic, Link2 } from 'lucide-react';\n\n/**\n * @-mention support (CWF-13) and the CWF-12 formatting toolbar (bold, italic, code, link), shared by every\n * comment composer — the top-level \"Add a comment\" box, a reply box and an edit box, on all three surfaces this\n * MFE has comments on (task, project, doc; see CommentThread.tsx).\n *\n * A mention is stored as the literal token `@[Name](actorId)` inside the comment body — the server parses it out\n * of the raw text (`PlanmagnetComment.mentions` / `PlanmagnetWorkItemComment.mentions` in graphql/wspace.graphql\n * are populated FROM this token, read-only). There is no separate \"mention\" API call: typing \"@\", picking someone\n * and posting the comment is the whole mechanism.\n *\n * Suggestions reuse the same workspace directory the assignee picker already calls\n * (`useWorkspaceDirectory`, see `controls/WorkItemControls.tsx`'s `useAssigneeOptions`): one paged list of\n * members, filtered case-insensitively client-side by `.search(query)`. The dropdown is anchored to the\n * composer box itself, not to the caret's pixel position — a caret-coordinate \"mirror div\" measurement is the\n * textbook way to do that, but it is fragile across zoom levels, font loading and line-wrap, and not worth it for\n * a box this short; anchoring to the field is the same trade-off GitHub's and Linear's own comment composers make.\n *\n * Mention delivery: the server records `mentions` but does NOT notify anyone yet (BOFF-7927, see the schema doc\n * comment on `mentions`) — this component only inserts the token and later renders it highlighted\n * (`CommentBody.tsx`), and never implies a mentioned person was paged, emailed or pushed.\n *\n * The toolbar inserts the same markdown-shaped tokens `CommentBody.tsx` renders and\n * wspace-planmagnet-svc's `rich-text-sanitizer.ts` accepts on write — it does not itself sanitize anything (the\n * server does, on every add/edit mutation); it only spares the author from memorizing the syntax. The Link\n * button refuses anything that isn't an `https://` url client-side too, so a typo never round-trips through the\n * server only to come back degraded to inert \"blocked link\" text.\n */\n\nconst MAX_SUGGESTIONS = 6;\n/** `@name` trigger: an \"@\" that starts a word (line start or preceded by whitespace), followed by up to 40\n * non-space, non-\"@\" characters up to the caret. Requiring a word boundary before \"@\" keeps this from firing\n * mid-token on something like an email address (\"ada@example.com\"). */\nconst TRIGGER_PATTERN = /(?:^|\\s)@([^\\s@]{0,40})$/;\n\nfunction findTrigger(text: string, caret: number): { start: number; query: string } | null {\n const head = text.slice(0, caret);\n const match = TRIGGER_PATTERN.exec(head);\n if (!match) return null;\n const start = match[0].startsWith('@') ? match.index : match.index + 1;\n return { start, query: match[1] };\n}\n\nexport interface MentionComposerProps {\n id: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n ariaLabel?: string;\n disabled?: boolean;\n /** Merged with the Textarea's own base styling (e.g. `\"min-h-[60px]\"` for a reply box). */\n className?: string;\n autoFocus?: boolean;\n workspaceId?: string;\n authToken?: string;\n apiGatewayUrl?: string;\n}\n\n/** A plain multi-line composer with an @-mention autocomplete overlay. Controlled, like every other composer in\n * this MFE (`value`/`onChange`) — the mention token is spliced into that same string, never a side channel. */\nexport function MentionComposer({\n id,\n value,\n onChange,\n placeholder,\n ariaLabel,\n disabled = false,\n className,\n autoFocus = false,\n workspaceId,\n authToken,\n apiGatewayUrl,\n}: MentionComposerProps) {\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n // Selection to restore after a mention/toolbar insertion splices the text — `value` is controlled by the\n // parent, so the new selection can only be applied once the DOM has the new text (the effect below, keyed on\n // `value`). A range rather than a bare caret position so a toolbar wrap (e.g. **bold text**) can leave the\n // inserted placeholder word SELECTED — the author can just start typing over it, same as every other editor's\n // toolbar — while a mention insertion (a collapsed `start === end` range) still lands the caret as before.\n const pendingSelectionRef = useRef<{ start: number; end: number } | null>(null);\n const [trigger, setTrigger] = useState<{ start: number; query: string } | null>(null);\n const [highlight, setHighlight] = useState(0);\n const [linkOpen, setLinkOpen] = useState(false);\n const [linkUrl, setLinkUrl] = useState('');\n const [linkLabel, setLinkLabel] = useState('');\n const [linkError, setLinkError] = useState<string | null>(null);\n\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken,\n apiGatewayUrl,\n });\n const { search, hasMore, fetchMore } = directory;\n\n useEffect(() => {\n search(trigger?.query ?? '');\n }, [trigger?.query, search]);\n\n useEffect(() => {\n if (pendingSelectionRef.current && textareaRef.current) {\n const { start, end } = pendingSelectionRef.current;\n pendingSelectionRef.current = null;\n textareaRef.current.focus();\n textareaRef.current.setSelectionRange(start, end);\n }\n }, [value]);\n\n const options = useMemo(\n () => (trigger ? directory.members.slice(0, MAX_SUGGESTIONS) : []),\n [trigger, directory.members]\n );\n const open = trigger !== null && !disabled;\n\n // Resets the highlighted option whenever the trigger changes (new \"@query\", or it closed/opened) — done here,\n // synchronously with the same event that changed it, rather than in an effect keyed on `trigger?.query`\n // (react-hooks/set-state-in-effect: a setState called directly in an effect body cascades an extra render).\n const recomputeTrigger = (text: string, caret: number) => {\n setTrigger(findTrigger(text, caret));\n setHighlight(0);\n };\n\n const insertMention = (member: WorkspaceDirectoryMember) => {\n if (!trigger) return;\n const end = trigger.start + 1 + trigger.query.length;\n const token = `@[${member.name}](${member.id})`;\n const next = `${value.slice(0, trigger.start)}${token} ${value.slice(end)}`;\n const caret = trigger.start + token.length + 1;\n pendingSelectionRef.current = { start: caret, end: caret };\n setTrigger(null);\n onChange(next);\n };\n\n /** Current selection in the textarea (or a collapsed caret at the end of the value when the ref isn't mounted\n * yet — defensive only, the textarea is always mounted by the time a toolbar button is clickable). */\n const currentSelection = () => {\n const el = textareaRef.current;\n return { start: el?.selectionStart ?? value.length, end: el?.selectionEnd ?? value.length };\n };\n\n /** Wraps the current selection (or inserts `placeholder`, pre-selected, when nothing is selected) in\n * `prefix`/`suffix` — the shared mechanic behind the Bold/Italic/Code toolbar buttons. Leaves the wrapped\n * text selected afterwards so typing immediately replaces it, same as every other rich-text toolbar. */\n const wrapSelection = (prefix: string, suffix: string, placeholder: string) => {\n const { start, end } = currentSelection();\n const hasSelection = end > start;\n const inner = hasSelection ? value.slice(start, end) : placeholder;\n const next = `${value.slice(0, start)}${prefix}${inner}${suffix}${value.slice(end)}`;\n pendingSelectionRef.current = {\n start: start + prefix.length,\n end: start + prefix.length + inner.length,\n };\n setTrigger(null);\n onChange(next);\n };\n\n const applyCode = () => {\n const { start, end } = currentSelection();\n // A multi-line selection becomes a fenced block (the inline-code shape in the grammar explicitly excludes\n // `\\n` — wspace-planmagnet-svc's INLINE_CODE_RE is `` [^`\\n]{1,500} `` — so wrapping multi-line text in\n // single backticks would post a body the server itself cannot round-trip as one inline-code span).\n if (value.slice(start, end).includes('\\n')) {\n wrapSelection('```\\n', '\\n```', 'code');\n } else {\n wrapSelection('`', '`', 'code');\n }\n };\n\n const openLinkPopover = () => {\n const { start, end } = currentSelection();\n setLinkLabel(value.slice(start, end));\n setLinkUrl('');\n setLinkError(null);\n setLinkOpen(true);\n };\n\n const submitLink = () => {\n const url = linkUrl.trim();\n // Same allow-list wspace-planmagnet-svc's sanitizer enforces (HTTPS_URL_RE) — refusing a non-https link\n // here means the author sees why immediately, instead of posting and getting back inert \"blocked link\" text.\n if (!/^https:\\/\\/[^\\s<>\"'`]+$/i.test(url)) {\n setLinkError('Enter a link starting with https://');\n return;\n }\n const { start, end } = currentSelection();\n const label = linkLabel.trim() || url;\n if (/[[\\]]/.test(label)) {\n setLinkError('Link text cannot contain [ or ].');\n return;\n }\n const token = `[${label}](${url})`;\n const next = `${value.slice(0, start)}${token}${value.slice(end)}`;\n const caret = start + token.length;\n pendingSelectionRef.current = { start: caret, end: caret };\n setTrigger(null);\n onChange(next);\n setLinkOpen(false);\n setLinkUrl('');\n setLinkLabel('');\n setLinkError(null);\n };\n\n const listId = `${id}-mentions`;\n const activeOption = open ? options[highlight] : undefined;\n\n return (\n <div className=\"relative\">\n <div\n role=\"toolbar\"\n aria-label=\"Formatting\"\n aria-controls={id}\n className=\"mb-1 flex items-center gap-0.5\"\n >\n <ToolbarButton\n icon={<Bold className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n label=\"Bold\"\n disabled={disabled}\n onClick={() => wrapSelection('**', '**', 'bold text')}\n />\n <ToolbarButton\n icon={<Italic className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n label=\"Italic\"\n disabled={disabled}\n onClick={() => wrapSelection('_', '_', 'italic text')}\n />\n <ToolbarButton\n icon={<Code2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n label=\"Code\"\n disabled={disabled}\n onClick={applyCode}\n />\n <Popover\n open={linkOpen}\n onOpenChange={(isOpen) => {\n setLinkOpen(isOpen);\n if (!isOpen) setLinkError(null);\n }}\n >\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 w-6 p-0\"\n aria-label=\"Link\"\n title=\"Link\"\n disabled={disabled}\n onClick={openLinkPopover}\n >\n <Link2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-72 space-y-2\" align=\"start\">\n <div className=\"space-y-1\">\n <Label htmlFor={`${id}-link-url`}>Link URL</Label>\n <Input\n id={`${id}-link-url`}\n placeholder=\"https://example.com\"\n value={linkUrl}\n autoFocus\n onChange={(e) => {\n setLinkUrl(e.target.value);\n setLinkError(null);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n submitLink();\n }\n }}\n />\n </div>\n <div className=\"space-y-1\">\n <Label htmlFor={`${id}-link-label`}>Link text (optional)</Label>\n <Input\n id={`${id}-link-label`}\n placeholder={linkUrl || 'Link text'}\n value={linkLabel}\n onChange={(e) => setLinkLabel(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n submitLink();\n }\n }}\n />\n </div>\n {linkError && <p className=\"text-xs text-destructive\">{linkError}</p>}\n <div className=\"flex justify-end\">\n <Button type=\"button\" size=\"sm\" onClick={submitLink} disabled={!linkUrl.trim()}>\n Insert link\n </Button>\n </div>\n </PopoverContent>\n </Popover>\n </div>\n <Textarea\n ref={textareaRef}\n id={id}\n aria-label={ariaLabel}\n className={className}\n placeholder={placeholder}\n value={value}\n disabled={disabled}\n autoFocus={autoFocus}\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"list\"\n aria-activedescendant={activeOption ? `${listId}-${activeOption.id}` : undefined}\n onChange={(e) => {\n onChange(e.target.value);\n recomputeTrigger(e.target.value, e.target.selectionStart ?? e.target.value.length);\n }}\n onSelect={(e) => {\n const el = e.currentTarget;\n recomputeTrigger(el.value, el.selectionStart ?? el.value.length);\n }}\n onBlur={() => setTrigger(null)}\n onKeyDown={(e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (!open || options.length === 0) return;\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setHighlight((h) => (h + 1) % options.length);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setHighlight((h) => (h - 1 + options.length) % options.length);\n } else if (e.key === 'Enter' || e.key === 'Tab') {\n // `highlight` can only be stale within the bounds already guarded above (`options.length === 0`\n // returns early); still resolved defensively rather than indexing blind.\n const member = options[highlight] ?? options[0];\n if (!member) return;\n e.preventDefault();\n insertMention(member);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n setTrigger(null);\n }\n }}\n />\n {open && (\n <div className=\"absolute z-20 mt-1 w-72 overflow-hidden rounded-md border border-border-default bg-bg-surface text-sm shadow-md\">\n <div\n role=\"listbox\"\n id={listId}\n aria-label=\"Workspace members\"\n data-testid=\"mention-suggestions\"\n className=\"max-h-56 overflow-y-auto p-1\"\n >\n {directory.loading && options.length === 0 && (\n <p className=\"px-2 py-1.5 text-xs text-muted-foreground\">Loading members…</p>\n )}\n {!directory.loading && options.length === 0 && (\n <p className=\"px-2 py-1.5 text-xs text-muted-foreground\">No matching members.</p>\n )}\n {options.map((member, i) => (\n <button\n key={member.id}\n type=\"button\"\n id={`${listId}-${member.id}`}\n role=\"option\"\n aria-selected={i === highlight}\n className={`flex w-full items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-left ${\n i === highlight ? 'bg-primary/10 text-primary' : 'hover:bg-muted'\n }`}\n // mousedown (not click) + preventDefault: keeps focus (and the caret we're about to restore) on\n // the textarea instead of letting the browser blur it into this button first.\n onMouseDown={(e) => {\n e.preventDefault();\n insertMention(member);\n }}\n onMouseEnter={() => setHighlight(i)}\n >\n <span className=\"truncate font-medium\">{member.name}</span>\n {member.email && (\n <span className=\"shrink-0 truncate text-xs text-muted-foreground\">\n {member.email}\n </span>\n )}\n </button>\n ))}\n </div>\n {\n // The directory loads members a page (50) at a time (`useWorkspaceDirectory`, PAGE_SIZE) and\n // `search()` only filters what's already loaded — someone outside the first page is unmentionable\n // until more is fetched. Mirrors the \"Show more members\" affordance the assignee picker and the\n // roadmap reviewer picker already wire onto this same hook (WorkItemControls' AssigneeSelect,\n // RoadmapReviewPanel).\n hasMore && (\n <button\n type=\"button\"\n className=\"w-full border-t border-border-default px-2 py-1.5 text-left text-xs text-primary hover:bg-muted hover:underline\"\n // Same mousedown/preventDefault trick as the option buttons above: fetchMore() must not blur\n // the textarea (which would close the dropdown via onBlur before the click is handled).\n onMouseDown={(e) => {\n e.preventDefault();\n fetchMore();\n }}\n >\n Show more members\n </button>\n )\n }\n </div>\n )}\n <span className=\"sr-only\" aria-live=\"polite\">\n {open\n ? activeOption\n ? `${activeOption.name} highlighted, ${options.length} member${options.length === 1 ? '' : 's'} matching`\n : directory.loading\n ? 'Loading members'\n : 'No matching members'\n : ''}\n </span>\n </div>\n );\n}\n\n/** One formatting-toolbar action: an icon-only ghost button sized to match the rest of this thread's compact\n * controls (the Edit/Reply/Resolve row in CommentThread.tsx is `h-6`). `label` is the only accessible name — the\n * icon is always `aria-hidden`. */\nfunction ToolbarButton({\n icon,\n label,\n onClick,\n disabled,\n}: {\n icon: ReactNode;\n label: string;\n onClick: () => void;\n disabled?: boolean;\n}) {\n return (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 w-6 p-0\"\n aria-label={label}\n title={label}\n disabled={disabled}\n onClick={onClick}\n >\n {icon}\n </Button>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TargetComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/TargetComments.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TargetComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/TargetComments.tsx"],"names":[],"mappings":"AAkGA,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,2CA2CA;AAmFD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC,2CA2CA"}
|
|
@@ -24,6 +24,13 @@ var u = r`
|
|
|
24
24
|
resolvedBy
|
|
25
25
|
resolvedAt
|
|
26
26
|
mentions
|
|
27
|
+
attachments {
|
|
28
|
+
id
|
|
29
|
+
fileId
|
|
30
|
+
name
|
|
31
|
+
mimeType
|
|
32
|
+
size
|
|
33
|
+
}
|
|
27
34
|
replies {
|
|
28
35
|
id
|
|
29
36
|
authorId
|
|
@@ -35,6 +42,13 @@ var u = r`
|
|
|
35
42
|
resolvedBy
|
|
36
43
|
resolvedAt
|
|
37
44
|
mentions
|
|
45
|
+
attachments {
|
|
46
|
+
id
|
|
47
|
+
fileId
|
|
48
|
+
name
|
|
49
|
+
mimeType
|
|
50
|
+
size
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
}
|
|
@@ -79,6 +93,10 @@ function h({ projectId: r, presence: h }) {
|
|
|
79
93
|
g === null ? "" : ` (${g})`
|
|
80
94
|
]
|
|
81
95
|
}) }), /* @__PURE__ */ i(s, { children: /* @__PURE__ */ i(e, {
|
|
96
|
+
target: {
|
|
97
|
+
type: "PROJECT",
|
|
98
|
+
id: r
|
|
99
|
+
},
|
|
82
100
|
targetNoun: "project",
|
|
83
101
|
idPrefix: `project-comment-${r}`,
|
|
84
102
|
query: {
|
|
@@ -89,10 +107,11 @@ function h({ projectId: r, presence: h }) {
|
|
|
89
107
|
},
|
|
90
108
|
add: {
|
|
91
109
|
document: d,
|
|
92
|
-
buildVariables: (e, t) => ({ input: {
|
|
110
|
+
buildVariables: (e, t, n) => ({ input: {
|
|
93
111
|
projectId: r,
|
|
94
112
|
body: e,
|
|
95
|
-
parentId: t
|
|
113
|
+
parentId: t,
|
|
114
|
+
...n.length > 0 ? { attachmentFileIds: n } : {}
|
|
96
115
|
} })
|
|
97
116
|
},
|
|
98
117
|
edit: f,
|
|
@@ -101,7 +120,7 @@ function h({ projectId: r, presence: h }) {
|
|
|
101
120
|
refetchQueries: ["ProjectComments"],
|
|
102
121
|
onTotalCount: _,
|
|
103
122
|
presence: h
|
|
104
|
-
}) })]
|
|
123
|
+
}, r) })]
|
|
105
124
|
});
|
|
106
125
|
}
|
|
107
126
|
var g = r`
|
|
@@ -123,6 +142,13 @@ var g = r`
|
|
|
123
142
|
resolvedBy
|
|
124
143
|
resolvedAt
|
|
125
144
|
mentions
|
|
145
|
+
attachments {
|
|
146
|
+
id
|
|
147
|
+
fileId
|
|
148
|
+
name
|
|
149
|
+
mimeType
|
|
150
|
+
size
|
|
151
|
+
}
|
|
126
152
|
replies {
|
|
127
153
|
id
|
|
128
154
|
authorId
|
|
@@ -134,6 +160,13 @@ var g = r`
|
|
|
134
160
|
resolvedBy
|
|
135
161
|
resolvedAt
|
|
136
162
|
mentions
|
|
163
|
+
attachments {
|
|
164
|
+
id
|
|
165
|
+
fileId
|
|
166
|
+
name
|
|
167
|
+
mimeType
|
|
168
|
+
size
|
|
169
|
+
}
|
|
137
170
|
}
|
|
138
171
|
}
|
|
139
172
|
}
|
|
@@ -178,6 +211,10 @@ function x({ docId: r, presence: u }) {
|
|
|
178
211
|
d === null ? "" : ` (${d})`
|
|
179
212
|
]
|
|
180
213
|
}) }), /* @__PURE__ */ i(s, { children: /* @__PURE__ */ i(e, {
|
|
214
|
+
target: {
|
|
215
|
+
type: "DOC",
|
|
216
|
+
id: r
|
|
217
|
+
},
|
|
181
218
|
targetNoun: "doc",
|
|
182
219
|
idPrefix: `doc-comment-${r}`,
|
|
183
220
|
query: {
|
|
@@ -188,10 +225,11 @@ function x({ docId: r, presence: u }) {
|
|
|
188
225
|
},
|
|
189
226
|
add: {
|
|
190
227
|
document: _,
|
|
191
|
-
buildVariables: (e, t) => ({ input: {
|
|
228
|
+
buildVariables: (e, t, n) => ({ input: {
|
|
192
229
|
docId: r,
|
|
193
230
|
body: e,
|
|
194
|
-
parentId: t
|
|
231
|
+
parentId: t,
|
|
232
|
+
...n.length > 0 ? { attachmentFileIds: n } : {}
|
|
195
233
|
} })
|
|
196
234
|
},
|
|
197
235
|
edit: v,
|
|
@@ -200,7 +238,7 @@ function x({ docId: r, presence: u }) {
|
|
|
200
238
|
refetchQueries: ["DocComments"],
|
|
201
239
|
onTotalCount: f,
|
|
202
240
|
presence: u
|
|
203
|
-
}) })]
|
|
241
|
+
}, r) })]
|
|
204
242
|
});
|
|
205
243
|
}
|
|
206
244
|
//#endregion
|
|
@@ -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 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) => ({
|
|
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 attachments {\n id\n fileId\n name\n mimeType\n size\n }\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n attachments {\n id\n fileId\n name\n mimeType\n size\n }\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 key={projectId}\n target={{ type: 'PROJECT', id: projectId }}\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, attachmentFileIds) => ({\n input: {\n projectId,\n body,\n parentId,\n ...(attachmentFileIds.length > 0 ? { attachmentFileIds } : {}),\n },\n }),\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 attachments {\n id\n fileId\n name\n mimeType\n size\n }\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n attachments {\n id\n fileId\n name\n mimeType\n size\n }\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 key={docId}\n target={{ type: 'DOC', id: docId }}\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, attachmentFileIds) => ({\n input: {\n docId,\n body,\n parentId,\n ...(attachmentFileIds.length > 0 ? { attachmentFileIds } : {}),\n },\n }),\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDtB,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;GAEE,QAAQ;IAAE,MAAM;IAAW,IAAI;IAAW;GAC1C,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,GAAU,OAAuB,EACtD,OAAO;KACL;KACA;KACA;KACA,GAAI,EAAkB,SAAS,IAAI,EAAE,sBAAmB,GAAG,EAAE;KAC9D,EACF;IACF;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,kBAAkB;GACnC,cAAc;GACJ;GACV,EA3BK,EA2BL,EACU,CAAA,CACT;;;AAIX,IAAM,IAAe,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDlB,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;GAEE,QAAQ;IAAE,MAAM;IAAO,IAAI;IAAO;GAClC,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,GAAU,OAAuB,EACtD,OAAO;KACL;KACA;KACA;KACA,GAAI,EAAkB,SAAS,IAAI,EAAE,sBAAmB,GAAG,EAAE;KAC9D,EACF;IACF;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,cAAc;GAC/B,cAAc;GACJ;GACV,EA3BK,EA2BL,EACU,CAAA,CACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkItemComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"WorkItemComments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/WorkItemComments.tsx"],"names":[],"mappings":"AA+FA,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,2CA2CA"}
|
|
@@ -24,6 +24,13 @@ var u = r`
|
|
|
24
24
|
resolvedBy
|
|
25
25
|
resolvedAt
|
|
26
26
|
mentions
|
|
27
|
+
attachments {
|
|
28
|
+
id
|
|
29
|
+
fileId
|
|
30
|
+
name
|
|
31
|
+
mimeType
|
|
32
|
+
size
|
|
33
|
+
}
|
|
27
34
|
replies {
|
|
28
35
|
id
|
|
29
36
|
authorId
|
|
@@ -35,6 +42,13 @@ var u = r`
|
|
|
35
42
|
resolvedBy
|
|
36
43
|
resolvedAt
|
|
37
44
|
mentions
|
|
45
|
+
attachments {
|
|
46
|
+
id
|
|
47
|
+
fileId
|
|
48
|
+
name
|
|
49
|
+
mimeType
|
|
50
|
+
size
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
}
|
|
@@ -79,6 +93,10 @@ function h({ workItemId: r, presence: h }) {
|
|
|
79
93
|
g === null ? "" : ` (${g})`
|
|
80
94
|
]
|
|
81
95
|
}) }), /* @__PURE__ */ i(s, { children: /* @__PURE__ */ i(e, {
|
|
96
|
+
target: {
|
|
97
|
+
type: "WORK_ITEM",
|
|
98
|
+
id: r
|
|
99
|
+
},
|
|
82
100
|
targetNoun: "task",
|
|
83
101
|
idPrefix: `work-item-comment-${r}`,
|
|
84
102
|
query: {
|
|
@@ -89,10 +107,11 @@ function h({ workItemId: r, presence: h }) {
|
|
|
89
107
|
},
|
|
90
108
|
add: {
|
|
91
109
|
document: d,
|
|
92
|
-
buildVariables: (e, t) => ({ input: {
|
|
110
|
+
buildVariables: (e, t, n) => ({ input: {
|
|
93
111
|
workItemId: r,
|
|
94
112
|
body: e,
|
|
95
|
-
parentId: t
|
|
113
|
+
parentId: t,
|
|
114
|
+
...n.length > 0 ? { attachmentFileIds: n } : {}
|
|
96
115
|
} })
|
|
97
116
|
},
|
|
98
117
|
edit: f,
|
|
@@ -101,7 +120,7 @@ function h({ workItemId: r, presence: h }) {
|
|
|
101
120
|
refetchQueries: ["WorkItemComments", "WorkItemActivity"],
|
|
102
121
|
onTotalCount: _,
|
|
103
122
|
presence: h
|
|
104
|
-
}) })]
|
|
123
|
+
}, r) })]
|
|
105
124
|
});
|
|
106
125
|
}
|
|
107
126
|
//#endregion
|
|
@@ -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 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) => ({
|
|
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 attachments {\n id\n fileId\n name\n mimeType\n size\n }\n replies {\n id\n authorId\n body\n createdAt\n editedAt\n parentId\n status\n resolvedBy\n resolvedAt\n mentions\n attachments {\n id\n fileId\n name\n mimeType\n size\n }\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 key={workItemId}\n target={{ type: 'WORK_ITEM', id: workItemId }}\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, attachmentFileIds) => ({\n input: {\n workItemId,\n body,\n parentId,\n ...(attachmentFileIds.length > 0 ? { attachmentFileIds } : {}),\n },\n }),\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDxB,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;GAEE,QAAQ;IAAE,MAAM;IAAa,IAAI;IAAY;GAC7C,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,GAAU,OAAuB,EACtD,OAAO;KACL;KACA;KACA;KACA,GAAI,EAAkB,SAAS,IAAI,EAAE,sBAAmB,GAAG,EAAE;KAC9D,EACF;IACF;GACD,MAAM;GACN,QAAQ;GACR,SAAS;GACT,gBAAgB,CAAC,oBAAoB,mBAAmB;GACxD,cAAc;GACJ;GACV,EA3BK,EA2BL,EACU,CAAA,CACT"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ApolloClient } from '@apollo/client';
|
|
2
|
+
|
|
3
|
+
/** The Files service switches from a direct PUT to S3 multipart strictly above 5 MiB. */
|
|
4
|
+
export declare const DIRECT_UPLOAD_LIMIT_BYTES: number;
|
|
5
|
+
export declare const MAX_COMMENT_ATTACHMENT_BYTES: number;
|
|
6
|
+
export interface UploadSession {
|
|
7
|
+
id: string;
|
|
8
|
+
startedById: string;
|
|
9
|
+
status: string;
|
|
10
|
+
uploadType: string;
|
|
11
|
+
partSize: number;
|
|
12
|
+
totalParts: number;
|
|
13
|
+
}
|
|
14
|
+
interface UploadInitiation {
|
|
15
|
+
session: UploadSession | null;
|
|
16
|
+
uploadUrl: string | null;
|
|
17
|
+
}
|
|
18
|
+
interface PresignedPartUrl {
|
|
19
|
+
partNumber: number;
|
|
20
|
+
url: string;
|
|
21
|
+
}
|
|
22
|
+
interface RegisteredPart {
|
|
23
|
+
session: {
|
|
24
|
+
id: string;
|
|
25
|
+
status: string;
|
|
26
|
+
uploadedParts: number;
|
|
27
|
+
} | null;
|
|
28
|
+
}
|
|
29
|
+
interface CompletedUpload {
|
|
30
|
+
session: {
|
|
31
|
+
status: string;
|
|
32
|
+
} | null;
|
|
33
|
+
file: {
|
|
34
|
+
id: string;
|
|
35
|
+
folderId: string | null;
|
|
36
|
+
visibility: string;
|
|
37
|
+
uploadedById: string;
|
|
38
|
+
} | null;
|
|
39
|
+
}
|
|
40
|
+
interface PutResult {
|
|
41
|
+
status: number;
|
|
42
|
+
etag: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** Injectable, non-React Files protocol; its unit tests run without an Apollo provider or DOM. */
|
|
45
|
+
export interface CommentAttachmentUploadTransport {
|
|
46
|
+
initiate(input: {
|
|
47
|
+
fileName: string;
|
|
48
|
+
fileSize: string;
|
|
49
|
+
mimeType: string;
|
|
50
|
+
extension: string;
|
|
51
|
+
visibility: 'PRIVATE';
|
|
52
|
+
}, signal: AbortSignal): Promise<UploadInitiation | null>;
|
|
53
|
+
partUrls(sessionId: string, partNumbers: number[], signal: AbortSignal): Promise<PresignedPartUrl[] | null>;
|
|
54
|
+
put(url: string, bytes: Blob, mimeType: string, signal: AbortSignal): Promise<PutResult>;
|
|
55
|
+
registerPart(input: {
|
|
56
|
+
sessionId: string;
|
|
57
|
+
partNumber: number;
|
|
58
|
+
etag: string;
|
|
59
|
+
}, signal: AbortSignal): Promise<RegisteredPart | null>;
|
|
60
|
+
complete(input: {
|
|
61
|
+
sessionId: string;
|
|
62
|
+
fileSize: string;
|
|
63
|
+
}, signal: AbortSignal): Promise<CompletedUpload | null>;
|
|
64
|
+
/** Deliberately does not use the canceled PUT's signal: cleanup still needs to reach Files. */
|
|
65
|
+
abort(sessionId: string): Promise<boolean>;
|
|
66
|
+
/** Injectable delay keeps retry tests deterministic. */
|
|
67
|
+
wait?(ms: number, signal: AbortSignal): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** Keep only server IDs, not presigned URLs, for a safe retry after an ambiguous completion. */
|
|
70
|
+
export interface CompletionSession {
|
|
71
|
+
sessionId: string;
|
|
72
|
+
startedById: string;
|
|
73
|
+
fileSize: number;
|
|
74
|
+
}
|
|
75
|
+
export declare class UploadCompletionUncertainError extends Error {
|
|
76
|
+
readonly session: CompletionSession;
|
|
77
|
+
readonly quotaLimited: boolean;
|
|
78
|
+
constructor(session: CompletionSession, quotaLimited?: boolean);
|
|
79
|
+
}
|
|
80
|
+
export declare class UploadCanceledError extends Error {
|
|
81
|
+
constructor();
|
|
82
|
+
}
|
|
83
|
+
/** Never reflect an object-store or Apollo exception that might contain a signed URL to the UI. */
|
|
84
|
+
export declare function commentUploadErrorMessage(error: unknown): string;
|
|
85
|
+
/** A pure byte-range calculation. Parts are 1-indexed and the final part may be shorter. */
|
|
86
|
+
export declare function attachmentPartRange(fileSize: number, partSize: number, partNumber: number): {
|
|
87
|
+
start: number;
|
|
88
|
+
end: number;
|
|
89
|
+
};
|
|
90
|
+
/** Completes only the original session: completeUpload is idempotent server-side. */
|
|
91
|
+
export declare function retryCommentAttachmentCompletion(session: CompletionSession, transport: CommentAttachmentUploadTransport, signal: AbortSignal): Promise<string>;
|
|
92
|
+
/**
|
|
93
|
+
* Private-root uploads only: no shared path, no folderId, and no broad workspace visibility.
|
|
94
|
+
* Once completion has been sent its outcome may be ambiguous; NEVER abort that session (the
|
|
95
|
+
* backend's abortUpload can mark even an already-FINALIZED session ABORTED). Retry completion
|
|
96
|
+
* on the same session instead. Prior failures and canceled PUTs are aborted best-effort.
|
|
97
|
+
*/
|
|
98
|
+
export declare function uploadCommentAttachment(file: File, transport: CommentAttachmentUploadTransport, signal: AbortSignal, onCompleting?: () => void): Promise<string>;
|
|
99
|
+
/** The host shell owns the Apollo provider (and its workspace/auth headers). */
|
|
100
|
+
export declare function createCommentAttachmentUploadTransport(client: ApolloClient): CommentAttachmentUploadTransport;
|
|
101
|
+
export {};
|
|
102
|
+
//# sourceMappingURL=commentAttachmentUploadClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commentAttachmentUploadClient.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/commentAttachmentUploadClient.ts"],"names":[],"mappings":"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"}
|