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
80 changes: 80 additions & 0 deletions apps/docs/content/docs/en/tools/jira_service_management.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -678,4 +678,84 @@ Get the fields required to create a request of a specific type in Jira Service M
| ↳ `defaultValues` | json | Default values for the field |
| ↳ `jiraSchema` | json | Jira field schema with type, system, custom, customId |

### `jsm_get_form_templates`

List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for request types

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `projectIdOrKey` | string | Project ID or key |
| `templates` | array | List of forms in the project |
| ↳ `id` | string | Form template ID \(UUID\) |
| ↳ `name` | string | Form template name |
| ↳ `updated` | string | Last updated timestamp \(ISO 8601\) |
| ↳ `issueCreateIssueTypeIds` | json | Issue type IDs that auto-attach this form on issue create |
| ↳ `issueCreateRequestTypeIds` | json | Request type IDs that auto-attach this form on issue create |
| ↳ `portalRequestTypeIds` | json | Request type IDs that show this form on the customer portal |
| ↳ `recommendedIssueRequestTypeIds` | json | Request type IDs that recommend this form |
| `total` | number | Total number of forms |

### `jsm_get_form_structure`

Get the full structure of a ProForma/JSM form including all questions, field types, choices, layout, and conditions

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `projectIdOrKey` | string | Yes | Jira project ID or key \(e.g., "10001" or "SD"\) |
| `formId` | string | Yes | Form ID \(UUID from Get Form Templates\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `projectIdOrKey` | string | Project ID or key |
| `formId` | string | Form ID |
| `design` | json | Full form design with questions \(field types, labels, choices, validation\), layout \(field ordering\), and conditions |
| `updated` | string | Last updated timestamp |
| `publish` | json | Publishing and request type configuration |

### `jsm_get_issue_forms`

List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123", "10001"\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `forms` | array | List of forms attached to the issue |
| ↳ `id` | string | Form instance ID \(UUID\) |
| ↳ `name` | string | Form name |
| ↳ `updated` | string | Last updated timestamp \(ISO 8601\) |
| ↳ `submitted` | boolean | Whether the form has been submitted |
| ↳ `lock` | boolean | Whether the form is locked |
| ↳ `internal` | boolean | Whether the form is internal-only |
| ↳ `formTemplateId` | string | Source form template ID \(UUID\) |
| `total` | number | Total number of forms |


14 changes: 13 additions & 1 deletion apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -6614,9 +6614,21 @@
{
"name": "Get Request Type Fields",
"description": "Get the fields required to create a request of a specific type in Jira Service Management"
},
{
"name": "Get Form Templates",
"description": "List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for request types"
},
{
"name": "Get Form Structure",
"description": "Get the full structure of a ProForma/JSM form including all questions, field types, choices, layout, and conditions"
},
{
"name": "Get Issue Forms",
"description": "List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)"
}
],
"operationCount": 21,
"operationCount": 24,
"triggers": [],
"triggerCount": 0,
"authType": "oauth",
Expand Down
115 changes: 115 additions & 0 deletions apps/sim/app/api/tools/jsm/forms/issue/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import {
getJiraCloudId,
getJsmFormsApiBaseUrl,
getJsmHeaders,
parseJsmErrorMessage,
} from '@/tools/jsm/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('JsmIssueFormsAPI')

export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body

if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}

if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}

if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}

const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))

const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}

const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}

const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`

logger.info('Fetching issue forms from:', { url, issueIdOrKey })

const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})

if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})

return NextResponse.json(
{
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}

const data = await response.json()

const forms = Array.isArray(data) ? data : (data.values ?? data.forms ?? [])

return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
forms: forms.map((form: Record<string, unknown>) => ({
id: form.id ?? null,
name: form.name ?? null,
updated: form.updated ?? null,
submitted: form.submitted ?? false,
lock: form.lock ?? false,
internal: form.internal ?? null,
formTemplateId: (form.formTemplate as Record<string, unknown>)?.id ?? null,
})),
total: forms.length,
},
})
} catch (error) {
logger.error('Error fetching issue forms:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})

return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
117 changes: 117 additions & 0 deletions apps/sim/app/api/tools/jsm/forms/structure/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import {
getJiraCloudId,
getJsmFormsApiBaseUrl,
getJsmHeaders,
parseJsmErrorMessage,
} from '@/tools/jsm/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('JsmFormStructureAPI')

export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, projectIdOrKey, formId } = body

if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}

if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}

if (!projectIdOrKey) {
logger.error('Missing projectIdOrKey in request')
return NextResponse.json({ error: 'Project ID or key is required' }, { status: 400 })
}

if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}

const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))

const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}

const projectIdOrKeyValidation = validateJiraIssueKey(projectIdOrKey, 'projectIdOrKey')
if (!projectIdOrKeyValidation.isValid) {
return NextResponse.json({ error: projectIdOrKeyValidation.error }, { status: 400 })
}

const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}

const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form/${encodeURIComponent(formId)}`

logger.info('Fetching form template from:', { url, projectIdOrKey, formId })

const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})

if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})

return NextResponse.json(
{
error: parseJsmErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}

const data = await response.json()

return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
projectIdOrKey,
formId,
design: data.design ?? null,
updated: data.updated ?? null,
publish: data.publish ?? null,
},
})
} catch (error) {
logger.error('Error fetching form structure:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})

return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
Loading
Loading