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 533acee20a..f2c0ed2020 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 a05fcbb7ef..bea1151cad 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 0000000000..e6f95490b1 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/forms/issue/route.ts @@ -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) => ({ + 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 0000000000..2958687dab --- /dev/null +++ b/apps/sim/app/api/tools/jsm/forms/structure/route.ts @@ -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 } + ) + } +} 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 0000000000..dc33e8bc5c --- /dev/null +++ b/apps/sim/app/api/tools/jsm/forms/templates/route.ts @@ -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('JsmFormTemplatesAPI') + +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 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` + + 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 68e8a357e9..fd0cc4b84d 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 0000000000..48193e3797 --- /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 0000000000..b29652f176 --- /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 0000000000..764fd20856 --- /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 56cd5f1029..8cf000e470 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 abd96ac53e..b76b6dbfdb 100644 --- a/apps/sim/tools/jsm/types.ts +++ b/apps/sim/tools/jsm/types.ts @@ -222,6 +222,44 @@ export const REQUEST_TYPE_FIELD_PROPERTIES = { }, } 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 +816,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 +926,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 b523e6ba2c..0081547258 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 @@ -26,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}` +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 037cc9d716..76b98a0d87 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,