From a779ea070752528abdfcbb43c18b89680210b7c1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Apr 2026 13:34:02 -0700 Subject: [PATCH 1/3] feat(jsm): add ProForma/JSM Forms discovery tools Add three new tools for discovering and inspecting JSM Forms (ProForma) templates and their structure, enabling dynamic form-based workflows: - jsm_get_form_templates: List form templates in a project with request type bindings - jsm_get_form_structure: Get full form design (questions, layout, conditions, sections) - jsm_get_issue_forms: List forms attached to an issue with submission status All endpoints validated against the official Atlassian Forms REST API OpenAPI spec. Uses the Forms Cloud API base URL (jira/forms/cloud/{cloudId}) with X-ExperimentalApi header. Co-Authored-By: Claude Opus 4.6 --- .../docs/en/tools/jira_service_management.mdx | 80 ++++++++++ .../integrations/data/integrations.json | 14 +- .../app/api/tools/jsm/forms/issue/route.ts | 124 +++++++++++++++ .../api/tools/jsm/forms/structure/route.ts | 116 ++++++++++++++ .../api/tools/jsm/forms/templates/route.ts | 119 +++++++++++++++ .../blocks/blocks/jira_service_management.ts | 79 ++++++++++ apps/sim/tools/jsm/get_form_structure.ts | 121 +++++++++++++++ apps/sim/tools/jsm/get_form_templates.ts | 108 +++++++++++++ apps/sim/tools/jsm/get_issue_forms.ts | 105 +++++++++++++ apps/sim/tools/jsm/index.ts | 6 + apps/sim/tools/jsm/types.ts | 144 ++++++++++++++++++ apps/sim/tools/jsm/utils.ts | 9 ++ apps/sim/tools/registry.ts | 6 + 13 files changed, 1030 insertions(+), 1 deletion(-) create mode 100644 apps/sim/app/api/tools/jsm/forms/issue/route.ts create mode 100644 apps/sim/app/api/tools/jsm/forms/structure/route.ts create mode 100644 apps/sim/app/api/tools/jsm/forms/templates/route.ts create mode 100644 apps/sim/tools/jsm/get_form_structure.ts create mode 100644 apps/sim/tools/jsm/get_form_templates.ts create mode 100644 apps/sim/tools/jsm/get_issue_forms.ts diff --git a/apps/docs/content/docs/en/tools/jira_service_management.mdx b/apps/docs/content/docs/en/tools/jira_service_management.mdx index 533acee20ad..f2c0ed2020d 100644 --- a/apps/docs/content/docs/en/tools/jira_service_management.mdx +++ b/apps/docs/content/docs/en/tools/jira_service_management.mdx @@ -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 | + diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index a05fcbb7eff..bea1151cad6 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -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", diff --git a/apps/sim/app/api/tools/jsm/forms/issue/route.ts b/apps/sim/app/api/tools/jsm/forms/issue/route.ts new file mode 100644 index 00000000000..0c3999d20af --- /dev/null +++ b/apps/sim/app/api/tools/jsm/forms/issue/route.ts @@ -0,0 +1,124 @@ +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 } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmIssueFormsAPI') + +function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string { + try { + const errorData = JSON.parse(errorText) + if (errorData.errorMessage) { + return `JSM Forms API error: ${errorData.errorMessage}` + } + } catch { + if (errorText) { + return `JSM Forms API error: ${errorText}` + } + } + return `JSM Forms API error: ${status} ${statusText}` +} + +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) => ({ + 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)?.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 } + ) + } +} diff --git a/apps/sim/app/api/tools/jsm/forms/structure/route.ts b/apps/sim/app/api/tools/jsm/forms/structure/route.ts new file mode 100644 index 00000000000..4f711619ff2 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/forms/structure/route.ts @@ -0,0 +1,116 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getJiraCloudId, getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmFormStructureAPI') + +function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string { + try { + const errorData = JSON.parse(errorText) + if (errorData.errorMessage) { + return `JSM Forms API error: ${errorData.errorMessage}` + } + } catch { + if (errorText) { + return `JSM Forms API error: ${errorText}` + } + } + return `JSM Forms API error: ${status} ${statusText}` +} + +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 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 } + ) + } +} diff --git a/apps/sim/app/api/tools/jsm/forms/templates/route.ts b/apps/sim/app/api/tools/jsm/forms/templates/route.ts new file mode 100644 index 00000000000..bdaa80a8d9f --- /dev/null +++ b/apps/sim/app/api/tools/jsm/forms/templates/route.ts @@ -0,0 +1,119 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getJiraCloudId, getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('JsmFormTemplatesAPI') + +function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string { + try { + const errorData = JSON.parse(errorText) + if (errorData.errorMessage) { + return `JSM Forms API error: ${errorData.errorMessage}` + } + } catch { + if (errorText) { + return `JSM Forms API error: ${errorText}` + } + } + return `JSM Forms API error: ${status} ${statusText}` +} + +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 } = 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 }) + } + + const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const baseUrl = getJsmFormsApiBaseUrl(cloudId) + const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form` + + logger.info('Fetching project form templates from:', { url, projectIdOrKey }) + + 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 templates = Array.isArray(data) ? data : (data.values ?? []) + + return NextResponse.json({ + success: true, + output: { + ts: new Date().toISOString(), + projectIdOrKey, + templates: templates.map((template: Record) => ({ + id: template.id ?? null, + name: template.name ?? null, + updated: template.updated ?? null, + issueCreateIssueTypeIds: template.issueCreateIssueTypeIds ?? [], + issueCreateRequestTypeIds: template.issueCreateRequestTypeIds ?? [], + portalRequestTypeIds: template.portalRequestTypeIds ?? [], + recommendedIssueRequestTypeIds: template.recommendedIssueRequestTypeIds ?? [], + })), + total: templates.length, + }, + }) + } catch (error) { + logger.error('Error fetching form templates:', { + 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 } + ) + } +} diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 68e8a357e9b..fd0cc4b84d7 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -44,6 +44,9 @@ export const JiraServiceManagementBlock: BlockConfig = { { label: 'Get Approvals', id: 'get_approvals' }, { label: 'Answer Approval', id: 'answer_approval' }, { label: 'Get Request Type Fields', id: 'get_request_type_fields' }, + { label: 'Get Form Templates', id: 'get_form_templates' }, + { label: 'Get Form Structure', id: 'get_form_structure' }, + { label: 'Get Issue Forms', id: 'get_issue_forms' }, ], value: () => 'get_service_desks', }, @@ -191,9 +194,26 @@ export const JiraServiceManagementBlock: BlockConfig = { 'add_participants', 'get_approvals', 'answer_approval', + 'get_issue_forms', ], }, }, + { + id: 'projectIdOrKey', + title: 'Project ID or Key', + type: 'short-input', + required: { field: 'operation', value: ['get_form_templates', 'get_form_structure'] }, + placeholder: 'Enter Jira project ID or key (e.g., 10001 or SD)', + condition: { field: 'operation', value: ['get_form_templates', 'get_form_structure'] }, + }, + { + id: 'formId', + title: 'Form ID', + type: 'short-input', + required: true, + placeholder: 'Enter form ID (UUID from Get Form Templates)', + condition: { field: 'operation', value: 'get_form_structure' }, + }, { id: 'summary', title: 'Summary', @@ -503,6 +523,9 @@ Return ONLY the comment text - no explanations.`, 'jsm_get_approvals', 'jsm_answer_approval', 'jsm_get_request_type_fields', + 'jsm_get_form_templates', + 'jsm_get_form_structure', + 'jsm_get_issue_forms', ], config: { tool: (params) => { @@ -549,6 +572,12 @@ Return ONLY the comment text - no explanations.`, return 'jsm_answer_approval' case 'get_request_type_fields': return 'jsm_get_request_type_fields' + case 'get_form_templates': + return 'jsm_get_form_templates' + case 'get_form_structure': + return 'jsm_get_form_structure' + case 'get_issue_forms': + return 'jsm_get_issue_forms' default: return 'jsm_get_service_desks' } @@ -808,6 +837,34 @@ Return ONLY the comment text - no explanations.`, serviceDeskId: params.serviceDeskId, requestTypeId: params.requestTypeId, } + case 'get_form_templates': + if (!params.projectIdOrKey) { + throw new Error('Project ID or key is required') + } + return { + ...baseParams, + projectIdOrKey: params.projectIdOrKey, + } + case 'get_form_structure': + if (!params.projectIdOrKey) { + throw new Error('Project ID or key is required') + } + if (!params.formId) { + throw new Error('Form ID is required') + } + return { + ...baseParams, + projectIdOrKey: params.projectIdOrKey, + formId: params.formId, + } + case 'get_issue_forms': + if (!params.issueIdOrKey) { + throw new Error('Issue ID or key is required') + } + return { + ...baseParams, + issueIdOrKey: params.issueIdOrKey, + } default: return baseParams } @@ -857,6 +914,8 @@ Return ONLY the comment text - no explanations.`, type: 'string', description: 'JSON object of form answers for form-based request types', }, + projectIdOrKey: { type: 'string', description: 'Jira project ID or key' }, + formId: { type: 'string', description: 'Form ID (UUID)' }, 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' }, @@ -899,5 +958,25 @@ Return ONLY the comment text - no explanations.`, type: 'boolean', description: 'Whether requests can be raised on behalf of another user', }, + templates: { + type: 'json', + description: + 'Array of form templates (id, name, updated, portalRequestTypeIds, issueCreateIssueTypeIds)', + }, + design: { + type: 'json', + description: + 'Full form design with questions (labels, types, choices, validation), layout, conditions, sections, settings', + }, + publish: { + type: 'json', + description: 'Form publishing and request type configuration', + }, + updated: { type: 'string', description: 'Last updated timestamp' }, + forms: { + type: 'json', + description: + 'Array of forms attached to an issue (id, name, updated, submitted, lock, internal, formTemplateId)', + }, }, } diff --git a/apps/sim/tools/jsm/get_form_structure.ts b/apps/sim/tools/jsm/get_form_structure.ts new file mode 100644 index 00000000000..48193e37972 --- /dev/null +++ b/apps/sim/tools/jsm/get_form_structure.ts @@ -0,0 +1,121 @@ +import type { JsmGetFormStructureParams, JsmGetFormStructureResponse } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmGetFormStructureTool: ToolConfig< + JsmGetFormStructureParams, + JsmGetFormStructureResponse +> = { + id: 'jsm_get_form_structure', + name: 'JSM Get Form Structure', + description: + 'Get the full structure of a ProForma/JSM form including all questions, field types, choices, layout, and conditions', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + projectIdOrKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira project ID or key (e.g., "10001" or "SD")', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Form ID (UUID from Get Form Templates)', + }, + }, + + request: { + url: '/api/tools/jsm/forms/structure', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + projectIdOrKey: params.projectIdOrKey, + formId: params.formId, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { + ts: new Date().toISOString(), + projectIdOrKey: '', + formId: '', + design: null, + updated: null, + publish: null, + }, + error: 'Empty response from API', + } + } + + const data = JSON.parse(responseText) + + if (data.success && data.output) { + return data + } + + return { + success: data.success || false, + output: data.output || { + ts: new Date().toISOString(), + projectIdOrKey: '', + formId: '', + design: null, + updated: null, + publish: null, + }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + projectIdOrKey: { type: 'string', description: 'Project ID or key' }, + formId: { type: 'string', description: 'Form ID' }, + design: { + type: 'json', + description: + 'Full form design with questions (field types, labels, choices, validation), layout (field ordering), and conditions', + }, + updated: { type: 'string', description: 'Last updated timestamp', optional: true }, + publish: { + type: 'json', + description: 'Publishing and request type configuration', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/jsm/get_form_templates.ts b/apps/sim/tools/jsm/get_form_templates.ts new file mode 100644 index 00000000000..b29652f1764 --- /dev/null +++ b/apps/sim/tools/jsm/get_form_templates.ts @@ -0,0 +1,108 @@ +import type { JsmGetFormTemplatesParams, JsmGetFormTemplatesResponse } from '@/tools/jsm/types' +import { FORM_TEMPLATE_PROPERTIES } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmGetFormTemplatesTool: ToolConfig< + JsmGetFormTemplatesParams, + JsmGetFormTemplatesResponse +> = { + id: 'jsm_get_form_templates', + name: 'JSM Get Form Templates', + description: + 'List forms (ProForma/JSM Forms) in a Jira project to discover form IDs for request types', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + projectIdOrKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira project ID or key (e.g., "10001" or "SD")', + }, + }, + + request: { + url: '/api/tools/jsm/forms/templates', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + projectIdOrKey: params.projectIdOrKey, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { + ts: new Date().toISOString(), + projectIdOrKey: '', + templates: [], + total: 0, + }, + error: 'Empty response from API', + } + } + + const data = JSON.parse(responseText) + + if (data.success && data.output) { + return data + } + + return { + success: data.success || false, + output: data.output || { + ts: new Date().toISOString(), + projectIdOrKey: '', + templates: [], + total: 0, + }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + projectIdOrKey: { type: 'string', description: 'Project ID or key' }, + templates: { + type: 'array', + description: 'List of forms in the project', + items: { + type: 'object', + properties: FORM_TEMPLATE_PROPERTIES, + }, + }, + total: { type: 'number', description: 'Total number of forms' }, + }, +} diff --git a/apps/sim/tools/jsm/get_issue_forms.ts b/apps/sim/tools/jsm/get_issue_forms.ts new file mode 100644 index 00000000000..764fd20856f --- /dev/null +++ b/apps/sim/tools/jsm/get_issue_forms.ts @@ -0,0 +1,105 @@ +import type { JsmGetIssueFormsParams, JsmGetIssueFormsResponse } from '@/tools/jsm/types' +import { ISSUE_FORM_PROPERTIES } from '@/tools/jsm/types' +import type { ToolConfig } from '@/tools/types' + +export const jsmGetIssueFormsTool: ToolConfig = { + id: 'jsm_get_issue_forms', + name: 'JSM Get Issue Forms', + description: + 'List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira Service Management', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Jira Cloud ID for the instance', + }, + issueIdOrKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Issue ID or key (e.g., "SD-123", "10001")', + }, + }, + + request: { + url: '/api/tools/jsm/forms/issue', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + issueIdOrKey: params.issueIdOrKey, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { + ts: new Date().toISOString(), + issueIdOrKey: '', + forms: [], + total: 0, + }, + error: 'Empty response from API', + } + } + + const data = JSON.parse(responseText) + + if (data.success && data.output) { + return data + } + + return { + success: data.success || false, + output: data.output || { + ts: new Date().toISOString(), + issueIdOrKey: '', + forms: [], + total: 0, + }, + error: data.error, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + issueIdOrKey: { type: 'string', description: 'Issue ID or key' }, + forms: { + type: 'array', + description: 'List of forms attached to the issue', + items: { + type: 'object', + properties: ISSUE_FORM_PROPERTIES, + }, + }, + total: { type: 'number', description: 'Total number of forms' }, + }, +} diff --git a/apps/sim/tools/jsm/index.ts b/apps/sim/tools/jsm/index.ts index 56cd5f1029b..8cf000e4703 100644 --- a/apps/sim/tools/jsm/index.ts +++ b/apps/sim/tools/jsm/index.ts @@ -8,6 +8,9 @@ import { jsmCreateRequestTool } from '@/tools/jsm/create_request' import { jsmGetApprovalsTool } from '@/tools/jsm/get_approvals' import { jsmGetCommentsTool } from '@/tools/jsm/get_comments' import { jsmGetCustomersTool } from '@/tools/jsm/get_customers' +import { jsmGetFormStructureTool } from '@/tools/jsm/get_form_structure' +import { jsmGetFormTemplatesTool } from '@/tools/jsm/get_form_templates' +import { jsmGetIssueFormsTool } from '@/tools/jsm/get_issue_forms' import { jsmGetOrganizationsTool } from '@/tools/jsm/get_organizations' import { jsmGetParticipantsTool } from '@/tools/jsm/get_participants' import { jsmGetQueuesTool } from '@/tools/jsm/get_queues' @@ -31,6 +34,9 @@ export { jsmGetApprovalsTool, jsmGetCommentsTool, jsmGetCustomersTool, + jsmGetFormStructureTool, + jsmGetFormTemplatesTool, + jsmGetIssueFormsTool, jsmGetOrganizationsTool, jsmGetParticipantsTool, jsmGetQueuesTool, diff --git a/apps/sim/tools/jsm/types.ts b/apps/sim/tools/jsm/types.ts index abd96ac53ea..f9337bf0edb 100644 --- a/apps/sim/tools/jsm/types.ts +++ b/apps/sim/tools/jsm/types.ts @@ -222,6 +222,64 @@ export const REQUEST_TYPE_FIELD_PROPERTIES = { }, } as const +/** Output properties for a FormQuestion per OpenAPI spec */ +export const FORM_QUESTION_PROPERTIES = { + label: { type: 'string', description: 'Question label' }, + type: { + type: 'string', + description: + 'Question type: ts=short text, tl=long text, cd=dropdown, cl=multiselect, cm=checkboxes, cs=radio, da=date, dt=date-time, no=number, at=attachment, us=single user, um=multi user, te=email, tu=URL, ti=time, rt=paragraph, ob=assets', + }, + validation: { type: 'json', description: 'Validation rules (rq=required)' }, + choices: { type: 'json', description: 'Available choices for select questions', optional: true }, + description: { type: 'string', description: 'Help text for the question', optional: true }, + jiraField: { + type: 'string', + description: 'Linked Jira field ID (e.g., summary, customfield_10010)', + optional: true, + }, + questionKey: { type: 'string', description: 'Question key identifier', optional: true }, + defaultAnswer: { type: 'json', description: 'Default answer value', optional: true }, +} as const + +/** Output properties for a FormTemplateIndexEntry (list endpoint) per OpenAPI spec */ +export const FORM_TEMPLATE_PROPERTIES = { + id: { type: 'string', description: 'Form template ID (UUID)' }, + name: { type: 'string', description: 'Form template name' }, + updated: { type: 'string', description: 'Last updated timestamp (ISO 8601)' }, + issueCreateIssueTypeIds: { + type: 'json', + description: 'Issue type IDs that auto-attach this form on issue create', + }, + issueCreateRequestTypeIds: { + type: 'json', + description: 'Request type IDs that auto-attach this form on issue create', + }, + portalRequestTypeIds: { + type: 'json', + description: 'Request type IDs that show this form on the customer portal', + }, + recommendedIssueRequestTypeIds: { + type: 'json', + description: 'Request type IDs that recommend this form', + }, +} as const + +/** Output properties for a FormIndexEntry (issue forms list endpoint) per OpenAPI spec */ +export const ISSUE_FORM_PROPERTIES = { + id: { type: 'string', description: 'Form instance ID (UUID)' }, + name: { type: 'string', description: 'Form name' }, + updated: { type: 'string', description: 'Last updated timestamp (ISO 8601)' }, + submitted: { type: 'boolean', description: 'Whether the form has been submitted' }, + lock: { type: 'boolean', description: 'Whether the form is locked' }, + internal: { type: 'boolean', description: 'Whether the form is internal-only', optional: true }, + formTemplateId: { + type: 'string', + description: 'Source form template ID (UUID)', + optional: true, + }, +} as const + // --------------------------------------------------------------------------- // Data model interfaces // --------------------------------------------------------------------------- @@ -778,6 +836,89 @@ export interface JsmGetRequestTypeFieldsResponse extends ToolResponse { } } +export interface JsmGetFormTemplatesParams extends JsmBaseParams { + projectIdOrKey: string +} + +export interface JsmGetFormStructureParams extends JsmBaseParams { + projectIdOrKey: string + formId: string +} + +export interface JsmGetIssueFormsParams extends JsmBaseParams { + issueIdOrKey: string +} + +/** FormQuestion per OpenAPI spec */ +export interface JsmFormQuestion { + label: string + type: string + validation: { rq?: boolean; [key: string]: unknown } + choices?: Array<{ id: string; label: string; other?: boolean }> + dcId?: string + defaultAnswer?: Record + description?: string + jiraField?: string + questionKey?: string +} + +/** FormTemplateIndexEntry per OpenAPI spec */ +export interface JsmFormTemplate { + id: string + name: string + updated: string + issueCreateIssueTypeIds: number[] + issueCreateRequestTypeIds: number[] + portalRequestTypeIds: number[] + recommendedIssueRequestTypeIds: number[] +} + +/** FormIndexEntry (issue form) per OpenAPI spec */ +export interface JsmIssueForm { + id: string + name: string + updated: string + submitted: boolean + lock: boolean + internal?: boolean + formTemplateId?: string +} + +export interface JsmGetFormTemplatesResponse extends ToolResponse { + output: { + ts: string + projectIdOrKey: string + templates: JsmFormTemplate[] + total: number + } +} + +export interface JsmGetFormStructureResponse extends ToolResponse { + output: { + ts: string + projectIdOrKey: string + formId: string + design: { + questions: Record + layout: unknown[] + conditions: Record + sections: Record + settings: { name: string; submit: { lock: boolean; pdf: boolean }; language?: string } + } | null + updated: string | null + publish: Record | null + } +} + +export interface JsmGetIssueFormsResponse extends ToolResponse { + output: { + ts: string + issueIdOrKey: string + forms: JsmIssueForm[] + total: number + } +} + // --------------------------------------------------------------------------- // Union type for all JSM responses // --------------------------------------------------------------------------- @@ -805,3 +946,6 @@ export type JsmResponse = | JsmGetApprovalsResponse | JsmAnswerApprovalResponse | JsmGetRequestTypeFieldsResponse + | JsmGetFormTemplatesResponse + | JsmGetFormStructureResponse + | JsmGetIssueFormsResponse diff --git a/apps/sim/tools/jsm/utils.ts b/apps/sim/tools/jsm/utils.ts index b523e6ba2c4..4560ad57003 100644 --- a/apps/sim/tools/jsm/utils.ts +++ b/apps/sim/tools/jsm/utils.ts @@ -13,6 +13,15 @@ export function getJsmApiBaseUrl(cloudId: string): string { return `https://api.atlassian.com/ex/jira/${cloudId}/rest/servicedeskapi` } +/** + * Build the base URL for JSM Forms (ProForma) API + * @param cloudId - The Jira Cloud ID + * @returns The base URL for the JSM Forms API + */ +export function getJsmFormsApiBaseUrl(cloudId: string): string { + return `https://api.atlassian.com/jira/forms/cloud/${cloudId}` +} + /** * Build common headers for JSM API requests * @param accessToken - The OAuth access token diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 037cc9d716e..76b98a0d87c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1292,6 +1292,9 @@ import { jsmGetApprovalsTool, jsmGetCommentsTool, jsmGetCustomersTool, + jsmGetFormStructureTool, + jsmGetFormTemplatesTool, + jsmGetIssueFormsTool, jsmGetOrganizationsTool, jsmGetParticipantsTool, jsmGetQueuesTool, @@ -3093,6 +3096,9 @@ export const tools: Record = { jsm_add_participants: jsmAddParticipantsTool, jsm_get_approvals: jsmGetApprovalsTool, jsm_answer_approval: jsmAnswerApprovalTool, + jsm_get_form_templates: jsmGetFormTemplatesTool, + jsm_get_form_structure: jsmGetFormStructureTool, + jsm_get_issue_forms: jsmGetIssueFormsTool, kalshi_get_markets: kalshiGetMarketsTool, kalshi_get_markets_v2: kalshiGetMarketsV2Tool, kalshi_get_market: kalshiGetMarketTool, From 6ec5cab3100640dd89d63ea37af8193464e49191 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Apr 2026 13:43:44 -0700 Subject: [PATCH 2/3] fix(jsm): add input validation and extract shared error parser - Add validateJiraIssueKey for projectIdOrKey in templates and structure routes - Add validateJiraCloudId for formId (UUID) in structure route - Extract parseJsmErrorMessage to shared utils.ts (was duplicated across 3 routes) Co-Authored-By: Claude Opus 4.6 --- .../app/api/tools/jsm/forms/issue/route.ts | 21 ++++-------- .../api/tools/jsm/forms/structure/route.ts | 33 ++++++++++--------- .../api/tools/jsm/forms/templates/route.ts | 28 +++++++--------- apps/sim/tools/jsm/utils.ts | 25 ++++++++++++++ 4 files changed, 60 insertions(+), 47 deletions(-) diff --git a/apps/sim/app/api/tools/jsm/forms/issue/route.ts b/apps/sim/app/api/tools/jsm/forms/issue/route.ts index 0c3999d20af..e6f95490b17 100644 --- a/apps/sim/app/api/tools/jsm/forms/issue/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/issue/route.ts @@ -2,26 +2,17 @@ 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 } from '@/tools/jsm/utils' +import { + getJiraCloudId, + getJsmFormsApiBaseUrl, + getJsmHeaders, + parseJsmErrorMessage, +} from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' const logger = createLogger('JsmIssueFormsAPI') -function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string { - try { - const errorData = JSON.parse(errorText) - if (errorData.errorMessage) { - return `JSM Forms API error: ${errorData.errorMessage}` - } - } catch { - if (errorText) { - return `JSM Forms API error: ${errorText}` - } - } - return `JSM Forms API error: ${status} ${statusText}` -} - export async function POST(request: NextRequest) { const auth = await checkInternalAuth(request) if (!auth.success || !auth.userId) { diff --git a/apps/sim/app/api/tools/jsm/forms/structure/route.ts b/apps/sim/app/api/tools/jsm/forms/structure/route.ts index 4f711619ff2..2958687dab3 100644 --- a/apps/sim/app/api/tools/jsm/forms/structure/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/structure/route.ts @@ -1,27 +1,18 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { validateJiraCloudId } from '@/lib/core/security/input-validation' -import { getJiraCloudId, getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' +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') -function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string { - try { - const errorData = JSON.parse(errorText) - if (errorData.errorMessage) { - return `JSM Forms API error: ${errorData.errorMessage}` - } - } catch { - if (errorText) { - return `JSM Forms API error: ${errorText}` - } - } - return `JSM Forms API error: ${status} ${statusText}` -} - export async function POST(request: NextRequest) { const auth = await checkInternalAuth(request) if (!auth.success || !auth.userId) { @@ -59,6 +50,16 @@ export async function POST(request: NextRequest) { 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)}` diff --git a/apps/sim/app/api/tools/jsm/forms/templates/route.ts b/apps/sim/app/api/tools/jsm/forms/templates/route.ts index bdaa80a8d9f..dc33e8bc5c9 100644 --- a/apps/sim/app/api/tools/jsm/forms/templates/route.ts +++ b/apps/sim/app/api/tools/jsm/forms/templates/route.ts @@ -1,27 +1,18 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { validateJiraCloudId } from '@/lib/core/security/input-validation' -import { getJiraCloudId, getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' +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('JsmFormTemplatesAPI') -function parseJsmErrorMessage(status: number, statusText: string, errorText: string): string { - try { - const errorData = JSON.parse(errorText) - if (errorData.errorMessage) { - return `JSM Forms API error: ${errorData.errorMessage}` - } - } catch { - if (errorText) { - return `JSM Forms API error: ${errorText}` - } - } - return `JSM Forms API error: ${status} ${statusText}` -} - export async function POST(request: NextRequest) { const auth = await checkInternalAuth(request) if (!auth.success || !auth.userId) { @@ -54,6 +45,11 @@ export async function POST(request: NextRequest) { 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 baseUrl = getJsmFormsApiBaseUrl(cloudId) const url = `${baseUrl}/project/${encodeURIComponent(projectIdOrKey)}/form` diff --git a/apps/sim/tools/jsm/utils.ts b/apps/sim/tools/jsm/utils.ts index 4560ad57003..00815472585 100644 --- a/apps/sim/tools/jsm/utils.ts +++ b/apps/sim/tools/jsm/utils.ts @@ -35,3 +35,28 @@ export function getJsmHeaders(accessToken: string): Record { 'X-ExperimentalApi': 'opt-in', } } + +/** + * Parse error messages from JSM/Forms API responses + * @param status - HTTP status code + * @param statusText - HTTP status text + * @param errorText - Raw error response body + * @returns Formatted error message string + */ +export function parseJsmErrorMessage( + status: number, + statusText: string, + errorText: string +): string { + try { + const errorData = JSON.parse(errorText) + if (errorData.errorMessage) { + return `JSM Forms API error: ${errorData.errorMessage}` + } + } catch { + if (errorText) { + return `JSM Forms API error: ${errorText}` + } + } + return `JSM Forms API error: ${status} ${statusText}` +} From 39b276bdacc14d2872123f2af7321bbefaf103bd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Apr 2026 13:51:25 -0700 Subject: [PATCH 3/3] chore(jsm): remove unused FORM_QUESTION_PROPERTIES constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dead code — the get_form_structure tool passes the raw design object through as JSON, so this output constant had no consumers. Co-Authored-By: Claude Opus 4.6 --- apps/sim/tools/jsm/types.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/apps/sim/tools/jsm/types.ts b/apps/sim/tools/jsm/types.ts index f9337bf0edb..b76b6dbfdb8 100644 --- a/apps/sim/tools/jsm/types.ts +++ b/apps/sim/tools/jsm/types.ts @@ -222,26 +222,6 @@ export const REQUEST_TYPE_FIELD_PROPERTIES = { }, } as const -/** Output properties for a FormQuestion per OpenAPI spec */ -export const FORM_QUESTION_PROPERTIES = { - label: { type: 'string', description: 'Question label' }, - type: { - type: 'string', - description: - 'Question type: ts=short text, tl=long text, cd=dropdown, cl=multiselect, cm=checkboxes, cs=radio, da=date, dt=date-time, no=number, at=attachment, us=single user, um=multi user, te=email, tu=URL, ti=time, rt=paragraph, ob=assets', - }, - validation: { type: 'json', description: 'Validation rules (rq=required)' }, - choices: { type: 'json', description: 'Available choices for select questions', optional: true }, - description: { type: 'string', description: 'Help text for the question', optional: true }, - jiraField: { - type: 'string', - description: 'Linked Jira field ID (e.g., summary, customfield_10010)', - optional: true, - }, - questionKey: { type: 'string', description: 'Question key identifier', optional: true }, - defaultAnswer: { type: 'json', description: 'Default answer value', optional: true }, -} as const - /** Output properties for a FormTemplateIndexEntry (list endpoint) per OpenAPI spec */ export const FORM_TEMPLATE_PROPERTIES = { id: { type: 'string', description: 'Form template ID (UUID)' },