@burdenoff/microfe-planmagnet 2026.930.24 → 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.
- package/dist/planmagnet/comments/CommentThread.d.ts +8 -1
- package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
- package/dist/planmagnet/comments/CommentThread.js +188 -181
- package/dist/planmagnet/comments/CommentThread.js.map +1 -1
- package/dist/planmagnet/comments/TargetComments.d.ts +10 -2
- package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/TargetComments.js +10 -8
- package/dist/planmagnet/comments/TargetComments.js.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.d.ts +7 -1
- package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
- package/dist/planmagnet/comments/WorkItemComments.js +5 -4
- package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts +8 -0
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts.map +1 -0
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js +217 -0
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js.map +1 -0
- package/dist/planmagnet/docs/DocDiffView.d.ts +26 -0
- package/dist/planmagnet/docs/DocDiffView.d.ts.map +1 -0
- package/dist/planmagnet/docs/DocDiffView.js +52 -0
- package/dist/planmagnet/docs/DocDiffView.js.map +1 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts +13 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts.map +1 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.js +308 -0
- package/dist/planmagnet/docs/DocSuggestionsPanel.js.map +1 -0
- package/dist/planmagnet/docs/DocVersionCompare.d.ts +7 -0
- package/dist/planmagnet/docs/DocVersionCompare.d.ts.map +1 -0
- package/dist/planmagnet/docs/DocVersionCompare.js +105 -0
- package/dist/planmagnet/docs/DocVersionCompare.js.map +1 -0
- package/dist/planmagnet/hooks/usePresence.d.ts +39 -0
- package/dist/planmagnet/hooks/usePresence.d.ts.map +1 -0
- package/dist/planmagnet/hooks/usePresence.js +146 -0
- package/dist/planmagnet/hooks/usePresence.js.map +1 -0
- package/dist/planmagnet/links/CodeLinksPanel.d.ts +22 -0
- package/dist/planmagnet/links/CodeLinksPanel.d.ts.map +1 -0
- package/dist/planmagnet/links/CodeLinksPanel.js +94 -0
- package/dist/planmagnet/links/CodeLinksPanel.js.map +1 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +488 -217
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +136 -130
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +205 -196
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +194 -160
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/presence/PresenceAvatars.d.ts +16 -0
- package/dist/planmagnet/presence/PresenceAvatars.d.ts.map +1 -0
- package/dist/planmagnet/presence/PresenceAvatars.js +56 -0
- package/dist/planmagnet/presence/PresenceAvatars.js.map +1 -0
- package/dist/planmagnet/utils/graphqlErrors.d.ts +6 -0
- package/dist/planmagnet/utils/graphqlErrors.d.ts.map +1 -1
- package/dist/planmagnet/utils/graphqlErrors.js +5 -2
- package/dist/planmagnet/utils/graphqlErrors.js.map +1 -1
- package/dist/planmagnet/utils/lineDiff.d.ts +21 -0
- package/dist/planmagnet/utils/lineDiff.d.ts.map +1 -0
- package/dist/planmagnet/utils/lineDiff.js +36 -0
- package/dist/planmagnet/utils/lineDiff.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkItemDiscussionPanel.js","names":[],"sources":["../../../src/planmagnet/discussion/WorkItemDiscussionPanel.tsx"],"sourcesContent":["import { useDeferredValue, useEffect, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Checkbox,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { ExternalLink, MessageCircle, MessagesSquare, Unlink as UnlinkIcon } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\n\n/**\n * Discussion on a task (CWF-26): a link to a real group conversation in the platform Conversations product, not\n * a comment thread of its own. `WorkItem.discussionConversationId` only stores and validates the id — the\n * conversation itself is created HERE, as the signed-in user, through the Conversations subgraph's own\n * `createConversation` (a different subgraph than everything else on this page; see its schema doc comment).\n * Once created, `planmagnetSetWorkItemDiscussion` links it back to this item.\n *\n * \"Open discussion\" is a plain `<a>`, not this MFE's own `usePlanMagnetNavigate()` — that hook prepends THIS\n * microfrontend's basePath, and `/conversations/<id>` is a sibling app's own top-level route, not a planmagnet\n * page (there is, correctly, no such route in `PlanMagnetRoutes.tsx`).\n */\n\nconst SET_DISCUSSION = gql`\n mutation PlanmagnetSetWorkItemDiscussionLink($workItemId: ID!, $conversationId: ID) {\n planmagnetSetWorkItemDiscussion(workItemId: $workItemId, conversationId: $conversationId) {\n id\n discussionConversationId\n }\n }\n`;\n\nconst CREATE_DISCUSSION_CONVERSATION = gql`\n mutation CreateWorkItemDiscussionConversation($input: CreateConversationInput!) {\n createConversation(input: $input) {\n success\n conversation {\n id\n }\n }\n }\n`;\n\nconst DISCUSSION_REFETCH = { refetchQueries: ['GetWorkItemDetail'], awaitRefetchQueries: true };\n\n/** Create a group conversation for this task and link it — participants default to the assignee + the current\n * user, editable through the same search-then-checkbox workspace directory picker\n * `roadmaps/RoadmapReviewPanel.tsx`'s `RequestReviewDialog` uses for reviewers. */\nfunction StartDiscussionDialog({\n workItemId,\n workItemKey,\n workItemTitle,\n assigneeId,\n}: {\n workItemId: string;\n workItemKey: string;\n workItemTitle: string;\n assigneeId: string | null;\n}) {\n const { workspaceId, authToken, apiGatewayUrl, currentUser } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const deferredSearch = useDeferredValue(search.trim());\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n const { search: setDirectoryFilter } = directory;\n useEffect(() => {\n setDirectoryFilter(deferredSearch);\n }, [deferredSearch, setDirectoryFilter]);\n\n const [createConversation, createState] = useMutation<{\n createConversation: { success: boolean; conversation: { id: string } | null };\n }>(CREATE_DISCUSSION_CONVERSATION);\n const [setDiscussion, setDiscussionState] = useMutation(SET_DISCUSSION, DISCUSSION_REFETCH);\n const busy = createState.loading || setDiscussionState.loading;\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const submit = async () => {\n if (!workspaceId) return;\n setError(null);\n try {\n const created = await throwOnError(\n createConversation({\n variables: {\n input: {\n workspaceId,\n type: 'GROUP',\n title: `${workItemKey}: ${workItemTitle}`,\n participantIds: [...selected],\n },\n },\n })\n );\n const conversationId = created.data?.createConversation?.conversation?.id;\n if (!created.data?.createConversation?.success || !conversationId) {\n throw new Error('The conversation could not be created.');\n }\n await throwOnError(setDiscussion({ variables: { workItemId, conversationId } }));\n setOpen(false);\n setSearch('');\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role cannot start a conversation in this workspace.'\n : mutationErrorMessage(e, 'The discussion could not be started.')\n );\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) {\n // Default selection (assignee + self), recomputed on every open so an earlier session's manual picks\n // never leak into a later one.\n const defaults = new Set<string>();\n if (assigneeId) defaults.add(assigneeId);\n if (currentUser?.id) defaults.add(currentUser.id);\n setSelected(defaults);\n } else {\n setSearch('');\n setError(null);\n }\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" data-testid=\"discussion-start\">\n <MessagesSquare className=\"mr-1 h-4 w-4\" aria-hidden=\"true\" />\n Discuss in Conversations\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Start a discussion</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Creates a group conversation for this task and links it back here. Add anyone else who\n should be in it.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <p className=\"text-sm font-medium\">\n {workItemKey}: {workItemTitle}\n </p>\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search workspace members\"\n aria-label=\"Search workspace members\"\n />\n <div className=\"max-h-64 space-y-1 overflow-y-auto\">\n {directory.loading && directory.members.length === 0 && (\n <p className=\"py-4 text-center text-sm text-muted-foreground\">Loading…</p>\n )}\n {!directory.loading && directory.members.length === 0 && (\n <p className=\"py-4 text-center text-sm text-muted-foreground\">No matching members.</p>\n )}\n {directory.members.map((member) => {\n const inputId = `discussion-participant-${member.id}`;\n return (\n <label\n key={member.id}\n htmlFor={inputId}\n className=\"flex cursor-pointer items-center gap-3 rounded-md border p-2 text-sm hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(member.id)}\n onCheckedChange={(checked) => toggle(member.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n {member.name}\n {member.id === currentUser?.id && (\n <span className=\"text-muted-foreground\"> (you)</span>\n )}\n {member.id === assigneeId && (\n <span className=\"text-muted-foreground\"> · assignee</span>\n )}\n </span>\n </label>\n );\n })}\n {directory.hasMore && (\n <button\n type=\"button\"\n className=\"w-full py-1 text-xs text-primary hover:underline\"\n onClick={directory.fetchMore}\n >\n Show more members\n </button>\n )}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void submit()}\n disabled={selected.size === 0 || busy}\n data-testid=\"discussion-create-confirm\"\n >\n {busy ? 'Starting…' : `Start discussion (${selected.size})`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nexport function WorkItemDiscussionPanel({\n workItemId,\n workItemKey,\n workItemTitle,\n assigneeId,\n discussionConversationId,\n}: {\n workItemId: string;\n workItemKey: string;\n workItemTitle: string;\n assigneeId: string | null;\n discussionConversationId: string | null;\n}) {\n const [unlink, unlinkState] = useMutation(SET_DISCUSSION, DISCUSSION_REFETCH);\n const [error, setError] = useState<string | null>(null);\n\n const doUnlink = async () => {\n setError(null);\n try {\n await throwOnError(unlink({ variables: { workItemId, conversationId: null } }));\n } catch (e) {\n setError(mutationErrorMessage(e, 'The discussion could not be unlinked.'));\n }\n };\n\n return (\n <Card data-testid=\"work-item-discussion\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageCircle className=\"h-4 w-4\" aria-hidden=\"true\" />\n Discussion\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {error && <ErrorLine message={error} />}\n {discussionConversationId ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <a\n href={`/conversations/${encodeURIComponent(discussionConversationId)}`}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n data-testid=\"discussion-open\"\n >\n Open discussion\n <ExternalLink className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </a>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={unlinkState.loading}\n onClick={() => void doUnlink()}\n data-testid=\"discussion-unlink\"\n >\n <UnlinkIcon className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Unlink\n </Button>\n </div>\n ) : (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">\n Not linked to a conversation yet. Starting one creates a group chat for this task in\n Conversations.\n </p>\n <StartDiscussionDialog\n workItemId={workItemId}\n workItemKey={workItemKey}\n workItemTitle={workItemTitle}\n assigneeId={assigneeId}\n />\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAsCA,IAAM,IAAiB,CAAG;;;;;;;GASpB,IAAiC,CAAG;;;;;;;;;GAWpC,IAAqB;CAAE,gBAAgB,CAAC,oBAAoB;CAAE,qBAAqB;CAAM;AAK/F,SAAS,EAAsB,EAC7B,eACA,gBACA,kBACA,iBAMC;CACD,IAAM,EAAE,gBAAa,cAAW,kBAAe,mBAAgB,GAAe,EACxE,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAiB,EAAiB,EAAO,MAAM,CAAC,EAChD,CAAC,GAAU,KAAe,kBAAsB,IAAI,KAAK,CAAC,EAC1D,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAY,EAAsB;EACtC,aAAa,KAAe;EAC5B,WAAW,KAAa,KAAA;EACxB,eAAe,KAAiB,KAAA;EACjC,CAAC,EACI,EAAE,QAAQ,MAAuB;AACvC,SAAgB;AACd,IAAmB,EAAe;IACjC,CAAC,GAAgB,EAAmB,CAAC;CAExC,IAAM,CAAC,GAAoB,KAAe,EAEvC,EAA+B,EAC5B,CAAC,GAAe,KAAsB,EAAY,GAAgB,EAAmB,EACrF,IAAO,EAAY,WAAW,EAAmB,SAEjD,KAAU,GAAY,MAC1B,GAAa,MAAS;EACpB,IAAM,IAAO,IAAI,IAAI,EAAK;AAG1B,SAFI,IAAS,EAAK,IAAI,EAAG,GACpB,EAAK,OAAO,EAAG,EACb;GACP,EAEE,IAAS,YAAY;AACpB,SACL;KAAS,KAAK;AACd,OAAI;IACF,IAAM,IAAU,MAAM,EACpB,EAAmB,EACjB,WAAW,EACT,OAAO;KACL;KACA,MAAM;KACN,OAAO,GAAG,EAAY,IAAI;KAC1B,gBAAgB,CAAC,GAAG,EAAS;KAC9B,EACF,EACF,CAAC,CACH,EACK,IAAiB,EAAQ,MAAM,oBAAoB,cAAc;AACvE,QAAI,CAAC,EAAQ,MAAM,oBAAoB,WAAW,CAAC,EACjD,OAAU,MAAM,yCAAyC;AAI3D,IAFA,MAAM,EAAa,EAAc,EAAE,WAAW;KAAE;KAAY;KAAgB,EAAE,CAAC,CAAC,EAChF,EAAQ,GAAM,EACd,EAAU,GAAG;YACN,GAAG;AACV,MACE,EAAY,EAAE,GACV,6DACA,EAAqB,GAAG,uCAAuC,CACpE;;;;AAIL,QACE,kBAAC,GAAD;EACQ;EACN,eAAe,MAAS;AAEtB,OADA,EAAQ,EAAK,EACT,GAAM;IAGR,IAAM,oBAAW,IAAI,KAAa;AAGlC,IAFI,KAAY,EAAS,IAAI,EAAW,EACpC,GAAa,MAAI,EAAS,IAAI,EAAY,GAAG,EACjD,EAAY,EAAS;SAGrB,CADA,EAAU,GAAG,EACb,EAAS,KAAK;;YAbpB,CAiBE,kBAAC,GAAD;GAAyB,SAAA;aACvB,kBAAC,GAAD;IAAQ,MAAK;IAAK,eAAY;cAA9B,CACE,kBAAC,GAAD;KAAgB,WAAU;KAAe,eAAY;KAAS,CAAA,EAAA,2BAEvD;;GACe,CAAA,EAC1B,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAuB,sBAA0C,CAAA,EACjE,kBAAC,GAAD,EAAA,UAA6B,2GAGC,CAAA,CACP,EAAA,CAAA;IACzB,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG;MAAY;MAAG;MACd;;IACJ,kBAAC,GAAD;KACE,OAAO;KACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;KAC1C,aAAY;KACZ,cAAW;KACX,CAAA;IACF,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAU,WAAW,EAAU,QAAQ,WAAW,KACjD,kBAAC,KAAD;OAAG,WAAU;iBAAiD;OAAY,CAAA;MAE3E,CAAC,EAAU,WAAW,EAAU,QAAQ,WAAW,KAClD,kBAAC,KAAD;OAAG,WAAU;iBAAiD;OAAwB,CAAA;MAEvF,EAAU,QAAQ,KAAK,MAAW;OACjC,IAAM,IAAU,0BAA0B,EAAO;AACjD,cACE,kBAAC,SAAD;QAEE,SAAS;QACT,WAAU;kBAHZ,CAKE,kBAAC,GAAD;SACE,IAAI;SACJ,SAAS,EAAS,IAAI,EAAO,GAAG;SAChC,kBAAkB,MAAY,EAAO,EAAO,IAAI,MAAY,GAAK;SACjE,CAAA,EACF,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACG,EAAO;UACP,EAAO,OAAO,GAAa,MAC1B,kBAAC,QAAD;WAAM,WAAU;qBAAwB;WAAa,CAAA;UAEtD,EAAO,OAAO,KACb,kBAAC,QAAD;WAAM,WAAU;qBAAwB;WAAkB,CAAA;UAEvD;WACD;UAlBD,EAAO,GAkBN;QAEV;MACD,EAAU,WACT,kBAAC,UAAD;OACE,MAAK;OACL,WAAU;OACV,SAAS,EAAU;iBACpB;OAEQ,CAAA;MAEP;;IACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA;IACvC,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;KACE,eAAe,KAAK,GAAQ;KAC5B,UAAU,EAAS,SAAS,KAAK;KACjC,eAAY;eAEX,IAAO,cAAc,qBAAqB,EAAS,KAAK;KAClD,CAAA,EACc,CAAA;IACD;KACT;;;AAIvB,SAAgB,EAAwB,EACtC,eACA,gBACA,kBACA,eACA,+BAOC;CACD,IAAM,CAAC,GAAQ,KAAe,EAAY,GAAgB,EAAmB,EACvE,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AACF,SAAM,EAAa,EAAO,EAAE,WAAW;IAAE;IAAY,gBAAgB;IAAM,EAAE,CAAC,CAAC;WACxE,GAAG;AACV,KAAS,EAAqB,GAAG,wCAAwC,CAAC;;;AAI9E,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAW,WAAU;aAArB,CACE,kBAAC,GAAD;IAAe,WAAU;IAAU,eAAY;IAAS,CAAA,EAAA,aAE9C;MACD,CAAA,EACb,kBAAC,GAAD;GAAa,WAAU;aAAvB,CACG,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,EACtC,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KACE,MAAM,kBAAkB,mBAAmB,EAAyB;KACpE,WAAU;KACV,eAAY;eAHd,CAIC,mBAEC,kBAAC,GAAD;MAAc,WAAU;MAAc,eAAY;MAAS,CAAA,CACzD;QACJ,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,UAAU,EAAY;KACtB,eAAe,KAAK,GAAU;KAC9B,eAAY;eALd,CAOE,kBAAC,GAAD;MAAY,WAAU;MAAmB,eAAY;MAAS,CAAA,EAAA,SAEvD;OACL;QAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAgC;KAGzC,CAAA,EACJ,kBAAC,GAAD;KACc;KACC;KACE;KACH;KACZ,CAAA,CACE;MAEI;KACT"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LineDiffKind } from '../utils/lineDiff';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders ADDED/REMOVED/UNCHANGED hunks (CWF-18 §2/§3) — shared by the suggestion diff preview
|
|
5
|
+
* (`DocSuggestionsPanel`, a client-side `lineDiff`) and the version-compare panel (`DocVersionCompare`, the
|
|
6
|
+
* server's `planmagnetDocRevisionDiff`); both produce the same `{kind, text}` shape.
|
|
7
|
+
*
|
|
8
|
+
* Colour is never the only signal: every line carries a leading `+`/`−`/` ` marker AND a screen-reader-only
|
|
9
|
+
* "Added:"/"Removed:" prefix, not just a background tint.
|
|
10
|
+
*/
|
|
11
|
+
export interface DocDiffHunkLike {
|
|
12
|
+
kind: LineDiffKind;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function DocDiffHunks({ hunks, truncated, emptyMessage, testId, }: {
|
|
16
|
+
hunks: readonly DocDiffHunkLike[];
|
|
17
|
+
truncated?: boolean;
|
|
18
|
+
emptyMessage?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Overridable so two instances never collide on the same page. `DocVersionCompare` (one result at a time) keeps
|
|
21
|
+
* the default `doc-version-diff`; `DocSuggestionsPanel` passes a per-suggestion id, since several suggestions'
|
|
22
|
+
* previews can be expanded at once in the Open tab.
|
|
23
|
+
*/
|
|
24
|
+
testId?: string;
|
|
25
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=DocDiffView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocDiffView.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/docs/DocDiffView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAgBD,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,YAAgC,EAChC,MAA2B,GAC5B,EAAE;IACD,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CA0BA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as e, jsxs as t } from "react/jsx-runtime";
|
|
2
|
+
//#region src/planmagnet/docs/DocDiffView.tsx
|
|
3
|
+
var n = {
|
|
4
|
+
ADDED: {
|
|
5
|
+
symbol: "+",
|
|
6
|
+
srLabel: "Added: ",
|
|
7
|
+
className: "bg-status-success-subtle text-status-success-text"
|
|
8
|
+
},
|
|
9
|
+
REMOVED: {
|
|
10
|
+
symbol: "−",
|
|
11
|
+
srLabel: "Removed: ",
|
|
12
|
+
className: "bg-status-error-subtle text-status-error-text"
|
|
13
|
+
},
|
|
14
|
+
UNCHANGED: {
|
|
15
|
+
symbol: " ",
|
|
16
|
+
srLabel: "",
|
|
17
|
+
className: "text-muted-foreground"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function r({ hunks: r, truncated: i, emptyMessage: a = "No differences.", testId: o = "doc-version-diff" }) {
|
|
21
|
+
return r.length === 0 ? /* @__PURE__ */ e("p", {
|
|
22
|
+
className: "text-sm text-muted-foreground",
|
|
23
|
+
children: a
|
|
24
|
+
}) : /* @__PURE__ */ t("div", {
|
|
25
|
+
className: "max-h-96 space-y-0.5 overflow-y-auto rounded-md border border-border-default bg-muted/30 p-2 font-mono text-xs",
|
|
26
|
+
"data-testid": o,
|
|
27
|
+
children: [r.map((r, i) => {
|
|
28
|
+
let a = n[r.kind];
|
|
29
|
+
return /* @__PURE__ */ t("div", {
|
|
30
|
+
className: `whitespace-pre-wrap rounded-sm px-1 ${a.className}`,
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ t("span", {
|
|
33
|
+
"aria-hidden": "true",
|
|
34
|
+
children: [a.symbol, " "]
|
|
35
|
+
}),
|
|
36
|
+
/* @__PURE__ */ e("span", {
|
|
37
|
+
className: "sr-only",
|
|
38
|
+
children: a.srLabel
|
|
39
|
+
}),
|
|
40
|
+
r.text.length > 0 ? r.text : "\xA0"
|
|
41
|
+
]
|
|
42
|
+
}, i);
|
|
43
|
+
}), i && /* @__PURE__ */ e("p", {
|
|
44
|
+
className: "pt-1 text-[11px] italic text-muted-foreground",
|
|
45
|
+
children: "Truncated — only the first part of the difference is shown."
|
|
46
|
+
})]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { r as DocDiffHunks };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=DocDiffView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocDiffView.js","names":[],"sources":["../../../src/planmagnet/docs/DocDiffView.tsx"],"sourcesContent":["import type { LineDiffKind } from '../utils/lineDiff';\n\n/**\n * Renders ADDED/REMOVED/UNCHANGED hunks (CWF-18 §2/§3) — shared by the suggestion diff preview\n * (`DocSuggestionsPanel`, a client-side `lineDiff`) and the version-compare panel (`DocVersionCompare`, the\n * server's `planmagnetDocRevisionDiff`); both produce the same `{kind, text}` shape.\n *\n * Colour is never the only signal: every line carries a leading `+`/`−`/` ` marker AND a screen-reader-only\n * \"Added:\"/\"Removed:\" prefix, not just a background tint.\n */\n\nexport interface DocDiffHunkLike {\n kind: LineDiffKind;\n text: string;\n}\n\nconst KIND_META: Record<LineDiffKind, { symbol: string; srLabel: string; className: string }> = {\n ADDED: {\n symbol: '+',\n srLabel: 'Added: ',\n className: 'bg-status-success-subtle text-status-success-text',\n },\n REMOVED: {\n symbol: '−',\n srLabel: 'Removed: ',\n className: 'bg-status-error-subtle text-status-error-text',\n },\n UNCHANGED: { symbol: ' ', srLabel: '', className: 'text-muted-foreground' },\n};\n\nexport function DocDiffHunks({\n hunks,\n truncated,\n emptyMessage = 'No differences.',\n testId = 'doc-version-diff',\n}: {\n hunks: readonly DocDiffHunkLike[];\n truncated?: boolean;\n emptyMessage?: string;\n /**\n * Overridable so two instances never collide on the same page. `DocVersionCompare` (one result at a time) keeps\n * the default `doc-version-diff`; `DocSuggestionsPanel` passes a per-suggestion id, since several suggestions'\n * previews can be expanded at once in the Open tab.\n */\n testId?: string;\n}) {\n if (hunks.length === 0) {\n return <p className=\"text-sm text-muted-foreground\">{emptyMessage}</p>;\n }\n return (\n <div\n className=\"max-h-96 space-y-0.5 overflow-y-auto rounded-md border border-border-default bg-muted/30 p-2 font-mono text-xs\"\n data-testid={testId}\n >\n {hunks.map((hunk, index) => {\n const meta = KIND_META[hunk.kind];\n return (\n <div key={index} className={`whitespace-pre-wrap rounded-sm px-1 ${meta.className}`}>\n <span aria-hidden=\"true\">{meta.symbol} </span>\n <span className=\"sr-only\">{meta.srLabel}</span>\n {hunk.text.length > 0 ? hunk.text : ' '}\n </div>\n );\n })}\n {truncated && (\n <p className=\"pt-1 text-[11px] italic text-muted-foreground\">\n Truncated — only the first part of the difference is shown.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;AAgBA,IAAM,IAA0F;CAC9F,OAAO;EACL,QAAQ;EACR,SAAS;EACT,WAAW;EACZ;CACD,SAAS;EACP,QAAQ;EACR,SAAS;EACT,WAAW;EACZ;CACD,WAAW;EAAE,QAAQ;EAAK,SAAS;EAAI,WAAW;EAAyB;CAC5E;AAED,SAAgB,EAAa,EAC3B,UACA,cACA,kBAAe,mBACf,YAAS,sBAWR;AAID,QAHI,EAAM,WAAW,IACZ,kBAAC,KAAD;EAAG,WAAU;YAAiC;EAAiB,CAAA,GAGtE,kBAAC,OAAD;EACE,WAAU;EACV,eAAa;YAFf,CAIG,EAAM,KAAK,GAAM,MAAU;GAC1B,IAAM,IAAO,EAAU,EAAK;AAC5B,UACE,kBAAC,OAAD;IAAiB,WAAW,uCAAuC,EAAK;cAAxE;KACE,kBAAC,QAAD;MAAM,eAAY;gBAAlB,CAA0B,EAAK,QAAO,IAAQ;;KAC9C,kBAAC,QAAD;MAAM,WAAU;gBAAW,EAAK;MAAe,CAAA;KAC9C,EAAK,KAAK,SAAS,IAAI,EAAK,OAAO;KAChC;MAJI,EAIJ;IAER,EACD,KACC,kBAAC,KAAD;GAAG,WAAU;aAAgD;GAEzD,CAAA,CAEF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DocRevisionForDiff {
|
|
2
|
+
version: number;
|
|
3
|
+
title: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function DocSuggestionsPanel({ docId, docTitle, docContent, revisions, currentUserId, }: {
|
|
7
|
+
docId: string;
|
|
8
|
+
docTitle: string;
|
|
9
|
+
docContent: string;
|
|
10
|
+
revisions: readonly DocRevisionForDiff[];
|
|
11
|
+
currentUserId: string | undefined;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=DocSuggestionsPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocSuggestionsPanel.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/docs/DocSuggestionsPanel.tsx"],"names":[],"mappings":"AA2GA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AA0QD,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,UAAU,EACV,SAAS,EACT,aAAa,GACd,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACzC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,2CAuCA"}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { PersonName as e } from "../people/People.js";
|
|
2
|
+
import { throwOnError as t } from "../utils/mutationResult.js";
|
|
3
|
+
import { ConfirmButton as n, ErrorLine as r } from "../controls/ConfirmButton.js";
|
|
4
|
+
import { isConflict as i, isForbidden as a, mutationErrorMessage as o } from "../utils/graphqlErrors.js";
|
|
5
|
+
import { lineDiff as s } from "../utils/lineDiff.js";
|
|
6
|
+
import { DocDiffHunks as c } from "./DocDiffView.js";
|
|
7
|
+
import { useState as l } from "react";
|
|
8
|
+
import { FileEdit as u, Undo2 as d } from "lucide-react";
|
|
9
|
+
import { gql as f } from "@apollo/client";
|
|
10
|
+
import { useMutation as p, useQuery as m } from "@apollo/client/react";
|
|
11
|
+
import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
12
|
+
import { Button as v, Card as y, CardContent as b, CardHeader as x, CardTitle as S, Tabs as C, TabsContent as w, TabsList as T, TabsTrigger as E } from "@burdenoff/fe-libs/ui";
|
|
13
|
+
//#region src/planmagnet/docs/DocSuggestionsPanel.tsx
|
|
14
|
+
var D = 10, O = "DocSuggestionsPanel", k = f`
|
|
15
|
+
query DocSuggestionsPanel(
|
|
16
|
+
$docId: ID!
|
|
17
|
+
$status: PlanmagnetDocSuggestionStatus
|
|
18
|
+
$pagination: PlanmagnetPaginationInput
|
|
19
|
+
) {
|
|
20
|
+
planmagnetDoc(id: $docId) {
|
|
21
|
+
id
|
|
22
|
+
suggestions(status: $status, pagination: $pagination) {
|
|
23
|
+
totalCount
|
|
24
|
+
pageInfo {
|
|
25
|
+
hasNextPage
|
|
26
|
+
endCursor
|
|
27
|
+
}
|
|
28
|
+
items {
|
|
29
|
+
id
|
|
30
|
+
authorId
|
|
31
|
+
baseVersion
|
|
32
|
+
proposedTitle
|
|
33
|
+
proposedContent
|
|
34
|
+
note
|
|
35
|
+
status
|
|
36
|
+
createdAt
|
|
37
|
+
decidedAt
|
|
38
|
+
decidedBy
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
`, A = f`
|
|
44
|
+
mutation PlanmagnetDecideDocSuggestionDecide($id: ID!, $accept: Boolean!) {
|
|
45
|
+
planmagnetDecideDocSuggestion(id: $id, accept: $accept) {
|
|
46
|
+
id
|
|
47
|
+
status
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`, j = f`
|
|
51
|
+
mutation PlanmagnetWithdrawDocSuggestionWithdraw($id: ID!) {
|
|
52
|
+
planmagnetWithdrawDocSuggestion(id: $id) {
|
|
53
|
+
id
|
|
54
|
+
status
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`, M = (e) => new Date(e).toLocaleString();
|
|
58
|
+
function N({ suggestion: u, docTitle: f, docContent: m, revisions: y, currentUserId: b }) {
|
|
59
|
+
let [x, S] = l(!1), [C, w] = l(null), [T, E] = p(A, {
|
|
60
|
+
refetchQueries: [O, "DocDetail"],
|
|
61
|
+
awaitRefetchQueries: !0
|
|
62
|
+
}), [D, k] = p(j, {
|
|
63
|
+
refetchQueries: [O],
|
|
64
|
+
awaitRefetchQueries: !0
|
|
65
|
+
}), N = y.find((e) => e.version === u.baseVersion), P = N?.content ?? m, F = N?.title ?? f, I = s(P, u.proposedContent), L = u.proposedTitle !== null && u.proposedTitle !== F, R = u.authorId === b, z = async (e, n) => {
|
|
66
|
+
w(null);
|
|
67
|
+
try {
|
|
68
|
+
await t(e());
|
|
69
|
+
} catch (e) {
|
|
70
|
+
w(a(e) ? "Your role cannot decide this suggestion." : i(e) ? "The doc changed since this was suggested." : o(e, n));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return /* @__PURE__ */ _("li", {
|
|
74
|
+
className: "space-y-2 rounded-md border border-border-default p-3",
|
|
75
|
+
"data-testid": "doc-suggestion",
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ _("div", {
|
|
78
|
+
className: "flex flex-wrap items-start justify-between gap-2",
|
|
79
|
+
children: [/* @__PURE__ */ _("div", {
|
|
80
|
+
className: "min-w-0",
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ _("p", {
|
|
83
|
+
className: "text-sm",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ g("span", {
|
|
86
|
+
className: "font-medium",
|
|
87
|
+
children: /* @__PURE__ */ g(e, { id: u.authorId })
|
|
88
|
+
}),
|
|
89
|
+
" ",
|
|
90
|
+
"proposed ",
|
|
91
|
+
M(u.createdAt),
|
|
92
|
+
" against version ",
|
|
93
|
+
u.baseVersion
|
|
94
|
+
]
|
|
95
|
+
}),
|
|
96
|
+
u.note && /* @__PURE__ */ g("p", {
|
|
97
|
+
className: "mt-0.5 text-xs text-muted-foreground",
|
|
98
|
+
children: u.note
|
|
99
|
+
}),
|
|
100
|
+
u.status !== "OPEN" && /* @__PURE__ */ _("p", {
|
|
101
|
+
className: "mt-0.5 text-xs text-muted-foreground",
|
|
102
|
+
children: [
|
|
103
|
+
u.status === "ACCEPTED" ? "Accepted" : "Rejected",
|
|
104
|
+
u.decidedBy ? /* @__PURE__ */ _(h, { children: [
|
|
105
|
+
" ",
|
|
106
|
+
"by ",
|
|
107
|
+
/* @__PURE__ */ g(e, { id: u.decidedBy })
|
|
108
|
+
] }) : null,
|
|
109
|
+
u.decidedAt ? ` · ${M(u.decidedAt)}` : ""
|
|
110
|
+
]
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
}), /* @__PURE__ */ g(v, {
|
|
114
|
+
size: "sm",
|
|
115
|
+
variant: "ghost",
|
|
116
|
+
className: "h-7 shrink-0 text-xs",
|
|
117
|
+
onClick: () => S((e) => !e),
|
|
118
|
+
"aria-expanded": x,
|
|
119
|
+
children: x ? "Hide diff" : "Show diff"
|
|
120
|
+
})]
|
|
121
|
+
}),
|
|
122
|
+
C && /* @__PURE__ */ g(r, { message: C }),
|
|
123
|
+
x && /* @__PURE__ */ _("div", {
|
|
124
|
+
className: "space-y-1",
|
|
125
|
+
children: [L && /* @__PURE__ */ _("p", {
|
|
126
|
+
className: "text-xs text-muted-foreground",
|
|
127
|
+
children: [
|
|
128
|
+
"Title: ",
|
|
129
|
+
/* @__PURE__ */ g("span", {
|
|
130
|
+
className: "line-through",
|
|
131
|
+
children: F
|
|
132
|
+
}),
|
|
133
|
+
" →",
|
|
134
|
+
" ",
|
|
135
|
+
/* @__PURE__ */ g("span", {
|
|
136
|
+
className: "font-medium text-foreground",
|
|
137
|
+
children: u.proposedTitle
|
|
138
|
+
})
|
|
139
|
+
]
|
|
140
|
+
}), /* @__PURE__ */ g(c, {
|
|
141
|
+
hunks: I.hunks,
|
|
142
|
+
truncated: I.truncated,
|
|
143
|
+
emptyMessage: "No content differences.",
|
|
144
|
+
testId: `doc-suggestion-diff-${u.id}`
|
|
145
|
+
})]
|
|
146
|
+
}),
|
|
147
|
+
u.status === "OPEN" && /* @__PURE__ */ _("div", {
|
|
148
|
+
className: "flex flex-wrap justify-end gap-2 border-t border-border-default pt-2",
|
|
149
|
+
children: [
|
|
150
|
+
R && /* @__PURE__ */ g(n, {
|
|
151
|
+
label: "Withdraw",
|
|
152
|
+
icon: /* @__PURE__ */ g(d, {
|
|
153
|
+
className: "h-3.5 w-3.5",
|
|
154
|
+
"aria-hidden": "true"
|
|
155
|
+
}),
|
|
156
|
+
title: "Withdraw this suggestion?",
|
|
157
|
+
description: "It moves to Withdrawn and can no longer be accepted or rejected.",
|
|
158
|
+
confirmLabel: "Withdraw",
|
|
159
|
+
variant: "outline",
|
|
160
|
+
disabled: k.loading,
|
|
161
|
+
onConfirm: () => void z(() => D({ variables: { id: u.id } }), "The suggestion could not be withdrawn."),
|
|
162
|
+
testId: "doc-suggestion-withdraw"
|
|
163
|
+
}),
|
|
164
|
+
/* @__PURE__ */ g(v, {
|
|
165
|
+
size: "sm",
|
|
166
|
+
variant: "outline",
|
|
167
|
+
disabled: E.loading,
|
|
168
|
+
"data-testid": "doc-suggestion-reject",
|
|
169
|
+
onClick: () => void z(() => T({ variables: {
|
|
170
|
+
id: u.id,
|
|
171
|
+
accept: !1
|
|
172
|
+
} }), "The suggestion could not be rejected."),
|
|
173
|
+
children: "Reject"
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ g(v, {
|
|
176
|
+
size: "sm",
|
|
177
|
+
disabled: E.loading,
|
|
178
|
+
"data-testid": "doc-suggestion-accept",
|
|
179
|
+
onClick: () => void z(() => T({ variables: {
|
|
180
|
+
id: u.id,
|
|
181
|
+
accept: !0
|
|
182
|
+
} }), "The suggestion could not be accepted."),
|
|
183
|
+
children: "Accept"
|
|
184
|
+
})
|
|
185
|
+
]
|
|
186
|
+
})
|
|
187
|
+
]
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function P({ docId: e, status: t, docTitle: n, docContent: i, revisions: o, currentUserId: s }) {
|
|
191
|
+
let [c, u] = l(!1), { data: d, loading: f, error: p, fetchMore: h } = m(k, {
|
|
192
|
+
variables: {
|
|
193
|
+
docId: e,
|
|
194
|
+
status: t,
|
|
195
|
+
pagination: { first: D }
|
|
196
|
+
},
|
|
197
|
+
skip: !e
|
|
198
|
+
}), y = d?.planmagnetDoc?.suggestions, b = y?.items ?? [], x = async () => {
|
|
199
|
+
let n = y?.pageInfo.endCursor;
|
|
200
|
+
if (n) {
|
|
201
|
+
u(!0);
|
|
202
|
+
try {
|
|
203
|
+
await h({
|
|
204
|
+
variables: {
|
|
205
|
+
docId: e,
|
|
206
|
+
status: t,
|
|
207
|
+
pagination: {
|
|
208
|
+
first: D,
|
|
209
|
+
after: n
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
updateQuery: (e, { fetchMoreResult: t }) => !e.planmagnetDoc || !t.planmagnetDoc ? e : { planmagnetDoc: {
|
|
213
|
+
...t.planmagnetDoc,
|
|
214
|
+
suggestions: {
|
|
215
|
+
...t.planmagnetDoc.suggestions,
|
|
216
|
+
items: [...e.planmagnetDoc.suggestions.items, ...t.planmagnetDoc.suggestions.items]
|
|
217
|
+
}
|
|
218
|
+
} }
|
|
219
|
+
});
|
|
220
|
+
} finally {
|
|
221
|
+
u(!1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
return /* @__PURE__ */ _("div", {
|
|
226
|
+
className: "space-y-2",
|
|
227
|
+
children: [
|
|
228
|
+
f && !d && /* @__PURE__ */ g("p", {
|
|
229
|
+
className: "text-sm text-muted-foreground",
|
|
230
|
+
children: "Loading…"
|
|
231
|
+
}),
|
|
232
|
+
p && /* @__PURE__ */ g(r, { message: a(p) ? "Your role cannot see this doc’s suggestions." : `Suggestions could not be loaded: ${p.message}` }),
|
|
233
|
+
y && b.length === 0 && /* @__PURE__ */ g("p", {
|
|
234
|
+
className: "text-sm text-muted-foreground",
|
|
235
|
+
children: t === "OPEN" ? "No open suggestions." : t === "ACCEPTED" ? "None accepted yet." : "None rejected yet."
|
|
236
|
+
}),
|
|
237
|
+
/* @__PURE__ */ g("ul", {
|
|
238
|
+
className: "space-y-2",
|
|
239
|
+
children: b.map((e) => /* @__PURE__ */ g(N, {
|
|
240
|
+
suggestion: e,
|
|
241
|
+
docTitle: n,
|
|
242
|
+
docContent: i,
|
|
243
|
+
revisions: o,
|
|
244
|
+
currentUserId: s
|
|
245
|
+
}, e.id))
|
|
246
|
+
}),
|
|
247
|
+
y?.pageInfo.hasNextPage && /* @__PURE__ */ g(v, {
|
|
248
|
+
size: "sm",
|
|
249
|
+
variant: "outline",
|
|
250
|
+
onClick: () => void x(),
|
|
251
|
+
disabled: c,
|
|
252
|
+
children: c ? "Loading…" : `Show more (${b.length} of ${y.totalCount})`
|
|
253
|
+
})
|
|
254
|
+
]
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function F({ docId: e, docTitle: t, docContent: n, revisions: r, currentUserId: i }) {
|
|
258
|
+
let [a, o] = l("OPEN");
|
|
259
|
+
return /* @__PURE__ */ _(y, {
|
|
260
|
+
"data-testid": "doc-suggestions",
|
|
261
|
+
children: [/* @__PURE__ */ g(x, { children: /* @__PURE__ */ _(S, {
|
|
262
|
+
className: "flex items-center gap-2",
|
|
263
|
+
children: [/* @__PURE__ */ g(u, {
|
|
264
|
+
className: "h-4 w-4",
|
|
265
|
+
"aria-hidden": "true"
|
|
266
|
+
}), "Suggestions"]
|
|
267
|
+
}) }), /* @__PURE__ */ g(b, { children: /* @__PURE__ */ _(C, {
|
|
268
|
+
value: a,
|
|
269
|
+
onValueChange: (e) => o(e),
|
|
270
|
+
children: [/* @__PURE__ */ _(T, { children: [
|
|
271
|
+
/* @__PURE__ */ g(E, {
|
|
272
|
+
value: "OPEN",
|
|
273
|
+
"data-testid": "doc-suggestions-tab-open",
|
|
274
|
+
children: "Open"
|
|
275
|
+
}),
|
|
276
|
+
/* @__PURE__ */ g(E, {
|
|
277
|
+
value: "ACCEPTED",
|
|
278
|
+
"data-testid": "doc-suggestions-tab-accepted",
|
|
279
|
+
children: "Accepted"
|
|
280
|
+
}),
|
|
281
|
+
/* @__PURE__ */ g(E, {
|
|
282
|
+
value: "REJECTED",
|
|
283
|
+
"data-testid": "doc-suggestions-tab-rejected",
|
|
284
|
+
children: "Rejected"
|
|
285
|
+
})
|
|
286
|
+
] }), [
|
|
287
|
+
"OPEN",
|
|
288
|
+
"ACCEPTED",
|
|
289
|
+
"REJECTED"
|
|
290
|
+
].map((a) => /* @__PURE__ */ g(w, {
|
|
291
|
+
value: a,
|
|
292
|
+
className: "mt-3",
|
|
293
|
+
children: /* @__PURE__ */ g(P, {
|
|
294
|
+
docId: e,
|
|
295
|
+
status: a,
|
|
296
|
+
docTitle: t,
|
|
297
|
+
docContent: n,
|
|
298
|
+
revisions: r,
|
|
299
|
+
currentUserId: i
|
|
300
|
+
})
|
|
301
|
+
}, a))]
|
|
302
|
+
}) })]
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
export { F as DocSuggestionsPanel };
|
|
307
|
+
|
|
308
|
+
//# sourceMappingURL=DocSuggestionsPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocSuggestionsPanel.js","names":[],"sources":["../../../src/planmagnet/docs/DocSuggestionsPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { FileEdit, Undo2 } from 'lucide-react';\nimport { PersonName } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isConflict, isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { lineDiff } from '../utils/lineDiff';\nimport { DocDiffHunks } from './DocDiffView';\n\n/**\n * Proposed edits against a doc (CWF-18 §2) that have not been applied: Open / Accepted / Rejected, each its own\n * tab and its own (paginated) `planmagnetDoc.suggestions(status: …)` read — Withdrawn ones are never shown here,\n * matching the three tabs this panel was scoped to (a withdrawn suggestion is, by design, gone from view — its\n * author already said it no longer applies).\n *\n * The diff preview is computed HERE, client-side (`lineDiff`): `planmagnetDocRevisionDiff` only compares two\n * already-saved revisions, and a suggestion's `proposedContent` was never saved as one — so the base side comes\n * from whichever loaded `PlanmagnetDocRevision` matches `baseVersion` (falling back to the live doc when the\n * suggestion is against the CURRENT version, which has no revision row of its own yet).\n *\n * Accept/Reject are shown to every viewer (the resolver is the real gate, same \"show it, let the server refuse\n * it\" house rule as every other write-tier control in this MFE) except Withdraw, which IS client-gated to the\n * suggestion's own author — `planmagnetWithdrawDocSuggestion`'s own schema doc says it is author-only, refused\n * NOT_FOUND (not FORBIDDEN) for anyone else, so showing it to a non-author would just be a button that always\n * fails.\n */\n\nconst PAGE = 10;\nconst SUGGESTION_QUERY_NAME = 'DocSuggestionsPanel';\n\nconst DOC_SUGGESTIONS = gql`\n query DocSuggestionsPanel(\n $docId: ID!\n $status: PlanmagnetDocSuggestionStatus\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetDoc(id: $docId) {\n id\n suggestions(status: $status, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n baseVersion\n proposedTitle\n proposedContent\n note\n status\n createdAt\n decidedAt\n decidedBy\n }\n }\n }\n }\n`;\n\nconst DECIDE = gql`\n mutation PlanmagnetDecideDocSuggestionDecide($id: ID!, $accept: Boolean!) {\n planmagnetDecideDocSuggestion(id: $id, accept: $accept) {\n id\n status\n }\n }\n`;\n\nconst WITHDRAW = gql`\n mutation PlanmagnetWithdrawDocSuggestionWithdraw($id: ID!) {\n planmagnetWithdrawDocSuggestion(id: $id) {\n id\n status\n }\n }\n`;\n\ntype SuggestionStatus = 'OPEN' | 'ACCEPTED' | 'REJECTED';\n\ninterface Suggestion {\n id: string;\n authorId: string;\n baseVersion: number;\n proposedTitle: string | null;\n proposedContent: string;\n note: string | null;\n status: 'OPEN' | 'ACCEPTED' | 'REJECTED' | 'WITHDRAWN';\n createdAt: string;\n decidedAt: string | null;\n decidedBy: string | null;\n}\n\nexport interface DocRevisionForDiff {\n version: number;\n title: string;\n content: string;\n}\n\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nfunction SuggestionRow({\n suggestion,\n docTitle,\n docContent,\n revisions,\n currentUserId,\n}: {\n suggestion: Suggestion;\n docTitle: string;\n docContent: string;\n revisions: readonly DocRevisionForDiff[];\n currentUserId: string | undefined;\n}) {\n const [showDiff, setShowDiff] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const decideRefetch = {\n refetchQueries: [SUGGESTION_QUERY_NAME, 'DocDetail'],\n awaitRefetchQueries: true,\n };\n const [decide, decideState] = useMutation(DECIDE, decideRefetch);\n const [withdraw, withdrawState] = useMutation(WITHDRAW, {\n refetchQueries: [SUGGESTION_QUERY_NAME],\n awaitRefetchQueries: true,\n });\n\n // The suggestion's base version: a saved revision when the doc has moved on since, otherwise the live doc\n // itself (a suggestion against the CURRENT version has no PlanmagnetDocRevision row yet — only a save creates\n // one).\n const baseRevision = revisions.find((r) => r.version === suggestion.baseVersion);\n const baseContent = baseRevision?.content ?? docContent;\n const baseTitle = baseRevision?.title ?? docTitle;\n const diff = lineDiff(baseContent, suggestion.proposedContent);\n const titleChanged = suggestion.proposedTitle !== null && suggestion.proposedTitle !== baseTitle;\n const mine = suggestion.authorId === currentUserId;\n\n const act = async (action: () => Promise<{ error?: unknown }>, fallback: string) => {\n setError(null);\n try {\n await throwOnError(action());\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role cannot decide this suggestion.'\n : isConflict(e)\n ? 'The doc changed since this was suggested.'\n : mutationErrorMessage(e, fallback)\n );\n }\n };\n\n return (\n <li\n className=\"space-y-2 rounded-md border border-border-default p-3\"\n data-testid=\"doc-suggestion\"\n >\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-sm\">\n <span className=\"font-medium\">\n <PersonName id={suggestion.authorId} />\n </span>{' '}\n proposed {when(suggestion.createdAt)} against version {suggestion.baseVersion}\n </p>\n {suggestion.note && (\n <p className=\"mt-0.5 text-xs text-muted-foreground\">{suggestion.note}</p>\n )}\n {suggestion.status !== 'OPEN' && (\n <p className=\"mt-0.5 text-xs text-muted-foreground\">\n {suggestion.status === 'ACCEPTED' ? 'Accepted' : 'Rejected'}\n {suggestion.decidedBy ? (\n <>\n {' '}\n by <PersonName id={suggestion.decidedBy} />\n </>\n ) : null}\n {suggestion.decidedAt ? ` · ${when(suggestion.decidedAt)}` : ''}\n </p>\n )}\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 shrink-0 text-xs\"\n onClick={() => setShowDiff((v) => !v)}\n aria-expanded={showDiff}\n >\n {showDiff ? 'Hide diff' : 'Show diff'}\n </Button>\n </div>\n {error && <ErrorLine message={error} />}\n {showDiff && (\n <div className=\"space-y-1\">\n {titleChanged && (\n <p className=\"text-xs text-muted-foreground\">\n Title: <span className=\"line-through\">{baseTitle}</span> →{' '}\n <span className=\"font-medium text-foreground\">{suggestion.proposedTitle}</span>\n </p>\n )}\n <DocDiffHunks\n hunks={diff.hunks}\n truncated={diff.truncated}\n emptyMessage=\"No content differences.\"\n testId={`doc-suggestion-diff-${suggestion.id}`}\n />\n </div>\n )}\n {suggestion.status === 'OPEN' && (\n <div className=\"flex flex-wrap justify-end gap-2 border-t border-border-default pt-2\">\n {mine && (\n <ConfirmButton\n label=\"Withdraw\"\n icon={<Undo2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Withdraw this suggestion?\"\n description=\"It moves to Withdrawn and can no longer be accepted or rejected.\"\n confirmLabel=\"Withdraw\"\n variant=\"outline\"\n disabled={withdrawState.loading}\n onConfirm={() =>\n void act(\n () => withdraw({ variables: { id: suggestion.id } }),\n 'The suggestion could not be withdrawn.'\n )\n }\n testId=\"doc-suggestion-withdraw\"\n />\n )}\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={decideState.loading}\n data-testid=\"doc-suggestion-reject\"\n onClick={() =>\n void act(\n () => decide({ variables: { id: suggestion.id, accept: false } }),\n 'The suggestion could not be rejected.'\n )\n }\n >\n Reject\n </Button>\n <Button\n size=\"sm\"\n disabled={decideState.loading}\n data-testid=\"doc-suggestion-accept\"\n onClick={() =>\n void act(\n () => decide({ variables: { id: suggestion.id, accept: true } }),\n 'The suggestion could not be accepted.'\n )\n }\n >\n Accept\n </Button>\n </div>\n )}\n </li>\n );\n}\n\nfunction SuggestionList({\n docId,\n status,\n docTitle,\n docContent,\n revisions,\n currentUserId,\n}: {\n docId: string;\n status: SuggestionStatus;\n docTitle: string;\n docContent: string;\n revisions: readonly DocRevisionForDiff[];\n currentUserId: string | undefined;\n}) {\n const [loadingMore, setLoadingMore] = useState(false);\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetDoc: {\n id: string;\n suggestions: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: Suggestion[];\n };\n } | null;\n }>(DOC_SUGGESTIONS, {\n variables: { docId, status, pagination: { first: PAGE } },\n skip: !docId,\n });\n\n const list = data?.planmagnetDoc?.suggestions;\n const items = list?.items ?? [];\n\n const loadMore = async () => {\n const after = list?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { docId, status, pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!prev.planmagnetDoc || !fetchMoreResult.planmagnetDoc) return prev;\n return {\n planmagnetDoc: {\n ...fetchMoreResult.planmagnetDoc,\n suggestions: {\n ...fetchMoreResult.planmagnetDoc.suggestions,\n items: [\n ...prev.planmagnetDoc.suggestions.items,\n ...fetchMoreResult.planmagnetDoc.suggestions.items,\n ],\n },\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-2\">\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading…</p>}\n {error && (\n <ErrorLine\n message={\n isForbidden(error)\n ? 'Your role cannot see this doc’s suggestions.'\n : `Suggestions could not be loaded: ${error.message}`\n }\n />\n )}\n {list && items.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n {status === 'OPEN'\n ? 'No open suggestions.'\n : status === 'ACCEPTED'\n ? 'None accepted yet.'\n : 'None rejected yet.'}\n </p>\n )}\n <ul className=\"space-y-2\">\n {items.map((s) => (\n <SuggestionRow\n key={s.id}\n suggestion={s}\n docTitle={docTitle}\n docContent={docContent}\n revisions={revisions}\n currentUserId={currentUserId}\n />\n ))}\n </ul>\n {list?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${items.length} of ${list.totalCount})`}\n </Button>\n )}\n </div>\n );\n}\n\nexport function DocSuggestionsPanel({\n docId,\n docTitle,\n docContent,\n revisions,\n currentUserId,\n}: {\n docId: string;\n docTitle: string;\n docContent: string;\n revisions: readonly DocRevisionForDiff[];\n currentUserId: string | undefined;\n}) {\n const [tab, setTab] = useState<SuggestionStatus>('OPEN');\n return (\n <Card data-testid=\"doc-suggestions\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <FileEdit className=\"h-4 w-4\" aria-hidden=\"true\" />\n Suggestions\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Tabs value={tab} onValueChange={(v) => setTab(v as SuggestionStatus)}>\n <TabsList>\n <TabsTrigger value=\"OPEN\" data-testid=\"doc-suggestions-tab-open\">\n Open\n </TabsTrigger>\n <TabsTrigger value=\"ACCEPTED\" data-testid=\"doc-suggestions-tab-accepted\">\n Accepted\n </TabsTrigger>\n <TabsTrigger value=\"REJECTED\" data-testid=\"doc-suggestions-tab-rejected\">\n Rejected\n </TabsTrigger>\n </TabsList>\n {(['OPEN', 'ACCEPTED', 'REJECTED'] as const).map((status) => (\n <TabsContent key={status} value={status} className=\"mt-3\">\n <SuggestionList\n docId={docId}\n status={status}\n docTitle={docTitle}\n docContent={docContent}\n revisions={revisions}\n currentUserId={currentUserId}\n />\n </TabsContent>\n ))}\n </Tabs>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocVersionCompare.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/docs/DocVersionCompare.tsx"],"names":[],"mappings":"AAwCA,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1C,2CAwGA"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ErrorLine as e } from "../controls/ConfirmButton.js";
|
|
2
|
+
import { isForbidden as t } from "../utils/graphqlErrors.js";
|
|
3
|
+
import { DocDiffHunks as n } from "./DocDiffView.js";
|
|
4
|
+
import { useState as r } from "react";
|
|
5
|
+
import { GitCompare as i } from "lucide-react";
|
|
6
|
+
import { gql as a } from "@apollo/client";
|
|
7
|
+
import { useLazyQuery as o } from "@apollo/client/react";
|
|
8
|
+
import { Fragment as s, jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
9
|
+
import { Button as u } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
//#region src/planmagnet/docs/DocVersionCompare.tsx
|
|
11
|
+
var d = a`
|
|
12
|
+
query PlanmagnetDocRevisionDiffPanel($docId: ID!, $fromVersion: Int!, $toVersion: Int) {
|
|
13
|
+
planmagnetDocRevisionDiff(docId: $docId, fromVersion: $fromVersion, toVersion: $toVersion) {
|
|
14
|
+
docId
|
|
15
|
+
fromVersion
|
|
16
|
+
toVersion
|
|
17
|
+
truncated
|
|
18
|
+
hunks {
|
|
19
|
+
kind
|
|
20
|
+
text
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
function f({ docId: a, versions: f }) {
|
|
26
|
+
let p = [...f].sort((e, t) => t.version - e.version), [m, h] = r(""), [g, _] = r(""), [v, y] = o(d), b = () => {
|
|
27
|
+
m && v({ variables: {
|
|
28
|
+
docId: a,
|
|
29
|
+
fromVersion: Number(m),
|
|
30
|
+
toVersion: g ? Number(g) : void 0
|
|
31
|
+
} });
|
|
32
|
+
}, x = y.data?.planmagnetDocRevisionDiff ?? null;
|
|
33
|
+
return /* @__PURE__ */ l("div", {
|
|
34
|
+
className: "space-y-3 border-t border-border-default pt-3",
|
|
35
|
+
"data-testid": "doc-version-compare",
|
|
36
|
+
children: [/* @__PURE__ */ l("p", {
|
|
37
|
+
className: "flex items-center gap-1 text-xs font-medium text-muted-foreground",
|
|
38
|
+
children: [/* @__PURE__ */ c(i, {
|
|
39
|
+
className: "h-3.5 w-3.5",
|
|
40
|
+
"aria-hidden": "true"
|
|
41
|
+
}), "Compare versions"]
|
|
42
|
+
}), p.length < 2 ? /* @__PURE__ */ c("p", {
|
|
43
|
+
className: "text-xs text-muted-foreground",
|
|
44
|
+
children: "Needs at least two versions to compare."
|
|
45
|
+
}) : /* @__PURE__ */ l(s, { children: [
|
|
46
|
+
/* @__PURE__ */ l("div", {
|
|
47
|
+
className: "grid grid-cols-2 gap-2",
|
|
48
|
+
children: [/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("label", {
|
|
49
|
+
htmlFor: "doc-diff-from",
|
|
50
|
+
className: "text-xs font-medium",
|
|
51
|
+
children: "From"
|
|
52
|
+
}), /* @__PURE__ */ l("select", {
|
|
53
|
+
id: "doc-diff-from",
|
|
54
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm",
|
|
55
|
+
value: m,
|
|
56
|
+
onChange: (e) => {
|
|
57
|
+
let t = e.target.value;
|
|
58
|
+
h(t), _((e) => e === t ? "" : e);
|
|
59
|
+
},
|
|
60
|
+
children: [/* @__PURE__ */ c("option", {
|
|
61
|
+
value: "",
|
|
62
|
+
children: "Select a version"
|
|
63
|
+
}), p.map((e) => /* @__PURE__ */ l("option", {
|
|
64
|
+
value: e.version,
|
|
65
|
+
children: ["Version ", e.version]
|
|
66
|
+
}, e.version))]
|
|
67
|
+
})] }), /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("label", {
|
|
68
|
+
htmlFor: "doc-diff-to",
|
|
69
|
+
className: "text-xs font-medium",
|
|
70
|
+
children: "To"
|
|
71
|
+
}), /* @__PURE__ */ l("select", {
|
|
72
|
+
id: "doc-diff-to",
|
|
73
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm",
|
|
74
|
+
value: g,
|
|
75
|
+
onChange: (e) => _(e.target.value),
|
|
76
|
+
children: [/* @__PURE__ */ c("option", {
|
|
77
|
+
value: "",
|
|
78
|
+
children: "Current"
|
|
79
|
+
}), p.filter((e) => String(e.version) !== m).map((e) => /* @__PURE__ */ l("option", {
|
|
80
|
+
value: e.version,
|
|
81
|
+
children: ["Version ", e.version]
|
|
82
|
+
}, e.version))]
|
|
83
|
+
})] })]
|
|
84
|
+
}),
|
|
85
|
+
/* @__PURE__ */ c(u, {
|
|
86
|
+
size: "sm",
|
|
87
|
+
variant: "outline",
|
|
88
|
+
onClick: b,
|
|
89
|
+
disabled: !m || y.loading,
|
|
90
|
+
"data-testid": "doc-version-compare-run",
|
|
91
|
+
children: y.loading ? "Comparing…" : "Compare"
|
|
92
|
+
}),
|
|
93
|
+
y.error && /* @__PURE__ */ c(e, { message: t(y.error) ? "Your role cannot compare versions of this doc." : `The comparison could not be loaded: ${y.error.message}` }),
|
|
94
|
+
x && /* @__PURE__ */ c(n, {
|
|
95
|
+
hunks: x.hunks,
|
|
96
|
+
truncated: x.truncated,
|
|
97
|
+
testId: "doc-version-diff"
|
|
98
|
+
})
|
|
99
|
+
] })]
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { f as DocVersionCompare };
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=DocVersionCompare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocVersionCompare.js","names":[],"sources":["../../../src/planmagnet/docs/DocVersionCompare.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useLazyQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { GitCompare } from 'lucide-react';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { DocDiffHunks, type DocDiffHunkLike } from './DocDiffView';\n\n/**\n * Version compare in the doc's history panel (CWF-18 §3): pick two SAVED versions and diff them through the\n * server's `planmagnetDocRevisionDiff` (line-based, bounded, same hunk shape `DocDiffHunks` already renders for\n * a suggestion preview). \"To\" left as \"Current\" diffs against the doc's live version, matching the query's own\n * \"omit toVersion = against current\" contract.\n */\n\nconst DOC_REVISION_DIFF = gql`\n query PlanmagnetDocRevisionDiffPanel($docId: ID!, $fromVersion: Int!, $toVersion: Int) {\n planmagnetDocRevisionDiff(docId: $docId, fromVersion: $fromVersion, toVersion: $toVersion) {\n docId\n fromVersion\n toVersion\n truncated\n hunks {\n kind\n text\n }\n }\n }\n`;\n\ninterface RevisionDiffData {\n planmagnetDocRevisionDiff: {\n fromVersion: number;\n toVersion: number;\n truncated: boolean;\n hunks: DocDiffHunkLike[];\n };\n}\n\nexport function DocVersionCompare({\n docId,\n versions,\n}: {\n docId: string;\n versions: readonly { version: number }[];\n}) {\n const sorted = [...versions].sort((a, b) => b.version - a.version);\n const [fromVersion, setFromVersion] = useState('');\n const [toVersion, setToVersion] = useState('');\n const [runDiff, diffState] = useLazyQuery<RevisionDiffData>(DOC_REVISION_DIFF);\n\n const compare = () => {\n if (!fromVersion) return;\n void runDiff({\n variables: {\n docId,\n fromVersion: Number(fromVersion),\n toVersion: toVersion ? Number(toVersion) : undefined,\n },\n });\n };\n\n const diff = diffState.data?.planmagnetDocRevisionDiff ?? null;\n\n return (\n <div\n className=\"space-y-3 border-t border-border-default pt-3\"\n data-testid=\"doc-version-compare\"\n >\n <p className=\"flex items-center gap-1 text-xs font-medium text-muted-foreground\">\n <GitCompare className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Compare versions\n </p>\n {sorted.length < 2 ? (\n <p className=\"text-xs text-muted-foreground\">Needs at least two versions to compare.</p>\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-2\">\n <div>\n <label htmlFor=\"doc-diff-from\" className=\"text-xs font-medium\">\n From\n </label>\n <select\n id=\"doc-diff-from\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\"\n value={fromVersion}\n onChange={(e) => {\n const next = e.target.value;\n setFromVersion(next);\n // Same \"To\" cannot equal \"From\" rule RoadmapVersionsPanel's compare section uses: picking a\n // version as \"From\" that is already selected as \"To\" silently falls the visible \"To\" back to\n // \"Current\" — reset the state to match what a Compare click would actually send.\n setToVersion((prevTo) => (prevTo === next ? '' : prevTo));\n }}\n >\n <option value=\"\">Select a version</option>\n {sorted.map((v) => (\n <option key={v.version} value={v.version}>\n Version {v.version}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"doc-diff-to\" className=\"text-xs font-medium\">\n To\n </label>\n <select\n id=\"doc-diff-to\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\"\n value={toVersion}\n onChange={(e) => setToVersion(e.target.value)}\n >\n <option value=\"\">Current</option>\n {sorted\n .filter((v) => String(v.version) !== fromVersion)\n .map((v) => (\n <option key={v.version} value={v.version}>\n Version {v.version}\n </option>\n ))}\n </select>\n </div>\n </div>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={compare}\n disabled={!fromVersion || diffState.loading}\n data-testid=\"doc-version-compare-run\"\n >\n {diffState.loading ? 'Comparing…' : 'Compare'}\n </Button>\n {diffState.error && (\n <ErrorLine\n message={\n isForbidden(diffState.error)\n ? 'Your role cannot compare versions of this doc.'\n : `The comparison could not be loaded: ${diffState.error.message}`\n }\n />\n )}\n {diff && (\n <DocDiffHunks hunks={diff.hunks} truncated={diff.truncated} testId=\"doc-version-diff\" />\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAgBA,IAAM,IAAoB,CAAG;;;;;;;;;;;;;;AAwB7B,SAAgB,EAAkB,EAChC,UACA,eAIC;CACD,IAAM,IAAS,CAAC,GAAG,EAAS,CAAC,MAAM,GAAG,MAAM,EAAE,UAAU,EAAE,QAAQ,EAC5D,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAS,KAAa,EAA+B,EAAkB,EAExE,UAAgB;AACf,OACA,EAAQ,EACX,WAAW;GACT;GACA,aAAa,OAAO,EAAY;GAChC,WAAW,IAAY,OAAO,EAAU,GAAG,KAAA;GAC5C,EACF,CAAC;IAGE,IAAO,EAAU,MAAM,6BAA6B;AAE1D,QACE,kBAAC,OAAD;EACE,WAAU;EACV,eAAY;YAFd,CAIE,kBAAC,KAAD;GAAG,WAAU;aAAb,CACE,kBAAC,GAAD;IAAY,WAAU;IAAc,eAAY;IAAS,CAAA,EAAA,mBAEvD;MACH,EAAO,SAAS,IACf,kBAAC,KAAD;GAAG,WAAU;aAAgC;GAA2C,CAAA,GAExF,kBAAA,GAAA,EAAA,UAAA;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,SAAQ;KAAgB,WAAU;eAAsB;KAEvD,CAAA,EACR,kBAAC,UAAD;KACE,IAAG;KACH,WAAU;KACV,OAAO;KACP,WAAW,MAAM;MACf,IAAM,IAAO,EAAE,OAAO;AAKtB,MAJA,EAAe,EAAK,EAIpB,GAAc,MAAY,MAAW,IAAO,KAAK,EAAQ;;eAV7D,CAaE,kBAAC,UAAD;MAAQ,OAAM;gBAAG;MAAyB,CAAA,EACzC,EAAO,KAAK,MACX,kBAAC,UAAD;MAAwB,OAAO,EAAE;gBAAjC,CAA0C,YAC/B,EAAE,QACJ;QAFI,EAAE,QAEN,CACT,CACK;OACL,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,SAAQ;KAAc,WAAU;eAAsB;KAErD,CAAA,EACR,kBAAC,UAAD;KACE,IAAG;KACH,WAAU;KACV,OAAO;KACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;eAJ/C,CAME,kBAAC,UAAD;MAAQ,OAAM;gBAAG;MAAgB,CAAA,EAChC,EACE,QAAQ,MAAM,OAAO,EAAE,QAAQ,KAAK,EAAY,CAChD,KAAK,MACJ,kBAAC,UAAD;MAAwB,OAAO,EAAE;gBAAjC,CAA0C,YAC/B,EAAE,QACJ;QAFI,EAAE,QAEN,CACT,CACG;OACL,EAAA,CAAA,CACF;;GACN,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,SAAS;IACT,UAAU,CAAC,KAAe,EAAU;IACpC,eAAY;cAEX,EAAU,UAAU,eAAe;IAC7B,CAAA;GACR,EAAU,SACT,kBAAC,GAAD,EACE,SACE,EAAY,EAAU,MAAM,GACxB,mDACA,uCAAuC,EAAU,MAAM,WAE7D,CAAA;GAEH,KACC,kBAAC,GAAD;IAAc,OAAO,EAAK;IAAO,WAAW,EAAK;IAAW,QAAO;IAAqB,CAAA;GAEzF,EAAA,CAAA,CAED"}
|