@burdenoff/microfe-planmagnet 2026.1002.1 → 2026.1002.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +1814 -401
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +5167 -54
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +172 -160
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +95 -83
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +7 -2
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +10 -9
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/index.d.ts +1 -1
- package/dist/planmagnet/index.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +4 -4
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +133 -131
- 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 +5 -5
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +6 -6
- 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 +105 -82
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js +174 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js +350 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js +168 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +156 -154
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts +29 -0
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts.map +1 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js +373 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js.map +1 -0
- package/dist/planmagnet/product/ProductControls.js +1 -1
- package/dist/planmagnet/product/ProductControls.js.map +1 -1
- package/dist/planmagnet/product/ProductFeatureControls.d.ts +39 -0
- package/dist/planmagnet/product/ProductFeatureControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureControls.js +315 -0
- package/dist/planmagnet/product/ProductFeatureControls.js.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts +4 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js +192 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts +52 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.js +417 -0
- package/dist/planmagnet/product/ProductLineControls.js.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts +4 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js +129 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts +4 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js +41 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js.map +1 -0
- package/dist/planmagnet/product/hierarchy.d.ts +127 -0
- package/dist/planmagnet/product/hierarchy.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchy.js +173 -0
- package/dist/planmagnet/product/hierarchy.js.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts +35 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.js +440 -0
- package/dist/planmagnet/product/hierarchyOperations.js.map +1 -0
- package/dist/planmagnet/product/refusal.d.ts +14 -0
- package/dist/planmagnet/product/refusal.d.ts.map +1 -0
- package/dist/planmagnet/product/refusal.js +10 -0
- package/dist/planmagnet/product/refusal.js.map +1 -0
- package/dist/planmagnet/product/routes.d.ts +4 -0
- package/dist/planmagnet/product/routes.d.ts.map +1 -1
- package/dist/planmagnet/product/routes.js +2 -2
- package/dist/planmagnet/product/routes.js.map +1 -1
- package/dist/shared/utils/i18n.d.ts +3 -0
- package/dist/shared/utils/i18n.d.ts.map +1 -1
- package/dist/shared/utils/i18n.js +1 -1
- package/dist/shared/utils/i18n.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductControls.js","names":[],"sources":["../../../src/planmagnet/product/ProductControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Checkbox,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Badge,\n} from '@burdenoff/fe-libs/ui';\nimport { Archive, ArchiveRestore, Pencil, Plus, Trash2, X } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { CompetitorSearchSelect } from '../controls/Pickers';\n\n/**\n * Product controls (BOFF-8045). A Product LINKS roadmaps, releases, feature requests and\n * OKR objectives — it never owns their rows (child FKs are nullable, `onDelete: SetNull`\n * server-side) and archiving/deleting a Product never deletes what it links to. Every link\n * mutation here is a pairwise link/unlink (mirrors planmagnetLinkKeyResultToWorkItem in the\n * okr module), not a batch \"set list\" — so adding several candidates at once fires one\n * mutation call per selection.\n */\n\n// ─── Shared refs ────────────────────────────────────────────────────────────\n\nexport interface ProductRoadmapRef {\n id: string;\n name: string;\n}\nexport interface ProductReleaseRef {\n id: string;\n name: string;\n version: string;\n status: string;\n}\nexport interface ProductFeatureRequestRef {\n id: string;\n title: string;\n status: string;\n}\nexport interface ProductObjectiveRef {\n id: string;\n title: string;\n status: string;\n}\nexport interface ProductCompetitorRef {\n id: string;\n name: string;\n description: string | null;\n website: string | null;\n}\n\n/** Every query that lists products or shows a product's detail/links. */\nexport const PRODUCT_QUERIES = ['GetProducts', 'GetProductForEdit', 'GetProductDetail'];\n\n// ─── Header actions (archive / unarchive / delete) ─────────────────────────\n\nconst DELETE_PRODUCT = gql`\n mutation ProductControlsDelete($id: ID!) {\n planmagnetDeleteProduct(id: $id)\n }\n`;\nconst ARCHIVE_PRODUCT = gql`\n mutation ProductControlsArchive($id: ID!) {\n planmagnetArchiveProduct(id: $id) {\n id\n archivedAt\n }\n }\n`;\nconst UNARCHIVE_PRODUCT = gql`\n mutation ProductControlsUnarchive($id: ID!) {\n planmagnetUnarchiveProduct(id: $id) {\n id\n archivedAt\n }\n }\n`;\n\nexport function ProductHeaderActions({\n productId,\n name,\n archivedAt,\n}: {\n productId: string;\n name: string;\n archivedAt: string | null;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_PRODUCT, {\n refetchQueries: PRODUCT_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [archive, archiveState] = useMutation(ARCHIVE_PRODUCT, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [unarchive, unarchiveState] = useMutation(UNARCHIVE_PRODUCT, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: productId } }));\n navigate('/products/products-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The product could not be deleted.');\n }\n };\n\n const onToggleArchive = async () => {\n setError(null);\n try {\n if (archivedAt) {\n await throwOnError(unarchive({ variables: { id: productId } }));\n } else {\n await throwOnError(archive({ variables: { id: productId } }));\n }\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The product could not be updated.');\n }\n };\n\n const archiving = archiveState.loading || unarchiveState.loading;\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"product-toggle-archive\"\n disabled={archiving}\n onClick={() => void onToggleArchive()}\n >\n {archivedAt ? (\n <ArchiveRestore className=\"mr-1 h-4 w-4\" />\n ) : (\n <Archive className=\"mr-1 h-4 w-4\" />\n )}\n {archivedAt ? 'Unarchive' : 'Archive'}\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete ${name}?`}\n description=\"Every linked roadmap, release and feature request stays intact — only its link to this product is cleared. This cannot be undone here.\"\n confirmLabel=\"Delete product\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n// ─── Roadmaps panel ─────────────────────────────────────────────────────────\n\nconst LINK_PRODUCT_ROADMAP = gql`\n mutation ProductControlsLinkRoadmap($productId: ID!, $roadmapId: ID!) {\n planmagnetLinkProductRoadmap(productId: $productId, roadmapId: $roadmapId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_ROADMAP = gql`\n mutation ProductControlsUnlinkRoadmap($productId: ID!, $roadmapId: ID!) {\n planmagnetUnlinkProductRoadmap(productId: $productId, roadmapId: $roadmapId) {\n id\n }\n }\n`;\nconst PRODUCT_CANDIDATE_ROADMAPS = gql`\n query ProductControlsCandidateRoadmaps($workspaceId: ID!) {\n planmagnetRoadmaps(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\nexport function ProductRoadmapsPanel({\n productId,\n roadmaps,\n}: {\n productId: string;\n roadmaps: ProductRoadmapRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_ROADMAP, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (roadmapId: string) => {\n setError(null);\n setRemovingId(roadmapId);\n try {\n await throwOnError(unlink({ variables: { productId, roadmapId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The roadmap could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-roadmaps\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {roadmaps.length} roadmap{roadmaps.length === 1 ? '' : 's'}\n </p>\n <AddProductRoadmapsDialog productId={productId} existingIds={roadmaps.map((r) => r.id)} />\n </div>\n {error && <ErrorLine message={error} />}\n {roadmaps.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No roadmaps linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {roadmaps.map((roadmap) => (\n <li\n key={roadmap.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/products/roadmap-detail-page', roadmap.id))}\n >\n {roadmap.name}\n </button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${roadmap.name} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(roadmap.id)}\n >\n {removingId === roadmap.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductRoadmapsDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetRoadmaps: { items: ProductRoadmapRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_ROADMAPS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetRoadmaps.items ?? []).filter(\n (r) => !existingIds.includes(r.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_ROADMAP, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((roadmapId) =>\n throwOnError(link({ variables: { productId, roadmapId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The roadmaps could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-roadmaps\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add roadmap\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link roadmaps</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Linking a roadmap does not move or copy it — it stays exactly where it is, now also\n reachable from this product.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-roadmap-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every roadmap in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((roadmap) => {\n const inputId = `product-add-roadmap-${roadmap.id}`;\n return (\n <label\n key={roadmap.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(roadmap.id)}\n onCheckedChange={(checked) => toggle(roadmap.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">{roadmap.name}</span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-roadmaps-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} roadmap${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Releases panel ─────────────────────────────────────────────────────────\n\nconst LINK_PRODUCT_RELEASE = gql`\n mutation ProductControlsLinkRelease($productId: ID!, $releaseId: ID!) {\n planmagnetLinkProductRelease(productId: $productId, releaseId: $releaseId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_RELEASE = gql`\n mutation ProductControlsUnlinkRelease($productId: ID!, $releaseId: ID!) {\n planmagnetUnlinkProductRelease(productId: $productId, releaseId: $releaseId) {\n id\n }\n }\n`;\nconst PRODUCT_CANDIDATE_RELEASES = gql`\n query ProductControlsCandidateReleases($workspaceId: ID!) {\n planmagnetReleases(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n version\n status\n }\n totalCount\n }\n }\n`;\n\nexport function ProductReleasesPanel({\n productId,\n releases,\n}: {\n productId: string;\n releases: ProductReleaseRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_RELEASE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (releaseId: string) => {\n setError(null);\n setRemovingId(releaseId);\n try {\n await throwOnError(unlink({ variables: { productId, releaseId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The release could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-releases\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {releases.length} release{releases.length === 1 ? '' : 's'}\n </p>\n <AddProductReleasesDialog productId={productId} existingIds={releases.map((r) => r.id)} />\n </div>\n {error && <ErrorLine message={error} />}\n {releases.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No releases linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {releases.map((release) => (\n <li\n key={release.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/products/release-detail-page', release.id))}\n >\n <span className=\"text-muted-foreground mr-2\">{release.version}</span>\n {release.name}\n </button>\n <div className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {enumLabel(release.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${release.name} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(release.id)}\n >\n {removingId === release.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductReleasesDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetReleases: { items: ProductReleaseRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_RELEASES, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetReleases.items ?? []).filter(\n (r) => !existingIds.includes(r.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_RELEASE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((releaseId) =>\n throwOnError(link({ variables: { productId, releaseId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The releases could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-releases\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add release\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link releases</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Linking a release does not move or copy it — it stays exactly where it is, now also\n reachable from this product.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-release-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every release in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((release) => {\n const inputId = `product-add-release-${release.id}`;\n return (\n <label\n key={release.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(release.id)}\n onCheckedChange={(checked) => toggle(release.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground mr-2\">{release.version}</span>\n {release.name}\n </span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-releases-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} release${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Feature requests panel ─────────────────────────────────────────────────\n\nconst LINK_PRODUCT_FEATURE_REQUEST = gql`\n mutation ProductControlsLinkFeatureRequest($productId: ID!, $featureRequestId: ID!) {\n planmagnetLinkProductFeatureRequest(\n productId: $productId\n featureRequestId: $featureRequestId\n ) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_FEATURE_REQUEST = gql`\n mutation ProductControlsUnlinkFeatureRequest($productId: ID!, $featureRequestId: ID!) {\n planmagnetUnlinkProductFeatureRequest(\n productId: $productId\n featureRequestId: $featureRequestId\n ) {\n id\n }\n }\n`;\nconst PRODUCT_CANDIDATE_FEATURE_REQUESTS = gql`\n query ProductControlsCandidateFeatureRequests($workspaceId: ID!) {\n planmagnetFeatureRequests(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n title\n status\n }\n totalCount\n }\n }\n`;\n\nexport function ProductFeatureRequestsPanel({\n productId,\n featureRequests,\n}: {\n productId: string;\n featureRequests: ProductFeatureRequestRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_FEATURE_REQUEST, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (featureRequestId: string) => {\n setError(null);\n setRemovingId(featureRequestId);\n try {\n await throwOnError(unlink({ variables: { productId, featureRequestId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The feature request could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-feature-requests\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {featureRequests.length} feature request{featureRequests.length === 1 ? '' : 's'}\n </p>\n <AddProductFeatureRequestsDialog\n productId={productId}\n existingIds={featureRequests.map((f) => f.id)}\n />\n </div>\n {error && <ErrorLine message={error} />}\n {featureRequests.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No feature requests linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {featureRequests.map((feature) => (\n <li\n key={feature.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/products/feature-detail-page', feature.id))}\n >\n {feature.title}\n </button>\n <div className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {enumLabel(feature.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${feature.title} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(feature.id)}\n >\n {removingId === feature.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductFeatureRequestsDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetFeatureRequests: { items: ProductFeatureRequestRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_FEATURE_REQUESTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetFeatureRequests.items ?? []).filter(\n (f) => !existingIds.includes(f.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_FEATURE_REQUEST, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((featureRequestId) =>\n throwOnError(link({ variables: { productId, featureRequestId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The feature requests could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-feature-requests\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add feature request\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link feature requests</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Linking a feature request does not move or copy it — it stays exactly where it is, now\n also reachable from this product.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-feature-request-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every feature request in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((feature) => {\n const inputId = `product-add-feature-${feature.id}`;\n return (\n <label\n key={feature.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(feature.id)}\n onCheckedChange={(checked) => toggle(feature.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">{feature.title}</span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-feature-requests-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} request${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Objectives (OKR) panel ─────────────────────────────────────────────────\n// Product<->Objective is a true many-to-many join (ProductObjectiveLink), same pairwise-link\n// shape as planmagnetLinkKeyResultToWorkItem. Unlink returns Boolean!, not Product! (§4).\n\nconst LINK_PRODUCT_OBJECTIVE = gql`\n mutation ProductControlsLinkObjective($productId: ID!, $objectiveId: ID!) {\n planmagnetLinkProductObjective(productId: $productId, objectiveId: $objectiveId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_OBJECTIVE = gql`\n mutation ProductControlsUnlinkObjective($productId: ID!, $objectiveId: ID!) {\n planmagnetUnlinkProductObjective(productId: $productId, objectiveId: $objectiveId)\n }\n`;\nconst PRODUCT_CANDIDATE_OBJECTIVES = gql`\n query ProductControlsCandidateObjectives($workspaceId: ID!) {\n planmagnetObjectives(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n title\n status\n }\n totalCount\n }\n }\n`;\n\nexport function ProductObjectivesPanel({\n productId,\n objectives,\n}: {\n productId: string;\n objectives: ProductObjectiveRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_OBJECTIVE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (objectiveId: string) => {\n setError(null);\n setRemovingId(objectiveId);\n try {\n await throwOnError(unlink({ variables: { productId, objectiveId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The objective could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-objectives\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {objectives.length} objective{objectives.length === 1 ? '' : 's'}\n </p>\n <AddProductObjectivesDialog\n productId={productId}\n existingIds={objectives.map((o) => o.id)}\n />\n </div>\n {error && <ErrorLine message={error} />}\n {objectives.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No OKR objectives linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {objectives.map((objective) => (\n <li\n key={objective.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(`/goals/objective-detail-page?objectiveId=${objective.id}`)}\n >\n {objective.title}\n </button>\n <div className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {enumLabel(objective.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${objective.title} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(objective.id)}\n >\n {removingId === objective.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductObjectivesDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetObjectives: { items: ProductObjectiveRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_OBJECTIVES, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetObjectives.items ?? []).filter(\n (o) => !existingIds.includes(o.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_OBJECTIVE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((objectiveId) =>\n throwOnError(link({ variables: { productId, objectiveId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The objectives could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-objectives\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add objective\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link OKR objectives</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Re-linking an objective that is already linked is a no-op, not an error.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-objective-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every objective in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((objective) => {\n const inputId = `product-add-objective-${objective.id}`;\n return (\n <label\n key={objective.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(objective.id)}\n onCheckedChange={(checked) => toggle(objective.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">{objective.title}</span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-objectives-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} objective${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── About this product (PRM-02) ────────────────────────────────────────────\n\nconst UPDATE_PRODUCT_INFO = gql`\n mutation ProductControlsUpdateInfo($id: ID!, $input: PlanmagnetUpdateProductInput!) {\n planmagnetUpdateProduct(id: $id, input: $input) {\n id\n mission\n valueProposition\n businessModel\n targetSegments\n }\n }\n`;\n\nconst MAX_ABOUT_TEXT = 2000;\nconst MAX_SEGMENTS = 20;\nconst MAX_SEGMENT_LENGTH = 120;\n\nexport interface ProductAboutValues {\n id: string;\n mission: string | null;\n valueProposition: string | null;\n businessModel: string | null;\n targetSegments: string[];\n}\n\n/**\n * A tag-style editor for `Product.targetSegments` (PRM-02): type a segment, Enter or the + button adds it as a\n * chip, its own X removes it. The server enforces at most 20 entries of at most 120 characters each — this\n * enforces the same limits client-side so a refusal never reaches the server for an obviously-oversized list.\n */\nfunction SegmentsEditor({\n value,\n onChange,\n disabled,\n}: {\n value: string[];\n onChange: (next: string[]) => void;\n disabled?: boolean;\n}) {\n const [draft, setDraft] = useState('');\n const atLimit = value.length >= MAX_SEGMENTS;\n const trimmed = draft.trim();\n const canAdd =\n !disabled &&\n !atLimit &&\n trimmed !== '' &&\n trimmed.length <= MAX_SEGMENT_LENGTH &&\n !value.includes(trimmed);\n\n const add = () => {\n if (!canAdd) return;\n onChange([...value, trimmed]);\n setDraft('');\n };\n const remove = (segment: string) => onChange(value.filter((s) => s !== segment));\n\n return (\n <div className=\"space-y-2\" data-testid=\"product-segments\">\n <div className=\"flex flex-wrap gap-1\">\n {value.length === 0 && (\n <span className=\"text-xs text-muted-foreground\">No target segments yet.</span>\n )}\n {value.map((segment) => (\n <Badge key={segment} variant=\"secondary\" className=\"gap-1 pr-1\">\n {segment}\n {!disabled && (\n <button\n type=\"button\"\n aria-label={`Remove ${segment}`}\n onClick={() => remove(segment)}\n className=\"rounded-full hover:bg-muted\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n )}\n </Badge>\n ))}\n </div>\n {!disabled && (\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label=\"Add a target segment\"\n placeholder={atLimit ? `Limit reached (${MAX_SEGMENTS})` : 'e.g. Mid-market SaaS teams'}\n value={draft}\n disabled={atLimit}\n maxLength={MAX_SEGMENT_LENGTH}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n add();\n }\n }}\n />\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n aria-label=\"Add segment\"\n disabled={!canAdd}\n onClick={add}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n )}\n {!disabled && (\n <p className=\"text-xs text-muted-foreground\">\n {value.length}/{MAX_SEGMENTS} segments, up to {MAX_SEGMENT_LENGTH} characters each.\n </p>\n )}\n </div>\n );\n}\n\n/**\n * The product detail page's \"About this product\" section (PRM-02): mission, value proposition, business model\n * and target segments, shown read-only with an inline \"Edit\" toggle for the four fields. Mirrors\n * `FeaturePrioritizationCard`'s own stays-mounted-across-save shape (not a dialog) so a validation refusal from\n * the server surfaces right where the fields are, and the saved values show immediately without navigating.\n */\nexport function ProductAboutPanel({ product }: { product: ProductAboutValues }) {\n const initial = {\n mission: product.mission ?? '',\n valueProposition: product.valueProposition ?? '',\n businessModel: product.businessModel ?? '',\n targetSegments: product.targetSegments,\n };\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation(UPDATE_PRODUCT_INFO, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const startEdit = () => {\n setForm(initial);\n setError(null);\n setEditing(true);\n };\n\n const textOverLimit = [form.mission, form.valueProposition, form.businessModel].some(\n (v) => v.length > MAX_ABOUT_TEXT\n );\n const segmentsValid =\n form.targetSegments.length <= MAX_SEGMENTS &&\n form.targetSegments.every((s) => s.length <= MAX_SEGMENT_LENGTH);\n const canSave = !textOverLimit && segmentsValid && !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\n try {\n await throwOnError(\n update({\n variables: {\n id: product.id,\n input: {\n mission: text(form.mission),\n valueProposition: text(form.valueProposition),\n businessModel: text(form.businessModel),\n targetSegments: form.targetSegments,\n },\n },\n })\n );\n setEditing(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The product information could not be saved.'));\n }\n };\n\n const textareaClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]';\n\n return (\n <Card data-testid=\"product-info\">\n <CardHeader className=\"flex flex-row items-center justify-between gap-2\">\n <CardTitle>About this product</CardTitle>\n {!editing && (\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-info-edit\" onClick={startEdit}>\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {!editing ? (\n <>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Mission</p>\n <p className=\"text-sm whitespace-pre-wrap\">{product.mission || 'Not set'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Value proposition</p>\n <p className=\"text-sm whitespace-pre-wrap\">{product.valueProposition || 'Not set'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Business model</p>\n <p className=\"text-sm whitespace-pre-wrap\">{product.businessModel || 'Not set'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Target segments</p>\n <div className=\"mt-1\">\n <SegmentsEditor value={product.targetSegments} onChange={() => {}} disabled />\n </div>\n </div>\n </>\n ) : (\n <>\n <div>\n <label htmlFor=\"product-info-mission\" className=\"text-sm font-medium\">\n Mission\n </label>\n <textarea\n id=\"product-info-mission\"\n className={textareaClass}\n maxLength={MAX_ABOUT_TEXT}\n value={form.mission}\n onChange={(e) => setForm({ ...form, mission: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"product-info-value-proposition\" className=\"text-sm font-medium\">\n Value proposition\n </label>\n <textarea\n id=\"product-info-value-proposition\"\n className={textareaClass}\n maxLength={MAX_ABOUT_TEXT}\n value={form.valueProposition}\n onChange={(e) => setForm({ ...form, valueProposition: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"product-info-business-model\" className=\"text-sm font-medium\">\n Business model\n </label>\n <textarea\n id=\"product-info-business-model\"\n className={textareaClass}\n maxLength={MAX_ABOUT_TEXT}\n value={form.businessModel}\n onChange={(e) => setForm({ ...form, businessModel: e.target.value })}\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Target segments</label>\n <div className=\"mt-1\">\n <SegmentsEditor\n value={form.targetSegments}\n onChange={(targetSegments) => setForm({ ...form, targetSegments })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(false)}>\n Cancel\n </Button>\n <Button\n size=\"sm\"\n data-testid=\"product-info-save\"\n disabled={!canSave}\n onClick={() => void save()}\n >\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ─── Competitive landscape (PRM-02) ─────────────────────────────────────────\n\nconst LINK_PRODUCT_COMPETITOR = gql`\n mutation ProductControlsLinkCompetitor($productId: ID!, $competitorId: ID!) {\n planmagnetLinkProductCompetitor(productId: $productId, competitorId: $competitorId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_COMPETITOR = gql`\n mutation ProductControlsUnlinkCompetitor($productId: ID!, $competitorId: ID!) {\n planmagnetUnlinkProductCompetitor(productId: $productId, competitorId: $competitorId)\n }\n`;\n\n/**\n * The product detail page's \"Competitive landscape\" panel (PRM-02): the competitors linked to this product, a\n * picker to link another from the workspace directory (`CompetitorSearchSelect`, shared with the feature\n * request detail page's own competitor panel), and an unlink confirm per row — unlike the feature request\n * panel's plain X, PRM-02 asks for a confirm step here since a product's competitive landscape is reviewed\n * material, not a quick per-request note.\n */\nexport function ProductCompetitorsPanel({\n productId,\n competitors,\n}: {\n productId: string;\n competitors: ProductCompetitorRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [addCompetitorId, setAddCompetitorId] = useState('');\n const [link, linkState] = useMutation(LINK_PRODUCT_COMPETITOR, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_COMPETITOR, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onAdd = async () => {\n if (!addCompetitorId) return;\n setError(null);\n try {\n await throwOnError(link({ variables: { productId, competitorId: addCompetitorId } }));\n setAddCompetitorId('');\n } catch (e) {\n setError(mutationErrorMessage(e, 'The competitor could not be linked.'));\n }\n };\n const onRemove = async (competitorId: string) => {\n setError(null);\n try {\n await throwOnError(unlink({ variables: { productId, competitorId } }));\n } catch (e) {\n setError(mutationErrorMessage(e, 'The competitor could not be unlinked.'));\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-competitors\">\n {competitors.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No competitors linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {competitors.map((comp) => (\n <li\n key={comp.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 flex-1 text-left truncate hover:underline\"\n onClick={() => navigate(detailPath('/products/competitor-detail-page', comp.id))}\n >\n {comp.name}\n {comp.website && <span className=\"ml-2 text-xs text-primary\">{comp.website}</span>}\n </button>\n <ConfirmButton\n label=\"\"\n ariaLabel={`Unlink ${comp.name} from this product`}\n testId=\"product-competitor-unlink\"\n variant=\"outline\"\n icon={<X className=\"h-4 w-4\" />}\n title={`Unlink ${comp.name}?`}\n description=\"The competitor stays in the workspace directory — only its link to this product is removed.\"\n confirmLabel=\"Unlink\"\n destructive\n disabled={unlinkState.loading}\n onConfirm={() => void onRemove(comp.id)}\n />\n </li>\n ))}\n </ul>\n )}\n <div className=\"flex items-start gap-2 pt-2 border-t\">\n <div className=\"flex-1\">\n <CompetitorSearchSelect\n id=\"product-add-competitor\"\n value={addCompetitorId}\n onChange={setAddCompetitorId}\n exclude={competitors.map((c) => c.id)}\n />\n </div>\n <Button\n size=\"sm\"\n data-testid=\"product-competitor-link\"\n disabled={!addCompetitorId || linkState.loading}\n onClick={() => void onAdd()}\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add\n </Button>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProductControls.js","names":[],"sources":["../../../src/planmagnet/product/ProductControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Checkbox,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Badge,\n} from '@burdenoff/fe-libs/ui';\nimport { Archive, ArchiveRestore, Pencil, Plus, Trash2, X } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { CompetitorSearchSelect } from '../controls/Pickers';\n\n/**\n * Product controls (BOFF-8045). A Product LINKS roadmaps, releases, feature requests and\n * OKR objectives — it never owns their rows (child FKs are nullable, `onDelete: SetNull`\n * server-side) and archiving/deleting a Product never deletes what it links to. Every link\n * mutation here is a pairwise link/unlink (mirrors planmagnetLinkKeyResultToWorkItem in the\n * okr module), not a batch \"set list\" — so adding several candidates at once fires one\n * mutation call per selection.\n */\n\n// ─── Shared refs ────────────────────────────────────────────────────────────\n\nexport interface ProductRoadmapRef {\n id: string;\n name: string;\n}\nexport interface ProductReleaseRef {\n id: string;\n name: string;\n version: string;\n status: string;\n}\nexport interface ProductFeatureRequestRef {\n id: string;\n title: string;\n status: string;\n}\nexport interface ProductObjectiveRef {\n id: string;\n title: string;\n status: string;\n}\nexport interface ProductCompetitorRef {\n id: string;\n name: string;\n description: string | null;\n website: string | null;\n}\n\n/** Every query that lists products or shows a product's detail/links. */\nexport const PRODUCT_QUERIES = ['GetProducts', 'GetProductForEdit', 'GetProductDetail'];\n\n// ─── Header actions (archive / unarchive / delete) ─────────────────────────\n\nconst DELETE_PRODUCT = gql`\n mutation ProductControlsDelete($id: ID!) {\n planmagnetDeleteProduct(id: $id)\n }\n`;\nconst ARCHIVE_PRODUCT = gql`\n mutation ProductControlsArchive($id: ID!) {\n planmagnetArchiveProduct(id: $id) {\n id\n archivedAt\n }\n }\n`;\nconst UNARCHIVE_PRODUCT = gql`\n mutation ProductControlsUnarchive($id: ID!) {\n planmagnetUnarchiveProduct(id: $id) {\n id\n archivedAt\n }\n }\n`;\n\nexport function ProductHeaderActions({\n productId,\n name,\n archivedAt,\n}: {\n productId: string;\n name: string;\n archivedAt: string | null;\n}) {\n const navigate = usePlanMagnetNavigate();\n const [remove, removeState] = useMutation(DELETE_PRODUCT, {\n refetchQueries: PRODUCT_QUERIES,\n context: LEAVES_PAGE_CONTEXT,\n });\n const [archive, archiveState] = useMutation(ARCHIVE_PRODUCT, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [unarchive, unarchiveState] = useMutation(UNARCHIVE_PRODUCT, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: productId } }));\n navigate('/products/products-page');\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The product could not be deleted.');\n }\n };\n\n const onToggleArchive = async () => {\n setError(null);\n try {\n if (archivedAt) {\n await throwOnError(unarchive({ variables: { id: productId } }));\n } else {\n await throwOnError(archive({ variables: { id: productId } }));\n }\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The product could not be updated.');\n }\n };\n\n const archiving = archiveState.loading || unarchiveState.loading;\n\n return (\n <div className=\"flex flex-col items-end gap-2\">\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n data-testid=\"product-toggle-archive\"\n disabled={archiving}\n onClick={() => void onToggleArchive()}\n >\n {archivedAt ? (\n <ArchiveRestore className=\"mr-1 h-4 w-4\" />\n ) : (\n <Archive className=\"mr-1 h-4 w-4\" />\n )}\n {archivedAt ? 'Unarchive' : 'Archive'}\n </Button>\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title={`Delete ${name}?`}\n description=\"Its features are deleted with it. Every linked roadmap, release, feature request and work item stays intact — only its link to this product or to its features is cleared — and removing its teams changes no one's access. This cannot be undone here.\"\n confirmLabel=\"Delete product\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onDelete()}\n />\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n// ─── Roadmaps panel ─────────────────────────────────────────────────────────\n\nconst LINK_PRODUCT_ROADMAP = gql`\n mutation ProductControlsLinkRoadmap($productId: ID!, $roadmapId: ID!) {\n planmagnetLinkProductRoadmap(productId: $productId, roadmapId: $roadmapId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_ROADMAP = gql`\n mutation ProductControlsUnlinkRoadmap($productId: ID!, $roadmapId: ID!) {\n planmagnetUnlinkProductRoadmap(productId: $productId, roadmapId: $roadmapId) {\n id\n }\n }\n`;\nconst PRODUCT_CANDIDATE_ROADMAPS = gql`\n query ProductControlsCandidateRoadmaps($workspaceId: ID!) {\n planmagnetRoadmaps(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\nexport function ProductRoadmapsPanel({\n productId,\n roadmaps,\n}: {\n productId: string;\n roadmaps: ProductRoadmapRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_ROADMAP, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (roadmapId: string) => {\n setError(null);\n setRemovingId(roadmapId);\n try {\n await throwOnError(unlink({ variables: { productId, roadmapId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The roadmap could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-roadmaps\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {roadmaps.length} roadmap{roadmaps.length === 1 ? '' : 's'}\n </p>\n <AddProductRoadmapsDialog productId={productId} existingIds={roadmaps.map((r) => r.id)} />\n </div>\n {error && <ErrorLine message={error} />}\n {roadmaps.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No roadmaps linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {roadmaps.map((roadmap) => (\n <li\n key={roadmap.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/products/roadmap-detail-page', roadmap.id))}\n >\n {roadmap.name}\n </button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${roadmap.name} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(roadmap.id)}\n >\n {removingId === roadmap.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductRoadmapsDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetRoadmaps: { items: ProductRoadmapRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_ROADMAPS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetRoadmaps.items ?? []).filter(\n (r) => !existingIds.includes(r.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_ROADMAP, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((roadmapId) =>\n throwOnError(link({ variables: { productId, roadmapId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The roadmaps could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-roadmaps\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add roadmap\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link roadmaps</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Linking a roadmap does not move or copy it — it stays exactly where it is, now also\n reachable from this product.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-roadmap-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every roadmap in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((roadmap) => {\n const inputId = `product-add-roadmap-${roadmap.id}`;\n return (\n <label\n key={roadmap.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(roadmap.id)}\n onCheckedChange={(checked) => toggle(roadmap.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">{roadmap.name}</span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-roadmaps-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} roadmap${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Releases panel ─────────────────────────────────────────────────────────\n\nconst LINK_PRODUCT_RELEASE = gql`\n mutation ProductControlsLinkRelease($productId: ID!, $releaseId: ID!) {\n planmagnetLinkProductRelease(productId: $productId, releaseId: $releaseId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_RELEASE = gql`\n mutation ProductControlsUnlinkRelease($productId: ID!, $releaseId: ID!) {\n planmagnetUnlinkProductRelease(productId: $productId, releaseId: $releaseId) {\n id\n }\n }\n`;\nconst PRODUCT_CANDIDATE_RELEASES = gql`\n query ProductControlsCandidateReleases($workspaceId: ID!) {\n planmagnetReleases(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n version\n status\n }\n totalCount\n }\n }\n`;\n\nexport function ProductReleasesPanel({\n productId,\n releases,\n}: {\n productId: string;\n releases: ProductReleaseRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_RELEASE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (releaseId: string) => {\n setError(null);\n setRemovingId(releaseId);\n try {\n await throwOnError(unlink({ variables: { productId, releaseId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The release could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-releases\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {releases.length} release{releases.length === 1 ? '' : 's'}\n </p>\n <AddProductReleasesDialog productId={productId} existingIds={releases.map((r) => r.id)} />\n </div>\n {error && <ErrorLine message={error} />}\n {releases.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No releases linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {releases.map((release) => (\n <li\n key={release.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/products/release-detail-page', release.id))}\n >\n <span className=\"text-muted-foreground mr-2\">{release.version}</span>\n {release.name}\n </button>\n <div className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {enumLabel(release.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${release.name} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(release.id)}\n >\n {removingId === release.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductReleasesDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetReleases: { items: ProductReleaseRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_RELEASES, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetReleases.items ?? []).filter(\n (r) => !existingIds.includes(r.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_RELEASE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((releaseId) =>\n throwOnError(link({ variables: { productId, releaseId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The releases could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-releases\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add release\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link releases</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Linking a release does not move or copy it — it stays exactly where it is, now also\n reachable from this product.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-release-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every release in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((release) => {\n const inputId = `product-add-release-${release.id}`;\n return (\n <label\n key={release.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(release.id)}\n onCheckedChange={(checked) => toggle(release.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground mr-2\">{release.version}</span>\n {release.name}\n </span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-releases-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} release${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Feature requests panel ─────────────────────────────────────────────────\n\nconst LINK_PRODUCT_FEATURE_REQUEST = gql`\n mutation ProductControlsLinkFeatureRequest($productId: ID!, $featureRequestId: ID!) {\n planmagnetLinkProductFeatureRequest(\n productId: $productId\n featureRequestId: $featureRequestId\n ) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_FEATURE_REQUEST = gql`\n mutation ProductControlsUnlinkFeatureRequest($productId: ID!, $featureRequestId: ID!) {\n planmagnetUnlinkProductFeatureRequest(\n productId: $productId\n featureRequestId: $featureRequestId\n ) {\n id\n }\n }\n`;\nconst PRODUCT_CANDIDATE_FEATURE_REQUESTS = gql`\n query ProductControlsCandidateFeatureRequests($workspaceId: ID!) {\n planmagnetFeatureRequests(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n title\n status\n }\n totalCount\n }\n }\n`;\n\nexport function ProductFeatureRequestsPanel({\n productId,\n featureRequests,\n}: {\n productId: string;\n featureRequests: ProductFeatureRequestRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_FEATURE_REQUEST, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (featureRequestId: string) => {\n setError(null);\n setRemovingId(featureRequestId);\n try {\n await throwOnError(unlink({ variables: { productId, featureRequestId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The feature request could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-feature-requests\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {featureRequests.length} feature request{featureRequests.length === 1 ? '' : 's'}\n </p>\n <AddProductFeatureRequestsDialog\n productId={productId}\n existingIds={featureRequests.map((f) => f.id)}\n />\n </div>\n {error && <ErrorLine message={error} />}\n {featureRequests.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No feature requests linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {featureRequests.map((feature) => (\n <li\n key={feature.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(detailPath('/products/feature-detail-page', feature.id))}\n >\n {feature.title}\n </button>\n <div className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {enumLabel(feature.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${feature.title} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(feature.id)}\n >\n {removingId === feature.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductFeatureRequestsDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetFeatureRequests: { items: ProductFeatureRequestRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_FEATURE_REQUESTS, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetFeatureRequests.items ?? []).filter(\n (f) => !existingIds.includes(f.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_FEATURE_REQUEST, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((featureRequestId) =>\n throwOnError(link({ variables: { productId, featureRequestId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The feature requests could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-feature-requests\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add feature request\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link feature requests</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Linking a feature request does not move or copy it — it stays exactly where it is, now\n also reachable from this product.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-feature-request-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every feature request in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((feature) => {\n const inputId = `product-add-feature-${feature.id}`;\n return (\n <label\n key={feature.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(feature.id)}\n onCheckedChange={(checked) => toggle(feature.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">{feature.title}</span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-feature-requests-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} request${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── Objectives (OKR) panel ─────────────────────────────────────────────────\n// Product<->Objective is a true many-to-many join (ProductObjectiveLink), same pairwise-link\n// shape as planmagnetLinkKeyResultToWorkItem. Unlink returns Boolean!, not Product! (§4).\n\nconst LINK_PRODUCT_OBJECTIVE = gql`\n mutation ProductControlsLinkObjective($productId: ID!, $objectiveId: ID!) {\n planmagnetLinkProductObjective(productId: $productId, objectiveId: $objectiveId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_OBJECTIVE = gql`\n mutation ProductControlsUnlinkObjective($productId: ID!, $objectiveId: ID!) {\n planmagnetUnlinkProductObjective(productId: $productId, objectiveId: $objectiveId)\n }\n`;\nconst PRODUCT_CANDIDATE_OBJECTIVES = gql`\n query ProductControlsCandidateObjectives($workspaceId: ID!) {\n planmagnetObjectives(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n title\n status\n }\n totalCount\n }\n }\n`;\n\nexport function ProductObjectivesPanel({\n productId,\n objectives,\n}: {\n productId: string;\n objectives: ProductObjectiveRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_OBJECTIVE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n const [removingId, setRemovingId] = useState<string | null>(null);\n\n const handleRemove = async (objectiveId: string) => {\n setError(null);\n setRemovingId(objectiveId);\n try {\n await throwOnError(unlink({ variables: { productId, objectiveId } }));\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The objective could not be unlinked.');\n } finally {\n setRemovingId(null);\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-objectives\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {objectives.length} objective{objectives.length === 1 ? '' : 's'}\n </p>\n <AddProductObjectivesDialog\n productId={productId}\n existingIds={objectives.map((o) => o.id)}\n />\n </div>\n {error && <ErrorLine message={error} />}\n {objectives.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No OKR objectives linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {objectives.map((objective) => (\n <li\n key={objective.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 truncate text-left hover:underline\"\n onClick={() => navigate(`/goals/objective-detail-page?objectiveId=${objective.id}`)}\n >\n {objective.title}\n </button>\n <div className=\"flex items-center gap-2 shrink-0\">\n <Badge variant=\"secondary\" className=\"text-xs capitalize\">\n {enumLabel(objective.status)}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Unlink ${objective.title} from product`}\n disabled={unlinkState.loading}\n onClick={() => void handleRemove(objective.id)}\n >\n {removingId === objective.id ? '…' : <X className=\"h-4 w-4\" />}\n </Button>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction AddProductObjectivesDialog({\n productId,\n existingIds,\n}: {\n productId: string;\n existingIds: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [open, setOpen] = useState(false);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [error, setError] = useState<string | null>(null);\n\n const { data, loading } = useQuery<{\n planmagnetObjectives: { items: ProductObjectiveRef[]; totalCount: number };\n }>(PRODUCT_CANDIDATE_OBJECTIVES, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !open || !workspaceId,\n });\n const candidates = (data?.planmagnetObjectives.items ?? []).filter(\n (o) => !existingIds.includes(o.id)\n );\n\n const [link, linkState] = useMutation(LINK_PRODUCT_OBJECTIVE, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const toggle = (id: string, checked: boolean) =>\n setSelected((prev) => {\n const next = new Set(prev);\n if (checked) next.add(id);\n else next.delete(id);\n return next;\n });\n\n const reset = () => {\n setSelected(new Set());\n setError(null);\n };\n\n const handleAdd = async () => {\n setError(null);\n try {\n await Promise.all(\n [...selected].map((objectiveId) =>\n throwOnError(link({ variables: { productId, objectiveId } }))\n )\n );\n reset();\n setOpen(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The objectives could not be linked.');\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) reset();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-add-objectives\">\n <Plus className=\"mr-1 h-4 w-4\" />\n Add objective\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Link OKR objectives</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Re-linking an objective that is already linked is a no-op, not an error.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div\n className=\"max-h-80 overflow-y-auto space-y-1\"\n data-testid=\"product-add-objective-candidates\"\n >\n {loading && <p className=\"text-sm text-muted-foreground py-4 text-center\">Loading…</p>}\n {!loading && candidates.length === 0 && (\n <p className=\"text-sm text-muted-foreground py-4 text-center\">\n Every objective in this workspace is already linked to this product.\n </p>\n )}\n {candidates.map((objective) => {\n const inputId = `product-add-objective-${objective.id}`;\n return (\n <label\n key={objective.id}\n htmlFor={inputId}\n className=\"flex items-center gap-3 border rounded-md p-2 text-sm cursor-pointer hover:bg-muted\"\n >\n <Checkbox\n id={inputId}\n checked={selected.has(objective.id)}\n onCheckedChange={(checked) => toggle(objective.id, checked === true)}\n />\n <span className=\"min-w-0 flex-1 truncate\">{objective.title}</span>\n </label>\n );\n })}\n </div>\n {error && <ErrorLine message={error} />}\n <ResponsiveDialogFooter>\n <Button\n onClick={() => void handleAdd()}\n disabled={selected.size === 0 || linkState.loading}\n data-testid=\"product-add-objectives-confirm\"\n >\n {linkState.loading\n ? 'Linking…'\n : `Link ${selected.size} objective${selected.size === 1 ? '' : 's'}`}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ─── About this product (PRM-02) ────────────────────────────────────────────\n\nconst UPDATE_PRODUCT_INFO = gql`\n mutation ProductControlsUpdateInfo($id: ID!, $input: PlanmagnetUpdateProductInput!) {\n planmagnetUpdateProduct(id: $id, input: $input) {\n id\n mission\n valueProposition\n businessModel\n targetSegments\n }\n }\n`;\n\nconst MAX_ABOUT_TEXT = 2000;\nconst MAX_SEGMENTS = 20;\nconst MAX_SEGMENT_LENGTH = 120;\n\nexport interface ProductAboutValues {\n id: string;\n mission: string | null;\n valueProposition: string | null;\n businessModel: string | null;\n targetSegments: string[];\n}\n\n/**\n * A tag-style editor for `Product.targetSegments` (PRM-02): type a segment, Enter or the + button adds it as a\n * chip, its own X removes it. The server enforces at most 20 entries of at most 120 characters each — this\n * enforces the same limits client-side so a refusal never reaches the server for an obviously-oversized list.\n */\nfunction SegmentsEditor({\n value,\n onChange,\n disabled,\n}: {\n value: string[];\n onChange: (next: string[]) => void;\n disabled?: boolean;\n}) {\n const [draft, setDraft] = useState('');\n const atLimit = value.length >= MAX_SEGMENTS;\n const trimmed = draft.trim();\n const canAdd =\n !disabled &&\n !atLimit &&\n trimmed !== '' &&\n trimmed.length <= MAX_SEGMENT_LENGTH &&\n !value.includes(trimmed);\n\n const add = () => {\n if (!canAdd) return;\n onChange([...value, trimmed]);\n setDraft('');\n };\n const remove = (segment: string) => onChange(value.filter((s) => s !== segment));\n\n return (\n <div className=\"space-y-2\" data-testid=\"product-segments\">\n <div className=\"flex flex-wrap gap-1\">\n {value.length === 0 && (\n <span className=\"text-xs text-muted-foreground\">No target segments yet.</span>\n )}\n {value.map((segment) => (\n <Badge key={segment} variant=\"secondary\" className=\"gap-1 pr-1\">\n {segment}\n {!disabled && (\n <button\n type=\"button\"\n aria-label={`Remove ${segment}`}\n onClick={() => remove(segment)}\n className=\"rounded-full hover:bg-muted\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n )}\n </Badge>\n ))}\n </div>\n {!disabled && (\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label=\"Add a target segment\"\n placeholder={atLimit ? `Limit reached (${MAX_SEGMENTS})` : 'e.g. Mid-market SaaS teams'}\n value={draft}\n disabled={atLimit}\n maxLength={MAX_SEGMENT_LENGTH}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n add();\n }\n }}\n />\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n aria-label=\"Add segment\"\n disabled={!canAdd}\n onClick={add}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n )}\n {!disabled && (\n <p className=\"text-xs text-muted-foreground\">\n {value.length}/{MAX_SEGMENTS} segments, up to {MAX_SEGMENT_LENGTH} characters each.\n </p>\n )}\n </div>\n );\n}\n\n/**\n * The product detail page's \"About this product\" section (PRM-02): mission, value proposition, business model\n * and target segments, shown read-only with an inline \"Edit\" toggle for the four fields. Mirrors\n * `FeaturePrioritizationCard`'s own stays-mounted-across-save shape (not a dialog) so a validation refusal from\n * the server surfaces right where the fields are, and the saved values show immediately without navigating.\n */\nexport function ProductAboutPanel({ product }: { product: ProductAboutValues }) {\n const initial = {\n mission: product.mission ?? '',\n valueProposition: product.valueProposition ?? '',\n businessModel: product.businessModel ?? '',\n targetSegments: product.targetSegments,\n };\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState(initial);\n const [error, setError] = useState<string | null>(null);\n const [update, state] = useMutation(UPDATE_PRODUCT_INFO, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n\n const startEdit = () => {\n setForm(initial);\n setError(null);\n setEditing(true);\n };\n\n const textOverLimit = [form.mission, form.valueProposition, form.businessModel].some(\n (v) => v.length > MAX_ABOUT_TEXT\n );\n const segmentsValid =\n form.targetSegments.length <= MAX_SEGMENTS &&\n form.targetSegments.every((s) => s.length <= MAX_SEGMENT_LENGTH);\n const canSave = !textOverLimit && segmentsValid && !state.loading;\n\n const save = async () => {\n setError(null);\n const text = (v: string) => (v.trim() ? v.trim() : null);\n try {\n await throwOnError(\n update({\n variables: {\n id: product.id,\n input: {\n mission: text(form.mission),\n valueProposition: text(form.valueProposition),\n businessModel: text(form.businessModel),\n targetSegments: form.targetSegments,\n },\n },\n })\n );\n setEditing(false);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The product information could not be saved.'));\n }\n };\n\n const textareaClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[72px]';\n\n return (\n <Card data-testid=\"product-info\">\n <CardHeader className=\"flex flex-row items-center justify-between gap-2\">\n <CardTitle>About this product</CardTitle>\n {!editing && (\n <Button size=\"sm\" variant=\"outline\" data-testid=\"product-info-edit\" onClick={startEdit}>\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {!editing ? (\n <>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Mission</p>\n <p className=\"text-sm whitespace-pre-wrap\">{product.mission || 'Not set'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Value proposition</p>\n <p className=\"text-sm whitespace-pre-wrap\">{product.valueProposition || 'Not set'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Business model</p>\n <p className=\"text-sm whitespace-pre-wrap\">{product.businessModel || 'Not set'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground\">Target segments</p>\n <div className=\"mt-1\">\n <SegmentsEditor value={product.targetSegments} onChange={() => {}} disabled />\n </div>\n </div>\n </>\n ) : (\n <>\n <div>\n <label htmlFor=\"product-info-mission\" className=\"text-sm font-medium\">\n Mission\n </label>\n <textarea\n id=\"product-info-mission\"\n className={textareaClass}\n maxLength={MAX_ABOUT_TEXT}\n value={form.mission}\n onChange={(e) => setForm({ ...form, mission: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"product-info-value-proposition\" className=\"text-sm font-medium\">\n Value proposition\n </label>\n <textarea\n id=\"product-info-value-proposition\"\n className={textareaClass}\n maxLength={MAX_ABOUT_TEXT}\n value={form.valueProposition}\n onChange={(e) => setForm({ ...form, valueProposition: e.target.value })}\n />\n </div>\n <div>\n <label htmlFor=\"product-info-business-model\" className=\"text-sm font-medium\">\n Business model\n </label>\n <textarea\n id=\"product-info-business-model\"\n className={textareaClass}\n maxLength={MAX_ABOUT_TEXT}\n value={form.businessModel}\n onChange={(e) => setForm({ ...form, businessModel: e.target.value })}\n />\n </div>\n <div>\n <label className=\"text-sm font-medium\">Target segments</label>\n <div className=\"mt-1\">\n <SegmentsEditor\n value={form.targetSegments}\n onChange={(targetSegments) => setForm({ ...form, targetSegments })}\n />\n </div>\n </div>\n {error && <ErrorLine message={error} />}\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(false)}>\n Cancel\n </Button>\n <Button\n size=\"sm\"\n data-testid=\"product-info-save\"\n disabled={!canSave}\n onClick={() => void save()}\n >\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n );\n}\n\n// ─── Competitive landscape (PRM-02) ─────────────────────────────────────────\n\nconst LINK_PRODUCT_COMPETITOR = gql`\n mutation ProductControlsLinkCompetitor($productId: ID!, $competitorId: ID!) {\n planmagnetLinkProductCompetitor(productId: $productId, competitorId: $competitorId) {\n id\n }\n }\n`;\nconst UNLINK_PRODUCT_COMPETITOR = gql`\n mutation ProductControlsUnlinkCompetitor($productId: ID!, $competitorId: ID!) {\n planmagnetUnlinkProductCompetitor(productId: $productId, competitorId: $competitorId)\n }\n`;\n\n/**\n * The product detail page's \"Competitive landscape\" panel (PRM-02): the competitors linked to this product, a\n * picker to link another from the workspace directory (`CompetitorSearchSelect`, shared with the feature\n * request detail page's own competitor panel), and an unlink confirm per row — unlike the feature request\n * panel's plain X, PRM-02 asks for a confirm step here since a product's competitive landscape is reviewed\n * material, not a quick per-request note.\n */\nexport function ProductCompetitorsPanel({\n productId,\n competitors,\n}: {\n productId: string;\n competitors: ProductCompetitorRef[];\n}) {\n const navigate = usePlanMagnetNavigate();\n const [addCompetitorId, setAddCompetitorId] = useState('');\n const [link, linkState] = useMutation(LINK_PRODUCT_COMPETITOR, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [unlink, unlinkState] = useMutation(UNLINK_PRODUCT_COMPETITOR, {\n refetchQueries: PRODUCT_QUERIES,\n awaitRefetchQueries: true,\n });\n const [error, setError] = useState<string | null>(null);\n\n const onAdd = async () => {\n if (!addCompetitorId) return;\n setError(null);\n try {\n await throwOnError(link({ variables: { productId, competitorId: addCompetitorId } }));\n setAddCompetitorId('');\n } catch (e) {\n setError(mutationErrorMessage(e, 'The competitor could not be linked.'));\n }\n };\n const onRemove = async (competitorId: string) => {\n setError(null);\n try {\n await throwOnError(unlink({ variables: { productId, competitorId } }));\n } catch (e) {\n setError(mutationErrorMessage(e, 'The competitor could not be unlinked.'));\n }\n };\n\n return (\n <div className=\"space-y-3\" data-testid=\"product-competitors\">\n {competitors.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No competitors linked to this product yet.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {competitors.map((comp) => (\n <li\n key={comp.id}\n className=\"flex items-center justify-between gap-2 border rounded-md p-2 text-sm\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 flex-1 text-left truncate hover:underline\"\n onClick={() => navigate(detailPath('/products/competitor-detail-page', comp.id))}\n >\n {comp.name}\n {comp.website && <span className=\"ml-2 text-xs text-primary\">{comp.website}</span>}\n </button>\n <ConfirmButton\n label=\"\"\n ariaLabel={`Unlink ${comp.name} from this product`}\n testId=\"product-competitor-unlink\"\n variant=\"outline\"\n icon={<X className=\"h-4 w-4\" />}\n title={`Unlink ${comp.name}?`}\n description=\"The competitor stays in the workspace directory — only its link to this product is removed.\"\n confirmLabel=\"Unlink\"\n destructive\n disabled={unlinkState.loading}\n onConfirm={() => void onRemove(comp.id)}\n />\n </li>\n ))}\n </ul>\n )}\n <div className=\"flex items-start gap-2 pt-2 border-t\">\n <div className=\"flex-1\">\n <CompetitorSearchSelect\n id=\"product-add-competitor\"\n value={addCompetitorId}\n onChange={setAddCompetitorId}\n exclude={competitors.map((c) => c.id)}\n />\n </div>\n <Button\n size=\"sm\"\n data-testid=\"product-competitor-link\"\n disabled={!addCompetitorId || linkState.loading}\n onClick={() => void onAdd()}\n >\n <Plus className=\"mr-1 h-4 w-4\" />\n Add\n </Button>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product features (PRM-01): what a product team decided to build — not a feature request, which is customer
|
|
3
|
+
* feedback asking for something and may point at the feature that addresses it. A feature belongs to one product
|
|
4
|
+
* for good, links EPIC work items (stories hang off their epics) and the requests it answers. Archiving keeps every
|
|
5
|
+
* link; deleting removes only the links — never a request or a work item.
|
|
6
|
+
*/
|
|
7
|
+
export interface ProductFeatureValues {
|
|
8
|
+
id: string;
|
|
9
|
+
productId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string | null;
|
|
12
|
+
status: string;
|
|
13
|
+
ownerId: string | null;
|
|
14
|
+
archivedAt: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function ProductFeatureStatusBadge({ status }: {
|
|
17
|
+
status: string;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
/**
|
|
20
|
+
* Create a feature under `productId` (no `feature`) or edit one: name, description, status and owner. An edit sends
|
|
21
|
+
* only what changed; an emptied description or a cleared owner is sent as null, which clears it. Status always has
|
|
22
|
+
* a value — the service refuses to clear it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ProductFeatureFormDialog({ productId, feature, disabled, }: {
|
|
25
|
+
productId: string;
|
|
26
|
+
feature?: ProductFeatureValues;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export declare function ProductFeatureHeaderActions({ feature, epicCount, requestCount, }: {
|
|
30
|
+
feature: ProductFeatureValues;
|
|
31
|
+
epicCount: number;
|
|
32
|
+
requestCount: number;
|
|
33
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
/** The product page's Features section: list with status, "Show archived", and "New feature". */
|
|
35
|
+
export declare function ProductFeaturesPanel({ productId, productArchived, }: {
|
|
36
|
+
productId: string;
|
|
37
|
+
productArchived: boolean;
|
|
38
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=ProductFeatureControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductFeatureControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/product/ProductFeatureControls.tsx"],"names":[],"mappings":"AAsCA;;;;;GAKG;AAEH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAiBD,wBAAgB,yBAAyB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAUvE;AAID;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,SAAS,EACT,OAAO,EACP,QAAgB,GACjB,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,2CAiMA;AAID,wBAAgB,2BAA2B,CAAC,EAC1C,OAAO,EACP,SAAS,EACT,YAAY,GACb,EAAE;IACD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB,2CAwFA;AAiBD,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,eAAe,GAChB,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;CAC1B,2CAwHA"}
|