Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions web/src/components/projects/pm-wizard-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,46 @@ export function useTrelloCustomFieldCreation(
return { createCustomFieldMutation };
}

// ============================================================================
// JIRA Custom Field Creation
// ============================================================================

export function useJiraCustomFieldCreation(
state: WizardState,
dispatch: React.Dispatch<WizardAction>,
) {
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
// ============================================================================
Expand Down
37 changes: 35 additions & 2 deletions web/src/components/projects/pm-wizard-jira-steps.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -148,10 +149,20 @@ export function JiraProjectStep({
export function JiraFieldMappingStep({
state,
dispatch,
onCreateCostField,
creatingCostField,
}: {
state: WizardState;
dispatch: React.Dispatch<WizardAction>;
onCreateCostField?: () => void;
creatingCostField?: boolean;
}) {
const existingCostField = state.jiraProjectDetails?.fields.some(
(f) => f.name.toLowerCase() === 'cost',
);
const showCreateCostButton =
state.jiraProjectDetails && onCreateCostField && !state.jiraCostFieldId && !existingCostField;

return (
<div className="space-y-6">
{/* Status mappings */}
Expand Down Expand Up @@ -267,7 +278,29 @@ export function JiraFieldMappingStep({

{/* Cost custom field */}
<div className="space-y-2">
<Label>Custom Field: Cost</Label>
<div className="flex items-center justify-between">
<Label>Custom Field: Cost</Label>
{showCreateCostButton && (
<Button
type="button"
variant="outline"
size="sm"
onClick={onCreateCostField}
disabled={creatingCostField}
className="h-7 text-xs"
>
{creatingCostField ? (
<Loader2 className="h-3 w-3 animate-spin mr-1" />
) : (
<Plus className="h-3 w-3 mr-1" />
)}
Create
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
JIRA custom fields are global and require admin permissions to create.
</p>
{state.jiraProjectDetails ? (
<FieldMappingRow
slotLabel="cost"
Expand Down
12 changes: 11 additions & 1 deletion web/src/components/projects/pm-wizard-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export type WizardAction =
| {
type: 'ADD_TRELLO_BOARD_CUSTOM_FIELD';
customField: { id: string; name: string; type: string };
};
}
| { type: 'ADD_JIRA_PROJECT_CUSTOM_FIELD'; field: { id: string; name: string; custom: boolean } };

// ============================================================================
// Initial state and constants
Expand Down Expand Up @@ -253,6 +254,15 @@ export const wizardReducer: Reducer<WizardState, WizardAction> = (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;
}
Expand Down
17 changes: 16 additions & 1 deletion web/src/components/projects/pm-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -73,6 +74,7 @@ export function PMWizard({
const [openSteps, setOpenSteps] = useState<Set<number>>(new Set([1]));
const [creatingSlot, setCreatingSlot] = useState<string | null>(null);
const [creatingCostField, setCreatingCostField] = useState(false);
const [creatingJiraCostField, setCreatingJiraCostField] = useState(false);

// ---- Step navigation helpers ----

Expand Down Expand Up @@ -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);

Expand All @@ -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()),
Expand Down Expand Up @@ -320,7 +330,12 @@ export function PMWizard({
creatingCostField={creatingCostField}
/>
) : (
<JiraFieldMappingStep state={state} dispatch={dispatch} />
<JiraFieldMappingStep
state={state}
dispatch={dispatch}
onCreateCostField={handleCreateJiraCostField}
creatingCostField={creatingJiraCostField}
/>
)}
</WizardStep>

Expand Down
Loading