Skip to content

Commit 6ce299b

Browse files
authored
feat(jsm): add all Forms API endpoints for jira (#4142)
* feat(jsm): add all Forms API endpoints for two-step form workflow * removed tyoes * fix(jsm): handle 204 No Content on action endpoints and reject array answers * fix(jsm): validate formIds is an array in copy_forms route and block * fix(jsm): add formTemplateId validation and conditional required on formAnswers
1 parent c75d7b9 commit 6ce299b

File tree

27 files changed

+2936
-30
lines changed

27 files changed

+2936
-30
lines changed

apps/docs/content/docs/en/tools/jira_service_management.mdx

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,4 +758,235 @@ List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, su
758758
|`formTemplateId` | string | Source form template ID \(UUID\) |
759759
| `total` | number | Total number of forms |
760760

761+
### `jsm_attach_form`
762+
763+
Attach a form template to an existing Jira issue or JSM request
764+
765+
#### Input
766+
767+
| Parameter | Type | Required | Description |
768+
| --------- | ---- | -------- | ----------- |
769+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
770+
| `cloudId` | string | No | Jira Cloud ID for the instance |
771+
| `issueIdOrKey` | string | Yes | Issue ID or key to attach the form to \(e.g., "SD-123"\) |
772+
| `formTemplateId` | string | Yes | Form template UUID \(from Get Form Templates\) |
773+
774+
#### Output
775+
776+
| Parameter | Type | Description |
777+
| --------- | ---- | ----------- |
778+
| `ts` | string | Timestamp of the operation |
779+
| `issueIdOrKey` | string | Issue ID or key |
780+
| `id` | string | Attached form instance ID \(UUID\) |
781+
| `name` | string | Form name |
782+
| `updated` | string | Last updated timestamp |
783+
| `submitted` | boolean | Whether the form has been submitted |
784+
| `lock` | boolean | Whether the form is locked |
785+
| `internal` | boolean | Whether the form is internal only |
786+
| `formTemplateId` | string | Form template ID |
787+
788+
### `jsm_save_form_answers`
789+
790+
Save answers to a form attached to a Jira issue or JSM request
791+
792+
#### Input
793+
794+
| Parameter | Type | Required | Description |
795+
| --------- | ---- | -------- | ----------- |
796+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
797+
| `cloudId` | string | No | Jira Cloud ID for the instance |
798+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
799+
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
800+
| `answers` | json | Yes | Form answers using numeric question IDs as keys \(e.g., \{"1": \{"text": "Title"\}, "4": \{"choices": \["5"\]\}\}\) |
801+
802+
#### Output
803+
804+
| Parameter | Type | Description |
805+
| --------- | ---- | ----------- |
806+
| `ts` | string | Timestamp of the operation |
807+
| `issueIdOrKey` | string | Issue ID or key |
808+
| `formId` | string | Form instance UUID |
809+
| `state` | json | Form state with status \(open, submitted, locked\) |
810+
| `updated` | string | Last updated timestamp |
811+
812+
### `jsm_submit_form`
813+
814+
Submit a form on a Jira issue or JSM request, locking it from further edits
815+
816+
#### Input
817+
818+
| Parameter | Type | Required | Description |
819+
| --------- | ---- | -------- | ----------- |
820+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
821+
| `cloudId` | string | No | Jira Cloud ID for the instance |
822+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
823+
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
824+
825+
#### Output
826+
827+
| Parameter | Type | Description |
828+
| --------- | ---- | ----------- |
829+
| `ts` | string | Timestamp of the operation |
830+
| `issueIdOrKey` | string | Issue ID or key |
831+
| `formId` | string | Form instance UUID |
832+
| `status` | string | Form status after submission \(open, submitted, locked\) |
833+
834+
### `jsm_get_form`
835+
836+
Get a single form with full design, state, and answers from a Jira issue
837+
838+
#### Input
839+
840+
| Parameter | Type | Required | Description |
841+
| --------- | ---- | -------- | ----------- |
842+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
843+
| `cloudId` | string | No | Jira Cloud ID for the instance |
844+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
845+
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
846+
847+
#### Output
848+
849+
| Parameter | Type | Description |
850+
| --------- | ---- | ----------- |
851+
| `ts` | string | Timestamp of the operation |
852+
| `issueIdOrKey` | string | Issue ID or key |
853+
| `formId` | string | Form instance UUID |
854+
| `design` | json | Full form design with questions, layout, conditions, sections, settings |
855+
| `state` | json | Form state with answers map, status \(o=open, s=submitted, l=locked\), visibility \(i=internal, e=external\) |
856+
| `updated` | string | Last updated timestamp |
857+
858+
### `jsm_get_form_answers`
859+
860+
Get simplified answers from a form attached to a Jira issue or JSM request
861+
862+
#### Input
863+
864+
| Parameter | Type | Required | Description |
865+
| --------- | ---- | -------- | ----------- |
866+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
867+
| `cloudId` | string | No | Jira Cloud ID for the instance |
868+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
869+
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
870+
871+
#### Output
872+
873+
| Parameter | Type | Description |
874+
| --------- | ---- | ----------- |
875+
| `ts` | string | Timestamp of the operation |
876+
| `issueIdOrKey` | string | Issue ID or key |
877+
| `formId` | string | Form instance UUID |
878+
| `answers` | json | Simplified form answers as key-value pairs \(question label to answer text/choices\) |
879+
880+
### `jsm_reopen_form`
881+
882+
Reopen a submitted form on a Jira issue or JSM request, allowing further edits
883+
884+
#### Input
885+
886+
| Parameter | Type | Required | Description |
887+
| --------- | ---- | -------- | ----------- |
888+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
889+
| `cloudId` | string | No | Jira Cloud ID for the instance |
890+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
891+
| `formId` | string | Yes | Form instance UUID \(from Get Issue Forms\) |
892+
893+
#### Output
894+
895+
| Parameter | Type | Description |
896+
| --------- | ---- | ----------- |
897+
| `ts` | string | Timestamp of the operation |
898+
| `issueIdOrKey` | string | Issue ID or key |
899+
| `formId` | string | Form instance UUID |
900+
| `status` | string | Form status after reopening \(open, submitted, locked\) |
901+
902+
### `jsm_delete_form`
903+
904+
Remove a form from a Jira issue or JSM request
905+
906+
#### Input
907+
908+
| Parameter | Type | Required | Description |
909+
| --------- | ---- | -------- | ----------- |
910+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
911+
| `cloudId` | string | No | Jira Cloud ID for the instance |
912+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
913+
| `formId` | string | Yes | Form instance UUID to delete |
914+
915+
#### Output
916+
917+
| Parameter | Type | Description |
918+
| --------- | ---- | ----------- |
919+
| `ts` | string | Timestamp of the operation |
920+
| `issueIdOrKey` | string | Issue ID or key |
921+
| `formId` | string | Deleted form instance UUID |
922+
| `deleted` | boolean | Whether the form was successfully deleted |
923+
924+
### `jsm_externalise_form`
925+
926+
Make a form visible to customers on a Jira issue or JSM request
927+
928+
#### Input
929+
930+
| Parameter | Type | Required | Description |
931+
| --------- | ---- | -------- | ----------- |
932+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
933+
| `cloudId` | string | No | Jira Cloud ID for the instance |
934+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
935+
| `formId` | string | Yes | Form instance UUID |
936+
937+
#### Output
938+
939+
| Parameter | Type | Description |
940+
| --------- | ---- | ----------- |
941+
| `ts` | string | Timestamp of the operation |
942+
| `issueIdOrKey` | string | Issue ID or key |
943+
| `formId` | string | Form instance UUID |
944+
| `visibility` | string | Form visibility after change \(internal or external\) |
945+
946+
### `jsm_internalise_form`
947+
948+
Make a form internal only (not visible to customers) on a Jira issue or JSM request
949+
950+
#### Input
951+
952+
| Parameter | Type | Required | Description |
953+
| --------- | ---- | -------- | ----------- |
954+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
955+
| `cloudId` | string | No | Jira Cloud ID for the instance |
956+
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
957+
| `formId` | string | Yes | Form instance UUID |
958+
959+
#### Output
960+
961+
| Parameter | Type | Description |
962+
| --------- | ---- | ----------- |
963+
| `ts` | string | Timestamp of the operation |
964+
| `issueIdOrKey` | string | Issue ID or key |
965+
| `formId` | string | Form instance UUID |
966+
| `visibility` | string | Form visibility after change \(internal or external\) |
967+
968+
### `jsm_copy_forms`
969+
970+
Copy forms from one Jira issue to another
971+
972+
#### Input
973+
974+
| Parameter | Type | Required | Description |
975+
| --------- | ---- | -------- | ----------- |
976+
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
977+
| `cloudId` | string | No | Jira Cloud ID for the instance |
978+
| `sourceIssueIdOrKey` | string | Yes | Source issue ID or key to copy forms from \(e.g., "SD-123"\) |
979+
| `targetIssueIdOrKey` | string | Yes | Target issue ID or key to copy forms to \(e.g., "SD-456"\) |
980+
| `formIds` | json | No | Optional JSON array of form UUIDs to copy \(e.g., \["uuid1", "uuid2"\]\). If omitted, copies all forms. |
981+
982+
#### Output
983+
984+
| Parameter | Type | Description |
985+
| --------- | ---- | ----------- |
986+
| `ts` | string | Timestamp of the operation |
987+
| `sourceIssueIdOrKey` | string | Source issue ID or key |
988+
| `targetIssueIdOrKey` | string | Target issue ID or key |
989+
| `copiedForms` | json | Array of successfully copied forms |
990+
| `errors` | json | Array of errors encountered during copy |
991+
761992

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6868,9 +6868,49 @@
68686868
{
68696869
"name": "Get Issue Forms",
68706870
"description": "List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)"
6871+
},
6872+
{
6873+
"name": "Attach Form",
6874+
"description": "Attach a form template to an existing Jira issue or JSM request"
6875+
},
6876+
{
6877+
"name": "Save Form Answers",
6878+
"description": "Save answers to a form attached to a Jira issue or JSM request"
6879+
},
6880+
{
6881+
"name": "Submit Form",
6882+
"description": "Submit a form on a Jira issue or JSM request, locking it from further edits"
6883+
},
6884+
{
6885+
"name": "Get Form",
6886+
"description": "Get a single form with full design, state, and answers from a Jira issue"
6887+
},
6888+
{
6889+
"name": "Get Form Answers",
6890+
"description": "Get simplified answers from a form attached to a Jira issue or JSM request"
6891+
},
6892+
{
6893+
"name": "Reopen Form",
6894+
"description": "Reopen a submitted form on a Jira issue or JSM request, allowing further edits"
6895+
},
6896+
{
6897+
"name": "Delete Form",
6898+
"description": "Remove a form from a Jira issue or JSM request"
6899+
},
6900+
{
6901+
"name": "Externalise Form",
6902+
"description": "Make a form visible to customers on a Jira issue or JSM request"
6903+
},
6904+
{
6905+
"name": "Internalise Form",
6906+
"description": "Make a form internal only (not visible to customers) on a Jira issue or JSM request"
6907+
},
6908+
{
6909+
"name": "Copy Forms",
6910+
"description": "Copy forms from one Jira issue to another"
68716911
}
68726912
],
6873-
"operationCount": 24,
6913+
"operationCount": 34,
68746914
"triggers": [],
68756915
"triggerCount": 0,
68766916
"authType": "oauth",
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkInternalAuth } from '@/lib/auth/hybrid'
4+
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
5+
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
6+
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
7+
8+
export const dynamic = 'force-dynamic'
9+
10+
const logger = createLogger('JsmGetFormAnswersAPI')
11+
12+
export async function POST(request: NextRequest) {
13+
const auth = await checkInternalAuth(request)
14+
if (!auth.success || !auth.userId) {
15+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
16+
}
17+
18+
try {
19+
const body = await request.json()
20+
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
21+
22+
if (!domain) {
23+
logger.error('Missing domain in request')
24+
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
25+
}
26+
27+
if (!accessToken) {
28+
logger.error('Missing access token in request')
29+
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
30+
}
31+
32+
if (!issueIdOrKey) {
33+
logger.error('Missing issueIdOrKey in request')
34+
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
35+
}
36+
37+
if (!formId) {
38+
logger.error('Missing formId in request')
39+
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
40+
}
41+
42+
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
43+
44+
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
45+
if (!cloudIdValidation.isValid) {
46+
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
47+
}
48+
49+
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
50+
if (!issueIdOrKeyValidation.isValid) {
51+
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
52+
}
53+
54+
const formIdValidation = validateJiraCloudId(formId, 'formId')
55+
if (!formIdValidation.isValid) {
56+
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
57+
}
58+
59+
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
60+
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/format/answers`
61+
62+
logger.info('Getting form answers:', { url, issueIdOrKey, formId })
63+
64+
const response = await fetch(url, {
65+
method: 'GET',
66+
headers: getJsmHeaders(accessToken),
67+
})
68+
69+
if (!response.ok) {
70+
const errorText = await response.text()
71+
logger.error('JSM Forms API error:', {
72+
status: response.status,
73+
statusText: response.statusText,
74+
error: errorText,
75+
})
76+
77+
return NextResponse.json(
78+
{
79+
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
80+
details: errorText,
81+
},
82+
{ status: response.status }
83+
)
84+
}
85+
86+
const data = await response.json()
87+
88+
return NextResponse.json({
89+
success: true,
90+
output: {
91+
ts: new Date().toISOString(),
92+
issueIdOrKey,
93+
formId,
94+
answers: data ?? null,
95+
},
96+
})
97+
} catch (error) {
98+
logger.error('Error getting form answers:', {
99+
error: error instanceof Error ? error.message : String(error),
100+
stack: error instanceof Error ? error.stack : undefined,
101+
})
102+
103+
return NextResponse.json(
104+
{
105+
error: error instanceof Error ? error.message : 'Internal server error',
106+
success: false,
107+
},
108+
{ status: 500 }
109+
)
110+
}
111+
}

0 commit comments

Comments
 (0)