@burdenoff/microfe-planmagnet 2026.925.15 → 2026.925.17
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/wspace-types.d.ts +50 -0
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +67 -59
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/docs/DocStatusBadge.d.ts +8 -0
- package/dist/planmagnet/docs/DocStatusBadge.d.ts.map +1 -0
- package/dist/planmagnet/docs/DocStatusBadge.js +28 -0
- package/dist/planmagnet/docs/DocStatusBadge.js.map +1 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +414 -0
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/DocsDocsPagePage.d.ts +3 -0
- package/dist/planmagnet/pages/DocsDocsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/DocsDocsPagePage.js +224 -0
- package/dist/planmagnet/pages/DocsDocsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +105 -98
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +130 -128
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/releases/ReleaseGatesCard.d.ts +7 -0
- package/dist/planmagnet/releases/ReleaseGatesCard.d.ts.map +1 -1
- package/dist/planmagnet/releases/ReleaseGatesCard.js +1 -1
- package/dist/planmagnet/releases/ReleaseGatesCard.js.map +1 -1
- package/dist/planmagnet/releases/ReleaseReadinessCard.d.ts +7 -0
- package/dist/planmagnet/releases/ReleaseReadinessCard.d.ts.map +1 -0
- package/dist/planmagnet/releases/ReleaseReadinessCard.js +390 -0
- package/dist/planmagnet/releases/ReleaseReadinessCard.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TasksTaskDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WorkItemComments } from '../comments/WorkItemComments';\nimport { WorkItemActivity } from '../comments/WorkItemActivity';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { FileText, Plus, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItemDetail($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n title\n description\n type\n status\n priority\n key\n assigneeId\n reporterId\n sprintId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n updatedAt\n }\n }\n`;\n\nconst GET_WORK_ITEM_DOCS = gql`\n query GetWorkItemDocs($workItemId: ID!) {\n planmagnetWorkItemDocs(workItemId: $workItemId) {\n id\n title\n status\n version\n updatedAt\n }\n }\n`;\n\nconst CREATE_DOC = gql`\n mutation CreateDocForWorkItem($input: PlanmagnetCreateDocInput!) {\n planmagnetCreateDoc(input: $input) {\n id\n }\n }\n`;\n\nconst LINK_DOC_TO_WORK_ITEM = gql`\n mutation LinkDocToWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetLinkDocToWorkItem(docId: $docId, workItemId: $workItemId) {\n id\n }\n }\n`;\n\nconst UNLINK_DOC_FROM_WORK_ITEM = gql`\n mutation UnlinkDocFromWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n sprintId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n}\n\ninterface WorkItemDocNode {\n id: string;\n title: string;\n status: string;\n version: number;\n updatedAt: string;\n}\n\nconst priorityColor: Record<string, string> = {\n LOW: 'bg-gray-100 text-gray-800',\n MEDIUM: 'bg-blue-100 text-blue-800',\n HIGH: 'bg-orange-100 text-orange-800',\n CRITICAL: 'bg-red-100 text-red-800',\n};\n\nconst statusColor: Record<string, string> = {\n BACKLOG: 'bg-gray-100 text-gray-800',\n TODO: 'bg-blue-100 text-blue-800',\n IN_PROGRESS: 'bg-yellow-100 text-yellow-800',\n IN_REVIEW: 'bg-purple-100 text-purple-800',\n DONE: 'bg-green-100 text-green-800',\n BLOCKED: 'bg-red-100 text-red-800',\n TESTING: 'bg-teal-100 text-teal-800',\n CANCELLED: 'bg-gray-100 text-gray-800',\n};\n\nconst docStatusColor: Record<string, string> = {\n DRAFT: 'bg-gray-100 text-gray-800',\n PUBLISHED: 'bg-green-100 text-green-800',\n ARCHIVED: 'bg-gray-100 text-gray-500',\n};\n\n/**\n * Linked-docs panel (BOFF-4443): create a doc and link it to this work item\n * in one action, view currently-linked docs, and unlink one. Reads/writes go\n * through the real planmagnetWorkItemDocs / planmagnetCreateDoc /\n * planmagnetLinkDocToWorkItem / planmagnetUnlinkDocFromWorkItem GraphQL\n * fields — no fabricated data. Full doc editing (revisions, revert) lives on\n * a dedicated doc detail view; this panel is scoped to linking.\n */\nfunction LinkedDocsPanel({ workItemId }: { workItemId: string }) {\n const [isComposing, setIsComposing] = useState(false);\n const [newTitle, setNewTitle] = useState('');\n const [newContent, setNewContent] = useState('');\n\n const { data, loading, error, refetch } = useQuery<{\n planmagnetWorkItemDocs: WorkItemDocNode[];\n }>(GET_WORK_ITEM_DOCS, {\n variables: { workItemId },\n skip: !workItemId,\n });\n\n const [createDoc, { loading: creating }] = useMutation(CREATE_DOC);\n const [linkDoc] = useMutation(LINK_DOC_TO_WORK_ITEM);\n const [unlinkDoc, { loading: unlinking }] = useMutation(UNLINK_DOC_FROM_WORK_ITEM);\n // A refused mutation resolves with { error } (fe-libs errorPolicy 'all');\n // the composer used to clear itself as if the doc had been saved.\n const [actionError, setActionError] = useState<string | null>(null);\n\n const docs = data?.planmagnetWorkItemDocs ?? [];\n\n const handleCreateAndLink = async () => {\n if (!newTitle.trim() || !newContent.trim()) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n createDoc({ variables: { input: { title: newTitle.trim(), content: newContent.trim() } } })\n );\n const docId = result.data?.planmagnetCreateDoc?.id;\n if (docId) {\n await throwOnError(linkDoc({ variables: { docId, workItemId } }));\n }\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be saved.');\n return;\n }\n setNewTitle('');\n setNewContent('');\n setIsComposing(false);\n void refetch();\n };\n\n const handleUnlink = async (docId: string) => {\n setActionError(null);\n try {\n await throwOnError(unlinkDoc({ variables: { docId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be unlinked.');\n return;\n }\n void refetch();\n };\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle>Linked Docs</CardTitle>\n {!isComposing && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsComposing(true)}>\n <Plus className=\"h-4 w-4 mr-1\" />\n New Doc\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {isComposing && (\n <div className=\"space-y-3 rounded-md border border-input p-3\">\n <Input\n placeholder=\"Doc title\"\n value={newTitle}\n onChange={(e) => setNewTitle(e.target.value)}\n />\n <MarkdownEditor\n value={newContent}\n onChange={setNewContent}\n placeholder=\"Write the spec/doc content...\"\n minHeight={140}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsComposing(false);\n setNewTitle('');\n setNewContent('');\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n disabled={creating || !newTitle.trim() || !newContent.trim()}\n onClick={handleCreateAndLink}\n >\n {creating ? 'Creating...' : 'Create & Link'}\n </Button>\n </div>\n </div>\n )}\n\n {loading && <p className=\"text-sm text-muted-foreground\">Loading docs...</p>}\n {error && <p className=\"text-sm text-red-500\">Error: {error.message}</p>}\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {actionError}\n </p>\n )}\n {!loading && !error && docs.length === 0 && !isComposing && (\n <p className=\"text-sm text-muted-foreground\">No docs linked to this work item yet.</p>\n )}\n {docs.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <FileText className=\"h-4 w-4 text-muted-foreground shrink-0\" />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium truncate\">{doc.title}</p>\n <p className=\"text-xs text-muted-foreground\">v{doc.version}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${docStatusColor[doc.status] ?? ''}`}\n >\n {doc.status.toLowerCase()}\n </span>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinking}\n onClick={() => handleUnlink(doc.id)}\n aria-label={`Unlink ${doc.title}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n );\n}\n\nexport function TasksTaskDetailPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from route params.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const task = data?.planmagnetWorkItem;\n if (!task) return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n\n const statusDisplay = task.status.toLowerCase().replace('_', ' ');\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{task.title}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {task.key} · {task.type.toLowerCase()}\n </p>\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{task.description ?? 'No description provided.'}</p>\n </CardContent>\n </Card>\n <LinkedDocsPanel workItemId={task.id} />\n <WorkItemComments workItemId={task.id} />\n <WorkItemActivity workItemId={task.id} />\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Status</span>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${statusColor[task.status] ?? ''}`}\n >\n {statusDisplay}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Priority</span>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${priorityColor[task.priority] ?? ''}`}\n >\n {task.priority.toLowerCase()}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Type</span>\n <Badge variant=\"outline\" className=\"capitalize\">\n {task.type.toLowerCase()}\n </Badge>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Story Points</span>\n <span className=\"font-medium\">{task.storyPoints ?? 0}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Assignee</span>\n <span>{task.assigneeId ?? 'Unassigned'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reporter</span>\n <span>{task.reporterId ?? 'Unknown'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span>{task.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Due Date</span>\n <span>{task.dueDate ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{task.createdAt}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{task.updatedAt}</span>\n </div>\n <div>\n <span className=\"text-muted-foreground text-xs\">Labels</span>\n <div className=\"flex gap-1 mt-1 flex-wrap\">\n {(task.labels ?? []).map((l) => (\n <span key={l} className=\"text-xs px-2 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAqBA,IAAM,IAAgB,CAAG;;;;;;;;;;;;;;;;;;;;;;GAwBnB,IAAqB,CAAG;;;;;;;;;;GAYxB,IAAa,CAAG;;;;;;GAQhB,IAAwB,CAAG;;;;;;GAQ3B,IAA4B,CAAG;;;;GAkC/B,IAAwC;CAC5C,KAAK;CACL,QAAQ;CACR,MAAM;CACN,UAAU;CACX,EAEK,IAAsC;CAC1C,SAAS;CACT,MAAM;CACN,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;CACT,SAAS;CACT,WAAW;CACZ,EAEK,IAAyC;CAC7C,OAAO;CACP,WAAW;CACX,UAAU;CACX;AAUD,SAAS,EAAgB,EAAE,iBAAsC;CAC/D,IAAM,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAU,KAAe,EAAS,GAAG,EACtC,CAAC,GAAY,KAAiB,EAAS,GAAG,EAE1C,EAAE,SAAM,YAAS,UAAO,eAAY,EAEvC,GAAoB;EACrB,WAAW,EAAE,eAAY;EACzB,MAAM,CAAC;EACR,CAAC,EAEI,CAAC,GAAW,EAAE,SAAS,OAAc,EAAY,EAAW,EAC5D,CAAC,KAAW,EAAY,EAAsB,EAC9C,CAAC,GAAW,EAAE,SAAS,OAAe,EAAY,EAA0B,EAG5E,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAE7D,IAAO,GAAM,0BAA0B,EAAE,EAEzC,IAAsB,YAAY;AAClC,SAAC,EAAS,MAAM,IAAI,CAAC,EAAW,MAAM,GAC1C;KAAe,KAAK;AACpB,OAAI;IAIF,IAAM,KAHS,MAAM,EACnB,EAAU,EAAE,WAAW,EAAE,OAAO;KAAE,OAAO,EAAS,MAAM;KAAE,SAAS,EAAW,MAAM;KAAE,EAAE,EAAE,CAAC,CAC5F,EACoB,MAAM,qBAAqB;AAChD,IAAI,KACF,MAAM,EAAa,EAAQ,EAAE,WAAW;KAAE;KAAO;KAAY,EAAE,CAAC,CAAC;YAE5D,GAAG;AACV,MAAe,aAAa,QAAQ,EAAE,UAAU,8BAA8B;AAC9E;;AAKG,GAHL,EAAY,GAAG,EACf,EAAc,GAAG,EACjB,EAAe,GAAM,EAChB,GAAS;;IAGV,IAAe,OAAO,MAAkB;AAC5C,IAAe,KAAK;AACpB,MAAI;AACF,SAAM,EAAa,EAAU,EAAE,WAAW;IAAE;IAAO;IAAY,EAAE,CAAC,CAAC;WAC5D,GAAG;AACV,KAAe,aAAa,QAAQ,EAAE,UAAU,iCAAiC;AACjF;;AAEG,KAAS;;AAGhB,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAY,WAAU;YAAtB,CACE,kBAAC,GAAD,EAAA,UAAW,eAAuB,CAAA,EACjC,CAAC,KACA,kBAAC,GAAD;GAAQ,MAAK;GAAK,SAAQ;GAAU,eAAe,EAAe,GAAK;aAAvE,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,UAE1B;KAEA;KACb,kBAAC,GAAD;EAAa,WAAU;YAAvB;GACG,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,aAAY;MACZ,OAAO;MACP,WAAW,MAAM,EAAY,EAAE,OAAO,MAAM;MAC5C,CAAA;KACF,kBAAC,GAAD;MACE,OAAO;MACP,UAAU;MACV,aAAY;MACZ,WAAW;MACX,CAAA;KACF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,MAAK;OACL,SAAQ;OACR,eAAe;AAGb,QAFA,EAAe,GAAM,EACrB,EAAY,GAAG,EACf,EAAc,GAAG;;iBAEpB;OAEQ,CAAA,EACT,kBAAC,GAAD;OACE,MAAK;OACL,UAAU,KAAY,CAAC,EAAS,MAAM,IAAI,CAAC,EAAW,MAAM;OAC5D,SAAS;iBAER,IAAW,gBAAgB;OACrB,CAAA,CACL;;KACF;;GAGP,KAAW,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAAmB,CAAA;GAC3E,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAb,CAAoC,WAAQ,EAAM,QAAY;;GACvE,KACC,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cACvB;IACC,CAAA;GAEL,CAAC,KAAW,CAAC,KAAS,EAAK,WAAW,KAAK,CAAC,KAC3C,kBAAC,KAAD;IAAG,WAAU;cAAgC;IAAyC,CAAA;GAEvF,EAAK,KAAK,MACT,kBAAC,OAAD;IAEE,WAAU;cAFZ,CAIE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAU,WAAU,0CAA2C,CAAA,EAC/D,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,KAAD;OAAG,WAAU;iBAAgC,EAAI;OAAU,CAAA,EAC3D,kBAAC,KAAD;OAAG,WAAU;iBAAb,CAA6C,KAAE,EAAI,QAAY;SAC3D;QACF;QACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MACE,WAAW,+CAA+C,EAAe,EAAI,WAAW;gBAEvF,EAAI,OAAO,aAAa;MACpB,CAAA,EACP,kBAAC,GAAD;MACE,MAAK;MACL,SAAQ;MACR,UAAU;MACV,eAAe,EAAa,EAAI,GAAG;MACnC,cAAY,UAAU,EAAI;gBAE1B,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA,CACL;OACF;MA1BC,EAAI,GA0BL,CACN;GACU;IACT,EAAA,CAAA;;AAIX,SAAgB,IAA0B;CACxC,IAAM,EAAE,mBAAgB,GAAe,EAGjC,IAAa,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAAC,IAAI,SAAS,IAAI,IAE1E,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAe;EAChB,WAAW,EAAE,eAAY;EACzB,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC;AAMF,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,kFAAmF,CAAA;EAC9F,CAAA;AAIV,KAAI,EACF,QAAO,kBAAC,OAAD;EAAK,WAAU;YAAf,CAAkC,WAAQ,EAAM,QAAc;;CAGvE,IAAM,IAAO,GAAM;AACnB,KAAI,CAAC,EAAM,QAAO,kBAAC,OAAD;EAAK,WAAU;YAA4B;EAAqB,CAAA;CAElF,IAAM,IAAgB,EAAK,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;AAEjE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA0B,EAAK;GAAW,CAAA,EACxD,kBAAC,KAAD;GAAG,WAAU;aAAb;IACG,EAAK;IAAI;IAAW,EAAK,KAAK,aAAa;IAC1C;KACA,EAAA,CAAA,EACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,eAAuB,CAAA,EACvB,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,KAAD;MAAG,WAAU;gBAAW,EAAK,eAAe;MAA+B,CAAA,EAC/D,CAAA,CACT,EAAA,CAAA;KACP,kBAAC,GAAD,EAAiB,YAAY,EAAK,IAAM,CAAA;KACxC,kBAAC,GAAD,EAAkB,YAAY,EAAK,IAAM,CAAA;KACzC,kBAAC,GAAD,EAAkB,YAAY,EAAK,IAAM,CAAA;KACrC;OACN,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,WAAmB,CAAA,EACnB,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eAAvB;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAa,CAAA,EACrD,kBAAC,QAAD;QACE,WAAW,+CAA+C,EAAY,EAAK,WAAW;kBAErF;QACI,CAAA,CACH;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAe,CAAA,EACvD,kBAAC,QAAD;QACE,WAAW,+CAA+C,EAAc,EAAK,aAAa;kBAEzF,EAAK,SAAS,aAAa;QACvB,CAAA,CACH;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAW,CAAA,EACnD,kBAAC,GAAD;QAAO,SAAQ;QAAU,WAAU;kBAChC,EAAK,KAAK,aAAa;QAClB,CAAA,CACJ;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAmB,CAAA,EAC3D,kBAAC,QAAD;QAAM,WAAU;kBAAe,EAAK,eAAe;QAAS,CAAA,CACxD;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAe,CAAA,EACvD,kBAAC,QAAD,EAAA,UAAO,EAAK,cAAc,cAAoB,CAAA,CAC1C;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAe,CAAA,EACvD,kBAAC,QAAD,EAAA,UAAO,EAAK,cAAc,WAAiB,CAAA,CACvC;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAU,CAAA,EAClD,kBAAC,QAAD,EAAA,UAAO,EAAK,KAAW,CAAA,CACnB;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAe,CAAA,EACvD,kBAAC,QAAD,EAAA,UAAO,EAAK,WAAW,WAAiB,CAAA,CACpC;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAc,CAAA,EACtD,kBAAC,QAAD,EAAA,UAAO,EAAK,WAAiB,CAAA,CACzB;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAwB;QAAc,CAAA,EACtD,kBAAC,QAAD,EAAA,UAAO,EAAK,WAAiB,CAAA,CACzB;;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAgC;OAAa,CAAA,EAC7D,kBAAC,OAAD;OAAK,WAAU;kBACX,EAAK,UAAU,EAAE,EAAE,KAAK,MACxB,kBAAC,QAAD;QAAc,WAAU;kBACrB;QACI,EAFI,EAEJ,CACP;OACE,CAAA,CACF,EAAA,CAAA;MACM;OACT,EAAA,CAAA;IACH,CAAA,CACF;KACF"}
|
|
1
|
+
{"version":3,"file":"TasksTaskDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WorkItemComments } from '../comments/WorkItemComments';\nimport { WorkItemActivity } from '../comments/WorkItemActivity';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { FileText, Plus, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItemDetail($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n title\n description\n type\n status\n priority\n key\n assigneeId\n reporterId\n sprintId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n updatedAt\n }\n }\n`;\n\nconst GET_WORK_ITEM_DOCS = gql`\n query GetWorkItemDocs($workItemId: ID!) {\n planmagnetWorkItemDocs(workItemId: $workItemId) {\n id\n title\n status\n version\n updatedAt\n }\n }\n`;\n\nconst CREATE_DOC = gql`\n mutation CreateDocForWorkItem($input: PlanmagnetCreateDocInput!) {\n planmagnetCreateDoc(input: $input) {\n id\n }\n }\n`;\n\nconst LINK_DOC_TO_WORK_ITEM = gql`\n mutation LinkDocToWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetLinkDocToWorkItem(docId: $docId, workItemId: $workItemId) {\n id\n }\n }\n`;\n\nconst UNLINK_DOC_FROM_WORK_ITEM = gql`\n mutation UnlinkDocFromWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n sprintId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n}\n\ninterface WorkItemDocNode {\n id: string;\n title: string;\n status: string;\n version: number;\n updatedAt: string;\n}\n\nconst priorityColor: Record<string, string> = {\n LOW: 'bg-gray-100 text-gray-800',\n MEDIUM: 'bg-blue-100 text-blue-800',\n HIGH: 'bg-orange-100 text-orange-800',\n CRITICAL: 'bg-red-100 text-red-800',\n};\n\nconst statusColor: Record<string, string> = {\n BACKLOG: 'bg-gray-100 text-gray-800',\n TODO: 'bg-blue-100 text-blue-800',\n IN_PROGRESS: 'bg-yellow-100 text-yellow-800',\n IN_REVIEW: 'bg-purple-100 text-purple-800',\n DONE: 'bg-green-100 text-green-800',\n BLOCKED: 'bg-red-100 text-red-800',\n TESTING: 'bg-teal-100 text-teal-800',\n CANCELLED: 'bg-gray-100 text-gray-800',\n};\n\nconst docStatusColor: Record<string, string> = {\n DRAFT: 'bg-gray-100 text-gray-800',\n PUBLISHED: 'bg-green-100 text-green-800',\n ARCHIVED: 'bg-gray-100 text-gray-500',\n};\n\n/**\n * Linked-docs panel (BOFF-4443): create a doc and link it to this work item\n * in one action, view currently-linked docs, and unlink one. Reads/writes go\n * through the real planmagnetWorkItemDocs / planmagnetCreateDoc /\n * planmagnetLinkDocToWorkItem / planmagnetUnlinkDocFromWorkItem GraphQL\n * fields — no fabricated data. Full doc editing (revisions, revert) lives on\n * a dedicated doc detail view; this panel is scoped to linking.\n */\nfunction LinkedDocsPanel({ workItemId }: { workItemId: string }) {\n const navigate = usePlanMagnetNavigate();\n const [isComposing, setIsComposing] = useState(false);\n const [newTitle, setNewTitle] = useState('');\n const [newContent, setNewContent] = useState('');\n\n const { data, loading, error, refetch } = useQuery<{\n planmagnetWorkItemDocs: WorkItemDocNode[];\n }>(GET_WORK_ITEM_DOCS, {\n variables: { workItemId },\n skip: !workItemId,\n });\n\n const [createDoc, { loading: creating }] = useMutation(CREATE_DOC);\n const [linkDoc] = useMutation(LINK_DOC_TO_WORK_ITEM);\n const [unlinkDoc, { loading: unlinking }] = useMutation(UNLINK_DOC_FROM_WORK_ITEM);\n // A refused mutation resolves with { error } (fe-libs errorPolicy 'all');\n // the composer used to clear itself as if the doc had been saved.\n const [actionError, setActionError] = useState<string | null>(null);\n\n const docs = data?.planmagnetWorkItemDocs ?? [];\n\n const handleCreateAndLink = async () => {\n if (!newTitle.trim() || !newContent.trim()) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n createDoc({ variables: { input: { title: newTitle.trim(), content: newContent.trim() } } })\n );\n const docId = result.data?.planmagnetCreateDoc?.id;\n if (docId) {\n await throwOnError(linkDoc({ variables: { docId, workItemId } }));\n }\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be saved.');\n return;\n }\n setNewTitle('');\n setNewContent('');\n setIsComposing(false);\n void refetch();\n };\n\n const handleUnlink = async (docId: string) => {\n setActionError(null);\n try {\n await throwOnError(unlinkDoc({ variables: { docId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be unlinked.');\n return;\n }\n void refetch();\n };\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle>Linked Docs</CardTitle>\n {!isComposing && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsComposing(true)}>\n <Plus className=\"h-4 w-4 mr-1\" />\n New Doc\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {isComposing && (\n <div className=\"space-y-3 rounded-md border border-input p-3\">\n <Input\n placeholder=\"Doc title\"\n value={newTitle}\n onChange={(e) => setNewTitle(e.target.value)}\n />\n <MarkdownEditor\n value={newContent}\n onChange={setNewContent}\n placeholder=\"Write the spec/doc content...\"\n minHeight={140}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsComposing(false);\n setNewTitle('');\n setNewContent('');\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n disabled={creating || !newTitle.trim() || !newContent.trim()}\n onClick={handleCreateAndLink}\n >\n {creating ? 'Creating...' : 'Create & Link'}\n </Button>\n </div>\n </div>\n )}\n\n {loading && <p className=\"text-sm text-muted-foreground\">Loading docs...</p>}\n {error && <p className=\"text-sm text-red-500\">Error: {error.message}</p>}\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {actionError}\n </p>\n )}\n {!loading && !error && docs.length === 0 && !isComposing && (\n <p className=\"text-sm text-muted-foreground\">No docs linked to this work item yet.</p>\n )}\n {docs.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <FileText className=\"h-4 w-4 text-muted-foreground shrink-0\" />\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n className=\"block max-w-full truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/docs/doc-detail-page?docId=${encodeURIComponent(doc.id)}`)\n }\n >\n {doc.title}\n </button>\n <p className=\"text-xs text-muted-foreground\">v{doc.version}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${docStatusColor[doc.status] ?? ''}`}\n >\n {doc.status.toLowerCase()}\n </span>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinking}\n onClick={() => handleUnlink(doc.id)}\n aria-label={`Unlink ${doc.title}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n );\n}\n\nexport function TasksTaskDetailPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from route params.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const task = data?.planmagnetWorkItem;\n if (!task) return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n\n const statusDisplay = task.status.toLowerCase().replace('_', ' ');\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{task.title}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {task.key} · {task.type.toLowerCase()}\n </p>\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{task.description ?? 'No description provided.'}</p>\n </CardContent>\n </Card>\n <LinkedDocsPanel workItemId={task.id} />\n <WorkItemComments workItemId={task.id} />\n <WorkItemActivity workItemId={task.id} />\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Status</span>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${statusColor[task.status] ?? ''}`}\n >\n {statusDisplay}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Priority</span>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${priorityColor[task.priority] ?? ''}`}\n >\n {task.priority.toLowerCase()}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Type</span>\n <Badge variant=\"outline\" className=\"capitalize\">\n {task.type.toLowerCase()}\n </Badge>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Story Points</span>\n <span className=\"font-medium\">{task.storyPoints ?? 0}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Assignee</span>\n <span>{task.assigneeId ?? 'Unassigned'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reporter</span>\n <span>{task.reporterId ?? 'Unknown'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span>{task.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Due Date</span>\n <span>{task.dueDate ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{task.createdAt}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{task.updatedAt}</span>\n </div>\n <div>\n <span className=\"text-muted-foreground text-xs\">Labels</span>\n <div className=\"flex gap-1 mt-1 flex-wrap\">\n {(task.labels ?? []).map((l) => (\n <span key={l} className=\"text-xs px-2 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gates holding a release (P1.4). The service refuses RELEASED while
|
|
3
|
+
* any gate here is not PASSED or WAIVED; this card shows which gates hold the
|
|
4
|
+
* release, and lets a reviewer add, pass, fail or waive them. There was no
|
|
5
|
+
* quality-gate UI anywhere before this.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RELEASE_GATES: import('graphql').DocumentNode;
|
|
1
8
|
export declare function ReleaseGatesCard({ releaseId, projectId, releaseStatus, }: {
|
|
2
9
|
releaseId: string;
|
|
3
10
|
projectId: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReleaseGatesCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/releases/ReleaseGatesCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReleaseGatesCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/releases/ReleaseGatesCard.tsx"],"names":[],"mappings":"AAQA;;;;;GAKG;AAEH,eAAO,MAAM,aAAa,gCAazB,CAAC;AAsDF,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,SAAS,EACT,aAAa,GACd,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;CACvB,2CA8HA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReleaseGatesCard.js","names":[],"sources":["../../../src/planmagnet/releases/ReleaseGatesCard.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle, Input } from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, CircleDashed, ShieldAlert, XCircle } from 'lucide-react';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Quality gates holding a release (P1.4). The service refuses RELEASED while\n * any gate here is not PASSED or WAIVED; this card shows which gates hold the\n * release, and lets a reviewer add, pass, fail or waive them. There was no\n * quality-gate UI anywhere before this.\n */\n\nconst GATES = gql`\n query ReleaseGates($id: ID!) {\n planmagnetRelease(id: $id) {\n id\n qualityGates {\n id\n name\n status\n reviewerId\n reviewedAt\n }\n }\n }\n`;\nconst CREATE_GATE = gql`\n mutation ReleaseGateCreate($input: PlanmagnetCreateQualityGateInput!) {\n planmagnetCreateQualityGate(input: $input) {\n id\n }\n }\n`;\nconst REVIEW_GATE = gql`\n mutation ReleaseGateReview($id: ID!, $input: PlanmagnetReviewQualityGateInput!) {\n planmagnetReviewQualityGate(id: $id, input: $input) {\n id\n status\n }\n }\n`;\n\ninterface Gate {\n id: string;\n name: string;\n status: string;\n reviewerId: string | null;\n reviewedAt: string | null;\n}\n\nconst CLEARING = new Set(['PASSED', 'WAIVED']);\n\nfunction StatusMark({ status }: { status: string }) {\n // Status carries an icon AND a label, never colour alone.\n if (status === 'PASSED')\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Passed\n </span>\n );\n if (status === 'WAIVED')\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Waived\n </span>\n );\n if (status === 'FAILED')\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-error-text\">\n <XCircle className=\"h-3.5 w-3.5\" /> Failed\n </span>\n );\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-warning-text\">\n <CircleDashed className=\"h-3.5 w-3.5\" /> {status === 'IN_REVIEW' ? 'In review' : 'Pending'}\n </span>\n );\n}\n\nexport function ReleaseGatesCard({\n releaseId,\n projectId,\n releaseStatus,\n}: {\n releaseId: string;\n projectId: string | null;\n releaseStatus: string;\n}) {\n const [name, setName] = useState('');\n const [error, setError] = useState<string | null>(null);\n const { data } = useQuery<{ planmagnetRelease: { qualityGates: Gate[] } | null }>(GATES, {\n variables: { id: releaseId },\n });\n const refetch = { refetchQueries: ['ReleaseGates', 'GetRelease'], awaitRefetchQueries: true };\n const [create, createState] = useMutation(CREATE_GATE, refetch);\n const [review, reviewState] = useMutation(REVIEW_GATE, refetch);\n\n const gates = data?.planmagnetRelease?.qualityGates ?? [];\n const open = gates.filter((g) => !CLEARING.has(g.status));\n const shipped = releaseStatus === 'RELEASED';\n\n const run = async (\n action: () => Promise<{ error?: unknown }>,\n fallback: string,\n after?: () => void\n ) => {\n setError(null);\n try {\n await throwOnError(action());\n after?.();\n } catch (e) {\n setError(e instanceof Error ? e.message : fallback);\n }\n };\n\n return (\n <Card data-testid=\"release-gates\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <ShieldAlert className=\"h-4 w-4\" /> Quality Gates\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n {!shipped && open.length > 0 && (\n <p className=\"text-sm text-status-warning-text\" data-testid=\"release-held\">\n This release cannot be released until {open.length} gate{open.length === 1 ? '' : 's'}{' '}\n {open.length === 1 ? 'is' : 'are'} passed or waived:{' '}\n {open.map((g) => g.name).join(', ')}.\n </p>\n )}\n {gates.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No quality gates hold this release.</p>\n )}\n <ul className=\"space-y-2\">\n {gates.map((gate) => (\n <li\n key={gate.id}\n className=\"rounded-md border p-2 space-y-1\"\n data-testid=\"release-gate\"\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium\">{gate.name}</span>\n <StatusMark status={gate.status} />\n </div>\n {gate.reviewerId && (\n <p className=\"text-xs text-muted-foreground\">\n Reviewed by <PersonName id={gate.reviewerId} />\n {gate.reviewedAt ? ` · ${new Date(gate.reviewedAt).toLocaleString()}` : ''}\n </p>\n )}\n <div className=\"flex gap-1\">\n {(['PASSED', 'FAILED', 'WAIVED'] as const).map((status) => (\n <Button\n key={status}\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={reviewState.loading || gate.status === status}\n onClick={() =>\n run(\n () => review({ variables: { id: gate.id, input: { status } } }),\n 'The gate could not be reviewed.'\n )\n }\n >\n {status === 'PASSED' ? 'Pass' : status === 'FAILED' ? 'Fail' : 'Waive'}\n </Button>\n ))}\n </div>\n </li>\n ))}\n </ul>\n {projectId ? (\n <div className=\"flex gap-2 border-t pt-3\">\n <Input\n aria-label=\"New gate name\"\n placeholder=\"Add a gate, e.g. Security review\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n />\n <Button\n size=\"sm\"\n disabled={!name.trim() || createState.loading}\n data-testid=\"release-gate-add\"\n onClick={() =>\n run(\n () =>\n create({\n variables: {\n input: { projectId, releaseId, name: name.trim(), criteria: {} },\n },\n }),\n 'The gate could not be added.',\n () => setName('')\n )\n }\n >\n Add gate\n </Button>\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n This release has no project, so gates cannot be added here.\n </p>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReleaseGatesCard.js","names":[],"sources":["../../../src/planmagnet/releases/ReleaseGatesCard.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle, Input } from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, CircleDashed, ShieldAlert, XCircle } from 'lucide-react';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * Quality gates holding a release (P1.4). The service refuses RELEASED while\n * any gate here is not PASSED or WAIVED; this card shows which gates hold the\n * release, and lets a reviewer add, pass, fail or waive them. There was no\n * quality-gate UI anywhere before this.\n */\n\nexport const RELEASE_GATES = gql`\n query ReleaseGates($id: ID!) {\n planmagnetRelease(id: $id) {\n id\n qualityGates {\n id\n name\n status\n reviewerId\n reviewedAt\n }\n }\n }\n`;\nconst CREATE_GATE = gql`\n mutation ReleaseGateCreate($input: PlanmagnetCreateQualityGateInput!) {\n planmagnetCreateQualityGate(input: $input) {\n id\n }\n }\n`;\nconst REVIEW_GATE = gql`\n mutation ReleaseGateReview($id: ID!, $input: PlanmagnetReviewQualityGateInput!) {\n planmagnetReviewQualityGate(id: $id, input: $input) {\n id\n status\n }\n }\n`;\n\ninterface Gate {\n id: string;\n name: string;\n status: string;\n reviewerId: string | null;\n reviewedAt: string | null;\n}\n\nconst CLEARING = new Set(['PASSED', 'WAIVED']);\n\nfunction StatusMark({ status }: { status: string }) {\n // Status carries an icon AND a label, never colour alone.\n if (status === 'PASSED')\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Passed\n </span>\n );\n if (status === 'WAIVED')\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Waived\n </span>\n );\n if (status === 'FAILED')\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-error-text\">\n <XCircle className=\"h-3.5 w-3.5\" /> Failed\n </span>\n );\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-warning-text\">\n <CircleDashed className=\"h-3.5 w-3.5\" /> {status === 'IN_REVIEW' ? 'In review' : 'Pending'}\n </span>\n );\n}\n\nexport function ReleaseGatesCard({\n releaseId,\n projectId,\n releaseStatus,\n}: {\n releaseId: string;\n projectId: string | null;\n releaseStatus: string;\n}) {\n const [name, setName] = useState('');\n const [error, setError] = useState<string | null>(null);\n const { data } = useQuery<{ planmagnetRelease: { qualityGates: Gate[] } | null }>(RELEASE_GATES, {\n variables: { id: releaseId },\n });\n const refetch = { refetchQueries: ['ReleaseGates', 'GetRelease'], awaitRefetchQueries: true };\n const [create, createState] = useMutation(CREATE_GATE, refetch);\n const [review, reviewState] = useMutation(REVIEW_GATE, refetch);\n\n const gates = data?.planmagnetRelease?.qualityGates ?? [];\n const open = gates.filter((g) => !CLEARING.has(g.status));\n const shipped = releaseStatus === 'RELEASED';\n\n const run = async (\n action: () => Promise<{ error?: unknown }>,\n fallback: string,\n after?: () => void\n ) => {\n setError(null);\n try {\n await throwOnError(action());\n after?.();\n } catch (e) {\n setError(e instanceof Error ? e.message : fallback);\n }\n };\n\n return (\n <Card data-testid=\"release-gates\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <ShieldAlert className=\"h-4 w-4\" /> Quality Gates\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n {!shipped && open.length > 0 && (\n <p className=\"text-sm text-status-warning-text\" data-testid=\"release-held\">\n This release cannot be released until {open.length} gate{open.length === 1 ? '' : 's'}{' '}\n {open.length === 1 ? 'is' : 'are'} passed or waived:{' '}\n {open.map((g) => g.name).join(', ')}.\n </p>\n )}\n {gates.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No quality gates hold this release.</p>\n )}\n <ul className=\"space-y-2\">\n {gates.map((gate) => (\n <li\n key={gate.id}\n className=\"rounded-md border p-2 space-y-1\"\n data-testid=\"release-gate\"\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium\">{gate.name}</span>\n <StatusMark status={gate.status} />\n </div>\n {gate.reviewerId && (\n <p className=\"text-xs text-muted-foreground\">\n Reviewed by <PersonName id={gate.reviewerId} />\n {gate.reviewedAt ? ` · ${new Date(gate.reviewedAt).toLocaleString()}` : ''}\n </p>\n )}\n <div className=\"flex gap-1\">\n {(['PASSED', 'FAILED', 'WAIVED'] as const).map((status) => (\n <Button\n key={status}\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={reviewState.loading || gate.status === status}\n onClick={() =>\n run(\n () => review({ variables: { id: gate.id, input: { status } } }),\n 'The gate could not be reviewed.'\n )\n }\n >\n {status === 'PASSED' ? 'Pass' : status === 'FAILED' ? 'Fail' : 'Waive'}\n </Button>\n ))}\n </div>\n </li>\n ))}\n </ul>\n {projectId ? (\n <div className=\"flex gap-2 border-t pt-3\">\n <Input\n aria-label=\"New gate name\"\n placeholder=\"Add a gate, e.g. Security review\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n />\n <Button\n size=\"sm\"\n disabled={!name.trim() || createState.loading}\n data-testid=\"release-gate-add\"\n onClick={() =>\n run(\n () =>\n create({\n variables: {\n input: { projectId, releaseId, name: name.trim(), criteria: {} },\n },\n }),\n 'The gate could not be added.',\n () => setName('')\n )\n }\n >\n Add gate\n </Button>\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n This release has no project, so gates cannot be added here.\n </p>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function ReleaseReadinessCard({ releaseId, projectId, releaseStatus, plannedDate, }: {
|
|
2
|
+
releaseId: string;
|
|
3
|
+
projectId: string | null;
|
|
4
|
+
releaseStatus: string;
|
|
5
|
+
plannedDate: string | null;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=ReleaseReadinessCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReleaseReadinessCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/releases/ReleaseReadinessCard.tsx"],"names":[],"mappings":"AA6MA,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,aAAa,EACb,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,2CAwRA"}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { CombinedGraphQLErrors as e } from "../../node_modules/@apollo/client/errors/CombinedGraphQLErrors.js";
|
|
2
|
+
import { usePlanMagnet as t, usePlanMagnetNavigate as n } from "../providers/PlanMagnetProvider.js";
|
|
3
|
+
import { RELEASE_GATES as r } from "./ReleaseGatesCard.js";
|
|
4
|
+
import { useState as i } from "react";
|
|
5
|
+
import { AlertTriangle as a, CheckCircle2 as o, Clock as s, Link2 as c, ShieldAlert as l, Users as u, Wallet as d } from "lucide-react";
|
|
6
|
+
import { gql as f } from "@apollo/client";
|
|
7
|
+
import { useQuery as p } from "@apollo/client/react";
|
|
8
|
+
import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
9
|
+
import { Card as _, CardContent as v, CardHeader as y, CardTitle as b } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
//#region src/planmagnet/releases/ReleaseReadinessCard.tsx
|
|
11
|
+
var x = f`
|
|
12
|
+
query ReleaseReadiness($releaseId: ID!) {
|
|
13
|
+
planmagnetReleaseReadiness(releaseId: $releaseId) {
|
|
14
|
+
scope
|
|
15
|
+
totalItems
|
|
16
|
+
doneItems
|
|
17
|
+
openWorkItemIds
|
|
18
|
+
blockers {
|
|
19
|
+
workItem {
|
|
20
|
+
id
|
|
21
|
+
key
|
|
22
|
+
title
|
|
23
|
+
status
|
|
24
|
+
}
|
|
25
|
+
waitingOn {
|
|
26
|
+
id
|
|
27
|
+
key
|
|
28
|
+
title
|
|
29
|
+
status
|
|
30
|
+
projectId
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`, S = f`
|
|
36
|
+
query ReleaseBudgets($workspaceId: ID!, $projectId: String!) {
|
|
37
|
+
planmagnetBudgets(
|
|
38
|
+
workspaceId: $workspaceId
|
|
39
|
+
filter: { projectId: $projectId }
|
|
40
|
+
pagination: { first: 50 }
|
|
41
|
+
) {
|
|
42
|
+
items {
|
|
43
|
+
id
|
|
44
|
+
name
|
|
45
|
+
status
|
|
46
|
+
currency
|
|
47
|
+
plannedAmount
|
|
48
|
+
spentAmount
|
|
49
|
+
remainingAmount
|
|
50
|
+
approvedAt
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`, C = f`
|
|
55
|
+
query ReleaseRisks($workspaceId: ID!, $projectId: String!) {
|
|
56
|
+
planmagnetRisks(
|
|
57
|
+
workspaceId: $workspaceId
|
|
58
|
+
filter: { projectId: $projectId }
|
|
59
|
+
pagination: { first: 100 }
|
|
60
|
+
) {
|
|
61
|
+
items {
|
|
62
|
+
id
|
|
63
|
+
title
|
|
64
|
+
severity
|
|
65
|
+
status
|
|
66
|
+
riskScore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
`, w = f`
|
|
71
|
+
query ReleaseCustomerRequests($workspaceId: ID!, $projectId: String!) {
|
|
72
|
+
planmagnetFeatureRequests(
|
|
73
|
+
workspaceId: $workspaceId
|
|
74
|
+
filter: { projectId: $projectId }
|
|
75
|
+
pagination: { first: 100 }
|
|
76
|
+
) {
|
|
77
|
+
items {
|
|
78
|
+
id
|
|
79
|
+
title
|
|
80
|
+
status
|
|
81
|
+
voteCount
|
|
82
|
+
workItemId
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
`, T = new Set([
|
|
87
|
+
"RESOLVED",
|
|
88
|
+
"ACCEPTED",
|
|
89
|
+
"CLOSED"
|
|
90
|
+
]), E = new Set([
|
|
91
|
+
"COMPLETED",
|
|
92
|
+
"DECLINED",
|
|
93
|
+
"DUPLICATE"
|
|
94
|
+
]), D = 1440 * 60 * 1e3;
|
|
95
|
+
function O(t) {
|
|
96
|
+
return e.is(t) && t.errors.some((e) => e.extensions?.code === "FORBIDDEN");
|
|
97
|
+
}
|
|
98
|
+
function k({ icon: e, title: t, testId: n, loading: r, error: i, children: a }) {
|
|
99
|
+
let o = a;
|
|
100
|
+
return i ? o = /* @__PURE__ */ h("p", {
|
|
101
|
+
className: "text-sm text-muted-foreground",
|
|
102
|
+
children: O(i) ? "Your role cannot see this." : `Could not be loaded: ${i instanceof Error ? i.message : "unknown error"}`
|
|
103
|
+
}) : r && (o = /* @__PURE__ */ h("p", {
|
|
104
|
+
className: "text-sm text-muted-foreground",
|
|
105
|
+
children: "Loading…"
|
|
106
|
+
})), /* @__PURE__ */ g("section", {
|
|
107
|
+
className: "space-y-1.5",
|
|
108
|
+
"data-testid": n,
|
|
109
|
+
children: [/* @__PURE__ */ g("h3", {
|
|
110
|
+
className: "flex items-center gap-2 text-sm font-medium",
|
|
111
|
+
children: [e, t]
|
|
112
|
+
}), o]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
var A = (e, t) => {
|
|
116
|
+
try {
|
|
117
|
+
return new Intl.NumberFormat(void 0, {
|
|
118
|
+
style: "currency",
|
|
119
|
+
currency: t
|
|
120
|
+
}).format(e);
|
|
121
|
+
} catch {
|
|
122
|
+
return `${e.toFixed(2)} ${t}`;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
function j({ releaseId: e, projectId: f, releaseStatus: O, plannedDate: j }) {
|
|
126
|
+
let { workspaceId: M } = t(), [N] = i(() => Date.now()), P = n(), F = {
|
|
127
|
+
variables: {
|
|
128
|
+
workspaceId: M,
|
|
129
|
+
projectId: f
|
|
130
|
+
},
|
|
131
|
+
skip: !M || !f
|
|
132
|
+
}, I = p(x, { variables: { releaseId: e } }), L = p(r, { variables: { id: e } }), R = p(S, F), z = p(C, F), B = p(w, F), V = O === "RELEASED", H = I.data?.planmagnetReleaseReadiness, U = (L.data?.planmagnetRelease?.qualityGates ?? []).filter((e) => e.status !== "PASSED" && e.status !== "WAIVED"), W = R.data?.planmagnetBudgets.items ?? [], G = (z.data?.planmagnetRisks.items ?? []).filter((e) => !T.has(e.status)).sort((e, t) => t.riskScore - e.riskScore), K = new Set(H?.openWorkItemIds ?? []), q = new Set((H?.blockers ?? []).map((e) => e.workItem.id)), J = (B.data?.planmagnetFeatureRequests.items ?? []).filter((e) => e.workItemId && K.has(e.workItemId) && !E.has(e.status)), Y = j && !V ? Math.floor((N - new Date(j).getTime()) / D) : null, X = [];
|
|
133
|
+
Y !== null && Y > 0 && X.push(`${Y} day${Y === 1 ? "" : "s"} past its planned date`), H && H.blockers.length > 0 && X.push(`${H.blockers.length} blocked item${H.blockers.length === 1 ? "" : "s"}`), H && H.openWorkItemIds.length > 0 && X.push(`${H.openWorkItemIds.length} unfinished item${H.openWorkItemIds.length === 1 ? "" : "s"}`), U.length > 0 && X.push(`${U.length} gate${U.length === 1 ? "" : "s"} not passed`), W.filter((e) => e.remainingAmount < 0 || e.status === "EXCEEDED").length > 0 && X.push("over budget");
|
|
134
|
+
let Z = !I.loading && !L.loading;
|
|
135
|
+
return /* @__PURE__ */ g(_, {
|
|
136
|
+
"data-testid": "release-readiness",
|
|
137
|
+
children: [/* @__PURE__ */ h(y, { children: /* @__PURE__ */ h(b, { children: "What stands between this release and shipping" }) }), /* @__PURE__ */ g(v, {
|
|
138
|
+
className: "space-y-5",
|
|
139
|
+
children: [
|
|
140
|
+
V ? /* @__PURE__ */ g("p", {
|
|
141
|
+
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
142
|
+
children: [/* @__PURE__ */ h(o, { className: "h-4 w-4" }), " Released."]
|
|
143
|
+
}) : Z && X.length === 0 && H && H.scope !== "NONE" ? /* @__PURE__ */ g("p", {
|
|
144
|
+
className: "flex items-center gap-2 text-sm text-status-success-text",
|
|
145
|
+
"data-testid": "readiness-headline",
|
|
146
|
+
children: [/* @__PURE__ */ h(o, { className: "h-4 w-4" }), " Nothing is holding this release."]
|
|
147
|
+
}) : Z && X.length > 0 ? /* @__PURE__ */ g("p", {
|
|
148
|
+
className: "flex items-center gap-2 text-sm text-status-warning-text",
|
|
149
|
+
"data-testid": "readiness-headline",
|
|
150
|
+
children: [
|
|
151
|
+
/* @__PURE__ */ h(a, { className: "h-4 w-4 shrink-0" }),
|
|
152
|
+
" Held by: ",
|
|
153
|
+
X.join(", "),
|
|
154
|
+
"."
|
|
155
|
+
]
|
|
156
|
+
}) : null,
|
|
157
|
+
/* @__PURE__ */ h(k, {
|
|
158
|
+
icon: /* @__PURE__ */ h(c, { className: "h-4 w-4" }),
|
|
159
|
+
title: "Work and blockers",
|
|
160
|
+
testId: "readiness-work",
|
|
161
|
+
loading: I.loading && !I.data,
|
|
162
|
+
error: I.error,
|
|
163
|
+
children: H && /* @__PURE__ */ g("div", {
|
|
164
|
+
className: "space-y-2",
|
|
165
|
+
children: [
|
|
166
|
+
/* @__PURE__ */ h("p", {
|
|
167
|
+
className: "text-sm text-muted-foreground",
|
|
168
|
+
children: H.scope === "NONE" ? "This release has no work items and no project, so there is nothing to track." : H.scope === "PROJECTS" ? `No work items are assigned to this release, so everything open in its project counts: ${H.totalItems} open.` : `${H.doneItems} of ${H.totalItems} work items done.`
|
|
169
|
+
}),
|
|
170
|
+
H.blockers.length === 0 && H.scope !== "NONE" && /* @__PURE__ */ h("p", {
|
|
171
|
+
className: "text-sm text-muted-foreground",
|
|
172
|
+
children: "No item is waiting on unfinished work."
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ h("ul", {
|
|
175
|
+
className: "space-y-1.5",
|
|
176
|
+
children: H.blockers.map((e) => /* @__PURE__ */ g("li", {
|
|
177
|
+
className: "rounded-md border p-2 text-sm",
|
|
178
|
+
"data-testid": "readiness-blocker",
|
|
179
|
+
children: [
|
|
180
|
+
/* @__PURE__ */ g("button", {
|
|
181
|
+
type: "button",
|
|
182
|
+
className: "font-medium hover:underline text-left",
|
|
183
|
+
onClick: () => P(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.workItem.id)}`),
|
|
184
|
+
children: [
|
|
185
|
+
e.workItem.key,
|
|
186
|
+
" ",
|
|
187
|
+
e.workItem.title
|
|
188
|
+
]
|
|
189
|
+
}),
|
|
190
|
+
/* @__PURE__ */ h("span", {
|
|
191
|
+
className: "text-muted-foreground",
|
|
192
|
+
children: " waits on "
|
|
193
|
+
}),
|
|
194
|
+
e.waitingOn.map((e, t) => /* @__PURE__ */ g("span", { children: [
|
|
195
|
+
t > 0 && ", ",
|
|
196
|
+
/* @__PURE__ */ g("button", {
|
|
197
|
+
type: "button",
|
|
198
|
+
className: "hover:underline",
|
|
199
|
+
onClick: () => P(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
|
|
200
|
+
children: [
|
|
201
|
+
e.key,
|
|
202
|
+
" ",
|
|
203
|
+
e.title
|
|
204
|
+
]
|
|
205
|
+
}),
|
|
206
|
+
" ",
|
|
207
|
+
/* @__PURE__ */ g("span", {
|
|
208
|
+
className: "text-xs text-muted-foreground",
|
|
209
|
+
children: [
|
|
210
|
+
"(",
|
|
211
|
+
e.status.toLowerCase().replace("_", " "),
|
|
212
|
+
f && e.projectId && e.projectId !== f ? ", another project" : "",
|
|
213
|
+
")"
|
|
214
|
+
]
|
|
215
|
+
})
|
|
216
|
+
] }, e.id))
|
|
217
|
+
]
|
|
218
|
+
}, e.workItem.id))
|
|
219
|
+
})
|
|
220
|
+
]
|
|
221
|
+
})
|
|
222
|
+
}),
|
|
223
|
+
/* @__PURE__ */ h(k, {
|
|
224
|
+
icon: /* @__PURE__ */ h(l, { className: "h-4 w-4" }),
|
|
225
|
+
title: "Approvals",
|
|
226
|
+
testId: "readiness-approvals",
|
|
227
|
+
loading: L.loading && !L.data,
|
|
228
|
+
error: L.error,
|
|
229
|
+
children: /* @__PURE__ */ h("p", {
|
|
230
|
+
className: "text-sm text-muted-foreground",
|
|
231
|
+
children: U.length === 0 ? "No quality gate is waiting for a decision." : `Waiting on: ${U.map((e) => `${e.name} (${e.status.toLowerCase().replace("_", " ")})`).join(", ")}.`
|
|
232
|
+
})
|
|
233
|
+
}),
|
|
234
|
+
f ? /* @__PURE__ */ g(m, { children: [
|
|
235
|
+
/* @__PURE__ */ h(k, {
|
|
236
|
+
icon: /* @__PURE__ */ h(d, { className: "h-4 w-4" }),
|
|
237
|
+
title: "Budget",
|
|
238
|
+
testId: "readiness-budget",
|
|
239
|
+
loading: R.loading && !R.data,
|
|
240
|
+
error: R.error,
|
|
241
|
+
children: W.length === 0 ? /* @__PURE__ */ h("p", {
|
|
242
|
+
className: "text-sm text-muted-foreground",
|
|
243
|
+
children: "This release's project has no budget."
|
|
244
|
+
}) : /* @__PURE__ */ h("ul", {
|
|
245
|
+
className: "space-y-1",
|
|
246
|
+
children: W.map((e) => {
|
|
247
|
+
let t = e.remainingAmount < 0;
|
|
248
|
+
return /* @__PURE__ */ g("li", {
|
|
249
|
+
className: "text-sm",
|
|
250
|
+
"data-testid": "readiness-budget-row",
|
|
251
|
+
children: [
|
|
252
|
+
/* @__PURE__ */ h("span", {
|
|
253
|
+
className: "font-medium",
|
|
254
|
+
children: e.name
|
|
255
|
+
}),
|
|
256
|
+
":",
|
|
257
|
+
" ",
|
|
258
|
+
A(e.spentAmount, e.currency),
|
|
259
|
+
" of ",
|
|
260
|
+
A(e.plannedAmount, e.currency),
|
|
261
|
+
" ",
|
|
262
|
+
"spent",
|
|
263
|
+
t ? /* @__PURE__ */ g("span", {
|
|
264
|
+
className: "text-status-error-text",
|
|
265
|
+
children: [
|
|
266
|
+
" ",
|
|
267
|
+
"— over by ",
|
|
268
|
+
A(-e.remainingAmount, e.currency)
|
|
269
|
+
]
|
|
270
|
+
}) : /* @__PURE__ */ g("span", {
|
|
271
|
+
className: "text-muted-foreground",
|
|
272
|
+
children: [
|
|
273
|
+
" ",
|
|
274
|
+
"— ",
|
|
275
|
+
A(e.remainingAmount, e.currency),
|
|
276
|
+
" left"
|
|
277
|
+
]
|
|
278
|
+
}),
|
|
279
|
+
!e.approvedAt && /* @__PURE__ */ h("span", {
|
|
280
|
+
className: "text-status-warning-text",
|
|
281
|
+
children: " · not approved"
|
|
282
|
+
})
|
|
283
|
+
]
|
|
284
|
+
}, e.id);
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
}),
|
|
288
|
+
/* @__PURE__ */ h(k, {
|
|
289
|
+
icon: /* @__PURE__ */ h(a, { className: "h-4 w-4" }),
|
|
290
|
+
title: "Open risks",
|
|
291
|
+
testId: "readiness-risks",
|
|
292
|
+
loading: z.loading && !z.data,
|
|
293
|
+
error: z.error,
|
|
294
|
+
children: G.length === 0 ? /* @__PURE__ */ h("p", {
|
|
295
|
+
className: "text-sm text-muted-foreground",
|
|
296
|
+
children: "No open risks on this release's project."
|
|
297
|
+
}) : /* @__PURE__ */ g("ul", {
|
|
298
|
+
className: "space-y-1",
|
|
299
|
+
children: [G.slice(0, 5).map((e) => /* @__PURE__ */ g("li", {
|
|
300
|
+
className: "text-sm",
|
|
301
|
+
children: [
|
|
302
|
+
/* @__PURE__ */ h("span", {
|
|
303
|
+
className: "font-medium",
|
|
304
|
+
children: e.title
|
|
305
|
+
}),
|
|
306
|
+
" ",
|
|
307
|
+
/* @__PURE__ */ g("span", {
|
|
308
|
+
className: "text-xs text-muted-foreground",
|
|
309
|
+
children: [
|
|
310
|
+
"(",
|
|
311
|
+
e.severity.toLowerCase(),
|
|
312
|
+
", score ",
|
|
313
|
+
e.riskScore,
|
|
314
|
+
",",
|
|
315
|
+
" ",
|
|
316
|
+
e.status.toLowerCase(),
|
|
317
|
+
")"
|
|
318
|
+
]
|
|
319
|
+
})
|
|
320
|
+
]
|
|
321
|
+
}, e.id)), G.length > 5 && /* @__PURE__ */ g("li", {
|
|
322
|
+
className: "text-xs text-muted-foreground",
|
|
323
|
+
children: [
|
|
324
|
+
"and ",
|
|
325
|
+
G.length - 5,
|
|
326
|
+
" more"
|
|
327
|
+
]
|
|
328
|
+
})]
|
|
329
|
+
})
|
|
330
|
+
}),
|
|
331
|
+
/* @__PURE__ */ h(k, {
|
|
332
|
+
icon: /* @__PURE__ */ h(u, { className: "h-4 w-4" }),
|
|
333
|
+
title: "Customer requests riding on unfinished work",
|
|
334
|
+
testId: "readiness-requests",
|
|
335
|
+
loading: B.loading && !B.data,
|
|
336
|
+
error: B.error,
|
|
337
|
+
children: J.length === 0 ? /* @__PURE__ */ h("p", {
|
|
338
|
+
className: "text-sm text-muted-foreground",
|
|
339
|
+
children: "No open customer request is tied to unfinished work in this release."
|
|
340
|
+
}) : /* @__PURE__ */ h("ul", {
|
|
341
|
+
className: "space-y-1",
|
|
342
|
+
children: J.map((e) => /* @__PURE__ */ g("li", {
|
|
343
|
+
className: "text-sm",
|
|
344
|
+
"data-testid": "readiness-request",
|
|
345
|
+
children: [
|
|
346
|
+
/* @__PURE__ */ h("span", {
|
|
347
|
+
className: "font-medium",
|
|
348
|
+
children: e.title
|
|
349
|
+
}),
|
|
350
|
+
" ",
|
|
351
|
+
/* @__PURE__ */ g("span", {
|
|
352
|
+
className: "text-xs text-muted-foreground",
|
|
353
|
+
children: [
|
|
354
|
+
"(",
|
|
355
|
+
e.voteCount,
|
|
356
|
+
" vote",
|
|
357
|
+
e.voteCount === 1 ? "" : "s",
|
|
358
|
+
")"
|
|
359
|
+
]
|
|
360
|
+
}),
|
|
361
|
+
e.workItemId && q.has(e.workItemId) && /* @__PURE__ */ h("span", {
|
|
362
|
+
className: "text-status-error-text",
|
|
363
|
+
children: " — its work is blocked"
|
|
364
|
+
})
|
|
365
|
+
]
|
|
366
|
+
}, e.id))
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
] }) : /* @__PURE__ */ h("p", {
|
|
370
|
+
className: "text-sm text-muted-foreground",
|
|
371
|
+
children: "This release has no project, so budget, risks and customer requests cannot be shown."
|
|
372
|
+
}),
|
|
373
|
+
j && Y !== null && /* @__PURE__ */ g("p", {
|
|
374
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
375
|
+
children: [
|
|
376
|
+
/* @__PURE__ */ h(s, { className: "h-3.5 w-3.5" }),
|
|
377
|
+
"Planned for ",
|
|
378
|
+
new Date(j).toLocaleDateString(),
|
|
379
|
+
Y > 0 ? ` (${Y} day${Y === 1 ? "" : "s"} ago)` : "",
|
|
380
|
+
"."
|
|
381
|
+
]
|
|
382
|
+
})
|
|
383
|
+
]
|
|
384
|
+
})]
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
export { j as ReleaseReadinessCard };
|
|
389
|
+
|
|
390
|
+
//# sourceMappingURL=ReleaseReadinessCard.js.map
|