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..0fc3b88e 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,10 +149,20 @@ export function JiraProjectStep({ export function JiraFieldMappingStep({ state, dispatch, + onCreateCostField, + creatingCostField, }: { state: WizardState; dispatch: React.Dispatch; + 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 */} @@ -267,7 +278,29 @@ export function JiraFieldMappingStep({ {/* Cost custom field */}
- +
+ + {showCreateCostButton && ( + + )} +
+

+ 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} /> ) : ( - + )}