diff --git a/CHANGELOG.md b/CHANGELOG.md index def54f2c..e26034ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable user-visible changes to CASCADE are documented here. The format is l - **PM integration plug-and-play (infrastructure).** Introduced `PMProviderManifest` as the canonical per-provider contract — one object declares credentials, webhook route and verifier, router adapter, trigger handlers, platform client, job-id extractor, and optional label-creation hook. Landed `pmProviderRegistry`, a conformance test harness (`tests/unit/integrations/pm-conformance.test.ts`), shared helpers (`_shared/auth-headers.ts`, `_shared/webhook-verifier.ts`, `_shared/label-id-resolver.ts`, `_shared/project-id-extractor.ts`), a new `pm.discovery` tRPC router, and a frontend provider-wizard registry with a generic step renderer. Dormant in this release — Trello, JIRA, and Linear continue to register through the legacy path; they migrate onto the manifest in follow-up PRs. No operator-visible changes. Closes plan 006/1 of spec [006](docs/specs/006-pm-integration-plug-and-play.md). - **PM integration plug-and-play (Trello migrated).** Trello's webhook signature verifier, router adapter, triggers, platform client, job-id extractor, wizard steps, and label/custom-field creation hooks are now composed via a single `trelloManifest` + `trelloProviderWizard`. Extended the `ProviderWizardDefinition` contract with an optional `useProviderHooks` field so provider-specific React hooks run inside a shell component — `ManifestProviderWizardSection` — rather than at the wizard root; this is how we satisfy the React rules-of-hooks while still keeping Trello's Discovery/LabelCreation/CustomFieldCreation hook composition per-provider. The conformance harness now exercises Trello alongside the test fixture (22 shared tests × provider). Trello's legacy registrations in `bootstrap.ts` stay for now because nine-plus call sites still use `pmRegistry.get('trello')` — plan 006/5 migrates those callers and deletes the legacy lines. No operator-visible changes. Closes plan 006/2 of spec [006](docs/specs/006-pm-integration-plug-and-play.md). +- **PM integration plug-and-play (JIRA migrated).** JIRA joins Trello on the manifest pattern with `jiraManifest` + `jiraProviderWizard`. `verifyWebhookSignature` uses the shared `makeHmacSha256Verifier` factory (Trello's bespoke scheme didn't fit, so this is the first consumer). Wizard steps + discovery / custom-field hooks moved into `jiraProviderWizard.useProviderHooks`; the JIRA-specific branches and hook instantiations are gone from `pm-wizard.tsx`. `worker-env.ts::extractProjectIdFromJob` JIRA branch removed (registry path handles it). Conformance harness now exercises Trello + JIRA + TestProvider (33 shared assertions × provider). Same deferrals as 006/2: `bootstrap.ts` JIRA registration stays until plan 006/5 migrates the `pmRegistry.get('jira')` callers. No operator-visible changes. Closes plan 006/3 of spec [006](docs/specs/006-pm-integration-plug-and-play.md). ### Added diff --git a/docs/plans/006-pm-integration-plug-and-play/3-migrate-jira.md b/docs/plans/006-pm-integration-plug-and-play/3-migrate-jira.md.done similarity index 90% rename from docs/plans/006-pm-integration-plug-and-play/3-migrate-jira.md rename to docs/plans/006-pm-integration-plug-and-play/3-migrate-jira.md.done index 0fb1cf4c..ba2d420e 100644 --- a/docs/plans/006-pm-integration-plug-and-play/3-migrate-jira.md +++ b/docs/plans/006-pm-integration-plug-and-play/3-migrate-jira.md.done @@ -5,8 +5,8 @@ plan: 3 plan_slug: migrate-jira level: plan parent_spec: docs/specs/006-pm-integration-plug-and-play.md -depends_on: [1-infrastructure.md] -status: pending +depends_on: [1-infrastructure.md.done, 2-migrate-trello.md.done] +status: done --- # 006/3: Migrate JIRA onto the PM provider manifest @@ -188,15 +188,17 @@ Automatic via `listPMProviders()` iteration. Ensure JIRA's manifest module is im ## Progress -- [ ] AC #1 JIRA manifest registered -- [ ] AC #2 Conformance harness passes JIRA -- [ ] AC #3 Existing JIRA tests green unchanged -- [ ] AC #4 Wizard JIRA branch removed -- [ ] AC #5 Legacy registration branches removed for JIRA -- [ ] AC #6 JIRA tRPC endpoints consolidated -- [ ] AC #7 Operator-facing JIRA behavior unchanged -- [ ] AC #8 All new code has tests -- [ ] AC #9 Build passes -- [ ] AC #10 Tests pass -- [ ] AC #11 Lint passes -- [ ] AC #12 Typecheck passes +- [x] AC #1 JIRA manifest registered +- [x] AC #2 Conformance harness passes JIRA (33 tests — 11 × 3 providers) +- [x] AC #3 Existing JIRA tests green unchanged +- [x] AC #4 Wizard JIRA branch removed — both step renders and hook instantiations +- [x] AC #5 Legacy registration branches removed for JIRA — `builtins.ts`, `worker-env.ts` extractor +- [ ] AC #6 JIRA tRPC endpoints consolidated — **deferred to plan 006/5** (same reasoning as 006/2; `createJiraCustomField` stays for now) +- [x] AC #7 Operator-facing JIRA behavior unchanged — 7782/7782 tests pass; JIRA SSR tests green +- [x] AC #8 All new code has tests (16 new JIRA manifest tests) +- [x] AC #9 Build passes (backend + web) +- [x] AC #10 Tests pass (7782/7782) +- [x] AC #11 Lint passes +- [x] AC #12 Typecheck passes + +**Partial-state**: `src/integrations/bootstrap.ts` JIRA registration retained — same reason as 006/2 (multiple `pmRegistry.get('jira')` callers still need migration, deferred to plan 006/5). diff --git a/src/integrations/README.md b/src/integrations/README.md index 5ab4eb09..318a4d99 100644 --- a/src/integrations/README.md +++ b/src/integrations/README.md @@ -4,8 +4,8 @@ CASCADE's PM providers (Trello, JIRA, Linear, and any future Asana/GitLab/ClickU This document is the canonical guide for adding a new PM provider. -> **Migration status (plans 006/3–006/4 in flight):** -> **Trello: ✓ migrated** (plan 006/2). JIRA and Linear continue to register through the legacy path described at the bottom of this file until plans 006/3 and 006/4 merge. Trello's `pmRegistry` registration is kept in `src/integrations/bootstrap.ts` for now because many call sites still look up `pmRegistry.get('trello')`; plan 006/5 removes those callers and the bootstrap line together. +> **Migration status (plan 006/4 in flight):** +> **Trello: ✓ migrated** (plan 006/2). **JIRA: ✓ migrated** (plan 006/3). Linear still on the legacy path — plan 006/4. Trello's and JIRA's `pmRegistry` registrations are kept in `src/integrations/bootstrap.ts` for now because many call sites still look up `pmRegistry.get('trello' | 'jira')`; plan 006/5 removes those callers and the bootstrap lines together. --- diff --git a/src/integrations/pm/index.ts b/src/integrations/pm/index.ts index a39f0f3a..82f930a4 100644 --- a/src/integrations/pm/index.ts +++ b/src/integrations/pm/index.ts @@ -7,3 +7,4 @@ */ import './trello/index.js'; +import './jira/index.js'; diff --git a/src/integrations/pm/jira/index.ts b/src/integrations/pm/jira/index.ts new file mode 100644 index 00000000..859f7a77 --- /dev/null +++ b/src/integrations/pm/jira/index.ts @@ -0,0 +1,10 @@ +/** + * JIRA PM provider — side-effect module that registers the manifest. + */ + +import { registerPMProvider } from '../registry.js'; +import { jiraManifest } from './manifest.js'; + +registerPMProvider(jiraManifest); + +export { jiraManifest }; diff --git a/src/integrations/pm/jira/manifest.ts b/src/integrations/pm/jira/manifest.ts new file mode 100644 index 00000000..41d048bd --- /dev/null +++ b/src/integrations/pm/jira/manifest.ts @@ -0,0 +1,67 @@ +/** + * JIRA PM provider manifest. + * + * Wires the existing JIRA implementation (JiraIntegration, JiraRouterAdapter, + * JIRA triggers, JiraPlatformClient) into the PMProviderManifest contract. + * + * Signing: JIRA uses `HMAC-SHA256(body)` with `sha256=` in the + * `X-Hub-Signature` header. This maps onto the shared + * `makeHmacSha256Verifier` factory landed in plan 006/1. + * + * Labels: JIRA labels are free-form names — the JIRA API auto-creates + * them on use. The shared `label-id-resolver` helper is NOT wired here; + * it's UUID-only. No `createLabel` manifest hook either for the same + * reason. + */ + +import { JiraIntegration } from '../../../pm/jira/integration.js'; +import { JiraRouterAdapter } from '../../../router/adapters/jira.js'; +import { JiraPlatformClient } from '../../../router/platformClients/jira.js'; +import { JiraCommentMentionTrigger } from '../../../triggers/jira/comment-mention.js'; +import { JiraReadyToProcessLabelTrigger } from '../../../triggers/jira/label-added.js'; +import { JiraStatusChangedTrigger } from '../../../triggers/jira/status-changed.js'; +import { makeHmacSha256Verifier } from '../_shared/webhook-verifier.js'; +import type { PMProviderManifest } from '../manifest.js'; + +const jiraIntegration = new JiraIntegration(); + +export const jiraManifest: PMProviderManifest = { + id: 'jira', + label: 'JIRA', + category: 'pm', + + credentialRoles: [ + { role: 'email', label: 'Email', envVarKey: 'JIRA_EMAIL' }, + { role: 'api_token', label: 'API Token', envVarKey: 'JIRA_API_TOKEN' }, + { + role: 'webhook_secret', + label: 'Webhook Secret', + envVarKey: 'JIRA_WEBHOOK_SECRET', + optional: true, + }, + ], + + webhookRoute: '/jira/webhook', + verifyWebhookSignature: makeHmacSha256Verifier({ + headerName: 'x-hub-signature', + headerPrefix: 'sha256=', + }), + + routerAdapter: new JiraRouterAdapter(), + + extractProjectIdFromJob: async (jobData) => { + const d = jobData as unknown as { type?: string; projectId?: string }; + if (d.type !== 'jira') return null; + return d.projectId ?? null; + }, + + pmIntegration: jiraIntegration, + + triggerHandlers: [ + new JiraCommentMentionTrigger(), + new JiraStatusChangedTrigger(), + new JiraReadyToProcessLabelTrigger(), + ], + + platformClientFactory: (projectId) => new JiraPlatformClient(projectId), +}; diff --git a/src/router/worker-env.ts b/src/router/worker-env.ts index b004d15f..e097b819 100644 --- a/src/router/worker-env.ts +++ b/src/router/worker-env.ts @@ -35,10 +35,9 @@ export async function extractProjectIdFromJob(data: CascadeJob): Promise { + await import('../../../../../src/integrations/pm/jira/index.js'); + const m = getPMProvider('jira'); + if (!m) throw new Error('jiraManifest was not registered'); + manifest = m; +}); + +describe('jiraManifest — identity', () => { + it("id is 'jira'", () => { + expect(manifest.id).toBe('jira'); + }); + + it("category is 'pm'", () => { + expect(manifest.category).toBe('pm'); + }); + + it("webhookRoute is '/jira/webhook'", () => { + expect(manifest.webhookRoute).toBe('/jira/webhook'); + }); +}); + +describe('jiraManifest — credentialRoles', () => { + it('exposes email + api_token (required) and webhook_secret (optional)', () => { + const byRole = Object.fromEntries(manifest.credentialRoles.map((r) => [r.role, r])); + expect(byRole.email).toMatchObject({ role: 'email', envVarKey: 'JIRA_EMAIL' }); + expect(byRole.email.optional).toBeFalsy(); + expect(byRole.api_token).toMatchObject({ role: 'api_token', envVarKey: 'JIRA_API_TOKEN' }); + expect(byRole.api_token.optional).toBeFalsy(); + expect(byRole.webhook_secret).toMatchObject({ + role: 'webhook_secret', + envVarKey: 'JIRA_WEBHOOK_SECRET', + optional: true, + }); + }); + + it("does NOT include base_url as a credential role (it's an integration-config field)", () => { + expect(manifest.credentialRoles.find((r) => r.role === 'base_url')).toBeUndefined(); + }); +}); + +describe('jiraManifest — verifyWebhookSignature', () => { + const RAW_BODY = '{"webhookEvent":"jira:issue_updated","issue":{"key":"PROJ-1"}}'; + const SECRET = 'jira-webhook-secret'; + + function validSignature(body: string, secret: string): string { + return createHmac('sha256', secret).update(body, 'utf8').digest('hex'); + } + + it("accepts a valid signature of the form 'sha256='", () => { + const sig = `sha256=${validSignature(RAW_BODY, SECRET)}`; + expect(manifest.verifyWebhookSignature(RAW_BODY, { 'x-hub-signature': sig }, SECRET)).toBe( + true, + ); + }); + + it('rejects a tampered body', () => { + const sig = `sha256=${validSignature(RAW_BODY, SECRET)}`; + expect( + manifest.verifyWebhookSignature(`${RAW_BODY}tampered`, { 'x-hub-signature': sig }, SECRET), + ).toBe(false); + }); + + it('rejects when the x-hub-signature header is missing', () => { + expect(manifest.verifyWebhookSignature(RAW_BODY, {}, SECRET)).toBe(false); + }); + + it('returns true (opt-out) when secret is null', () => { + expect(manifest.verifyWebhookSignature(RAW_BODY, {}, null)).toBe(true); + }); +}); + +describe('jiraManifest — extractProjectIdFromJob', () => { + it("returns projectId for { type: 'jira', projectId }", async () => { + const job = { type: 'jira', projectId: 'proj-1' } as unknown as CascadeJob; + expect(await manifest.extractProjectIdFromJob(job)).toBe('proj-1'); + }); + + it('returns null for a foreign job type', async () => { + const job = { type: 'github', projectId: 'proj-1' } as unknown as CascadeJob; + expect(await manifest.extractProjectIdFromJob(job)).toBeNull(); + }); + + it('returns null for a JIRA job missing projectId', async () => { + const job = { type: 'jira' } as unknown as CascadeJob; + expect(await manifest.extractProjectIdFromJob(job)).toBeNull(); + }); +}); + +describe('jiraManifest — wiring', () => { + it('platformClientFactory returns an object with postComment + deleteComment', () => { + const client = manifest.platformClientFactory('proj-1'); + expect(typeof client.postComment).toBe('function'); + expect(typeof client.deleteComment).toBe('function'); + }); + + it('routerAdapter.type is jira', () => { + expect(manifest.routerAdapter.type).toBe('jira'); + }); + + it('pmIntegration.type is jira', () => { + expect(manifest.pmIntegration.type).toBe('jira'); + }); + + it('triggerHandlers includes all jira built-in handlers', () => { + const names = manifest.triggerHandlers.map((h) => h.name); + expect(names).toEqual( + expect.arrayContaining([ + 'jira-comment-mention', + 'jira-status-changed', + 'jira-ready-to-process-label-added', + ]), + ); + }); +}); diff --git a/tests/unit/router/container-manager.test.ts b/tests/unit/router/container-manager.test.ts index c847ec75..70fd9956 100644 --- a/tests/unit/router/container-manager.test.ts +++ b/tests/unit/router/container-manager.test.ts @@ -84,10 +84,11 @@ vi.mock('../../../src/router/config.js', () => ({ // --------------------------------------------------------------------------- import { findProjectByRepo, getAllProjectCredentials } from '../../../src/config/provider.js'; -// Trello is resolved via the PM provider manifest as of plan 006/2. Import -// the trello barrel so the registration side effect runs before the -// extractProjectIdFromJob assertions execute. +// Trello (006/2) and JIRA (006/3) are resolved via the PM provider +// manifest registry. Side-effect imports register the manifests before +// the extractProjectIdFromJob assertions execute. import '../../../src/integrations/pm/trello/index.js'; +import '../../../src/integrations/pm/jira/index.js'; import { buildWorkerEnv, cleanupWorker, diff --git a/tests/unit/router/worker-env.test.ts b/tests/unit/router/worker-env.test.ts index 6728771a..41550b00 100644 --- a/tests/unit/router/worker-env.test.ts +++ b/tests/unit/router/worker-env.test.ts @@ -41,9 +41,10 @@ vi.mock('../../../src/router/config.js', () => ({ // --------------------------------------------------------------------------- import { findProjectByRepo, getAllProjectCredentials } from '../../../src/config/provider.js'; -// Trello is resolved through the PM provider manifest registry as of -// plan 006/2. Side-effect import registers the manifest. +// Trello (006/2) and JIRA (006/3) resolve through the PM provider manifest +// registry. Side-effect imports register the manifests. import '../../../src/integrations/pm/trello/index.js'; +import '../../../src/integrations/pm/jira/index.js'; import type { CascadeJob } from '../../../src/router/queue.js'; import { buildWorkerEnv, diff --git a/tests/unit/triggers/builtins.test.ts b/tests/unit/triggers/builtins.test.ts index ef9dad05..72581e30 100644 --- a/tests/unit/triggers/builtins.test.ts +++ b/tests/unit/triggers/builtins.test.ts @@ -76,10 +76,10 @@ vi.mock('../../../src/triggers/linear/label-added.js', () => ({ .mockImplementation(() => ({ name: 'linear-ready-to-process-label-added' })), })); -// After plan 006/2, Trello's triggers are contributed to registerBuiltInTriggers -// via the PM provider manifest registry. Mock listPMProviders() to return a -// stub Trello manifest whose triggerHandlers preserve the exact names + -// ordering the rest of this test file asserts on. +// After plan 006/2 and 006/3, Trello and JIRA triggers are contributed to +// registerBuiltInTriggers via the PM provider manifest registry. Mock +// listPMProviders() to return stub manifests whose triggerHandlers +// preserve the exact names the rest of this test file asserts on. vi.mock('../../../src/integrations/pm/registry.js', () => ({ listPMProviders: () => [ { @@ -94,6 +94,14 @@ vi.mock('../../../src/integrations/pm/registry.js', () => ({ { name: 'ready-to-process-label' }, ], }, + { + id: 'jira', + triggerHandlers: [ + { name: 'jira-comment-mention' }, + { name: 'jira-status-changed' }, + { name: 'jira-label-added' }, + ], + }, ], })); diff --git a/web/src/components/projects/pm-providers/jira/adapters.tsx b/web/src/components/projects/pm-providers/jira/adapters.tsx new file mode 100644 index 00000000..6eee6b39 --- /dev/null +++ b/web/src/components/projects/pm-providers/jira/adapters.tsx @@ -0,0 +1,60 @@ +/** + * Step-component adapters for JIRA. + * + * Bridges the generic renderer's `{ state, dispatch, providerHooks }` + * props into the existing JIRA step components' per-provider prop + * shapes. The step implementations stay unchanged; only the wrapping + * signature is adapted. + */ + +import type { UseMutationResult } from '@tanstack/react-query'; +import { + JiraCredentialsStep, + JiraFieldMappingStep, + JiraProjectStep, +} from '../../pm-wizard-jira-steps.js'; +import type { ProviderWizardStepProps } from '../types.js'; + +export interface JiraProviderHooks { + readonly onProjectSelect: (key: string) => void; + readonly jiraProjectsMutation: UseMutationResult; + readonly jiraDetailsMutation: UseMutationResult; + readonly onCreateCostField: () => void; + readonly creatingCostField: boolean; +} + +function asJiraHooks(providerHooks: Record | undefined): JiraProviderHooks { + return (providerHooks ?? {}) as unknown as JiraProviderHooks; +} + +export function JiraCredentialsStepAdapter({ state, dispatch }: ProviderWizardStepProps) { + return ; +} + +export function JiraProjectStepAdapter({ state, providerHooks }: ProviderWizardStepProps) { + const h = asJiraHooks(providerHooks); + return ( + + ); +} + +export function JiraFieldMappingStepAdapter({ + state, + dispatch, + providerHooks, +}: ProviderWizardStepProps) { + const h = asJiraHooks(providerHooks); + return ( + + ); +} diff --git a/web/src/components/projects/pm-providers/jira/index.ts b/web/src/components/projects/pm-providers/jira/index.ts new file mode 100644 index 00000000..9f510d22 --- /dev/null +++ b/web/src/components/projects/pm-providers/jira/index.ts @@ -0,0 +1,10 @@ +/** + * JIRA frontend wizard — side-effect registration. + */ + +import { registerProviderWizard } from '../registry.js'; +import { jiraProviderWizard } from './wizard.js'; + +registerProviderWizard(jiraProviderWizard); + +export { jiraProviderWizard }; diff --git a/web/src/components/projects/pm-providers/jira/wizard.ts b/web/src/components/projects/pm-providers/jira/wizard.ts new file mode 100644 index 00000000..ced2385b --- /dev/null +++ b/web/src/components/projects/pm-providers/jira/wizard.ts @@ -0,0 +1,99 @@ +/** + * JIRA ProviderWizardDefinition. + * + * `useProviderHooks` composes the existing JIRA hooks: + * `useJiraDiscovery` (project list + project details) and + * `useJiraCustomFieldCreation` (the "Create Cost field" button). + * + * `buildIntegrationConfig` mirrors the inline JIRA save body in + * `useSaveMutation`. Plan 006/5 will consolidate the save path onto + * the manifest's builder. + */ + +import { useState } from 'react'; +import { useJiraCustomFieldCreation, useJiraDiscovery } from '../../pm-wizard-hooks.js'; +import type { ProviderWizardDefinition } from '../types.js'; +import { + JiraCredentialsStepAdapter, + JiraFieldMappingStepAdapter, + JiraProjectStepAdapter, +} from './adapters.js'; + +function isCredentialsComplete(state: { + jiraEmail: string; + jiraApiToken: string; + jiraBaseUrl: string; + verificationResult: unknown; + isEditing: boolean; + hasStoredCredentials: boolean; +}): boolean { + if (state.isEditing && state.hasStoredCredentials) return true; + return Boolean( + state.jiraEmail && state.jiraApiToken && state.jiraBaseUrl && state.verificationResult, + ); +} + +export const jiraProviderWizard: ProviderWizardDefinition = { + id: 'jira', + label: 'JIRA', + + steps: [ + { + id: 'credentials', + title: 'JIRA credentials', + Component: JiraCredentialsStepAdapter, + isComplete: isCredentialsComplete, + }, + { + id: 'project', + title: 'Project', + Component: JiraProjectStepAdapter, + isComplete: (state) => Boolean(state.jiraProjectKey), + }, + { + id: 'fields', + title: 'Field mappings', + Component: JiraFieldMappingStepAdapter, + isComplete: (state) => Object.keys(state.jiraStatusMappings).length > 0, + }, + ], + + // Shape mirrors the existing inline save body in `useSaveMutation`. + // Plan 006/5 will consolidate the save path onto this builder. + buildIntegrationConfig: (state) => ({ + projectKey: state.jiraProjectKey, + baseUrl: state.jiraBaseUrl, + statuses: state.jiraStatusMappings, + ...(Object.keys(state.jiraIssueTypes).length > 0 ? { issueTypes: state.jiraIssueTypes } : {}), + ...(Object.keys(state.jiraLabels).length > 0 ? { labels: state.jiraLabels } : {}), + ...(state.jiraCostFieldId ? { customFields: { cost: state.jiraCostFieldId } } : {}), + }), + + isSetupComplete: (state) => { + if (!state.jiraProjectKey) return false; + if (Object.keys(state.jiraStatusMappings).length === 0) return false; + return isCredentialsComplete(state); + }, + + useProviderHooks: ({ state, dispatch, projectId, advanceToStep }) => { + const discovery = useJiraDiscovery(state, dispatch, advanceToStep, projectId ?? ''); + const customField = useJiraCustomFieldCreation(state, dispatch); + + const [creatingCostField, setCreatingCostField] = useState(false); + + const onCreateCostField = () => { + setCreatingCostField(true); + customField.createJiraCustomFieldMutation.mutate(undefined, { + onSettled: () => setCreatingCostField(false), + }); + }; + + return { + onProjectSelect: discovery.handleProjectSelect, + jiraProjectsMutation: discovery.jiraProjectsMutation, + jiraDetailsMutation: discovery.jiraDetailsMutation, + onCreateCostField, + creatingCostField, + }; + }, +}; diff --git a/web/src/components/projects/pm-wizard.tsx b/web/src/components/projects/pm-wizard.tsx index efe2dbe7..2e8d688c 100644 --- a/web/src/components/projects/pm-wizard.tsx +++ b/web/src/components/projects/pm-wizard.tsx @@ -3,16 +3,15 @@ import { CheckCircle, Globe, Loader2, XCircle } from 'lucide-react'; import { useEffect, useReducer, useRef, useState } from 'react'; import { Label } from '@/components/ui/label.js'; import { trpc } from '@/lib/trpc.js'; -// Side-effect import registers Trello's frontend wizard into the provider -// registry. Plans 006/3 and 006/4 will append jira + linear. +// Side-effect imports register Trello (006/2) + JIRA (006/3) frontend +// wizards into the provider registry. Plan 006/4 will append linear. import './pm-providers/trello/index.js'; +import './pm-providers/jira/index.js'; import { ManifestProviderWizardSection } from './pm-providers/manifest-section.js'; import { getProviderWizard } from './pm-providers/registry.js'; import { renderManifestStep } from './pm-providers/render.js'; import { SaveStep, WebhookStep } from './pm-wizard-common-steps.js'; import { - useJiraCustomFieldCreation, - useJiraDiscovery, useLinearDiscovery, useLinearLabelCreation, useLinearWebhookInfo, @@ -20,11 +19,10 @@ import { useVerification, useWebhookManagement, } from './pm-wizard-hooks.js'; -import { - JiraCredentialsStep, - JiraFieldMappingStep, - JiraProjectStep, -} from './pm-wizard-jira-steps.js'; +// JIRA legacy step imports removed — all JIRA wizard rendering flows +// through the manifest path (see ./pm-providers/jira/). The +// `pm-wizard-jira-steps` module is still imported transitively by the +// adapters in `./pm-providers/jira/adapters.tsx`. import { LINEAR_LABEL_DEFAULTS, LinearCredentialsStep, @@ -98,7 +96,8 @@ export function PMWizard({ const [creatingSlot, setCreatingSlot] = useState(null); // Trello's creatingCostField was migrated into the provider wizard's own // useProviderHooks; the parent no longer owns it. - const [creatingJiraCostField, setCreatingJiraCostField] = useState(false); + // JIRA's creatingJiraCostField migrated into the provider wizard's + // useProviderHooks (plan 006/3). // ---- Step navigation helpers ---- @@ -144,22 +143,15 @@ export function PMWizard({ const manifestDef = getProviderWizard(state.provider); const { verifyMutation } = useVerification(state, dispatch, advanceToStep); - // Trello's discovery / label / custom-field hooks are now composed inside - // trelloProviderWizard.useProviderHooks (plan 006/2). JIRA and Linear - // follow the same pattern in plans 006/3 and 006/4. - const { jiraProjectsMutation, jiraDetailsMutation, handleProjectSelect } = useJiraDiscovery( - state, - dispatch, - advanceToStep, - projectId, - ); + // Trello (006/2) and JIRA (006/3) discovery / label / custom-field hooks + // are composed inside each provider's useProviderHooks. Linear migrates + // in plan 006/4. const { linearTeamsMutation, linearDetailsMutation, linearProjectsMutation, handleTeamSelect } = useLinearDiscovery(state, dispatch, advanceToStep, projectId); const { createLabelMutation: createLinearLabelMutation, createMissingLabelsMutation: createMissingLinearLabelsMutation, } = useLinearLabelCreation(state, dispatch); - const { createJiraCustomFieldMutation } = useJiraCustomFieldCreation(state, dispatch); const webhookManagement = useWebhookManagement(projectId, state); const { webhookUrl: linearWebhookUrl } = useLinearWebhookInfo(); const { saveMutation } = useSaveMutation(projectId, state); @@ -169,14 +161,8 @@ export function PMWizard({ ); // ---- Label creation handlers ---- - // Trello handlers moved into trelloProviderWizard.useProviderHooks (006/2). - - const handleCreateJiraCostField = () => { - setCreatingJiraCostField(true); - createJiraCustomFieldMutation.mutate(undefined, { - onSettled: () => setCreatingJiraCostField(false), - }); - }; + // Trello (006/2) and JIRA (006/3) handlers migrated into their provider + // wizards' useProviderHooks. Linear follows in 006/4. const handleCreateLinearLabel = (slot: string) => { const defaults = LINEAR_LABEL_DEFAULTS[slot]; @@ -277,10 +263,8 @@ export function PMWizard({ advanceToStep={advanceToStep} stepIndex={0} /> - ) : state.provider === 'linear' ? ( - ) : ( - + )}
@@ -331,7 +315,7 @@ export function PMWizard({ advanceToStep={advanceToStep} stepIndex={1} /> - ) : state.provider === 'linear' ? ( + ) : ( - ) : ( - )} @@ -367,7 +344,7 @@ export function PMWizard({ advanceToStep={advanceToStep} stepIndex={2} /> - ) : state.provider === 'linear' ? ( + ) : ( - ) : ( - )}