@burdenoff/microfe-planmagnet 2026.927.1 → 2026.927.3
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/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +128 -124
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
- package/dist/planmagnet/controls/ConfirmButton.d.ts +25 -0
- package/dist/planmagnet/controls/ConfirmButton.d.ts.map +1 -0
- package/dist/planmagnet/controls/ConfirmButton.js +30 -0
- package/dist/planmagnet/controls/ConfirmButton.js.map +1 -0
- package/dist/planmagnet/controls/ProjectControls.d.ts +13 -0
- package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/ProjectControls.js +104 -0
- package/dist/planmagnet/controls/ProjectControls.js.map +1 -0
- package/dist/planmagnet/controls/WorkItemControls.d.ts +52 -0
- package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/WorkItemControls.js +198 -0
- package/dist/planmagnet/controls/WorkItemControls.js.map +1 -0
- package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +14 -4
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +4 -4
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DocsDocsPagePage.js +4 -4
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +102 -100
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +95 -94
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +97 -90
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +150 -111
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +121 -94
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +84 -73
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js +13 -8
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +129 -125
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +112 -109
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +134 -132
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +72 -83
- package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +109 -108
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +167 -111
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +101 -99
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +149 -147
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.js +4 -8
- package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +68 -69
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.js +1 -5
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.js.map +1 -1
- package/dist/planmagnet/people/People.d.ts +18 -0
- package/dist/planmagnet/people/People.d.ts.map +1 -1
- package/dist/planmagnet/people/People.js +26 -12
- package/dist/planmagnet/people/People.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.d.ts +34 -0
- package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -0
- package/dist/planmagnet/roadmaps/RoadmapControls.js +301 -0
- package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -0
- package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.js +194 -215
- package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkItemControls.js","names":[],"sources":["../../../src/planmagnet/controls/WorkItemControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Trash2 } from 'lucide-react';\nimport { usePersonProfile } from '../people/People';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\n\n/**\n * Task controls (BOFF-7938, BOFF-7939). Until 2026-09-27 the task detail page was read-only: no status, priority or\n * assignee control, no Edit link (the Edit Task page existed but nothing led to it) and no Delete. A task's status\n * could only change by dragging its card on a project board, and nobody could be assigned at all — so every Team\n * page, sprint \"Team Members\" list and board card read \"Unassigned\".\n *\n * Every control saves through planmagnetUpdateWorkItem, the same path the board uses, so a status change records\n * its history (activity feed, cycle time) exactly like a drag. A refusal keeps the stored value and shows the reason.\n */\n\nexport const WORK_ITEM_STATUSES = [\n 'BACKLOG',\n 'TODO',\n 'IN_PROGRESS',\n 'IN_REVIEW',\n 'TESTING',\n 'DONE',\n 'CANCELLED',\n] as const;\nexport const WORK_ITEM_PRIORITIES = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;\n\nexport function enumLabel(value: string): string {\n const words = value.toLowerCase().replace(/_/g, ' ');\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nconst UPDATE_WORK_ITEM = gql`\n mutation WorkItemControlsUpdate($id: ID!, $input: PlanmagnetUpdateWorkItemInput!) {\n planmagnetUpdateWorkItem(id: $id, input: $input) {\n id\n status\n priority\n assigneeId\n }\n }\n`;\nconst DELETE_WORK_ITEM = gql`\n mutation WorkItemControlsDelete($id: ID!) {\n planmagnetDeleteWorkItem(id: $id)\n }\n`;\n\n/** Every query that shows a task's status, priority or assignee. */\nconst LIST_QUERIES = ['GetWorkItems', 'GetWorkItemCounts', 'ProjectBoard', 'GetDashboardData'];\nconst DETAIL_QUERIES = ['GetWorkItemDetail', 'WorkItemActivity', ...LIST_QUERIES];\n\nconst selectClass =\n 'rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60 max-w-[12rem]';\n\nfunction useWorkItemUpdate(workItemId: string) {\n const [update, state] = useMutation(UPDATE_WORK_ITEM, {\n refetchQueries: DETAIL_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const save = async (input: Record<string, unknown>) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: workItemId, input } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be updated.');\n }\n };\n return { save, saving: state.loading, error };\n}\n\n/** A status or priority select that saves on change. */\nexport function WorkItemEnumSelect({\n workItemId,\n field,\n value,\n options,\n label,\n}: {\n workItemId: string;\n field: 'status' | 'priority';\n value: string;\n options: readonly string[];\n label: string;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n // A stored value the list does not know (older data) stays selectable rather than silently showing another one.\n const all = options.includes(value) ? options : [value, ...options];\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <select\n aria-label={label}\n data-testid={`task-${field}`}\n className={selectClass}\n value={value}\n disabled={saving}\n onChange={(e) => {\n if (e.target.value !== value) void save({ [field]: e.target.value });\n }}\n >\n {all.map((option) => (\n <option key={option} value={option}>\n {enumLabel(option)}\n </option>\n ))}\n </select>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Workspace members for an assignee picker, from the fe-libs workspace directory (what the sharing picker uses; any\n * member can read it). Option values are USER ids. `planmagnetWorkspaceEditors` is not used: it needs PlanMagnet\n * admin rights, so an ordinary member would get an empty picker.\n */\nexport function useAssigneeOptions(currentId: string | null | undefined) {\n const { workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const directory = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n const current = usePersonProfile(currentId);\n const options = directory.members.map((m) => ({ id: m.id, name: m.name }));\n // The current assignee may be outside the loaded page (or no longer a member): keep them selectable by name.\n if (currentId && current && !options.some((o) => o.id === currentId)) {\n options.unshift({ id: currentId, name: current.displayName });\n }\n return {\n options,\n loading: directory.loading,\n hasMore: directory.hasMore,\n fetchMore: directory.fetchMore,\n };\n}\n\n/** A plain assignee select (for forms). '' means unassigned. */\nexport function AssigneeSelect({\n id,\n value,\n onChange,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (userId: string) => void;\n disabled?: boolean;\n}) {\n const { options, loading, hasMore, fetchMore } = useAssigneeOptions(value || null);\n return (\n <div className=\"flex flex-col gap-1\">\n <select\n id={id}\n aria-label={id ? undefined : 'Assignee'}\n data-testid=\"assignee-select\"\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">Unassigned</option>\n {options.map((o) => (\n <option key={o.id} value={o.id}>\n {o.name}\n </option>\n ))}\n </select>\n {loading && options.length === 0 && (\n <span className=\"text-xs text-muted-foreground\">Loading members…</span>\n )}\n {hasMore && (\n <button\n type=\"button\"\n className=\"self-start text-xs text-primary hover:underline\"\n onClick={fetchMore}\n >\n Show more members\n </button>\n )}\n </div>\n );\n}\n\n/** The task detail's assignee control: saves on change. */\nexport function WorkItemAssigneeSelect({\n workItemId,\n assigneeId,\n}: {\n workItemId: string;\n assigneeId: string | null;\n}) {\n const { save, saving, error } = useWorkItemUpdate(workItemId);\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <AssigneeSelect\n value={assigneeId ?? ''}\n disabled={saving}\n onChange={(next) => {\n if (next !== (assigneeId ?? '')) void save({ assigneeId: next || null });\n }}\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/** Edit and Delete for the task detail header. */\nexport function WorkItemHeaderActions({\n workItemId,\n title,\n}: {\n workItemId: string;\n title: string;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_WORK_ITEM, { refetchQueries: LIST_QUERIES });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: workItemId } }));\n navigate('/tasks/tasks-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The task could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"task-edit\"\n onClick={() => navigate(`/tasks/edit-task-page?taskId=${encodeURIComponent(workItemId)}`)}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete \"${title}\"?`}\n description=\"The task is deleted and disappears from boards, sprints and releases. This cannot be undone here.\"\n confirmLabel=\"Delete task\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"AAgGA,wBAAgB,0BAA0B,4CAqWzC"}
|
|
@@ -30,6 +30,10 @@ var S = d`
|
|
|
30
30
|
id
|
|
31
31
|
name
|
|
32
32
|
projectId
|
|
33
|
+
project {
|
|
34
|
+
id
|
|
35
|
+
name
|
|
36
|
+
}
|
|
33
37
|
status
|
|
34
38
|
startDate
|
|
35
39
|
endDate
|
|
@@ -50,6 +54,10 @@ var S = d`
|
|
|
50
54
|
name
|
|
51
55
|
description
|
|
52
56
|
projectId
|
|
57
|
+
project {
|
|
58
|
+
id
|
|
59
|
+
name
|
|
60
|
+
}
|
|
53
61
|
status
|
|
54
62
|
dueDate
|
|
55
63
|
ownerId
|
|
@@ -164,8 +172,9 @@ function C() {
|
|
|
164
172
|
className: "text-xs text-muted-foreground",
|
|
165
173
|
children: [
|
|
166
174
|
C("planmagnet.dashboard.shared.project", "Project"),
|
|
167
|
-
":
|
|
168
|
-
|
|
175
|
+
":",
|
|
176
|
+
" ",
|
|
177
|
+
e.project?.name ?? C("planmagnet.dashboard.shared.projectUnavailable", "not available")
|
|
169
178
|
]
|
|
170
179
|
})] }), /* @__PURE__ */ h(_, {
|
|
171
180
|
variant: "secondary",
|
|
@@ -242,8 +251,9 @@ function C() {
|
|
|
242
251
|
className: "text-xs text-muted-foreground",
|
|
243
252
|
children: [
|
|
244
253
|
C("planmagnet.dashboard.shared.project", "Project"),
|
|
245
|
-
":
|
|
246
|
-
|
|
254
|
+
":",
|
|
255
|
+
" ",
|
|
256
|
+
e.project?.name ?? C("planmagnet.dashboard.shared.projectUnavailable", "not available")
|
|
247
257
|
]
|
|
248
258
|
}),
|
|
249
259
|
/* @__PURE__ */ h("p", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardDashboardPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n LayoutDashboard,\n FolderKanban,\n CheckSquare,\n Zap,\n Users,\n Calendar,\n TrendingUp,\n AlertTriangle,\n ArrowUpRight,\n} from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\n\nconst GET_DASHBOARD_DATA = gql`\n query GetDashboardData($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n description\n key\n status\n methodology\n ownerId\n startDate\n endDate\n tags\n createdAt\n }\n totalCount\n }\n planmagnetSprints(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n projectId\n status\n startDate\n endDate\n goal\n capacity\n velocity\n workItems {\n id\n status\n storyPoints\n }\n }\n totalCount\n }\n planmagnetMilestones(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n description\n projectId\n status\n dueDate\n ownerId\n createdAt\n }\n totalCount\n }\n planmagnetWorkItems(workspaceId: $workspaceId, pagination: { first: 20 }) {\n items {\n id\n key\n description\n status\n priority\n storyPoints\n assigneeId\n projectId\n labels\n createdAt\n }\n totalCount\n }\n }\n`;\n\nexport function DashboardDashboardPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_DASHBOARD_DATA, {\n variables: { workspaceId: workspaceId ?? 'default' },\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 if (error)\n return (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.dashboard.errorLoading', 'Error loading dashboard')}: {error.message}\n </div>\n );\n\n const projects = data?.planmagnetProjects?.items ?? [];\n const sprints = data?.planmagnetSprints?.items ?? [];\n const milestones = data?.planmagnetMilestones?.items ?? [];\n const workItems = data?.planmagnetWorkItems?.items ?? [];\n\n const activeProjects = projects.filter((p: { status: string }) => p.status === 'ACTIVE');\n const completedProjects = projects.filter((p: { status: string }) => p.status === 'COMPLETED');\n const activeSprints = sprints.filter((s: { status: string }) => s.status === 'ACTIVE');\n const openMilestones = milestones.filter(\n (m: { status: string }) => m.status === 'PENDING' || m.status === 'IN_PROGRESS'\n );\n // The list shows four; the \"upcoming\" count is all of them (it was the\n // sliced list's length, so it could never exceed 4).\n const upcomingMilestones = openMilestones.slice(0, 4);\n const recentWorkItems = workItems.slice(0, 5);\n // Work items carry only a projectId; the page already holds the projects, so\n // show the name instead of the raw id (P1.1). A project outside the first 50\n // falls back to a short id rather than the full UUID. Deliberately NOT a\n // useMemo: this line runs after the loading/error early returns, and a hook\n // here would change the hook count between renders and crash the page.\n const projectNameById = new Map<string, string>(\n projects.map((p: { id: string; name: string }) => [p.id, p.name])\n );\n\n const metrics = [\n {\n label: t('planmagnet.dashboard.metrics.totalProjects.label', 'Total Projects'),\n // Totals are the server's totalCount: the lists here hold one page (50).\n value: String(data?.planmagnetProjects?.totalCount ?? projects.length),\n trend: `${activeProjects.length} ${t('planmagnet.dashboard.metrics.totalProjects.trend', 'active')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.activeSprints.label', 'Active Sprints'),\n value: String(activeSprints.length),\n trend: `${data?.planmagnetSprints?.totalCount ?? sprints.length} ${t('planmagnet.dashboard.metrics.activeSprints.trend', 'total')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.workItems.label', 'Work Items'),\n value: String(data?.planmagnetWorkItems?.totalCount ?? 0),\n trend: t('planmagnet.dashboard.metrics.workItems.trend', 'across all projects'),\n },\n {\n label: t('planmagnet.dashboard.metrics.milestones.label', 'Milestones'),\n value: String(data?.planmagnetMilestones?.totalCount ?? milestones.length),\n trend: `${openMilestones.length} ${t('planmagnet.dashboard.metrics.milestones.trend', 'upcoming')}`,\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <LayoutDashboard className=\"h-6 w-6\" />\n {t('planmagnet.dashboard.title', 'Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t('planmagnet.dashboard.subtitle', 'Welcome back! Here is your project overview.')}\n </p>\n </div>\n </div>\n\n {/* KPI Metrics */}\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">{m.label}</CardTitle>\n <TrendingUp className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{m.value}</div>\n <p className=\"text-xs text-muted-foreground\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-12\">\n {/* Active Sprints */}\n <Card className=\"lg:col-span-8\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Zap className=\"h-5 w-5 text-blue-500\" />\n {t('planmagnet.dashboard.activeSprints.title', 'Active Sprints')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {activeSprints.map(\n (sprint: {\n id: string;\n name: string;\n projectId: string;\n capacity: number | null;\n velocity: number | null;\n startDate: string | null;\n endDate: string | null;\n workItems: { id: string; status: string; storyPoints: number | null }[];\n }) => {\n const totalPoints = sprint.workItems.reduce(\n (sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0),\n 0\n );\n const completedPoints = sprint.workItems\n .filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (sum: number, wi: { storyPoints: number | null }) =>\n sum + (wi.storyPoints ?? 0),\n 0\n );\n const totalTasks = sprint.workItems.length;\n const completedTasks = sprint.workItems.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length;\n const pct = totalPoints > 0 ? Math.round((completedPoints / totalPoints) * 100) : 0;\n return (\n <div key={sprint.id} className=\"rounded-lg border p-4\">\n <div className=\"flex items-center justify-between mb-2\">\n <div>\n <p className=\"font-medium\">{sprint.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.project', 'Project')}: {sprint.projectId}\n </p>\n </div>\n <Badge variant=\"secondary\">\n {completedPoints}/{totalPoints}{' '}\n {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n </div>\n <div className=\"w-full h-2 bg-muted rounded-full overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between mt-2 text-xs text-muted-foreground\">\n <span>\n {completedTasks}/{totalTasks}{' '}\n {t('planmagnet.dashboard.shared.tasks', 'tasks')}\n </span>\n <span>\n {sprint.startDate ? new Date(sprint.startDate).toLocaleDateString() : '—'} -{' '}\n {sprint.endDate ? new Date(sprint.endDate).toLocaleDateString() : '—'}\n </span>\n </div>\n </div>\n );\n }\n )}\n {activeSprints.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.activeSprints.empty', 'No active sprints.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n {/* Sidebar */}\n <div className=\"lg:col-span-4 space-y-6\">\n {/* On-Hold Projects */}\n {projects.filter((p: { status: string }) => p.status === 'ON_HOLD').length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <AlertTriangle className=\"h-5 w-5 text-orange-500\" />\n {t('planmagnet.dashboard.onHoldProjects.title', 'On-Hold Projects')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {projects\n .filter((p: { status: string }) => p.status === 'ON_HOLD')\n .map((p: { id: string; name: string; status: string }) => (\n <div\n key={p.id}\n className=\"flex items-center justify-between rounded border p-2\"\n >\n <div>\n <p className=\"text-sm font-medium\">{p.name}</p>\n </div>\n <Badge variant=\"destructive\">\n {p.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n {/* Upcoming Milestones */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Calendar className=\"h-5 w-5 text-green-500\" />\n {t('planmagnet.dashboard.upcomingMilestones.title', 'Upcoming Milestones')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingMilestones.map(\n (ms: {\n id: string;\n name: string;\n projectId: string;\n dueDate: string | null;\n status: string;\n }) => (\n <div key={ms.id} className=\"flex items-start gap-3 rounded border p-2\">\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{ms.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.project', 'Project')}: {ms.projectId}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.due', 'Due')}{' '}\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : '—'}\n </p>\n </div>\n <Badge variant=\"secondary\" className=\"text-xs flex-shrink-0 capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n )\n )}\n {upcomingMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">\n {t('planmagnet.dashboard.upcomingMilestones.empty', 'No upcoming milestones.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n {/* Recent Work Items & Project Status */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <CheckSquare className=\"h-5 w-5 text-purple-500\" />\n {t('planmagnet.dashboard.recentWorkItems.title', 'Recent Work Items')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {recentWorkItems.map(\n (item: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n assigneeId: string | null;\n projectId: string;\n }) => (\n <div key={item.id} className=\"flex items-center justify-between rounded border p-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">{item.key}</p>\n <p className=\"text-xs text-muted-foreground\">\n {projectNameById.get(item.projectId) ??\n `Project ${item.projectId.slice(0, 6)}`}\n {item.assigneeId ? (\n <>\n {' \\u00b7 '}\n <PersonName id={item.assigneeId} />\n </>\n ) : null}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {item.storyPoints ?? 0} {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {item.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {recentWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.recentWorkItems.empty', 'No work items yet.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <FolderKanban className=\"h-5 w-5 text-cyan-500\" />\n {t('planmagnet.dashboard.projectStatus.title', 'Project Status')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activeProjects.map(\n (p: { id: string; name: string; status: string; ownerId: string }) => (\n <div key={p.id} className=\"flex items-center justify-between rounded border p-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">{p.name}</p>\n <p className=\"text-xs text-muted-foreground capitalize\">\n {p.status.toLowerCase().replace('_', ' ')}\n </p>\n </div>\n <div className=\"flex items-center gap-2 ml-3 flex-shrink-0\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />\n <span className=\"text-xs text-muted-foreground\">\n <PersonName id={p.ownerId} />\n </span>\n <ArrowUpRight className=\"h-3 w-3 text-muted-foreground\" />\n </div>\n </div>\n )\n )}\n {activeProjects.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.projectStatus.empty', 'No active projects.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n LayoutDashboard,\n FolderKanban,\n CheckSquare,\n Zap,\n Users,\n Calendar,\n TrendingUp,\n AlertTriangle,\n ArrowUpRight,\n} from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\n\nconst GET_DASHBOARD_DATA = gql`\n query GetDashboardData($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n description\n key\n status\n methodology\n ownerId\n startDate\n endDate\n tags\n createdAt\n }\n totalCount\n }\n planmagnetSprints(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n projectId\n project {\n id\n name\n }\n status\n startDate\n endDate\n goal\n capacity\n velocity\n workItems {\n id\n status\n storyPoints\n }\n }\n totalCount\n }\n planmagnetMilestones(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n description\n projectId\n project {\n id\n name\n }\n status\n dueDate\n ownerId\n createdAt\n }\n totalCount\n }\n planmagnetWorkItems(workspaceId: $workspaceId, pagination: { first: 20 }) {\n items {\n id\n key\n description\n status\n priority\n storyPoints\n assigneeId\n projectId\n labels\n createdAt\n }\n totalCount\n }\n }\n`;\n\nexport function DashboardDashboardPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_DASHBOARD_DATA, {\n variables: { workspaceId: workspaceId ?? 'default' },\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 if (error)\n return (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.dashboard.errorLoading', 'Error loading dashboard')}: {error.message}\n </div>\n );\n\n const projects = data?.planmagnetProjects?.items ?? [];\n const sprints = data?.planmagnetSprints?.items ?? [];\n const milestones = data?.planmagnetMilestones?.items ?? [];\n const workItems = data?.planmagnetWorkItems?.items ?? [];\n\n const activeProjects = projects.filter((p: { status: string }) => p.status === 'ACTIVE');\n const completedProjects = projects.filter((p: { status: string }) => p.status === 'COMPLETED');\n const activeSprints = sprints.filter((s: { status: string }) => s.status === 'ACTIVE');\n const openMilestones = milestones.filter(\n (m: { status: string }) => m.status === 'PENDING' || m.status === 'IN_PROGRESS'\n );\n // The list shows four; the \"upcoming\" count is all of them (it was the\n // sliced list's length, so it could never exceed 4).\n const upcomingMilestones = openMilestones.slice(0, 4);\n const recentWorkItems = workItems.slice(0, 5);\n // Work items carry only a projectId; the page already holds the projects, so\n // show the name instead of the raw id (P1.1). A project outside the first 50\n // falls back to a short id rather than the full UUID. Deliberately NOT a\n // useMemo: this line runs after the loading/error early returns, and a hook\n // here would change the hook count between renders and crash the page.\n const projectNameById = new Map<string, string>(\n projects.map((p: { id: string; name: string }) => [p.id, p.name])\n );\n\n const metrics = [\n {\n label: t('planmagnet.dashboard.metrics.totalProjects.label', 'Total Projects'),\n // Totals are the server's totalCount: the lists here hold one page (50).\n value: String(data?.planmagnetProjects?.totalCount ?? projects.length),\n trend: `${activeProjects.length} ${t('planmagnet.dashboard.metrics.totalProjects.trend', 'active')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.activeSprints.label', 'Active Sprints'),\n value: String(activeSprints.length),\n trend: `${data?.planmagnetSprints?.totalCount ?? sprints.length} ${t('planmagnet.dashboard.metrics.activeSprints.trend', 'total')}`,\n },\n {\n label: t('planmagnet.dashboard.metrics.workItems.label', 'Work Items'),\n value: String(data?.planmagnetWorkItems?.totalCount ?? 0),\n trend: t('planmagnet.dashboard.metrics.workItems.trend', 'across all projects'),\n },\n {\n label: t('planmagnet.dashboard.metrics.milestones.label', 'Milestones'),\n value: String(data?.planmagnetMilestones?.totalCount ?? milestones.length),\n trend: `${openMilestones.length} ${t('planmagnet.dashboard.metrics.milestones.trend', 'upcoming')}`,\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold flex items-center gap-2\">\n <LayoutDashboard className=\"h-6 w-6\" />\n {t('planmagnet.dashboard.title', 'Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t('planmagnet.dashboard.subtitle', 'Welcome back! Here is your project overview.')}\n </p>\n </div>\n </div>\n\n {/* KPI Metrics */}\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">{m.label}</CardTitle>\n <TrendingUp className=\"h-4 w-4 text-muted-foreground\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{m.value}</div>\n <p className=\"text-xs text-muted-foreground\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-12\">\n {/* Active Sprints */}\n <Card className=\"lg:col-span-8\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Zap className=\"h-5 w-5 text-blue-500\" />\n {t('planmagnet.dashboard.activeSprints.title', 'Active Sprints')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {activeSprints.map(\n (sprint: {\n id: string;\n name: string;\n projectId: string;\n capacity: number | null;\n velocity: number | null;\n startDate: string | null;\n endDate: string | null;\n workItems: { id: string; status: string; storyPoints: number | null }[];\n }) => {\n const totalPoints = sprint.workItems.reduce(\n (sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0),\n 0\n );\n const completedPoints = sprint.workItems\n .filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (sum: number, wi: { storyPoints: number | null }) =>\n sum + (wi.storyPoints ?? 0),\n 0\n );\n const totalTasks = sprint.workItems.length;\n const completedTasks = sprint.workItems.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length;\n const pct = totalPoints > 0 ? Math.round((completedPoints / totalPoints) * 100) : 0;\n return (\n <div key={sprint.id} className=\"rounded-lg border p-4\">\n <div className=\"flex items-center justify-between mb-2\">\n <div>\n <p className=\"font-medium\">{sprint.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.project', 'Project')}:{' '}\n {sprint.project?.name ??\n t('planmagnet.dashboard.shared.projectUnavailable', 'not available')}\n </p>\n </div>\n <Badge variant=\"secondary\">\n {completedPoints}/{totalPoints}{' '}\n {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n </div>\n <div className=\"w-full h-2 bg-muted rounded-full overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between mt-2 text-xs text-muted-foreground\">\n <span>\n {completedTasks}/{totalTasks}{' '}\n {t('planmagnet.dashboard.shared.tasks', 'tasks')}\n </span>\n <span>\n {sprint.startDate ? new Date(sprint.startDate).toLocaleDateString() : '—'} -{' '}\n {sprint.endDate ? new Date(sprint.endDate).toLocaleDateString() : '—'}\n </span>\n </div>\n </div>\n );\n }\n )}\n {activeSprints.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.activeSprints.empty', 'No active sprints.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n {/* Sidebar */}\n <div className=\"lg:col-span-4 space-y-6\">\n {/* On-Hold Projects */}\n {projects.filter((p: { status: string }) => p.status === 'ON_HOLD').length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <AlertTriangle className=\"h-5 w-5 text-orange-500\" />\n {t('planmagnet.dashboard.onHoldProjects.title', 'On-Hold Projects')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {projects\n .filter((p: { status: string }) => p.status === 'ON_HOLD')\n .map((p: { id: string; name: string; status: string }) => (\n <div\n key={p.id}\n className=\"flex items-center justify-between rounded border p-2\"\n >\n <div>\n <p className=\"text-sm font-medium\">{p.name}</p>\n </div>\n <Badge variant=\"destructive\">\n {p.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n {/* Upcoming Milestones */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Calendar className=\"h-5 w-5 text-green-500\" />\n {t('planmagnet.dashboard.upcomingMilestones.title', 'Upcoming Milestones')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingMilestones.map(\n (ms: {\n id: string;\n name: string;\n projectId: string;\n dueDate: string | null;\n status: string;\n }) => (\n <div key={ms.id} className=\"flex items-start gap-3 rounded border p-2\">\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{ms.name}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.project', 'Project')}:{' '}\n {ms.project?.name ??\n t('planmagnet.dashboard.shared.projectUnavailable', 'not available')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.dashboard.shared.due', 'Due')}{' '}\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : '—'}\n </p>\n </div>\n <Badge variant=\"secondary\" className=\"text-xs flex-shrink-0 capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n )\n )}\n {upcomingMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">\n {t('planmagnet.dashboard.upcomingMilestones.empty', 'No upcoming milestones.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n {/* Recent Work Items & Project Status */}\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <CheckSquare className=\"h-5 w-5 text-purple-500\" />\n {t('planmagnet.dashboard.recentWorkItems.title', 'Recent Work Items')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {recentWorkItems.map(\n (item: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n assigneeId: string | null;\n projectId: string;\n }) => (\n <div key={item.id} className=\"flex items-center justify-between rounded border p-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">{item.key}</p>\n <p className=\"text-xs text-muted-foreground\">\n {projectNameById.get(item.projectId) ??\n `Project ${item.projectId.slice(0, 6)}`}\n {item.assigneeId ? (\n <>\n {' \\u00b7 '}\n <PersonName id={item.assigneeId} />\n </>\n ) : null}\n </p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {item.storyPoints ?? 0} {t('planmagnet.dashboard.shared.pointsShort', 'pts')}\n </Badge>\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {item.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {recentWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.recentWorkItems.empty', 'No work items yet.')}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <FolderKanban className=\"h-5 w-5 text-cyan-500\" />\n {t('planmagnet.dashboard.projectStatus.title', 'Project Status')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activeProjects.map(\n (p: { id: string; name: string; status: string; ownerId: string }) => (\n <div key={p.id} className=\"flex items-center justify-between rounded border p-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium truncate\">{p.name}</p>\n <p className=\"text-xs text-muted-foreground capitalize\">\n {p.status.toLowerCase().replace('_', ' ')}\n </p>\n </div>\n <div className=\"flex items-center gap-2 ml-3 flex-shrink-0\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />\n <span className=\"text-xs text-muted-foreground\">\n <PersonName id={p.ownerId} />\n </span>\n <ArrowUpRight className=\"h-3 w-3 text-muted-foreground\" />\n </div>\n </div>\n )\n )}\n {activeProjects.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.dashboard.projectStatus.empty', 'No active projects.')}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAoBA,IAAM,IAAqB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4E9B,SAAgB,IAA6B;CAC3C,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,GAAS,EAEjB,EACJ,SACA,YACA,aAGA,EAA8B,GAAoB,EAChD,WAAW,EAAE,aAAa,KAAe,WAAW,EACrD,CAAC;AAMJ,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,kFAAmF,CAAA;EAC9F,CAAA;AAEV,KAAI,EACF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,EAAE,qCAAqC,0BAA0B;GAAC;GAAG,EAAM;GACxE;;CAGV,IAAM,IAAW,GAAM,oBAAoB,SAAS,EAAE,EAChD,IAAU,GAAM,mBAAmB,SAAS,EAAE,EAC9C,IAAa,GAAM,sBAAsB,SAAS,EAAE,EACpD,IAAY,GAAM,qBAAqB,SAAS,EAAE,EAElD,IAAiB,EAAS,QAAQ,MAA0B,EAAE,WAAW,SAAS;AAC9D,GAAS,QAAQ,MAA0B,EAAE,WAAW,YAAY;CAC9F,IAAM,IAAgB,EAAQ,QAAQ,MAA0B,EAAE,WAAW,SAAS,EAChF,IAAiB,EAAW,QAC/B,MAA0B,EAAE,WAAW,aAAa,EAAE,WAAW,cACnE,EAGK,IAAqB,EAAe,MAAM,GAAG,EAAE,EAC/C,IAAkB,EAAU,MAAM,GAAG,EAAE,EAMvC,IAAkB,IAAI,IAC1B,EAAS,KAAK,MAAoC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAClE,EAEK,IAAU;EACd;GACE,OAAO,EAAE,oDAAoD,iBAAiB;GAE9E,OAAO,OAAO,GAAM,oBAAoB,cAAc,EAAS,OAAO;GACtE,OAAO,GAAG,EAAe,OAAO,GAAG,EAAE,oDAAoD,SAAS;GACnG;EACD;GACE,OAAO,EAAE,oDAAoD,iBAAiB;GAC9E,OAAO,OAAO,EAAc,OAAO;GACnC,OAAO,GAAG,GAAM,mBAAmB,cAAc,EAAQ,OAAO,GAAG,EAAE,oDAAoD,QAAQ;GAClI;EACD;GACE,OAAO,EAAE,gDAAgD,aAAa;GACtE,OAAO,OAAO,GAAM,qBAAqB,cAAc,EAAE;GACzD,OAAO,EAAE,gDAAgD,sBAAsB;GAChF;EACD;GACE,OAAO,EAAE,iDAAiD,aAAa;GACvE,OAAO,OAAO,GAAM,sBAAsB,cAAc,EAAW,OAAO;GAC1E,OAAO,GAAG,EAAe,OAAO,GAAG,EAAE,iDAAiD,WAAW;GAClG;EACF;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,GAAD,EAAiB,WAAU,WAAY,CAAA,EACtC,EAAE,8BAA8B,YAAY,CAC1C;QACL,kBAAC,KAAD;KAAG,WAAU;eACV,EAAE,iCAAiC,+CAA+C;KACjF,CAAA,CACA,EAAA,CAAA;IACF,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,KAAK,MACZ,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAY,WAAU;eAAtB,CACE,kBAAC,GAAD;MAAW,WAAU;gBAAuB,EAAE;MAAkB,CAAA,EAChE,kBAAC,GAAD,EAAY,WAAU,iCAAkC,CAAA,CAC7C;QACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eAAsB,EAAE;KAAY,CAAA,EACnD,kBAAC,KAAD;KAAG,WAAU;eAAiC,EAAE;KAAU,CAAA,CAC9C,EAAA,CAAA,CACT,EAAA,EATI,EAAE,MASN,CACP;IACE,CAAA;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,GAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;MAAW,WAAU;gBAArB,CACE,kBAAC,GAAD,EAAK,WAAU,yBAA0B,CAAA,EACxC,EAAE,4CAA4C,iBAAiB,CACtD;SACD,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACG,EAAc,KACZ,MASK;OACJ,IAAM,IAAc,EAAO,UAAU,QAClC,GAAa,MAAuC,KAAO,EAAG,eAAe,IAC9E,EACD,EACK,IAAkB,EAAO,UAC5B,QAAQ,MAA2B,EAAG,WAAW,OAAO,CACxD,QACE,GAAa,MACZ,KAAO,EAAG,eAAe,IAC3B,EACD,EACG,IAAa,EAAO,UAAU,QAC9B,IAAiB,EAAO,UAAU,QACrC,MAA2B,EAAG,WAAW,OAC3C,CAAC,QACI,IAAM,IAAc,IAAI,KAAK,MAAO,IAAkB,IAAe,IAAI,GAAG;AAClF,cACE,kBAAC,OAAD;QAAqB,WAAU;kBAA/B;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAe,EAAO;WAAS,CAAA,EAC5C,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAE,uCAAuC,UAAU;YAAC;YAAE;YACtD,EAAO,SAAS,QACf,EAAE,kDAAkD,gBAAgB;YACpE;aACA,EAAA,CAAA,EACN,kBAAC,GAAD;WAAO,SAAQ;qBAAf;YACG;YAAgB;YAAE;YAAa;YAC/B,EAAE,2CAA2C,MAAM;YAC9C;aACJ;;SACN,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,OAAD;WACE,WAAU;WACV,OAAO,EAAE,OAAO,GAAG,EAAI,IAAI;WAC3B,CAAA;UACE,CAAA;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD,EAAA,UAAA;WACG;WAAe;WAAE;WAAY;WAC7B,EAAE,qCAAqC,QAAQ;WAC3C,EAAA,CAAA,EACP,kBAAC,QAAD,EAAA,UAAA;WACG,EAAO,YAAY,IAAI,KAAK,EAAO,UAAU,CAAC,oBAAoB,GAAG;WAAI;WAAG;WAC5E,EAAO,UAAU,IAAI,KAAK,EAAO,QAAQ,CAAC,oBAAoB,GAAG;WAC7D,EAAA,CAAA,CACH;;SACF;UA/BI,EAAO,GA+BX;QAGX,EACA,EAAc,WAAW,KACxB,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE,4CAA4C,qBAAqB;OAClE,CAAA,CAEM;QACT;QAGP,kBAAC,OAAD;KAAK,WAAU;eAAf,CAEG,EAAS,QAAQ,MAA0B,EAAE,WAAW,UAAU,CAAC,SAAS,KAC3E,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;MAAW,WAAU;gBAArB,CACE,kBAAC,GAAD,EAAe,WAAU,2BAA4B,CAAA,EACpD,EAAE,6CAA6C,mBAAmB,CACzD;SACD,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBACpB,EACE,QAAQ,MAA0B,EAAE,WAAW,UAAU,CACzD,KAAK,MACJ,kBAAC,OAAD;OAEE,WAAU;iBAFZ,CAIE,kBAAC,OAAD,EAAA,UACE,kBAAC,KAAD;QAAG,WAAU;kBAAuB,EAAE;QAAS,CAAA,EAC3C,CAAA,EACN,kBAAC,GAAD;QAAO,SAAQ;kBACZ,EAAE,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;QACnC,CAAA,CACJ;SATC,EAAE,GASH,CACN;MACQ,CAAA,CACT,EAAA,CAAA,EAIT,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;MAAW,WAAU;gBAArB,CACE,kBAAC,GAAD,EAAU,WAAU,0BAA2B,CAAA,EAC9C,EAAE,iDAAiD,sBAAsB,CAChE;SACD,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACG,EAAmB,KACjB,MAOC,kBAAC,OAAD;OAAiB,WAAU;iBAA3B,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,KAAD;UAAG,WAAU;oBAAgC,EAAG;UAAS,CAAA;SACzD,kBAAC,KAAD;UAAG,WAAU;oBAAb;WACG,EAAE,uCAAuC,UAAU;WAAC;WAAE;WACtD,EAAG,SAAS,QACX,EAAE,kDAAkD,gBAAgB;WACpE;;SACJ,kBAAC,KAAD;UAAG,WAAU;oBAAb;WACG,EAAE,mCAAmC,MAAM;WAAE;WAC7C,EAAG,UAAU,IAAI,KAAK,EAAG,QAAQ,CAAC,oBAAoB,GAAG;WACxD;;SACA;WACN,kBAAC,GAAD;QAAO,SAAQ;QAAY,WAAU;kBAClC,EAAG,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;QACpC,CAAA,CACJ;SAhBI,EAAG,GAgBP,CAET,EACA,EAAmB,WAAW,KAC7B,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE,iDAAiD,0BAA0B;OAC5E,CAAA,CAEM;QACT,EAAA,CAAA,CACH;OACF;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;KAAW,WAAU;eAArB,CACE,kBAAC,GAAD,EAAa,WAAU,2BAA4B,CAAA,EAClD,EAAE,8CAA8C,oBAAoB,CAC3D;QACD,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eAAvB,CACG,EAAgB,KACd,MASC,kBAAC,OAAD;MAAmB,WAAU;gBAA7B,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAgC,EAAK;QAAQ,CAAA,EAC1D,kBAAC,KAAD;QAAG,WAAU;kBAAb,CACG,EAAgB,IAAI,EAAK,UAAU,IAClC,WAAW,EAAK,UAAU,MAAM,GAAG,EAAE,IACtC,EAAK,aACJ,kBAAA,GAAA,EAAA,UAAA,CACG,OACD,kBAAC,GAAD,EAAY,IAAI,EAAK,YAAc,CAAA,CAClC,EAAA,CAAA,GACD,KACF;UACA;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;QAAU,WAAU;kBAAnC;SACG,EAAK,eAAe;SAAE;SAAE,EAAE,2CAA2C,MAAM;SACtE;WACR,kBAAC,GAAD;QAAO,SAAQ;QAAY,WAAU;kBAClC,EAAK,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;QACtC,CAAA,CACJ;SACF;QAtBI,EAAK,GAsBT,CAET,EACA,EAAgB,WAAW,KAC1B,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,8CAA8C,qBAAqB;MACpE,CAAA,CAEM;OACT,EAAA,CAAA,EAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;KAAW,WAAU;eAArB,CACE,kBAAC,GAAD,EAAc,WAAU,yBAA0B,CAAA,EACjD,EAAE,4CAA4C,iBAAiB,CACtD;QACD,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eAAvB,CACG,EAAe,KACb,MACC,kBAAC,OAAD;MAAgB,WAAU;gBAA1B,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAgC,EAAE;QAAS,CAAA,EACxD,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAE,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;QACvC,CAAA,CACA;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAO,WAAU,iCAAkC,CAAA;QACnD,kBAAC,QAAD;SAAM,WAAU;mBACd,kBAAC,GAAD,EAAY,IAAI,EAAE,SAAW,CAAA;SACxB,CAAA;QACP,kBAAC,GAAD,EAAc,WAAU,iCAAkC,CAAA;QACtD;SACF;QAdI,EAAE,GAcN,CAET,EACA,EAAe,WAAW,KACzB,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,4CAA4C,sBAAsB;MACnE,CAAA,CAEM;OACT,EAAA,CAAA,CACH;;GACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
2
|
import { PersonName as t } from "../people/People.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { throwOnError as n } from "../utils/mutationResult.js";
|
|
4
|
+
import { MarkdownEditor as ee } from "../../components/MarkdownEditor.js";
|
|
5
5
|
import { DOC_STATUSES as te, DocStatusBadge as ne } from "../docs/DocStatusBadge.js";
|
|
6
6
|
import { useState as r } from "react";
|
|
7
7
|
import { ArrowLeft as re, History as ie, Link2 as ae, X as oe } from "lucide-react";
|
|
@@ -76,7 +76,7 @@ function A() {
|
|
|
76
76
|
}, [q, J] = a(T, K), [ge, _e] = a(E, K), [ve, ye] = a(D, { refetchQueries: ["DocsList"] }), [be, xe] = a(de, K), Y = W?.planmagnetDoc, X = J.loading || _e.loading || ye.loading, Z = async (e, t) => {
|
|
77
77
|
V(null);
|
|
78
78
|
try {
|
|
79
|
-
return await
|
|
79
|
+
return await n(e()), !0;
|
|
80
80
|
} catch (e) {
|
|
81
81
|
return V(O(e, t)), ue(), await he.query({
|
|
82
82
|
query: w,
|
|
@@ -230,7 +230,7 @@ function A() {
|
|
|
230
230
|
value: P,
|
|
231
231
|
onChange: (e) => F(e.target.value)
|
|
232
232
|
}),
|
|
233
|
-
/* @__PURE__ */ o(
|
|
233
|
+
/* @__PURE__ */ o(ee, {
|
|
234
234
|
value: I,
|
|
235
235
|
onChange: L,
|
|
236
236
|
minHeight: 240
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocsDocDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DocsDocDetailPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { gql } from '@apollo/client';\nimport { useApolloClient, useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql';\nimport { ArrowLeft, History, Link2, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\nimport { DOC_STATUSES, DocStatusBadge, type DocStatus } from '../docs/DocStatusBadge';\n\n/**\n * One doc (P1.4): read it, edit it with a change note, publish or archive it,\n * browse its history and restore an older version, see and unlink the work\n * items it is attached to, and delete it.\n *\n * Every write carries `expectedVersion` — the version on screen — so a save\n * over someone else's newer edit is refused with the reason, and the editor\n * keeps the unsaved text rather than overwriting theirs.\n */\n\nconst DOC = gql`\n query DocDetail($id: ID!) {\n planmagnetDoc(id: $id) {\n id\n title\n content\n status\n version\n createdBy\n createdAt\n updatedBy\n updatedAt\n revisions {\n id\n version\n title\n content\n changeNote\n createdBy\n createdAt\n }\n linkedWorkItems {\n id\n key\n title\n }\n }\n }\n`;\nconst UPDATE = gql`\n mutation DocUpdate($id: ID!, $input: PlanmagnetUpdateDocInput!) {\n planmagnetUpdateDoc(id: $id, input: $input) {\n id\n version\n status\n }\n }\n`;\nconst REVERT = gql`\n mutation DocRevert($id: ID!, $toVersion: Int!, $expectedVersion: Int) {\n planmagnetRevertDoc(id: $id, toVersion: $toVersion, expectedVersion: $expectedVersion) {\n id\n version\n }\n }\n`;\nconst DELETE = gql`\n mutation DocDelete($id: ID!) {\n planmagnetDeleteDoc(id: $id)\n }\n`;\nconst UNLINK = gql`\n mutation DocUnlinkWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface Revision {\n id: string;\n version: number;\n title: string;\n content: string;\n changeNote: string | null;\n createdBy: string;\n createdAt: string;\n}\n\ninterface Doc {\n id: string;\n title: string;\n content: string;\n status: string;\n version: number;\n createdBy: string;\n createdAt: string;\n updatedBy: string | null;\n updatedAt: string;\n revisions: Revision[];\n linkedWorkItems: { id: string; key: string; title: string }[];\n}\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nexport function DocsDocDetailPagePage() {\n const [params] = useSearchParams();\n const docId = params.get('docId') ?? '';\n const navigate = usePlanMagnetNavigate();\n const [editing, setEditing] = useState(false);\n const [title, setTitle] = useState('');\n const [content, setContent] = useState('');\n const [changeNote, setChangeNote] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [viewing, setViewing] = useState<number | null>(null);\n // The version the editor started from — not the one on screen now, which a\n // refetch after someone else's save may already have moved past.\n const [editBase, setEditBase] = useState<number | null>(null);\n\n const {\n data,\n loading,\n error: loadError,\n } = useQuery<{ planmagnetDoc: Doc | null }>(DOC, {\n variables: { id: docId },\n skip: !docId,\n // Cached copies go stale here: a deleted doc, or someone else's newer version.\n fetchPolicy: 'cache-and-network',\n });\n const client = useApolloClient();\n const after = { refetchQueries: ['DocDetail'], awaitRefetchQueries: true };\n const [update, updateState] = useMutation(UPDATE, after);\n const [revert, revertState] = useMutation(REVERT, after);\n const [remove, removeState] = useMutation(DELETE, { refetchQueries: ['DocsList'] });\n const [unlink, unlinkState] = useMutation(UNLINK, after);\n\n const doc = data?.planmagnetDoc;\n const busy = updateState.loading || revertState.loading || removeState.loading;\n\n // Refusals are panel-level: a refused write refetches, and anything the\n // refetch re-renders must not take the reason with it.\n const run = async (action: () => Promise<{ error?: unknown }>, fallback: string) => {\n setError(null);\n try {\n await throwOnError(action());\n return true;\n } catch (e) {\n setError(messageOf(e, fallback));\n // A refusal because the doc moved on: show the current version, but\n // leave the editor's text alone so nothing typed is lost.\n //\n // fe-libs replays any identical query answered in the last 15 s\n // (fetch.ts RECENT_QUERY_CACHE_TTL_MS) underneath Apollo's fetch\n // policies, and only a SUCCESSFUL mutation clears that cache. Measured in\n // prod 2026-09-25: after a refused save, even a network-only read was\n // served the pre-refusal response, so the page kept the old version and\n // never said the doc had moved on. A refused write is still a write\n // attempt: clear the cache (the public API fe-libs documents for this),\n // then read the doc; it lands in the normalized cache the page renders.\n clearGraphqlFetchResponseCache();\n await client\n .query({ query: DOC, variables: { id: docId }, fetchPolicy: 'network-only' })\n .catch(() => undefined);\n return false;\n }\n };\n\n if (!docId) {\n return <div className=\"p-6 text-sm text-muted-foreground\">No doc selected.</div>;\n }\n if (loading && !data) {\n return <div className=\"p-6 text-sm text-muted-foreground\">Loading doc…</div>;\n }\n if (loadError && !doc) {\n return (\n <p role=\"alert\" className=\"p-6 text-sm text-destructive\">\n The doc could not be loaded: {loadError.message}\n </p>\n );\n }\n if (!doc) {\n return (\n <div className=\"space-y-3 p-6\">\n <p className=\"text-sm text-muted-foreground\">\n This doc does not exist in this workspace, or it was deleted.\n </p>\n <Button size=\"sm\" variant=\"outline\" onClick={() => navigate('/docs/docs-page')}>\n Back to docs\n </Button>\n </div>\n );\n }\n\n const startEditing = () => {\n setTitle(doc.title);\n setContent(doc.content);\n setChangeNote('');\n setError(null);\n setEditBase(doc.version);\n setEditing(true);\n };\n const save = async () => {\n const saved = await run(\n () =>\n update({\n variables: {\n id: doc.id,\n input: {\n title: title.trim(),\n content,\n ...(changeNote.trim() ? { changeNote: changeNote.trim() } : {}),\n expectedVersion: editBase,\n },\n },\n }),\n 'The doc could not be saved.'\n );\n if (saved) setEditing(false);\n };\n\n const revisions = [...doc.revisions].sort((a, b) => b.version - a.version);\n const shown = viewing === null ? null : revisions.find((r) => r.version === viewing);\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"doc-detail\">\n <Button size=\"sm\" variant=\"ghost\" onClick={() => navigate('/docs/docs-page')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" />\n All docs\n </Button>\n\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0 space-y-1\">\n <h1 className=\"text-2xl font-semibold break-words\" data-testid=\"doc-title\">\n {doc.title}\n </h1>\n <p className=\"text-xs text-muted-foreground\">\n <DocStatusBadge status={doc.status} /> · version {doc.version} · updated{' '}\n {when(doc.updatedAt)} by <PersonName id={doc.updatedBy ?? doc.createdBy} />\n </p>\n </div>\n {!editing && (\n <div className=\"flex flex-wrap gap-2\">\n {DOC_STATUSES.filter((s) => s !== doc.status).map((s: DocStatus) => (\n <Button\n key={s}\n size=\"sm\"\n variant=\"outline\"\n disabled={busy}\n onClick={() =>\n run(\n () =>\n update({\n variables: {\n id: doc.id,\n input: { status: s, expectedVersion: doc.version },\n },\n }),\n 'The status could not be changed.'\n )\n }\n >\n {s === 'PUBLISHED' ? 'Publish' : s === 'ARCHIVED' ? 'Archive' : 'Back to draft'}\n </Button>\n ))}\n <Button size=\"sm\" onClick={startEditing} data-testid=\"doc-edit\">\n Edit\n </Button>\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" disabled={busy}>\n Delete\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete “{doc.title}”?</AlertDialogTitle>\n <AlertDialogDescription>\n The doc and its history are removed for everyone, and it is unlinked from{' '}\n {doc.linkedWorkItems.length} work item\n {doc.linkedWorkItems.length === 1 ? '' : 's'}.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n onClick={async () => {\n const deleted = await run(\n () => remove({ variables: { id: doc.id } }),\n 'The doc could not be deleted.'\n );\n if (deleted) navigate('/docs/docs-page');\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n )}\n </div>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\" data-testid=\"doc-error\">\n {error}\n </p>\n )}\n {editing && editBase !== doc.version && (\n <p className=\"text-sm text-status-warning-text\" data-testid=\"doc-moved-on\">\n Someone saved version {doc.version} after you started editing version {editBase}. Your\n text is kept below; copy what you need, then discard and edit the latest.\n </p>\n )}\n\n {editing ? (\n <Card>\n <CardContent className=\"space-y-3 pt-6\">\n <Input\n aria-label=\"Doc title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n />\n <MarkdownEditor value={content} onChange={setContent} minHeight={240} />\n <Input\n aria-label=\"Change note\"\n placeholder=\"What changed? (optional, shown in the history)\"\n value={changeNote}\n onChange={(e) => setChangeNote(e.target.value)}\n />\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setEditing(false);\n setError(null);\n }}\n >\n {editBase !== doc.version ? 'Discard my edits' : 'Cancel'}\n </Button>\n <Button\n size=\"sm\"\n onClick={save}\n disabled={updateState.loading || editBase !== doc.version}\n data-testid=\"doc-save\"\n >\n {updateState.loading ? 'Saving…' : 'Save new version'}\n </Button>\n </div>\n </CardContent>\n </Card>\n ) : (\n <Card>\n <CardContent className=\"prose prose-sm max-w-none pt-6 dark:prose-invert\">\n {doc.content.trim() ? (\n <Markdown>{doc.content}</Markdown>\n ) : (\n <p className=\"text-sm text-muted-foreground\">This doc is empty.</p>\n )}\n </CardContent>\n </Card>\n )}\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <Card data-testid=\"doc-history\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <History className=\"h-4 w-4\" /> History ({revisions.length})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <ol className=\"space-y-2\">\n {revisions.map((rev) => {\n const current = rev.version === doc.version;\n return (\n <li\n key={rev.id}\n className=\"space-y-1 rounded-md border p-2\"\n data-testid=\"doc-revision\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span className=\"text-sm font-medium\">\n Version {rev.version}\n {current ? ' (current)' : ''}\n </span>\n <span className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 text-xs\"\n aria-expanded={viewing === rev.version}\n onClick={() => setViewing(viewing === rev.version ? null : rev.version)}\n >\n {viewing === rev.version ? 'Hide' : 'View'}\n </Button>\n {!current && !editing && (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={busy}\n data-testid=\"doc-restore\"\n >\n Restore\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Restore version {rev.version}?</AlertDialogTitle>\n <AlertDialogDescription>\n Its title and text become version {doc.version + 1}. Nothing is\n deleted: version {doc.version} stays in the history.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={() =>\n run(\n () =>\n revert({\n variables: {\n id: doc.id,\n toVersion: rev.version,\n expectedVersion: doc.version,\n },\n }),\n 'The version could not be restored.'\n )\n }\n >\n Restore\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n )}\n </span>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {when(rev.createdAt)} · <PersonName id={rev.createdBy} />\n {rev.changeNote ? ` · ${rev.changeNote}` : ''}\n </p>\n </li>\n );\n })}\n </ol>\n {shown && (\n <div\n className=\"space-y-1 rounded-md border bg-muted/40 p-3\"\n data-testid=\"doc-revision-view\"\n >\n <p className=\"text-sm font-medium\">\n Version {shown.version}: {shown.title}\n </p>\n <div className=\"prose prose-sm max-w-none dark:prose-invert\">\n <Markdown>{shown.content}</Markdown>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n\n <Card data-testid=\"doc-linked-items\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Link2 className=\"h-4 w-4\" /> Linked work items ({doc.linkedWorkItems.length})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {doc.linkedWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n Not linked to any work item. A doc created from a task's “Linked Docs” panel is\n linked to that task; linking an existing doc is not available yet.\n </p>\n )}\n {doc.linkedWorkItems.map((item) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between gap-2 rounded-md border p-2\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left text-sm hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(item.id)}`)\n }\n >\n <span className=\"text-muted-foreground\">{item.key}</span> {item.title}\n </button>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinkState.loading}\n aria-label={`Unlink ${item.title}`}\n onClick={() =>\n run(\n () => unlink({ variables: { docId: doc.id, workItemId: item.id } }),\n 'The work item could not be unlinked.'\n )\n }\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DocsDocDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DocsDocDetailPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { gql } from '@apollo/client';\nimport { useApolloClient, useMutation, useQuery } from '@apollo/client/react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql';\nimport { ArrowLeft, History, Link2, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { PersonName } from '../people/People';\nimport { throwOnError } from '../utils/mutationResult';\nimport { DOC_STATUSES, DocStatusBadge, type DocStatus } from '../docs/DocStatusBadge';\n\n/**\n * One doc (P1.4): read it, edit it with a change note, publish or archive it,\n * browse its history and restore an older version, see and unlink the work\n * items it is attached to, and delete it.\n *\n * Every write carries `expectedVersion` — the version on screen — so a save\n * over someone else's newer edit is refused with the reason, and the editor\n * keeps the unsaved text rather than overwriting theirs.\n */\n\nconst DOC = gql`\n query DocDetail($id: ID!) {\n planmagnetDoc(id: $id) {\n id\n title\n content\n status\n version\n createdBy\n createdAt\n updatedBy\n updatedAt\n revisions {\n id\n version\n title\n content\n changeNote\n createdBy\n createdAt\n }\n linkedWorkItems {\n id\n key\n title\n }\n }\n }\n`;\nconst UPDATE = gql`\n mutation DocUpdate($id: ID!, $input: PlanmagnetUpdateDocInput!) {\n planmagnetUpdateDoc(id: $id, input: $input) {\n id\n version\n status\n }\n }\n`;\nconst REVERT = gql`\n mutation DocRevert($id: ID!, $toVersion: Int!, $expectedVersion: Int) {\n planmagnetRevertDoc(id: $id, toVersion: $toVersion, expectedVersion: $expectedVersion) {\n id\n version\n }\n }\n`;\nconst DELETE = gql`\n mutation DocDelete($id: ID!) {\n planmagnetDeleteDoc(id: $id)\n }\n`;\nconst UNLINK = gql`\n mutation DocUnlinkWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface Revision {\n id: string;\n version: number;\n title: string;\n content: string;\n changeNote: string | null;\n createdBy: string;\n createdAt: string;\n}\n\ninterface Doc {\n id: string;\n title: string;\n content: string;\n status: string;\n version: number;\n createdBy: string;\n createdAt: string;\n updatedBy: string | null;\n updatedAt: string;\n revisions: Revision[];\n linkedWorkItems: { id: string; key: string; title: string }[];\n}\n\nconst messageOf = (e: unknown, fallback: string) => (e instanceof Error ? e.message : fallback);\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nexport function DocsDocDetailPagePage() {\n const [params] = useSearchParams();\n const docId = params.get('docId') ?? '';\n const navigate = usePlanMagnetNavigate();\n const [editing, setEditing] = useState(false);\n const [title, setTitle] = useState('');\n const [content, setContent] = useState('');\n const [changeNote, setChangeNote] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [viewing, setViewing] = useState<number | null>(null);\n // The version the editor started from — not the one on screen now, which a\n // refetch after someone else's save may already have moved past.\n const [editBase, setEditBase] = useState<number | null>(null);\n\n const {\n data,\n loading,\n error: loadError,\n } = useQuery<{ planmagnetDoc: Doc | null }>(DOC, {\n variables: { id: docId },\n skip: !docId,\n // Cached copies go stale here: a deleted doc, or someone else's newer version.\n fetchPolicy: 'cache-and-network',\n });\n const client = useApolloClient();\n const after = { refetchQueries: ['DocDetail'], awaitRefetchQueries: true };\n const [update, updateState] = useMutation(UPDATE, after);\n const [revert, revertState] = useMutation(REVERT, after);\n const [remove, removeState] = useMutation(DELETE, { refetchQueries: ['DocsList'] });\n const [unlink, unlinkState] = useMutation(UNLINK, after);\n\n const doc = data?.planmagnetDoc;\n const busy = updateState.loading || revertState.loading || removeState.loading;\n\n // Refusals are panel-level: a refused write refetches, and anything the\n // refetch re-renders must not take the reason with it.\n const run = async (action: () => Promise<{ error?: unknown }>, fallback: string) => {\n setError(null);\n try {\n await throwOnError(action());\n return true;\n } catch (e) {\n setError(messageOf(e, fallback));\n // A refusal because the doc moved on: show the current version, but\n // leave the editor's text alone so nothing typed is lost.\n //\n // fe-libs replays any identical query answered in the last 15 s\n // (fetch.ts RECENT_QUERY_CACHE_TTL_MS) underneath Apollo's fetch\n // policies, and only a SUCCESSFUL mutation clears that cache. Measured in\n // prod 2026-09-25: after a refused save, even a network-only read was\n // served the pre-refusal response, so the page kept the old version and\n // never said the doc had moved on. A refused write is still a write\n // attempt: clear the cache (the public API fe-libs documents for this),\n // then read the doc; it lands in the normalized cache the page renders.\n clearGraphqlFetchResponseCache();\n await client\n .query({ query: DOC, variables: { id: docId }, fetchPolicy: 'network-only' })\n .catch(() => undefined);\n return false;\n }\n };\n\n if (!docId) {\n return <div className=\"p-6 text-sm text-muted-foreground\">No doc selected.</div>;\n }\n if (loading && !data) {\n return <div className=\"p-6 text-sm text-muted-foreground\">Loading doc…</div>;\n }\n if (loadError && !doc) {\n return (\n <p role=\"alert\" className=\"p-6 text-sm text-destructive\">\n The doc could not be loaded: {loadError.message}\n </p>\n );\n }\n if (!doc) {\n return (\n <div className=\"space-y-3 p-6\">\n <p className=\"text-sm text-muted-foreground\">\n This doc does not exist in this workspace, or it was deleted.\n </p>\n <Button size=\"sm\" variant=\"outline\" onClick={() => navigate('/docs/docs-page')}>\n Back to docs\n </Button>\n </div>\n );\n }\n\n const startEditing = () => {\n setTitle(doc.title);\n setContent(doc.content);\n setChangeNote('');\n setError(null);\n setEditBase(doc.version);\n setEditing(true);\n };\n const save = async () => {\n const saved = await run(\n () =>\n update({\n variables: {\n id: doc.id,\n input: {\n title: title.trim(),\n content,\n ...(changeNote.trim() ? { changeNote: changeNote.trim() } : {}),\n expectedVersion: editBase,\n },\n },\n }),\n 'The doc could not be saved.'\n );\n if (saved) setEditing(false);\n };\n\n const revisions = [...doc.revisions].sort((a, b) => b.version - a.version);\n const shown = viewing === null ? null : revisions.find((r) => r.version === viewing);\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"doc-detail\">\n <Button size=\"sm\" variant=\"ghost\" onClick={() => navigate('/docs/docs-page')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" />\n All docs\n </Button>\n\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0 space-y-1\">\n <h1 className=\"text-2xl font-semibold break-words\" data-testid=\"doc-title\">\n {doc.title}\n </h1>\n <p className=\"text-xs text-muted-foreground\">\n <DocStatusBadge status={doc.status} /> · version {doc.version} · updated{' '}\n {when(doc.updatedAt)} by <PersonName id={doc.updatedBy ?? doc.createdBy} />\n </p>\n </div>\n {!editing && (\n <div className=\"flex flex-wrap gap-2\">\n {DOC_STATUSES.filter((s) => s !== doc.status).map((s: DocStatus) => (\n <Button\n key={s}\n size=\"sm\"\n variant=\"outline\"\n disabled={busy}\n onClick={() =>\n run(\n () =>\n update({\n variables: {\n id: doc.id,\n input: { status: s, expectedVersion: doc.version },\n },\n }),\n 'The status could not be changed.'\n )\n }\n >\n {s === 'PUBLISHED' ? 'Publish' : s === 'ARCHIVED' ? 'Archive' : 'Back to draft'}\n </Button>\n ))}\n <Button size=\"sm\" onClick={startEditing} data-testid=\"doc-edit\">\n Edit\n </Button>\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" disabled={busy}>\n Delete\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete “{doc.title}”?</AlertDialogTitle>\n <AlertDialogDescription>\n The doc and its history are removed for everyone, and it is unlinked from{' '}\n {doc.linkedWorkItems.length} work item\n {doc.linkedWorkItems.length === 1 ? '' : 's'}.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Keep it</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n onClick={async () => {\n const deleted = await run(\n () => remove({ variables: { id: doc.id } }),\n 'The doc could not be deleted.'\n );\n if (deleted) navigate('/docs/docs-page');\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n )}\n </div>\n\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\" data-testid=\"doc-error\">\n {error}\n </p>\n )}\n {editing && editBase !== doc.version && (\n <p className=\"text-sm text-status-warning-text\" data-testid=\"doc-moved-on\">\n Someone saved version {doc.version} after you started editing version {editBase}. Your\n text is kept below; copy what you need, then discard and edit the latest.\n </p>\n )}\n\n {editing ? (\n <Card>\n <CardContent className=\"space-y-3 pt-6\">\n <Input\n aria-label=\"Doc title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n />\n <MarkdownEditor value={content} onChange={setContent} minHeight={240} />\n <Input\n aria-label=\"Change note\"\n placeholder=\"What changed? (optional, shown in the history)\"\n value={changeNote}\n onChange={(e) => setChangeNote(e.target.value)}\n />\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setEditing(false);\n setError(null);\n }}\n >\n {editBase !== doc.version ? 'Discard my edits' : 'Cancel'}\n </Button>\n <Button\n size=\"sm\"\n onClick={save}\n disabled={updateState.loading || editBase !== doc.version}\n data-testid=\"doc-save\"\n >\n {updateState.loading ? 'Saving…' : 'Save new version'}\n </Button>\n </div>\n </CardContent>\n </Card>\n ) : (\n <Card>\n <CardContent className=\"prose prose-sm max-w-none pt-6 dark:prose-invert\">\n {doc.content.trim() ? (\n <Markdown>{doc.content}</Markdown>\n ) : (\n <p className=\"text-sm text-muted-foreground\">This doc is empty.</p>\n )}\n </CardContent>\n </Card>\n )}\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <Card data-testid=\"doc-history\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <History className=\"h-4 w-4\" /> History ({revisions.length})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n <ol className=\"space-y-2\">\n {revisions.map((rev) => {\n const current = rev.version === doc.version;\n return (\n <li\n key={rev.id}\n className=\"space-y-1 rounded-md border p-2\"\n data-testid=\"doc-revision\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span className=\"text-sm font-medium\">\n Version {rev.version}\n {current ? ' (current)' : ''}\n </span>\n <span className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 text-xs\"\n aria-expanded={viewing === rev.version}\n onClick={() => setViewing(viewing === rev.version ? null : rev.version)}\n >\n {viewing === rev.version ? 'Hide' : 'View'}\n </Button>\n {!current && !editing && (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={busy}\n data-testid=\"doc-restore\"\n >\n Restore\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Restore version {rev.version}?</AlertDialogTitle>\n <AlertDialogDescription>\n Its title and text become version {doc.version + 1}. Nothing is\n deleted: version {doc.version} stays in the history.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={() =>\n run(\n () =>\n revert({\n variables: {\n id: doc.id,\n toVersion: rev.version,\n expectedVersion: doc.version,\n },\n }),\n 'The version could not be restored.'\n )\n }\n >\n Restore\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n )}\n </span>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {when(rev.createdAt)} · <PersonName id={rev.createdBy} />\n {rev.changeNote ? ` · ${rev.changeNote}` : ''}\n </p>\n </li>\n );\n })}\n </ol>\n {shown && (\n <div\n className=\"space-y-1 rounded-md border bg-muted/40 p-3\"\n data-testid=\"doc-revision-view\"\n >\n <p className=\"text-sm font-medium\">\n Version {shown.version}: {shown.title}\n </p>\n <div className=\"prose prose-sm max-w-none dark:prose-invert\">\n <Markdown>{shown.content}</Markdown>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n\n <Card data-testid=\"doc-linked-items\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Link2 className=\"h-4 w-4\" /> Linked work items ({doc.linkedWorkItems.length})\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {doc.linkedWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n Not linked to any work item. A doc created from a task's “Linked Docs” panel is\n linked to that task; linking an existing doc is not available yet.\n </p>\n )}\n {doc.linkedWorkItems.map((item) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between gap-2 rounded-md border p-2\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left text-sm hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(item.id)}`)\n }\n >\n <span className=\"text-muted-foreground\">{item.key}</span> {item.title}\n </button>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinkState.loading}\n aria-label={`Unlink ${item.title}`}\n onClick={() =>\n run(\n () => unlink({ variables: { docId: doc.id, workItemId: item.id } }),\n 'The work item could not be unlinked.'\n )\n }\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n ))}\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
2
|
import { PersonName as n } from "../people/People.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { throwOnError as r } from "../utils/mutationResult.js";
|
|
4
|
+
import { MarkdownEditor as i } from "../../components/MarkdownEditor.js";
|
|
5
5
|
import { DOC_STATUSES as a, DOC_STATUS_LABEL as o, DocStatusBadge as s } from "../docs/DocStatusBadge.js";
|
|
6
6
|
import { useDeferredValue as c, useState as l } from "react";
|
|
7
7
|
import { FileText as u, Plus as d, Search as f } from "lucide-react";
|
|
@@ -55,7 +55,7 @@ function E() {
|
|
|
55
55
|
}), [K, q] = m(w, { refetchQueries: ["DocsList"] }), J = H?.planmagnetDocs, Y = J?.items ?? [], X = async () => {
|
|
56
56
|
R(null);
|
|
57
57
|
try {
|
|
58
|
-
let e = (await
|
|
58
|
+
let e = (await r(K({ variables: { input: {
|
|
59
59
|
title: N.trim(),
|
|
60
60
|
content: F
|
|
61
61
|
} } }))).data?.planmagnetCreateDoc.id;
|
|
@@ -116,7 +116,7 @@ function E() {
|
|
|
116
116
|
value: N,
|
|
117
117
|
onChange: (e) => P(e.target.value)
|
|
118
118
|
}),
|
|
119
|
-
/* @__PURE__ */ g(
|
|
119
|
+
/* @__PURE__ */ g(i, {
|
|
120
120
|
value: F,
|
|
121
121
|
onChange: I,
|
|
122
122
|
placeholder: "Write the doc…",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"AA4DA,wBAAgB,6BAA6B,4CAqN5C"}
|