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
231 changes: 231 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 @@ -758,4 +758,235 @@ List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, su
| ↳ `formTemplateId` | string | Source form template ID \(UUID\) |
| `total` | number | Total number of forms |

### `jsm_attach_form`

Attach a form template to an existing Jira issue or JSM request

#### 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 to attach the form to \(e.g., "SD-123"\) |
| `formTemplateId` | string | Yes | Form template UUID \(from Get Form Templates\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `id` | string | Attached form instance ID \(UUID\) |
| `name` | string | Form name |
| `updated` | string | Last updated timestamp |
| `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 | Form template ID |

### `jsm_save_form_answers`

Save answers to a form attached to a Jira issue or JSM request

#### 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"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
| `answers` | json | Yes | Form answers using numeric question IDs as keys \(e.g., \{"1": \{"text": "Title"\}, "4": \{"choices": \["5"\]\}\}\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `state` | json | Form state with status \(open, submitted, locked\) |
| `updated` | string | Last updated timestamp |

### `jsm_submit_form`

Submit a form on a Jira issue or JSM request, locking it from further edits

#### 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"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `status` | string | Form status after submission \(open, submitted, locked\) |

### `jsm_get_form`

Get a single form with full design, state, and answers from a Jira issue

#### 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"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `design` | json | Full form design with questions, layout, conditions, sections, settings |
| `state` | json | Form state with answers map, status \(o=open, s=submitted, l=locked\), visibility \(i=internal, e=external\) |
| `updated` | string | Last updated timestamp |

### `jsm_get_form_answers`

Get simplified answers from a form attached to a Jira issue or JSM request

#### 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"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `answers` | json | Simplified form answers as key-value pairs \(question label to answer text/choices\) |

### `jsm_reopen_form`

Reopen a submitted form on a Jira issue or JSM request, allowing further edits

#### 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"\) |
| `formId` | string | Yes | Form instance UUID \(from Get Issue Forms\) |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `status` | string | Form status after reopening \(open, submitted, locked\) |

### `jsm_delete_form`

Remove a form from a Jira issue or JSM request

#### 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"\) |
| `formId` | string | Yes | Form instance UUID to delete |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Deleted form instance UUID |
| `deleted` | boolean | Whether the form was successfully deleted |

### `jsm_externalise_form`

Make a form visible to customers on a Jira issue or JSM request

#### 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"\) |
| `formId` | string | Yes | Form instance UUID |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `visibility` | string | Form visibility after change \(internal or external\) |

### `jsm_internalise_form`

Make a form internal only (not visible to customers) on a Jira issue or JSM request

#### 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"\) |
| `formId` | string | Yes | Form instance UUID |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `visibility` | string | Form visibility after change \(internal or external\) |

### `jsm_copy_forms`

Copy forms from one Jira issue to another

#### 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 |
| `sourceIssueIdOrKey` | string | Yes | Source issue ID or key to copy forms from \(e.g., "SD-123"\) |
| `targetIssueIdOrKey` | string | Yes | Target issue ID or key to copy forms to \(e.g., "SD-456"\) |
| `formIds` | json | No | Optional JSON array of form UUIDs to copy \(e.g., \["uuid1", "uuid2"\]\). If omitted, copies all forms. |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `sourceIssueIdOrKey` | string | Source issue ID or key |
| `targetIssueIdOrKey` | string | Target issue ID or key |
| `copiedForms` | json | Array of successfully copied forms |
| `errors` | json | Array of errors encountered during copy |


42 changes: 41 additions & 1 deletion apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -6868,9 +6868,49 @@
{
"name": "Get Issue Forms",
"description": "List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)"
},
{
"name": "Attach Form",
"description": "Attach a form template to an existing Jira issue or JSM request"
},
{
"name": "Save Form Answers",
"description": "Save answers to a form attached to a Jira issue or JSM request"
},
{
"name": "Submit Form",
"description": "Submit a form on a Jira issue or JSM request, locking it from further edits"
},
{
"name": "Get Form",
"description": "Get a single form with full design, state, and answers from a Jira issue"
},
{
"name": "Get Form Answers",
"description": "Get simplified answers from a form attached to a Jira issue or JSM request"
},
{
"name": "Reopen Form",
"description": "Reopen a submitted form on a Jira issue or JSM request, allowing further edits"
},
{
"name": "Delete Form",
"description": "Remove a form from a Jira issue or JSM request"
},
{
"name": "Externalise Form",
"description": "Make a form visible to customers on a Jira issue or JSM request"
},
{
"name": "Internalise Form",
"description": "Make a form internal only (not visible to customers) on a Jira issue or JSM request"
},
{
"name": "Copy Forms",
"description": "Copy forms from one Jira issue to another"
}
],
"operationCount": 24,
"operationCount": 34,
"triggers": [],
"triggerCount": 0,
"authType": "oauth",
Expand Down
111 changes: 111 additions & 0 deletions apps/sim/app/api/tools/jsm/forms/answers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('JsmGetFormAnswersAPI')

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, 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 (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue 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 issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.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}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/format/answers`

logger.info('Getting form answers:', { url, issueIdOrKey, 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: parseAtlassianErrorMessage(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(),
issueIdOrKey,
formId,
answers: data ?? null,
},
})
} catch (error) {
logger.error('Error getting form answers:', {
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