@burdenoff/microfe-planmagnet 2026.1001.6 → 2026.1001.8
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/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +10 -6
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/automation/automationRuleTypes.d.ts +31 -19
- package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -1
- package/dist/planmagnet/automation/automationRuleTypes.js +4 -8
- package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts +18 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.js +259 -202
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
- package/dist/planmagnet/controls/FeedbackControls.d.ts +70 -0
- package/dist/planmagnet/controls/FeedbackControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/FeedbackControls.js +153 -0
- package/dist/planmagnet/controls/FeedbackControls.js.map +1 -0
- package/dist/planmagnet/controls/SyncControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/SyncControls.js +1 -0
- package/dist/planmagnet/controls/SyncControls.js.map +1 -1
- package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts +47 -0
- package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/WebhookSubscriptionControls.js +542 -0
- package/dist/planmagnet/controls/WebhookSubscriptionControls.js.map +1 -0
- package/dist/planmagnet/dashboards/DashboardUI.js +7 -7
- package/dist/planmagnet/dashboards/cards/RiskSeverityCard.js +4 -4
- package/dist/planmagnet/forecast/DeliveryForecastCard.js +8 -8
- package/dist/planmagnet/forecast/ProjectOutlookCard.js +10 -10
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +9 -9
- package/dist/planmagnet/forecast/ResourceForecastCard.js +6 -6
- package/dist/planmagnet/hooks/useLiveRefresh.d.ts +33 -2
- package/dist/planmagnet/hooks/useLiveRefresh.d.ts.map +1 -1
- package/dist/planmagnet/hooks/useLiveRefresh.js +71 -42
- package/dist/planmagnet/hooks/useLiveRefresh.js.map +1 -1
- package/dist/planmagnet/integrations/syncTypes.d.ts +2 -1
- package/dist/planmagnet/integrations/syncTypes.d.ts.map +1 -1
- package/dist/planmagnet/integrations/syncTypes.js +19 -9
- package/dist/planmagnet/integrations/syncTypes.js.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +8 -8
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +23 -23
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +340 -248
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js +172 -167
- package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +92 -83
- package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js +175 -0
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +151 -126
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +136 -129
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +149 -119
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +13 -13
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +153 -117
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +173 -138
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +9 -9
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +8 -8
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +12 -12
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +23 -23
- package/dist/planmagnet/pages/TasksMyDashboardPagePage.js +18 -18
- package/dist/planmagnet/pages/TasksMyWorkPagePage.js +10 -10
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +5 -5
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -8
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +6 -6
- package/dist/planmagnet/pages/analytics/MetricsCard.js +6 -6
- package/dist/planmagnet/pages/analytics/QualityMetricsCard.js +6 -6
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +8 -8
- package/dist/planmagnet/pages/analytics/StatusFlowCard.js +7 -7
- package/dist/planmagnet/pages/analytics/ThroughputCard.js +7 -7
- package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +7 -7
- package/dist/planmagnet/people/People.d.ts +14 -0
- package/dist/planmagnet/people/People.d.ts.map +1 -1
- package/dist/planmagnet/people/People.js +36 -22
- package/dist/planmagnet/people/People.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.d.ts.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +171 -153
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js.map +1 -1
- package/dist/planmagnet/webhooks/webhookTypes.d.ts +54 -0
- package/dist/planmagnet/webhooks/webhookTypes.d.ts.map +1 -0
- package/dist/planmagnet/webhooks/webhookTypes.js +17 -0
- package/dist/planmagnet/webhooks/webhookTypes.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsFeaturesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsFeaturesPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge, Button } from '@burdenoff/fe-libs/ui';\nimport {\n Star,\n Plus,\n ThumbsUp,\n MessageSquare,\n Target,\n Filter,\n ArrowUpDown,\n Loader2,\n Gauge,\n} from 'lucide-react';\nimport { countLabel, useFeatureCounts } from '../hooks/useCounts';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps, detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { FeatureTriageActions, isSnoozed } from '../controls/FeatureTriage';\nimport {\n MOSCOW_VALUES,\n MOSCOW_META,\n KANO_VALUES,\n KANO_META,\n QUADRANT_VALUES,\n QUADRANT_META,\n QuadrantBadge,\n riceScoreMissingHint,\n} from '../controls/PrioritizationControls';\n\nconst LIST_FEATURE_REQUESTS = gql`\n query ListFeatureRequests(\n $workspaceId: ID!\n $filter: PlanmagnetFeatureRequestFilter\n $pagination: PlanmagnetPaginationInput\n $orderBy: PlanmagnetFeatureOrder\n ) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n orderBy: $orderBy\n ) {\n items {\n id\n title\n description\n status\n businessValue\n effortEstimate\n priorityScore\n voteCount\n submittedBy\n tags\n projectId\n snoozedUntil\n moscow\n kano\n valueEffortQuadrant\n reach\n impact\n confidence\n effortScore\n riceScore\n createdAt\n updatedAt\n }\n totalCount\n }\n }\n`;\n\ninterface FeatureRequestNode {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n priorityScore: number | null;\n voteCount: number;\n submittedBy: string;\n tags: string[];\n reach: number | null;\n impact: number | null;\n confidence: number | null;\n effortScore: number | null;\n riceScore: number | null;\n createdAt: string;\n updatedAt: string;\n}\n\nconst statusColor: Record<string, string> = {\n SUBMITTED: 'bg-gray-100 text-gray-800',\n UNDER_REVIEW: 'bg-yellow-100 text-yellow-800',\n PLANNED: 'bg-blue-100 text-blue-800',\n IN_PROGRESS: 'bg-purple-100 text-purple-800',\n COMPLETED: 'bg-green-100 text-green-800',\n DECLINED: 'bg-red-100 text-red-800',\n DUPLICATE: 'bg-orange-100 text-orange-800',\n};\n\nexport function ProductsFeaturesPagePage() {\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n const [statusFilter, setStatusFilter] = useState<string>('all');\n // Snooze state is read against the moment the page opened (Date.now() may not run during render).\n const [now] = useState(() => Date.now());\n // Sorting happens on the server over every request (BOFF-7965: it re-sorted only the 50 loaded rows).\n const [sortBy, setSortBy] = useState<'NEWEST' | 'MOST_VOTED'>('NEWEST');\n // PRM-26: riceScore is computed, not a stored column (utils/prioritization.ts#computeRiceScore) — there is\n // no server `orderBy` value for it (PlanmagnetFeatureOrder only has NEWEST/MOST_VOTED), so this can only ever\n // re-sort the page already loaded above, never every matching request. Kept as its own toggle, independent\n // of `sortBy`, specifically so it is never confused for a third server-side order — the BOFF-7965 regression\n // this file's own comment above warns about.\n const [riceSort, setRiceSort] = useState(false);\n // PRM-27: MoSCoW/Kano/quadrant are server filters too, combined with the status tabs above — not a\n // client-side re-filter of the 50 loaded rows (same reasoning as BOFF-7965 for sorting).\n const [moscowFilter, setMoscowFilter] = useState('');\n const [kanoFilter, setKanoFilter] = useState('');\n const [quadrantFilter, setQuadrantFilter] = useState('');\n const counts = useFeatureCounts();\n\n const baseFilter =\n statusFilter === 'TRIAGE'\n ? { triage: true }\n : statusFilter === 'SNOOZED'\n ? { snoozed: true }\n : statusFilter !== 'all'\n ? { status: statusFilter }\n : {};\n const filter = {\n ...baseFilter,\n ...(moscowFilter ? { moscow: moscowFilter } : {}),\n ...(kanoFilter ? { kano: kanoFilter } : {}),\n ...(quadrantFilter ? { quadrant: quadrantFilter } : {}),\n };\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(LIST_FEATURE_REQUESTS, {\n variables: {\n workspaceId,\n filter: Object.keys(filter).length > 0 ? filter : undefined,\n pagination: { first: 50 },\n orderBy: sortBy,\n },\n skip: !workspaceId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load features: {error.message}</div>;\n }\n\n const features: FeatureRequestNode[] = data?.planmagnetFeatureRequests?.items ?? [];\n const totalCount = data?.planmagnetFeatureRequests?.totalCount ?? 0;\n\n // PRM-26 — client-side only (see the `riceSort` state comment above): never claims to be a full re-sort of\n // every matching request, only of the up-to-50 rows the server already returned in `sortBy` order. Unscored\n // (null) rows sort last either way, never first as if they scored 0.\n const sorted = riceSort\n ? [...features].sort((a, b) => {\n if (a.riceScore == null && b.riceScore == null) return 0;\n if (a.riceScore == null) return 1;\n if (b.riceScore == null) return -1;\n return b.riceScore - a.riceScore;\n })\n : features;\n\n const statuses = [\n 'all',\n 'TRIAGE',\n 'SNOOZED',\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n ];\n\n // Tiles are unfiltered server counts; the tabs change only the list (BOFF-7965).\n const stats = {\n total: countLabel(counts?.all),\n open: countLabel(counts?.open),\n inProgress: countLabel(counts?.byStatus.IN_PROGRESS),\n completed: countLabel(counts?.byStatus.COMPLETED),\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Features</h1>\n {/* Under a tab, the count is of what matches it (BOFF-7990). */}\n <p className=\"text-sm text-muted-foreground\">\n {statusFilter === 'all'\n ? `${stats.total} feature requests`\n : statusFilter === 'TRIAGE'\n ? `${totalCount} waiting for triage`\n : `${totalCount} ${statusFilter.toLowerCase().replace('_', ' ')} match`}\n </p>\n </div>\n <Button size=\"sm\" onClick={() => navigate('/products/new-feature-page')}>\n <Plus className=\"mr-1 h-4 w-4\" />\n New Feature\n </Button>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Total Features</p>\n <p className=\"text-xl font-bold\">{stats.total}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">In Progress</p>\n <p className=\"text-xl font-bold\">{stats.inProgress}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Completed</p>\n <p className=\"text-xl font-bold\">{stats.completed}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Open</p>\n <p className=\"text-xl font-bold\">{stats.open}</p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <Filter className=\"h-4 w-4 text-muted-foreground\" />\n {statuses.map((s) => (\n <button\n key={s}\n onClick={() => setStatusFilter(s)}\n className={`px-2 py-1 text-xs rounded-md capitalize ${statusFilter === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'all'\n ? 'All'\n : s === 'TRIAGE'\n ? `Triage (${countLabel(counts?.triage)})`\n : s === 'SNOOZED'\n ? `Snoozed (${countLabel(counts?.snoozed)})`\n : s.toLowerCase().replace('_', ' ')}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <ArrowUpDown className=\"h-4 w-4 text-muted-foreground\" />\n {(['NEWEST', 'MOST_VOTED'] as const).map((s) => (\n <button\n key={s}\n onClick={() => setSortBy(s)}\n className={`px-2 py-1 text-xs rounded-md ${!riceSort && sortBy === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'NEWEST' ? 'Newest' : 'Most voted'}\n </button>\n ))}\n {/* PRM-26 — see the `riceSort` state comment: re-sorts only the rows already loaded above, never a\n server-side order (PlanmagnetFeatureOrder has no RICE value). */}\n <button\n type=\"button\"\n aria-pressed={riceSort}\n data-testid=\"feature-sort-rice\"\n onClick={() => setRiceSort((v) => !v)}\n className={`px-2 py-1 text-xs rounded-md ${riceSort ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n RICE score\n </button>\n </div>\n </div>\n {riceSort && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"feature-sort-rice-note\">\n Sorted by RICE score within the {sorted.length} loaded results above — not a server-wide\n sort.\n </p>\n )}\n\n {/* PRM-27: MoSCoW/Kano/Value-Effort quadrant filters — server-side, combined with the status tabs above. */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-moscow\" className=\"text-xs text-muted-foreground\">\n MoSCoW\n </label>\n <select\n id=\"feature-filter-moscow\"\n data-testid=\"feature-filter-moscow\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={moscowFilter}\n onChange={(e) => setMoscowFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {MOSCOW_VALUES.map((v) => (\n <option key={v} value={v}>\n {MOSCOW_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-kano\" className=\"text-xs text-muted-foreground\">\n Kano\n </label>\n <select\n id=\"feature-filter-kano\"\n data-testid=\"feature-filter-kano\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={kanoFilter}\n onChange={(e) => setKanoFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {KANO_VALUES.map((v) => (\n <option key={v} value={v}>\n {KANO_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-quadrant\" className=\"text-xs text-muted-foreground\">\n Quadrant\n </label>\n <select\n id=\"feature-filter-quadrant\"\n data-testid=\"feature-filter-quadrant\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={quadrantFilter}\n onChange={(e) => setQuadrantFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {QUADRANT_VALUES.map((v) => (\n <option key={v} value={v}>\n {QUADRANT_META[v].label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"space-y-3\">\n {sorted.map((feature) => (\n <Card\n key={feature.id}\n {...rowLinkProps(\n () => navigate(detailPath('/products/feature-detail-page', feature.id)),\n feature.title\n )}\n >\n <CardContent className=\"py-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{feature.title}</span>\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${statusColor[feature.status] ?? 'bg-gray-100'}`}\n >\n {feature.status.toLowerCase().replace('_', ' ')}\n </span>\n {/* PRM-27 — hidden when unscored so most cards (not yet prioritized) stay uncluttered. */}\n <QuadrantBadge quadrant={feature.valueEffortQuadrant} hideWhenUnscored />\n </div>\n <p className=\"text-xs text-muted-foreground\">{feature.description}</p>\n <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n <span>Effort: {feature.effortEstimate ?? 'N/A'}</span>\n {/* By name, not user UUID (BOFF-7950). */}\n <span>\n Submitted by: <PersonName id={feature.submittedBy} />\n </span>\n <span>{new Date(feature.updatedAt).toLocaleDateString()}</span>\n </div>\n {isSnoozed(feature.snoozedUntil, now) && (\n <p className=\"text-xs text-muted-foreground\">\n Snoozed until {new Date(feature.snoozedUntil).toLocaleDateString()}\n </p>\n )}\n <FeatureTriageActions feature={feature} now={now} />\n {feature.tags.length > 0 && (\n <div className=\"flex gap-1\">\n {feature.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-[10px]\">\n {tag}\n </Badge>\n ))}\n </div>\n )}\n </div>\n <div className=\"flex items-center gap-4 shrink-0\">\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Target className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\">{feature.priorityScore ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Priority</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <ThumbsUp className=\"h-3 w-3 text-green-500\" />\n <span className=\"text-sm font-bold\">{feature.voteCount}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Votes</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-yellow-500\" />\n <span className=\"text-sm font-bold\">{feature.businessValue ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Value</p>\n </div>\n {/* PRM-26 */}\n <div\n className=\"text-center\"\n title={\n feature.riceScore == null\n ? (riceScoreMissingHint(feature) ?? undefined)\n : undefined\n }\n >\n <div className=\"flex items-center gap-1\">\n <Gauge className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\" data-testid=\"feature-rice-score-cell\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">RICE</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n {sorted.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n {statusFilter === 'TRIAGE'\n ? 'Nothing waits for triage.'\n : 'No features match the selected filters.'}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProductsFeaturesPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsFeaturesPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge, Button } from '@burdenoff/fe-libs/ui';\nimport {\n Star,\n Plus,\n ThumbsUp,\n MessageSquare,\n Target,\n Filter,\n ArrowUpDown,\n Loader2,\n Gauge,\n} from 'lucide-react';\nimport { countLabel, useFeatureCounts } from '../hooks/useCounts';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { rowLinkProps, detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { FeatureTriageActions, isSnoozed } from '../controls/FeatureTriage';\nimport { CategoryBadge, RatingDisplay } from '../controls/FeedbackControls';\nimport {\n MOSCOW_VALUES,\n MOSCOW_META,\n KANO_VALUES,\n KANO_META,\n QUADRANT_VALUES,\n QUADRANT_META,\n QuadrantBadge,\n riceScoreMissingHint,\n} from '../controls/PrioritizationControls';\n\nconst LIST_FEATURE_REQUESTS = gql`\n query ListFeatureRequests(\n $workspaceId: ID!\n $filter: PlanmagnetFeatureRequestFilter\n $pagination: PlanmagnetPaginationInput\n $orderBy: PlanmagnetFeatureOrder\n ) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: $filter\n pagination: $pagination\n orderBy: $orderBy\n ) {\n items {\n id\n title\n description\n status\n businessValue\n effortEstimate\n priorityScore\n voteCount\n submittedBy\n tags\n projectId\n snoozedUntil\n moscow\n kano\n valueEffortQuadrant\n reach\n impact\n confidence\n effortScore\n riceScore\n rating\n category\n createdAt\n updatedAt\n }\n totalCount\n }\n }\n`;\n\ninterface FeatureRequestNode {\n id: string;\n title: string;\n description: string | null;\n status: string;\n businessValue: number | null;\n effortEstimate: string | null;\n priorityScore: number | null;\n voteCount: number;\n submittedBy: string;\n tags: string[];\n reach: number | null;\n impact: number | null;\n confidence: number | null;\n effortScore: number | null;\n riceScore: number | null;\n rating: number | null;\n category: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nconst statusColor: Record<string, string> = {\n SUBMITTED: 'bg-gray-100 text-gray-800',\n UNDER_REVIEW: 'bg-yellow-100 text-yellow-800',\n PLANNED: 'bg-blue-100 text-blue-800',\n IN_PROGRESS: 'bg-purple-100 text-purple-800',\n COMPLETED: 'bg-green-100 text-green-800',\n DECLINED: 'bg-red-100 text-red-800',\n DUPLICATE: 'bg-orange-100 text-orange-800',\n};\n\nexport function ProductsFeaturesPagePage() {\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n const [statusFilter, setStatusFilter] = useState<string>('all');\n // Snooze state is read against the moment the page opened (Date.now() may not run during render).\n const [now] = useState(() => Date.now());\n // Sorting happens on the server over every request (BOFF-7965: it re-sorted only the 50 loaded rows).\n const [sortBy, setSortBy] = useState<'NEWEST' | 'MOST_VOTED'>('NEWEST');\n // PRM-26: riceScore is computed, not a stored column (utils/prioritization.ts#computeRiceScore) — there is\n // no server `orderBy` value for it (PlanmagnetFeatureOrder only has NEWEST/MOST_VOTED), so this can only ever\n // re-sort the page already loaded above, never every matching request. Kept as its own toggle, independent\n // of `sortBy`, specifically so it is never confused for a third server-side order — the BOFF-7965 regression\n // this file's own comment above warns about.\n const [riceSort, setRiceSort] = useState(false);\n // PRM-27: MoSCoW/Kano/quadrant are server filters too, combined with the status tabs above — not a\n // client-side re-filter of the 50 loaded rows (same reasoning as BOFF-7965 for sorting).\n const [moscowFilter, setMoscowFilter] = useState('');\n const [kanoFilter, setKanoFilter] = useState('');\n const [quadrantFilter, setQuadrantFilter] = useState('');\n const counts = useFeatureCounts();\n\n const baseFilter =\n statusFilter === 'TRIAGE'\n ? { triage: true }\n : statusFilter === 'SNOOZED'\n ? { snoozed: true }\n : statusFilter !== 'all'\n ? { status: statusFilter }\n : {};\n const filter = {\n ...baseFilter,\n ...(moscowFilter ? { moscow: moscowFilter } : {}),\n ...(kanoFilter ? { kano: kanoFilter } : {}),\n ...(quadrantFilter ? { quadrant: quadrantFilter } : {}),\n };\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(LIST_FEATURE_REQUESTS, {\n variables: {\n workspaceId,\n filter: Object.keys(filter).length > 0 ? filter : undefined,\n pagination: { first: 50 },\n orderBy: sortBy,\n },\n skip: !workspaceId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load features: {error.message}</div>;\n }\n\n const features: FeatureRequestNode[] = data?.planmagnetFeatureRequests?.items ?? [];\n const totalCount = data?.planmagnetFeatureRequests?.totalCount ?? 0;\n\n // PRM-26 — client-side only (see the `riceSort` state comment above): never claims to be a full re-sort of\n // every matching request, only of the up-to-50 rows the server already returned in `sortBy` order. Unscored\n // (null) rows sort last either way, never first as if they scored 0.\n const sorted = riceSort\n ? [...features].sort((a, b) => {\n if (a.riceScore == null && b.riceScore == null) return 0;\n if (a.riceScore == null) return 1;\n if (b.riceScore == null) return -1;\n return b.riceScore - a.riceScore;\n })\n : features;\n\n const statuses = [\n 'all',\n 'TRIAGE',\n 'SNOOZED',\n 'SUBMITTED',\n 'UNDER_REVIEW',\n 'PLANNED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DECLINED',\n 'DUPLICATE',\n ];\n\n // Tiles are unfiltered server counts; the tabs change only the list (BOFF-7965).\n const stats = {\n total: countLabel(counts?.all),\n open: countLabel(counts?.open),\n inProgress: countLabel(counts?.byStatus.IN_PROGRESS),\n completed: countLabel(counts?.byStatus.COMPLETED),\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Features</h1>\n {/* Under a tab, the count is of what matches it (BOFF-7990). */}\n <p className=\"text-sm text-muted-foreground\">\n {statusFilter === 'all'\n ? `${stats.total} feature requests`\n : statusFilter === 'TRIAGE'\n ? `${totalCount} waiting for triage`\n : `${totalCount} ${statusFilter.toLowerCase().replace('_', ' ')} match`}\n </p>\n </div>\n <Button size=\"sm\" onClick={() => navigate('/products/new-feature-page')}>\n <Plus className=\"mr-1 h-4 w-4\" />\n New Feature\n </Button>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Total Features</p>\n <p className=\"text-xl font-bold\">{stats.total}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">In Progress</p>\n <p className=\"text-xl font-bold\">{stats.inProgress}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Completed</p>\n <p className=\"text-xl font-bold\">{stats.completed}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">Open</p>\n <p className=\"text-xl font-bold\">{stats.open}</p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <Filter className=\"h-4 w-4 text-muted-foreground\" />\n {statuses.map((s) => (\n <button\n key={s}\n onClick={() => setStatusFilter(s)}\n className={`px-2 py-1 text-xs rounded-md capitalize ${statusFilter === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'all'\n ? 'All'\n : s === 'TRIAGE'\n ? `Triage (${countLabel(counts?.triage)})`\n : s === 'SNOOZED'\n ? `Snoozed (${countLabel(counts?.snoozed)})`\n : s.toLowerCase().replace('_', ' ')}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <ArrowUpDown className=\"h-4 w-4 text-muted-foreground\" />\n {(['NEWEST', 'MOST_VOTED'] as const).map((s) => (\n <button\n key={s}\n onClick={() => setSortBy(s)}\n className={`px-2 py-1 text-xs rounded-md ${!riceSort && sortBy === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'NEWEST' ? 'Newest' : 'Most voted'}\n </button>\n ))}\n {/* PRM-26 — see the `riceSort` state comment: re-sorts only the rows already loaded above, never a\n server-side order (PlanmagnetFeatureOrder has no RICE value). */}\n <button\n type=\"button\"\n aria-pressed={riceSort}\n data-testid=\"feature-sort-rice\"\n onClick={() => setRiceSort((v) => !v)}\n className={`px-2 py-1 text-xs rounded-md ${riceSort ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n RICE score\n </button>\n </div>\n </div>\n {riceSort && (\n <p className=\"text-xs text-muted-foreground\" data-testid=\"feature-sort-rice-note\">\n Sorted by RICE score within the {sorted.length} loaded results above — not a server-wide\n sort.\n </p>\n )}\n\n {/* PRM-27: MoSCoW/Kano/Value-Effort quadrant filters — server-side, combined with the status tabs above. */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-moscow\" className=\"text-xs text-muted-foreground\">\n MoSCoW\n </label>\n <select\n id=\"feature-filter-moscow\"\n data-testid=\"feature-filter-moscow\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={moscowFilter}\n onChange={(e) => setMoscowFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {MOSCOW_VALUES.map((v) => (\n <option key={v} value={v}>\n {MOSCOW_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-kano\" className=\"text-xs text-muted-foreground\">\n Kano\n </label>\n <select\n id=\"feature-filter-kano\"\n data-testid=\"feature-filter-kano\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={kanoFilter}\n onChange={(e) => setKanoFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {KANO_VALUES.map((v) => (\n <option key={v} value={v}>\n {KANO_META[v].label}\n </option>\n ))}\n </select>\n </div>\n <div className=\"flex items-center gap-1\">\n <label htmlFor=\"feature-filter-quadrant\" className=\"text-xs text-muted-foreground\">\n Quadrant\n </label>\n <select\n id=\"feature-filter-quadrant\"\n data-testid=\"feature-filter-quadrant\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-xs\"\n value={quadrantFilter}\n onChange={(e) => setQuadrantFilter(e.target.value)}\n >\n <option value=\"\">All</option>\n {QUADRANT_VALUES.map((v) => (\n <option key={v} value={v}>\n {QUADRANT_META[v].label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"space-y-3\">\n {sorted.map((feature) => (\n <Card\n key={feature.id}\n {...rowLinkProps(\n () => navigate(detailPath('/products/feature-detail-page', feature.id)),\n feature.title\n )}\n >\n <CardContent className=\"py-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{feature.title}</span>\n <span\n className={`text-[10px] px-1.5 py-0.5 rounded-full capitalize ${statusColor[feature.status] ?? 'bg-gray-100'}`}\n >\n {feature.status.toLowerCase().replace('_', ' ')}\n </span>\n {/* PRM-27 — hidden when unscored so most cards (not yet prioritized) stay uncluttered. */}\n <QuadrantBadge quadrant={feature.valueEffortQuadrant} hideWhenUnscored />\n </div>\n <p className=\"text-xs text-muted-foreground\">{feature.description}</p>\n <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n <span>Effort: {feature.effortEstimate ?? 'N/A'}</span>\n {/* By name, not user UUID (BOFF-7950). */}\n <span>\n Submitted by: <PersonName id={feature.submittedBy} />\n </span>\n <span>{new Date(feature.updatedAt).toLocaleDateString()}</span>\n </div>\n {/* CWF-15: structured feedback — rating/category, hidden when neither is set. */}\n {(feature.rating != null || feature.category) && (\n <div className=\"flex items-center gap-2\">\n {feature.rating != null && <RatingDisplay rating={feature.rating} />}\n <CategoryBadge category={feature.category} />\n </div>\n )}\n {isSnoozed(feature.snoozedUntil, now) && (\n <p className=\"text-xs text-muted-foreground\">\n Snoozed until {new Date(feature.snoozedUntil).toLocaleDateString()}\n </p>\n )}\n <FeatureTriageActions feature={feature} now={now} />\n {feature.tags.length > 0 && (\n <div className=\"flex gap-1\">\n {feature.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-[10px]\">\n {tag}\n </Badge>\n ))}\n </div>\n )}\n </div>\n <div className=\"flex items-center gap-4 shrink-0\">\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Target className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\">{feature.priorityScore ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Priority</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <ThumbsUp className=\"h-3 w-3 text-green-500\" />\n <span className=\"text-sm font-bold\">{feature.voteCount}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Votes</p>\n </div>\n <div className=\"text-center\">\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-yellow-500\" />\n <span className=\"text-sm font-bold\">{feature.businessValue ?? 0}</span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">Value</p>\n </div>\n {/* PRM-26 */}\n <div\n className=\"text-center\"\n title={\n feature.riceScore == null\n ? (riceScoreMissingHint(feature) ?? undefined)\n : undefined\n }\n >\n <div className=\"flex items-center gap-1\">\n <Gauge className=\"h-3 w-3 text-primary\" />\n <span className=\"text-sm font-bold\" data-testid=\"feature-rice-score-cell\">\n {feature.riceScore == null ? '—' : feature.riceScore.toFixed(1)}\n </span>\n </div>\n <p className=\"text-[10px] text-muted-foreground\">RICE</p>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n {sorted.length === 0 && (\n <p className=\"text-center text-muted-foreground py-8\">\n {statusFilter === 'TRIAGE'\n ? 'Nothing waits for triage.'\n : 'No features match the selected filters.'}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsNewFeaturePagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsNewFeaturePagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ProductsNewFeaturePagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsNewFeaturePagePage.tsx"],"names":[],"mappings":"AA4CA,wBAAgB,0BAA0B,4CAsUzC"}
|
|
@@ -3,14 +3,15 @@ import { throwOnError as n } from "../utils/mutationResult.js";
|
|
|
3
3
|
import { ProjectSelect as r, WorkItemSearchSelect as i } from "../controls/Pickers.js";
|
|
4
4
|
import { parseIntField as a } from "../utils/formValidation.js";
|
|
5
5
|
import { EMPTY_PRIORITIZATION_FORM as o, EMPTY_RICE_FORM as s, PrioritizationScoreFields as c, RiceScoreFields as l, prioritizationScoresValid as u, riceInputsValid as d } from "../controls/PrioritizationControls.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
6
|
+
import { AnonymousSubmissionCheckbox as f, EMPTY_FEEDBACK_FORM as p, FeedbackFields as m } from "../controls/FeedbackControls.js";
|
|
7
|
+
import { useState as h } from "react";
|
|
8
|
+
import { Loader2 as g, Save as _, Star as v, X as ee } from "lucide-react";
|
|
9
|
+
import { gql as y } from "@apollo/client";
|
|
10
|
+
import { useMutation as b } from "@apollo/client/react";
|
|
11
|
+
import { jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
12
|
+
import { Button as C, Card as w, CardContent as T, CardHeader as E, CardTitle as D, Input as O } from "@burdenoff/fe-libs/ui";
|
|
12
13
|
//#region src/planmagnet/pages/ProductsNewFeaturePagePage.tsx
|
|
13
|
-
var
|
|
14
|
+
var k = y`
|
|
14
15
|
mutation CreateFeatureRequest($input: PlanmagnetCreateFeatureRequestInput!) {
|
|
15
16
|
planmagnetCreateFeatureRequest(input: $input) {
|
|
16
17
|
id
|
|
@@ -19,11 +20,14 @@ var D = _`
|
|
|
19
20
|
businessValue
|
|
20
21
|
effortEstimate
|
|
21
22
|
priorityScore
|
|
23
|
+
rating
|
|
24
|
+
category
|
|
25
|
+
isAnonymous
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
`;
|
|
25
|
-
function
|
|
26
|
-
let { workspaceId:
|
|
29
|
+
function A() {
|
|
30
|
+
let { workspaceId: y } = e(), A = t(), [j, M] = h({
|
|
27
31
|
title: "",
|
|
28
32
|
description: "",
|
|
29
33
|
status: "SUBMITTED",
|
|
@@ -32,118 +36,124 @@ function O() {
|
|
|
32
36
|
tags: "",
|
|
33
37
|
projectId: "",
|
|
34
38
|
workItemId: ""
|
|
35
|
-
}), [
|
|
39
|
+
}), [N, P] = h(o), [F, I] = h(s), [L, R] = h(p), [z, B] = h(!1), [V, { loading: H }] = b(k, { refetchQueries: ["ListFeatureRequests"] }), [U, W] = h(null), G = a(j.businessValue, {
|
|
36
40
|
min: 0,
|
|
37
41
|
max: 100
|
|
38
|
-
}),
|
|
42
|
+
}), K = u(N), q = a(N.valueScore, {
|
|
39
43
|
min: 1,
|
|
40
44
|
max: 10
|
|
41
|
-
}),
|
|
45
|
+
}), J = a(N.effortScore, {
|
|
42
46
|
min: 1,
|
|
43
47
|
max: 10
|
|
44
|
-
}),
|
|
48
|
+
}), Y = d(F), X = a(F.reach, {
|
|
45
49
|
min: 1,
|
|
46
50
|
max: 10
|
|
47
|
-
}),
|
|
51
|
+
}), Z = a(F.impact, {
|
|
48
52
|
min: 1,
|
|
49
53
|
max: 10
|
|
50
|
-
}),
|
|
54
|
+
}), Q = a(F.confidence, {
|
|
51
55
|
min: 1,
|
|
52
56
|
max: 10
|
|
57
|
+
}), $ = a(L.rating, {
|
|
58
|
+
min: 1,
|
|
59
|
+
max: 5
|
|
53
60
|
});
|
|
54
|
-
return /* @__PURE__ */
|
|
61
|
+
return /* @__PURE__ */ S("div", {
|
|
55
62
|
className: "space-y-6 p-6 max-w-4xl",
|
|
56
63
|
children: [
|
|
57
|
-
/* @__PURE__ */
|
|
64
|
+
/* @__PURE__ */ S("div", {
|
|
58
65
|
className: "flex items-center justify-between",
|
|
59
|
-
children: [/* @__PURE__ */
|
|
66
|
+
children: [/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("h1", {
|
|
60
67
|
className: "text-2xl font-semibold",
|
|
61
68
|
children: "New Feature"
|
|
62
|
-
}), /* @__PURE__ */
|
|
69
|
+
}), /* @__PURE__ */ x("p", {
|
|
63
70
|
className: "text-sm text-muted-foreground",
|
|
64
71
|
children: "Define a new feature request with business value metrics."
|
|
65
|
-
})] }), /* @__PURE__ */
|
|
72
|
+
})] }), /* @__PURE__ */ S("div", {
|
|
66
73
|
className: "flex gap-2",
|
|
67
|
-
children: [/* @__PURE__ */
|
|
74
|
+
children: [/* @__PURE__ */ S(C, {
|
|
68
75
|
variant: "outline",
|
|
69
76
|
size: "sm",
|
|
70
|
-
onClick: () =>
|
|
71
|
-
children: [/* @__PURE__ */
|
|
72
|
-
}), /* @__PURE__ */
|
|
77
|
+
onClick: () => A("/products/features-page"),
|
|
78
|
+
children: [/* @__PURE__ */ x(ee, { className: "mr-1 h-4 w-4" }), "Cancel"]
|
|
79
|
+
}), /* @__PURE__ */ S(C, {
|
|
73
80
|
size: "sm",
|
|
74
81
|
onClick: async () => {
|
|
75
|
-
if (!(!
|
|
76
|
-
|
|
82
|
+
if (!(!j.title.trim() || G.invalid || !K || !Y || $.invalid)) {
|
|
83
|
+
W(null);
|
|
77
84
|
try {
|
|
78
|
-
await n(
|
|
79
|
-
title:
|
|
80
|
-
description:
|
|
81
|
-
status:
|
|
82
|
-
businessValue:
|
|
83
|
-
effortEstimate:
|
|
84
|
-
projectId:
|
|
85
|
-
workItemId:
|
|
86
|
-
tags:
|
|
87
|
-
moscow:
|
|
88
|
-
kano:
|
|
89
|
-
valueScore:
|
|
90
|
-
effortScore:
|
|
91
|
-
reach:
|
|
92
|
-
impact:
|
|
93
|
-
confidence:
|
|
94
|
-
|
|
85
|
+
await n(V({ variables: { input: {
|
|
86
|
+
title: j.title,
|
|
87
|
+
description: j.description || void 0,
|
|
88
|
+
status: j.status,
|
|
89
|
+
businessValue: G.value,
|
|
90
|
+
effortEstimate: j.effortEstimate,
|
|
91
|
+
projectId: j.projectId || void 0,
|
|
92
|
+
workItemId: j.workItemId || void 0,
|
|
93
|
+
tags: j.tags ? j.tags.split(",").map((e) => e.trim()).filter(Boolean) : void 0,
|
|
94
|
+
moscow: N.moscow || void 0,
|
|
95
|
+
kano: N.kano || void 0,
|
|
96
|
+
valueScore: q.value,
|
|
97
|
+
effortScore: J.value,
|
|
98
|
+
reach: X.value,
|
|
99
|
+
impact: Z.value,
|
|
100
|
+
confidence: Q.value,
|
|
101
|
+
rating: $.value,
|
|
102
|
+
category: L.category.trim() || void 0,
|
|
103
|
+
isAnonymous: z
|
|
104
|
+
} } })), A("/products/features-page");
|
|
95
105
|
} catch (e) {
|
|
96
|
-
|
|
106
|
+
W(e instanceof Error ? e.message : "The feature could not be created.");
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
},
|
|
100
|
-
disabled:
|
|
101
|
-
children: [
|
|
110
|
+
disabled: H || !j.title.trim() || G.invalid || !K || !Y || $.invalid,
|
|
111
|
+
children: [H ? /* @__PURE__ */ x(g, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(_, { className: "mr-1 h-4 w-4" }), "Create Feature"]
|
|
102
112
|
})]
|
|
103
113
|
})]
|
|
104
114
|
}),
|
|
105
|
-
|
|
115
|
+
U && /* @__PURE__ */ S("div", {
|
|
106
116
|
role: "alert",
|
|
107
117
|
className: "p-3 rounded-md bg-destructive/10 text-destructive text-sm",
|
|
108
|
-
children: ["Failed to create feature: ",
|
|
118
|
+
children: ["Failed to create feature: ", U]
|
|
109
119
|
}),
|
|
110
|
-
/* @__PURE__ */
|
|
120
|
+
/* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ x(D, { children: "Feature Details" }) }), /* @__PURE__ */ S(T, {
|
|
111
121
|
className: "space-y-4",
|
|
112
122
|
children: [
|
|
113
|
-
/* @__PURE__ */
|
|
123
|
+
/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
114
124
|
className: "text-sm font-medium",
|
|
115
125
|
children: "Title"
|
|
116
|
-
}), /* @__PURE__ */
|
|
117
|
-
value:
|
|
118
|
-
onChange: (e) =>
|
|
119
|
-
...
|
|
126
|
+
}), /* @__PURE__ */ x(O, {
|
|
127
|
+
value: j.title,
|
|
128
|
+
onChange: (e) => M({
|
|
129
|
+
...j,
|
|
120
130
|
title: e.target.value
|
|
121
131
|
}),
|
|
122
132
|
className: "mt-1",
|
|
123
133
|
placeholder: "e.g. Advanced Search Filters"
|
|
124
134
|
})] }),
|
|
125
|
-
/* @__PURE__ */
|
|
135
|
+
/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
126
136
|
className: "text-sm font-medium",
|
|
127
137
|
children: "Description"
|
|
128
|
-
}), /* @__PURE__ */
|
|
138
|
+
}), /* @__PURE__ */ x("textarea", {
|
|
129
139
|
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[100px]",
|
|
130
|
-
value:
|
|
131
|
-
onChange: (e) =>
|
|
132
|
-
...
|
|
140
|
+
value: j.description,
|
|
141
|
+
onChange: (e) => M({
|
|
142
|
+
...j,
|
|
133
143
|
description: e.target.value
|
|
134
144
|
}),
|
|
135
145
|
placeholder: "Describe the feature and its user value..."
|
|
136
146
|
})] }),
|
|
137
|
-
/* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ S("div", {
|
|
138
148
|
className: "grid grid-cols-2 gap-4",
|
|
139
|
-
children: [/* @__PURE__ */
|
|
149
|
+
children: [/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
140
150
|
className: "text-sm font-medium",
|
|
141
151
|
children: "Status"
|
|
142
|
-
}), /* @__PURE__ */
|
|
152
|
+
}), /* @__PURE__ */ x("select", {
|
|
143
153
|
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm capitalize",
|
|
144
|
-
value:
|
|
145
|
-
onChange: (e) =>
|
|
146
|
-
...
|
|
154
|
+
value: j.status,
|
|
155
|
+
onChange: (e) => M({
|
|
156
|
+
...j,
|
|
147
157
|
status: e.target.value
|
|
148
158
|
}),
|
|
149
159
|
children: [
|
|
@@ -152,18 +162,18 @@ function O() {
|
|
|
152
162
|
"PLANNED",
|
|
153
163
|
"IN_PROGRESS",
|
|
154
164
|
"COMPLETED"
|
|
155
|
-
].map((e) => /* @__PURE__ */
|
|
165
|
+
].map((e) => /* @__PURE__ */ x("option", {
|
|
156
166
|
value: e,
|
|
157
167
|
children: e.toLowerCase().replace("_", " ")
|
|
158
168
|
}, e))
|
|
159
|
-
})] }), /* @__PURE__ */
|
|
169
|
+
})] }), /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
160
170
|
className: "text-sm font-medium",
|
|
161
171
|
children: "Effort Estimate"
|
|
162
|
-
}), /* @__PURE__ */
|
|
172
|
+
}), /* @__PURE__ */ x("select", {
|
|
163
173
|
className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm capitalize",
|
|
164
|
-
value:
|
|
165
|
-
onChange: (e) =>
|
|
166
|
-
...
|
|
174
|
+
value: j.effortEstimate,
|
|
175
|
+
onChange: (e) => M({
|
|
176
|
+
...j,
|
|
167
177
|
effortEstimate: e.target.value
|
|
168
178
|
}),
|
|
169
179
|
children: [
|
|
@@ -171,54 +181,54 @@ function O() {
|
|
|
171
181
|
"medium",
|
|
172
182
|
"large",
|
|
173
183
|
"epic"
|
|
174
|
-
].map((e) => /* @__PURE__ */
|
|
184
|
+
].map((e) => /* @__PURE__ */ x("option", {
|
|
175
185
|
value: e,
|
|
176
186
|
children: e
|
|
177
187
|
}, e))
|
|
178
188
|
})] })]
|
|
179
189
|
}),
|
|
180
|
-
/* @__PURE__ */
|
|
190
|
+
/* @__PURE__ */ S("div", {
|
|
181
191
|
className: "grid grid-cols-2 gap-4",
|
|
182
|
-
children: [/* @__PURE__ */
|
|
192
|
+
children: [/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
183
193
|
htmlFor: "feature-project",
|
|
184
194
|
className: "text-sm font-medium",
|
|
185
195
|
children: "Project"
|
|
186
|
-
}), /* @__PURE__ */
|
|
196
|
+
}), /* @__PURE__ */ x("div", {
|
|
187
197
|
className: "mt-1",
|
|
188
|
-
children: /* @__PURE__ */
|
|
198
|
+
children: /* @__PURE__ */ x(r, {
|
|
189
199
|
id: "feature-project",
|
|
190
|
-
value:
|
|
200
|
+
value: j.projectId,
|
|
191
201
|
noneLabel: "No project",
|
|
192
|
-
onChange: (e) =>
|
|
193
|
-
...
|
|
202
|
+
onChange: (e) => M({
|
|
203
|
+
...j,
|
|
194
204
|
projectId: e,
|
|
195
205
|
workItemId: ""
|
|
196
206
|
})
|
|
197
207
|
})
|
|
198
|
-
})] }), /* @__PURE__ */
|
|
208
|
+
})] }), /* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
199
209
|
htmlFor: "feature-work-item",
|
|
200
210
|
className: "text-sm font-medium",
|
|
201
211
|
children: "Delivered by work item"
|
|
202
|
-
}), /* @__PURE__ */
|
|
212
|
+
}), /* @__PURE__ */ x("div", {
|
|
203
213
|
className: "mt-1",
|
|
204
|
-
children: /* @__PURE__ */
|
|
214
|
+
children: /* @__PURE__ */ x(i, {
|
|
205
215
|
id: "feature-work-item",
|
|
206
|
-
projectId:
|
|
207
|
-
value:
|
|
208
|
-
onChange: (e) =>
|
|
209
|
-
...
|
|
216
|
+
projectId: j.projectId || void 0,
|
|
217
|
+
value: j.workItemId,
|
|
218
|
+
onChange: (e) => M({
|
|
219
|
+
...j,
|
|
210
220
|
workItemId: e
|
|
211
221
|
})
|
|
212
222
|
})
|
|
213
223
|
})] })]
|
|
214
224
|
}),
|
|
215
|
-
/* @__PURE__ */
|
|
225
|
+
/* @__PURE__ */ S("div", { children: [/* @__PURE__ */ x("label", {
|
|
216
226
|
className: "text-sm font-medium",
|
|
217
227
|
children: "Tags (comma-separated)"
|
|
218
|
-
}), /* @__PURE__ */
|
|
219
|
-
value:
|
|
220
|
-
onChange: (e) =>
|
|
221
|
-
...
|
|
228
|
+
}), /* @__PURE__ */ x(O, {
|
|
229
|
+
value: j.tags,
|
|
230
|
+
onChange: (e) => M({
|
|
231
|
+
...j,
|
|
222
232
|
tags: e.target.value
|
|
223
233
|
}),
|
|
224
234
|
className: "mt-1",
|
|
@@ -226,70 +236,90 @@ function O() {
|
|
|
226
236
|
})] })
|
|
227
237
|
]
|
|
228
238
|
})] }),
|
|
229
|
-
/* @__PURE__ */
|
|
239
|
+
/* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ x(D, { children: "Prioritization" }) }), /* @__PURE__ */ S(T, {
|
|
230
240
|
className: "space-y-4",
|
|
231
|
-
children: [/* @__PURE__ */
|
|
241
|
+
children: [/* @__PURE__ */ x(c, {
|
|
232
242
|
idPrefix: "feature-new",
|
|
233
|
-
values:
|
|
234
|
-
onChange:
|
|
235
|
-
}), /* @__PURE__ */
|
|
243
|
+
values: N,
|
|
244
|
+
onChange: P
|
|
245
|
+
}), /* @__PURE__ */ S("div", {
|
|
236
246
|
className: "border-t pt-3",
|
|
237
|
-
children: [/* @__PURE__ */
|
|
247
|
+
children: [/* @__PURE__ */ x("p", {
|
|
238
248
|
className: "text-sm font-medium mb-1",
|
|
239
249
|
children: "RICE"
|
|
240
|
-
}), /* @__PURE__ */
|
|
250
|
+
}), /* @__PURE__ */ x(l, {
|
|
241
251
|
idPrefix: "feature-new",
|
|
242
|
-
values:
|
|
243
|
-
onChange:
|
|
244
|
-
effortScoreMissing:
|
|
252
|
+
values: F,
|
|
253
|
+
onChange: I,
|
|
254
|
+
effortScoreMissing: J.value == null
|
|
245
255
|
})]
|
|
246
256
|
})]
|
|
247
257
|
})] }),
|
|
248
|
-
/* @__PURE__ */
|
|
258
|
+
/* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ x(D, { children: "Feedback" }) }), /* @__PURE__ */ S(T, {
|
|
259
|
+
className: "space-y-4",
|
|
260
|
+
children: [
|
|
261
|
+
/* @__PURE__ */ x(m, {
|
|
262
|
+
idPrefix: "feature-new",
|
|
263
|
+
values: L,
|
|
264
|
+
onChange: R
|
|
265
|
+
}),
|
|
266
|
+
$.invalid && /* @__PURE__ */ x("p", {
|
|
267
|
+
role: "alert",
|
|
268
|
+
className: "text-xs text-destructive",
|
|
269
|
+
children: "Rating is a whole number from 1 to 5."
|
|
270
|
+
}),
|
|
271
|
+
/* @__PURE__ */ x(f, {
|
|
272
|
+
idPrefix: "feature-new",
|
|
273
|
+
checked: z,
|
|
274
|
+
onChange: B
|
|
275
|
+
})
|
|
276
|
+
]
|
|
277
|
+
})] }),
|
|
278
|
+
/* @__PURE__ */ S(w, { children: [/* @__PURE__ */ x(E, { children: /* @__PURE__ */ S(D, {
|
|
249
279
|
className: "flex items-center gap-2",
|
|
250
|
-
children: [/* @__PURE__ */
|
|
251
|
-
}) }), /* @__PURE__ */
|
|
280
|
+
children: [/* @__PURE__ */ x(v, { className: "h-4 w-4" }), "Business Value"]
|
|
281
|
+
}) }), /* @__PURE__ */ S(T, {
|
|
252
282
|
className: "space-y-4",
|
|
253
|
-
children: [/* @__PURE__ */
|
|
254
|
-
/* @__PURE__ */
|
|
283
|
+
children: [/* @__PURE__ */ S("div", { children: [
|
|
284
|
+
/* @__PURE__ */ x("label", {
|
|
255
285
|
htmlFor: "feature-business-value",
|
|
256
286
|
className: "text-sm font-medium",
|
|
257
287
|
children: "Business Value (0-100)"
|
|
258
288
|
}),
|
|
259
|
-
/* @__PURE__ */
|
|
289
|
+
/* @__PURE__ */ x(O, {
|
|
260
290
|
id: "feature-business-value",
|
|
261
291
|
type: "number",
|
|
262
292
|
min: 0,
|
|
263
293
|
max: 100,
|
|
264
294
|
step: 1,
|
|
265
|
-
value:
|
|
266
|
-
onChange: (e) =>
|
|
267
|
-
...
|
|
295
|
+
value: j.businessValue,
|
|
296
|
+
onChange: (e) => M({
|
|
297
|
+
...j,
|
|
268
298
|
businessValue: e.target.value
|
|
269
299
|
}),
|
|
270
|
-
"aria-invalid":
|
|
300
|
+
"aria-invalid": G.invalid,
|
|
271
301
|
className: "mt-1"
|
|
272
302
|
}),
|
|
273
|
-
|
|
303
|
+
G.invalid ? /* @__PURE__ */ x("p", {
|
|
274
304
|
role: "alert",
|
|
275
305
|
className: "text-xs text-destructive mt-1",
|
|
276
306
|
children: "Business value must be a whole number from 0 to 100."
|
|
277
|
-
}) : /* @__PURE__ */
|
|
307
|
+
}) : /* @__PURE__ */ x("p", {
|
|
278
308
|
className: "text-xs text-muted-foreground mt-1",
|
|
279
309
|
children: "How much business value does this feature deliver?"
|
|
280
310
|
})
|
|
281
|
-
] }), /* @__PURE__ */
|
|
311
|
+
] }), /* @__PURE__ */ S("div", {
|
|
282
312
|
className: "p-4 bg-muted rounded-md text-center",
|
|
283
313
|
children: [
|
|
284
|
-
/* @__PURE__ */
|
|
314
|
+
/* @__PURE__ */ x("p", {
|
|
285
315
|
className: "text-xs text-muted-foreground",
|
|
286
316
|
children: "Business Value Score"
|
|
287
317
|
}),
|
|
288
|
-
/* @__PURE__ */
|
|
318
|
+
/* @__PURE__ */ x("p", {
|
|
289
319
|
className: "text-3xl font-bold text-primary mt-2",
|
|
290
|
-
children:
|
|
320
|
+
children: G.invalid || G.value === void 0 ? "—" : G.value
|
|
291
321
|
}),
|
|
292
|
-
/* @__PURE__ */
|
|
322
|
+
/* @__PURE__ */ x("p", {
|
|
293
323
|
className: "text-xs text-muted-foreground",
|
|
294
324
|
children: "out of 100"
|
|
295
325
|
})
|
|
@@ -300,6 +330,6 @@ function O() {
|
|
|
300
330
|
});
|
|
301
331
|
}
|
|
302
332
|
//#endregion
|
|
303
|
-
export {
|
|
333
|
+
export { A as ProductsNewFeaturePagePage };
|
|
304
334
|
|
|
305
335
|
//# sourceMappingURL=ProductsNewFeaturePagePage.js.map
|