diff --git a/CHANGELOG.md b/CHANGELOG.md index 990d5ab0..8a60cb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable user-visible changes to CASCADE are documented here. The format is l ### Internal +- **PM webhook-UX manifest migration complete (spec 012).** Closes the final gap from spec 011 — every PM wizard step, without exception, now renders via the manifest path. Plans 012/1-3 migrated Trello, JIRA, and Linear webhook steps into per-provider adapters composed of the shared `WebhookUrlDisplayStep` + provider-specific UX: Trello and JIRA each wrap the shared step with a programmatic "Create Webhook" button + active-webhooks list + per-webhook delete + curl fallback template (via existing `webhooks.*` tRPC endpoints with the `{trelloOnly|jiraOnly}` discriminator); Linear wraps it with a "Manual Webhook Setup Required" banner + `ProjectSecretField` (self-managing `LINEAR_WEBHOOK_SECRET` persistence) + 5-step manual-setup instructions. Plan 012/4 deleted the legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo`, the legacy `pm-wizard-webhooks-step.test.ts` file, and the `-webhook` id-skip filter introduced by plan 011/4. `pm-wizard-common-steps.tsx` now only exports `SaveStep`. `pm-wizard.tsx` iterates `manifestDef.steps` without exception; the legacy webhook slot is gone. No operator-visible regression. See spec [012](docs/specs/012-pm-webhook-manifest-migration.md.done). - **PM wizard shared-component migration complete (spec 011).** Migrates the three production PM provider wizards (Trello, JIRA, Linear) off their per-provider step files and onto the shared `StandardStepKind` components landed by spec 010. Five plans landed: shared-components widenings (`container-pick` / `project-scope` `searchable?: boolean` → cmdk `Combobox`; `webhook-url-display` optional inline signing-secret input; 7th `StandardStepKind: custom-field-mapping` wired to `manifest.createCustomField`); Trello migration (OAuth popup stays as `kind: 'custom'` via `TrelloOAuthStep`; `labelDefaults?` + `fieldDefaults?` forward-edit additive widenings pre-populate Create inputs); JIRA migration (task/subtask mapping stays as `kind: 'custom'` via `IssueTypeMappingStep`; free-text label mode exercises the empty-`providerLabels` path); Linear migration (credentials, team picker, status/label/project-scope all shared; webhook step composes shared `WebhookUrlDisplayStep` with `ProjectSecretField` for `LINEAR_WEBHOOK_SECRET`); cleanup (the three `pm-wizard-{trello,jira,linear}-steps.tsx` files deleted, ≈1,085 lines of legacy UI retired). Plan 011/4 also fixed a latent regression plans 011/2 + 011/3 introduced: `pm-wizard.tsx` hardcoded 3 manifest step slots from the spec-006 era; it now iterates over `manifestDef.steps` dynamically, rendering one `WizardStep` per entry. Legacy `WebhookStep` (programmatic webhook registration for Trello/JIRA + Linear signing-secret UX) retained in its own slot — migration into the manifest path is follow-up scope. No operator-visible wizard-UX change beyond consistency: every provider now has searchable pickers, and Trello/JIRA gain inline custom-field create affordances. See spec [011](docs/specs/011-pm-wizard-shared-migration.md.done). - **PM integration hardening follow-ups complete (spec 010).** Finishes the PM-layer cleanup started by spec 009. Three plans landed: generic `pm.discovery.createLabel(providerId, containerId, name, color?)` and `pm.discovery.createCustomField(providerId, containerId, name)` tRPC mutation endpoints + optional `createLabel` / `createCustomField` manifest hooks replace five per-provider wizard call sites; the `currentUser` `DiscoveryCapability` is declared on all three real providers (Trello `/members/me`, JIRA `/rest/api/3/myself`, Linear `viewer`) and served through the unified `pm.discovery.discover` endpoint; six real shared React step components (`credentials`, `container-pick`, `status-mapping`, `label-mapping`, `webhook-url-display`, `project-scope`) now live at `web/src/components/projects/pm-providers/steps/*.tsx` and the wizard generator dispatches to them via a `STANDARD_STEP_COMPONENTS` registry. Existing Trello/JIRA/Linear wizards continue to use their spec-006-era per-provider step adapters; the shared path is additive — a new PM provider with purely-standard steps now writes zero per-provider step components. The `new-provider-surface` snapshot guard is tightened to include the six step files. No operator-visible changes. See spec [010](docs/specs/010-pm-integration-hardening-followups.md.done). - **PM integration hardening (spec 009).** Makes the `PMProviderManifest` a behavioral contract rather than a wiring convention. Five plans landed: branded `StateId` / `LabelId` / `ContainerId` types in `src/pm/ids.ts` (state-name-vs-ID confusion is now a compile error at direct-adapter call sites); manifest-owned Zod `configSchema` for each provider (the central `src/config/schema.ts` imports from `src/integrations/pm//config-schema.ts` — #1138/#1142 drift class becomes a round-trip CI failure); unified `pm.discovery.discover(providerId, capability, args)` tRPC endpoint driven by `manifest.discoveryCapabilities`; behavioral conformance harness (`tests/unit/integrations/pm-conformance.test.ts`) runs round-trip + lifecycle + webhook-verify + trigger-self-hook against every registered provider; single registration entrypoint at `src/integrations/entrypoint.ts` (router, worker, CLI, dashboard all import one file — guarded by `entrypoint-usage.test.ts`); shared `_shared/auth-headers.ts` helpers enforced by provenance test; `tests/unit/pm/linear/regression-2026-04.test.ts` locks in fixes for six Linear bug classes (#1112/#1117/#1118/#1119/#1131/#1133/#1134/#1137/#1138/#1139/#1142). No operator-visible changes. See spec [009](docs/specs/009-pm-integration-hardening.md.done). diff --git a/CLAUDE.md b/CLAUDE.md index be6d20b7..357b70d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ Three separate services, **no monolithic server mode**: Flow: `PM/SCM/alerting webhook → Router → Redis → Worker → TriggerRegistry → Agent → Code → PR`. -Integration abstraction lives in `src/integrations/`. For **adding a new PM provider**, see @src/integrations/README.md — PM providers (Trello, JIRA, Linear) use the `PMProviderManifest` registry with a **behavioral conformance harness** (spec 009 — config round-trip, discovery shape, full lifecycle scenario, auth-header provenance, single-entrypoint invariant). Each provider owns its Zod config schema (`src/integrations/pm//config-schema.ts`) as the single source of truth — the central `src/config/schema.ts` imports it. PM adapter method signatures use branded `StateId` / `LabelId` / `ContainerId` from `src/pm/ids.ts` to make state-name-vs-ID confusion a compile error at direct-adapter call sites. All runtime surfaces (router, worker, CLI, dashboard) register integrations through a single entrypoint at `src/integrations/entrypoint.ts`. **Spec 010 follow-ups** added generic `pm.discovery.createLabel` / `createCustomField` mutation endpoints + `currentUser` discovery capability + real shared React components for every `StandardStepKind` under `web/src/components/projects/pm-providers/steps/`. **Spec 011** migrated all three production providers (Trello, JIRA, Linear) onto those shared components, added a 7th `StandardStepKind: custom-field-mapping`, widened `container-pick` / `project-scope` / `webhook-url-display` with optional props, and deleted the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` files. A new PM provider now writes zero per-provider step UI outside explicit `kind: 'custom'` steps (declared on the manifest and resolved to provider-folder components by the wizard definition). SCM (GitHub) and alerting (Sentry) still use the legacy `IntegrationModule` pattern via self-registration in `src/github/register.ts` + `src/sentry/register.ts`. Don't improvise; the README covers both patterns. +Integration abstraction lives in `src/integrations/`. For **adding a new PM provider**, see @src/integrations/README.md — PM providers (Trello, JIRA, Linear) use the `PMProviderManifest` registry with a **behavioral conformance harness** (spec 009 — config round-trip, discovery shape, full lifecycle scenario, auth-header provenance, single-entrypoint invariant). Each provider owns its Zod config schema (`src/integrations/pm//config-schema.ts`) as the single source of truth — the central `src/config/schema.ts` imports it. PM adapter method signatures use branded `StateId` / `LabelId` / `ContainerId` from `src/pm/ids.ts` to make state-name-vs-ID confusion a compile error at direct-adapter call sites. All runtime surfaces (router, worker, CLI, dashboard) register integrations through a single entrypoint at `src/integrations/entrypoint.ts`. **Spec 010 follow-ups** added generic `pm.discovery.createLabel` / `createCustomField` mutation endpoints + `currentUser` discovery capability + real shared React components for every `StandardStepKind` under `web/src/components/projects/pm-providers/steps/`. **Spec 011** migrated all three production providers (Trello, JIRA, Linear) onto those shared components, added a 7th `StandardStepKind: custom-field-mapping`, widened `container-pick` / `project-scope` / `webhook-url-display` with optional props, and deleted the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` files. **Spec 012** migrated each provider's webhook UX (programmatic create for Trello/JIRA, signing-secret + instructions for Linear) into per-provider manifest webhook adapters (Fragment compositions around the shared `WebhookUrlDisplayStep`); deleted the legacy `WebhookStep` + `LinearWebhookInfoPanel` + `useWebhookManagement` + `useLinearWebhookInfo`. Every PM wizard step now renders via the manifest path without exception. A new PM provider writes zero edits to shared orchestration (`pm-wizard.tsx`, `pm-wizard-common-steps.tsx`, `pm-wizard-hooks.ts`); provider-specific UI ships either as `kind: 'custom'` steps or as Fragment compositions inside the provider folder's wizard adapters. SCM (GitHub) and alerting (Sentry) still use the legacy `IntegrationModule` pattern via self-registration in `src/github/register.ts` + `src/sentry/register.ts`. Don't improvise; the README covers both patterns. ## PR checkout (worker) — gotcha diff --git a/docs/plans/011-pm-wizard-shared-migration/4-linear.md.done b/docs/plans/011-pm-wizard-shared-migration/4-linear.md.done index 890e08ed..05d319d2 100644 --- a/docs/plans/011-pm-wizard-shared-migration/4-linear.md.done +++ b/docs/plans/011-pm-wizard-shared-migration/4-linear.md.done @@ -210,10 +210,10 @@ Originally out of scope for the spec (repeated for clarity): - [x] AC #1 (all 6 Linear standard steps render via the generator in the refactored pm-wizard.tsx) - [x] AC #2 (team picker passes `searchable: true`) -- [ ] AC #3 — **partial**: `LinearWebhookDisplayAdapter` composes shared `WebhookUrlDisplayStep` + `ProjectSecretField` for `LINEAR_WEBHOOK_SECRET`. Currently dormant; legacy `WebhookStep` still renders Linear's webhook UX. Functionality is preserved via legacy path; the shared path activates in a future plan. +- [x] AC #3 — **closed downstream by spec 012** (plans 012/3 + 012/4): `LinearWebhookAdapter` renders via the manifest path; `ProjectSecretField` for `LINEAR_WEBHOOK_SECRET` is inline; legacy `WebhookStep` deleted. Originally landed partial because the widened shared step was dormant; spec 012 activated it. - [x] AC #4 (project-scope via shared component; spec 005 preserved) - [x] AC #5 (label-mapping dropdown mode + Create affordance with `LINEAR_LABEL_DEFAULTS`) -- [ ] AC #6 — **partial**: `linear-webhook-info-panel.test.ts` deleted; `LinearWebhookInfoPanel` still referenced by legacy `WebhookStep`. Full retirement follows webhook-creation migration. +- [x] AC #6 — **closed downstream by spec 012** (plan 012/4): `LinearWebhookInfoPanel` + legacy `WebhookStep` both deleted. Webhook-creation migration completed; retirement is total. - [x] AC #7 (`pm-wizard-linear-steps.tsx` retained with "Retained until 011/5" marker; no live importers) - [x] AC #8 (8 new tests in `linear-wizard-generator.test.ts`; 3 legacy step tests deleted net -450 lines) - [x] AC #9 (conformance harness green) diff --git a/docs/plans/011-pm-wizard-shared-migration/_coverage.md b/docs/plans/011-pm-wizard-shared-migration/_coverage.md index 87855a49..ebb562d5 100644 --- a/docs/plans/011-pm-wizard-shared-migration/_coverage.md +++ b/docs/plans/011-pm-wizard-shared-migration/_coverage.md @@ -8,7 +8,7 @@ Auto-generated by /plan. Tracks which plans satisfy which spec ACs. |---|---|---|---| | 1 | Trello wizard on shared components + OAuth custom step | plan 1 (shared-components) + plan 2 (trello) | partial chain | | 2 | JIRA wizard on shared components + issue-type custom step | plan 1 + plan 3 (jira) | partial chain | -| 3 | Linear wizard on shared components + inline secret + project-scope | plan 1 + plan 4 (linear) | partial chain | +| 3 | Linear wizard on shared components + inline secret + project-scope | plan 1 + plan 4 (linear); ✅ fully closed by spec 012 downstream (legacy `WebhookStep`/`LinearWebhookInfoPanel` deleted; inline secret renders via manifest path) | partial chain → **closed by spec 012** | | 4 | Three `pm-wizard-{trello,jira,linear}-steps.tsx` files deleted | plan 5 (cleanup) | full | | 5 | No operator regression per provider | plan 2 (Trello) + plan 3 (JIRA) + plan 4 (Linear) | full per provider | | 6 | UX normalized upward (searchable pickers, inline create) | plan 1 (capability) + plans 2/3/4 (per-provider activation) | partial chain per provider | diff --git a/docs/plans/012-pm-webhook-manifest-migration/1-trello-webhook.md.done b/docs/plans/012-pm-webhook-manifest-migration/1-trello-webhook.md.done new file mode 100644 index 00000000..c4e0a5be --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/1-trello-webhook.md.done @@ -0,0 +1,190 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 1 +plan_slug: trello-webhook +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [] +status: done +--- + +# 012/1: Trello Webhook — Migrate Creation UX Into Manifest Path + +> Part 1 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +First of three per-provider migrations. Moves Trello's webhook-creation UX from the legacy `WebhookStep` (in `pm-wizard-common-steps.tsx`) into a new Trello webhook adapter rendered from the manifest path. Operator-visible: Trello's webhook step now lives as a peer step in the wizard's dynamic slot list, rendered from the Trello provider folder. Trello's programmatic Create button, active-webhooks list, delete buttons, and curl fallback all continue to work — same tRPC endpoints, same backend behavior, different render path. + +Mechanism: extend `pm-wizard.tsx`'s `-webhook` id-skip filter (introduced by plan 011/4) to exclude Trello specifically — `filter(entry => !(entry.step.id === 'jira-webhook' || entry.step.id === 'linear-webhook'))`. The legacy `WebhookStep` keeps rendering for JIRA + Linear until plans 2 + 3 land; plan 4 then deletes the filter entirely. + +**Components delivered:** +- `web/src/components/projects/pm-providers/trello/webhook-step.tsx` — new file. `TrelloWebhookAdapter` React component. Renders the shared `WebhookUrlDisplayStep` inside a Fragment, composed with Trello-specific UI: active-webhooks list, "Create Webhook" button, delete buttons per active webhook, curl-fallback `
` block. All tRPC calls (`webhooks.list`, `webhooks.create`, `webhooks.delete` with `trelloOnly: true`) happen inside the adapter's `useProviderHooks` slice or inline. +- `web/src/components/projects/pm-providers/trello/wizard.ts` — replace `TrelloWebhookDisplayAdapter` Component (currently renders just the shared step) with the new `TrelloWebhookAdapter`. Extend `useProviderHooks` return shape with Trello-webhook-specific values + callbacks (`activeTrelloWebhooks`, `createTrelloWebhook`, `deleteTrelloWebhook`, `webhookCreateLoading`, etc.). +- `web/src/components/projects/pm-wizard.tsx` — update the filter from `id.endsWith('-webhook')` to `id === 'jira-webhook' || id === 'linear-webhook'`. One-line change. +- `tests/unit/web/trello-webhook-step.test.ts` — new file. Assertions on the rendered adapter: active-list shape, Create button presence + disabled-state, curl command interpolation with `trelloBoardId`, delete-button data-action presence. + +**Deferred to later plans in this spec:** +- JIRA webhook migration — plan 2. +- Linear webhook migration — plan 3. +- Legacy `WebhookStep` + `LinearWebhookInfoPanel` deletion — plan 4. +- `useWebhookManagement` + `useLinearWebhookInfo` hook deletion — plan 4. +- `-webhook` filter entire-removal from `pm-wizard.tsx` — plan 4. +- README / CLAUDE.md / CHANGELOG / spec-011 forward-reference / coverage-map updates — plan 4. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #2 (Trello programmatic create/list/delete/curl via manifest path) — **full**. +- Spec AC #1 (every wizard renders webhook via manifest) — **partial** (Trello done; JIRA + Linear in plans 2, 3). +- Spec AC #8 (no operator regression) — **full for Trello**. +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +No plan dependencies. Builds on the existing shared `WebhookUrlDisplayStep` (spec 011) and the existing `webhooks.*` tRPC endpoints. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy `WebhookStep` Trello branch to enumerate behaviors to preserve + +Read `web/src/components/projects/pm-wizard-common-steps.tsx` (the `WebhookStep` function) and `web/src/components/projects/pm-wizard-hooks.ts` (the `useWebhookManagement` hook). Catalog every Trello-specific rendering + mutation — Create button state machine, curl template variables, delete button extraction logic, error display. Output the inventory as a checklist under this task in the `.wip` file. Any behavior that can't map to the Fragment-composition pattern forces a spec divergence note + user sign-off. + +### 2. Trello webhook adapter component + +**Tests first** (`tests/unit/web/trello-webhook-step.test.ts` — new file): +- `renders the shared WebhookUrlDisplayStep (URL + copy button)` — assert `data-step-component="webhook-url-display"` in SSR. +- `renders the active-webhooks list when provided` — assert each active webhook appears with its URL. +- `renders "No Trello webhooks configured" when activeTrelloWebhooks is empty` — assert fallback text. +- `renders the "Create Webhook" button with data-action="create-webhook"` — assert element + attribute. +- `disables the Create button when callbackBaseUrl is empty` — assert `disabled="disabled"` via attribute-agnostic regex. +- `renders the curl fallback with trelloBoardId interpolated into the curl template` — render with `trelloBoardId: 'board-xyz'`, assert body contains `"idModel": "board-xyz"`. +- `falls back to placeholder when trelloBoardId is empty` — render with empty boardId, assert placeholder appears. +- `renders delete buttons (data-action="delete-webhook") per active webhook` — count matches. +- `does not render LinearWebhookInfoPanel or Linear signing-secret field` — regression guard. + +**Implementation** (`web/src/components/projects/pm-providers/trello/webhook-step.tsx`): +- Named export: `TrelloWebhookAdapter: React.FC`. Signature matches `ProviderWizardStepProps`. +- Reads `providerHooks` via the existing `asTrelloHooks` adapter (extend the `TrelloProviderHooks` interface). +- Renders Fragment of: `WebhookUrlDisplayStep` (with `webhookUrl` from providerHooks), then a `
` with active-webhooks list rendered from `providerHooks.activeTrelloWebhooks`, then the Create button wired to `providerHooks.createTrelloWebhook`, then delete buttons wired to `providerHooks.deleteTrelloWebhook(id)`, then a `
` with the curl command constructed inline from `state.trelloBoardId` + `providerHooks.callbackBaseUrl`. +- All DOM elements carry provider-agnostic data attributes (`data-action`, `data-step-component`, …) so tests don't pin class names. + +### 3. Extend Trello wizard's `useProviderHooks` + +**Tests first**: covered by task 2's SSR tests — they exercise the adapter + providerHooks plumbing end-to-end. + +**Implementation** (`web/src/components/projects/pm-providers/trello/wizard.ts`): +- Extend `TrelloProviderHooks` interface: add `activeTrelloWebhooks`, `webhooksLoading`, `createTrelloWebhook`, `createLoading`, `createError`, `deleteTrelloWebhook`, `deleteLoading`, `callbackBaseUrl`. +- Inside `useProviderHooks`, call `useQuery(trpc.webhooks.list.queryOptions({ projectId }))` to fetch active webhooks. Normalize via `deriveActiveWebhooks(state.provider, ...)`. +- Add `createTrelloWebhook = () => createMutation.mutate({ projectId, callbackBaseUrl, trelloOnly: true })` and `deleteTrelloWebhook = (baseUrl: string) => deleteMutation.mutate({ projectId, callbackBaseUrl: baseUrl, trelloOnly: true })` using `useMutation`. +- Compute `callbackBaseUrl` from `API_URL || window.location.origin.replace(':5173', ':3000')` — same inline formula as `useWebhookManagement`. (Don't call `useWebhookManagement` directly; it gets deleted in plan 4.) +- Replace the existing `TrelloWebhookDisplayAdapter` step Component in `trelloProviderWizard.steps` with the new `TrelloWebhookAdapter`. + +### 4. Flip the `-webhook` id-skip filter to exclude Trello + +**Tests first** (`tests/unit/web/pm-wizard-webhook-filter.test.ts` — new file; can reuse an existing test file if one fits): +- `pm-wizard.tsx filter excludes trello-webhook from skip list` — import the filter predicate (extract as a named export if needed) and assert it returns `true` for `{id: 'trello-webhook'}`. +- Alternative if extracting the predicate is too invasive: assert via snapshot of the render pipeline (mount `PMWizard` with a Trello project; assert the Trello webhook step renders). + +**Implementation** (`web/src/components/projects/pm-wizard.tsx`): +- Update the filter from `.filter((entry) => !entry.step.id.endsWith('-webhook'))` to `.filter((entry) => entry.step.id !== 'jira-webhook' && entry.step.id !== 'linear-webhook')`. +- Update the surrounding comment block to note the migration is in-flight (plan 012/1 migrated Trello; plans 012/2-3 migrate JIRA + Linear; plan 012/4 deletes the filter). + +### 5. Conformance harness smoke + +No code change; run `npx vitest run --project unit-core tests/unit/integrations/pm-conformance.test.ts` to confirm Trello's behavioral contract (unchanged in this plan — pure frontend wiring) still passes. + +### 6. Manual dashboard verification + +Per CLAUDE.md: start the dev server and exercise the Trello wizard in a browser. Verify: +- Trello's webhook step now appears as a peer step (its own `WizardStep` slot), not inside the legacy WebhookStep slot. +- Legacy WebhookStep still renders for JIRA + Linear (not for Trello). +- Create Webhook button registers a webhook; active list updates; delete removes it. +- Curl fallback shows with the current board ID interpolated. + +AC #8 (no regression) is browser-verifiable here; flag as deferred if unavailable. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/trello-webhook-step.test.ts` — new file, ~9 tests. +- [ ] `tests/unit/web/pm-wizard-webhook-filter.test.ts` (or integrated into existing wizard test) — 1 test pinning the new filter predicate. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for Trello. +- [ ] Browser smoke test — Trello wizard renders with peer webhook step, JIRA + Linear retain legacy WebhookStep. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `trelloProviderWizard.steps`'s `trello-webhook` entry has `Component = TrelloWebhookAdapter` (identity-asserted). +2. `TrelloWebhookAdapter` renders the shared `WebhookUrlDisplayStep` + Trello-specific UI (active list, Create button, curl fallback, delete buttons). +3. The `pm-wizard.tsx` filter now excludes Trello — `trello-webhook` passes through the manifest iteration (asserted via predicate test or render snapshot). +4. Legacy `WebhookStep` continues to render for JIRA + Linear (not for Trello) — no regression for the un-migrated providers. +5. Every previously-available Trello webhook action (Create / active list / delete / curl) is exercised in tests against the new adapter. +6. Conformance harness (`pm-conformance.test.ts`) passes for Trello. +7. `npm run build` passes. +8. `npm test` passes. +9. `npm run lint` passes. +10. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — all doc updates deferred to plan 4 (cleanup) to reflect the final state in one pass. + +| File | Change | +|---|---| +| — | Deferred to plan 4. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- JIRA webhook migration — plan 2. +- Linear webhook migration — plan 3. +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- `-webhook` filter full removal + legacy test file deletion — plan 4. +- README / CLAUDE.md / CHANGELOG / spec-011 forward-reference / `_coverage.md` updates — plan 4. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.create/list/delete` tRPC endpoints beyond their `{trelloOnly, jiraOnly}` flags. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM or alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or the `ProviderWizardDefinition` contract. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + +- [x] AC #1 (`trelloProviderWizard.steps[trello-webhook].Component = TrelloWebhookAdapter`) +- [x] AC #2 (Fragment composes shared `WebhookUrlDisplayStep` + active list + Create + curl + delete) +- [x] AC #3 (`pm-wizard.tsx` filter: `id !== 'jira-webhook' && id !== 'linear-webhook'`) +- [x] AC #4 (legacy `WebhookStep` still renders for JIRA + Linear; no code change to the legacy slot) +- [x] AC #5 (10 Trello-webhook tests covering URL / active list / Create / curl / delete / regression guard) +- [x] AC #6 (conformance harness green — full suite 8177/8177 passes) +- [x] AC #7 (`npm run build` green) +- [x] AC #8 (`npm test` green — 8177/8177) +- [x] AC #9 (`npm run lint` green) +- [x] AC #10 (`npm run typecheck` green) diff --git a/docs/plans/012-pm-webhook-manifest-migration/2-jira-webhook.md.done b/docs/plans/012-pm-webhook-manifest-migration/2-jira-webhook.md.done new file mode 100644 index 00000000..20ada755 --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/2-jira-webhook.md.done @@ -0,0 +1,191 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 2 +plan_slug: jira-webhook +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [1-trello-webhook.md] +status: done +--- + +# 012/2: JIRA Webhook — Migrate Creation UX Into Manifest Path + +> Part 2 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +Second of three per-provider migrations. Mirrors plan 012/1's Trello migration for JIRA. Moves JIRA's webhook-creation UX (programmatic Create button + active list + delete + curl fallback + `jiraEnsureLabels` side-effect) from the legacy `WebhookStep` into a new JIRA webhook adapter rendered from the manifest path. + +Mechanism mirrors plan 1: adjust `pm-wizard.tsx`'s filter to exclude JIRA from the skip list, leaving only Linear still routed through the legacy WebhookStep. Plan 3 completes the Linear migration; plan 4 deletes the filter. + +**Components delivered:** +- `web/src/components/projects/pm-providers/jira/webhook-step.tsx` — new file. `JiraWebhookAdapter` React component. Same Fragment-composition shape as `TrelloWebhookAdapter`: shared `WebhookUrlDisplayStep` + active-webhooks list + "Create Webhook" button + curl fallback. Curl template uses JIRA's REST API v3 shape with `jiraBaseUrl` interpolated. Delete button per active webhook. `jiraEnsureLabels` side-effect preserved via the existing `webhooks.create` mutation call path (no change to backend). +- `web/src/components/projects/pm-providers/jira/wizard.ts` — extend `JiraProviderHooks` interface with JIRA-webhook-specific fields; in `useProviderHooks`, add `useQuery(trpc.webhooks.list…)` + create/delete `useMutation` calls with `jiraOnly: true`; replace `JiraWebhookDisplayAdapter` step Component with the new `JiraWebhookAdapter`. +- `web/src/components/projects/pm-wizard.tsx` — update filter to `entry.step.id !== 'linear-webhook'`. +- `tests/unit/web/jira-webhook-step.test.ts` — new file. Assertions mirroring `trello-webhook-step.test.ts` but scoped to JIRA. + +**Deferred to later plans in this spec:** +- Linear webhook migration — plan 3. +- Legacy `WebhookStep` deletion + full filter removal — plan 4. +- Docs + coverage-map updates — plan 4. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #3 (JIRA programmatic create/list/delete/curl + `jiraEnsureLabels`) — **full**. +- Spec AC #1 (every wizard renders webhook via manifest) — **partial** (Trello + JIRA done; Linear in plan 3). +- Spec AC #8 (no operator regression) — **full for JIRA**. +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 1 (`trello-webhook`) — establishes the Fragment-composition pattern JIRA mirrors. Also flushes out any shared-component gap Trello would have revealed; JIRA inherits the fix. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy `WebhookStep` JIRA branch + +Same pre-flight as plan 1, for JIRA. Catalog every JIRA-specific rendering + mutation path in the legacy `WebhookStep`. Identify the `jiraEnsureLabels` side-effect call path. Output the inventory as a checklist under this task in the `.wip` file. + +### 2. JIRA webhook adapter component + +**Tests first** (`tests/unit/web/jira-webhook-step.test.ts` — new file): +- `renders the shared WebhookUrlDisplayStep` — asserts presence. +- `renders active-webhooks list when provided`. +- `renders "No JIRA webhooks configured" fallback when empty`. +- `renders the "Create Webhook" button with data-action="create-webhook"`. +- `disables Create button when callbackBaseUrl is empty`. +- `renders the curl fallback with jiraBaseUrl interpolated` — asserts body contains `"url": "{jiraBaseUrl}/rest/api/3/webhook"` shape. +- `falls back to placeholder when jiraBaseUrl is empty`. +- `renders delete buttons per active webhook`. +- `does not render LinearWebhookInfoPanel or Linear signing-secret field`. + +**Implementation** (`web/src/components/projects/pm-providers/jira/webhook-step.tsx`): +- Named export: `JiraWebhookAdapter: React.FC`. +- Fragment composing shared `WebhookUrlDisplayStep` + active-webhooks list + Create button + curl fallback. +- Curl template uses the JIRA REST API v3 POST shape, with events `['jira:issue_created', 'jira:issue_updated', 'comment_created', 'comment_updated']` and Basic-auth placeholder for email + API token. +- The Create button wiring calls `providerHooks.createJiraWebhook` which runs `webhooks.create` with `jiraOnly: true` — this triggers the backend-side `jiraEnsureLabels` side-effect unchanged. + +### 3. Extend JIRA wizard's `useProviderHooks` + +**Tests first**: covered by task 2's SSR tests. + +**Implementation** (`web/src/components/projects/pm-providers/jira/wizard.ts`): +- Extend `JiraProviderHooks` with `activeJiraWebhooks`, `webhooksLoading`, `createJiraWebhook`, `createLoading`, `createError`, `deleteJiraWebhook`, `deleteLoading`, `callbackBaseUrl`. +- Inside `useProviderHooks`, add `useQuery(trpc.webhooks.list.queryOptions({ projectId }))` and normalize via `deriveActiveWebhooks`. Add create/delete `useMutation` calls with `jiraOnly: true`. +- Replace the existing `JiraWebhookDisplayAdapter` Component in `jiraProviderWizard.steps` with the new `JiraWebhookAdapter`. +- Compute `callbackBaseUrl` inline (same formula as plan 1's Trello adapter) — do not consume `useWebhookManagement` (which gets deleted in plan 4). + +### 4. Update the `-webhook` id-skip filter to exclude JIRA + +**Tests first**: extend the existing filter test from plan 1 with a row for JIRA. + +**Implementation** (`web/src/components/projects/pm-wizard.tsx`): +- Update filter to `entry.step.id !== 'linear-webhook'`. +- Update the comment block. + +### 5. Conformance harness smoke + +No code change; verify harness passes. + +### 6. Manual dashboard verification + +Per CLAUDE.md: browser smoke test. Verify: +- JIRA wizard's webhook step now renders as a peer step (not inside legacy WebhookStep). +- Legacy WebhookStep still renders for Linear only (not Trello, not JIRA). +- Create / active list / delete / curl all work. +- `jiraEnsureLabels` fires (check Atlassian side-effect — first issue gets + loses CASCADE labels on successful Create). + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/jira-webhook-step.test.ts` — new file, ~9 tests. +- [ ] Filter-predicate test extended with JIRA row. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for JIRA. +- [ ] Browser smoke test — JIRA wizard renders with peer webhook step. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `jiraProviderWizard.steps`'s `jira-webhook` entry has `Component = JiraWebhookAdapter`. +2. `JiraWebhookAdapter` renders the shared `WebhookUrlDisplayStep` + JIRA-specific UI (active list, Create button, curl fallback, delete buttons). +3. `jiraEnsureLabels` side-effect preserved — verified by confirming the Create button calls the existing `webhooks.create({ jiraOnly: true })` path (backend unchanged). +4. `pm-wizard.tsx` filter now excludes JIRA; `jira-webhook` passes through the manifest iteration. +5. Legacy `WebhookStep` continues to render for Linear only. +6. Every previously-available JIRA webhook action is exercised in tests against the new adapter. +7. Conformance harness green for JIRA. +8. `npm run build` passes. +9. `npm test` passes. +10. `npm run lint` passes. +11. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — deferred to plan 4. + +| File | Change | +|---|---| +| — | Deferred to plan 4. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Linear webhook migration — plan 3. +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- `-webhook` filter full removal + legacy test file deletion — plan 4. +- Docs / coverage-map updates — plan 4. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.*` tRPC endpoints. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM / alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or `ProviderWizardDefinition`. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + + +> **Note on curl template**: spec and plan text mentioned JIRA REST API v3 +> shape (`/rest/api/3/webhook`). The legacy `WebhookStep` actually used +> the older v1 endpoint (`/rest/webhooks/1.0/webhook`). Preserved v1 +> verbatim to satisfy zero-regression; migrating to v3 would require a +> coordinated payload change (different event names + JQL filter shape) +> and is out of scope. Documented in the adapter source. + +- [x] AC #1 (`jiraProviderWizard.steps[jira-webhook].Component = JiraWebhookAdapter`) +- [x] AC #2 (Fragment composes shared `WebhookUrlDisplayStep` + active list + Create + curl + delete) +- [x] AC #3 (`jiraEnsureLabels` side-effect preserved — server-side; adapter calls `webhooks.create({jiraOnly:true})` unchanged) +- [x] AC #4 (`pm-wizard.tsx` filter: `id !== 'linear-webhook'`) +- [x] AC #5 (legacy `WebhookStep` still renders for Linear only; no code change to legacy slot) +- [x] AC #6 (10 JIRA-webhook tests covering URL / active list / Create / curl / delete / regression guard) +- [x] AC #7 (conformance harness green — full suite 8187/8187) +- [x] AC #8 (`npm run build` green) +- [x] AC #9 (`npm test` green — 8187/8187) +- [x] AC #10 (`npm run lint` green) +- [x] AC #11 (`npm run typecheck` green) diff --git a/docs/plans/012-pm-webhook-manifest-migration/3-linear-webhook.md.done b/docs/plans/012-pm-webhook-manifest-migration/3-linear-webhook.md.done new file mode 100644 index 00000000..78bfba06 --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/3-linear-webhook.md.done @@ -0,0 +1,186 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 3 +plan_slug: linear-webhook +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [2-jira-webhook.md] +status: done +--- + +# 012/3: Linear Webhook — Activate Manifest Path With Signing-Secret + Instructions + +> Part 3 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +Third and final per-provider migration. Activates the `LinearWebhookDisplayAdapter` that plan 011/4 already shipped (Fragment composing shared `WebhookUrlDisplayStep` + `ProjectSecretField` for `LINEAR_WEBHOOK_SECRET`) and extends it with the 5-step manual setup-instructions list currently owned by the legacy `LinearWebhookInfoPanel`. Removes the last remaining id in the `-webhook` filter so Linear's webhook step passes through the manifest iteration. + +Smaller than plans 1 + 2 — Linear has no programmatic registration (no Create button, no active list, no delete, no curl fallback — Linear's API forbids it). The secret-field + instructions list are the only UI to migrate. The adapter skeleton already exists from plan 011/4; this plan completes it and wires it up. + +**Components delivered:** +- `web/src/components/projects/pm-providers/linear/webhook-step.tsx` — new file (or extend the dormant `LinearWebhookDisplayAdapter` currently inlined in `linear/wizard.ts`). Named export: `LinearWebhookAdapter: React.FC`. Fragment composing shared `WebhookUrlDisplayStep` + a new setup-instructions block (ordered list, 5 items matching the current `LinearWebhookInfoPanel` copy) + `ProjectSecretField` bound to `LINEAR_WEBHOOK_SECRET`. +- `web/src/components/projects/pm-providers/linear/wizard.ts` — replace the inlined `LinearWebhookDisplayAdapter` with the new extracted component. Extend `LinearProviderHooks` only if new fields are needed beyond what plan 011/4 declared (`projectIdForSecret`, `webhookSecretCredential`). Existing plumbing likely covers it. +- `web/src/components/projects/pm-wizard.tsx` — remove the conditional filter entirely, replacing `filter((entry) => entry.step.id !== 'linear-webhook')` with **no filter** (i.e. remove the `.filter(...)` call from `renderedManifestSteps` construction). The manifest iteration now includes every step. **Note**: plan 4 owns the deletion of the legacy `WebhookStep` slot and the `useWebhookManagement`/`useLinearWebhookInfo` hook imports. This plan leaves those in place but with no provider routed to them; the legacy slot becomes a no-op (renders but no provider's state matches the branch conditions inside `WebhookStep`). Plan 4 deletes it. +- `tests/unit/web/linear-webhook-step.test.ts` — new file. Assertions on the rendered adapter: `ProjectSecretField` rendered with `envVarKey="LINEAR_WEBHOOK_SECRET"`, the 5-step instructions list present, webhook URL + copy button preserved. + +**Deferred to later plans in this spec:** +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- Legacy test file deletion — plan 4. +- Docs / coverage-map updates — plan 4. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #4 (Linear signing-secret + instructions inline via manifest) — **full**. +- Spec AC #1 (every wizard renders webhook via manifest) — **partial** (Linear closes the chain; plan 4 removes the filter stopgap). +- Spec AC #7 (plan 011/4 ACs #3 + #6 close) — **partial** (manifest path now renders Linear's webhook inline; the deletion of `LinearWebhookInfoPanel` + `_coverage.md` update land in plan 4). +- Spec AC #8 (no operator regression) — **full for Linear**. +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 2 (`jira-webhook`) — establishes two-provider track record of the Fragment-composition pattern; any carry-forward widening from plans 1/2 is already in place. +- Plan 1 (`trello-webhook`) — transitively; the Fragment pattern originated there. + +--- + +## Detailed Task List (TDD) + +### 1. Re-read the legacy `LinearWebhookInfoPanel` + `useLinearWebhookInfo` hook + +Catalog every piece of UI the legacy panel renders: the blue-info banner, the 5-step instructions list, the `ProjectSecretField`, the webhook URL display. Confirm the URL formula (`${callbackBaseUrl}/linear/webhook`) matches what `webhookUrl` carries in the Linear wizard's `useProviderHooks`. Output checklist under this task in the `.wip` file. + +### 2. Extract the Linear webhook adapter component + +**Tests first** (`tests/unit/web/linear-webhook-step.test.ts` — new file): +- `renders the shared WebhookUrlDisplayStep with webhookUrl prop`. +- `renders ProjectSecretField with envVarKey="LINEAR_WEBHOOK_SECRET"`. +- `renders ProjectSecretField with the threaded credential metadata` (when `webhookSecretCredential` is populated). +- `renders a 5-item ordered setup-instructions list`. +- `includes the linear.app/settings/api link in the instructions`. +- `does not render Trello/JIRA UI elements (Create button, active-webhooks list)` — regression guard. + +**Implementation** (`web/src/components/projects/pm-providers/linear/webhook-step.tsx`): +- Named export: `LinearWebhookAdapter: React.FC`. +- Fragment composing: shared `WebhookUrlDisplayStep` + a `
` with the info banner + an ordered list of 5 setup steps (copy lifted verbatim from `LinearWebhookInfoPanel`) + `ProjectSecretField` bound to `LINEAR_WEBHOOK_SECRET` with the threaded credential. +- The 5-step instructions are hardcoded in this component (Linear-specific). + +### 3. Wire the new adapter into `linearProviderWizard.steps` + +**Implementation** (`web/src/components/projects/pm-providers/linear/wizard.ts`): +- Replace the inlined `LinearWebhookDisplayAdapter` (currently a Fragment of `WebhookUrlDisplayStep` + `ProjectSecretField`) with an import of the new `LinearWebhookAdapter` from `./webhook-step.js`. +- Update the `linear-webhook` step's `Component` reference. +- Confirm `providerHooks` already returns `projectIdForSecret` + `webhookSecretCredential` (plan 011/4 added these) — no new fields expected. + +### 4. Remove the `-webhook` filter from `pm-wizard.tsx` + +**Tests first**: +- Filter-predicate test from plans 1 + 2: update to confirm no step is filtered out now. +- Render-snapshot test (if present) for Linear — confirm the Linear webhook step renders as a peer step. + +**Implementation** (`web/src/components/projects/pm-wizard.tsx`): +- Remove the `.filter(...)` call from `renderedManifestSteps` construction. `renderedManifestSteps` becomes `manifestDef ? manifestDef.steps.map((step, index) => ({ step, index })) : []`. +- Update the surrounding comment to note the migration is complete and the legacy `WebhookStep` slot is now a no-op that plan 4 will delete. + +### 5. Legacy `WebhookStep` slot behavior post-plan-3 + +No code change in this plan beyond removing the filter. The legacy `WebhookStep` still renders in its hardcoded slot; with no provider entering the Trello / JIRA / Linear conditional branches (all three now own their webhook UX in the manifest path), the slot renders a narrow artifact — likely the "No webhooks configured" fallback or similar. Document this in the commit message + PR description as transient visual noise; plan 4 deletes the slot. + +### 6. Conformance harness smoke + manual dashboard verification + +Per CLAUDE.md: browser smoke test. +- Linear wizard renders the webhook step via the manifest path with signing-secret + instructions inline. +- Trello + JIRA + Linear all use manifest-path webhooks; the legacy `WebhookStep` slot renders but is effectively empty (transient; plan 4 deletes). +- Secret-field save / load / clear cycle via `ProjectSecretField` works unchanged. + +--- + +## Test Plan + +### Unit tests +- [ ] `tests/unit/web/linear-webhook-step.test.ts` — new file, ~6 tests. +- [ ] Filter-predicate test: update to assert no step is filtered out. + +### Integration tests +- None. + +### Acceptance tests +- [ ] Conformance harness passes for Linear. +- [ ] `tests/unit/pm/linear/regression-2026-04.test.ts` passes (adapter untouched). +- [ ] Browser smoke test — Linear webhook step renders inline; secret-field save / load works. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `linearProviderWizard.steps`'s `linear-webhook` entry has `Component = LinearWebhookAdapter` (identity-asserted). +2. `LinearWebhookAdapter` renders the shared `WebhookUrlDisplayStep` + `ProjectSecretField(envVarKey=LINEAR_WEBHOOK_SECRET)` + 5-step instructions list. +3. The `-webhook` filter in `pm-wizard.tsx` is removed; `manifestDef.steps` iterates in full. +4. Legacy `WebhookStep` slot still renders but no provider is routed to its Trello / JIRA / Linear branches — deletion deferred to plan 4. +5. Every Linear webhook behavior (URL display + copy, signing-secret save/load/clear, 5-step instructions) is exercised in tests against the new adapter. +6. Conformance harness passes for Linear. +7. `regression-2026-04.test.ts` passes (adapter unchanged). +8. `npm run build` passes. +9. `npm test` passes. +10. `npm run lint` passes. +11. `npm run typecheck` passes. + +--- + +## Documentation Impact (this plan only) + +None — deferred to plan 4. + +| File | Change | +|---|---| +| — | Deferred to plan 4. | + +--- + +## Out of Scope (this plan) + +Deferred to later plans in this spec: +- Legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo` deletion — plan 4. +- Legacy test file deletion — plan 4. +- Docs / coverage-map updates — plan 4. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.*` tRPC endpoints. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM / alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or `ProviderWizardDefinition`. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + + +> **Note on test mock**: `ProjectSecretField` uses `useQueryClient` +> internally which pulls React from `web/node_modules` (different +> instance from the root-aliased React the test env uses). SSR crashes on +> the null Context hook. Same pattern as plan 011/1's Combobox — mocked +> `ProjectSecretField` to a deterministic stub preserving props. + +- [x] AC #1 (`linearProviderWizard.steps[linear-webhook].Component = LinearWebhookAdapter`) +- [x] AC #2 (Fragment composes shared `WebhookUrlDisplayStep` + info banner + 5-step instructions + `ProjectSecretField(envVarKey=LINEAR_WEBHOOK_SECRET)`) +- [x] AC #3 (`-webhook` filter removed from `pm-wizard.tsx`; `manifestDef.steps` iterates in full) +- [x] AC #4 (legacy `WebhookStep` slot still renders but no provider is routed to its branches; plan 4 deletes) +- [x] AC #5 (7 Linear-webhook tests covering URL display, secret-field presence + absence, 5-step instructions, linear.app link, info banner, Trello/JIRA-UI regression guard) +- [x] AC #6 (conformance harness green — full suite 8194/8194) +- [x] AC #7 (`regression-2026-04.test.ts` passes — adapter untouched) +- [x] AC #8 (`npm run build` green) +- [x] AC #9 (`npm test` green — 8194/8194) +- [x] AC #10 (`npm run lint` green) +- [x] AC #11 (`npm run typecheck` green) diff --git a/docs/plans/012-pm-webhook-manifest-migration/4-cleanup.md.done b/docs/plans/012-pm-webhook-manifest-migration/4-cleanup.md.done new file mode 100644 index 00000000..82d26ecf --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/4-cleanup.md.done @@ -0,0 +1,240 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +plan: 4 +plan_slug: cleanup +level: plan +parent_spec: docs/specs/012-pm-webhook-manifest-migration.md +depends_on: [3-linear-webhook.md] +status: done +--- + +# 012/4: Cleanup — Delete Legacy Webhook UX + Rewrite Docs + Close Spec + +> Part 4 of 4 in the 012-pm-webhook-manifest-migration plan. See [parent spec](../../specs/012-pm-webhook-manifest-migration.md). + +## Summary + +Closes spec 012. Deletes the legacy webhook surface now that all three providers own their webhook steps via the manifest path: + +- `WebhookStep` + `LinearWebhookInfoPanel` from `pm-wizard-common-steps.tsx` (the remaining two exports; `SaveStep` stays as the only live export). +- `useWebhookManagement` + `useLinearWebhookInfo` from `pm-wizard-hooks.ts`. +- `tests/unit/web/pm-wizard-webhooks-step.test.ts` (181 lines, 8 tests — all assertions now live in the three per-provider adapter tests from plans 012/1-3). +- The legacy-webhook slot rendering in `pm-wizard.tsx` (the entire `` block + `useWebhookManagement` / `useLinearWebhookInfo` imports + related state). + +Also rewrites the docs to reflect the fully-migrated state (same plan-5-style cleanup pattern from spec 011) and updates `docs/plans/011-pm-wizard-shared-migration/_coverage.md` to mark plan 011/4 ACs #3 + #6 as fully closed by this spec downstream. + +Cleanup-only plan. Zero new features, zero widenings, zero behavior changes. Deletions + prose. + +**Components delivered:** +- `web/src/components/projects/pm-wizard-common-steps.tsx` — `WebhookStep` + `LinearWebhookInfoPanel` deleted. File retained as `SaveStep` is still a live export. +- `web/src/components/projects/pm-wizard-hooks.ts` — `useWebhookManagement` + `useLinearWebhookInfo` deleted. +- `web/src/components/projects/pm-wizard.tsx` — legacy webhook `WizardStep` slot deleted; associated imports + call-sites removed. `webhookStepNumber` / `saveStepNumber` simplified: the save step becomes `renderedManifestSteps.length + 2`. +- `tests/unit/web/pm-wizard-webhooks-step.test.ts` — **deleted**. +- `src/integrations/README.md` — rewrite: + - Four-specs → five-specs paragraph (add spec 012). + - Update the provider migration status table: Trello/JIRA/Linear all show `✅ manifest-driven webhook step`. + - Add a Post-spec-012 additions peer section. + - Update the "Adding a new PM provider" section's webhook guidance: compose the shared step with provider-specific UX via Fragment; no shared-orchestration edits needed. +- `CLAUDE.md` (project root) — PM-integration summary references spec 012 alongside 009/010/011; removes any phrasing about the legacy `WebhookStep` still owning webhook UX. +- `CHANGELOG.md` — single Internal-change entry summarizing spec 012. +- `docs/specs/011-pm-wizard-shared-migration.md.done` — forward-reference blockquote to spec 012 at the top (mirrors 009→010→011 pointer). +- `docs/plans/011-pm-wizard-shared-migration/_coverage.md` — update the entries for plan 011/4 ACs #3 + #6 to `✅ fully closed by spec 012`. + +**Deferred to follow-up specs**: nothing. Closes spec 012. + +--- + +## Spec ACs satisfied by this plan + +- Spec AC #5 (legacy `WebhookStep`, `LinearWebhookInfoPanel`, 2 hooks, legacy test file deleted) — **full**. +- Spec AC #6 (`-webhook` id-skip filter removed; `pm-wizard.tsx` iterates every manifest step) — **full** (plan 012/3 removed the filter itself; this plan removes the now-empty legacy slot that sat alongside it). +- Spec AC #7 (plan 011/4 ACs #3 + #6 close; `_coverage.md` updated) — **full** (closes the chain started by plan 012/3). +- Spec AC #9 (new provider requires zero shared-orchestration edits) — **full** (verifiable only after legacy slot + hooks deleted). +- Spec AC #10 (conformance harness green) — hygiene. +- Spec AC #11 (build / test / lint / typecheck) — hygiene. + +--- + +## Depends On + +- Plan 3 (`linear-webhook`) — last plan that routed a provider through the legacy `WebhookStep`. Once it merges, the legacy slot is a no-op safe to delete. +- Transitively: plans 1 + 2. + +--- + +## Detailed Task List + +### 1. Dead-code audit before deletion + +Grep for every reference to the legacy symbols: +- `grep -r "WebhookStep\|LinearWebhookInfoPanel" web/src tests` — expected: only the file being deleted + the test file being deleted. +- `grep -r "useWebhookManagement\|useLinearWebhookInfo" web/src tests` — expected: only the hook definitions. +- `grep -r "webhookStepNumber" web/src tests` — expected: only the `pm-wizard.tsx` block being deleted. + +Any surviving live reference is a plan-3 gap (escalate). + +### 2. Delete legacy exports + imports + +**Implementation**: +- `web/src/components/projects/pm-wizard-common-steps.tsx`: + - Delete the `LinearWebhookInfoPanel` function export. + - Delete the `WebhookStep` function export. + - Leave `SaveStep` in place. + - Clean up any now-unused imports at the top of the file. +- `web/src/components/projects/pm-wizard-hooks.ts`: + - Delete `useWebhookManagement` export. + - Delete `useLinearWebhookInfo` export. + - Clean up any now-unused imports. +- `web/src/components/projects/pm-wizard.tsx`: + - Delete the entire legacy-webhook `` block. + - Delete imports of `WebhookStep`, `useWebhookManagement`, `useLinearWebhookInfo`. + - Delete the `webhookManagement = useWebhookManagement(...)` call. + - Delete the `linearWebhookUrl` + `linearWebhookSecretCredential` derivations (providers now compute these inside their own `useProviderHooks`). + - Rename `webhookStepNumber` → `saveStepNumber`; saveStep becomes `renderedManifestSteps.length + 2`. + +### 3. Delete the legacy test file + +**Implementation**: +- `git rm tests/unit/web/pm-wizard-webhooks-step.test.ts`. +- Verify the three per-provider adapter tests cover the assertions that file made (they should after plans 1-3). + +### 4. Rewrite `src/integrations/README.md` + +**Implementation**: +- Update the top-of-file spec list: "Four specs shape it" → "Five specs shape it"; add spec 012 bullet. +- Update the "Adding a new PM provider" section's step 3 webhook guidance: "the webhook step composes the shared `WebhookUrlDisplayStep` with whatever provider-specific UI the provider needs (programmatic registration, secret fields, setup instructions) via Fragment in the provider's wizard definition." +- Add a new "Post-spec-012 additions (2026-04-18+)" peer section: + - Every PM wizard step, without exception, renders via the manifest path. + - Legacy `WebhookStep` + `LinearWebhookInfoPanel` deleted. `pm-wizard-common-steps.tsx` now only exports `SaveStep`. + - `useWebhookManagement` + `useLinearWebhookInfo` deleted. + - Provider-specific webhook UX (Trello/JIRA programmatic create, Linear secret + instructions) all live in each provider's folder. + - Adding a new PM provider's webhook step: declare `webhook-url-display` in `wizardSpec`, compose the shared step with any provider-specific UX via Fragment in the provider's `ProviderWizardDefinition.steps` Component. + +### 5. Update root `CLAUDE.md` + +**Implementation**: +- Append a spec-012 line to the PM-integration summary: "Spec 012 completed the webhook-UX manifest migration — Trello/JIRA/Linear all render their webhook steps via the manifest path. Legacy `WebhookStep` + `LinearWebhookInfoPanel` deleted." +- Remove any phrasing about the legacy `WebhookStep` retaining webhook creation UX (from spec-011 era). + +### 6. CHANGELOG entry + +**Implementation**: add under `## Unreleased` → `### Internal`: + +> **PM webhook-UX manifest migration complete (spec 012).** Closes the final gap from spec 011 — every PM wizard step, without exception, now renders via the manifest path. Plans 012/1-3 migrated Trello, JIRA, and Linear webhook steps into per-provider adapters composed of the shared `WebhookUrlDisplayStep` + provider-specific UX (Trello/JIRA programmatic Create + active list + delete + curl fallback via existing `webhooks.*` tRPC endpoints; Linear signing-secret via `ProjectSecretField` + 5-step manual-setup instructions). Plan 012/4 deleted the legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo`, the legacy test file, and the `-webhook` id-skip filter introduced by plan 011/4. Plan 011/4 ACs #3 + #6 fully close; spec 011 `_coverage.md` updated. No operator-visible regression. See spec [012](docs/specs/012-pm-webhook-manifest-migration.md.done). + +### 7. Forward-reference in spec 011 + +**Implementation**: prepend a blockquote to `docs/specs/011-pm-wizard-shared-migration.md.done` right after the H1: + +> **Forward reference (2026-04-18+):** the remaining webhook-UX migration (deferred at close of 011/4) landed in spec [012 — PM Webhook Manifest Migration](./012-pm-webhook-manifest-migration.md.done). That spec migrated Trello/JIRA/Linear webhook steps into the manifest path, deleted the legacy `WebhookStep` + `LinearWebhookInfoPanel` + supporting hooks, and fully closed plan 011/4 ACs #3 + #6. + +### 8. Update spec 011 `_coverage.md` + +**Implementation**: in `docs/plans/011-pm-wizard-shared-migration/_coverage.md`, update the rows for plan 011/4 ACs #3 + #6 (currently marked "partial — deferred to follow-up spec" or similar): +- `AC #3 (Linear inline secret via shared)` → `✅ fully closed by spec 012 downstream`. +- `AC #6 (LinearWebhookInfoPanel retired)` → `✅ fully closed by spec 012 downstream`. + +### 9. Full verification before marking done + +- `npm run build` green. +- `npm test` green. +- `npm run lint` green. +- `npm run typecheck` green. +- `new-provider-surface.test.ts` passes — the guard doesn't care about the deletions, but verify for safety. +- Conformance harness green for all three providers. + +### 10. Mark spec 012 `.done` + +Per `/implement` Phase 8: rename `docs/specs/012-pm-webhook-manifest-migration.md` → `.md.done` in a separate trailing commit after the plan-4 `.wip` → `.done` commit. + +--- + +## Test Plan + +### Unit tests +- No new test files. +- `tests/unit/web/pm-wizard-webhooks-step.test.ts` — **deleted**. +- Existing tests (the three per-provider adapter tests from plans 1-3) continue to pass. + +### Integration tests +- None. + +### Acceptance tests +- [ ] `grep -r "WebhookStep\|LinearWebhookInfoPanel\|useWebhookManagement\|useLinearWebhookInfo" web/src tests` returns zero results (outside git history). +- [ ] Every documentation file listed above has been updated. +- [ ] `npm run build`, `npm test`, `npm run lint`, `npm run typecheck` — all green. +- [ ] Conformance harness green for all three providers. + +--- + +## Acceptance Criteria (per-plan, testable) + +1. `WebhookStep` export removed from `pm-wizard-common-steps.tsx`. +2. `LinearWebhookInfoPanel` export removed from `pm-wizard-common-steps.tsx`. +3. `useWebhookManagement` + `useLinearWebhookInfo` removed from `pm-wizard-hooks.ts`. +4. `pm-wizard.tsx` no longer imports the above; the legacy webhook `` slot is gone. +5. `tests/unit/web/pm-wizard-webhooks-step.test.ts` is deleted. +6. Dead-code grep returns zero live references to the retired symbols. +7. `src/integrations/README.md` reflects the post-spec-012 state (five specs, updated "Adding a new PM provider" webhook guidance, Post-spec-012 additions section). +8. Root `CLAUDE.md` PM-integration summary references spec 012. +9. `CHANGELOG.md` has a single Internal entry summarizing spec 012. +10. `docs/specs/011-pm-wizard-shared-migration.md.done` has a forward-reference to spec 012. +11. `docs/plans/011-pm-wizard-shared-migration/_coverage.md` rows for plan 011/4 ACs #3 + #6 updated to closed. +12. Conformance harness passes for all three providers. +13. `npm run build` passes. +14. `npm test` passes. +15. `npm run lint` passes. +16. `npm run typecheck` passes. +17. Spec 012 is marked `.done` via file rename. + +--- + +## Documentation Impact (this plan only) + +| File | Change | +|---|---| +| `src/integrations/README.md` | Rewrite: five-specs preamble; updated "Adding a new PM provider" webhook guidance; Post-spec-012 additions section. | +| `CLAUDE.md` (project root) | Append spec-012 line to PM-integration summary; remove any "legacy WebhookStep still owns..." phrasing. | +| `CHANGELOG.md` | New Internal entry summarizing spec 012. | +| `docs/specs/011-pm-wizard-shared-migration.md.done` | Forward-reference blockquote to spec 012 at the top. | +| `docs/plans/011-pm-wizard-shared-migration/_coverage.md` | Plan 011/4 ACs #3 + #6 rows updated to reflect closure via spec 012. | + +--- + +## Out of Scope (this plan) + +Deferred to follow-up specs: nothing — this plan closes the spec. + +Originally out of scope for the spec (repeated for clarity): +- Generalizing `webhooks.*` tRPC endpoints. +- Backend webhook API changes. +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM / alerting. +- New shared UI primitives. +- Schema migrations. +- Rewriting the form-state model or `ProviderWizardDefinition`. +- Further widening of `WebhookUrlDisplayStep`. + +--- + +## Progress + + +- [x] AC #1 (`WebhookStep` export removed from `pm-wizard-common-steps.tsx`) +- [x] AC #2 (`LinearWebhookInfoPanel` export removed) +- [x] AC #3 (`useWebhookManagement` + `useLinearWebhookInfo` removed from `pm-wizard-hooks.ts`) +- [x] AC #4 (`pm-wizard.tsx` legacy webhook slot + imports deleted; `webhookStepNumber` removed) +- [x] AC #5 (`tests/unit/web/pm-wizard-webhooks-step.test.ts` deleted) +- [x] AC #6 (dead-code grep: only doc-comment references remain; no live imports/usages) +- [x] AC #7 (`src/integrations/README.md` five-specs preamble + Post-spec-012 additions + updated step-3 webhook guidance) +- [x] AC #8 (root `CLAUDE.md` PM-integration summary references spec 012) +- [x] AC #9 (`CHANGELOG.md` Internal entry for spec 012) +- [x] AC #10 (spec 011 forward-reference blockquote to spec 012 added) +- [x] AC #11 (spec 011 `_coverage.md` AC #3 marked "closed by spec 012"; plan 011/4 ACs #3 + #6 marked closed downstream) +- [x] AC #12 (conformance harness green — full suite 8186/8186) +- [x] AC #13 (`npm run build` green) +- [x] AC #14 (`npm test` green — 8186/8186) +- [x] AC #15 (`npm run lint` green) +- [x] AC #16 (`npm run typecheck` green) +- [x] AC #17 (spec 012 `.done` rename — handled by Phase 8 in the trailing commit) diff --git a/docs/plans/012-pm-webhook-manifest-migration/_coverage.md b/docs/plans/012-pm-webhook-manifest-migration/_coverage.md new file mode 100644 index 00000000..f8af58f6 --- /dev/null +++ b/docs/plans/012-pm-webhook-manifest-migration/_coverage.md @@ -0,0 +1,55 @@ +# Coverage map for spec 012-pm-webhook-manifest-migration + +Auto-generated by /plan. Tracks which plans satisfy which spec ACs. + +## Spec ACs + +| # | Spec AC (short) | Satisfied by | Status | +|---|---|---|---| +| 1 | Every wizard renders webhook step via manifest path | plan 1 (trello-webhook) + plan 2 (jira-webhook) + plan 3 (linear-webhook) | partial chain (plan 4 removes the legacy slot) | +| 2 | Trello programmatic create / active list / delete / curl fallback via manifest | plan 1 | full | +| 3 | JIRA equivalent + `jiraEnsureLabels` preserved | plan 2 | full | +| 4 | Linear signing-secret + 5-step instructions inline | plan 3 | full | +| 5 | Legacy `WebhookStep` + `LinearWebhookInfoPanel` + 2 hooks + legacy test file deleted | plan 4 (cleanup) | full | +| 6 | `-webhook` id-skip filter removed; `pm-wizard.tsx` iterates every manifest step | plan 3 (filter itself) + plan 4 (legacy slot removal) | partial chain | +| 7 | Plan 011/4 ACs #3 + #6 fully close; spec 011 `_coverage.md` updated | plan 3 (manifest-path rendering) + plan 4 (deletion + coverage-map edit) | partial chain | +| 8 | No operator regression per provider | plan 1 (Trello) + plan 2 (JIRA) + plan 3 (Linear) | full per provider | +| 9 | New provider requires zero shared-orchestration edits | plan 4 (verified after legacy slot + hooks deleted) | full | +| 10 | Conformance harness stays green every step | plan 1, 2, 3, 4 | hygiene every plan | +| 11 | Build/test/lint/typecheck green | plan 1, 2, 3, 4 | hygiene every plan | + +## Coverage summary + +- **11 spec ACs** mapped to **4 plans** +- **5 plans-worth** of full-coverage ACs (standalone: AC #2, #3, #4, #5, #9) +- **6 plans-worth** of partial-chain ACs (AC #1, #6, #7 require multiple plans; AC #8 full per-provider across three plans) +- **2 plans-worth** of hygiene-only coverage (AC #10, #11 — every plan asserts them) + +## Plan dependency graph + +``` +1-trello-webhook ──→ 2-jira-webhook ──→ 3-linear-webhook ──→ 4-cleanup +``` + +Serial DAG — same pattern as spec 011. Each provider plan may reveal a composition gap that carries forward; Trello first concentrates risk (most behavior: programmatic create + list + delete + curl). + +## Per-plan AC count (for reference) + +| Plan | Per-plan ACs | Spec ACs cited | +|---|---|---| +| 1 (trello-webhook) | 10 | 2 (full), 1 (partial), 8 (full for Trello), 10, 11 | +| 2 (jira-webhook) | 11 | 3 (full), 1 (partial), 8 (full for JIRA), 10, 11 | +| 3 (linear-webhook) | 11 | 4 (full), 1 (partial), 7 (partial), 8 (full for Linear), 10, 11 | +| 4 (cleanup) | 17 | 5 (full), 6 (closes), 7 (closes), 9 (full), 10, 11 | + +## Doc impact distribution + +Every doc update lives in **plan 4 (cleanup)**. Rationale: docs reflect the final state; updating per-plan = 3 rewrites of the same section. Plan 4 also writes a single CHANGELOG entry covering the whole spec (mirrors spec 010 + 011 cadence). + +| Top-level doc | Owner plan | +|---|---| +| `src/integrations/README.md` | 4 | +| `CLAUDE.md` (project root) | 4 | +| `CHANGELOG.md` | 4 | +| `docs/specs/011-pm-wizard-shared-migration.md.done` (forward-ref) | 4 | +| `docs/plans/011-pm-wizard-shared-migration/_coverage.md` (update) | 4 | diff --git a/docs/specs/011-pm-wizard-shared-migration.md.done b/docs/specs/011-pm-wizard-shared-migration.md.done index 6a9c54f6..717838dc 100644 --- a/docs/specs/011-pm-wizard-shared-migration.md.done +++ b/docs/specs/011-pm-wizard-shared-migration.md.done @@ -9,6 +9,8 @@ status: done # 011: PM Wizard Migration — Existing Providers Onto Shared Components +> **Forward reference (2026-04-18+):** the remaining webhook-UX migration (deferred at close of plan 011/4 — legacy `WebhookStep` + `LinearWebhookInfoPanel` still owned webhook registration + signing-secret UX) landed in spec [012 — PM Webhook Manifest Migration](./012-pm-webhook-manifest-migration.md.done). That spec migrated Trello/JIRA/Linear webhook steps into the manifest path, deleted the legacy surface, and fully closed plan 011/4 ACs #3 + #6. + ## Problem & Motivation Spec 010 shipped six real shared React step components — one per `StandardStepKind` — so a new PM provider could configure its wizard with zero per-provider step UI. The shared path works: the wizard generator dispatches to the shared registry, every component has tests, the `new-provider-surface` guard pins them. But the three **existing** production providers (Trello, JIRA, Linear) never migrated. They still fork their own step components in per-provider wizard-step files — roughly 1,085 lines of UI that duplicates what the shared components already do. diff --git a/docs/specs/012-pm-webhook-manifest-migration.md.done b/docs/specs/012-pm-webhook-manifest-migration.md.done new file mode 100644 index 00000000..ecbe854e --- /dev/null +++ b/docs/specs/012-pm-webhook-manifest-migration.md.done @@ -0,0 +1,153 @@ +--- +id: 012 +slug: pm-webhook-manifest-migration +level: spec +title: PM Webhook Manifest Migration — Retire Legacy WebhookStep +created: 2026-04-18 +status: done +--- + +# 012: PM Webhook Manifest Migration — Retire Legacy WebhookStep + +## Problem & Motivation + +Spec 011 migrated five of six wizard step types (credentials, container-pick, status-mapping, label-mapping, project-scope, custom-field-mapping) from per-provider forks onto the shared `StandardStepKind` components, and deleted the three `pm-wizard-{trello,jira,linear}-steps.tsx` files. One step type did not migrate: `webhook-url-display`. The legacy `WebhookStep` + `LinearWebhookInfoPanel` (in `pm-wizard-common-steps.tsx`) still render Trello's programmatic "Create Webhook" button + active-webhooks list + delete + curl fallback, JIRA's equivalent, and Linear's signing-secret `ProjectSecretField` + setup-instructions panel. + +This wasn't oversight — it was honest scope management. Webhook-creation UX is **richer** than a URL-display-with-copy-button: Trello and JIRA have programmatic registration flows with active-webhooks lists and delete buttons; Linear has secret-field persistence (different lifecycle from wizard state) and long-form setup instructions. Migrating them into the manifest path requires per-provider composition around the shared `WebhookUrlDisplayStep`, a conscious decision about whether to widen the shared step or compose it, and an end-of-run cleanup of the `-webhook` id-skip filter that plan 011/4 shipped as a stopgap. + +The outcome we want: every PM wizard step — without exception — renders via `manifestDef.steps` dispatch. The parent wizard (`pm-wizard.tsx`) iterates manifest steps uniformly, no stopgap filter. Each provider owns its webhook step's composition in its provider folder. `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, and `useLinearWebhookInfo` go away. Plan 011/4 ACs #3 + #6 close. A fourth PM provider added tomorrow declares a `webhook-url-display` step, composes it with whatever programmatic-registration / secret-field / instructions UI it needs in its wizard definition, and never touches shared orchestration. + +--- + +## Goals + +- Every PM wizard renders its webhook step via the manifest path (`manifestDef.steps` → `ManifestProviderWizardSection` → provider adapter). +- Trello retains its programmatic "Create Webhook" button, active-webhooks list, delete, and curl fallback — now rendered from the Trello provider folder. +- JIRA retains its equivalent flows (including the `jiraEnsureLabels` side-effect on Create). +- Linear retains its signing-secret persistence via `ProjectSecretField` and its setup-instructions panel — now rendered from the Linear provider folder. +- The legacy `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, and `useLinearWebhookInfo` are deleted. Their test file is migrated or deleted per relevance. +- The `-webhook` id-skip filter introduced by plan 011/4 is removed. `pm-wizard.tsx` iterates every manifest step without exception. +- Zero operator-visible regression. Every create / list / delete / curl / secret behavior available today continues to work, just through a different render path. +- A new PM provider writes zero per-provider wizard code outside its provider folder + the manifest — closing the final gap in spec 011's promise. + +--- + +## Non-goals + +- Generalizing the `webhooks.create/list/delete` tRPC endpoints into a `{providerId}`-driven API. Flag-based endpoints stay; that refactor is a separate spec if ever needed. +- Backend webhook API changes. Router, signature verification, event parsing, and registration endpoints unchanged. +- Adding programmatic webhook registration for Linear (Linear's API doesn't expose it; manual-only stays). +- Extending the manifest/conformance pattern to SCM (GitHub) or alerting (Sentry). +- New shared UI primitives. `ProjectSecretField` + `CopyButton` + the widened `WebhookUrlDisplayStep` are reused verbatim. +- Schema migrations or config-shape changes. +- Rewriting the form-state model or `ProviderWizardDefinition` contract. +- Further widening of `WebhookUrlDisplayStep`. Composition via Fragment covers every provider need; the shared step stays focused on URL display + copy + optional instructions. +- Changing operator-visible wizard UX behavior. Exactly one allowed micro-change: each provider's webhook section gets consistent visual positioning because it's now a peer step rather than a mode-switched sub-panel. + +--- + +## Constraints + +- **Zero operator-visible regression.** The three wizards must continue to expose every existing webhook action — create, list, delete, curl fallback (Trello + JIRA); secret-field save / load / clear + setup instructions (Linear). +- **Composition preferred over widening.** The shared `WebhookUrlDisplayStep` API stays stable; provider adapters render it alongside provider-specific UI via a Fragment. Only widen if composition demonstrably can't cover a need. +- **One reviewable PR per provider plus a cleanup PR.** No single-PR big-bang. +- **Test surface net-positive.** Legacy tests are case-by-case ported (where the behavior is still asserted) or deleted (where the assertion pinned an obsolete shape). Shared-component coverage must not shrink. +- **Conformance harness stays green.** `tests/unit/integrations/pm-conformance.test.ts` passes for every provider at every migration step. +- **`new-provider-surface` invariant holds.** No new shared-orchestration edits needed to add a provider after this lands. + +--- + +## User stories / Requirements + +As an **operator setting up a new Trello project**: +- I can click "Create Webhook" and CASCADE programmatically registers the webhook on my Trello board, the same way it does today. +- I see the list of active Trello webhooks with their URLs + status indicators, and I can delete any of them. +- If programmatic registration fails, I can copy a curl command and register the webhook manually. + +As an **operator configuring JIRA**: +- I can click "Create Webhook" and CASCADE registers it on my JIRA instance; my first issue gets seeded with CASCADE labels, then un-seeded (existing `jiraEnsureLabels` side-effect). +- Same active-list + delete + curl fallback as Trello. + +As an **operator configuring Linear**: +- I see the webhook URL + copy button + 5-step setup instructions pointing me to Linear's UI. +- I can paste the signing secret into an inline field; CASCADE stores it server-side and shows the masked last-4 chars on re-entry. +- I can clear the secret. + +As a **CASCADE reviewer inspecting a wizard PR**: +- The diff is focused: migrate one provider's webhook step, retain the legacy slot for the others (until plan 5 deletes it). + +As a **CASCADE contributor adding a fourth PM provider**: +- I declare a `webhook-url-display` step in my manifest's `wizardSpec`. +- I compose any provider-specific UI (programmatic create, active list, signing-secret field, …) around the shared step in my provider folder's wizard definition. +- I touch zero files in shared orchestration (`pm-wizard.tsx`, `pm-wizard-common-steps.tsx`, `pm-wizard-hooks.ts`, the router). + +--- + +## Research Notes + +- **Strangler-fig migration pattern** — same shape as spec 011's wizard migration. Migrate one provider at a time; each plan is independently reversible. +- **No new OSS.** Every primitive (tRPC procedures, `ProjectSecretField`, `CopyButton`, shared `WebhookUrlDisplayStep`) is already in the repo. +- **No industry pattern worth citing.** "Active webhooks list with delete + programmatic create + curl fallback" is a standard admin-panel idiom; nothing to research. + +--- + +## Open Source Decisions + +| Tool | Solves | Decision | Reason | +|---|---|---|---| +| — | — | — | No new OSS. Every primitive already adopted in spec 011. | + +--- + +## Strategic decisions + +1. **Composition over widening** — chose composing the shared `WebhookUrlDisplayStep` with provider-specific UI via Fragment over adding more optional props to the shared step. Reason: the shared step's job is URL display + copy + optional instructions; programmatic-create / active-list / secret-field are provider concerns and don't belong in the shared API. +2. **Standard `webhook-url-display` kind + composition** — chose keeping each provider's wizardSpec webhook entry as `kind: 'webhook-url-display'` over converting to `kind: 'custom'`. Reason: the URL-display semantics ARE standard across providers; the provider-specific layers wrap around it. Mirrors the pattern plan 011/4 established for Linear. +3. **Keep `webhooks.*` tRPC endpoints as-is** — chose retaining the `{trelloOnly, jiraOnly}` flag-based endpoints over generalizing to `{providerId}`. Reason: out of scope — this is a wizard-UX migration, not a backend refactor. +4. **Per-provider active-webhooks rendering** — chose each provider adapter rendering its own active-list over extracting a shared sub-component. Reason: only two providers have lists (Trello + JIRA); shared extraction would be premature abstraction. +5. **`jiraEnsureLabels` preserved as-is** — side-effect lives in the JIRA webhook-creation call path. Reason: works today; out of scope to refactor. +6. **Delete `useWebhookManagement` + `useLinearWebhookInfo`** — chose deletion over retention after migration. Reason: each provider adapter calls `trpc.webhooks.*` and reads the credential query directly inside its own `useProviderHooks`; the shared hooks become one-caller helpers — dead code. +7. **Remove the `-webhook` id-skip filter** — `pm-wizard.tsx` iterates every manifest step uniformly after this spec. Reason: the filter was a stopgap from plan 011/4; its removal is the point of the migration. +8. **Migration sequence: Trello → JIRA → Linear → cleanup** — chose linear over parallel. Reason: each provider may reveal a shared-component gap (e.g. composition can't handle something); fixing once and carrying forward is cheaper than three parallel streams discovering the same thing. +9. **Test migration: case-by-case port-or-delete** — chose over port-verbatim. Reason: legacy tests pin DOM shapes that are being deleted; porting would assert nothing useful. + +--- + +## Acceptance Criteria (outcome-level) + +1. Each of the three PM wizards renders its webhook step via `manifestDef.steps` dispatch — no provider-selector branching anywhere in rendering. +2. The Trello wizard exposes every programmatic-webhook behavior available today: Create button calling the backend registration endpoint, active-webhooks list, delete button per webhook, curl fallback with the current board ID interpolated. +3. The JIRA wizard exposes its equivalent: Create button, active list, delete, curl fallback with the current base URL interpolated. The `jiraEnsureLabels` side-effect fires on successful creation, same as today. +4. The Linear wizard exposes its signing-secret field (save / load / clear via `projects.credentials.*`), webhook URL display + copy, and the 5-step setup instructions — all inside the manifest-rendered webhook step. +5. `WebhookStep`, `LinearWebhookInfoPanel`, `useWebhookManagement`, `useLinearWebhookInfo`, and the legacy webhook test file are deleted from the repository. +6. `pm-wizard.tsx` no longer filters steps by id suffix — `manifestDef.steps` iterates in full, and every provider's wizard UI is entirely manifest-driven. +7. Plan 011/4's partial ACs #3 (Linear inline-secret via shared component) and #6 (`LinearWebhookInfoPanel` retired) fully close; spec 011's `_coverage.md` is updated to reflect this. +8. An operator sees no functional regression — every input, selection, action, and feedback message that worked before still works after. +9. Adding a fourth PM provider requires zero edits to shared orchestration (`pm-wizard.tsx`, `pm-wizard-common-steps.tsx`, `pm-wizard-hooks.ts`). The `new-provider-surface` guard continues to hold. +10. Conformance harness passes for every provider at every migration step. +11. Build / test / lint / typecheck all green after each plan. + +--- + +## Documentation Impact (high-level) + +- `src/integrations/README.md` — add a Post-spec-012 additions section; update the "Adding a new PM provider" section's webhook-step guidance (composition pattern + examples). +- `CLAUDE.md` (project root) — PM-integration summary mentions spec 012 alongside 009 / 010 / 011; removes any phrasing about the legacy `WebhookStep` still owning webhook registration. +- `CHANGELOG.md` — single Internal-change entry summarizing the full spec (mirrors the spec-010 / spec-011 cadence). +- `docs/specs/011-pm-wizard-shared-migration.md.done` — forward-reference blockquote at the top pointing at spec 012, mirroring the 009 → 010 and 010 → 011 pattern. +- `docs/plans/011-pm-wizard-shared-migration/_coverage.md` — update to reflect that plan 011/4 ACs #3 + #6 are now fully closed by spec 012 downstream. + +--- + +## Out of Scope + +- Generalizing `webhooks.create/list/delete` tRPC endpoints beyond their existing `{trelloOnly, jiraOnly}` flags. +- Backend webhook API changes (router, signature verification, event parsing, registration). +- Adding programmatic webhook registration for Linear. +- Extending the manifest pattern to SCM (GitHub) or alerting (Sentry). +- New shared UI primitives. +- Schema migrations or config-shape changes. +- Rewriting the form-state model or the `ProviderWizardDefinition` contract. +- Further widening of `WebhookUrlDisplayStep` — composition covers every need. +- Consolidating Trello + JIRA active-webhooks rendering into a shared sub-component. +- Changing operator-visible wizard UX behavior beyond the one allowed micro-change (peer-step positioning rather than mode-switched sub-panel). diff --git a/src/integrations/README.md b/src/integrations/README.md index 3c53fdf9..3b94150b 100644 --- a/src/integrations/README.md +++ b/src/integrations/README.md @@ -2,12 +2,13 @@ CASCADE's PM providers (Trello, JIRA, Linear, and any future Asana/GitLab/ClickUp) are built on a **provider manifest** pattern. One file describes the provider end-to-end; one registry iterates manifests; a behavioral conformance harness guarantees each manifest satisfies its declared contracts. -This document is the canonical guide for adding a new PM provider. Four specs shape it: +This document is the canonical guide for adding a new PM provider. Five specs shape it: - **Spec [006](../../docs/specs/006-pm-integration-plug-and-play.md.done)** — introduced the manifest pattern + wiring-level conformance (2026-04-15/16). - **Spec [009](../../docs/specs/009-pm-integration-hardening.md.done)** — hardened the contracts: branded ID types, manifest-owned config schemas (eliminating the #1138/#1142 drift class), unified `pm.discover` endpoint, behavioral conformance harness with in-memory lifecycle scenario, single registration entrypoint, and auth-header provenance enforcement. - **Spec [010](../../docs/specs/010-pm-integration-hardening-followups.md.done)** — follow-up cleanup: generic `pm.discovery.createLabel` / `createCustomField` mutation endpoints + manifest hooks, `currentUser` discovery capability, real shared React components for every `StandardStepKind`. - **Spec [011](../../docs/specs/011-pm-wizard-shared-migration.md.done)** — migrated all three production providers (Trello, JIRA, Linear) onto the shared step components; added a 7th `StandardStepKind: custom-field-mapping`; widened `container-pick` / `project-scope` / `webhook-url-display` with optional props; deleted the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` files. +- **Spec [012](../../docs/specs/012-pm-webhook-manifest-migration.md.done)** — migrated each provider's webhook UX (programmatic create for Trello/JIRA, signing-secret + manual-setup for Linear) into its own manifest webhook step adapter. Deleted the legacy `WebhookStep` + `LinearWebhookInfoPanel` + supporting hooks. Every PM wizard step now renders via the manifest path without exception. --- @@ -181,6 +182,15 @@ All three real providers are now on the hardened contracts. Plan 009/4 also ship | Shared-component widenings (additive) | `container-pick` and `project-scope` support optional `searchable: boolean` (renders via cmdk `Combobox`). `webhook-url-display` supports optional inline signing-secret input (`secretFieldRole` / `secretValue` / `onSecretChange`). `label-mapping` supports optional `labelDefaults?` to pre-populate the Create input + thread color. `custom-field-mapping` supports optional `fieldDefaults?`. | | Shared surface guard | Step-component file pin extended to seven entries. | +### Post-spec-012 additions (2026-04-18+) + +| Area | Change | +|---|---| +| Webhook-UX migration complete | Every PM wizard step, without exception, renders via the manifest path. Trello, JIRA, and Linear each own their webhook step via a per-provider adapter (`pm-providers//webhook-step.tsx`) — Fragment composition around the shared `WebhookUrlDisplayStep`. Trello + JIRA compose with programmatic "Create Webhook" button + active-webhooks list + delete + curl fallback (via existing `webhooks.create/list/delete({trelloOnly|jiraOnly:true})` tRPC endpoints). Linear composes with info banner + `ProjectSecretField` (`LINEAR_WEBHOOK_SECRET`) + 5-step manual setup instructions. | +| Legacy deletions | `WebhookStep` + `LinearWebhookInfoPanel` + `useWebhookManagement` + `useLinearWebhookInfo` all deleted. `pm-wizard-common-steps.tsx` now only exports `SaveStep`. Legacy test file `pm-wizard-webhooks-step.test.ts` deleted — assertions moved into per-provider adapter tests. | +| Parent-wizard filter | The `-webhook` id-skip filter (stopgap from plan 011/4) is gone. `renderedManifestSteps = manifestDef.steps.map(...)` — no filter. | +| New-provider guarantee | Adding a PM provider requires zero edits to `pm-wizard.tsx`, `pm-wizard-common-steps.tsx`, or `pm-wizard-hooks.ts`. Webhook UX composition lives in the provider folder. | + --- ## Adding a new PM provider (step by step) @@ -197,7 +207,7 @@ Spec 009 AC #10: **a new PM provider PR should not need to edit shared router / 2. **Wire the manifest** via a single import in `src/integrations/pm/index.ts` (`import './/index.js';`). No other edit to any shared file is needed for registration — the `single-entrypoint` test guards this. -3. **Frontend folder** at `web/src/components/projects/pm-providers//`: `wizard.ts` (`ProviderWizardDefinition` with `useProviderHooks` if the provider needs discovery / label creation / custom-field creation), `index.ts`. Add one line to `pm-wizard.tsx` to register. For shared wizard steps declared on `manifest.wizardSpec`, the generator in `pm-providers/generator.tsx` dispatches directly to the real shared step components at `pm-providers/steps/*.tsx` — there are **seven** kinds: `credentials`, `container-pick`, `status-mapping`, `label-mapping`, `webhook-url-display`, `project-scope`, `custom-field-mapping`. A provider with purely standard steps writes **zero** per-provider step components; this is what Trello, JIRA, and Linear all do post-spec 011. Provide `providerHooks` (returned from `useProviderHooks`) to forward discovery data + mutation callbacks into the shared components; the generator spreads `ctx.providerHooks` as props. Unknown step `kind` values still warn-and-render a placeholder. **Provider-specific UI** (Trello OAuth popup, JIRA issue-type mapping) ships as `kind: 'custom'` steps declared on the manifest and resolved to provider-folder components by the wizard definition. +3. **Frontend folder** at `web/src/components/projects/pm-providers//`: `wizard.ts` (`ProviderWizardDefinition` with `useProviderHooks` if the provider needs discovery / label creation / custom-field creation / webhook registration), `index.ts`. Add one line to `pm-wizard.tsx` to register. For shared wizard steps declared on `manifest.wizardSpec`, the generator in `pm-providers/generator.tsx` dispatches directly to the real shared step components at `pm-providers/steps/*.tsx` — there are **seven** kinds: `credentials`, `container-pick`, `status-mapping`, `label-mapping`, `webhook-url-display`, `project-scope`, `custom-field-mapping`. A provider with purely standard steps writes **zero** per-provider step components; Trello, JIRA, and Linear all use the shared components for every standard kind. Provide `providerHooks` (returned from `useProviderHooks`) to forward discovery data + mutation callbacks into the shared components; the generator spreads `ctx.providerHooks` as props. Unknown step `kind` values still warn-and-render a placeholder. **Provider-specific UI** ships either as (a) `kind: 'custom'` steps declared on the manifest and resolved to provider-folder components (Trello OAuth popup, JIRA issue-type mapping), or (b) Fragment compositions around a shared step when the base UX is standard but needs augmentation (Trello/JIRA webhook steps compose `WebhookUrlDisplayStep` + programmatic Create UX; Linear composes `WebhookUrlDisplayStep` + `ProjectSecretField` + setup instructions — see `pm-providers/{trello,jira,linear}/webhook-step.tsx` for the reference composition pattern). 4. **Lifecycle fixture** at `tests/helpers/LifecycleFixture.ts`. Add the fixture key to `LIFECYCLE_FIXTURES` in `tests/unit/integrations/pm-conformance.test.ts`. Trivial providers can reuse `createFakePMProvider()` (see Trello/JIRA/Linear fixtures). diff --git a/tests/unit/web/jira-webhook-step.test.ts b/tests/unit/web/jira-webhook-step.test.ts new file mode 100644 index 00000000..77962732 --- /dev/null +++ b/tests/unit/web/jira-webhook-step.test.ts @@ -0,0 +1,167 @@ +/** + * Tests for JiraWebhookAdapter (plan 012/2). + * + * JIRA-provider webhook step adapter. Fragment composing shared + * `WebhookUrlDisplayStep` + JIRA-specific UX: active-webhooks list, + * programmatic "Create Webhook" button (wired to webhooks.create with + * jiraOnly: true — the backend-side `jiraEnsureLabels` side-effect + * fires there unchanged), delete buttons, curl fallback template. + */ + +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import { JiraWebhookAdapter } from '../../../web/src/components/projects/pm-providers/jira/webhook-step.js'; +import type { WizardState } from '../../../web/src/components/projects/pm-wizard-state.js'; + +function makeState(overrides: Partial = {}): WizardState { + return { + jiraBaseUrl: '', + ...overrides, + } as WizardState; +} + +function makeProviderHooks(overrides: Record = {}): Record { + return { + webhookUrl: 'https://router.example.com/jira/webhook', + callbackBaseUrl: 'https://router.example.com', + activeJiraWebhooks: [], + webhooksLoading: false, + createJiraWebhook: () => {}, + createLoading: false, + createError: undefined, + deleteJiraWebhook: (_id: string) => {}, + deleteLoading: false, + ...overrides, + }; +} + +describe('JiraWebhookAdapter', () => { + it('renders the shared WebhookUrlDisplayStep (URL + copy button)', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('data-step-component="webhook-url-display"'); + expect(html).toContain('https://router.example.com/jira/webhook'); + }); + + it('renders active-webhooks list when provided', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ + activeJiraWebhooks: [ + { id: 'wh-1', url: 'https://router.example.com/jira/webhook', active: true }, + { id: 'wh-2', url: 'https://other.example.com/jira/webhook', active: false }, + ], + }), + }), + ); + expect(html).toContain('https://router.example.com/jira/webhook'); + expect(html).toContain('https://other.example.com/jira/webhook'); + }); + + it('renders a "No JIRA webhooks configured" fallback when active list is empty', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ activeJiraWebhooks: [] }), + }), + ); + expect(html).toContain('No JIRA webhooks configured'); + }); + + it('renders the Create Webhook button with data-action="create-webhook"', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('data-action="create-webhook"'); + }); + + it('disables the Create button when callbackBaseUrl is empty', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ callbackBaseUrl: '' }), + }), + ); + const buttonTag = html.match(/]*data-action="create-webhook"[^>]*>/)?.[0]; + expect(buttonTag).toBeDefined(); + expect(buttonTag).toMatch(/\sdisabled=""/); + }); + + it('enables the Create button when callbackBaseUrl is populated', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ callbackBaseUrl: 'https://router.example.com' }), + }), + ); + const buttonTag = html.match(/]*data-action="create-webhook"[^>]*>/)?.[0]; + expect(buttonTag).toBeDefined(); + expect(buttonTag).not.toMatch(/\sdisabled=""/); + }); + + it('interpolates jiraBaseUrl into the curl fallback template', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState({ jiraBaseUrl: 'https://acme.atlassian.net' }), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('https://acme.atlassian.net/rest/webhooks/1.0/webhook'); + }); + + it('falls back to placeholder when jiraBaseUrl is empty', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState({ jiraBaseUrl: '' }), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('YOUR_JIRA_BASE_URL'); + }); + + it('renders delete buttons with data-action="delete-webhook" per active webhook', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ + activeJiraWebhooks: [ + { id: 'wh-1', url: 'https://router.example.com/jira/webhook', active: true }, + { id: 'wh-2', url: 'https://other.example.com/jira/webhook', active: false }, + ], + }), + }), + ); + const deleteButtons = html.match(/data-action="delete-webhook"/g) ?? []; + expect(deleteButtons.length).toBe(2); + }); + + it('does not render Linear signing-secret field (regression guard)', () => { + const html = renderToStaticMarkup( + createElement(JiraWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).not.toMatch(/data-role="webhook_secret"/); + expect(html).not.toContain('LINEAR_WEBHOOK_SECRET'); + }); +}); diff --git a/tests/unit/web/linear-webhook-step.test.ts b/tests/unit/web/linear-webhook-step.test.ts new file mode 100644 index 00000000..ff6b2405 --- /dev/null +++ b/tests/unit/web/linear-webhook-step.test.ts @@ -0,0 +1,150 @@ +/** + * Tests for LinearWebhookAdapter (plan 012/3). + * + * Linear has no programmatic webhook registration (Linear's API forbids + * it). The adapter renders: shared `WebhookUrlDisplayStep` + info banner + + * 5-step manual setup instructions + `ProjectSecretField` bound to + * `LINEAR_WEBHOOK_SECRET`. + */ + +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it, vi } from 'vitest'; + +// Mock ProjectSecretField — it uses `useQueryClient` which pulls React +// from web/node_modules (different instance than the root-aliased React +// the test env uses), causing a null-context crash during SSR. The stub +// renders a deterministic `
` preserving the props we want to assert. +vi.mock('../../../web/src/components/projects/project-secret-field.js', () => ({ + ProjectSecretField: (props: { + projectId: string; + envVarKey: string; + label: string; + description?: string; + placeholder?: string; + }) => + createElement( + 'div', + { + 'data-component': 'ProjectSecretField', + 'data-env-var-key': props.envVarKey, + 'data-project-id': props.projectId, + }, + createElement('label', null, props.label), + createElement('input', { type: 'password', placeholder: props.placeholder ?? '' }), + ), +})); + +import { LinearWebhookAdapter } from '../../../web/src/components/projects/pm-providers/linear/webhook-step.js'; +import type { WizardState } from '../../../web/src/components/projects/pm-wizard-state.js'; + +function makeState(overrides: Partial = {}): WizardState { + return { + ...overrides, + } as WizardState; +} + +function makeProviderHooks(overrides: Record = {}): Record { + return { + webhookUrl: 'https://router.example.com/linear/webhook', + projectIdForSecret: 'proj-123', + webhookSecretCredential: undefined, + ...overrides, + }; +} + +describe('LinearWebhookAdapter', () => { + it('renders the shared WebhookUrlDisplayStep with webhookUrl', () => { + const html = renderToStaticMarkup( + createElement(LinearWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('data-step-component="webhook-url-display"'); + expect(html).toContain('https://router.example.com/linear/webhook'); + }); + + it('renders a ProjectSecretField with envVarKey="LINEAR_WEBHOOK_SECRET"', () => { + const html = renderToStaticMarkup( + createElement(LinearWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + // ProjectSecretField renders an input with the envVarKey as data attr + // or in a label — pin via its label text + presence. + expect(html).toContain('Webhook Signing Secret'); + }); + + it('does not render the ProjectSecretField when projectIdForSecret is empty', () => { + const html = renderToStaticMarkup( + createElement(LinearWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ projectIdForSecret: '' }), + }), + ); + expect(html).not.toContain('Webhook Signing Secret'); + }); + + it('renders a 5-step setup instructions list', () => { + const html = renderToStaticMarkup( + createElement(LinearWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + // Match 5
  • elements inside an
      (the top-level steps). + // The events sub-list renders
    1. s inside a nested
        ; count the + // direct
        1. ...
        pattern by counting inside
          . + const olMatch = html.match(//); + expect(olMatch).toBeDefined(); + // Count only top-level
        1. elements by excluding nested ones (there's + // a
            inside step 3 with 3 nested
          • s). Simpler: assert the + // copy-strings that identify each of the 5 steps appear. + expect(html).toContain('linear.app/settings/api'); + expect(html).toContain('New webhook'); + expect(html).toContain('Enable these events'); + expect(html).toContain('Select your team and save'); + expect(html).toContain('signing secret'); + }); + + it('links to linear.app/settings/api in the instructions', () => { + const html = renderToStaticMarkup( + createElement(LinearWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('href="https://linear.app/settings/api"'); + }); + + it('does not render Trello/JIRA UI (Create button, active-webhooks list)', () => { + const html = renderToStaticMarkup( + createElement(LinearWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).not.toContain('data-action="create-webhook"'); + expect(html).not.toContain('data-action="delete-webhook"'); + expect(html).not.toContain('curl -X POST'); + }); + + it('renders the "Manual Webhook Setup Required" info banner', () => { + const html = renderToStaticMarkup( + createElement(LinearWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('Manual Webhook Setup Required'); + }); +}); diff --git a/tests/unit/web/pm-wizard-webhooks-step.test.ts b/tests/unit/web/pm-wizard-webhooks-step.test.ts deleted file mode 100644 index 121dd12d..00000000 --- a/tests/unit/web/pm-wizard-webhooks-step.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Unit tests for WebhookStep — Node SSR. - * - * Covers Linear credential threading and Trello / JIRA non-regression. See the - * plan-divergence note in the parent plan for why this is SSR-only (interactive - * tests need jsdom + testing-library, which web/ does not ship). - */ - -import { createElement } from 'react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { describe, expect, it, vi } from 'vitest'; - -// Stub ProjectSecretField to keep React Query + tRPC out of module load. -vi.mock('../../../web/src/components/projects/project-secret-field.js', () => ({ - ProjectSecretField: ({ - envVarKey, - label, - credential, - }: { - projectId: string; - envVarKey: string; - label: string; - credential?: { isConfigured: boolean; maskedValue: string }; - }) => - createElement( - 'div', - { - 'data-testid': 'project-secret-field', - 'data-envvarkey': envVarKey, - }, - createElement('label', null, label), - credential?.isConfigured ? createElement('span', null, credential.maskedValue) : null, - ), -})); - -import { WebhookStep } from '../../../web/src/components/projects/pm-wizard-common-steps.js'; -import type { WizardState } from '../../../web/src/components/projects/pm-wizard-state.js'; - -function makeState(overrides: Partial): WizardState { - return { - provider: 'trello', - trelloApiKey: '', - trelloToken: '', - trelloApiSecret: '', - trelloBoardId: '', - trelloOrgId: '', - trelloLists: {}, - trelloLabels: {}, - trelloCostField: null, - jiraEmail: '', - jiraApiToken: '', - jiraBaseUrl: '', - jiraProjectId: '', - jiraProjectKey: '', - jiraStatuses: {}, - jiraLabels: {}, - jiraCostField: null, - linearApiKey: '', - linearTeamId: '', - linearStatuses: {}, - linearLabels: {}, - isEditing: false, - verifyStatus: 'idle', - verifyMessage: null, - verifiedLogin: null, - availableBoards: [], - availableOrgs: [], - availableProjects: [], - availableTeams: [], - availableLists: [], - availableStatuses: [], - availableLabels: [], - availableTrelloCustomFields: [], - availableJiraCustomFields: [], - trelloLabelColors: {}, - ...overrides, - } as unknown as WizardState; -} - -const baseMutations = { - createWebhookMutation: { - mutate: () => {}, - isPending: false, - isError: false, - isSuccess: false, - error: null, - }, - deleteWebhookMutation: { - mutate: () => {}, - isPending: false, - isError: false, - }, -} as unknown as { - createWebhookMutation: Parameters[0]['createWebhookMutation']; - deleteWebhookMutation: Parameters[0]['deleteWebhookMutation']; -}; - -const baseProps = { - webhooksQuery: { isLoading: false, data: undefined, refetch: () => {} }, - activeWebhooks: [], - callbackBaseUrl: 'https://dev.api.ca.sca.de.com', - linearWebhookUrl: 'https://dev.api.ca.sca.de.com/linear/webhook', - projectId: 'test-project', - ...baseMutations, -} as const; - -function render(extra: Partial[0]>) { - return renderToStaticMarkup( - createElement(WebhookStep, { - ...baseProps, - ...extra, - } as Parameters[0]), - ); -} - -describe('WebhookStep — Linear credential threading', () => { - it('renders the LINEAR_WEBHOOK_SECRET field when state.provider is linear', () => { - const html = render({ - state: makeState({ provider: 'linear' }), - }); - expect(html).toContain('data-envvarkey="LINEAR_WEBHOOK_SECRET"'); - expect(html).toContain('Webhook Signing Secret (optional)'); - }); - - it('surfaces the masked credential value when one is threaded through', () => { - const html = render({ - state: makeState({ provider: 'linear' }), - linearWebhookSecretCredential: { - envVarKey: 'LINEAR_WEBHOOK_SECRET', - name: 'Webhook Signing Secret (optional)', - isConfigured: true, - maskedValue: '...abcd', - }, - }); - expect(html).toContain('...abcd'); - }); - - it('renders the three-item events list on the Linear step', () => { - const html = render({ state: makeState({ provider: 'linear' }) }); - expect(html).toMatch(/Issues<\/strong>/); - expect(html).toMatch(/Comments<\/strong>/); - expect(html).toMatch(/Issue Labels<\/strong>/); - }); -}); - -describe('WebhookStep — Trello non-regression', () => { - it('does not render the Linear secret field for Trello', () => { - const html = render({ state: makeState({ provider: 'trello' }) }); - expect(html).not.toContain('LINEAR_WEBHOOK_SECRET'); - expect(html).not.toContain('Webhook Signing Secret'); - }); - - it('still renders the Trello curl command block', () => { - const html = render({ - state: makeState({ provider: 'trello', trelloBoardId: 'b1' }), - }); - expect(html).toContain('curl -X POST'); - expect(html).toContain('api.trello.com'); - }); - - it('still renders the Create Webhook button', () => { - const html = render({ state: makeState({ provider: 'trello' }) }); - expect(html).toContain('Create Webhook'); - }); -}); - -describe('WebhookStep — JIRA non-regression', () => { - it('does not render the Linear secret field for JIRA', () => { - const html = render({ state: makeState({ provider: 'jira' }) }); - expect(html).not.toContain('LINEAR_WEBHOOK_SECRET'); - expect(html).not.toContain('Webhook Signing Secret'); - }); - - it('still renders the JIRA curl command block', () => { - const html = render({ - state: makeState({ provider: 'jira', jiraBaseUrl: 'https://example.atlassian.net' }), - }); - expect(html).toContain('curl -X POST'); - expect(html).toContain('/rest/webhooks/1.0/webhook'); - }); -}); diff --git a/tests/unit/web/trello-webhook-step.test.ts b/tests/unit/web/trello-webhook-step.test.ts new file mode 100644 index 00000000..73f248cf --- /dev/null +++ b/tests/unit/web/trello-webhook-step.test.ts @@ -0,0 +1,170 @@ +/** + * Tests for TrelloWebhookAdapter (plan 012/1). + * + * The Trello-provider webhook step adapter. Fragment composing the shared + * WebhookUrlDisplayStep + Trello-specific UX: active-webhooks list, + * programmatic "Create Webhook" button, per-webhook delete buttons, curl + * fallback template with trelloBoardId interpolated. + * + * Every tRPC call (webhooks.list/create/delete with trelloOnly flag) goes + * through providerHooks; these tests pin the adapter rendering and wiring, + * not the tRPC layer. + */ + +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import { TrelloWebhookAdapter } from '../../../web/src/components/projects/pm-providers/trello/webhook-step.js'; +import type { WizardState } from '../../../web/src/components/projects/pm-wizard-state.js'; + +function makeState(overrides: Partial = {}): WizardState { + return { + trelloBoardId: '', + ...overrides, + } as WizardState; +} + +function makeProviderHooks(overrides: Record = {}): Record { + return { + webhookUrl: 'https://router.example.com/trello/webhook', + callbackBaseUrl: 'https://router.example.com', + activeTrelloWebhooks: [], + webhooksLoading: false, + createTrelloWebhook: () => {}, + createLoading: false, + createError: undefined, + deleteTrelloWebhook: (_id: string) => {}, + deleteLoading: false, + ...overrides, + }; +} + +describe('TrelloWebhookAdapter', () => { + it('renders the shared WebhookUrlDisplayStep (URL + copy button)', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('data-step-component="webhook-url-display"'); + expect(html).toContain('https://router.example.com/trello/webhook'); + }); + + it('renders active-webhooks list when provided', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ + activeTrelloWebhooks: [ + { id: 'wh-1', url: 'https://router.example.com/trello/webhook', active: true }, + { id: 'wh-2', url: 'https://other.example.com/trello/webhook', active: false }, + ], + }), + }), + ); + expect(html).toContain('https://router.example.com/trello/webhook'); + expect(html).toContain('https://other.example.com/trello/webhook'); + }); + + it('renders a "No Trello webhooks configured" fallback when active list is empty', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ activeTrelloWebhooks: [] }), + }), + ); + expect(html).toContain('No Trello webhooks configured'); + }); + + it('renders the Create Webhook button with data-action="create-webhook"', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('data-action="create-webhook"'); + }); + + it('disables the Create button when callbackBaseUrl is empty', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ callbackBaseUrl: '' }), + }), + ); + const buttonTag = html.match(/]*data-action="create-webhook"[^>]*>/)?.[0]; + expect(buttonTag).toBeDefined(); + expect(buttonTag).toMatch(/\sdisabled=""/); + }); + + it('enables the Create button when callbackBaseUrl is populated', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ callbackBaseUrl: 'https://router.example.com' }), + }), + ); + const buttonTag = html.match(/]*data-action="create-webhook"[^>]*>/)?.[0]; + expect(buttonTag).toBeDefined(); + expect(buttonTag).not.toMatch(/\sdisabled=""/); + }); + + it('interpolates trelloBoardId into the curl fallback template', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState({ trelloBoardId: 'board-xyz' }), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('"idModel": "board-xyz"'); + }); + + it('falls back to placeholder when trelloBoardId is empty', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState({ trelloBoardId: '' }), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).toContain('YOUR_BOARD_ID'); + }); + + it('renders delete buttons with data-action="delete-webhook" per active webhook', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks({ + activeTrelloWebhooks: [ + { id: 'wh-1', url: 'https://router.example.com/trello/webhook', active: true }, + { id: 'wh-2', url: 'https://other.example.com/trello/webhook', active: false }, + ], + }), + }), + ); + const deleteButtons = html.match(/data-action="delete-webhook"/g) ?? []; + expect(deleteButtons.length).toBe(2); + }); + + it('does not render Linear signing-secret field (regression guard)', () => { + const html = renderToStaticMarkup( + createElement(TrelloWebhookAdapter, { + state: makeState(), + dispatch: () => {}, + providerHooks: makeProviderHooks(), + }), + ); + expect(html).not.toMatch(/data-role="webhook_secret"/); + expect(html).not.toContain('LINEAR_WEBHOOK_SECRET'); + }); +}); diff --git a/web/src/components/projects/pm-providers/jira/webhook-step.tsx b/web/src/components/projects/pm-providers/jira/webhook-step.tsx new file mode 100644 index 00000000..7e966009 --- /dev/null +++ b/web/src/components/projects/pm-providers/jira/webhook-step.tsx @@ -0,0 +1,173 @@ +/** + * JIRA webhook step adapter (plan 012/2). + * + * Replaces the JIRA branch of the legacy `WebhookStep` (plan 012/4 + * deletes that). Fragment composing the shared `WebhookUrlDisplayStep` + * (URL + copy) with JIRA-specific UX: active-webhooks list, + * programmatic "Create Webhook" button, delete buttons, curl fallback. + * + * The `jiraEnsureLabels` side-effect (adds + removes CASCADE labels to + * seed autocomplete on first webhook creation) runs server-side inside + * `webhooks.create({ jiraOnly: true })`. No frontend change needed to + * preserve it. + */ + +import { createElement, Fragment, type ReactElement } from 'react'; +import { WebhookUrlDisplayStep } from '../steps/webhook-url-display.js'; +import type { ProviderWizardStepProps } from '../types.js'; + +interface ActiveWebhook { + readonly id: string; + readonly url: string; + readonly active: boolean; +} + +interface JiraWebhookProviderHooks { + readonly webhookUrl: string; + readonly callbackBaseUrl: string; + readonly activeJiraWebhooks: ReadonlyArray; + readonly webhooksLoading: boolean; + readonly createJiraWebhook: () => void; + readonly createLoading: boolean; + readonly createError: string | undefined; + readonly deleteJiraWebhook: (callbackBaseUrl: string) => void; + readonly deleteLoading: boolean; +} + +function asJiraWebhookHooks( + providerHooks: Record | undefined, +): JiraWebhookProviderHooks { + return (providerHooks ?? {}) as unknown as JiraWebhookProviderHooks; +} + +// Legacy endpoint path preserved (v1) — matches what operators currently +// copy-paste. JIRA v3 `/rest/api/3/webhook` has a different payload shape +// and changing it would require a coordinated docs update. Out of scope. +function buildJiraCurl(baseUrl: string, callbackBaseUrl: string): string { + const effectiveBaseUrl = baseUrl || ''; + const callbackUrl = callbackBaseUrl + ? `${callbackBaseUrl}/jira/webhook` + : '/jira/webhook'; + return `curl -X POST "${effectiveBaseUrl}/rest/webhooks/1.0/webhook" \\ + -H "Content-Type: application/json" \\ + -u ":" \\ + -d '{ + "name": "CASCADE webhook", + "url": "${callbackUrl}", + "events": ["jira:issue_updated", "jira:issue_created"], + "filters": {}, + "excludeBody": false + }'`; +} + +export function JiraWebhookAdapter({ + state, + providerHooks, +}: ProviderWizardStepProps): ReactElement { + const h = asJiraWebhookHooks(providerHooks); + const createDisabled = !h.callbackBaseUrl || h.createLoading; + + return createElement( + Fragment, + null, + // Shared URL display + copy button. + WebhookUrlDisplayStep({ + step: { + kind: 'webhook-url-display', + id: 'jira-webhook', + config: { + instructions: + 'Click "Create Webhook" below to register automatically, or use the curl command for manual setup.', + }, + }, + providerId: 'jira', + webhookUrl: h.webhookUrl, + }), + + // Active-webhooks list. + createElement( + 'div', + { className: 'pm-wizard-webhook-active-list', 'data-section': 'active-webhooks' }, + h.webhooksLoading + ? createElement('p', { 'data-state': 'loading' }, 'Loading webhooks…') + : h.activeJiraWebhooks.length === 0 + ? createElement( + 'p', + { className: 'pm-wizard-webhook-empty' }, + 'No JIRA webhooks configured for this project.', + ) + : createElement( + 'ul', + { className: 'pm-wizard-webhook-list' }, + ...h.activeJiraWebhooks.map((wh) => + createElement( + 'li', + { key: wh.id, className: 'pm-wizard-webhook-row', 'data-webhook-id': wh.id }, + createElement( + 'span', + { + className: 'pm-wizard-webhook-status', + 'data-active': wh.active ? 'true' : 'false', + }, + wh.active ? '●' : '○', + ), + createElement('code', { className: 'pm-wizard-webhook-url' }, wh.url), + createElement( + 'button', + { + type: 'button', + 'data-action': 'delete-webhook', + 'data-webhook-id': wh.id, + disabled: h.deleteLoading, + onClick: () => { + const base = wh.url.replace(/\/jira\/webhook$/, ''); + h.deleteJiraWebhook(base); + }, + }, + 'Delete', + ), + ), + ), + ), + ), + + // Create button. + createElement( + 'div', + { className: 'pm-wizard-webhook-create' }, + createElement( + 'button', + { + type: 'button', + 'data-action': 'create-webhook', + disabled: createDisabled, + onClick: () => h.createJiraWebhook(), + }, + h.createLoading ? 'Creating…' : 'Create Webhook', + ), + h.createError + ? createElement( + 'p', + { className: 'pm-wizard-webhook-error', 'data-state': 'error' }, + h.createError, + ) + : null, + ), + + // Curl fallback. + createElement( + 'details', + { className: 'pm-wizard-webhook-curl' }, + createElement( + 'summary', + null, + 'Manual webhook creation (alternative: if the button above doesn\u0027t work)', + ), + createElement( + 'pre', + { className: 'pm-wizard-webhook-curl-command' }, + buildJiraCurl(state.jiraBaseUrl ?? '', h.callbackBaseUrl), + ), + ), + ); +} diff --git a/web/src/components/projects/pm-providers/jira/wizard.ts b/web/src/components/projects/pm-providers/jira/wizard.ts index 940615c9..7ae5c8ec 100644 --- a/web/src/components/projects/pm-providers/jira/wizard.ts +++ b/web/src/components/projects/pm-providers/jira/wizard.ts @@ -16,16 +16,20 @@ * 7. webhook-url-display — shared */ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; +import { API_URL } from '@/lib/api.js'; +import { trpc, trpcClient } from '@/lib/trpc.js'; import { useJiraCustomFieldCreation, useJiraDiscovery } from '../../pm-wizard-hooks.js'; +import { deriveActiveWebhooks } from '../../pm-wizard-state.js'; import { ContainerPickStep } from '../steps/container-pick.js'; import { CredentialsStep } from '../steps/credentials.js'; import { CustomFieldMappingStep } from '../steps/custom-field-mapping.js'; import { LabelMappingStep } from '../steps/label-mapping.js'; import { StatusMappingStep } from '../steps/status-mapping.js'; -import { WebhookUrlDisplayStep } from '../steps/webhook-url-display.js'; import type { ProviderWizardDefinition, ProviderWizardStepProps } from '../types.js'; import { IssueTypeMappingStep } from './issue-type-step.js'; +import { JiraWebhookAdapter } from './webhook-step.js'; // CASCADE stage keys that map to JIRA statuses (name-based, not id-based // — JIRA statuses are configured per project, name is the stable identity). @@ -91,6 +95,19 @@ interface JiraProviderHooks { readonly issueTypes: ReadonlyArray<{ readonly name: string; readonly subtask: boolean }>; readonly onCreateCustomField: (slotKey: string, name: string) => void; readonly webhookUrl: string; + // Plan 012/2 — webhook-step plumbing: programmatic Create + active list + delete. + readonly callbackBaseUrl: string; + readonly activeJiraWebhooks: ReadonlyArray<{ + readonly id: string; + readonly url: string; + readonly active: boolean; + }>; + readonly webhooksLoading: boolean; + readonly createJiraWebhook: () => void; + readonly createLoading: boolean; + readonly createError: string | undefined; + readonly deleteJiraWebhook: (callbackBaseUrl: string) => void; + readonly deleteLoading: boolean; } function asJiraHooks(providerHooks: Record | undefined): JiraProviderHooks { @@ -199,21 +216,12 @@ function JiraIssueTypeAdapter({ }); } -function JiraWebhookDisplayAdapter({ providerHooks }: ProviderWizardStepProps): ReactElement { - const h = asJiraHooks(providerHooks); - return WebhookUrlDisplayStep({ - step: { - kind: 'webhook-url-display', - id: 'jira-webhook', - config: { - instructions: - 'In JIRA Automation or a custom webhook configuration, post issue events to this URL.', - }, - }, - providerId: 'jira', - webhookUrl: h.webhookUrl, - }); -} +// Plan 012/2: the jira-webhook step's Component is now `JiraWebhookAdapter` +// (imported from `./webhook-step.js`), a Fragment composing the shared +// WebhookUrlDisplayStep with JIRA-specific UX: active-webhooks list, +// programmatic Create button, delete buttons, curl fallback. The +// jiraEnsureLabels side-effect fires server-side inside +// `webhooks.create({ jiraOnly: true })`. export const jiraProviderWizard: ProviderWizardDefinition = { id: 'jira', @@ -259,7 +267,7 @@ export const jiraProviderWizard: ProviderWizardDefinition = { { id: 'jira-webhook', title: 'Webhook', - Component: JiraWebhookDisplayAdapter, + Component: JiraWebhookAdapter, isComplete: () => true, }, ], @@ -282,6 +290,7 @@ export const jiraProviderWizard: ProviderWizardDefinition = { useProviderHooks: ({ state, dispatch, projectId, advanceToStep }) => { const discovery = useJiraDiscovery(state, dispatch, advanceToStep, projectId ?? ''); const customField = useJiraCustomFieldCreation(state, dispatch); + const queryClient = useQueryClient(); const onCreateCustomField = (_slotKey: string, name: string) => { customField.createJiraCustomFieldMutation.mutate({ name }); @@ -289,6 +298,49 @@ export const jiraProviderWizard: ProviderWizardDefinition = { const webhookUrl = projectId ? `${window.location.origin}/webhooks/${projectId}/jira` : ''; + // Plan 012/2 — webhook plumbing. Mirrors the legacy `useWebhookManagement` + // formula (plan 012/4 deletes that hook). The server-side + // `jiraEnsureLabels` side-effect fires inside + // `webhooks.create({ jiraOnly: true })` unchanged. + const callbackBaseUrl = + API_URL || + (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); + + const webhooksQuery = useQuery(trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' })); + const activeJiraWebhooks = deriveActiveWebhooks('jira', webhooksQuery.data) as Array<{ + id: string; + url: string; + active: boolean; + }>; + + const createWebhookMutation = useMutation({ + mutationFn: () => + trpcClient.webhooks.create.mutate({ + projectId: projectId ?? '', + callbackBaseUrl, + jiraOnly: true, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' }).queryKey, + }); + }, + }); + + const deleteWebhookMutation = useMutation({ + mutationFn: (deleteBaseUrl: string) => + trpcClient.webhooks.delete.mutate({ + projectId: projectId ?? '', + callbackBaseUrl: deleteBaseUrl, + jiraOnly: true, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' }).queryKey, + }); + }, + }); + const details = state.jiraProjectDetails; return { @@ -306,6 +358,17 @@ export const jiraProviderWizard: ProviderWizardDefinition = { issueTypes: details?.issueTypes ?? [], onCreateCustomField, webhookUrl, + // Plan 012/2 — webhook plumbing consumed by `JiraWebhookAdapter`. + callbackBaseUrl, + activeJiraWebhooks, + webhooksLoading: webhooksQuery.isLoading, + createJiraWebhook: () => createWebhookMutation.mutate(), + createLoading: createWebhookMutation.isPending, + createError: createWebhookMutation.isError + ? (createWebhookMutation.error as Error).message + : undefined, + deleteJiraWebhook: (baseUrl: string) => deleteWebhookMutation.mutate(baseUrl), + deleteLoading: deleteWebhookMutation.isPending, } satisfies JiraProviderHooks & Record; }, }; diff --git a/web/src/components/projects/pm-providers/linear/webhook-step.tsx b/web/src/components/projects/pm-providers/linear/webhook-step.tsx new file mode 100644 index 00000000..b8ef8250 --- /dev/null +++ b/web/src/components/projects/pm-providers/linear/webhook-step.tsx @@ -0,0 +1,161 @@ +/** + * Linear webhook step adapter (plan 012/3). + * + * Linear has no programmatic webhook registration — Linear's API forbids + * it. This adapter replaces the legacy `LinearWebhookInfoPanel` (plan + * 012/4 deletes it) and the inlined `LinearWebhookDisplayAdapter` that + * plan 011/4 staged in `linear/wizard.ts`. Composition: shared + * `WebhookUrlDisplayStep` (URL + copy) + info banner + 5-step manual + * setup instructions + `ProjectSecretField` bound to + * `LINEAR_WEBHOOK_SECRET`. + * + * The secret field is NOT a controlled input — `ProjectSecretField` + * manages its own server round-trip via the project-credentials API. + * That's why the webhook step is a Fragment composition rather than + * a widening of the shared `WebhookUrlDisplayStep` (which would require + * a controlled secretValue + onSecretChange pattern). + */ + +import { createElement, Fragment, type ReactElement } from 'react'; +import { type ProjectCredentialMeta, ProjectSecretField } from '../../project-secret-field.js'; +import { WebhookUrlDisplayStep } from '../steps/webhook-url-display.js'; +import type { ProviderWizardStepProps } from '../types.js'; + +interface LinearWebhookProviderHooks { + readonly webhookUrl: string; + readonly projectIdForSecret: string; + readonly webhookSecretCredential: ProjectCredentialMeta | undefined; +} + +function asLinearWebhookHooks( + providerHooks: Record | undefined, +): LinearWebhookProviderHooks { + return (providerHooks ?? {}) as unknown as LinearWebhookProviderHooks; +} + +export function LinearWebhookAdapter({ providerHooks }: ProviderWizardStepProps): ReactElement { + const h = asLinearWebhookHooks(providerHooks); + + return createElement( + Fragment, + null, + // Info banner — manual setup required. + createElement( + 'div', + { className: 'pm-wizard-linear-webhook-info', 'data-section': 'info-banner' }, + createElement( + 'p', + { className: 'pm-wizard-linear-webhook-info-title' }, + 'Manual Webhook Setup Required', + ), + createElement( + 'p', + { className: 'pm-wizard-linear-webhook-info-body' }, + 'Linear webhooks must be configured manually in your Linear team settings. CASCADE cannot create them programmatically.', + ), + ), + + // Shared URL display + copy button. + WebhookUrlDisplayStep({ + step: { + kind: 'webhook-url-display', + id: 'linear-webhook', + config: { + instructions: + 'Configure this webhook URL manually in Linear → Settings → API → Webhooks.', + }, + }, + providerId: 'linear', + webhookUrl: h.webhookUrl, + }), + + // Signing-secret field (self-managing persistence via project-credentials + // API). Hidden when no projectId is available (e.g. wizard before save). + h.projectIdForSecret + ? createElement(ProjectSecretField, { + projectId: h.projectIdForSecret, + envVarKey: 'LINEAR_WEBHOOK_SECRET', + label: 'Webhook Signing Secret (optional)', + description: + 'Paste the signing secret from your Linear webhook. CASCADE verifies HMAC-SHA256 on every incoming Linear webhook request when this is set; verification is skipped if left blank.', + placeholder: 'lin_wh_...', + credential: h.webhookSecretCredential, + }) + : null, + + // 5-step setup instructions (copy lifted from the retiring + // LinearWebhookInfoPanel). + createElement( + 'div', + { className: 'pm-wizard-linear-webhook-instructions' }, + createElement( + 'p', + { className: 'pm-wizard-linear-webhook-instructions-heading' }, + 'Setup instructions:', + ), + createElement( + 'ol', + { className: 'pm-wizard-linear-webhook-steps' }, + createElement( + 'li', + null, + 'Go to ', + createElement( + 'a', + { + href: 'https://linear.app/settings/api', + target: '_blank', + rel: 'noopener noreferrer', + }, + 'linear.app/settings/api', + ), + ' and navigate to ', + createElement('strong', null, 'Webhooks'), + ), + createElement('li', null, 'Click "New webhook" and enter the URL above'), + createElement( + 'li', + null, + 'Enable these events (each maps to a CASCADE trigger handler):', + createElement( + 'ul', + { className: 'pm-wizard-linear-webhook-events' }, + createElement( + 'li', + null, + createElement('strong', null, 'Issues'), + ' — status transitions drive CASCADE\u2019s splitting, planning, and implementation agents', + ), + createElement( + 'li', + null, + createElement('strong', null, 'Comments'), + ' — @mentions of the CASCADE bot trigger a response agent', + ), + createElement( + 'li', + null, + createElement('strong', null, 'Issue Labels'), + ' — adding the "Ready to Process" label starts an agent on the issue', + ), + ), + ), + createElement('li', null, 'Select your team and save — webhooks are team-scoped in Linear'), + createElement( + 'li', + null, + 'If you set a signing secret in Linear, paste it into the field above so CASCADE can verify webhook authenticity', + ), + ), + ), + + // Project-scope cross-reference (identical copy to legacy). + createElement( + 'p', + { className: 'pm-wizard-linear-webhook-scope-note' }, + 'If you also set a Linear ', + createElement('strong', null, 'project scope'), + ' in the Board / Project Selection step, CASCADE applies that filter on its side after receiving each webhook — your Linear webhook configuration stays team-scoped and unchanged.', + ), + ); +} diff --git a/web/src/components/projects/pm-providers/linear/wizard.ts b/web/src/components/projects/pm-providers/linear/wizard.ts index 493305b0..552779a4 100644 --- a/web/src/components/projects/pm-providers/linear/wizard.ts +++ b/web/src/components/projects/pm-providers/linear/wizard.ts @@ -20,18 +20,18 @@ */ import { useQuery } from '@tanstack/react-query'; -import { createElement, Fragment, type ReactElement, useState } from 'react'; +import { type ReactElement, useState } from 'react'; import { trpc } from '@/lib/trpc.js'; import { useLinearDiscovery, useLinearLabelCreation } from '../../pm-wizard-hooks.js'; import { buildLinearIntegrationConfig } from '../../pm-wizard-state.js'; -import { type ProjectCredentialMeta, ProjectSecretField } from '../../project-secret-field.js'; +import type { ProjectCredentialMeta } from '../../project-secret-field.js'; import { ContainerPickStep } from '../steps/container-pick.js'; import { CredentialsStep } from '../steps/credentials.js'; import { LabelMappingStep } from '../steps/label-mapping.js'; import { ProjectScopeStep } from '../steps/project-scope.js'; import { StatusMappingStep } from '../steps/status-mapping.js'; -import { WebhookUrlDisplayStep } from '../steps/webhook-url-display.js'; import type { ProviderWizardDefinition, ProviderWizardStepProps } from '../types.js'; +import { LinearWebhookAdapter } from './webhook-step.js'; // CASCADE stage keys that map to Linear workflow state IDs (UUIDs — // Linear's issue-update API requires state UUIDs, not names; the @@ -192,48 +192,11 @@ function LinearProjectScopeAdapter({ providerHooks }: ProviderWizardStepProps): }); } -/** - * Linear's webhook step composes the shared `WebhookUrlDisplayStep` - * (URL + copy button + instructions) with the `ProjectSecretField` for - * `LINEAR_WEBHOOK_SECRET`. The secret field is NOT a simple controlled - * input — it manages its own server round-trip (save/load/cleared - * indicators) via the project-credentials API. Plan 011/1's widening - * of `webhook-url-display` (secretFieldRole + onSecretChange) assumed a - * controlled pattern; it remains useful for providers with simpler - * secret management but isn't the right fit here. Composition via - * Fragment is cleaner than forcing ProjectSecretField into the widened - * props. - */ -function LinearWebhookDisplayAdapter({ providerHooks }: ProviderWizardStepProps): ReactElement { - const h = asLinearHooks(providerHooks); - return createElement( - Fragment, - null, - WebhookUrlDisplayStep({ - step: { - kind: 'webhook-url-display', - id: 'linear-webhook', - config: { - instructions: - 'Configure this webhook URL manually in Linear → Settings → API → Webhooks. Enable the Issues, Comments, and Issue Labels event families.', - }, - }, - providerId: 'linear', - webhookUrl: h.webhookUrl, - }), - h.projectIdForSecret - ? createElement(ProjectSecretField, { - projectId: h.projectIdForSecret, - envVarKey: 'LINEAR_WEBHOOK_SECRET', - label: 'Webhook Signing Secret (optional)', - description: - 'Paste the signing secret from your Linear webhook. CASCADE verifies HMAC-SHA256 on every incoming Linear webhook request when this is set; verification is skipped if left blank.', - placeholder: 'lin_wh_...', - credential: h.webhookSecretCredential, - }) - : null, - ); -} +// Plan 012/3: the linear-webhook step's Component is now `LinearWebhookAdapter` +// (imported from `./webhook-step.js`) — a Fragment composing the shared +// WebhookUrlDisplayStep + info banner + 5-step setup instructions + +// ProjectSecretField for LINEAR_WEBHOOK_SECRET. Linear's API forbids +// programmatic webhook registration, so no Create/delete/curl UX. export const linearProviderWizard: ProviderWizardDefinition = { id: 'linear', @@ -273,7 +236,7 @@ export const linearProviderWizard: ProviderWizardDefinition = { { id: 'linear-webhook', title: 'Webhook', - Component: LinearWebhookDisplayAdapter, + Component: LinearWebhookAdapter, isComplete: () => true, }, ], diff --git a/web/src/components/projects/pm-providers/trello/webhook-step.tsx b/web/src/components/projects/pm-providers/trello/webhook-step.tsx new file mode 100644 index 00000000..27f290d0 --- /dev/null +++ b/web/src/components/projects/pm-providers/trello/webhook-step.tsx @@ -0,0 +1,171 @@ +/** + * Trello webhook step adapter (plan 012/1). + * + * Replaces the Trello branch of the legacy `WebhookStep` (plan 012/4 + * deletes that). Fragment composing the shared `WebhookUrlDisplayStep` + * (URL + copy) with Trello-specific UX: active-webhooks list, + * programmatic "Create Webhook" button, delete buttons, curl fallback. + * + * Rendered as the Trello wizard's `trello-webhook` step Component via the + * manifest path. All tRPC wiring (webhooks.list/create/delete with + * trelloOnly:true) lives in the Trello wizard's `useProviderHooks`; this + * component just renders what it receives. + */ + +import { createElement, Fragment, type ReactElement } from 'react'; +import { WebhookUrlDisplayStep } from '../steps/webhook-url-display.js'; +import type { ProviderWizardStepProps } from '../types.js'; + +interface ActiveWebhook { + readonly id: string; + readonly url: string; + readonly active: boolean; +} + +interface TrelloWebhookProviderHooks { + readonly webhookUrl: string; + readonly callbackBaseUrl: string; + readonly activeTrelloWebhooks: ReadonlyArray; + readonly webhooksLoading: boolean; + readonly createTrelloWebhook: () => void; + readonly createLoading: boolean; + readonly createError: string | undefined; + readonly deleteTrelloWebhook: (callbackBaseUrl: string) => void; + readonly deleteLoading: boolean; +} + +function asTrelloWebhookHooks( + providerHooks: Record | undefined, +): TrelloWebhookProviderHooks { + return (providerHooks ?? {}) as unknown as TrelloWebhookProviderHooks; +} + +function buildTrelloCurl(boardId: string, callbackBaseUrl: string): string { + const effectiveBoardId = boardId || ''; + const callbackUrl = callbackBaseUrl + ? `${callbackBaseUrl}/trello/webhook` + : '/trello/webhook'; + return `curl -X POST "https://api.trello.com/1/webhooks" \\ + -H "Content-Type: application/json" \\ + -d '{ + "key": "", + "token": "", + "callbackURL": "${callbackUrl}", + "idModel": "${effectiveBoardId}", + "description": "CASCADE webhook" + }'`; +} + +export function TrelloWebhookAdapter({ + state, + providerHooks, +}: ProviderWizardStepProps): ReactElement { + const h = asTrelloWebhookHooks(providerHooks); + const createDisabled = !h.callbackBaseUrl || h.createLoading; + + return createElement( + Fragment, + null, + // Shared URL display + copy button. + WebhookUrlDisplayStep({ + step: { + kind: 'webhook-url-display', + id: 'trello-webhook', + config: { + instructions: + 'Click "Create Webhook" below to register automatically, or use the curl command for manual setup.', + }, + }, + providerId: 'trello', + webhookUrl: h.webhookUrl, + }), + + // Active-webhooks list. + createElement( + 'div', + { className: 'pm-wizard-webhook-active-list', 'data-section': 'active-webhooks' }, + h.webhooksLoading + ? createElement('p', { 'data-state': 'loading' }, 'Loading webhooks…') + : h.activeTrelloWebhooks.length === 0 + ? createElement( + 'p', + { className: 'pm-wizard-webhook-empty' }, + 'No Trello webhooks configured for this project.', + ) + : createElement( + 'ul', + { className: 'pm-wizard-webhook-list' }, + ...h.activeTrelloWebhooks.map((wh) => + createElement( + 'li', + { key: wh.id, className: 'pm-wizard-webhook-row', 'data-webhook-id': wh.id }, + createElement( + 'span', + { + className: 'pm-wizard-webhook-status', + 'data-active': wh.active ? 'true' : 'false', + }, + wh.active ? '●' : '○', + ), + createElement('code', { className: 'pm-wizard-webhook-url' }, wh.url), + createElement( + 'button', + { + type: 'button', + 'data-action': 'delete-webhook', + 'data-webhook-id': wh.id, + disabled: h.deleteLoading, + onClick: () => { + // Extract base URL by stripping the trailing /trello/webhook + // path (matches the legacy WebhookStep delete behavior). + const base = wh.url.replace(/\/trello\/webhook$/, ''); + h.deleteTrelloWebhook(base); + }, + }, + 'Delete', + ), + ), + ), + ), + ), + + // Create button. + createElement( + 'div', + { className: 'pm-wizard-webhook-create' }, + createElement( + 'button', + { + type: 'button', + 'data-action': 'create-webhook', + disabled: createDisabled, + onClick: () => h.createTrelloWebhook(), + }, + h.createLoading ? 'Creating…' : 'Create Webhook', + ), + h.createError + ? createElement( + 'p', + { className: 'pm-wizard-webhook-error', 'data-state': 'error' }, + h.createError, + ) + : null, + ), + + // Curl fallback. + createElement( + 'details', + { className: 'pm-wizard-webhook-curl' }, + createElement( + 'summary', + null, + 'Manual webhook creation (alternative: if the button above doesn\u0027t work)', + ), + createElement( + 'pre', + { className: 'pm-wizard-webhook-curl-command' }, + buildTrelloCurl(state.trelloBoardId ?? '', h.callbackBaseUrl), + ), + ), + ); +} diff --git a/web/src/components/projects/pm-providers/trello/wizard.ts b/web/src/components/projects/pm-providers/trello/wizard.ts index 9d6cc5d9..49178fdb 100644 --- a/web/src/components/projects/pm-providers/trello/wizard.ts +++ b/web/src/components/projects/pm-providers/trello/wizard.ts @@ -15,21 +15,25 @@ * 6. webhook-url-display — router URL + copy button */ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2 } from 'lucide-react'; import type { ReactElement } from 'react'; import { useState } from 'react'; +import { API_URL } from '@/lib/api.js'; +import { trpc, trpcClient } from '@/lib/trpc.js'; import { useTrelloCustomFieldCreation, useTrelloDiscovery, useTrelloLabelCreation, } from '../../pm-wizard-hooks.js'; +import { deriveActiveWebhooks } from '../../pm-wizard-state.js'; import { ContainerPickStep } from '../steps/container-pick.js'; import { CustomFieldMappingStep } from '../steps/custom-field-mapping.js'; import { LabelMappingStep } from '../steps/label-mapping.js'; import { StatusMappingStep } from '../steps/status-mapping.js'; -import { WebhookUrlDisplayStep } from '../steps/webhook-url-display.js'; import type { ProviderWizardDefinition, ProviderWizardStepProps } from '../types.js'; import { TrelloOAuthStep } from './oauth-step.js'; +import { TrelloWebhookAdapter } from './webhook-step.js'; // CASCADE stage keys that map to Trello lists (one list per stage). const TRELLO_LIST_SLOTS = [ @@ -110,6 +114,19 @@ interface TrelloProviderHooks { readonly onCreateCustomField: (slotKey: string, name: string) => void; readonly webhookUrl: string; readonly creatingSlot: string | null; + // Plan 012/1 — webhook-step plumbing: programmatic Create + active list + delete. + readonly callbackBaseUrl: string; + readonly activeTrelloWebhooks: ReadonlyArray<{ + readonly id: string; + readonly url: string; + readonly active: boolean; + }>; + readonly webhooksLoading: boolean; + readonly createTrelloWebhook: () => void; + readonly createLoading: boolean; + readonly createError: string | undefined; + readonly deleteTrelloWebhook: (callbackBaseUrl: string) => void; + readonly deleteLoading: boolean; } function asTrelloHooks(providerHooks: Record | undefined): TrelloProviderHooks { @@ -193,21 +210,10 @@ function TrelloCustomFieldMappingAdapter({ }); } -function TrelloWebhookDisplayAdapter({ providerHooks }: ProviderWizardStepProps): ReactElement { - const h = asTrelloHooks(providerHooks); - return WebhookUrlDisplayStep({ - step: { - kind: 'webhook-url-display', - id: 'trello-webhook', - config: { - instructions: - 'Add this URL as a Trello webhook using the REST API. See docs for the exact curl command.', - }, - }, - providerId: 'trello', - webhookUrl: h.webhookUrl, - }); -} +// Plan 012/1: the trello-webhook step's Component is now `TrelloWebhookAdapter` +// (imported from `./webhook-step.js`), a Fragment composing the shared +// WebhookUrlDisplayStep with Trello-specific UX: active-webhooks list, +// programmatic Create button, delete buttons, curl fallback. export const trelloProviderWizard: ProviderWizardDefinition = { id: 'trello', @@ -248,7 +254,7 @@ export const trelloProviderWizard: ProviderWizardDefinition = { { id: 'trello-webhook', title: 'Webhook', - Component: TrelloWebhookDisplayAdapter, + Component: TrelloWebhookAdapter, isComplete: () => true, }, ], @@ -270,6 +276,7 @@ export const trelloProviderWizard: ProviderWizardDefinition = { const discovery = useTrelloDiscovery(state, dispatch, advanceToStep, projectId ?? ''); const labels = useTrelloLabelCreation(state, dispatch); const customField = useTrelloCustomFieldCreation(state, dispatch); + const queryClient = useQueryClient(); const [creatingSlot, setCreatingSlot] = useState(null); @@ -289,6 +296,50 @@ export const trelloProviderWizard: ProviderWizardDefinition = { const webhookUrl = projectId ? `${window.location.origin}/webhooks/${projectId}/trello` : ''; + // Plan 012/1 — webhook plumbing. Mirrors the legacy `useWebhookManagement` + // formula (plan 012/4 deletes that hook). Computes the public router URL + // from the Vite env (dev) or current origin (prod), fetches active + // webhooks via `trpc.webhooks.list`, wraps create/delete mutations with + // `trelloOnly: true`. + const callbackBaseUrl = + API_URL || + (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); + + const webhooksQuery = useQuery(trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' })); + const activeTrelloWebhooks = deriveActiveWebhooks('trello', webhooksQuery.data) as Array<{ + id: string; + url: string; + active: boolean; + }>; + + const createWebhookMutation = useMutation({ + mutationFn: () => + trpcClient.webhooks.create.mutate({ + projectId: projectId ?? '', + callbackBaseUrl, + trelloOnly: true, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' }).queryKey, + }); + }, + }); + + const deleteWebhookMutation = useMutation({ + mutationFn: (deleteBaseUrl: string) => + trpcClient.webhooks.delete.mutate({ + projectId: projectId ?? '', + callbackBaseUrl: deleteBaseUrl, + trelloOnly: true, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: trpc.webhooks.list.queryOptions({ projectId: projectId ?? '' }).queryKey, + }); + }, + }); + const boardDetails = state.trelloBoardDetails; return { @@ -309,6 +360,17 @@ export const trelloProviderWizard: ProviderWizardDefinition = { // Exposed for any caller that wants to render a secondary // loading indicator near the board-picker step. boardDetailsLoadingIcon: Loader2, + // Plan 012/1 — webhook plumbing consumed by `TrelloWebhookAdapter`. + callbackBaseUrl, + activeTrelloWebhooks, + webhooksLoading: webhooksQuery.isLoading, + createTrelloWebhook: () => createWebhookMutation.mutate(), + createLoading: createWebhookMutation.isPending, + createError: createWebhookMutation.isError + ? (createWebhookMutation.error as Error).message + : undefined, + deleteTrelloWebhook: (baseUrl: string) => deleteWebhookMutation.mutate(baseUrl), + deleteLoading: deleteWebhookMutation.isPending, } satisfies TrelloProviderHooks & Record; }, }; diff --git a/web/src/components/projects/pm-wizard-common-steps.tsx b/web/src/components/projects/pm-wizard-common-steps.tsx index 428df778..ce83b140 100644 --- a/web/src/components/projects/pm-wizard-common-steps.tsx +++ b/web/src/components/projects/pm-wizard-common-steps.tsx @@ -1,369 +1,16 @@ /** - * Provider-agnostic step renderer components for PMWizard: - * WebhookStep and SaveStep. + * Provider-agnostic step renderer components for PMWizard. + * + * **Plan 012/4 (2026-04-18+):** `WebhookStep` + `LinearWebhookInfoPanel` + * deleted. Every PM provider now owns its webhook step via the manifest + * path (see `./pm-providers//webhook-step.tsx`). Only + * `SaveStep` remains in this file — it's the one cross-provider step + * that doesn't fit the per-provider-step model (operates on the + * `saveMutation` from the parent wizard). */ import type { UseMutationResult } from '@tanstack/react-query'; -import { - AlertCircle, - AlertTriangle, - Check, - Clipboard, - ExternalLink, - Info, - Loader2, - RefreshCw, - Trash2, -} from 'lucide-react'; -import { useState } from 'react'; -import { Label } from '@/components/ui/label.js'; import type { WizardState } from './pm-wizard-state.js'; -import { type ProjectCredentialMeta, ProjectSecretField } from './project-secret-field.js'; - -// ============================================================================ -// WebhookStep -// ============================================================================ - -interface ActiveWebhook { - id: string; - url: string; - active: boolean; -} - -interface WebhooksQueryProps { - isLoading: boolean; - data?: { - errors?: Record; - }; - refetch: () => void; -} - -function CopyButton({ text }: { text: string }) { - const [copied, setCopied] = useState(false); - const handleCopy = async () => { - await navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - return ( - - ); -} - -// ============================================================================ -// LinearWebhookInfoPanel -// ============================================================================ - -export function LinearWebhookInfoPanel({ - webhookUrl, - projectId, - webhookSecretCredential, -}: { - webhookUrl: string; - projectId: string; - webhookSecretCredential?: ProjectCredentialMeta; -}) { - return ( -
            -
            -
            - -
            -

            - Manual Webhook Setup Required -

            -

            - Linear webhooks must be configured manually in your Linear team settings. CASCADE - cannot create them programmatically. -

            -
            -
            -
            - -
            - -
            - {webhookUrl} - -
            -
            - - - -
            -

            Setup instructions:

            -
              -
            1. - Go to{' '} - - linear.app/settings/api - {' '} - and navigate to Webhooks -
            2. -
            3. Click "New webhook" and enter the URL above
            4. -
            5. - Enable these events (each maps to a CASCADE trigger handler): -
                -
              • - Issues — status transitions drive CASCADE's splitting, - planning, and implementation agents -
              • -
              • - Comments — @mentions of the CASCADE bot trigger a response agent -
              • -
              • - Issue Labels — adding the "Ready to Process" label starts - an agent on the issue -
              • -
              -
            6. -
            7. Select your team and save — webhooks are team-scoped in Linear
            8. -
            9. - If you set a signing secret in Linear, paste it into the field above so CASCADE can - verify webhook authenticity -
            10. -
            -
            - -

            - If you also set a Linear project scope in the Board / Project Selection - step, CASCADE applies that filter on its side after receiving each webhook — your Linear - webhook configuration stays team-scoped and unchanged. -

            -
            - ); -} - -// ============================================================================ -// WebhookStep -// ============================================================================ - -export function WebhookStep({ - state, - webhooksQuery, - activeWebhooks, - callbackBaseUrl, - createWebhookMutation, - deleteWebhookMutation, - linearWebhookUrl, - projectId, - linearWebhookSecretCredential, -}: { - state: WizardState; - webhooksQuery: WebhooksQueryProps; - activeWebhooks: ActiveWebhook[]; - callbackBaseUrl: string; - createWebhookMutation: UseMutationResult; - deleteWebhookMutation: UseMutationResult; - linearWebhookUrl?: string; - projectId: string; - linearWebhookSecretCredential?: ProjectCredentialMeta; -}) { - // Linear uses a display-only panel — no create/delete buttons - if (state.provider === 'linear') { - return ( - - ); - } - - const isTrello = state.provider === 'trello'; - const providerName = isTrello ? 'Trello' : 'JIRA'; - - // Build curl commands for manual webhook creation - const buildTrelloCurl = () => { - const boardId = state.trelloBoardId || ''; - const callbackUrl = callbackBaseUrl - ? `${callbackBaseUrl}/trello/webhook` - : '/trello/webhook'; - return `curl -X POST "https://api.trello.com/1/webhooks" \\ - -H "Content-Type: application/json" \\ - -d '{ - "key": "", - "token": "", - "callbackURL": "${callbackUrl}", - "idModel": "${boardId}", - "description": "CASCADE webhook" - }'`; - }; - - const buildJiraCurl = () => { - const baseUrl = state.jiraBaseUrl || ''; - const callbackUrl = callbackBaseUrl - ? `${callbackBaseUrl}/jira/webhook` - : '/jira/webhook'; - return `curl -X POST "${baseUrl}/rest/webhooks/1.0/webhook" \\ - -H "Content-Type: application/json" \\ - -u ":" \\ - -d '{ - "name": "CASCADE webhook", - "url": "${callbackUrl}", - "events": ["jira:issue_updated", "jira:issue_created"], - "filters": {}, - "excludeBody": false - }'`; - }; - - const curlCommand = isTrello ? buildTrelloCurl() : buildJiraCurl(); - - return ( -
            - {/* Per-provider errors */} - {webhooksQuery.data?.errors && - Object.entries(webhooksQuery.data.errors) - .filter(([provider, err]) => err != null && provider !== 'github') - .map(([provider, err]) => ( -
            - -
            - - {provider} - - : {String(err)} -
            - -
            - ))} - - {webhooksQuery.isLoading ? ( -
            - Loading webhooks... -
            - ) : activeWebhooks.length > 0 ? ( -
            - - {activeWebhooks.map((w) => ( -
            -
            - - {w.url} -
            - -
            - ))} -
            - ) : ( -
            - - No {providerName} webhooks configured for this project. -
            - )} - - {/* curl instructions for manual webhook creation (collapsible) */} -
            - - -

            - Manual webhook creation (alternative: if the button below doesn't work) -

            -
            -
            -

            - Use the following curl command to create the {providerName} webhook manually with your - own credentials: -

            -
            -
            - -
            -
            -							{curlCommand}
            -						
            -
            -
            -
            - -
            -
            - -
            - {createWebhookMutation.isError && ( -

            - {(createWebhookMutation.error as Error).message} -

            - )} - {createWebhookMutation.isSuccess && ( -

            - {webhooksQuery.data?.errors && - Object.entries(webhooksQuery.data.errors) - .filter(([provider]) => provider !== 'github') - .some(([, e]) => e != null) - ? 'Webhook created, but some providers failed to load — see warnings above.' - : 'Webhook created successfully.'} -

            - )} -
            -
            - ); -} - -// ============================================================================ -// SaveStep -// ============================================================================ export function SaveStep({ state, diff --git a/web/src/components/projects/pm-wizard-hooks.ts b/web/src/components/projects/pm-wizard-hooks.ts index fc881da2..573d7c13 100644 --- a/web/src/components/projects/pm-wizard-hooks.ts +++ b/web/src/components/projects/pm-wizard-hooks.ts @@ -457,68 +457,10 @@ export function useVerification( return { verifyMutation }; } -// ============================================================================ -// Webhook Management -// ============================================================================ - -export function useWebhookManagement(projectId: string, state: WizardState) { - const queryClient = useQueryClient(); - const callbackBaseUrl = - API_URL || - (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); - - const createWebhookMutation = useMutation({ - mutationFn: () => - trpcClient.webhooks.create.mutate({ - projectId, - callbackBaseUrl, - trelloOnly: state.provider === 'trello' ? true : undefined, - jiraOnly: state.provider === 'jira' ? true : undefined, - }), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: trpc.webhooks.list.queryOptions({ projectId }).queryKey, - }); - }, - }); - - const deleteWebhookMutation = useMutation({ - mutationFn: (deleteCallbackBaseUrl: string) => - trpcClient.webhooks.delete.mutate({ - projectId, - callbackBaseUrl: deleteCallbackBaseUrl, - trelloOnly: state.provider === 'trello' ? true : undefined, - jiraOnly: state.provider === 'jira' ? true : undefined, - }), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: trpc.webhooks.list.queryOptions({ projectId }).queryKey, - }); - }, - }); - - return { - callbackBaseUrl, - createWebhookMutation, - deleteWebhookMutation, - }; -} - -// ============================================================================ -// Linear Webhook Info (display-only) -// ============================================================================ - -export function useLinearWebhookInfo() { - const callbackBaseUrl = - API_URL || - (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); - - const webhookUrl = callbackBaseUrl - ? `${callbackBaseUrl}/linear/webhook` - : '/linear/webhook'; - - return { webhookUrl }; -} +// Plan 012/4: `useWebhookManagement` + `useLinearWebhookInfo` deleted. +// Each provider's `useProviderHooks` now inlines the webhook plumbing +// (`webhooks.list/create/delete` + `callbackBaseUrl` formula) — +// see `./pm-providers/{trello,jira,linear}/wizard.ts`. // ============================================================================ // Trello Label Creation diff --git a/web/src/components/projects/pm-wizard.tsx b/web/src/components/projects/pm-wizard.tsx index 1d7525a2..12636903 100644 --- a/web/src/components/projects/pm-wizard.tsx +++ b/web/src/components/projects/pm-wizard.tsx @@ -11,21 +11,17 @@ import './pm-providers/jira/index.js'; import './pm-providers/linear/index.js'; import { ManifestProviderWizardSection } from './pm-providers/manifest-section.js'; import { getProviderWizard } from './pm-providers/registry.js'; -import { SaveStep, WebhookStep } from './pm-wizard-common-steps.js'; -import { - useLinearWebhookInfo, - useSaveMutation, - useVerification, - useWebhookManagement, -} from './pm-wizard-hooks.js'; +import { SaveStep } from './pm-wizard-common-steps.js'; +import { useSaveMutation, useVerification } from './pm-wizard-hooks.js'; // Plan 011/5: the three legacy `pm-wizard-{trello,jira,linear}-steps.tsx` // files were deleted; all three providers now render exclusively through // the manifest path (see `./pm-providers//wizard.ts`). +// Plan 012/4: `WebhookStep` + `LinearWebhookInfoPanel` + supporting hooks +// deleted; every provider owns its webhook UX via the manifest path. import { areCredentialsReady, buildEditState, createInitialState, - deriveActiveWebhooks, isStep1Complete, wizardReducer, } from './pm-wizard-state.js'; @@ -67,7 +63,6 @@ export function PMWizard({ initialProvider: string; initialConfig?: Record; }) { - const webhooksQuery = useQuery(trpc.webhooks.list.queryOptions({ projectId })); const credentialsQuery = useQuery(trpc.projects.credentials.list.queryOptions({ projectId })); const [state, dispatch] = useReducer(wizardReducer, undefined, createInitialState); @@ -123,19 +118,11 @@ export function PMWizard({ const { verifyMutation } = useVerification(state, dispatch, advanceToStep); // Every PM provider (Trello 006/2, JIRA 006/3, Linear 006/4) composes its - // discovery / label / custom-field hooks inside its own useProviderHooks. - // The parent wizard no longer calls any provider-specific React hook. - const webhookManagement = useWebhookManagement(projectId, state); - const { webhookUrl: linearWebhookUrl } = useLinearWebhookInfo(); + // discovery / label / custom-field / webhook hooks inside its own + // useProviderHooks. The parent wizard no longer calls any provider- + // specific React hook. const { saveMutation } = useSaveMutation(projectId, state); - const linearWebhookSecretCredential = credentialsQuery.data?.find( - (c) => c.envVarKey === 'LINEAR_WEBHOOK_SECRET', - ); - - // Label creation + discovery handlers now live inside each provider's - // useProviderHooks (Trello 006/2, JIRA 006/3, Linear 006/4). - // ---- Step status ---- const credsReady = areCredentialsReady(state); @@ -149,25 +136,15 @@ export function PMWizard({ return 'pending'; } - // ---- Active webhooks for this provider ---- - const activeWebhooks = deriveActiveWebhooks(state.provider, webhooksQuery.data); - - // ---- Manifest step layout (plan 011/4) ---- - // Iterate over `manifestDef.steps`, but skip the provider's webhook - // display step — the legacy WebhookStep below still owns webhook - // registration (Trello/JIRA API calls) + signing-secret UX (Linear). - // Convention: every provider's webhook step id ends with `-webhook` - // (`trello-webhook`, `jira-webhook`, `linear-webhook`). The shared - // `webhook-url-display` component (widened in plan 011/1) is dormant - // until a future plan migrates webhook-creation UX into the manifest - // path. + // ---- Manifest step layout (plans 011/4 + 012/1-4) ---- + // Iterate over `manifestDef.steps`. Every PM provider owns every + // wizard step via the manifest path — credentials, container-pick, + // mappings, webhook, everything. Parent wizard only owns the provider + // picker (step 1) and the final Save step. const renderedManifestSteps = manifestDef - ? manifestDef.steps - .map((step, index) => ({ step, index })) - .filter((entry) => !entry.step.id.endsWith('-webhook')) + ? manifestDef.steps.map((step, index) => ({ step, index })) : []; - const webhookStepNumber = renderedManifestSteps.length + 2; // +1 for provider, +1 for 1-indexed - const saveStepNumber = webhookStepNumber + 1; + const saveStepNumber = renderedManifestSteps.length + 2; // +1 for provider picker, +1 for 1-indexed // ---- Render ---- @@ -208,14 +185,11 @@ export function PMWizard({ {/* - * Plan 011/4: dynamic manifest-step rendering. Each provider's - * `wizardSpec.steps` drives its own slot count. We skip steps of - * kind `webhook-url-display` because the legacy WebhookStep below - * still owns programmatic webhook creation (Trello/JIRA API calls) - * and per-provider setup UX. The shared `webhook-url-display` - * component (widened in plan 011/1) is dormant for the three - * existing providers until a future plan ports webhook-creation - * UX into the manifest path. + * Plan 011/4 + 012/4: dynamic manifest-step rendering. Each + * provider's `wizardSpec.steps` drives its own slot count. Every + * step — including webhook-url-display — renders via the manifest + * path. Parent wizard owns only the provider picker (step 1) and + * the final Save step. */} {manifestDef && renderedManifestSteps.map((entry, idx) => { @@ -277,28 +251,6 @@ export function PMWizard({ ); })} - {/* Legacy Webhook slot — still handles programmatic webhook - registration for Trello/JIRA + the signing-secret UX for - Linear. Scheduled for migration into the manifest path in a - future plan. */} - toggleStep(webhookStepNumber)} - > - - - {/* Save slot. */}