@burdenoff/microfe-planmagnet 2026.927.2 → 2026.927.4
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/FeatureControls.d.ts +26 -0
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/FeatureControls.js +318 -0
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -0
- package/dist/planmagnet/controls/MilestoneControls.d.ts +17 -0
- package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/MilestoneControls.js +200 -0
- package/dist/planmagnet/controls/MilestoneControls.js.map +1 -0
- package/dist/planmagnet/controls/ObjectiveControls.d.ts +31 -0
- package/dist/planmagnet/controls/ObjectiveControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/ObjectiveControls.js +296 -0
- package/dist/planmagnet/controls/ObjectiveControls.js.map +1 -0
- package/dist/planmagnet/controls/Pickers.d.ts +26 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -0
- package/dist/planmagnet/controls/Pickers.js +171 -0
- package/dist/planmagnet/controls/Pickers.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/ReleaseControls.d.ts +14 -0
- package/dist/planmagnet/controls/ReleaseControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/ReleaseControls.js +108 -0
- package/dist/planmagnet/controls/ReleaseControls.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/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +188 -154
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DocsDocsPagePage.js +4 -4
- package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.js +63 -56
- package/dist/planmagnet/pages/GoalsCreateObjectivePagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js +229 -214
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js +142 -114
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditReleasePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditReleasePagePage.js +134 -105
- package/dist/planmagnet/pages/ProductsEditReleasePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +102 -108
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +119 -94
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.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/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +107 -106
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js +2 -0
- package/dist/planmagnet/pages/ProductsReleasesPagePage.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 +116 -93
- 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 +55 -63
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +4 -4
- 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/SprintsStartSprintPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +6 -5
- 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 -116
- 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 +139 -107
- 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 +205 -158
- 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 +1 -0
- 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 +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/people/People.js +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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlanMagnetAdminRoot.js","names":[],"sources":["../../../src/planmagnet/admin/PlanMagnetAdminRoot.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Checkbox,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { AlertCircle, Archive, FolderKanban, LayoutTemplate, Loader2, Zap } from 'lucide-react';\n\nimport { PlanMagnetProvider, usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { cn } from '../utils/cn';\nimport type { PlanMagnetRootProps } from '../types';\nimport { PersonName } from '../people/People';\n\n// ------------------------------------------------------------------------------\n// Domain types (mirror the planmagnet subgraph until generated types are fixed)\n// ------------------------------------------------------------------------------\n\ntype ProjectStatus = 'ACTIVE' | 'ARCHIVED' | 'CANCELLED' | 'COMPLETED' | 'ON_HOLD' | 'PLANNING';\n\ninterface AdminProject {\n id: string;\n name: string;\n key: string;\n status: ProjectStatus;\n methodology: string;\n ownerId: string;\n createdAt: string;\n}\n\ninterface AdminBoard {\n id: string;\n name: string;\n type: string;\n projectId?: string | null;\n}\n\ninterface AdminAutomationRule {\n id: string;\n name: string;\n status: string;\n triggerType: string;\n executionCount: number;\n lastExecutedAt?: string | null;\n createdAt: string;\n}\n\n// ------------------------------------------------------------------------------\n// GraphQL documents (inline so the admin surface does not depend on generated\n// hooks while the rest of the MFE's operation files are being reconciled with\n// the schema).\n// ------------------------------------------------------------------------------\n\nconst ADMIN_PROJECTS_QUERY = gql`\n query AdminPlanmagnetProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n key\n status\n methodology\n ownerId\n createdAt\n }\n totalCount\n }\n }\n`;\n\nconst ADMIN_BOARDS_QUERY = gql`\n query AdminPlanmagnetBoards($workspaceId: ID!) {\n planmagnetBoards(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n type\n projectId\n }\n totalCount\n }\n }\n`;\n\nconst ADMIN_AUTOMATION_RULES_QUERY = gql`\n query AdminPlanmagnetAutomationRules($workspaceId: ID!) {\n planmagnetAutomationRules(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n status\n triggerType\n executionCount\n lastExecutedAt\n createdAt\n }\n totalCount\n }\n }\n`;\n\nconst ARCHIVE_PROJECT_MUTATION = gql`\n mutation AdminArchiveProject($id: ID!) {\n planmagnetUpdateProject(id: $id, input: { status: ARCHIVED }) {\n id\n status\n }\n }\n`;\n\n// ------------------------------------------------------------------------------\n// Presentation helpers\n// ------------------------------------------------------------------------------\n\nfunction projectStatusVariantr(status: ProjectStatus) {\n switch (status) {\n case 'ACTIVE':\n return 'success';\n case 'ARCHIVED':\n return 'secondary';\n case 'CANCELLED':\n return 'destructive';\n case 'COMPLETED':\n return 'info';\n case 'ON_HOLD':\n return 'warning';\n case 'PLANNING':\n return 'outline';\n default:\n return 'default';\n }\n}\n\nfunction automationStatusVariantr(status: string) {\n switch (status) {\n case 'ACTIVE':\n return 'success';\n case 'PAUSED':\n return 'warning';\n case 'ERROR':\n return 'destructive';\n case 'DISABLED':\n return 'secondary';\n default:\n return 'default';\n }\n}\n\nfunction formatDate(value: string | null | undefined) {\n if (!value) return '—';\n try {\n return new Date(value).toLocaleDateString();\n } catch {\n return value;\n }\n}\n\nfunction localizeMethodology(value: string) {\n return value.toLowerCase().replace(/_/g, ' ');\n}\n\nfunction localizeTrigger(value: string) {\n return value.toLowerCase().replace(/_/g, ' ');\n}\n\n// ------------------------------------------------------------------------------\n// Sub-components\n// ------------------------------------------------------------------------------\n\nfunction QuotaCard({\n icon: Icon,\n label,\n used,\n limit,\n className,\n}: {\n icon: React.ComponentType<{ className?: string }>;\n label: string;\n used: number;\n limit: number;\n className?: string;\n}) {\n const percentage = Math.min(100, Math.round((used / Math.max(1, limit)) * 100));\n\n return (\n <Card className={className}>\n <CardContent className=\"pt-6\">\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n <Icon className=\"h-4 w-4\" />\n <span className=\"text-sm\">{label}</span>\n </div>\n <div className=\"mt-2 flex items-baseline gap-2\">\n <span className=\"text-2xl font-semibold\">{used}</span>\n <span className=\"text-sm text-muted-foreground\">/ {limit}</span>\n </div>\n <div className=\"mt-3 h-2 w-full overflow-hidden rounded-full bg-muted\">\n <div\n className={cn(\n 'h-full rounded-full transition-all',\n percentage >= 90 ? 'bg-destructive' : percentage >= 70 ? 'bg-warning' : 'bg-success'\n )}\n style={{ width: `${percentage}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-muted-foreground\">{percentage}% used</p>\n </CardContent>\n </Card>\n );\n}\n\nfunction SectionLoader() {\n return (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-8 w-1/3\" />\n <Skeleton className=\"h-32 w-full\" />\n </div>\n );\n}\n\ninterface SimpleTableColumn<T> {\n key: string;\n header: React.ReactNode;\n cell: (row: T) => React.ReactNode;\n className?: string;\n}\n\nfunction SimpleTable<T extends { id: string }>({\n columns,\n data,\n emptyState,\n}: {\n columns: SimpleTableColumn<T>[];\n data: T[];\n emptyState: React.ReactNode;\n}) {\n if (data.length === 0) {\n return <div className=\"py-8 text-center text-sm text-muted-foreground\">{emptyState}</div>;\n }\n\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-muted/50\">\n <tr>\n {columns.map((column) => (\n <th\n key={column.key}\n className={cn(\n 'px-4 py-3 text-left font-medium text-muted-foreground',\n column.className\n )}\n >\n {column.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y\">\n {data.map((row) => (\n <tr key={row.id} className=\"hover:bg-muted/30\">\n {columns.map((column) => (\n <td key={`${row.id}-${column.key}`} className={cn('px-4 py-3', column.className)}>\n {column.cell(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\n// ------------------------------------------------------------------------------\n// Main dashboard\n// ------------------------------------------------------------------------------\n\nfunction PlanMagnetAdminDashboard() {\n const { workspaceId } = usePlanMagnet();\n const tr = useTr();\n\n const [selectedProjectIds, setSelectedProjectIds] = useState<Set<string>>(new Set());\n const [archiving, setArchiving] = useState(false);\n const [archiveError, setArchiveError] = useState<string | null>(null);\n\n const projectsQuery = useQuery<{\n planmagnetProjects: { items: AdminProject[]; totalCount: number };\n }>(ADMIN_PROJECTS_QUERY, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n\n const boardsQuery = useQuery<{\n planmagnetBoards: { items: AdminBoard[]; totalCount: number };\n }>(ADMIN_BOARDS_QUERY, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n\n const rulesQuery = useQuery<{\n planmagnetAutomationRules: { items: AdminAutomationRule[]; totalCount: number };\n }>(ADMIN_AUTOMATION_RULES_QUERY, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n\n const [archiveProject] = useMutation(ARCHIVE_PROJECT_MUTATION);\n\n const projects = projectsQuery.data?.planmagnetProjects.items ?? [];\n const boards = boardsQuery.data?.planmagnetBoards.items ?? [];\n const rules = rulesQuery.data?.planmagnetAutomationRules.items ?? [];\n\n const activeProjects = useMemo(\n () => projects.filter((project) => project.status === 'ACTIVE'),\n [projects]\n );\n\n const toggleProjectSelection = (id: string) => {\n setSelectedProjectIds((previous) => {\n const next = new Set(previous);\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n };\n\n const toggleAll = () => {\n const selectable = activeProjects.filter((project) => project.status !== 'ARCHIVED');\n if (selectedProjectIds.size === selectable.length) {\n setSelectedProjectIds(new Set());\n } else {\n setSelectedProjectIds(new Set(selectable.map((project) => project.id)));\n }\n };\n\n const handleBulkArchive = async () => {\n if (selectedProjectIds.size === 0) return;\n setArchiving(true);\n setArchiveError(null);\n try {\n await Promise.all(\n Array.from(selectedProjectIds).map((id) => archiveProject({ variables: { id } }))\n );\n await projectsQuery.refetch();\n setSelectedProjectIds(new Set());\n } catch (error) {\n setArchiveError(\n error instanceof Error\n ? error.message\n : tr('planmagnet.admin.archive.unknownError', 'Bulk archive failed')\n );\n } finally {\n setArchiving(false);\n }\n };\n\n if (!workspaceId) {\n return (\n <div className=\"min-h-screen bg-background\">\n <div className=\"flex min-h-[320px] flex-col items-center justify-center gap-3 text-muted-foreground\">\n <AlertCircle className=\"h-8 w-8\" />\n <p>\n {tr(\n 'planmagnet.admin.noWorkspace',\n 'Select a workspace to manage PlanMagnet admin settings.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const isLoading = projectsQuery.loading || boardsQuery.loading || rulesQuery.loading;\n\n return (\n <div className=\"min-h-screen bg-background\">\n <div className=\"mx-auto max-w-7xl space-y-6 p-6\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold tracking-tight\">\n {tr('planmagnet.admin.title', 'PlanMagnet Admin')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.admin.purpose',\n 'Workspace quota overview, bulk project archive, and automation rule audit.'\n )}\n </p>\n </div>\n </div>\n\n {/* Quota overview — counts are real; limits are placeholder until a\n dedicated admin quota query exists in the backend. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <QuotaCard\n icon={FolderKanban}\n label={tr('planmagnet.admin.quota.projects', 'Projects')}\n used={projects.length}\n limit={50}\n />\n <QuotaCard\n icon={LayoutTemplate}\n label={tr('planmagnet.admin.quota.boards', 'Boards')}\n used={boards.length}\n limit={100}\n />\n <QuotaCard\n icon={Zap}\n label={tr('planmagnet.admin.quota.automationRules', 'Automation Rules')}\n used={rules.length}\n limit={25}\n />\n </div>\n\n {isLoading ? (\n <SectionLoader />\n ) : (\n <>\n {/* Bulk project archive */}\n <Card>\n <CardHeader className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <CardTitle>\n {tr('planmagnet.admin.archive.title', 'Bulk Project Archive')}\n </CardTitle>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.admin.archive.subtitle',\n 'Select active projects and archive them in one action.'\n )}\n </p>\n </div>\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={selectedProjectIds.size === 0 || archiving}\n onClick={handleBulkArchive}\n >\n {archiving ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"mr-2 h-4 w-4\" />\n )}\n {tr('planmagnet.admin.archive.action', 'Archive')} ({selectedProjectIds.size})\n </Button>\n </CardHeader>\n <CardContent>\n {archiveError && (\n <div className=\"mb-4 flex items-center gap-2 rounded-md bg-destructive/10 p-3 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4\" />\n {archiveError}\n </div>\n )}\n <div className=\"rounded-md border\">\n <SimpleTable<AdminProject>\n data={activeProjects}\n columns={[\n {\n key: 'select',\n header: (\n <Checkbox\n checked={\n activeProjects.length > 0 &&\n selectedProjectIds.size === activeProjects.length\n }\n onCheckedChange={toggleAll}\n aria-label={tr(\n 'planmagnet.admin.archive.selectAll',\n 'Select all active projects'\n )}\n />\n ),\n cell: (row) => (\n <Checkbox\n checked={selectedProjectIds.has(row.id)}\n onCheckedChange={() => toggleProjectSelection(row.id)}\n aria-label={tr(\n 'planmagnet.admin.archive.selectProject',\n 'Select project'\n )}\n />\n ),\n className: 'w-12',\n },\n {\n key: 'name',\n header: tr('planmagnet.admin.archive.column.name', 'Project'),\n cell: (row) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.name}</span>\n <span className=\"text-xs text-muted-foreground font-mono\">\n {row.key}\n </span>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('planmagnet.admin.archive.column.status', 'Status'),\n cell: (row) => (\n <Badge variant={projectStatusVariantr(row.status)}>\n {row.status.toLowerCase().replace('_', ' ')}\n </Badge>\n ),\n },\n {\n key: 'methodology',\n header: tr('planmagnet.admin.archive.column.methodology', 'Methodology'),\n cell: (row) => (\n <span className=\"text-sm capitalize\">\n {localizeMethodology(row.methodology)}\n </span>\n ),\n },\n {\n key: 'owner',\n header: tr('planmagnet.admin.archive.column.owner', 'Owner'),\n cell: (row) => (\n <span className=\"text-sm text-muted-foreground font-mono\">\n <PersonName id={row.ownerId} />\n </span>\n ),\n },\n {\n key: 'created',\n header: tr('planmagnet.admin.archive.column.created', 'Created'),\n cell: (row) => <span className=\"text-sm\">{formatDate(row.createdAt)}</span>,\n },\n ]}\n emptyState={tr(\n 'planmagnet.admin.archive.empty',\n 'No active projects to archive.'\n )}\n />\n </div>\n </CardContent>\n </Card>\n\n {/* Automation rule audit */}\n <Card>\n <CardHeader>\n <CardTitle>\n {tr('planmagnet.admin.automation.title', 'Automation Rule Audit')}\n </CardTitle>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.admin.automation.subtitle',\n 'Review rule health, execution counts, and last run timestamps.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n <div className=\"rounded-md border\">\n <SimpleTable<AdminAutomationRule>\n data={rules}\n columns={[\n {\n key: 'name',\n header: tr('planmagnet.admin.automation.column.name', 'Rule'),\n cell: (row) => <span className=\"font-medium\">{row.name}</span>,\n },\n {\n key: 'trigger',\n header: tr('planmagnet.admin.automation.column.trigger', 'Trigger'),\n cell: (row) => (\n <span className=\"text-sm\">{localizeTrigger(row.triggerType)}</span>\n ),\n },\n {\n key: 'status',\n header: tr('planmagnet.admin.automation.column.status', 'Status'),\n cell: (row) => (\n <Badge variant={automationStatusVariantr(row.status)}>\n {row.status.toLowerCase()}\n </Badge>\n ),\n },\n {\n key: 'executions',\n header: tr('planmagnet.admin.automation.column.executions', 'Executions'),\n cell: (row) => <span className=\"text-sm\">{row.executionCount}</span>,\n },\n {\n key: 'lastRun',\n header: tr('planmagnet.admin.automation.column.lastRun', 'Last Run'),\n cell: (row) => (\n <span className=\"text-sm\">{formatDate(row.lastExecutedAt)}</span>\n ),\n },\n {\n key: 'created',\n header: tr('planmagnet.admin.automation.column.created', 'Created'),\n cell: (row) => <span className=\"text-sm\">{formatDate(row.createdAt)}</span>,\n },\n ]}\n emptyState={tr(\n 'planmagnet.admin.automation.empty',\n 'No automation rules found.'\n )}\n />\n </div>\n </CardContent>\n </Card>\n </>\n )}\n </div>\n </div>\n );\n}\n\n// ------------------------------------------------------------------------------\n// Public root export consumed by the admin app shell\n// ------------------------------------------------------------------------------\n\nexport function PlanMagnetAdminRoot(props: PlanMagnetRootProps) {\n return (\n <PlanMagnetProvider {...props}>\n <PlanMagnetAdminDashboard />\n </PlanMagnetProvider>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlanMagnetAdminRoot.js","names":[],"sources":["../../../src/planmagnet/admin/PlanMagnetAdminRoot.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Checkbox,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { AlertCircle, Archive, FolderKanban, LayoutTemplate, Loader2, Zap } from 'lucide-react';\n\nimport { PlanMagnetProvider, usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { cn } from '../utils/cn';\nimport { throwOnError } from '../utils/mutationResult';\nimport type { PlanMagnetRootProps } from '../types';\nimport { PersonName } from '../people/People';\n\n// ------------------------------------------------------------------------------\n// Domain types (mirror the planmagnet subgraph until generated types are fixed)\n// ------------------------------------------------------------------------------\n\ntype ProjectStatus = 'ACTIVE' | 'ARCHIVED' | 'CANCELLED' | 'COMPLETED' | 'ON_HOLD' | 'PLANNING';\n\ninterface AdminProject {\n id: string;\n name: string;\n key: string;\n status: ProjectStatus;\n methodology: string;\n ownerId: string;\n createdAt: string;\n}\n\ninterface AdminBoard {\n id: string;\n name: string;\n type: string;\n projectId?: string | null;\n}\n\ninterface AdminAutomationRule {\n id: string;\n name: string;\n status: string;\n triggerType: string;\n executionCount: number;\n lastExecutedAt?: string | null;\n createdAt: string;\n}\n\n// ------------------------------------------------------------------------------\n// GraphQL documents (inline so the admin surface does not depend on generated\n// hooks while the rest of the MFE's operation files are being reconciled with\n// the schema).\n// ------------------------------------------------------------------------------\n\nconst ADMIN_PROJECTS_QUERY = gql`\n query AdminPlanmagnetProjects($workspaceId: ID!) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n key\n status\n methodology\n ownerId\n createdAt\n }\n totalCount\n }\n }\n`;\n\nconst ADMIN_BOARDS_QUERY = gql`\n query AdminPlanmagnetBoards($workspaceId: ID!) {\n planmagnetBoards(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n type\n projectId\n }\n totalCount\n }\n }\n`;\n\nconst ADMIN_AUTOMATION_RULES_QUERY = gql`\n query AdminPlanmagnetAutomationRules($workspaceId: ID!) {\n planmagnetAutomationRules(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n status\n triggerType\n executionCount\n lastExecutedAt\n createdAt\n }\n totalCount\n }\n }\n`;\n\nconst ARCHIVE_PROJECT_MUTATION = gql`\n mutation AdminArchiveProject($id: ID!) {\n planmagnetUpdateProject(id: $id, input: { status: ARCHIVED }) {\n id\n status\n }\n }\n`;\n\n// ------------------------------------------------------------------------------\n// Presentation helpers\n// ------------------------------------------------------------------------------\n\nfunction projectStatusVariantr(status: ProjectStatus) {\n switch (status) {\n case 'ACTIVE':\n return 'success';\n case 'ARCHIVED':\n return 'secondary';\n case 'CANCELLED':\n return 'destructive';\n case 'COMPLETED':\n return 'info';\n case 'ON_HOLD':\n return 'warning';\n case 'PLANNING':\n return 'outline';\n default:\n return 'default';\n }\n}\n\nfunction automationStatusVariantr(status: string) {\n switch (status) {\n case 'ACTIVE':\n return 'success';\n case 'PAUSED':\n return 'warning';\n case 'ERROR':\n return 'destructive';\n case 'DISABLED':\n return 'secondary';\n default:\n return 'default';\n }\n}\n\nfunction formatDate(value: string | null | undefined) {\n if (!value) return '—';\n try {\n return new Date(value).toLocaleDateString();\n } catch {\n return value;\n }\n}\n\nfunction localizeMethodology(value: string) {\n return value.toLowerCase().replace(/_/g, ' ');\n}\n\nfunction localizeTrigger(value: string) {\n return value.toLowerCase().replace(/_/g, ' ');\n}\n\n// ------------------------------------------------------------------------------\n// Sub-components\n// ------------------------------------------------------------------------------\n\nfunction QuotaCard({\n icon: Icon,\n label,\n used,\n limit,\n className,\n}: {\n icon: React.ComponentType<{ className?: string }>;\n label: string;\n used: number;\n limit: number;\n className?: string;\n}) {\n const percentage = Math.min(100, Math.round((used / Math.max(1, limit)) * 100));\n\n return (\n <Card className={className}>\n <CardContent className=\"pt-6\">\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n <Icon className=\"h-4 w-4\" />\n <span className=\"text-sm\">{label}</span>\n </div>\n <div className=\"mt-2 flex items-baseline gap-2\">\n <span className=\"text-2xl font-semibold\">{used}</span>\n <span className=\"text-sm text-muted-foreground\">/ {limit}</span>\n </div>\n <div className=\"mt-3 h-2 w-full overflow-hidden rounded-full bg-muted\">\n <div\n className={cn(\n 'h-full rounded-full transition-all',\n percentage >= 90 ? 'bg-destructive' : percentage >= 70 ? 'bg-warning' : 'bg-success'\n )}\n style={{ width: `${percentage}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-muted-foreground\">{percentage}% used</p>\n </CardContent>\n </Card>\n );\n}\n\nfunction SectionLoader() {\n return (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-8 w-1/3\" />\n <Skeleton className=\"h-32 w-full\" />\n </div>\n );\n}\n\ninterface SimpleTableColumn<T> {\n key: string;\n header: React.ReactNode;\n cell: (row: T) => React.ReactNode;\n className?: string;\n}\n\nfunction SimpleTable<T extends { id: string }>({\n columns,\n data,\n emptyState,\n}: {\n columns: SimpleTableColumn<T>[];\n data: T[];\n emptyState: React.ReactNode;\n}) {\n if (data.length === 0) {\n return <div className=\"py-8 text-center text-sm text-muted-foreground\">{emptyState}</div>;\n }\n\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-muted/50\">\n <tr>\n {columns.map((column) => (\n <th\n key={column.key}\n className={cn(\n 'px-4 py-3 text-left font-medium text-muted-foreground',\n column.className\n )}\n >\n {column.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y\">\n {data.map((row) => (\n <tr key={row.id} className=\"hover:bg-muted/30\">\n {columns.map((column) => (\n <td key={`${row.id}-${column.key}`} className={cn('px-4 py-3', column.className)}>\n {column.cell(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\n// ------------------------------------------------------------------------------\n// Main dashboard\n// ------------------------------------------------------------------------------\n\nfunction PlanMagnetAdminDashboard() {\n const { workspaceId } = usePlanMagnet();\n const tr = useTr();\n\n const [selectedProjectIds, setSelectedProjectIds] = useState<Set<string>>(new Set());\n const [archiving, setArchiving] = useState(false);\n const [archiveError, setArchiveError] = useState<string | null>(null);\n\n const projectsQuery = useQuery<{\n planmagnetProjects: { items: AdminProject[]; totalCount: number };\n }>(ADMIN_PROJECTS_QUERY, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n\n const boardsQuery = useQuery<{\n planmagnetBoards: { items: AdminBoard[]; totalCount: number };\n }>(ADMIN_BOARDS_QUERY, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n\n const rulesQuery = useQuery<{\n planmagnetAutomationRules: { items: AdminAutomationRule[]; totalCount: number };\n }>(ADMIN_AUTOMATION_RULES_QUERY, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n\n const [archiveProject] = useMutation(ARCHIVE_PROJECT_MUTATION);\n\n const projects = projectsQuery.data?.planmagnetProjects.items ?? [];\n const boards = boardsQuery.data?.planmagnetBoards.items ?? [];\n const rules = rulesQuery.data?.planmagnetAutomationRules.items ?? [];\n\n // Every project that is not archived yet (BOFF-7940). Only ACTIVE ones were offered, so completed projects — the\n // natural candidates — and anything left in PLANNING or ON_HOLD could never be archived here.\n const archivableProjects = useMemo(\n () => projects.filter((project) => project.status !== 'ARCHIVED'),\n [projects]\n );\n\n const toggleProjectSelection = (id: string) => {\n setSelectedProjectIds((previous) => {\n const next = new Set(previous);\n if (next.has(id)) {\n next.delete(id);\n } else {\n next.add(id);\n }\n return next;\n });\n };\n\n const toggleAll = () => {\n if (selectedProjectIds.size === archivableProjects.length) {\n setSelectedProjectIds(new Set());\n } else {\n setSelectedProjectIds(new Set(archivableProjects.map((project) => project.id)));\n }\n };\n\n const handleBulkArchive = async () => {\n if (selectedProjectIds.size === 0) return;\n setArchiving(true);\n setArchiveError(null);\n // Each archive is checked on its own (BOFF-7958): a refused mutation RESOLVES with { error } (errorPolicy 'all'),\n // so Promise.all over the raw calls reported success for archives the service refused. Failures stay selected\n // and are named, with the reason.\n const ids = Array.from(selectedProjectIds);\n try {\n const outcomes = await Promise.allSettled(\n ids.map((id) => throwOnError(archiveProject({ variables: { id } })))\n );\n const failed = ids.filter((_, index) => outcomes[index]!.status === 'rejected');\n await projectsQuery.refetch();\n setSelectedProjectIds(new Set(failed));\n if (failed.length > 0) {\n const first = outcomes.find((o) => o.status === 'rejected') as PromiseRejectedResult;\n const reason = first.reason instanceof Error ? first.reason.message : String(first.reason);\n const names = failed.map((id) => projects.find((p) => p.id === id)?.name ?? id).join(', ');\n setArchiveError(\n `${failed.length} of ${ids.length} could not be archived (${names}): ${reason}`\n );\n }\n } catch (error) {\n setArchiveError(\n error instanceof Error\n ? error.message\n : tr('planmagnet.admin.archive.unknownError', 'Bulk archive failed')\n );\n } finally {\n setArchiving(false);\n }\n };\n\n if (!workspaceId) {\n return (\n <div className=\"min-h-screen bg-background\">\n <div className=\"flex min-h-[320px] flex-col items-center justify-center gap-3 text-muted-foreground\">\n <AlertCircle className=\"h-8 w-8\" />\n <p>\n {tr(\n 'planmagnet.admin.noWorkspace',\n 'Select a workspace to manage PlanMagnet admin settings.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const isLoading = projectsQuery.loading || boardsQuery.loading || rulesQuery.loading;\n\n return (\n <div className=\"min-h-screen bg-background\">\n <div className=\"mx-auto max-w-7xl space-y-6 p-6\">\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold tracking-tight\">\n {tr('planmagnet.admin.title', 'PlanMagnet Admin')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.admin.purpose',\n 'Workspace quota overview, bulk project archive, and automation rule audit.'\n )}\n </p>\n </div>\n </div>\n\n {/* Quota overview — counts are real; limits are placeholder until a\n dedicated admin quota query exists in the backend. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <QuotaCard\n icon={FolderKanban}\n label={tr('planmagnet.admin.quota.projects', 'Projects')}\n used={projects.length}\n limit={50}\n />\n <QuotaCard\n icon={LayoutTemplate}\n label={tr('planmagnet.admin.quota.boards', 'Boards')}\n used={boards.length}\n limit={100}\n />\n <QuotaCard\n icon={Zap}\n label={tr('planmagnet.admin.quota.automationRules', 'Automation Rules')}\n used={rules.length}\n limit={25}\n />\n </div>\n\n {isLoading ? (\n <SectionLoader />\n ) : (\n <>\n {/* Bulk project archive */}\n <Card>\n <CardHeader className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <CardTitle>\n {tr('planmagnet.admin.archive.title', 'Bulk Project Archive')}\n </CardTitle>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.admin.archive.subtitle',\n 'Select projects and archive them in one action.'\n )}\n </p>\n </div>\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={selectedProjectIds.size === 0 || archiving}\n onClick={handleBulkArchive}\n >\n {archiving ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"mr-2 h-4 w-4\" />\n )}\n {tr('planmagnet.admin.archive.action', 'Archive')} ({selectedProjectIds.size})\n </Button>\n </CardHeader>\n <CardContent>\n {archiveError && (\n <div className=\"mb-4 flex items-center gap-2 rounded-md bg-destructive/10 p-3 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4\" />\n {archiveError}\n </div>\n )}\n <div className=\"rounded-md border\">\n <SimpleTable<AdminProject>\n data={archivableProjects}\n columns={[\n {\n key: 'select',\n header: (\n <Checkbox\n checked={\n archivableProjects.length > 0 &&\n selectedProjectIds.size === archivableProjects.length\n }\n onCheckedChange={toggleAll}\n aria-label={tr(\n 'planmagnet.admin.archive.selectAll',\n 'Select all projects'\n )}\n />\n ),\n cell: (row) => (\n <Checkbox\n checked={selectedProjectIds.has(row.id)}\n onCheckedChange={() => toggleProjectSelection(row.id)}\n aria-label={tr(\n 'planmagnet.admin.archive.selectProject',\n 'Select project'\n )}\n />\n ),\n className: 'w-12',\n },\n {\n key: 'name',\n header: tr('planmagnet.admin.archive.column.name', 'Project'),\n cell: (row) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.name}</span>\n <span className=\"text-xs text-muted-foreground font-mono\">\n {row.key}\n </span>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('planmagnet.admin.archive.column.status', 'Status'),\n cell: (row) => (\n <Badge variant={projectStatusVariantr(row.status)}>\n {row.status.toLowerCase().replace('_', ' ')}\n </Badge>\n ),\n },\n {\n key: 'methodology',\n header: tr('planmagnet.admin.archive.column.methodology', 'Methodology'),\n cell: (row) => (\n <span className=\"text-sm capitalize\">\n {localizeMethodology(row.methodology)}\n </span>\n ),\n },\n {\n key: 'owner',\n header: tr('planmagnet.admin.archive.column.owner', 'Owner'),\n cell: (row) => (\n <span className=\"text-sm text-muted-foreground font-mono\">\n <PersonName id={row.ownerId} />\n </span>\n ),\n },\n {\n key: 'created',\n header: tr('planmagnet.admin.archive.column.created', 'Created'),\n cell: (row) => <span className=\"text-sm\">{formatDate(row.createdAt)}</span>,\n },\n ]}\n emptyState={tr(\n 'planmagnet.admin.archive.empty',\n 'No active projects to archive.'\n )}\n />\n </div>\n </CardContent>\n </Card>\n\n {/* Automation rule audit */}\n <Card>\n <CardHeader>\n <CardTitle>\n {tr('planmagnet.admin.automation.title', 'Automation Rule Audit')}\n </CardTitle>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.admin.automation.subtitle',\n 'Review rule health, execution counts, and last run timestamps.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n <div className=\"rounded-md border\">\n <SimpleTable<AdminAutomationRule>\n data={rules}\n columns={[\n {\n key: 'name',\n header: tr('planmagnet.admin.automation.column.name', 'Rule'),\n cell: (row) => <span className=\"font-medium\">{row.name}</span>,\n },\n {\n key: 'trigger',\n header: tr('planmagnet.admin.automation.column.trigger', 'Trigger'),\n cell: (row) => (\n <span className=\"text-sm\">{localizeTrigger(row.triggerType)}</span>\n ),\n },\n {\n key: 'status',\n header: tr('planmagnet.admin.automation.column.status', 'Status'),\n cell: (row) => (\n <Badge variant={automationStatusVariantr(row.status)}>\n {row.status.toLowerCase()}\n </Badge>\n ),\n },\n {\n key: 'executions',\n header: tr('planmagnet.admin.automation.column.executions', 'Executions'),\n cell: (row) => <span className=\"text-sm\">{row.executionCount}</span>,\n },\n {\n key: 'lastRun',\n header: tr('planmagnet.admin.automation.column.lastRun', 'Last Run'),\n cell: (row) => (\n <span className=\"text-sm\">{formatDate(row.lastExecutedAt)}</span>\n ),\n },\n {\n key: 'created',\n header: tr('planmagnet.admin.automation.column.created', 'Created'),\n cell: (row) => <span className=\"text-sm\">{formatDate(row.createdAt)}</span>,\n },\n ]}\n emptyState={tr(\n 'planmagnet.admin.automation.empty',\n 'No automation rules found.'\n )}\n />\n </div>\n </CardContent>\n </Card>\n </>\n )}\n </div>\n </div>\n );\n}\n\n// ------------------------------------------------------------------------------\n// Public root export consumed by the admin app shell\n// ------------------------------------------------------------------------------\n\nexport function PlanMagnetAdminRoot(props: PlanMagnetRootProps) {\n return (\n <PlanMagnetProvider {...props}>\n <PlanMagnetAdminDashboard />\n </PlanMagnetProvider>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A button that asks before it acts. Used for anything that cannot be undone from the UI (complete or cancel a
|
|
5
|
+
* sprint, delete a task, a project or a roadmap).
|
|
6
|
+
*/
|
|
7
|
+
export declare function ConfirmButton({ label, icon, title, description, confirmLabel, onConfirm, disabled, destructive, variant, cancelLabel, ariaLabel, }: {
|
|
8
|
+
label: string;
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
confirmLabel: string;
|
|
13
|
+
onConfirm: () => void;
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
destructive?: boolean;
|
|
16
|
+
variant?: 'default' | 'outline';
|
|
17
|
+
cancelLabel?: string;
|
|
18
|
+
/** Required when `label` is empty (an icon-only button): icons are aria-hidden. */
|
|
19
|
+
ariaLabel?: string;
|
|
20
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
/** An inline refusal reason, announced to screen readers. */
|
|
22
|
+
export declare function ErrorLine({ message }: {
|
|
23
|
+
message: string;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=ConfirmButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmButton.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/ConfirmButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAcvC;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,IAAI,EACJ,KAAK,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,WAAmB,EACnB,OAAmB,EACnB,WAAuB,EACvB,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CA8BA;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,2CAMzD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as e, jsxs as t } from "react/jsx-runtime";
|
|
2
|
+
import { AlertDialog as n, AlertDialogAction as r, AlertDialogCancel as i, AlertDialogContent as a, AlertDialogDescription as o, AlertDialogFooter as s, AlertDialogHeader as c, AlertDialogTitle as l, AlertDialogTrigger as u, Button as d } from "@burdenoff/fe-libs/ui";
|
|
3
|
+
//#region src/planmagnet/controls/ConfirmButton.tsx
|
|
4
|
+
function f({ label: f, icon: p, title: m, description: h, confirmLabel: g, onConfirm: _, disabled: v, destructive: y = !1, variant: b = "default", cancelLabel: x = "Keep it", ariaLabel: S }) {
|
|
5
|
+
return /* @__PURE__ */ t(n, { children: [/* @__PURE__ */ e(u, {
|
|
6
|
+
asChild: !0,
|
|
7
|
+
children: /* @__PURE__ */ t(d, {
|
|
8
|
+
size: "sm",
|
|
9
|
+
variant: b,
|
|
10
|
+
disabled: v,
|
|
11
|
+
"aria-label": S,
|
|
12
|
+
children: [p, f]
|
|
13
|
+
})
|
|
14
|
+
}), /* @__PURE__ */ t(a, { children: [/* @__PURE__ */ t(c, { children: [/* @__PURE__ */ e(l, { children: m }), /* @__PURE__ */ e(o, { children: h })] }), /* @__PURE__ */ t(s, { children: [/* @__PURE__ */ e(i, { children: x }), /* @__PURE__ */ e(r, {
|
|
15
|
+
onClick: _,
|
|
16
|
+
className: y ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : "",
|
|
17
|
+
children: g
|
|
18
|
+
})] })] })] });
|
|
19
|
+
}
|
|
20
|
+
function p({ message: t }) {
|
|
21
|
+
return /* @__PURE__ */ e("p", {
|
|
22
|
+
role: "alert",
|
|
23
|
+
className: "text-sm text-destructive",
|
|
24
|
+
children: t
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { f as ConfirmButton, p as ErrorLine };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=ConfirmButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmButton.js","names":[],"sources":["../../../src/planmagnet/controls/ConfirmButton.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n Button,\n} from '@burdenoff/fe-libs/ui';\n\n/**\n * A button that asks before it acts. Used for anything that cannot be undone from the UI (complete or cancel a\n * sprint, delete a task, a project or a roadmap).\n */\nexport function ConfirmButton({\n label,\n icon,\n title,\n description,\n confirmLabel,\n onConfirm,\n disabled,\n destructive = false,\n variant = 'default',\n cancelLabel = 'Keep it',\n ariaLabel,\n}: {\n label: string;\n icon: ReactNode;\n title: string;\n description: string;\n confirmLabel: string;\n onConfirm: () => void;\n disabled: boolean;\n destructive?: boolean;\n variant?: 'default' | 'outline';\n cancelLabel?: string;\n /** Required when `label` is empty (an icon-only button): icons are aria-hidden. */\n ariaLabel?: string;\n}) {\n return (\n <AlertDialog>\n <AlertDialogTrigger asChild>\n <Button size=\"sm\" variant={variant} disabled={disabled} aria-label={ariaLabel}>\n {icon}\n {label}\n </Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription>{description}</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>{cancelLabel}</AlertDialogCancel>\n <AlertDialogAction\n onClick={onConfirm}\n className={\n destructive\n ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'\n : ''\n }\n >\n {confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n\n/** An inline refusal reason, announced to screen readers. */\nexport function ErrorLine({ message }: { message: string }) {\n return (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {message}\n </p>\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,EAAc,EAC5B,UACA,SACA,UACA,gBACA,iBACA,cACA,aACA,iBAAc,IACd,aAAU,WACV,iBAAc,WACd,gBAcC;AACD,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAoB,SAAA;YAClB,kBAAC,GAAD;GAAQ,MAAK;GAAc;GAAmB;GAAU,cAAY;aAApE,CACG,GACA,EACM;;EACU,CAAA,EACrB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAmB,GAAyB,CAAA,EAC5C,kBAAC,GAAD,EAAA,UAAyB,GAAqC,CAAA,CAC5C,EAAA,CAAA,EACpB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAoB,GAAgC,CAAA,EACpD,kBAAC,GAAD;EACE,SAAS;EACT,WACE,IACI,uEACA;YAGL;EACiB,CAAA,CACF,EAAA,CAAA,CACD,EAAA,CAAA,CACT,EAAA,CAAA;;AAKlB,SAAgB,EAAU,EAAE,cAAgC;AAC1D,QACE,kBAAC,KAAD;EAAG,MAAK;EAAQ,WAAU;YACvB;EACC,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature request controls (BOFF-7951/7963). Feature requests were read-only: no vote, no status change after
|
|
3
|
+
* creation, no edit or delete — so every UI-created request stayed at 0 votes and its creation status, and the
|
|
4
|
+
* Declined/Duplicate tabs could only ever be empty.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FEATURE_STATUSES: readonly ["SUBMITTED", "UNDER_REVIEW", "PLANNED", "IN_PROGRESS", "COMPLETED", "DECLINED", "DUPLICATE"];
|
|
7
|
+
/** Vote for a request, or take your vote back. */
|
|
8
|
+
export declare function FeatureVoteButton({ featureId, voted }: {
|
|
9
|
+
featureId: string;
|
|
10
|
+
voted: boolean;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export interface FeatureValues {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description: string | null;
|
|
16
|
+
status: string;
|
|
17
|
+
businessValue: number | null;
|
|
18
|
+
effortEstimate: string | null;
|
|
19
|
+
projectId: string | null;
|
|
20
|
+
workItemId: string | null;
|
|
21
|
+
}
|
|
22
|
+
/** Status select, Edit and Delete for the feature detail header. */
|
|
23
|
+
export declare function FeatureHeaderActions({ feature }: {
|
|
24
|
+
feature: FeatureValues;
|
|
25
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=FeatureControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"names":[],"mappings":"AAuBA;;;;GAIG;AAEH,eAAO,MAAM,gBAAgB,wGAQnB,CAAC;AA+CX,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,2CA6B5F;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA2E3E"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { throwOnError as t } from "../utils/mutationResult.js";
|
|
3
|
+
import { ProjectSelect as n, WorkItemSearchSelect as r } from "./Pickers.js";
|
|
4
|
+
import { ConfirmButton as i, ErrorLine as a } from "./ConfirmButton.js";
|
|
5
|
+
import { parseIntField as o } from "../utils/formValidation.js";
|
|
6
|
+
import { enumLabel as s } from "./WorkItemControls.js";
|
|
7
|
+
import { detailPath as c } from "../utils/rowNavigation.js";
|
|
8
|
+
import { useState as l } from "react";
|
|
9
|
+
import { BarChart3 as u, Pencil as d, ThumbsUp as f, Trash2 as p } from "lucide-react";
|
|
10
|
+
import { gql as m } from "@apollo/client";
|
|
11
|
+
import { useMutation as h } from "@apollo/client/react";
|
|
12
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
13
|
+
import { Button as v, Input as y, ResponsiveDialog as b, ResponsiveDialogContent as x, ResponsiveDialogDescription as S, ResponsiveDialogFooter as C, ResponsiveDialogHeader as w, ResponsiveDialogTitle as T, ResponsiveDialogTrigger as E } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/planmagnet/controls/FeatureControls.tsx
|
|
15
|
+
var D = [
|
|
16
|
+
"SUBMITTED",
|
|
17
|
+
"UNDER_REVIEW",
|
|
18
|
+
"PLANNED",
|
|
19
|
+
"IN_PROGRESS",
|
|
20
|
+
"COMPLETED",
|
|
21
|
+
"DECLINED",
|
|
22
|
+
"DUPLICATE"
|
|
23
|
+
], O = [
|
|
24
|
+
"GetFeatureRequest",
|
|
25
|
+
"ListFeatureRequests",
|
|
26
|
+
"ListFeatureRequestsForProduct",
|
|
27
|
+
"ListFeatureRequestsForProducts",
|
|
28
|
+
"ListAllFeaturesAnalytics",
|
|
29
|
+
"GetFeatureRequestAnalytics",
|
|
30
|
+
"ReleaseCustomerRequests"
|
|
31
|
+
], k = m`
|
|
32
|
+
mutation FeatureControlsVote($id: ID!) {
|
|
33
|
+
planmagnetVoteOnFeature(featureRequestId: $id) {
|
|
34
|
+
id
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
`, A = m`
|
|
38
|
+
mutation FeatureControlsUnvote($id: ID!) {
|
|
39
|
+
planmagnetRemoveFeatureVote(featureRequestId: $id)
|
|
40
|
+
}
|
|
41
|
+
`, j = m`
|
|
42
|
+
mutation FeatureControlsUpdate($id: ID!, $input: PlanmagnetUpdateFeatureRequestInput!) {
|
|
43
|
+
planmagnetUpdateFeatureRequest(id: $id, input: $input) {
|
|
44
|
+
id
|
|
45
|
+
title
|
|
46
|
+
description
|
|
47
|
+
status
|
|
48
|
+
businessValue
|
|
49
|
+
effortEstimate
|
|
50
|
+
projectId
|
|
51
|
+
workItemId
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`, M = m`
|
|
55
|
+
mutation FeatureControlsDelete($id: ID!) {
|
|
56
|
+
planmagnetDeleteFeatureRequest(id: $id)
|
|
57
|
+
}
|
|
58
|
+
`, N = {
|
|
59
|
+
refetchQueries: O,
|
|
60
|
+
awaitRefetchQueries: !0
|
|
61
|
+
};
|
|
62
|
+
function P({ featureId: e, voted: n }) {
|
|
63
|
+
let [r, i] = h(k, N), [o, s] = h(A, N), [c, u] = l(null), d = i.loading || s.loading, p = async () => {
|
|
64
|
+
u(null);
|
|
65
|
+
try {
|
|
66
|
+
await t((n ? o : r)({ variables: { id: e } }));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
u(e instanceof Error ? e.message : "Your vote could not be saved.");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return /* @__PURE__ */ _("div", {
|
|
72
|
+
className: "flex flex-col items-end gap-1",
|
|
73
|
+
children: [/* @__PURE__ */ _(v, {
|
|
74
|
+
size: "sm",
|
|
75
|
+
variant: n ? "default" : "outline",
|
|
76
|
+
"data-testid": "feature-vote",
|
|
77
|
+
"aria-pressed": n,
|
|
78
|
+
disabled: d,
|
|
79
|
+
onClick: () => void p(),
|
|
80
|
+
children: [/* @__PURE__ */ g(f, { className: "mr-1 h-4 w-4" }), n ? "Voted" : "Vote"]
|
|
81
|
+
}), c && /* @__PURE__ */ g(a, { message: c })]
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function F({ feature: n }) {
|
|
85
|
+
let r = e(), [o, d] = h(j, N), [f, m] = h(M, { refetchQueries: O.filter((e) => e !== "GetFeatureRequest") }), [y, b] = l(null), x = d.loading || m.loading, S = D.includes(n.status) ? D : [n.status, ...D], C = async (e) => {
|
|
86
|
+
b(null);
|
|
87
|
+
try {
|
|
88
|
+
await t(o({ variables: {
|
|
89
|
+
id: n.id,
|
|
90
|
+
input: { status: e }
|
|
91
|
+
} }));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
b(e instanceof Error ? e.message : "The status could not be changed.");
|
|
94
|
+
}
|
|
95
|
+
}, w = async () => {
|
|
96
|
+
b(null);
|
|
97
|
+
try {
|
|
98
|
+
await t(f({ variables: { id: n.id } })), r("/products/features-page");
|
|
99
|
+
} catch (e) {
|
|
100
|
+
b(e instanceof Error ? e.message : "The request could not be deleted.");
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return /* @__PURE__ */ _("div", {
|
|
104
|
+
className: "flex flex-col items-end gap-2",
|
|
105
|
+
children: [/* @__PURE__ */ _("div", {
|
|
106
|
+
className: "flex flex-wrap items-center justify-end gap-2",
|
|
107
|
+
children: [
|
|
108
|
+
/* @__PURE__ */ g("select", {
|
|
109
|
+
"aria-label": "Status",
|
|
110
|
+
"data-testid": "feature-status",
|
|
111
|
+
className: "rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60",
|
|
112
|
+
value: n.status,
|
|
113
|
+
disabled: x,
|
|
114
|
+
onChange: (e) => {
|
|
115
|
+
e.target.value !== n.status && C(e.target.value);
|
|
116
|
+
},
|
|
117
|
+
children: S.map((e) => /* @__PURE__ */ g("option", {
|
|
118
|
+
value: e,
|
|
119
|
+
children: s(e)
|
|
120
|
+
}, e))
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ g(I, { feature: n }),
|
|
123
|
+
/* @__PURE__ */ _(v, {
|
|
124
|
+
size: "sm",
|
|
125
|
+
variant: "outline",
|
|
126
|
+
"data-testid": "feature-analytics",
|
|
127
|
+
onClick: () => r(c("/products/feature-analytics-page", n.id)),
|
|
128
|
+
children: [/* @__PURE__ */ g(u, { className: "mr-1 h-4 w-4" }), "Analytics"]
|
|
129
|
+
}),
|
|
130
|
+
/* @__PURE__ */ g(i, {
|
|
131
|
+
label: "Delete",
|
|
132
|
+
variant: "outline",
|
|
133
|
+
icon: /* @__PURE__ */ g(p, { className: "mr-1 h-4 w-4" }),
|
|
134
|
+
title: `Delete "${n.title}"?`,
|
|
135
|
+
description: "The request and its votes are deleted. This cannot be undone here.",
|
|
136
|
+
confirmLabel: "Delete request",
|
|
137
|
+
destructive: !0,
|
|
138
|
+
disabled: x,
|
|
139
|
+
onConfirm: () => void w()
|
|
140
|
+
})
|
|
141
|
+
]
|
|
142
|
+
}), y && /* @__PURE__ */ g(a, { message: y })]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function I({ feature: e }) {
|
|
146
|
+
let i = {
|
|
147
|
+
title: e.title,
|
|
148
|
+
description: e.description ?? "",
|
|
149
|
+
businessValue: e.businessValue == null ? "" : String(e.businessValue),
|
|
150
|
+
effortEstimate: e.effortEstimate ?? "",
|
|
151
|
+
projectId: e.projectId ?? "",
|
|
152
|
+
workItemId: e.workItemId ?? ""
|
|
153
|
+
}, [s, c] = l(!1), [u, f] = l(i), [p, m] = l(null), [D, O] = h(j, N), k = o(u.businessValue, {
|
|
154
|
+
min: 0,
|
|
155
|
+
max: 100
|
|
156
|
+
}), A = u.title.trim() !== "" && !k.invalid && !O.loading, M = async () => {
|
|
157
|
+
m(null);
|
|
158
|
+
let n = (e) => e.trim() ? e.trim() : null, r = {
|
|
159
|
+
title: u.title.trim(),
|
|
160
|
+
description: n(u.description),
|
|
161
|
+
businessValue: k.value ?? null,
|
|
162
|
+
effortEstimate: n(u.effortEstimate),
|
|
163
|
+
projectId: u.projectId || null,
|
|
164
|
+
workItemId: u.workItemId || null
|
|
165
|
+
}, a = {
|
|
166
|
+
title: i.title,
|
|
167
|
+
description: n(i.description),
|
|
168
|
+
businessValue: i.businessValue === "" ? null : Number(i.businessValue),
|
|
169
|
+
effortEstimate: n(i.effortEstimate),
|
|
170
|
+
projectId: i.projectId || null,
|
|
171
|
+
workItemId: i.workItemId || null
|
|
172
|
+
}, o = Object.fromEntries(Object.entries(r).filter(([e, t]) => t !== a[e]));
|
|
173
|
+
if (Object.keys(o).length === 0) {
|
|
174
|
+
c(!1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
await t(D({ variables: {
|
|
179
|
+
id: e.id,
|
|
180
|
+
input: o
|
|
181
|
+
} })), c(!1);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
m(e instanceof Error ? e.message : "The request could not be saved.");
|
|
184
|
+
}
|
|
185
|
+
}, P = "text-sm font-medium";
|
|
186
|
+
return /* @__PURE__ */ _(b, {
|
|
187
|
+
open: s,
|
|
188
|
+
onOpenChange: (e) => {
|
|
189
|
+
c(e), e ? f(i) : m(null);
|
|
190
|
+
},
|
|
191
|
+
children: [/* @__PURE__ */ g(E, {
|
|
192
|
+
asChild: !0,
|
|
193
|
+
children: /* @__PURE__ */ _(v, {
|
|
194
|
+
size: "sm",
|
|
195
|
+
variant: "outline",
|
|
196
|
+
"data-testid": "feature-edit",
|
|
197
|
+
children: [/* @__PURE__ */ g(d, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
198
|
+
})
|
|
199
|
+
}), /* @__PURE__ */ _(x, {
|
|
200
|
+
className: "max-w-lg",
|
|
201
|
+
children: [
|
|
202
|
+
/* @__PURE__ */ _(w, { children: [/* @__PURE__ */ g(T, { children: "Edit feature request" }), /* @__PURE__ */ g(S, { children: "Link the request to the work item that delivers it so releases can show who is waiting on it." })] }),
|
|
203
|
+
/* @__PURE__ */ _("div", {
|
|
204
|
+
className: "space-y-3",
|
|
205
|
+
children: [
|
|
206
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
207
|
+
htmlFor: "feature-edit-title",
|
|
208
|
+
className: P,
|
|
209
|
+
children: "Title"
|
|
210
|
+
}), /* @__PURE__ */ g(y, {
|
|
211
|
+
id: "feature-edit-title",
|
|
212
|
+
className: "mt-1",
|
|
213
|
+
value: u.title,
|
|
214
|
+
onChange: (e) => f({
|
|
215
|
+
...u,
|
|
216
|
+
title: e.target.value
|
|
217
|
+
})
|
|
218
|
+
})] }),
|
|
219
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
220
|
+
htmlFor: "feature-edit-description",
|
|
221
|
+
className: P,
|
|
222
|
+
children: "Description"
|
|
223
|
+
}), /* @__PURE__ */ g("textarea", {
|
|
224
|
+
id: "feature-edit-description",
|
|
225
|
+
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]",
|
|
226
|
+
value: u.description,
|
|
227
|
+
onChange: (e) => f({
|
|
228
|
+
...u,
|
|
229
|
+
description: e.target.value
|
|
230
|
+
})
|
|
231
|
+
})] }),
|
|
232
|
+
/* @__PURE__ */ _("div", {
|
|
233
|
+
className: "grid grid-cols-2 gap-3",
|
|
234
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
235
|
+
htmlFor: "feature-edit-value",
|
|
236
|
+
className: P,
|
|
237
|
+
children: "Business value (0–100)"
|
|
238
|
+
}), /* @__PURE__ */ g(y, {
|
|
239
|
+
id: "feature-edit-value",
|
|
240
|
+
type: "number",
|
|
241
|
+
min: 0,
|
|
242
|
+
max: 100,
|
|
243
|
+
className: "mt-1",
|
|
244
|
+
value: u.businessValue,
|
|
245
|
+
"aria-invalid": k.invalid,
|
|
246
|
+
onChange: (e) => f({
|
|
247
|
+
...u,
|
|
248
|
+
businessValue: e.target.value
|
|
249
|
+
})
|
|
250
|
+
})] }), /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
251
|
+
htmlFor: "feature-edit-effort",
|
|
252
|
+
className: P,
|
|
253
|
+
children: "Effort"
|
|
254
|
+
}), /* @__PURE__ */ g(y, {
|
|
255
|
+
id: "feature-edit-effort",
|
|
256
|
+
className: "mt-1",
|
|
257
|
+
placeholder: "e.g. M",
|
|
258
|
+
value: u.effortEstimate,
|
|
259
|
+
onChange: (e) => f({
|
|
260
|
+
...u,
|
|
261
|
+
effortEstimate: e.target.value
|
|
262
|
+
})
|
|
263
|
+
})] })]
|
|
264
|
+
}),
|
|
265
|
+
k.invalid && /* @__PURE__ */ g(a, { message: "Business value is a whole number from 0 to 100." }),
|
|
266
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
267
|
+
htmlFor: "feature-edit-project",
|
|
268
|
+
className: P,
|
|
269
|
+
children: "Project"
|
|
270
|
+
}), /* @__PURE__ */ g("div", {
|
|
271
|
+
className: "mt-1",
|
|
272
|
+
children: /* @__PURE__ */ g(n, {
|
|
273
|
+
id: "feature-edit-project",
|
|
274
|
+
value: u.projectId,
|
|
275
|
+
noneLabel: "No project",
|
|
276
|
+
onChange: (e) => f({
|
|
277
|
+
...u,
|
|
278
|
+
projectId: e,
|
|
279
|
+
workItemId: ""
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})] }),
|
|
283
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
284
|
+
htmlFor: "feature-edit-workitem",
|
|
285
|
+
className: P,
|
|
286
|
+
children: "Delivered by work item"
|
|
287
|
+
}), /* @__PURE__ */ g("div", {
|
|
288
|
+
className: "mt-1",
|
|
289
|
+
children: /* @__PURE__ */ g(r, {
|
|
290
|
+
id: "feature-edit-workitem",
|
|
291
|
+
projectId: u.projectId || void 0,
|
|
292
|
+
value: u.workItemId,
|
|
293
|
+
onChange: (e) => f({
|
|
294
|
+
...u,
|
|
295
|
+
workItemId: e
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
})] }),
|
|
299
|
+
p && /* @__PURE__ */ g(a, { message: p })
|
|
300
|
+
]
|
|
301
|
+
}),
|
|
302
|
+
/* @__PURE__ */ _(C, { children: [/* @__PURE__ */ g(v, {
|
|
303
|
+
variant: "outline",
|
|
304
|
+
onClick: () => c(!1),
|
|
305
|
+
children: "Cancel"
|
|
306
|
+
}), /* @__PURE__ */ g(v, {
|
|
307
|
+
onClick: () => void M(),
|
|
308
|
+
disabled: !A,
|
|
309
|
+
children: O.loading ? "Saving…" : "Save"
|
|
310
|
+
})] })
|
|
311
|
+
]
|
|
312
|
+
})]
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
export { F as FeatureHeaderActions, P as FeatureVoteButton };
|
|
317
|
+
|
|
318
|
+
//# sourceMappingURL=FeatureControls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureControls.js","names":[],"sources":["../../../src/planmagnet/controls/FeatureControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation } from '@apollo/client/react';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { BarChart3, Pencil, ThumbsUp, Trash2 } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError } from '../utils/mutationResult';\nimport { parseIntField } from '../utils/formValidation';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { ProjectSelect, WorkItemSearchSelect } from './Pickers';\nimport { enumLabel } from './WorkItemControls';\n\n/**\n * Feature request controls (BOFF-7951/7963). Feature requests were read-only: no vote, no status change after\n * creation, no edit or delete — so every UI-created request stayed at 0 votes and its creation status, and the\n * Declined/Duplicate tabs could only ever be empty.\n */\n\nexport const FEATURE_STATUSES = [\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n] as const;\n\n/** Every query that shows a feature request, its votes or feature counts. */\nconst FEATURE_QUERIES = [\n 'GetFeatureRequest',\n 'ListFeatureRequests',\n 'ListFeatureRequestsForProduct',\n 'ListFeatureRequestsForProducts',\n 'ListAllFeaturesAnalytics',\n 'GetFeatureRequestAnalytics',\n 'ReleaseCustomerRequests',\n];\n\nconst VOTE = gql`\n mutation FeatureControlsVote($id: ID!) {\n planmagnetVoteOnFeature(featureRequestId: $id) {\n id\n }\n }\n`;\nconst UNVOTE = gql`\n mutation FeatureControlsUnvote($id: ID!) {\n planmagnetRemoveFeatureVote(featureRequestId: $id)\n }\n`;\nconst UPDATE = gql`\n mutation FeatureControlsUpdate($id: ID!, $input: PlanmagnetUpdateFeatureRequestInput!) {\n planmagnetUpdateFeatureRequest(id: $id, input: $input) {\n id\n title\n description\n status\n businessValue\n effortEstimate\n projectId\n workItemId\n }\n }\n`;\nconst DELETE = gql`\n mutation FeatureControlsDelete($id: ID!) {\n planmagnetDeleteFeatureRequest(id: $id)\n }\n`;\n\nconst options = { refetchQueries: FEATURE_QUERIES, awaitRefetchQueries: true };\n\n/** Vote for a request, or take your vote back. */\nexport function FeatureVoteButton({ featureId, voted }: { featureId: string; voted: boolean }) {\n const [vote, voteState] = useMutation(VOTE, options);\n const [unvote, unvoteState] = useMutation(UNVOTE, options);\n const [error, setError] = useState<string | null>(null);\n const busy = voteState.loading || unvoteState.loading;\n const toggle = async () => {\n setError(null);\n try {\n await throwOnError((voted ? unvote : vote)({ variables: { id: featureId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Your vote could not be saved.');\n }\n };\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <Button\n size=\"sm\"\n variant={voted ? 'default' : 'outline'}\n data-testid=\"feature-vote\"\n aria-pressed={voted}\n disabled={busy}\n onClick={() => void toggle()}\n >\n <ThumbsUp className=\"mr-1 h-4 w-4\" />\n {voted ? 'Voted' : 'Vote'}\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nexport interface FeatureValues {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n projectId: string | null;\n workItemId: string | null;\n}\n\n/** Status select, Edit and Delete for the feature detail header. */\nexport function FeatureHeaderActions({ feature }: { feature: FeatureValues }) {\n const navigate = usePlanMagnetNavigate();\n const [update, updateState] = useMutation(UPDATE, options);\n const [remove, removeState] = useMutation(DELETE, {\n refetchQueries: FEATURE_QUERIES.filter((q) => q !== 'GetFeatureRequest'),\n });\n const [error, setError] = useState<string | null>(null);\n const busy = updateState.loading || removeState.loading;\n const statuses = (FEATURE_STATUSES as readonly string[]).includes(feature.status)\n ? FEATURE_STATUSES\n : [feature.status, ...FEATURE_STATUSES];\n\n const onStatus = async (status: string) => {\n setError(null);\n try {\n await throwOnError(update({ variables: { id: feature.id, input: { status } } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The status could not be changed.');\n }\n };\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: feature.id } }));\n navigate('/products/features-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The request could not be deleted.');\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n <select\n aria-label=\"Status\"\n data-testid=\"feature-status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm disabled:opacity-60\"\n value={feature.status}\n disabled={busy}\n onChange={(e) => {\n if (e.target.value !== feature.status) void onStatus(e.target.value);\n }}\n >\n {statuses.map((s) => (\n <option key={s} value={s}>\n {enumLabel(s)}\n </option>\n ))}\n </select>\n <FeatureEditDialog feature={feature} />\n {/* BOFF-7983: the analytics page existed with no way in. */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"feature-analytics\"\n onClick={() => navigate(detailPath('/products/feature-analytics-page', feature.id))}\n >\n <BarChart3 className=\"mr-1 h-4 w-4\" />\n Analytics\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete \"${feature.title}\"?`}\n description=\"The request and its votes are deleted. This cannot be undone here.\"\n confirmLabel=\"Delete request\"\n destructive\n disabled={busy}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * Edit a request's fields, including the project and the work item it rides on (the release readiness card needs\n * that link, and no UI could set it — BOFF-7966). Only changed fields are sent; an emptied field is cleared.\n */\nfunction FeatureEditDialog({ feature }: { feature: FeatureValues }) {\n const initial = {\n title: feature.title,\n description: feature.description ?? '',\n businessValue: feature.businessValue == null ? '' : String(feature.businessValue),\n effortEstimate: feature.effortEstimate ?? '',\n projectId: feature.projectId ?? '',\n workItemId: feature.workItemId ?? '',\n };\n const [open, setOpen] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation(UPDATE, options);\n const businessValue = parseIntField(form.businessValue, { min: 0, max: 100 });\n const canSave = form.title.trim() !== '' && !businessValue.invalid && !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\n const next = {\n title: form.title.trim(),\n description: text(form.description),\n businessValue: businessValue.value ?? null,\n effortEstimate: text(form.effortEstimate),\n projectId: form.projectId || null,\n workItemId: form.workItemId || null,\n };\n const before = {\n title: initial.title,\n description: text(initial.description),\n businessValue: initial.businessValue === '' ? null : Number(initial.businessValue),\n effortEstimate: text(initial.effortEstimate),\n projectId: initial.projectId || null,\n workItemId: initial.workItemId || null,\n };\n const input = Object.fromEntries(\n Object.entries(next).filter(([k, v]) => v !== before[k as keyof typeof before])\n );\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: feature.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The request could not be saved.');\n }\n };\n\n const label = 'text-sm font-medium';\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (next) setForm(initial);\n else setError(null);\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"feature-edit\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit feature request</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Link the request to the work item that delivers it so releases can show who is waiting\n on it.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3\">\n <div>\n <label htmlFor=\"feature-edit-title\" className={label}>\n Title\n </label>\n <Input\n id=\"feature-edit-title\"\n className=\"mt-1\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-description\" className={label}>\n Description\n </label>\n <textarea\n id=\"feature-edit-description\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor=\"feature-edit-value\" className={label}>\n Business value (0–100)\n </label>\n <Input\n id=\"feature-edit-value\"\n type=\"number\"\n min={0}\n max={100}\n className=\"mt-1\"\n value={form.businessValue}\n aria-invalid={businessValue.invalid}\n onChange={(e) => setForm({ ...form, businessValue: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"feature-edit-effort\" className={label}>\n Effort\n </label>\n <Input\n id=\"feature-edit-effort\"\n className=\"mt-1\"\n placeholder=\"e.g. M\"\n value={form.effortEstimate}\n onChange={(e) => setForm({ ...form, effortEstimate: e.target.value })}\n />\n </div>\n </div>\n {businessValue.invalid && (\n <ErrorLine message=\"Business value is a whole number from 0 to 100.\" />\n )}\n <div>\n <label htmlFor=\"feature-edit-project\" className={label}>\n Project\n </label>\n <div className=\"mt-1\">\n <ProjectSelect\n id=\"feature-edit-project\"\n value={form.projectId}\n noneLabel=\"No project\"\n onChange={(projectId) => setForm({ ...form, projectId, workItemId: '' })}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"feature-edit-workitem\" className={label}>\n Delivered by work item\n </label>\n <div className=\"mt-1\">\n <WorkItemSearchSelect\n id=\"feature-edit-workitem\"\n projectId={form.projectId || undefined}\n value={form.workItemId}\n onChange={(workItemId) => setForm({ ...form, workItemId })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Cancel\n </Button>\n <Button onClick={() => void save()} disabled={!canSave}>\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milestone controls (BOFF-7953/7963). A milestone was created Pending and could never move: no status control,
|
|
3
|
+
* no edit, no delete — so the In progress / Completed / Missed / Cancelled tabs could only be empty, and the
|
|
4
|
+
* "Completed" date stayed "—". Completing now also records the date (service, 2026-09-27).
|
|
5
|
+
*/
|
|
6
|
+
export declare const MILESTONE_STATUSES: readonly ["PENDING", "IN_PROGRESS", "COMPLETED", "MISSED", "CANCELLED"];
|
|
7
|
+
export interface MilestoneValues {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string | null;
|
|
11
|
+
status: string;
|
|
12
|
+
dueDate: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function MilestoneHeaderActions({ milestone }: {
|
|
15
|
+
milestone: MilestoneValues;
|
|
16
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=MilestoneControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MilestoneControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/MilestoneControls.tsx"],"names":[],"mappings":"AAoBA;;;;GAIG;AAEH,eAAO,MAAM,kBAAkB,yEAMrB,CAAC;AA2BX,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,eAAe,CAAA;CAAE,2CAoEnF"}
|