mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(jsm): improve create request error handling, add form-based submission support (#4066)
* fix(jsm): improve create request error handling, add form-based submission support * refactor(jsm): extract parseJsmErrorMessage helper to deduplicate error handling * fix(jsm): remove required on summary for advanced mode, add JSON.parse error handling * fix(jsm): include description in requestFieldValues gate for form-only requests
This commit is contained in:
@@ -113,10 +113,11 @@ Create a new service request in Jira Service Management
|
||||
| `cloudId` | string | No | Jira Cloud ID for the instance |
|
||||
| `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) |
|
||||
| `requestTypeId` | string | Yes | Request Type ID \(e.g., "10", "15"\) |
|
||||
| `summary` | string | Yes | Summary/title for the service request |
|
||||
| `summary` | string | No | Summary/title for the service request \(required unless using Form Answers\) |
|
||||
| `description` | string | No | Description for the service request |
|
||||
| `raiseOnBehalfOf` | string | No | Account ID of customer to raise request on behalf of |
|
||||
| `requestFieldValues` | json | No | Request field values as key-value pairs \(overrides summary/description if provided\) |
|
||||
| `formAnswers` | json | No | Form answers for form-based request types \(e.g., \{"summary": \{"text": "Title"\}, "customfield_10010": \{"choices": \["10320"\]\}\}\) |
|
||||
| `requestParticipants` | string | No | Comma-separated account IDs to add as request participants |
|
||||
| `channel` | string | No | Channel the request originates from \(e.g., portal, email\) |
|
||||
|
||||
|
||||
@@ -12,6 +12,20 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestAPI')
|
||||
|
||||
function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string {
|
||||
try {
|
||||
const errorData = JSON.parse(errorText)
|
||||
if (errorData.errorMessage) {
|
||||
return `JSM API error: ${errorData.errorMessage}`
|
||||
}
|
||||
} catch {
|
||||
if (errorText) {
|
||||
return `JSM API error: ${errorText}`
|
||||
}
|
||||
}
|
||||
return `JSM API error: ${status} ${statusText}`
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
@@ -31,6 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
description,
|
||||
raiseOnBehalfOf,
|
||||
requestFieldValues,
|
||||
formAnswers,
|
||||
requestParticipants,
|
||||
channel,
|
||||
expand,
|
||||
@@ -55,7 +70,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const isCreateOperation = serviceDeskId && requestTypeId && summary
|
||||
const isCreateOperation = serviceDeskId && requestTypeId && (summary || formAnswers)
|
||||
|
||||
if (isCreateOperation) {
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
@@ -69,15 +84,30 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
const url = `${baseUrl}/request`
|
||||
|
||||
logger.info('Creating request at:', url)
|
||||
logger.info('Creating request at:', { url, serviceDeskId, requestTypeId })
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
requestFieldValues: requestFieldValues || {
|
||||
summary,
|
||||
...(description && { description }),
|
||||
},
|
||||
}
|
||||
|
||||
if (summary || description || requestFieldValues) {
|
||||
const fieldValues =
|
||||
requestFieldValues && typeof requestFieldValues === 'object'
|
||||
? {
|
||||
...(!requestFieldValues.summary && summary ? { summary } : {}),
|
||||
...(!requestFieldValues.description && description ? { description } : {}),
|
||||
...requestFieldValues,
|
||||
}
|
||||
: {
|
||||
...(summary && { summary }),
|
||||
...(description && { description }),
|
||||
}
|
||||
requestBody.requestFieldValues = fieldValues
|
||||
}
|
||||
|
||||
if (formAnswers && typeof formAnswers === 'object') {
|
||||
requestBody.form = { answers: formAnswers }
|
||||
}
|
||||
|
||||
if (raiseOnBehalfOf) {
|
||||
@@ -112,7 +142,10 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{
|
||||
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
@@ -178,7 +211,10 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{
|
||||
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
|
||||
details: errorText,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -198,7 +198,6 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
|
||||
id: 'summary',
|
||||
title: 'Summary',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
placeholder: 'Enter request summary',
|
||||
condition: { field: 'operation', value: 'create_request' },
|
||||
wandConfig: {
|
||||
@@ -238,6 +237,7 @@ Return ONLY the description text - no explanations.`,
|
||||
title: 'Raise on Behalf Of',
|
||||
type: 'short-input',
|
||||
placeholder: 'Account ID to raise request on behalf of',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_request' },
|
||||
},
|
||||
{
|
||||
@@ -245,6 +245,7 @@ Return ONLY the description text - no explanations.`,
|
||||
title: 'Request Participants',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated account IDs to add as participants',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_request' },
|
||||
},
|
||||
{
|
||||
@@ -252,6 +253,7 @@ Return ONLY the description text - no explanations.`,
|
||||
title: 'Channel',
|
||||
type: 'short-input',
|
||||
placeholder: 'Channel (e.g., portal, email)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_request' },
|
||||
},
|
||||
{
|
||||
@@ -260,6 +262,16 @@ Return ONLY the description text - no explanations.`,
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON object of field values (e.g., {"summary": "Title", "customfield_10010": "value"})',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_request' },
|
||||
},
|
||||
{
|
||||
id: 'formAnswers',
|
||||
title: 'Form Answers',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON object for form-based request types (e.g., {"summary": {"text": "Title"}, "customfield_10010": {"choices": ["10320"]}})',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_request' },
|
||||
},
|
||||
{
|
||||
@@ -571,8 +583,8 @@ Return ONLY the comment text - no explanations.`,
|
||||
if (!params.requestTypeId) {
|
||||
throw new Error('Request Type ID is required')
|
||||
}
|
||||
if (!params.summary) {
|
||||
throw new Error('Summary is required')
|
||||
if (!params.summary && !params.formAnswers) {
|
||||
throw new Error('Summary is required (unless using Form Answers)')
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
@@ -584,7 +596,22 @@ Return ONLY the comment text - no explanations.`,
|
||||
requestParticipants: params.requestParticipants,
|
||||
channel: params.channel,
|
||||
requestFieldValues: params.requestFieldValues
|
||||
? JSON.parse(params.requestFieldValues)
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(params.requestFieldValues)
|
||||
} catch {
|
||||
throw new Error('requestFieldValues must be valid JSON')
|
||||
}
|
||||
})()
|
||||
: undefined,
|
||||
formAnswers: params.formAnswers
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(params.formAnswers)
|
||||
} catch {
|
||||
throw new Error('formAnswers must be valid JSON')
|
||||
}
|
||||
})()
|
||||
: undefined,
|
||||
}
|
||||
case 'get_request':
|
||||
@@ -826,6 +853,10 @@ Return ONLY the comment text - no explanations.`,
|
||||
},
|
||||
channel: { type: 'string', description: 'Channel (e.g., portal, email)' },
|
||||
requestFieldValues: { type: 'string', description: 'JSON object of request field values' },
|
||||
formAnswers: {
|
||||
type: 'string',
|
||||
description: 'JSON object of form answers for form-based request types',
|
||||
},
|
||||
searchQuery: { type: 'string', description: 'Filter request types by name' },
|
||||
groupId: { type: 'string', description: 'Filter by request type group ID' },
|
||||
expand: { type: 'string', description: 'Comma-separated fields to expand' },
|
||||
|
||||
@@ -39,6 +39,12 @@ export interface ErrorExtractorConfig {
|
||||
}
|
||||
|
||||
const ERROR_EXTRACTORS: ErrorExtractorConfig[] = [
|
||||
{
|
||||
id: 'atlassian-errors',
|
||||
description: 'Atlassian REST API errorMessage field',
|
||||
examples: ['Jira', 'Jira Service Management', 'Confluence'],
|
||||
extract: (errorInfo) => errorInfo?.data?.errorMessage,
|
||||
},
|
||||
{
|
||||
id: 'graphql-errors',
|
||||
description: 'GraphQL errors array with message field',
|
||||
@@ -221,6 +227,7 @@ export function extractErrorMessage(errorInfo?: ErrorInfo, extractorId?: string)
|
||||
}
|
||||
|
||||
export const ErrorExtractorId = {
|
||||
ATLASSIAN_ERRORS: 'atlassian-errors',
|
||||
GRAPHQL_ERRORS: 'graphql-errors',
|
||||
TWITTER_ERRORS: 'twitter-errors',
|
||||
DETAILS_ARRAY: 'details-array',
|
||||
|
||||
@@ -45,9 +45,9 @@ export const jsmCreateRequestTool: ToolConfig<JsmCreateRequestParams, JsmCreateR
|
||||
},
|
||||
summary: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Summary/title for the service request',
|
||||
description: 'Summary/title for the service request (required unless using Form Answers)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
@@ -68,6 +68,13 @@ export const jsmCreateRequestTool: ToolConfig<JsmCreateRequestParams, JsmCreateR
|
||||
description:
|
||||
'Request field values as key-value pairs (overrides summary/description if provided)',
|
||||
},
|
||||
formAnswers: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Form answers for form-based request types (e.g., {"summary": {"text": "Title"}, "customfield_10010": {"choices": ["10320"]}})',
|
||||
},
|
||||
requestParticipants: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -98,6 +105,7 @@ export const jsmCreateRequestTool: ToolConfig<JsmCreateRequestParams, JsmCreateR
|
||||
description: params.description,
|
||||
raiseOnBehalfOf: params.raiseOnBehalfOf,
|
||||
requestFieldValues: params.requestFieldValues,
|
||||
formAnswers: params.formAnswers,
|
||||
requestParticipants: params.requestParticipants,
|
||||
channel: params.channel,
|
||||
}),
|
||||
|
||||
@@ -411,9 +411,10 @@ export interface JsmGetRequestTypesParams extends JsmBaseParams {
|
||||
export interface JsmCreateRequestParams extends JsmBaseParams {
|
||||
serviceDeskId: string
|
||||
requestTypeId: string
|
||||
summary: string
|
||||
summary?: string
|
||||
description?: string
|
||||
requestFieldValues?: Record<string, unknown>
|
||||
formAnswers?: Record<string, unknown>
|
||||
raiseOnBehalfOf?: string
|
||||
requestParticipants?: string[]
|
||||
channel?: string
|
||||
|
||||
Reference in New Issue
Block a user