@burdenoff/microfe-planmagnet 2026.1001.5 → 2026.1001.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoutes.js +10 -6
  3. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  4. package/dist/planmagnet/automation/automationRuleTypes.d.ts +31 -19
  5. package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -1
  6. package/dist/planmagnet/automation/automationRuleTypes.js +4 -8
  7. package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -1
  8. package/dist/planmagnet/controls/CompetitorControls.d.ts +71 -0
  9. package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/CompetitorControls.js +346 -12
  11. package/dist/planmagnet/controls/CompetitorControls.js.map +1 -1
  12. package/dist/planmagnet/controls/FeatureControls.d.ts +23 -1
  13. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  14. package/dist/planmagnet/controls/FeatureControls.js +431 -229
  15. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  16. package/dist/planmagnet/controls/FeedbackControls.d.ts +70 -0
  17. package/dist/planmagnet/controls/FeedbackControls.d.ts.map +1 -0
  18. package/dist/planmagnet/controls/FeedbackControls.js +153 -0
  19. package/dist/planmagnet/controls/FeedbackControls.js.map +1 -0
  20. package/dist/planmagnet/controls/Pickers.d.ts +15 -0
  21. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  22. package/dist/planmagnet/controls/Pickers.js +61 -1
  23. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  24. package/dist/planmagnet/controls/PrioritizationControls.d.ts +36 -0
  25. package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -1
  26. package/dist/planmagnet/controls/PrioritizationControls.js +118 -1
  27. package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -1
  28. package/dist/planmagnet/controls/SyncControls.d.ts.map +1 -1
  29. package/dist/planmagnet/controls/SyncControls.js +1 -0
  30. package/dist/planmagnet/controls/SyncControls.js.map +1 -1
  31. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts +47 -0
  32. package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts.map +1 -0
  33. package/dist/planmagnet/controls/WebhookSubscriptionControls.js +542 -0
  34. package/dist/planmagnet/controls/WebhookSubscriptionControls.js.map +1 -0
  35. package/dist/planmagnet/dashboards/DashboardUI.js +8 -8
  36. package/dist/planmagnet/dashboards/cards/RiskSeverityCard.js +4 -4
  37. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
  38. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +67 -59
  39. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
  40. package/dist/planmagnet/forecast/DeliveryForecastCard.js +8 -8
  41. package/dist/planmagnet/forecast/ProjectOutlookCard.js +10 -10
  42. package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +9 -9
  43. package/dist/planmagnet/forecast/ResourceForecastCard.js +6 -6
  44. package/dist/planmagnet/integrations/syncTypes.d.ts +2 -1
  45. package/dist/planmagnet/integrations/syncTypes.d.ts.map +1 -1
  46. package/dist/planmagnet/integrations/syncTypes.js +19 -9
  47. package/dist/planmagnet/integrations/syncTypes.js.map +1 -1
  48. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
  49. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +290 -283
  50. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  51. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -1
  52. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +126 -63
  53. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -1
  54. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -1
  55. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +14 -0
  56. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -1
  57. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -1
  58. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +93 -57
  59. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -1
  60. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -1
  61. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +125 -83
  62. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -1
  63. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +23 -23
  64. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  65. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +340 -248
  66. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  67. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.d.ts.map +1 -1
  68. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js +172 -167
  69. package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js.map +1 -1
  70. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.d.ts.map +1 -1
  71. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +92 -83
  72. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js.map +1 -1
  73. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts +2 -0
  74. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts.map +1 -0
  75. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js +175 -0
  76. package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js.map +1 -0
  77. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
  78. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +226 -126
  79. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
  80. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  81. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +151 -126
  82. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  83. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  84. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +169 -122
  85. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  86. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  87. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +165 -109
  88. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  89. package/dist/planmagnet/pages/ProductsProductsPagePage.js +13 -13
  90. package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +9 -9
  91. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +8 -8
  92. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  93. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +15 -3
  94. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  95. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +12 -12
  96. package/dist/planmagnet/pages/ReportsReportsPagePage.js +23 -23
  97. package/dist/planmagnet/pages/TasksMyDashboardPagePage.js +18 -18
  98. package/dist/planmagnet/pages/TasksMyWorkPagePage.js +10 -10
  99. package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +5 -5
  100. package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
  101. package/dist/planmagnet/pages/TeamTeamPagePage.js +59 -53
  102. package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
  103. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -8
  104. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +6 -6
  105. package/dist/planmagnet/pages/analytics/MetricsCard.d.ts +8 -0
  106. package/dist/planmagnet/pages/analytics/MetricsCard.d.ts.map +1 -0
  107. package/dist/planmagnet/pages/analytics/MetricsCard.js +164 -0
  108. package/dist/planmagnet/pages/analytics/MetricsCard.js.map +1 -0
  109. package/dist/planmagnet/pages/analytics/QualityMetricsCard.js +6 -6
  110. package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +8 -8
  111. package/dist/planmagnet/pages/analytics/StatusFlowCard.js +7 -7
  112. package/dist/planmagnet/pages/analytics/ThroughputCard.js +7 -7
  113. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +7 -7
  114. package/dist/planmagnet/people/People.d.ts +14 -0
  115. package/dist/planmagnet/people/People.d.ts.map +1 -1
  116. package/dist/planmagnet/people/People.js +36 -22
  117. package/dist/planmagnet/people/People.js.map +1 -1
  118. package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts +2 -0
  119. package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts.map +1 -0
  120. package/dist/planmagnet/team/UnmatchedCrewIdentities.js +155 -0
  121. package/dist/planmagnet/team/UnmatchedCrewIdentities.js.map +1 -0
  122. package/dist/planmagnet/webhooks/webhookTypes.d.ts +54 -0
  123. package/dist/planmagnet/webhooks/webhookTypes.d.ts.map +1 -0
  124. package/dist/planmagnet/webhooks/webhookTypes.js +17 -0
  125. package/dist/planmagnet/webhooks/webhookTypes.js.map +1 -0
  126. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SyncControls.js","names":[],"sources":["../../../src/planmagnet/controls/SyncControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n Input,\n Textarea,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n ChevronDown,\n ChevronUp,\n Loader2,\n Pause,\n Pencil,\n Play,\n PlugZap,\n X,\n} from 'lucide-react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { enumLabel } from './WorkItemControls';\nimport { useSyncProviderCapabilities } from '../integrations/useSyncProviderCapabilities';\nimport {\n CONFLICT_RESOLUTIONS,\n directionLabel,\n directionSupported,\n GITHUB_FIELD_MAPPING_DEFAULTS,\n isValidGithubOwner,\n isValidGithubRepo,\n parseGithubFieldMapping,\n SYNC_DIRECTIONS,\n type ConflictResolution,\n type RunHealth,\n type SyncConnectionTestResult,\n type SyncDirection,\n type SyncRunResult,\n type SyncStatus,\n} from '../integrations/syncTypes';\n\n/**\n * Shared row/detail controls for External Sync (AUT-09/10/24/40): status badges, health, run/pause/resume/test\n * actions and conflict resolution. Every write here follows the same two rules the rest of this MFE does\n * (`utils/mutationResult.ts`, `utils/graphqlErrors.ts`): a refused mutation is surfaced, never swallowed, and a\n * FORBIDDEN refusal reads as \"your role can view but not change this\" rather than a raw GraphQL error string —\n * this page cannot know ahead of time whether the viewer's role allows a given write, so it always offers the\n * action and reports the refusal if and when it happens, rather than guessing at a client-side role check.\n */\n\nexport const SYNC_LIST_QUERIES = ['SyncsList'];\nexport const SYNC_DETAIL_QUERIES = ['SyncDetail', 'SyncRuns', 'SyncConflicts'];\n\nconst STATUS_VARIANT: Record<\n SyncStatus,\n 'default' | 'success' | 'warning' | 'info' | 'secondary' | 'destructive' | 'outline'\n> = {\n IDLE: 'secondary',\n SYNCING: 'info',\n SUCCESS: 'success',\n FAILED: 'destructive',\n PAUSED: 'warning',\n DISABLED: 'outline',\n};\n\nexport function SyncStatusBadge({ status }: { status: string }) {\n return (\n <Badge variant={STATUS_VARIANT[status as SyncStatus] ?? 'outline'} className=\"text-xs\">\n {enumLabel(status)}\n </Badge>\n );\n}\n\nconst RUN_STATUS_VARIANT: Record<string, 'success' | 'destructive' | 'warning'> = {\n SUCCEEDED: 'success',\n FAILED: 'destructive',\n PARTIAL: 'warning',\n};\n\nexport function SyncRunStatusBadge({ status }: { status: string }) {\n return (\n <Badge variant={RUN_STATUS_VARIANT[status] ?? 'secondary'} className=\"text-xs\">\n {enumLabel(status)}\n </Badge>\n );\n}\n\nexport function ConflictStatusBadge({ status }: { status: string }) {\n return (\n <Badge variant={status === 'OPEN' ? 'warning' : 'secondary'} className=\"text-xs\">\n {enumLabel(status)}\n </Badge>\n );\n}\n\n/** A long string (a sync's `lastSyncError`, an issue body) collapsed to `max` chars with an expand toggle. */\nexport function TruncatedText({\n text,\n max = 140,\n className = 'text-muted-foreground',\n testId,\n}: {\n text: string;\n max?: number;\n className?: string;\n testId?: string;\n}) {\n const [expanded, setExpanded] = useState(false);\n if (text.length <= max) return <span className={className}>{text}</span>;\n return (\n <span data-testid={testId}>\n <span className={className}>{expanded ? text : `${text.slice(0, max)}…`}</span>{' '}\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-0.5 text-xs font-medium text-primary hover:underline\"\n aria-expanded={expanded}\n onClick={() => setExpanded((v) => !v)}\n >\n {expanded ? (\n <>\n Show less <ChevronUp className=\"h-3 w-3\" aria-hidden=\"true\" />\n </>\n ) : (\n <>\n Show more <ChevronDown className=\"h-3 w-3\" aria-hidden=\"true\" />\n </>\n )}\n </button>\n </span>\n );\n}\n\n/**\n * Last run time/status + consecutive-failure streak (AUT-09/22). `consecutiveFailures` is 0 once the latest run\n * did not fail — highlighted only while it is actually nonzero, never merely because the last run failed once\n * and then recovered.\n */\nexport function SyncHealthSummary({ health }: { health: RunHealth }) {\n return (\n <div className=\"flex flex-wrap items-center gap-2 text-xs\" data-testid=\"sync-health\">\n {health.lastRunAt ? (\n <>\n <span className=\"text-muted-foreground\">\n Last run {new Date(health.lastRunAt).toLocaleString()}\n </span>\n {health.lastStatus && <SyncRunStatusBadge status={health.lastStatus} />}\n </>\n ) : (\n <span className=\"text-muted-foreground\">Never run</span>\n )}\n {health.consecutiveFailures > 0 && (\n <span className=\"inline-flex items-center gap-1 font-medium text-status-error-text\">\n <AlertTriangle className=\"h-3 w-3\" aria-hidden=\"true\" />\n {health.consecutiveFailures} run{health.consecutiveFailures === 1 ? '' : 's'} failed in a\n row\n </span>\n )}\n <span className=\"text-muted-foreground\">\n {health.runsLast7Days} run(s) in the last 7 days\n </span>\n </div>\n );\n}\n\nconst START_SYNC = gql`\n mutation SyncControlsStartSync($id: ID!) {\n planmagnetStartSync(id: $id) {\n syncId\n success\n itemsCreated\n itemsUpdated\n itemsSkipped\n errorCount\n errors\n conflictCount\n durationMs\n syncedAt\n }\n }\n`;\n\n/** \"Run now\" (AUT-40): starts a sync in the direction `ExternalSync.direction` already records, and reports the\n * real result — including an honest `NOT_IMPLEMENTED` outcome for a provider with no live connector, never a\n * fabricated success. */\nexport function RunNowButton({\n syncId,\n onResult,\n size = 'sm',\n}: {\n syncId: string;\n onResult?: (result: SyncRunResult) => void;\n size?: 'sm' | 'default';\n}) {\n const [start, state] = useMutation<{ planmagnetStartSync: SyncRunResult }>(START_SYNC, {\n refetchQueries: [...SYNC_LIST_QUERIES, ...SYNC_DETAIL_QUERIES],\n });\n const [error, setError] = useState<string | null>(null);\n\n const run = async () => {\n setError(null);\n try {\n const res = await throwOnError(start({ variables: { id: syncId } }));\n if (res.data) onResult?.(res.data.planmagnetStartSync);\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The sync could not be started.')\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\">\n <Button\n size={size}\n variant=\"outline\"\n disabled={state.loading}\n onClick={() => void run()}\n data-testid=\"sync-run-now\"\n >\n {state.loading ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <PlugZap className=\"mr-1 h-4 w-4\" />\n )}\n Run now\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * What `planmagnetStartSync` actually did (AUT-40). A completed run is a normal GraphQL success even when the\n * sync itself failed on every item — bad token, unreachable provider, a refused status transition, … — the\n * service records `success: false` with `errors` populated rather than throwing (see\n * `sync.service.ts`'s `runGithubImport`/`runGithubExport`). This is the one place that outcome is rendered, so\n * every `RunNowButton` call site wires its `onResult` here instead of relying on a delayed list/detail refetch.\n */\nexport function SyncRunResultSummary({ result }: { result: SyncRunResult }) {\n return (\n <div\n role=\"status\"\n className={`space-y-1 rounded-md border p-3 text-sm ${\n result.success\n ? 'border-border bg-muted text-status-success-text'\n : 'border-destructive/40 text-status-error-text'\n }`}\n data-testid=\"sync-run-result\"\n >\n <p className=\"font-medium\">{result.success ? 'Sync run succeeded.' : 'Sync run failed.'}</p>\n <p className=\"text-xs text-muted-foreground\">\n {result.itemsCreated} created · {result.itemsUpdated} updated · {result.itemsSkipped}{' '}\n skipped\n {result.conflictCount > 0\n ? ` · ${result.conflictCount} conflict${result.conflictCount === 1 ? '' : 's'}`\n : ''}\n </p>\n {result.errors && result.errors.length > 0 && (\n <ul className=\"list-disc space-y-0.5 pl-4 text-xs\">\n {result.errors.map((message, i) => (\n <li key={i}>{message}</li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nconst PAUSE_SYNC = gql`\n mutation SyncControlsPause($id: ID!) {\n planmagnetPauseExternalSync(id: $id) {\n id\n status\n }\n }\n`;\nconst RESUME_SYNC = gql`\n mutation SyncControlsResume($id: ID!) {\n planmagnetResumeExternalSync(id: $id) {\n id\n status\n }\n }\n`;\n\n/** Pause when active, Resume when PAUSED — the only two states these two mutations move between. */\nexport function PauseResumeButton({ syncId, status }: { syncId: string; status: string }) {\n const [pause, pauseState] = useMutation(PAUSE_SYNC, { refetchQueries: SYNC_LIST_QUERIES });\n const [resume, resumeState] = useMutation(RESUME_SYNC, { refetchQueries: SYNC_LIST_QUERIES });\n const [error, setError] = useState<string | null>(null);\n const loading = pauseState.loading || resumeState.loading;\n const paused = status === 'PAUSED';\n\n const toggle = async () => {\n setError(null);\n try {\n if (paused) {\n await throwOnError(resume({ variables: { id: syncId } }));\n } else {\n await throwOnError(pause({ variables: { id: syncId } }));\n }\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, `The sync could not be ${paused ? 'resumed' : 'paused'}.`)\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={() => void toggle()}\n data-testid={paused ? 'sync-resume' : 'sync-pause'}\n >\n {loading ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : paused ? (\n <Play className=\"mr-1 h-4 w-4\" />\n ) : (\n <Pause className=\"mr-1 h-4 w-4\" />\n )}\n {paused ? 'Resume' : 'Pause'}\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst TEST_SYNC = gql`\n mutation SyncControlsTest($id: ID!) {\n planmagnetTestExternalSync(id: $id) {\n ok\n message\n rateLimitRemaining\n }\n }\n`;\n\n/** AUT-23/20 — probes the connection through the real connector, writing nothing. Offered whenever the page can\n * reach it; a provider with no live connector still answers, honestly, with `ok: false`. */\nexport function TestConnectionButton({ syncId }: { syncId: string }) {\n const [test, state] = useMutation<{ planmagnetTestExternalSync: SyncConnectionTestResult }>(\n TEST_SYNC\n );\n const [result, setResult] = useState<SyncConnectionTestResult | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const run = async () => {\n setError(null);\n setResult(null);\n try {\n const res = await throwOnError(test({ variables: { id: syncId } }));\n if (res.data) setResult(res.data.planmagnetTestExternalSync);\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The connection could not be tested.')\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={state.loading}\n onClick={() => void run()}\n data-testid=\"sync-test-connection\"\n >\n {state.loading ? <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" /> : null}\n Test connection\n </Button>\n {result && (\n <p\n className={`text-sm ${result.ok ? 'text-status-success-text' : 'text-status-error-text'}`}\n data-testid=\"sync-test-result\"\n >\n {result.ok ? 'Connected: ' : 'Failed: '}\n {result.message}\n {result.rateLimitRemaining !== null && ` (${result.rateLimitRemaining} calls remaining)`}\n </p>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst DELETE_SYNC = gql`\n mutation SyncControlsDelete($id: ID!) {\n planmagnetDeleteExternalSync(id: $id)\n }\n`;\n\nexport function DeleteSyncButton({\n syncId,\n label,\n /** Passed by the detail page, which leaves for the list afterward — see LEAVES_PAGE_CONTEXT's own doc. */\n onDeleted,\n}: {\n syncId: string;\n label: string;\n onDeleted?: () => void;\n}) {\n const [remove, { loading }] = useMutation(DELETE_SYNC, { refetchQueries: SYNC_LIST_QUERIES });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: syncId } }));\n onDeleted?.();\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The sync could not be deleted.')\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<X className=\"mr-1 h-4 w-4\" />}\n title={`Delete \"${label}\"?`}\n description=\"This stops the sync and removes its configuration and run/conflict history. Work items already created from it are kept.\"\n confirmLabel=\"Delete sync\"\n destructive\n disabled={loading}\n onConfirm={() => void onDelete()}\n testId=\"sync-delete\"\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst RESOLVE_CONFLICT = gql`\n mutation SyncControlsResolveConflict($id: ID!, $resolution: PlanmagnetSyncConflictResolution!) {\n planmagnetResolveSyncConflict(id: $id, resolution: $resolution) {\n id\n status\n resolution\n resolvedAt\n resolvedBy\n }\n }\n`;\n\n/** AUT-10/24 — SOURCE_WINS/TARGET_WINS actually push/pull the winning side's data; MANUAL just closes the flag.\n * A refused or failed resolution (e.g. the provider call itself fails) throws server-side and leaves the\n * conflict OPEN rather than recording a resolution that never happened — so on error this shows the reason and\n * the conflict stays in the list exactly as it was. */\nexport function ConflictResolveActions({ conflictId }: { conflictId: string }) {\n const [resolve, state] = useMutation(RESOLVE_CONFLICT, { refetchQueries: SYNC_DETAIL_QUERIES });\n const [error, setError] = useState<string | null>(null);\n const [pending, setPending] = useState<ConflictResolution | null>(null);\n\n const act = async (resolution: ConflictResolution) => {\n setError(null);\n setPending(resolution);\n try {\n await throwOnError(resolve({ variables: { id: conflictId, resolution } }));\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The conflict could not be resolved.')\n );\n } finally {\n setPending(null);\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\" data-testid=\"sync-conflict-resolve\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n size=\"sm\"\n disabled={state.loading}\n onClick={() => void act('SOURCE_WINS')}\n data-testid=\"sync-conflict-keep-local\"\n >\n {pending === 'SOURCE_WINS' && <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />}\n Keep PlanMagnet\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={state.loading}\n onClick={() => void act('TARGET_WINS')}\n data-testid=\"sync-conflict-take-provider\"\n >\n {pending === 'TARGET_WINS' && <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />}\n Take provider\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={state.loading}\n onClick={() => void act('MANUAL')}\n data-testid=\"sync-conflict-dismiss\"\n >\n {pending === 'MANUAL' && <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />}\n Dismiss (manual)\n </Button>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nexport const CONFLICT_STRATEGY_EXPLANATION: Record<ConflictResolution, string> = {\n SOURCE_WINS:\n 'PlanMagnet wins — the local work item overwrites the provider on a detected conflict.',\n TARGET_WINS: 'Provider wins — the external tool overwrites PlanMagnet on a detected conflict.',\n MANUAL: 'Flag and skip — a conflict is recorded and left alone until resolved here.',\n};\n\nconst FIELD = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n\nconst GET_SYNC_CONNECTION = gql`\n query SyncControlsConnection($id: ID!) {\n planmagnetExternalSync(id: $id) {\n id\n connectionConfig\n }\n }\n`;\n\nconst UPDATE_SYNC = gql`\n mutation SyncControlsUpdate($id: ID!, $input: PlanmagnetUpdateExternalSyncInput!) {\n planmagnetUpdateExternalSync(id: $id, input: $input) {\n id\n direction\n conflictStrategy\n fieldMapping\n syncFilters\n externalProjectId\n externalProjectUrl\n }\n }\n`;\n\nexport interface EditableSync {\n id: string;\n provider: string;\n direction: SyncDirection;\n conflictStrategy: ConflictResolution;\n fieldMapping: unknown;\n syncFilters: unknown;\n}\n\n/**\n * The one \"Edit settings\" surface for a sync: direction, conflict strategy, field mapping and sync filters, plus\n * — for GITHUB only, where `connectionConfig`'s shape is real and known (`{ token, owner, repo }`) — an explicit\n * \"Replace connection\" group. The token is never pre-filled (it reads back as `[REDACTED]`, which must never be\n * echoed into the input or resent as if it were real); owner/repo are not secret and DO read back real values, so\n * they pre-fill and stay editable. Because `PlanmagnetUpdateExternalSyncInput.connectionConfig` REPLACES the\n * whole stored config rather than merging, changing owner or repo without also supplying a fresh token would\n * discard the stored token — so the form requires all three together whenever any of the three changed, and\n * sends none of them (keeping the stored connection untouched) when none did.\n */\nexport function EditSyncDialog({ sync }: { sync: EditableSync }) {\n const [open, setOpen] = useState(false);\n const isGithub = sync.provider === 'GITHUB';\n const { capabilities } = useSyncProviderCapabilities();\n const capability = capabilities.find((c) => c.provider === sync.provider);\n const allowedDirections = capability\n ? SYNC_DIRECTIONS.filter((d) => directionSupported(capability, d))\n : SYNC_DIRECTIONS;\n\n const initialMapping = parseGithubFieldMapping(sync.fieldMapping);\n const initialFiltersText = sync.syncFilters ? JSON.stringify(sync.syncFilters, null, 2) : '';\n\n const [direction, setDirection] = useState<SyncDirection>(sync.direction);\n const [conflictStrategy, setConflictStrategy] = useState<ConflictResolution>(\n sync.conflictStrategy\n );\n const [priorityPrefix, setPriorityPrefix] = useState(initialMapping.priority);\n const [dueDatePrefix, setDueDatePrefix] = useState(initialMapping.dueDate);\n const [filtersText, setFiltersText] = useState(initialFiltersText);\n const [filtersError, setFiltersError] = useState<string | null>(null);\n // Raw user edits, '' meaning \"untouched\". `effectiveOwner`/`effectiveRepo` below fall back to the real,\n // non-secret value the query loads — no `useEffect` seeding required (and none of the cascading-render risk\n // it brings): until the user types, the field simply displays the loaded value; the moment they type anything,\n // their edit takes over, even if it round-trips to the same text.\n const [owner, setOwner] = useState('');\n const [repo, setRepo] = useState('');\n const [token, setToken] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const { data: connectionData } = useQuery<{\n planmagnetExternalSync: { id: string; connectionConfig: Record<string, unknown> } | null;\n }>(GET_SYNC_CONNECTION, { variables: { id: sync.id }, skip: !open || !isGithub });\n\n const config = connectionData?.planmagnetExternalSync?.connectionConfig;\n const loadedOwner = typeof config?.owner === 'string' ? config.owner : '';\n const loadedRepo = typeof config?.repo === 'string' ? config.repo : '';\n const effectiveOwner = owner || loadedOwner;\n const effectiveRepo = repo || loadedRepo;\n\n const resetForm = () => {\n setDirection(sync.direction);\n setConflictStrategy(sync.conflictStrategy);\n setPriorityPrefix(initialMapping.priority);\n setDueDatePrefix(initialMapping.dueDate);\n setFiltersText(initialFiltersText);\n setFiltersError(null);\n setOwner('');\n setRepo('');\n setToken('');\n setError(null);\n };\n\n const [update, state] = useMutation(UPDATE_SYNC, {\n refetchQueries: [...SYNC_LIST_QUERIES, ...SYNC_DETAIL_QUERIES],\n });\n\n const connectionChanged =\n isGithub &&\n !!config &&\n (effectiveOwner !== loadedOwner || effectiveRepo !== loadedRepo || token.trim() !== '');\n const mappingChanged =\n priorityPrefix.trim() !== initialMapping.priority ||\n dueDatePrefix.trim() !== initialMapping.dueDate;\n const filtersChanged = filtersText.trim() !== initialFiltersText.trim();\n\n const save = async () => {\n setError(null);\n setFiltersError(null);\n const input: Record<string, unknown> = {};\n if (direction !== sync.direction) input.direction = direction;\n if (conflictStrategy !== sync.conflictStrategy) input.conflictStrategy = conflictStrategy;\n if (mappingChanged) {\n input.fieldMapping = {\n ...(priorityPrefix.trim() ? { priority: priorityPrefix.trim() } : {}),\n ...(dueDatePrefix.trim() ? { dueDate: dueDatePrefix.trim() } : {}),\n };\n }\n if (filtersChanged) {\n const trimmed = filtersText.trim();\n if (!trimmed) {\n input.syncFilters = null;\n } else {\n try {\n input.syncFilters = JSON.parse(trimmed);\n } catch {\n setFiltersError('Sync filters must be valid JSON.');\n return;\n }\n }\n }\n if (connectionChanged) {\n if (!token.trim() || !effectiveOwner.trim() || !effectiveRepo.trim()) {\n setError('Changing the owner, repo or token requires all three — re-enter the token too.');\n return;\n }\n if (!isValidGithubOwner(effectiveOwner) || !isValidGithubRepo(effectiveRepo)) {\n setError(\n 'Owner and repo must look like real GitHub names (letters, digits, \"-\", \"_\", \".\").'\n );\n return;\n }\n input.connectionConfig = {\n token: token.trim(),\n owner: effectiveOwner.trim(),\n repo: effectiveRepo.trim(),\n };\n }\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: sync.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The sync could not be saved.')\n );\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) resetForm();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"sync-edit-settings\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit settings\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg max-h-[85vh] overflow-y-auto\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit sync settings</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>Only changed fields are saved.</ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-4\">\n <div>\n <span className=\"text-sm font-medium\">Direction</span>\n <div className=\"mt-1 flex flex-col gap-1\" role=\"radiogroup\" aria-label=\"Direction\">\n {allowedDirections.map((d) => (\n <label key={d} className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n name=\"sync-edit-direction\"\n checked={direction === d}\n onChange={() => setDirection(d)}\n />\n {directionLabel(d)}\n </label>\n ))}\n </div>\n </div>\n <div>\n <label htmlFor=\"sync-edit-strategy\" className=\"text-sm font-medium\">\n Conflict strategy\n </label>\n <select\n id=\"sync-edit-strategy\"\n className={FIELD}\n value={conflictStrategy}\n onChange={(e) => setConflictStrategy(e.target.value as ConflictResolution)}\n >\n {CONFLICT_RESOLUTIONS.map((r) => (\n <option key={r} value={r}>\n {enumLabel(r)}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {CONFLICT_STRATEGY_EXPLANATION[conflictStrategy]}\n </p>\n </div>\n {isGithub && (\n <div className=\"space-y-2 rounded-md border p-3\">\n <p className=\"text-sm font-medium\">Field mapping (GitHub labels)</p>\n <p className=\"text-xs text-muted-foreground\">\n GitHub issues have no native priority or due-date field — PlanMagnet encodes each as\n a label of the form <code>prefix+value</code> (e.g. <code>priority:HIGH</code>).\n Leave blank to use the built-in prefix.\n </p>\n <div>\n <label\n htmlFor=\"sync-edit-priority-prefix\"\n className=\"text-xs text-muted-foreground\"\n >\n Priority label prefix (default &quot;{GITHUB_FIELD_MAPPING_DEFAULTS.priority}\n &quot;)\n </label>\n <Input\n id=\"sync-edit-priority-prefix\"\n className=\"mt-1\"\n placeholder={GITHUB_FIELD_MAPPING_DEFAULTS.priority}\n value={priorityPrefix}\n onChange={(e) => setPriorityPrefix(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor=\"sync-edit-duedate-prefix\" className=\"text-xs text-muted-foreground\">\n Due-date label prefix (default &quot;{GITHUB_FIELD_MAPPING_DEFAULTS.dueDate}\n &quot;)\n </label>\n <Input\n id=\"sync-edit-duedate-prefix\"\n className=\"mt-1\"\n placeholder={GITHUB_FIELD_MAPPING_DEFAULTS.dueDate}\n value={dueDatePrefix}\n onChange={(e) => setDueDatePrefix(e.target.value)}\n />\n </div>\n </div>\n )}\n <div>\n <label htmlFor=\"sync-edit-filters\" className=\"text-sm font-medium\">\n Sync filters (advanced, JSON)\n </label>\n <p className=\"text-xs text-muted-foreground\">\n Stored on the sync but not yet read by any run — reserved for a future filtering\n feature. Leave blank unless you know you need this.\n </p>\n <Textarea\n id=\"sync-edit-filters\"\n className=\"mt-1 font-mono text-xs min-h-[72px]\"\n value={filtersText}\n onChange={(e) => setFiltersText(e.target.value)}\n />\n {filtersError && <ErrorLine message={filtersError} />}\n </div>\n {isGithub && (\n <div className=\"space-y-2 rounded-md border p-3\">\n <p className=\"text-sm font-medium\">Replace connection</p>\n <p className=\"text-xs text-muted-foreground\">\n The token is never shown once saved. Changing the owner, repo or token requires\n re-entering all three — PlanMagnet cannot resend the stored token for you.\n </p>\n <div>\n <label htmlFor=\"sync-edit-owner\" className=\"text-xs text-muted-foreground\">\n Owner\n </label>\n <Input\n id=\"sync-edit-owner\"\n className=\"mt-1\"\n value={effectiveOwner}\n onChange={(e) => setOwner(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor=\"sync-edit-repo\" className=\"text-xs text-muted-foreground\">\n Repository\n </label>\n <Input\n id=\"sync-edit-repo\"\n className=\"mt-1\"\n value={effectiveRepo}\n onChange={(e) => setRepo(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor=\"sync-edit-token\" className=\"text-xs text-muted-foreground\">\n Replace token\n </label>\n <Input\n id=\"sync-edit-token\"\n type=\"password\"\n autoComplete=\"off\"\n className=\"mt-1\"\n placeholder=\"Leave blank to keep the current token\"\n value={token}\n onChange={(e) => setToken(e.target.value)}\n data-testid=\"sync-edit-token\"\n />\n </div>\n </div>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => {\n resetForm();\n setOpen(false);\n }}\n >\n Cancel\n </Button>\n <Button\n onClick={() => void save()}\n disabled={state.loading}\n data-testid=\"sync-edit-submit\"\n >\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SyncControls.js","names":[],"sources":["../../../src/planmagnet/controls/SyncControls.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n Input,\n Textarea,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n ChevronDown,\n ChevronUp,\n Loader2,\n Pause,\n Pencil,\n Play,\n PlugZap,\n X,\n} from 'lucide-react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { ConfirmButton, ErrorLine } from './ConfirmButton';\nimport { enumLabel } from './WorkItemControls';\nimport { useSyncProviderCapabilities } from '../integrations/useSyncProviderCapabilities';\nimport {\n CONFLICT_RESOLUTIONS,\n directionLabel,\n directionSupported,\n GITHUB_FIELD_MAPPING_DEFAULTS,\n isValidGithubOwner,\n isValidGithubRepo,\n parseGithubFieldMapping,\n SYNC_DIRECTIONS,\n type ConflictResolution,\n type RunHealth,\n type SyncConnectionTestResult,\n type SyncDirection,\n type SyncRunResult,\n type SyncStatus,\n} from '../integrations/syncTypes';\n\n/**\n * Shared row/detail controls for External Sync (AUT-09/10/24/40): status badges, health, run/pause/resume/test\n * actions and conflict resolution. Every write here follows the same two rules the rest of this MFE does\n * (`utils/mutationResult.ts`, `utils/graphqlErrors.ts`): a refused mutation is surfaced, never swallowed, and a\n * FORBIDDEN refusal reads as \"your role can view but not change this\" rather than a raw GraphQL error string —\n * this page cannot know ahead of time whether the viewer's role allows a given write, so it always offers the\n * action and reports the refusal if and when it happens, rather than guessing at a client-side role check.\n */\n\nexport const SYNC_LIST_QUERIES = ['SyncsList'];\nexport const SYNC_DETAIL_QUERIES = ['SyncDetail', 'SyncRuns', 'SyncConflicts'];\n\nconst STATUS_VARIANT: Record<\n SyncStatus,\n 'default' | 'success' | 'warning' | 'info' | 'secondary' | 'destructive' | 'outline'\n> = {\n IDLE: 'secondary',\n SYNCING: 'info',\n SUCCESS: 'success',\n FAILED: 'destructive',\n PAUSED: 'warning',\n DISABLED: 'outline',\n};\n\nexport function SyncStatusBadge({ status }: { status: string }) {\n return (\n <Badge variant={STATUS_VARIANT[status as SyncStatus] ?? 'outline'} className=\"text-xs\">\n {enumLabel(status)}\n </Badge>\n );\n}\n\nconst RUN_STATUS_VARIANT: Record<string, 'success' | 'destructive' | 'warning'> = {\n SUCCEEDED: 'success',\n FAILED: 'destructive',\n PARTIAL: 'warning',\n};\n\nexport function SyncRunStatusBadge({ status }: { status: string }) {\n return (\n <Badge variant={RUN_STATUS_VARIANT[status] ?? 'secondary'} className=\"text-xs\">\n {enumLabel(status)}\n </Badge>\n );\n}\n\nexport function ConflictStatusBadge({ status }: { status: string }) {\n return (\n <Badge variant={status === 'OPEN' ? 'warning' : 'secondary'} className=\"text-xs\">\n {enumLabel(status)}\n </Badge>\n );\n}\n\n/** A long string (a sync's `lastSyncError`, an issue body) collapsed to `max` chars with an expand toggle. */\nexport function TruncatedText({\n text,\n max = 140,\n className = 'text-muted-foreground',\n testId,\n}: {\n text: string;\n max?: number;\n className?: string;\n testId?: string;\n}) {\n const [expanded, setExpanded] = useState(false);\n if (text.length <= max) return <span className={className}>{text}</span>;\n return (\n <span data-testid={testId}>\n <span className={className}>{expanded ? text : `${text.slice(0, max)}…`}</span>{' '}\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-0.5 text-xs font-medium text-primary hover:underline\"\n aria-expanded={expanded}\n onClick={() => setExpanded((v) => !v)}\n >\n {expanded ? (\n <>\n Show less <ChevronUp className=\"h-3 w-3\" aria-hidden=\"true\" />\n </>\n ) : (\n <>\n Show more <ChevronDown className=\"h-3 w-3\" aria-hidden=\"true\" />\n </>\n )}\n </button>\n </span>\n );\n}\n\n/**\n * Last run time/status + consecutive-failure streak (AUT-09/22). `consecutiveFailures` is 0 once the latest run\n * did not fail — highlighted only while it is actually nonzero, never merely because the last run failed once\n * and then recovered.\n */\nexport function SyncHealthSummary({ health }: { health: RunHealth }) {\n return (\n <div className=\"flex flex-wrap items-center gap-2 text-xs\" data-testid=\"sync-health\">\n {health.lastRunAt ? (\n <>\n <span className=\"text-muted-foreground\">\n Last run {new Date(health.lastRunAt).toLocaleString()}\n </span>\n {health.lastStatus && <SyncRunStatusBadge status={health.lastStatus} />}\n </>\n ) : (\n <span className=\"text-muted-foreground\">Never run</span>\n )}\n {health.consecutiveFailures > 0 && (\n <span className=\"inline-flex items-center gap-1 font-medium text-status-error-text\">\n <AlertTriangle className=\"h-3 w-3\" aria-hidden=\"true\" />\n {health.consecutiveFailures} run{health.consecutiveFailures === 1 ? '' : 's'} failed in a\n row\n </span>\n )}\n <span className=\"text-muted-foreground\">\n {health.runsLast7Days} run(s) in the last 7 days\n </span>\n </div>\n );\n}\n\nconst START_SYNC = gql`\n mutation SyncControlsStartSync($id: ID!) {\n planmagnetStartSync(id: $id) {\n syncId\n success\n itemsCreated\n itemsUpdated\n itemsSkipped\n errorCount\n errors\n conflictCount\n durationMs\n syncedAt\n }\n }\n`;\n\n/** \"Run now\" (AUT-40): starts a sync in the direction `ExternalSync.direction` already records, and reports the\n * real result — including an honest `NOT_IMPLEMENTED` outcome for a provider with no live connector, never a\n * fabricated success. */\nexport function RunNowButton({\n syncId,\n onResult,\n size = 'sm',\n}: {\n syncId: string;\n onResult?: (result: SyncRunResult) => void;\n size?: 'sm' | 'default';\n}) {\n const [start, state] = useMutation<{ planmagnetStartSync: SyncRunResult }>(START_SYNC, {\n refetchQueries: [...SYNC_LIST_QUERIES, ...SYNC_DETAIL_QUERIES],\n });\n const [error, setError] = useState<string | null>(null);\n\n const run = async () => {\n setError(null);\n try {\n const res = await throwOnError(start({ variables: { id: syncId } }));\n if (res.data) onResult?.(res.data.planmagnetStartSync);\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The sync could not be started.')\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\">\n <Button\n size={size}\n variant=\"outline\"\n disabled={state.loading}\n onClick={() => void run()}\n data-testid=\"sync-run-now\"\n >\n {state.loading ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <PlugZap className=\"mr-1 h-4 w-4\" />\n )}\n Run now\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\n/**\n * What `planmagnetStartSync` actually did (AUT-40). A completed run is a normal GraphQL success even when the\n * sync itself failed on every item — bad token, unreachable provider, a refused status transition, … — the\n * service records `success: false` with `errors` populated rather than throwing (see\n * `sync.service.ts`'s `runGithubImport`/`runGithubExport`). This is the one place that outcome is rendered, so\n * every `RunNowButton` call site wires its `onResult` here instead of relying on a delayed list/detail refetch.\n */\nexport function SyncRunResultSummary({ result }: { result: SyncRunResult }) {\n return (\n <div\n role=\"status\"\n className={`space-y-1 rounded-md border p-3 text-sm ${\n result.success\n ? 'border-border bg-muted text-status-success-text'\n : 'border-destructive/40 text-status-error-text'\n }`}\n data-testid=\"sync-run-result\"\n >\n <p className=\"font-medium\">{result.success ? 'Sync run succeeded.' : 'Sync run failed.'}</p>\n <p className=\"text-xs text-muted-foreground\">\n {result.itemsCreated} created · {result.itemsUpdated} updated · {result.itemsSkipped}{' '}\n skipped\n {result.conflictCount > 0\n ? ` · ${result.conflictCount} conflict${result.conflictCount === 1 ? '' : 's'}`\n : ''}\n </p>\n {result.errors && result.errors.length > 0 && (\n <ul className=\"list-disc space-y-0.5 pl-4 text-xs\">\n {result.errors.map((message, i) => (\n <li key={i}>{message}</li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nconst PAUSE_SYNC = gql`\n mutation SyncControlsPause($id: ID!) {\n planmagnetPauseExternalSync(id: $id) {\n id\n status\n }\n }\n`;\nconst RESUME_SYNC = gql`\n mutation SyncControlsResume($id: ID!) {\n planmagnetResumeExternalSync(id: $id) {\n id\n status\n }\n }\n`;\n\n/** Pause when active, Resume when PAUSED — the only two states these two mutations move between. */\nexport function PauseResumeButton({ syncId, status }: { syncId: string; status: string }) {\n const [pause, pauseState] = useMutation(PAUSE_SYNC, { refetchQueries: SYNC_LIST_QUERIES });\n const [resume, resumeState] = useMutation(RESUME_SYNC, { refetchQueries: SYNC_LIST_QUERIES });\n const [error, setError] = useState<string | null>(null);\n const loading = pauseState.loading || resumeState.loading;\n const paused = status === 'PAUSED';\n\n const toggle = async () => {\n setError(null);\n try {\n if (paused) {\n await throwOnError(resume({ variables: { id: syncId } }));\n } else {\n await throwOnError(pause({ variables: { id: syncId } }));\n }\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, `The sync could not be ${paused ? 'resumed' : 'paused'}.`)\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={() => void toggle()}\n data-testid={paused ? 'sync-resume' : 'sync-pause'}\n >\n {loading ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : paused ? (\n <Play className=\"mr-1 h-4 w-4\" />\n ) : (\n <Pause className=\"mr-1 h-4 w-4\" />\n )}\n {paused ? 'Resume' : 'Pause'}\n </Button>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst TEST_SYNC = gql`\n mutation SyncControlsTest($id: ID!) {\n planmagnetTestExternalSync(id: $id) {\n ok\n message\n rateLimitRemaining\n }\n }\n`;\n\n/** AUT-23/20 — probes the connection through the real connector, writing nothing. Offered whenever the page can\n * reach it; a provider with no live connector still answers, honestly, with `ok: false`. */\nexport function TestConnectionButton({ syncId }: { syncId: string }) {\n const [test, state] = useMutation<{ planmagnetTestExternalSync: SyncConnectionTestResult }>(\n TEST_SYNC\n );\n const [result, setResult] = useState<SyncConnectionTestResult | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const run = async () => {\n setError(null);\n setResult(null);\n try {\n const res = await throwOnError(test({ variables: { id: syncId } }));\n if (res.data) setResult(res.data.planmagnetTestExternalSync);\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The connection could not be tested.')\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={state.loading}\n onClick={() => void run()}\n data-testid=\"sync-test-connection\"\n >\n {state.loading ? <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" /> : null}\n Test connection\n </Button>\n {result && (\n <p\n className={`text-sm ${result.ok ? 'text-status-success-text' : 'text-status-error-text'}`}\n data-testid=\"sync-test-result\"\n >\n {result.ok ? 'Connected: ' : 'Failed: '}\n {result.message}\n {result.rateLimitRemaining !== null && ` (${result.rateLimitRemaining} calls remaining)`}\n </p>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst DELETE_SYNC = gql`\n mutation SyncControlsDelete($id: ID!) {\n planmagnetDeleteExternalSync(id: $id)\n }\n`;\n\nexport function DeleteSyncButton({\n syncId,\n label,\n /** Passed by the detail page, which leaves for the list afterward — see LEAVES_PAGE_CONTEXT's own doc. */\n onDeleted,\n}: {\n syncId: string;\n label: string;\n onDeleted?: () => void;\n}) {\n const [remove, { loading }] = useMutation(DELETE_SYNC, { refetchQueries: SYNC_LIST_QUERIES });\n const [error, setError] = useState<string | null>(null);\n\n const onDelete = async () => {\n setError(null);\n try {\n await throwOnError(remove({ variables: { id: syncId } }));\n onDeleted?.();\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The sync could not be deleted.')\n );\n }\n };\n\n return (\n <div className=\"flex flex-col items-end gap-1\">\n <ConfirmButton\n label=\"Delete\"\n variant=\"outline\"\n icon={<X className=\"mr-1 h-4 w-4\" />}\n title={`Delete \"${label}\"?`}\n description=\"This stops the sync and removes its configuration and run/conflict history. Work items already created from it are kept.\"\n confirmLabel=\"Delete sync\"\n destructive\n disabled={loading}\n onConfirm={() => void onDelete()}\n testId=\"sync-delete\"\n />\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nconst RESOLVE_CONFLICT = gql`\n mutation SyncControlsResolveConflict($id: ID!, $resolution: PlanmagnetSyncConflictResolution!) {\n planmagnetResolveSyncConflict(id: $id, resolution: $resolution) {\n id\n status\n resolution\n resolvedAt\n resolvedBy\n }\n }\n`;\n\n/** AUT-10/24 — SOURCE_WINS/TARGET_WINS actually push/pull the winning side's data; MANUAL just closes the flag.\n * A refused or failed resolution (e.g. the provider call itself fails) throws server-side and leaves the\n * conflict OPEN rather than recording a resolution that never happened — so on error this shows the reason and\n * the conflict stays in the list exactly as it was. */\nexport function ConflictResolveActions({ conflictId }: { conflictId: string }) {\n const [resolve, state] = useMutation(RESOLVE_CONFLICT, { refetchQueries: SYNC_DETAIL_QUERIES });\n const [error, setError] = useState<string | null>(null);\n const [pending, setPending] = useState<ConflictResolution | null>(null);\n\n const act = async (resolution: ConflictResolution) => {\n setError(null);\n setPending(resolution);\n try {\n await throwOnError(resolve({ variables: { id: conflictId, resolution } }));\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The conflict could not be resolved.')\n );\n } finally {\n setPending(null);\n }\n };\n\n return (\n <div className=\"flex flex-col items-start gap-1\" data-testid=\"sync-conflict-resolve\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n size=\"sm\"\n disabled={state.loading}\n onClick={() => void act('SOURCE_WINS')}\n data-testid=\"sync-conflict-keep-local\"\n >\n {pending === 'SOURCE_WINS' && <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />}\n Keep PlanMagnet\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={state.loading}\n onClick={() => void act('TARGET_WINS')}\n data-testid=\"sync-conflict-take-provider\"\n >\n {pending === 'TARGET_WINS' && <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />}\n Take provider\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={state.loading}\n onClick={() => void act('MANUAL')}\n data-testid=\"sync-conflict-dismiss\"\n >\n {pending === 'MANUAL' && <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />}\n Dismiss (manual)\n </Button>\n </div>\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n\nexport const CONFLICT_STRATEGY_EXPLANATION: Record<ConflictResolution, string> = {\n SOURCE_WINS:\n 'PlanMagnet wins — the local work item overwrites the provider on a detected conflict.',\n TARGET_WINS: 'Provider wins — the external tool overwrites PlanMagnet on a detected conflict.',\n // AUT-24 — field-level merge, not a whole-side pick: see PlanmagnetSyncConflictResolution's own doc comment.\n MERGE:\n \"Merge — keep this project's title and description, take the remote status, priority, labels and due date.\",\n MANUAL: 'Flag and skip — a conflict is recorded and left alone until resolved here.',\n};\n\nconst FIELD = 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm';\n\nconst GET_SYNC_CONNECTION = gql`\n query SyncControlsConnection($id: ID!) {\n planmagnetExternalSync(id: $id) {\n id\n connectionConfig\n }\n }\n`;\n\nconst UPDATE_SYNC = gql`\n mutation SyncControlsUpdate($id: ID!, $input: PlanmagnetUpdateExternalSyncInput!) {\n planmagnetUpdateExternalSync(id: $id, input: $input) {\n id\n direction\n conflictStrategy\n fieldMapping\n syncFilters\n externalProjectId\n externalProjectUrl\n }\n }\n`;\n\nexport interface EditableSync {\n id: string;\n provider: string;\n direction: SyncDirection;\n conflictStrategy: ConflictResolution;\n fieldMapping: unknown;\n syncFilters: unknown;\n}\n\n/**\n * The one \"Edit settings\" surface for a sync: direction, conflict strategy, field mapping and sync filters, plus\n * — for GITHUB only, where `connectionConfig`'s shape is real and known (`{ token, owner, repo }`) — an explicit\n * \"Replace connection\" group. The token is never pre-filled (it reads back as `[REDACTED]`, which must never be\n * echoed into the input or resent as if it were real); owner/repo are not secret and DO read back real values, so\n * they pre-fill and stay editable. Because `PlanmagnetUpdateExternalSyncInput.connectionConfig` REPLACES the\n * whole stored config rather than merging, changing owner or repo without also supplying a fresh token would\n * discard the stored token — so the form requires all three together whenever any of the three changed, and\n * sends none of them (keeping the stored connection untouched) when none did.\n */\nexport function EditSyncDialog({ sync }: { sync: EditableSync }) {\n const [open, setOpen] = useState(false);\n const isGithub = sync.provider === 'GITHUB';\n const { capabilities } = useSyncProviderCapabilities();\n const capability = capabilities.find((c) => c.provider === sync.provider);\n const allowedDirections = capability\n ? SYNC_DIRECTIONS.filter((d) => directionSupported(capability, d))\n : SYNC_DIRECTIONS;\n\n const initialMapping = parseGithubFieldMapping(sync.fieldMapping);\n const initialFiltersText = sync.syncFilters ? JSON.stringify(sync.syncFilters, null, 2) : '';\n\n const [direction, setDirection] = useState<SyncDirection>(sync.direction);\n const [conflictStrategy, setConflictStrategy] = useState<ConflictResolution>(\n sync.conflictStrategy\n );\n const [priorityPrefix, setPriorityPrefix] = useState(initialMapping.priority);\n const [dueDatePrefix, setDueDatePrefix] = useState(initialMapping.dueDate);\n const [filtersText, setFiltersText] = useState(initialFiltersText);\n const [filtersError, setFiltersError] = useState<string | null>(null);\n // Raw user edits, '' meaning \"untouched\". `effectiveOwner`/`effectiveRepo` below fall back to the real,\n // non-secret value the query loads — no `useEffect` seeding required (and none of the cascading-render risk\n // it brings): until the user types, the field simply displays the loaded value; the moment they type anything,\n // their edit takes over, even if it round-trips to the same text.\n const [owner, setOwner] = useState('');\n const [repo, setRepo] = useState('');\n const [token, setToken] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const { data: connectionData } = useQuery<{\n planmagnetExternalSync: { id: string; connectionConfig: Record<string, unknown> } | null;\n }>(GET_SYNC_CONNECTION, { variables: { id: sync.id }, skip: !open || !isGithub });\n\n const config = connectionData?.planmagnetExternalSync?.connectionConfig;\n const loadedOwner = typeof config?.owner === 'string' ? config.owner : '';\n const loadedRepo = typeof config?.repo === 'string' ? config.repo : '';\n const effectiveOwner = owner || loadedOwner;\n const effectiveRepo = repo || loadedRepo;\n\n const resetForm = () => {\n setDirection(sync.direction);\n setConflictStrategy(sync.conflictStrategy);\n setPriorityPrefix(initialMapping.priority);\n setDueDatePrefix(initialMapping.dueDate);\n setFiltersText(initialFiltersText);\n setFiltersError(null);\n setOwner('');\n setRepo('');\n setToken('');\n setError(null);\n };\n\n const [update, state] = useMutation(UPDATE_SYNC, {\n refetchQueries: [...SYNC_LIST_QUERIES, ...SYNC_DETAIL_QUERIES],\n });\n\n const connectionChanged =\n isGithub &&\n !!config &&\n (effectiveOwner !== loadedOwner || effectiveRepo !== loadedRepo || token.trim() !== '');\n const mappingChanged =\n priorityPrefix.trim() !== initialMapping.priority ||\n dueDatePrefix.trim() !== initialMapping.dueDate;\n const filtersChanged = filtersText.trim() !== initialFiltersText.trim();\n\n const save = async () => {\n setError(null);\n setFiltersError(null);\n const input: Record<string, unknown> = {};\n if (direction !== sync.direction) input.direction = direction;\n if (conflictStrategy !== sync.conflictStrategy) input.conflictStrategy = conflictStrategy;\n if (mappingChanged) {\n input.fieldMapping = {\n ...(priorityPrefix.trim() ? { priority: priorityPrefix.trim() } : {}),\n ...(dueDatePrefix.trim() ? { dueDate: dueDatePrefix.trim() } : {}),\n };\n }\n if (filtersChanged) {\n const trimmed = filtersText.trim();\n if (!trimmed) {\n input.syncFilters = null;\n } else {\n try {\n input.syncFilters = JSON.parse(trimmed);\n } catch {\n setFiltersError('Sync filters must be valid JSON.');\n return;\n }\n }\n }\n if (connectionChanged) {\n if (!token.trim() || !effectiveOwner.trim() || !effectiveRepo.trim()) {\n setError('Changing the owner, repo or token requires all three — re-enter the token too.');\n return;\n }\n if (!isValidGithubOwner(effectiveOwner) || !isValidGithubRepo(effectiveRepo)) {\n setError(\n 'Owner and repo must look like real GitHub names (letters, digits, \"-\", \"_\", \".\").'\n );\n return;\n }\n input.connectionConfig = {\n token: token.trim(),\n owner: effectiveOwner.trim(),\n repo: effectiveRepo.trim(),\n };\n }\n if (Object.keys(input).length === 0) {\n setOpen(false);\n return;\n }\n try {\n await throwOnError(update({ variables: { id: sync.id, input } }));\n setOpen(false);\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can view integrations but not change them.'\n : mutationErrorMessage(e, 'The sync could not be saved.')\n );\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) resetForm();\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"sync-edit-settings\">\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit settings\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-w-lg max-h-[85vh] overflow-y-auto\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Edit sync settings</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>Only changed fields are saved.</ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-4\">\n <div>\n <span className=\"text-sm font-medium\">Direction</span>\n <div className=\"mt-1 flex flex-col gap-1\" role=\"radiogroup\" aria-label=\"Direction\">\n {allowedDirections.map((d) => (\n <label key={d} className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n name=\"sync-edit-direction\"\n checked={direction === d}\n onChange={() => setDirection(d)}\n />\n {directionLabel(d)}\n </label>\n ))}\n </div>\n </div>\n <div>\n <label htmlFor=\"sync-edit-strategy\" className=\"text-sm font-medium\">\n Conflict strategy\n </label>\n <select\n id=\"sync-edit-strategy\"\n className={FIELD}\n value={conflictStrategy}\n onChange={(e) => setConflictStrategy(e.target.value as ConflictResolution)}\n >\n {CONFLICT_RESOLUTIONS.map((r) => (\n <option key={r} value={r}>\n {enumLabel(r)}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {CONFLICT_STRATEGY_EXPLANATION[conflictStrategy]}\n </p>\n </div>\n {isGithub && (\n <div className=\"space-y-2 rounded-md border p-3\">\n <p className=\"text-sm font-medium\">Field mapping (GitHub labels)</p>\n <p className=\"text-xs text-muted-foreground\">\n GitHub issues have no native priority or due-date field — PlanMagnet encodes each as\n a label of the form <code>prefix+value</code> (e.g. <code>priority:HIGH</code>).\n Leave blank to use the built-in prefix.\n </p>\n <div>\n <label\n htmlFor=\"sync-edit-priority-prefix\"\n className=\"text-xs text-muted-foreground\"\n >\n Priority label prefix (default &quot;{GITHUB_FIELD_MAPPING_DEFAULTS.priority}\n &quot;)\n </label>\n <Input\n id=\"sync-edit-priority-prefix\"\n className=\"mt-1\"\n placeholder={GITHUB_FIELD_MAPPING_DEFAULTS.priority}\n value={priorityPrefix}\n onChange={(e) => setPriorityPrefix(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor=\"sync-edit-duedate-prefix\" className=\"text-xs text-muted-foreground\">\n Due-date label prefix (default &quot;{GITHUB_FIELD_MAPPING_DEFAULTS.dueDate}\n &quot;)\n </label>\n <Input\n id=\"sync-edit-duedate-prefix\"\n className=\"mt-1\"\n placeholder={GITHUB_FIELD_MAPPING_DEFAULTS.dueDate}\n value={dueDatePrefix}\n onChange={(e) => setDueDatePrefix(e.target.value)}\n />\n </div>\n </div>\n )}\n <div>\n <label htmlFor=\"sync-edit-filters\" className=\"text-sm font-medium\">\n Sync filters (advanced, JSON)\n </label>\n <p className=\"text-xs text-muted-foreground\">\n Stored on the sync but not yet read by any run — reserved for a future filtering\n feature. Leave blank unless you know you need this.\n </p>\n <Textarea\n id=\"sync-edit-filters\"\n className=\"mt-1 font-mono text-xs min-h-[72px]\"\n value={filtersText}\n onChange={(e) => setFiltersText(e.target.value)}\n />\n {filtersError && <ErrorLine message={filtersError} />}\n </div>\n {isGithub && (\n <div className=\"space-y-2 rounded-md border p-3\">\n <p className=\"text-sm font-medium\">Replace connection</p>\n <p className=\"text-xs text-muted-foreground\">\n The token is never shown once saved. Changing the owner, repo or token requires\n re-entering all three — PlanMagnet cannot resend the stored token for you.\n </p>\n <div>\n <label htmlFor=\"sync-edit-owner\" className=\"text-xs text-muted-foreground\">\n Owner\n </label>\n <Input\n id=\"sync-edit-owner\"\n className=\"mt-1\"\n value={effectiveOwner}\n onChange={(e) => setOwner(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor=\"sync-edit-repo\" className=\"text-xs text-muted-foreground\">\n Repository\n </label>\n <Input\n id=\"sync-edit-repo\"\n className=\"mt-1\"\n value={effectiveRepo}\n onChange={(e) => setRepo(e.target.value)}\n />\n </div>\n <div>\n <label htmlFor=\"sync-edit-token\" className=\"text-xs text-muted-foreground\">\n Replace token\n </label>\n <Input\n id=\"sync-edit-token\"\n type=\"password\"\n autoComplete=\"off\"\n className=\"mt-1\"\n placeholder=\"Leave blank to keep the current token\"\n value={token}\n onChange={(e) => setToken(e.target.value)}\n data-testid=\"sync-edit-token\"\n />\n </div>\n </div>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n <ResponsiveDialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => {\n resetForm();\n setOpen(false);\n }}\n >\n Cancel\n </Button>\n <Button\n onClick={() => void save()}\n disabled={state.loading}\n data-testid=\"sync-edit-submit\"\n >\n {state.loading ? 'Saving…' : 'Save'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import { WebhookSubscriptionRow } from '../webhooks/webhookTypes';
2
+
3
+ /**
4
+ * Outbound webhook subscriptions (AUT-33) — create/rotate/status/delete controls, shared by
5
+ * `pages/IntegrationsWebhooksPagePage.tsx`. See `webhooks/webhookTypes.ts` for the backing shapes and the real
6
+ * SDL doc comments they are grounded in.
7
+ */
8
+ export declare const WEBHOOK_LIST_QUERIES: string[];
9
+ export declare function WebhookStatusBadge({ status }: {
10
+ status: string;
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ export declare function WebhookDeliveryStatusBadge({ status }: {
13
+ status: string;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ /** Which `PlanmagnetAutomationTriggerType` values this subscription is delivered for, as small badges. */
16
+ export declare function WebhookEventBadges({ events }: {
17
+ events: string[];
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ /**
20
+ * "New subscription": https:// URL + events checklist, then the signing secret shown exactly once. Every way of
21
+ * closing (the footer button, the X, Escape, an outside click) goes through one `close()` — see
22
+ * `reports/ShareDialog.tsx`'s own fix (#45) for why setting `open` directly on the controlled dialog is not
23
+ * enough: the footer button used to skip the reset there, and the just-created secret reappeared on reopen.
24
+ */
25
+ export declare function CreateWebhookSubscriptionDialog(): import("react/jsx-runtime").JSX.Element;
26
+ /**
27
+ * "Rotate secret": the old secret stops verifying deliveries immediately, the new one is shown exactly once.
28
+ * Same one-`close()`-for-every-exit discipline as the create dialog above.
29
+ */
30
+ export declare function RotateWebhookSecretButton({ subscriptionId }: {
31
+ subscriptionId: string;
32
+ }): import("react/jsx-runtime").JSX.Element;
33
+ /** Pause/Resume/Disable/Re-enable — every transition `PlanmagnetWebhookSubscriptionStatus` allows, via the one
34
+ * update mutation (its own doc comment: "PAUSED/ACTIVE are the owner's own choice; setting DISABLED by hand is
35
+ * also accepted"). Re-activating (PAUSED or DISABLED -> ACTIVE) resets `consecutiveFailures` server-side. */
36
+ export declare function WebhookStatusActions({ subscription }: {
37
+ subscription: WebhookSubscriptionRow;
38
+ }): import("react/jsx-runtime").JSX.Element;
39
+ export declare function DeleteWebhookSubscriptionButton({ subscriptionId }: {
40
+ subscriptionId: string;
41
+ }): import("react/jsx-runtime").JSX.Element;
42
+ /** The delivery log for one subscription (AUT-33): event, status, response status, attempts, error, time —
43
+ * newest first, loaded only once expanded. */
44
+ export declare function WebhookDeliveryLog({ subscriptionId }: {
45
+ subscriptionId: string;
46
+ }): import("react/jsx-runtime").JSX.Element;
47
+ //# sourceMappingURL=WebhookSubscriptionControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebhookSubscriptionControls.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/WebhookSubscriptionControls.tsx"],"names":[],"mappings":"AAuBA,OAAO,EAIL,KAAK,sBAAsB,EAE5B,MAAM,0BAA0B,CAAC;AAElC;;;;GAIG;AAEH,eAAO,MAAM,oBAAoB,UAA+B,CAAC;AASjE,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CAUhE;AAOD,wBAAgB,0BAA0B,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,2CASxE;AAED,0GAA0G;AAC1G,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,2CAclE;AAqHD;;;;;GAKG;AACH,wBAAgB,+BAA+B,4CA8G9C;AAgBD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,cAAc,EAAE,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,2CAyEvF;AAkBD;;8GAE8G;AAC9G,wBAAgB,oBAAoB,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,sBAAsB,CAAA;CAAE,2CAwE9F;AAQD,wBAAgB,+BAA+B,CAAC,EAAE,cAAc,EAAE,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,2CAkC7F;AA+BD;+CAC+C;AAC/C,wBAAgB,kBAAkB,CAAC,EAAE,cAAc,EAAE,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,2CAiHhF"}