@burdenoff/microfe-planmagnet 2026.927.3 → 2026.927.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/generated/global-types.d.ts +9 -1
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-operations.d.ts +0 -12
  4. package/dist/generated/wspace-operations.d.ts.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  6. package/dist/planmagnet/PlanMagnetRoutes.js +14 -10
  7. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  8. package/dist/planmagnet/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
  9. package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +149 -127
  10. package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
  11. package/dist/planmagnet/controls/FeatureControls.d.ts +26 -0
  12. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -0
  13. package/dist/planmagnet/controls/FeatureControls.js +321 -0
  14. package/dist/planmagnet/controls/FeatureControls.js.map +1 -0
  15. package/dist/planmagnet/controls/FeatureTriage.d.ts +21 -0
  16. package/dist/planmagnet/controls/FeatureTriage.d.ts.map +1 -0
  17. package/dist/planmagnet/controls/FeatureTriage.js +398 -0
  18. package/dist/planmagnet/controls/FeatureTriage.js.map +1 -0
  19. package/dist/planmagnet/controls/MilestoneControls.d.ts +17 -0
  20. package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -0
  21. package/dist/planmagnet/controls/MilestoneControls.js +203 -0
  22. package/dist/planmagnet/controls/MilestoneControls.js.map +1 -0
  23. package/dist/planmagnet/controls/ObjectiveControls.d.ts +31 -0
  24. package/dist/planmagnet/controls/ObjectiveControls.d.ts.map +1 -0
  25. package/dist/planmagnet/controls/ObjectiveControls.js +296 -0
  26. package/dist/planmagnet/controls/ObjectiveControls.js.map +1 -0
  27. package/dist/planmagnet/controls/Pickers.d.ts +26 -0
  28. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -0
  29. package/dist/planmagnet/controls/Pickers.js +171 -0
  30. package/dist/planmagnet/controls/Pickers.js.map +1 -0
  31. package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
  32. package/dist/planmagnet/controls/ProjectControls.js +47 -44
  33. package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
  34. package/dist/planmagnet/controls/ReleaseControls.d.ts +14 -0
  35. package/dist/planmagnet/controls/ReleaseControls.d.ts.map +1 -0
  36. package/dist/planmagnet/controls/ReleaseControls.js +111 -0
  37. package/dist/planmagnet/controls/ReleaseControls.js.map +1 -0
  38. package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
  39. package/dist/planmagnet/controls/WorkItemControls.js +68 -65
  40. package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
  41. package/dist/planmagnet/hooks/useCounts.d.ts +39 -0
  42. package/dist/planmagnet/hooks/useCounts.d.ts.map +1 -0
  43. package/dist/planmagnet/hooks/useCounts.js +209 -0
  44. package/dist/planmagnet/hooks/useCounts.js.map +1 -0
  45. package/dist/planmagnet/import/exportColumns.d.ts +52 -0
  46. package/dist/planmagnet/import/exportColumns.d.ts.map +1 -0
  47. package/dist/planmagnet/import/exportColumns.js +81 -0
  48. package/dist/planmagnet/import/exportColumns.js.map +1 -0
  49. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
  50. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +255 -242
  51. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  52. package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  53. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +199 -160
  54. package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
  55. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  56. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +190 -153
  57. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  58. package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.d.ts.map +1 -1
  59. package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.js +63 -56
  60. package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.js.map +1 -1
  61. package/dist/planmagnet/pages/GoalsGoalsPagePage.d.ts.map +1 -1
  62. package/dist/planmagnet/pages/GoalsGoalsPagePage.js +2 -7
  63. package/dist/planmagnet/pages/GoalsGoalsPagePage.js.map +1 -1
  64. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.d.ts.map +1 -1
  65. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js +229 -214
  66. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js.map +1 -1
  67. package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
  68. package/dist/planmagnet/pages/ProductsEditProductPagePage.js +145 -114
  69. package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
  70. package/dist/planmagnet/pages/ProductsEditReleasePagePage.d.ts.map +1 -1
  71. package/dist/planmagnet/pages/ProductsEditReleasePagePage.js +134 -105
  72. package/dist/planmagnet/pages/ProductsEditReleasePagePage.js.map +1 -1
  73. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.d.ts.map +1 -1
  74. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +97 -117
  75. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
  76. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  77. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +169 -110
  78. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  79. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  80. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +117 -103
  81. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  82. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  83. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +119 -94
  84. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  85. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  86. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +4 -8
  87. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  88. package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
  89. package/dist/planmagnet/pages/ProductsProductsPagePage.js +85 -90
  90. package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
  91. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
  92. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +118 -107
  93. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
  94. package/dist/planmagnet/pages/ProductsReleasesPagePage.d.ts.map +1 -1
  95. package/dist/planmagnet/pages/ProductsReleasesPagePage.js +4 -2
  96. package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
  97. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
  98. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +48 -71
  99. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
  100. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
  101. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +17 -13
  102. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
  103. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.d.ts.map +1 -1
  104. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js +1 -1
  105. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js.map +1 -1
  106. package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.d.ts.map +1 -1
  107. package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +55 -63
  108. package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js.map +1 -1
  109. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  110. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +11 -8
  111. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  112. package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
  113. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +1 -1
  114. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
  115. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  116. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +152 -139
  117. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  118. package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
  119. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +75 -74
  120. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
  121. package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts +7 -0
  122. package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
  123. package/dist/planmagnet/pages/ReportsReportsPagePage.js +39 -201
  124. package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
  125. package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
  126. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +118 -99
  127. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
  128. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +123 -128
  129. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
  130. package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
  131. package/dist/planmagnet/pages/TasksEditTaskPagePage.js +139 -107
  132. package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
  133. package/dist/planmagnet/pages/TasksImportPagePage.d.ts +2 -0
  134. package/dist/planmagnet/pages/TasksImportPagePage.d.ts.map +1 -0
  135. package/dist/planmagnet/pages/TasksImportPagePage.js +309 -0
  136. package/dist/planmagnet/pages/TasksImportPagePage.js.map +1 -0
  137. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  138. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +173 -126
  139. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  140. package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
  141. package/dist/planmagnet/pages/TasksTasksPagePage.js +111 -104
  142. package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
  143. package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
  144. package/dist/planmagnet/pages/TeamTeamPagePage.js +99 -177
  145. package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
  146. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
  147. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -4
  148. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
  149. package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts.map +1 -1
  150. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js +10 -4
  151. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js.map +1 -1
  152. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
  153. package/dist/planmagnet/roadmaps/RoadmapControls.js +150 -137
  154. package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
  155. package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
  156. package/dist/planmagnet/sprints/SprintControls.js +32 -30
  157. package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
  158. package/dist/planmagnet/utils/mutationResult.d.ts +10 -0
  159. package/dist/planmagnet/utils/mutationResult.d.ts.map +1 -1
  160. package/dist/planmagnet/utils/mutationResult.js +2 -1
  161. package/dist/planmagnet/utils/mutationResult.js.map +1 -1
  162. package/dist/planmagnet/utils/rowNavigation.d.ts +2 -0
  163. package/dist/planmagnet/utils/rowNavigation.d.ts.map +1 -1
  164. package/dist/planmagnet/utils/rowNavigation.js +9 -8
  165. package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
  166. package/package.json +1 -1
@@ -1 +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
+ {"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, LEAVES_PAGE_CONTEXT } 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, {\n refetchQueries: LIST_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\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":";;;;;;;;;;;;AAqBA,IAAa,IAAqB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EACY,IAAuB;CAAC;CAAQ;CAAO;CAAU;CAAQ;CAAW;AAEjF,SAAgB,EAAU,GAAuB;CAC/C,IAAM,IAAQ,EAAM,aAAa,CAAC,QAAQ,MAAM,IAAI;AACpD,QAAO,EAAM,OAAO,EAAE,CAAC,aAAa,GAAG,EAAM,MAAM,EAAE;;AAGvD,IAAM,IAAmB,CAAG;;;;;;;;;GAUtB,IAAmB,CAAG;;;;GAOtB,IAAe;CAAC;CAAgB;CAAqB;CAAgB;CAAmB,EACxF,IAAiB;CAAC;CAAqB;CAAoB,GAAG;CAAa,EAE3E,IACJ;AAEF,SAAS,EAAkB,GAAoB;CAC7C,IAAM,CAAC,GAAQ,KAAS,EAAY,GAAkB;EACpD,gBAAgB;EAChB,qBAAqB;EACtB,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK;AASvD,QAAO;EAAE,MARI,OAAO,MAAmC;AACrD,KAAS,KAAK;AACd,OAAI;AACF,UAAM,EAAa,EAAO,EAAE,WAAW;KAAE,IAAI;KAAY;KAAO,EAAE,CAAC,CAAC;YAC7D,GAAG;AACV,MAAS,aAAa,QAAQ,EAAE,UAAU,iCAAiC;;;EAGhE,QAAQ,EAAM;EAAS;EAAO;;AAI/C,SAAgB,EAAmB,EACjC,eACA,UACA,UACA,YACA,YAOC;CACD,IAAM,EAAE,SAAM,WAAQ,aAAU,EAAkB,EAAW,EAEvD,IAAM,EAAQ,SAAS,EAAM,GAAG,IAAU,CAAC,GAAO,GAAG,EAAQ;AACnE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,cAAY;GACZ,eAAa,QAAQ;GACrB,WAAW;GACJ;GACP,UAAU;GACV,WAAW,MAAM;AACf,IAAI,EAAE,OAAO,UAAU,KAAY,EAAK,GAAG,IAAQ,EAAE,OAAO,OAAO,CAAC;;aAGrE,EAAI,KAAK,MACR,kBAAC,UAAD;IAAqB,OAAO;cACzB,EAAU,EAAO;IACX,EAFI,EAEJ,CACT;GACK,CAAA,EACR,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC;;;AASV,SAAgB,EAAmB,GAAsC;CACvE,IAAM,EAAE,gBAAa,cAAW,qBAAkB,GAAe,EAC3D,IAAY,EAAsB;EACtC,aAAa,KAAe;EAC5B,WAAW,KAAa,KAAA;EACxB,eAAe,KAAiB,KAAA;EACjC,CAAC,EACI,IAAU,EAAiB,EAAU,EACrC,IAAU,EAAU,QAAQ,KAAK,OAAO;EAAE,IAAI,EAAE;EAAI,MAAM,EAAE;EAAM,EAAE;AAK1E,QAHI,KAAa,KAAW,CAAC,EAAQ,MAAM,MAAM,EAAE,OAAO,EAAU,IAClE,EAAQ,QAAQ;EAAE,IAAI;EAAW,MAAM,EAAQ;EAAa,CAAC,EAExD;EACL;EACA,SAAS,EAAU;EACnB,SAAS,EAAU;EACnB,WAAW,EAAU;EACtB;;AAIH,SAAgB,EAAe,EAC7B,OACA,UACA,aACA,cAAW,MAMV;CACD,IAAM,EAAE,YAAS,YAAS,YAAS,iBAAc,EAAmB,KAAS,KAAK;AAClF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,UAAD;IACM;IACJ,cAAY,IAAK,KAAA,IAAY;IAC7B,eAAY;IACZ,WAAW;IACJ;IACG;IACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAP3C,CASE,kBAAC,UAAD;KAAQ,OAAM;eAAG;KAAmB,CAAA,EACnC,EAAQ,KAAK,MACZ,kBAAC,UAAD;KAAmB,OAAO,EAAE;eACzB,EAAE;KACI,EAFI,EAAE,GAEN,CACT,CACK;;GACR,KAAW,EAAQ,WAAW,KAC7B,kBAAC,QAAD;IAAM,WAAU;cAAgC;IAAuB,CAAA;GAExE,KACC,kBAAC,UAAD;IACE,MAAK;IACL,WAAU;IACV,SAAS;cACV;IAEQ,CAAA;GAEP;;;AAKV,SAAgB,EAAuB,EACrC,eACA,iBAIC;CACD,IAAM,EAAE,SAAM,WAAQ,aAAU,EAAkB,EAAW;AAC7D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GACE,OAAO,KAAc;GACrB,UAAU;GACV,WAAW,MAAS;AAClB,IAAI,OAAU,KAAc,OAAU,EAAK,EAAE,YAAY,KAAQ,MAAM,CAAC;;GAE1E,CAAA,EACD,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC;;;AAKV,SAAgB,EAAsB,EACpC,eACA,YAIC;CACD,IAAM,IAAW,GAAuB,EAClC,CAAC,GAAQ,KAAe,EAAY,GAAkB;EAC1D,gBAAgB;EAChB,SAAS;EACV,CAAC,EACI,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAW,YAAY;AAC3B,IAAS,KAAK;AACd,MAAI;AAEF,GADA,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,GAAY,EAAE,CAAC,CAAC,EAC7D,EAAS,oBAAoB;WACtB,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,iCAAiC;;;AAI/E,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,eAAY;IACZ,eAAe,EAAS,gCAAgC,mBAAmB,EAAW,GAAG;cAJ3F,CAME,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,OAE5B;OACT,kBAAC,GAAD;IACE,OAAM;IACN,SAAQ;IACR,MAAM,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA;IACzC,OAAO,WAAW,EAAM;IACxB,aAAY;IACZ,cAAa;IACb,aAAA;IACA,UAAU,EAAY;IACtB,iBAAiB,KAAK,GAAU;IAChC,CAAA,CACE;MACL,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA,CACnC"}
@@ -0,0 +1,39 @@
1
+ export interface ProjectCounts {
2
+ all: number;
3
+ active: number;
4
+ planning: number;
5
+ onHold: number;
6
+ completed: number;
7
+ }
8
+ export declare function useProjectCounts(): ProjectCounts | null;
9
+ export interface SprintCounts {
10
+ all: number;
11
+ /** ACTIVE + REVIEW: a sprint in review is still running (the same rule on every page). */
12
+ running: number;
13
+ planning: number;
14
+ completed: number;
15
+ }
16
+ export declare function useSprintCounts(): SprintCounts | null;
17
+ export interface FeatureCounts {
18
+ all: number;
19
+ /** Still wanted: SUBMITTED, UNDER_REVIEW, PLANNED or IN_PROGRESS. */
20
+ open: number;
21
+ /** Waiting for a triage decision: SUBMITTED and not snoozed (P1.3). */
22
+ triage: number;
23
+ /** SUBMITTED and snoozed into the future. */
24
+ snoozed: number;
25
+ byStatus: Record<'SUBMITTED' | 'UNDER_REVIEW' | 'PLANNED' | 'IN_PROGRESS' | 'COMPLETED' | 'DECLINED' | 'DUPLICATE', number>;
26
+ }
27
+ export declare function useFeatureCounts(): FeatureCounts | null;
28
+ /** "—" for a count that is loading or could not be read — never a number derived from a loaded page. */
29
+ export declare function countLabel(value: number | null | undefined): string;
30
+ /**
31
+ * Progress of a set of work: DONE over everything except CANCELLED (cancelled work no longer needs doing), by
32
+ * story points when there are any, otherwise by task count (BOFF-7989: an un-estimated sprint read 0% with every
33
+ * task done, and cancelled work counted as unfinished). null when nothing is left to measure.
34
+ */
35
+ export declare function workProgress(items: {
36
+ status: string;
37
+ storyPoints?: number | null;
38
+ }[]): number | null;
39
+ //# sourceMappingURL=useCounts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCounts.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/hooks/useCounts.ts"],"names":[],"mappings":"AAmDA,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAcvD;AAsCD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAgBrD;AAyED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CACZ,WAAW,GACX,cAAc,GACd,SAAS,GACT,aAAa,GACb,WAAW,GACX,UAAU,GACV,WAAW,EACb,MAAM,CACP,CAAC;CACH;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAiCvD;AAED,wGAAwG;AACxG,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,GACvD,MAAM,GAAG,IAAI,CAWf"}
@@ -0,0 +1,209 @@
1
+ import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
2
+ import { gql as t } from "@apollo/client";
3
+ import { useQuery as n } from "@apollo/client/react";
4
+ //#region src/planmagnet/hooks/useCounts.ts
5
+ var r = t`
6
+ query CountsProjects($workspaceId: ID!) {
7
+ all: planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 1 }) {
8
+ totalCount
9
+ }
10
+ active: planmagnetProjects(
11
+ workspaceId: $workspaceId
12
+ filter: { status: ACTIVE }
13
+ pagination: { first: 1 }
14
+ ) {
15
+ totalCount
16
+ }
17
+ planning: planmagnetProjects(
18
+ workspaceId: $workspaceId
19
+ filter: { status: PLANNING }
20
+ pagination: { first: 1 }
21
+ ) {
22
+ totalCount
23
+ }
24
+ onHold: planmagnetProjects(
25
+ workspaceId: $workspaceId
26
+ filter: { status: ON_HOLD }
27
+ pagination: { first: 1 }
28
+ ) {
29
+ totalCount
30
+ }
31
+ completed: planmagnetProjects(
32
+ workspaceId: $workspaceId
33
+ filter: { status: COMPLETED }
34
+ pagination: { first: 1 }
35
+ ) {
36
+ totalCount
37
+ }
38
+ }
39
+ `;
40
+ function i() {
41
+ let { workspaceId: t } = e(), { data: i, error: a } = n(r, {
42
+ variables: { workspaceId: t ?? "" },
43
+ skip: !t
44
+ });
45
+ return !i || a ? null : {
46
+ all: i.all.totalCount,
47
+ active: i.active.totalCount,
48
+ planning: i.planning.totalCount,
49
+ onHold: i.onHold.totalCount,
50
+ completed: i.completed.totalCount
51
+ };
52
+ }
53
+ var a = t`
54
+ query CountsSprints($workspaceId: ID!) {
55
+ all: planmagnetSprints(workspaceId: $workspaceId, pagination: { first: 1 }) {
56
+ totalCount
57
+ }
58
+ active: planmagnetSprints(
59
+ workspaceId: $workspaceId
60
+ filter: { status: ACTIVE }
61
+ pagination: { first: 1 }
62
+ ) {
63
+ totalCount
64
+ }
65
+ review: planmagnetSprints(
66
+ workspaceId: $workspaceId
67
+ filter: { status: REVIEW }
68
+ pagination: { first: 1 }
69
+ ) {
70
+ totalCount
71
+ }
72
+ planning: planmagnetSprints(
73
+ workspaceId: $workspaceId
74
+ filter: { status: PLANNING }
75
+ pagination: { first: 1 }
76
+ ) {
77
+ totalCount
78
+ }
79
+ completed: planmagnetSprints(
80
+ workspaceId: $workspaceId
81
+ filter: { status: COMPLETED }
82
+ pagination: { first: 1 }
83
+ ) {
84
+ totalCount
85
+ }
86
+ }
87
+ `;
88
+ function o() {
89
+ let { workspaceId: t } = e(), { data: r, error: i } = n(a, {
90
+ variables: { workspaceId: t ?? "" },
91
+ skip: !t
92
+ });
93
+ return !r || i ? null : {
94
+ all: r.all.totalCount,
95
+ running: r.active.totalCount + r.review.totalCount,
96
+ planning: r.planning.totalCount,
97
+ completed: r.completed.totalCount
98
+ };
99
+ }
100
+ var s = t`
101
+ query CountsFeatures($workspaceId: ID!) {
102
+ all: planmagnetFeatureRequests(workspaceId: $workspaceId, pagination: { first: 1 }) {
103
+ totalCount
104
+ }
105
+ submitted: planmagnetFeatureRequests(
106
+ workspaceId: $workspaceId
107
+ filter: { status: SUBMITTED }
108
+ pagination: { first: 1 }
109
+ ) {
110
+ totalCount
111
+ }
112
+ underReview: planmagnetFeatureRequests(
113
+ workspaceId: $workspaceId
114
+ filter: { status: UNDER_REVIEW }
115
+ pagination: { first: 1 }
116
+ ) {
117
+ totalCount
118
+ }
119
+ planned: planmagnetFeatureRequests(
120
+ workspaceId: $workspaceId
121
+ filter: { status: PLANNED }
122
+ pagination: { first: 1 }
123
+ ) {
124
+ totalCount
125
+ }
126
+ inProgress: planmagnetFeatureRequests(
127
+ workspaceId: $workspaceId
128
+ filter: { status: IN_PROGRESS }
129
+ pagination: { first: 1 }
130
+ ) {
131
+ totalCount
132
+ }
133
+ completed: planmagnetFeatureRequests(
134
+ workspaceId: $workspaceId
135
+ filter: { status: COMPLETED }
136
+ pagination: { first: 1 }
137
+ ) {
138
+ totalCount
139
+ }
140
+ declined: planmagnetFeatureRequests(
141
+ workspaceId: $workspaceId
142
+ filter: { status: DECLINED }
143
+ pagination: { first: 1 }
144
+ ) {
145
+ totalCount
146
+ }
147
+ duplicate: planmagnetFeatureRequests(
148
+ workspaceId: $workspaceId
149
+ filter: { status: DUPLICATE }
150
+ pagination: { first: 1 }
151
+ ) {
152
+ totalCount
153
+ }
154
+ triage: planmagnetFeatureRequests(
155
+ workspaceId: $workspaceId
156
+ filter: { triage: true }
157
+ pagination: { first: 1 }
158
+ ) {
159
+ totalCount
160
+ }
161
+ snoozed: planmagnetFeatureRequests(
162
+ workspaceId: $workspaceId
163
+ filter: { snoozed: true }
164
+ pagination: { first: 1 }
165
+ ) {
166
+ totalCount
167
+ }
168
+ }
169
+ `;
170
+ function c() {
171
+ let { workspaceId: t } = e(), { data: r, error: i } = n(s, {
172
+ variables: { workspaceId: t ?? "" },
173
+ skip: !t
174
+ });
175
+ if (!r || i) return null;
176
+ let a = {
177
+ SUBMITTED: r.submitted.totalCount,
178
+ UNDER_REVIEW: r.underReview.totalCount,
179
+ PLANNED: r.planned.totalCount,
180
+ IN_PROGRESS: r.inProgress.totalCount,
181
+ COMPLETED: r.completed.totalCount,
182
+ DECLINED: r.declined.totalCount,
183
+ DUPLICATE: r.duplicate.totalCount
184
+ };
185
+ return {
186
+ all: r.all.totalCount,
187
+ open: a.SUBMITTED + a.UNDER_REVIEW + a.PLANNED + a.IN_PROGRESS,
188
+ triage: r.triage.totalCount,
189
+ snoozed: r.snoozed.totalCount,
190
+ byStatus: a
191
+ };
192
+ }
193
+ function l(e) {
194
+ return e == null ? "—" : String(e);
195
+ }
196
+ function u(e) {
197
+ let t = e.filter((e) => e.status !== "CANCELLED");
198
+ if (t.length === 0) return null;
199
+ let n = t.reduce((e, t) => e + (t.storyPoints ?? 0), 0);
200
+ if (n > 0) {
201
+ let e = t.filter((e) => e.status === "DONE").reduce((e, t) => e + (t.storyPoints ?? 0), 0);
202
+ return Math.round(e / n * 100);
203
+ }
204
+ return Math.round(t.filter((e) => e.status === "DONE").length / t.length * 100);
205
+ }
206
+ //#endregion
207
+ export { l as countLabel, c as useFeatureCounts, i as useProjectCounts, o as useSprintCounts, u as workProgress };
208
+
209
+ //# sourceMappingURL=useCounts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCounts.js","names":[],"sources":["../../../src/planmagnet/hooks/useCounts.ts"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\n/**\n * One number per concept, from the server (BOFF-7969/7970/7965). Pages used to count the rows of whatever page they\n * had loaded — the 50 or 100 newest — so Dashboard, All Projects, Analytics and Sprint Management each showed a\n * different \"active\" count. These read `totalCount` of filtered queries (one request, aliased), so every page shows\n * the same number and nothing depends on how many rows a list happens to load.\n *\n * `null` while loading or when the counts could not be read: callers show \"—\", never a page-derived number.\n */\n\nconst PROJECT_COUNTS = gql`\n query CountsProjects($workspaceId: ID!) {\n all: planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 1 }) {\n totalCount\n }\n active: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: ACTIVE }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n planning: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: PLANNING }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n onHold: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: ON_HOLD }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n completed: planmagnetProjects(\n workspaceId: $workspaceId\n filter: { status: COMPLETED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n }\n`;\n\ntype Totals<K extends string> = Record<K, { totalCount: number }>;\n\nexport interface ProjectCounts {\n all: number;\n active: number;\n planning: number;\n onHold: number;\n completed: number;\n}\n\nexport function useProjectCounts(): ProjectCounts | null {\n const { workspaceId } = usePlanMagnet();\n const { data, error } = useQuery<Totals<keyof ProjectCounts>>(PROJECT_COUNTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n if (!data || error) return null;\n return {\n all: data.all.totalCount,\n active: data.active.totalCount,\n planning: data.planning.totalCount,\n onHold: data.onHold.totalCount,\n completed: data.completed.totalCount,\n };\n}\n\nconst SPRINT_COUNTS = gql`\n query CountsSprints($workspaceId: ID!) {\n all: planmagnetSprints(workspaceId: $workspaceId, pagination: { first: 1 }) {\n totalCount\n }\n active: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: ACTIVE }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n review: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: REVIEW }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n planning: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: PLANNING }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n completed: planmagnetSprints(\n workspaceId: $workspaceId\n filter: { status: COMPLETED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n }\n`;\n\nexport interface SprintCounts {\n all: number;\n /** ACTIVE + REVIEW: a sprint in review is still running (the same rule on every page). */\n running: number;\n planning: number;\n completed: number;\n}\n\nexport function useSprintCounts(): SprintCounts | null {\n const { workspaceId } = usePlanMagnet();\n const { data, error } = useQuery<Totals<'all' | 'active' | 'review' | 'planning' | 'completed'>>(\n SPRINT_COUNTS,\n {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n }\n );\n if (!data || error) return null;\n return {\n all: data.all.totalCount,\n running: data.active.totalCount + data.review.totalCount,\n planning: data.planning.totalCount,\n completed: data.completed.totalCount,\n };\n}\n\nconst FEATURE_COUNTS = gql`\n query CountsFeatures($workspaceId: ID!) {\n all: planmagnetFeatureRequests(workspaceId: $workspaceId, pagination: { first: 1 }) {\n totalCount\n }\n submitted: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { status: SUBMITTED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n underReview: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { status: UNDER_REVIEW }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n planned: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { status: PLANNED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n inProgress: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { status: IN_PROGRESS }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n completed: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { status: COMPLETED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n declined: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { status: DECLINED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n duplicate: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { status: DUPLICATE }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n triage: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { triage: true }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n snoozed: planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { snoozed: true }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n }\n`;\n\nexport interface FeatureCounts {\n all: number;\n /** Still wanted: SUBMITTED, UNDER_REVIEW, PLANNED or IN_PROGRESS. */\n open: number;\n /** Waiting for a triage decision: SUBMITTED and not snoozed (P1.3). */\n triage: number;\n /** SUBMITTED and snoozed into the future. */\n snoozed: number;\n byStatus: Record<\n | 'SUBMITTED'\n | 'UNDER_REVIEW'\n | 'PLANNED'\n | 'IN_PROGRESS'\n | 'COMPLETED'\n | 'DECLINED'\n | 'DUPLICATE',\n number\n >;\n}\n\nexport function useFeatureCounts(): FeatureCounts | null {\n const { workspaceId } = usePlanMagnet();\n const { data, error } = useQuery<\n Totals<\n | 'all'\n | 'submitted'\n | 'underReview'\n | 'planned'\n | 'inProgress'\n | 'completed'\n | 'declined'\n | 'duplicate'\n | 'triage'\n | 'snoozed'\n >\n >(FEATURE_COUNTS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n if (!data || error) return null;\n const byStatus = {\n SUBMITTED: data.submitted.totalCount,\n UNDER_REVIEW: data.underReview.totalCount,\n PLANNED: data.planned.totalCount,\n IN_PROGRESS: data.inProgress.totalCount,\n COMPLETED: data.completed.totalCount,\n DECLINED: data.declined.totalCount,\n DUPLICATE: data.duplicate.totalCount,\n };\n return {\n all: data.all.totalCount,\n open: byStatus.SUBMITTED + byStatus.UNDER_REVIEW + byStatus.PLANNED + byStatus.IN_PROGRESS,\n triage: data.triage.totalCount,\n snoozed: data.snoozed.totalCount,\n byStatus,\n };\n}\n\n/** \"—\" for a count that is loading or could not be read — never a number derived from a loaded page. */\nexport function countLabel(value: number | null | undefined): string {\n return value == null ? '—' : String(value);\n}\n\n/**\n * Progress of a set of work: DONE over everything except CANCELLED (cancelled work no longer needs doing), by\n * story points when there are any, otherwise by task count (BOFF-7989: an un-estimated sprint read 0% with every\n * task done, and cancelled work counted as unfinished). null when nothing is left to measure.\n */\nexport function workProgress(\n items: { status: string; storyPoints?: number | null }[]\n): number | null {\n const live = items.filter((i) => i.status !== 'CANCELLED');\n if (live.length === 0) return null;\n const points = live.reduce((sum, i) => sum + (i.storyPoints ?? 0), 0);\n if (points > 0) {\n const donePoints = live\n .filter((i) => i.status === 'DONE')\n .reduce((sum, i) => sum + (i.storyPoints ?? 0), 0);\n return Math.round((donePoints / points) * 100);\n }\n return Math.round((live.filter((i) => i.status === 'DONE').length / live.length) * 100);\n}\n"],"mappings":";;;;AAaA,IAAM,IAAiB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8C1B,SAAgB,IAAyC;CACvD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,aAAU,EAAsC,GAAgB;EAC5E,WAAW,EAAE,aAAa,KAAe,IAAI;EAC7C,MAAM,CAAC;EACR,CAAC;AAEF,QADI,CAAC,KAAQ,IAAc,OACpB;EACL,KAAK,EAAK,IAAI;EACd,QAAQ,EAAK,OAAO;EACpB,UAAU,EAAK,SAAS;EACxB,QAAQ,EAAK,OAAO;EACpB,WAAW,EAAK,UAAU;EAC3B;;AAGH,IAAM,IAAgB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CzB,SAAgB,IAAuC;CACrD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,aAAU,EACtB,GACA;EACE,WAAW,EAAE,aAAa,KAAe,IAAI;EAC7C,MAAM,CAAC;EACR,CACF;AAED,QADI,CAAC,KAAQ,IAAc,OACpB;EACL,KAAK,EAAK,IAAI;EACd,SAAS,EAAK,OAAO,aAAa,EAAK,OAAO;EAC9C,UAAU,EAAK,SAAS;EACxB,WAAW,EAAK,UAAU;EAC3B;;AAGH,IAAM,IAAiB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2F1B,SAAgB,IAAyC;CACvD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,aAAU,EAatB,GAAgB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC;AACxF,KAAI,CAAC,KAAQ,EAAO,QAAO;CAC3B,IAAM,IAAW;EACf,WAAW,EAAK,UAAU;EAC1B,cAAc,EAAK,YAAY;EAC/B,SAAS,EAAK,QAAQ;EACtB,aAAa,EAAK,WAAW;EAC7B,WAAW,EAAK,UAAU;EAC1B,UAAU,EAAK,SAAS;EACxB,WAAW,EAAK,UAAU;EAC3B;AACD,QAAO;EACL,KAAK,EAAK,IAAI;EACd,MAAM,EAAS,YAAY,EAAS,eAAe,EAAS,UAAU,EAAS;EAC/E,QAAQ,EAAK,OAAO;EACpB,SAAS,EAAK,QAAQ;EACtB;EACD;;AAIH,SAAgB,EAAW,GAA0C;AACnE,QAAO,KAAS,OAAO,MAAM,OAAO,EAAM;;AAQ5C,SAAgB,EACd,GACe;CACf,IAAM,IAAO,EAAM,QAAQ,MAAM,EAAE,WAAW,YAAY;AAC1D,KAAI,EAAK,WAAW,EAAG,QAAO;CAC9B,IAAM,IAAS,EAAK,QAAQ,GAAK,MAAM,KAAO,EAAE,eAAe,IAAI,EAAE;AACrE,KAAI,IAAS,GAAG;EACd,IAAM,IAAa,EAChB,QAAQ,MAAM,EAAE,WAAW,OAAO,CAClC,QAAQ,GAAK,MAAM,KAAO,EAAE,eAAe,IAAI,EAAE;AACpD,SAAO,KAAK,MAAO,IAAa,IAAU,IAAI;;AAEhD,QAAO,KAAK,MAAO,EAAK,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC,SAAS,EAAK,SAAU,IAAI"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The columns of a Linear export, read in the browser so the import page can offer a column picker before anything
3
+ * is sent. Only the header matters here — the service parses the rows.
4
+ */
5
+ export type ExportFormat = 'CSV' | 'JSON';
6
+ /** Our fields, in the order the picker shows them. `required` fields must resolve to a column for a row to import. */
7
+ export declare const IMPORT_FIELDS: readonly [{
8
+ readonly key: "externalId";
9
+ readonly label: "Issue ID";
10
+ readonly required: true;
11
+ }, {
12
+ readonly key: "title";
13
+ readonly label: "Title";
14
+ readonly required: true;
15
+ }, {
16
+ readonly key: "description";
17
+ readonly label: "Description";
18
+ readonly required: false;
19
+ }, {
20
+ readonly key: "status";
21
+ readonly label: "State";
22
+ readonly required: false;
23
+ }, {
24
+ readonly key: "priority";
25
+ readonly label: "Priority";
26
+ readonly required: false;
27
+ }, {
28
+ readonly key: "labels";
29
+ readonly label: "Labels";
30
+ readonly required: false;
31
+ }, {
32
+ readonly key: "assigneeEmail";
33
+ readonly label: "Assignee";
34
+ readonly required: false;
35
+ }, {
36
+ readonly key: "dueDate";
37
+ readonly label: "Due date";
38
+ readonly required: false;
39
+ }, {
40
+ readonly key: "externalUrl";
41
+ readonly label: "Link to the issue";
42
+ readonly required: false;
43
+ }];
44
+ export type ImportField = (typeof IMPORT_FIELDS)[number]['key'];
45
+ export declare function formatOf(fileName: string): ExportFormat | null;
46
+ /** Columns of the export, or an error message a person can act on. */
47
+ export declare function exportColumns(text: string, format: ExportFormat): {
48
+ columns: string[];
49
+ } | {
50
+ error: string;
51
+ };
52
+ //# sourceMappingURL=exportColumns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exportColumns.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/import/exportColumns.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1C,sHAAsH;AACtH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUhB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;AAEhE,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAK9D;AA2CD,sEAAsE;AACtE,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,GACnB;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAY3C"}
@@ -0,0 +1,81 @@
1
+ //#region src/planmagnet/import/exportColumns.ts
2
+ var e = [
3
+ {
4
+ key: "externalId",
5
+ label: "Issue ID",
6
+ required: !0
7
+ },
8
+ {
9
+ key: "title",
10
+ label: "Title",
11
+ required: !0
12
+ },
13
+ {
14
+ key: "description",
15
+ label: "Description",
16
+ required: !1
17
+ },
18
+ {
19
+ key: "status",
20
+ label: "State",
21
+ required: !1
22
+ },
23
+ {
24
+ key: "priority",
25
+ label: "Priority",
26
+ required: !1
27
+ },
28
+ {
29
+ key: "labels",
30
+ label: "Labels",
31
+ required: !1
32
+ },
33
+ {
34
+ key: "assigneeEmail",
35
+ label: "Assignee",
36
+ required: !1
37
+ },
38
+ {
39
+ key: "dueDate",
40
+ label: "Due date",
41
+ required: !1
42
+ },
43
+ {
44
+ key: "externalUrl",
45
+ label: "Link to the issue",
46
+ required: !1
47
+ }
48
+ ];
49
+ function t(e) {
50
+ let t = e.toLowerCase();
51
+ return t.endsWith(".csv") ? "CSV" : t.endsWith(".json") ? "JSON" : null;
52
+ }
53
+ function n(e) {
54
+ let t = [], n = "", r = !1;
55
+ for (let i = 0; i < e.length; i++) {
56
+ let a = e[i];
57
+ if (r) a === "\"" && e[i + 1] === "\"" ? (n += "\"", i++) : a === "\"" ? r = !1 : n += a;
58
+ else if (a === "\"") r = !0;
59
+ else if (a === ",") t.push(n.trim()), n = "";
60
+ else if (a === "\n" || a === "\r") break;
61
+ else n += a;
62
+ }
63
+ return t.push(n.trim()), t.filter((e) => e !== "");
64
+ }
65
+ function r(e) {
66
+ let t = JSON.parse(e), n = Array.isArray(t) ? t : t?.issues ?? t?.data?.issues?.nodes ?? [], r = /* @__PURE__ */ new Set();
67
+ for (let e of n.slice(0, 50)) if (e && typeof e == "object" && !Array.isArray(e)) for (let t of Object.keys(e)) r.add(t);
68
+ return [...r];
69
+ }
70
+ function i(e, t) {
71
+ try {
72
+ let i = t === "CSV" ? n(e) : r(e);
73
+ return i.length === 0 ? { error: t === "CSV" ? "The file has no header row." : "The file holds no issues." } : { columns: i };
74
+ } catch {
75
+ return { error: "The file is not valid JSON." };
76
+ }
77
+ }
78
+ //#endregion
79
+ export { e as IMPORT_FIELDS, i as exportColumns, t as formatOf };
80
+
81
+ //# sourceMappingURL=exportColumns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exportColumns.js","names":[],"sources":["../../../src/planmagnet/import/exportColumns.ts"],"sourcesContent":["/**\n * The columns of a Linear export, read in the browser so the import page can offer a column picker before anything\n * is sent. Only the header matters here — the service parses the rows.\n */\n\nexport type ExportFormat = 'CSV' | 'JSON';\n\n/** Our fields, in the order the picker shows them. `required` fields must resolve to a column for a row to import. */\nexport const IMPORT_FIELDS = [\n { key: 'externalId', label: 'Issue ID', required: true },\n { key: 'title', label: 'Title', required: true },\n { key: 'description', label: 'Description', required: false },\n { key: 'status', label: 'State', required: false },\n { key: 'priority', label: 'Priority', required: false },\n { key: 'labels', label: 'Labels', required: false },\n { key: 'assigneeEmail', label: 'Assignee', required: false },\n { key: 'dueDate', label: 'Due date', required: false },\n { key: 'externalUrl', label: 'Link to the issue', required: false },\n] as const;\n\nexport type ImportField = (typeof IMPORT_FIELDS)[number]['key'];\n\nexport function formatOf(fileName: string): ExportFormat | null {\n const lower = fileName.toLowerCase();\n if (lower.endsWith('.csv')) return 'CSV';\n if (lower.endsWith('.json')) return 'JSON';\n return null;\n}\n\n/** The first CSV record (RFC 4180: quoted fields may hold commas, escaped quotes and line breaks). */\nfunction csvHeader(text: string): string[] {\n const out: string[] = [];\n let field = '';\n let quoted = false;\n for (let i = 0; i < text.length; i++) {\n const c = text[i];\n if (quoted) {\n if (c === '\"' && text[i + 1] === '\"') {\n field += '\"';\n i++;\n } else if (c === '\"') quoted = false;\n else field += c;\n } else if (c === '\"') quoted = true;\n else if (c === ',') {\n out.push(field.trim());\n field = '';\n } else if (c === '\\n' || c === '\\r') break;\n else field += c;\n }\n out.push(field.trim());\n return out.filter((h) => h !== '');\n}\n\n/** Keys of the issues in a JSON export (a list, `{ issues }`, or the Linear API's `{ data: { issues: { nodes } } }`). */\nfunction jsonKeys(text: string): string[] {\n const parsed: unknown = JSON.parse(text);\n const list: unknown[] = Array.isArray(parsed)\n ? parsed\n : ((parsed as { issues?: unknown[] })?.issues ??\n (parsed as { data?: { issues?: { nodes?: unknown[] } } })?.data?.issues?.nodes ??\n []);\n const keys = new Set<string>();\n for (const item of list.slice(0, 50)) {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n for (const k of Object.keys(item)) keys.add(k);\n }\n }\n return [...keys];\n}\n\n/** Columns of the export, or an error message a person can act on. */\nexport function exportColumns(\n text: string,\n format: ExportFormat\n): { columns: string[] } | { error: string } {\n try {\n const columns = format === 'CSV' ? csvHeader(text) : jsonKeys(text);\n if (columns.length === 0) {\n return {\n error: format === 'CSV' ? 'The file has no header row.' : 'The file holds no issues.',\n };\n }\n return { columns };\n } catch {\n return { error: 'The file is not valid JSON.' };\n }\n}\n"],"mappings":";AAQA,IAAa,IAAgB;CAC3B;EAAE,KAAK;EAAc,OAAO;EAAY,UAAU;EAAM;CACxD;EAAE,KAAK;EAAS,OAAO;EAAS,UAAU;EAAM;CAChD;EAAE,KAAK;EAAe,OAAO;EAAe,UAAU;EAAO;CAC7D;EAAE,KAAK;EAAU,OAAO;EAAS,UAAU;EAAO;CAClD;EAAE,KAAK;EAAY,OAAO;EAAY,UAAU;EAAO;CACvD;EAAE,KAAK;EAAU,OAAO;EAAU,UAAU;EAAO;CACnD;EAAE,KAAK;EAAiB,OAAO;EAAY,UAAU;EAAO;CAC5D;EAAE,KAAK;EAAW,OAAO;EAAY,UAAU;EAAO;CACtD;EAAE,KAAK;EAAe,OAAO;EAAqB,UAAU;EAAO;CACpE;AAID,SAAgB,EAAS,GAAuC;CAC9D,IAAM,IAAQ,EAAS,aAAa;AAGpC,QAFI,EAAM,SAAS,OAAO,GAAS,QAC/B,EAAM,SAAS,QAAQ,GAAS,SAC7B;;AAIT,SAAS,EAAU,GAAwB;CACzC,IAAM,IAAgB,EAAE,EACpB,IAAQ,IACR,IAAS;AACb,MAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAAK;EACpC,IAAM,IAAI,EAAK;AACf,MAAI,EACF,CAAI,MAAM,QAAO,EAAK,IAAI,OAAO,QAC/B,KAAS,MACT,OACS,MAAM,OAAK,IAAS,KAC1B,KAAS;WACL,MAAM,KAAK,KAAS;WACtB,MAAM,IAEb,CADA,EAAI,KAAK,EAAM,MAAM,CAAC,EACtB,IAAQ;WACC,MAAM,QAAQ,MAAM,KAAM;MAChC,MAAS;;AAGhB,QADA,EAAI,KAAK,EAAM,MAAM,CAAC,EACf,EAAI,QAAQ,MAAM,MAAM,GAAG;;AAIpC,SAAS,EAAS,GAAwB;CACxC,IAAM,IAAkB,KAAK,MAAM,EAAK,EAClC,IAAkB,MAAM,QAAQ,EAAO,GACzC,IACE,GAAmC,UACpC,GAA0D,MAAM,QAAQ,SACzE,EAAE,EACA,oBAAO,IAAI,KAAa;AAC9B,MAAK,IAAM,KAAQ,EAAK,MAAM,GAAG,GAAG,CAClC,KAAI,KAAQ,OAAO,KAAS,YAAY,CAAC,MAAM,QAAQ,EAAK,CAC1D,MAAK,IAAM,KAAK,OAAO,KAAK,EAAK,CAAE,GAAK,IAAI,EAAE;AAGlD,QAAO,CAAC,GAAG,EAAK;;AAIlB,SAAgB,EACd,GACA,GAC2C;AAC3C,KAAI;EACF,IAAM,IAAU,MAAW,QAAQ,EAAU,EAAK,GAAG,EAAS,EAAK;AAMnE,SALI,EAAQ,WAAW,IACd,EACL,OAAO,MAAW,QAAQ,gCAAgC,6BAC3D,GAEI,EAAE,YAAS;SACZ;AACN,SAAO,EAAE,OAAO,+BAA+B"}
@@ -1 +1 @@
1
- {"version":3,"file":"AnalyticsAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"names":[],"mappings":"AAmSA,wBAAgB,0BAA0B,4CA6dzC"}
1
+ {"version":3,"file":"AnalyticsAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"names":[],"mappings":"AA6QA,wBAAgB,0BAA0B,4CAqfzC"}