From 0db20e627e3b8905178a1e3bdb185caecb5d30d8 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Fri, 13 Mar 2026 20:06:15 +0000 Subject: [PATCH 1/2] feat(wizard): add Create button for JIRA cost custom field --- .../components/projects/pm-wizard-hooks.ts | 40 +++++++++++++++++++ .../projects/pm-wizard-jira-steps.tsx | 31 +++++++++++++- .../components/projects/pm-wizard-state.ts | 12 +++++- web/src/components/projects/pm-wizard.tsx | 17 +++++++- 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/web/src/components/projects/pm-wizard-hooks.ts b/web/src/components/projects/pm-wizard-hooks.ts index 2d29ffe6..7b8ee9d1 100644 --- a/web/src/components/projects/pm-wizard-hooks.ts +++ b/web/src/components/projects/pm-wizard-hooks.ts @@ -444,6 +444,46 @@ export function useTrelloCustomFieldCreation( return { createCustomFieldMutation }; } +// ============================================================================ +// JIRA Custom Field Creation +// ============================================================================ + +export function useJiraCustomFieldCreation( + state: WizardState, + dispatch: React.Dispatch, +) { + const createJiraCustomFieldMutation = useMutation({ + mutationFn: () => { + if (!state.jiraEmailCredentialId || !state.jiraApiTokenCredentialId || !state.jiraBaseUrl) { + throw new Error('Missing JIRA credentials or base URL'); + } + return trpcClient.integrationsDiscovery.createJiraCustomField.mutate({ + emailCredentialId: state.jiraEmailCredentialId, + apiTokenCredentialId: state.jiraApiTokenCredentialId, + baseUrl: state.jiraBaseUrl, + name: 'Cost', + }); + }, + onSuccess: (field) => { + dispatch({ type: 'ADD_JIRA_PROJECT_CUSTOM_FIELD', field: { ...field, custom: true } }); + dispatch({ type: 'SET_JIRA_COST_FIELD', id: field.id }); + }, + onError: (error) => { + console.error('Failed to create JIRA custom field:', error); + const message = error instanceof Error ? error.message : String(error); + if (message.includes('403') || message.toLowerCase().includes('admin')) { + alert( + 'Failed to create custom field: JIRA admin permissions are required to create global custom fields. Please contact your JIRA administrator.', + ); + } else { + alert(`Failed to create JIRA custom field: ${message}`); + } + }, + }); + + return { createJiraCustomFieldMutation }; +} + // ============================================================================ // Save Mutation // ============================================================================ diff --git a/web/src/components/projects/pm-wizard-jira-steps.tsx b/web/src/components/projects/pm-wizard-jira-steps.tsx index bd09cfc6..923beb8c 100644 --- a/web/src/components/projects/pm-wizard-jira-steps.tsx +++ b/web/src/components/projects/pm-wizard-jira-steps.tsx @@ -1,10 +1,11 @@ /** * JIRA-specific step renderer components for PMWizard. */ +import { Button } from '@/components/ui/button.js'; import { Input } from '@/components/ui/input.js'; import { Label } from '@/components/ui/label.js'; import type { UseMutationResult } from '@tanstack/react-query'; -import { Loader2 } from 'lucide-react'; +import { Loader2, Plus } from 'lucide-react'; import type { WizardAction, WizardState } from './pm-wizard-state.js'; import { FieldMappingRow, InlineCredentialCreator, SearchableSelect } from './wizard-shared.js'; import type { CredentialOption } from './wizard-shared.js'; @@ -148,9 +149,13 @@ export function JiraProjectStep({ export function JiraFieldMappingStep({ state, dispatch, + onCreateCostField, + creatingCostField, }: { state: WizardState; dispatch: React.Dispatch; + onCreateCostField?: () => void; + creatingCostField?: boolean; }) { return (
@@ -267,7 +272,29 @@ export function JiraFieldMappingStep({ {/* Cost custom field */}
- +
+ + {state.jiraProjectDetails && onCreateCostField && !state.jiraCostFieldId && ( + + )} +
+

+ JIRA custom fields are global and require admin permissions to create. +

{state.jiraProjectDetails ? ( = (state, action) customFields: [...state.trelloBoardDetails.customFields, action.customField], }, }; + case 'ADD_JIRA_PROJECT_CUSTOM_FIELD': + if (!state.jiraProjectDetails) return state; + return { + ...state, + jiraProjectDetails: { + ...state.jiraProjectDetails, + fields: [...state.jiraProjectDetails.fields, action.field], + }, + }; default: return state; } diff --git a/web/src/components/projects/pm-wizard.tsx b/web/src/components/projects/pm-wizard.tsx index 8a5e7512..3b66b46e 100644 --- a/web/src/components/projects/pm-wizard.tsx +++ b/web/src/components/projects/pm-wizard.tsx @@ -5,6 +5,7 @@ import { CheckCircle, Globe, Loader2, XCircle } from 'lucide-react'; import { useEffect, useReducer, useState } from 'react'; import { SaveStep, WebhookStep } from './pm-wizard-common-steps.js'; import { + useJiraCustomFieldCreation, useJiraDiscovery, useSaveMutation, useTrelloCustomFieldCreation, @@ -73,6 +74,7 @@ export function PMWizard({ const [openSteps, setOpenSteps] = useState>(new Set([1])); const [creatingSlot, setCreatingSlot] = useState(null); const [creatingCostField, setCreatingCostField] = useState(false); + const [creatingJiraCostField, setCreatingJiraCostField] = useState(false); // ---- Step navigation helpers ---- @@ -123,6 +125,7 @@ export function PMWizard({ dispatch, ); const { createCustomFieldMutation } = useTrelloCustomFieldCreation(state, dispatch); + const { createJiraCustomFieldMutation } = useJiraCustomFieldCreation(state, dispatch); const webhookManagement = useWebhookManagement(projectId, state); const { saveMutation } = useSaveMutation(projectId, state); @@ -147,6 +150,13 @@ export function PMWizard({ }); }; + const handleCreateJiraCostField = () => { + setCreatingJiraCostField(true); + createJiraCustomFieldMutation.mutate(undefined, { + onSettled: () => setCreatingJiraCostField(false), + }); + }; + const handleCreateAllMissingLabels = () => { const existingLabelNames = new Set( (state.trelloBoardDetails?.labels ?? []).map((l) => l.name.toLowerCase()), @@ -320,7 +330,12 @@ export function PMWizard({ creatingCostField={creatingCostField} /> ) : ( - + )} From a008923c01d3bff5d3dfe1f082885d4b78326e18 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Fri, 13 Mar 2026 20:18:30 +0000 Subject: [PATCH 2/2] fix(wizard): add duplicate-field guard to JIRA cost field Create button Check whether a "Cost" field already exists in jiraProjectDetails.fields before showing the Create button, mirroring the Trello equivalent guard. This prevents creating duplicate global JIRA custom fields when a Cost field already exists but hasn't been selected yet. Co-Authored-By: Claude Opus 4.6 --- web/src/components/projects/pm-wizard-jira-steps.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/components/projects/pm-wizard-jira-steps.tsx b/web/src/components/projects/pm-wizard-jira-steps.tsx index 923beb8c..0fc3b88e 100644 --- a/web/src/components/projects/pm-wizard-jira-steps.tsx +++ b/web/src/components/projects/pm-wizard-jira-steps.tsx @@ -157,6 +157,12 @@ export function JiraFieldMappingStep({ onCreateCostField?: () => void; creatingCostField?: boolean; }) { + const existingCostField = state.jiraProjectDetails?.fields.some( + (f) => f.name.toLowerCase() === 'cost', + ); + const showCreateCostButton = + state.jiraProjectDetails && onCreateCostField && !state.jiraCostFieldId && !existingCostField; + return (
{/* Status mappings */} @@ -274,7 +280,7 @@ export function JiraFieldMappingStep({
- {state.jiraProjectDetails && onCreateCostField && !state.jiraCostFieldId && ( + {showCreateCostButton && (