diff --git a/apps/sim/blocks/blocks/jotform.ts b/apps/sim/blocks/blocks/jotform.ts new file mode 100644 index 0000000000..56609807d2 --- /dev/null +++ b/apps/sim/blocks/blocks/jotform.ts @@ -0,0 +1,174 @@ +import { JotformIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { JotformSubmissionsResponse } from '@/tools/jotform/types' +import { getTrigger } from '@/triggers' + +export const JotformBlock: BlockConfig = { + type: 'jotform', + name: 'Jotform', + description: 'Interact with Jotform', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Jotform into the workflow. Can retrieve form submissions, get form details, and list forms. Can be used in trigger mode to trigger a workflow when a form is submitted. Requires API Key.', + docsLink: 'https://docs.sim.ai/tools/jotform', + category: 'tools', + bgColor: '#FF6100', // Jotform brand color + icon: JotformIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get Submissions', id: 'jotform_submissions' }, + { label: 'Get Form Details', id: 'jotform_get_form' }, + { label: 'List Forms', id: 'jotform_list_forms' }, + ], + value: () => 'jotform_submissions', + }, + { + id: 'formId', + title: 'Form ID', + type: 'short-input', + placeholder: 'Enter your Jotform form ID', + required: true, + condition: { + field: 'operation', + value: ['jotform_submissions', 'jotform_get_form'], + }, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Jotform API key', + password: true, + required: true, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Number of submissions to retrieve (default: 20, max: 1000)', + condition: { field: 'operation', value: 'jotform_submissions' }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: 'Start offset for pagination (default: 0)', + condition: { field: 'operation', value: 'jotform_submissions' }, + }, + { + id: 'filter', + title: 'Filter', + type: 'short-input', + placeholder: 'Filter submissions (e.g., {"status:ne":"DELETED"})', + condition: { field: 'operation', value: 'jotform_submissions' }, + }, + { + id: 'orderby', + title: 'Order By', + type: 'short-input', + placeholder: 'Order results by field (e.g., "created_at" or "id")', + condition: { field: 'operation', value: 'jotform_submissions' }, + }, + { + id: 'listOffset', + title: 'Offset', + type: 'short-input', + placeholder: 'Start offset for pagination (default: 0)', + condition: { field: 'operation', value: 'jotform_list_forms' }, + }, + { + id: 'listLimit', + title: 'Limit', + type: 'short-input', + placeholder: 'Number of forms to retrieve (default: 20)', + condition: { field: 'operation', value: 'jotform_list_forms' }, + }, + { + id: 'listFilter', + title: 'Filter', + type: 'short-input', + placeholder: 'Filter forms (e.g., {"status:ne":"DELETED"})', + condition: { field: 'operation', value: 'jotform_list_forms' }, + }, + { + id: 'listOrderby', + title: 'Order By', + type: 'short-input', + placeholder: 'Order results by field (e.g., "created_at" or "title")', + condition: { field: 'operation', value: 'jotform_list_forms' }, + }, + ...getTrigger('jotform_webhook').subBlocks, + ], + tools: { + access: ['jotform_submissions', 'jotform_get_form', 'jotform_list_forms'], + config: { + tool: (params) => { + switch (params.operation) { + case 'jotform_submissions': + return 'jotform_submissions' + case 'jotform_get_form': + return 'jotform_get_form' + case 'jotform_list_forms': + return 'jotform_list_forms' + default: + return 'jotform_submissions' + } + }, + params: (params) => { + const { operation, listLimit, listOffset, listFilter, listOrderby, ...rest } = params + + if (operation === 'jotform_list_forms') { + return { + apiKey: params.apiKey, + ...(listLimit && { limit: listLimit }), + ...(listOffset && { offset: listOffset }), + ...(listFilter && { filter: listFilter }), + ...(listOrderby && { orderby: listOrderby }), + } + } + + return rest + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + formId: { type: 'string', description: 'Jotform form identifier' }, + apiKey: { type: 'string', description: 'Jotform API key' }, + limit: { type: 'number', description: 'Number of submissions to retrieve' }, + offset: { type: 'number', description: 'Pagination offset' }, + filter: { type: 'string', description: 'Filter submissions' }, + orderby: { type: 'string', description: 'Order submissions by field' }, + listLimit: { type: 'number', description: 'Number of forms to retrieve' }, + listOffset: { type: 'number', description: 'Pagination offset for forms' }, + listFilter: { type: 'string', description: 'Filter forms' }, + listOrderby: { type: 'string', description: 'Order forms by field' }, + }, + outputs: { + resultSet: { + type: 'array', + description: + 'Array of submission objects with id, form_id, created_at, status, answers, and metadata', + }, + forms: { + type: 'array', + description: 'Array of form objects with id, title, status, created_at, url, and metadata', + }, + id: { type: 'string', description: 'Form unique identifier' }, + title: { type: 'string', description: 'Form title' }, + status: { type: 'string', description: 'Form status' }, + created_at: { type: 'string', description: 'Form creation timestamp' }, + updated_at: { type: 'string', description: 'Form last update timestamp' }, + count: { type: 'string', description: 'Number of submissions' }, + url: { type: 'string', description: 'Form URL' }, + }, + triggers: { + enabled: true, + available: ['jotform_webhook'], + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index b9b30e5fa7..9ffecaac28 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -54,6 +54,7 @@ import { InputTriggerBlock } from '@/blocks/blocks/input_trigger' import { IntercomBlock } from '@/blocks/blocks/intercom' import { JinaBlock } from '@/blocks/blocks/jina' import { JiraBlock } from '@/blocks/blocks/jira' +import { JotformBlock } from '@/blocks/blocks/jotform' import { KalshiBlock } from '@/blocks/blocks/kalshi' import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LinearBlock } from '@/blocks/blocks/linear' @@ -198,6 +199,7 @@ export const registry: Record = { intercom: IntercomBlock, jina: JinaBlock, jira: JiraBlock, + jotform: JotformBlock, kalshi: KalshiBlock, knowledge: KnowledgeBlock, linear: LinearBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 5f5e0c2bb4..062e2fdbe3 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1390,6 +1390,38 @@ export function TypeformIcon(props: SVGProps) { ) } +export function JotformIcon(props: SVGProps) { + return ( + + + + + ) +} + export function DocumentIcon(props: SVGProps) { return ( = {} + for (const [key, value] of formData.entries()) { + if (typeof value === 'string') { + formObject[key] = value + } + } + body = formObject + logger.debug(`[${requestId}] Parsed multipart/form-data webhook into object`) + } + } catch (multipartError) { + logger.error(`[${requestId}] Failed to parse multipart/form-data`, { + error: multipartError instanceof Error ? multipartError.message : String(multipartError), + }) + throw new Error(`Failed to parse multipart/form-data: ${multipartError}`) + } } else { body = JSON.parse(rawBody) logger.debug(`[${requestId}] Parsed JSON webhook payload`) diff --git a/apps/sim/tools/jotform/get_form.ts b/apps/sim/tools/jotform/get_form.ts new file mode 100644 index 0000000000..91d9bd21d1 --- /dev/null +++ b/apps/sim/tools/jotform/get_form.ts @@ -0,0 +1,74 @@ +import type { JotformGetFormParams, JotformGetFormResponse } from '@/tools/jotform/types' +import type { ToolConfig } from '@/tools/types' + +export const getFormTool: ToolConfig = { + id: 'jotform_get_form', + name: 'Jotform Get Form', + description: 'Retrieve form details from Jotform', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Jotform API Key', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Jotform form ID', + }, + }, + + request: { + url: (params: JotformGetFormParams) => { + return `https://api.jotform.com/form/${params.formId}?apiKey=${encodeURIComponent(params.apiKey)}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: data.content || {}, + } + }, + + outputs: { + id: { + type: 'string', + description: 'Form ID', + }, + title: { + type: 'string', + description: 'Form title', + }, + status: { + type: 'string', + description: 'Form status', + }, + created_at: { + type: 'string', + description: 'Form creation timestamp', + }, + updated_at: { + type: 'string', + description: 'Form last update timestamp', + }, + count: { + type: 'string', + description: 'Number of submissions', + }, + url: { + type: 'string', + description: 'Form URL', + }, + }, +} diff --git a/apps/sim/tools/jotform/index.ts b/apps/sim/tools/jotform/index.ts new file mode 100644 index 0000000000..0ce83e8668 --- /dev/null +++ b/apps/sim/tools/jotform/index.ts @@ -0,0 +1,7 @@ +import { getFormTool } from '@/tools/jotform/get_form' +import { listFormsTool } from '@/tools/jotform/list_forms' +import { submissionsTool } from '@/tools/jotform/submissions' + +export const jotformSubmissionsTool = submissionsTool +export const jotformGetFormTool = getFormTool +export const jotformListFormsTool = listFormsTool diff --git a/apps/sim/tools/jotform/list_forms.ts b/apps/sim/tools/jotform/list_forms.ts new file mode 100644 index 0000000000..abb6505f26 --- /dev/null +++ b/apps/sim/tools/jotform/list_forms.ts @@ -0,0 +1,90 @@ +import type { JotformListFormsParams, JotformListFormsResponse } from '@/tools/jotform/types' +import type { ToolConfig } from '@/tools/types' + +export const listFormsTool: ToolConfig = { + id: 'jotform_list_forms', + name: 'Jotform List Forms', + description: 'List all forms from Jotform account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Jotform API Key', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Start offset for pagination (default: 0)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of forms to retrieve (default: 20)', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter forms (e.g., {"status:ne":"DELETED"})', + }, + orderby: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order results by field (e.g., "created_at" or "title")', + }, + }, + + request: { + url: (params: JotformListFormsParams) => { + const url = 'https://api.jotform.com/user/forms' + + const queryParams = [`apiKey=${encodeURIComponent(params.apiKey)}`] + + if (params.offset) { + queryParams.push(`offset=${Number(params.offset)}`) + } + + if (params.limit) { + queryParams.push(`limit=${Number(params.limit)}`) + } + + if (params.filter) { + queryParams.push(`filter=${encodeURIComponent(params.filter)}`) + } + + if (params.orderby) { + queryParams.push(`orderby=${encodeURIComponent(params.orderby)}`) + } + + return `${url}?${queryParams.join('&')}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + forms: data.content || [], + }, + } + }, + + outputs: { + forms: { + type: 'array', + description: 'Array of form objects with id, title, status, created_at, url, and metadata', + }, + }, +} diff --git a/apps/sim/tools/jotform/submissions.ts b/apps/sim/tools/jotform/submissions.ts new file mode 100644 index 0000000000..fa21e2403f --- /dev/null +++ b/apps/sim/tools/jotform/submissions.ts @@ -0,0 +1,100 @@ +import type { JotformSubmissionsParams, JotformSubmissionsResponse } from '@/tools/jotform/types' +import type { ToolConfig } from '@/tools/types' + +export const submissionsTool: ToolConfig< + JotformSubmissionsParams, + JotformSubmissionsResponse +> = { + id: 'jotform_submissions', + name: 'Jotform Submissions', + description: 'Retrieve form submissions from Jotform', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Jotform API Key', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Jotform form ID', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of submissions to retrieve (default: 20, max: 1000)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Start offset for pagination (default: 0)', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter submissions (e.g., {"status:ne":"DELETED"})', + }, + orderby: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order results by field (e.g., "created_at" or "id")', + }, + }, + + request: { + url: (params: JotformSubmissionsParams) => { + const url = `https://api.jotform.com/form/${params.formId}/submissions` + + const queryParams = [`apiKey=${encodeURIComponent(params.apiKey)}`] + + if (params.limit) { + queryParams.push(`limit=${Number(params.limit)}`) + } + + if (params.offset) { + queryParams.push(`offset=${Number(params.offset)}`) + } + + if (params.filter) { + queryParams.push(`filter=${encodeURIComponent(params.filter)}`) + } + + if (params.orderby) { + queryParams.push(`orderby=${encodeURIComponent(params.orderby)}`) + } + + return `${url}?${queryParams.join('&')}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + resultSet: data.content || [], + }, + } + }, + + outputs: { + resultSet: { + type: 'array', + description: + 'Array of submission objects with id, form_id, created_at, status, answers, and metadata', + }, + }, +} diff --git a/apps/sim/tools/jotform/types.ts b/apps/sim/tools/jotform/types.ts new file mode 100644 index 0000000000..5eafc450bd --- /dev/null +++ b/apps/sim/tools/jotform/types.ts @@ -0,0 +1,89 @@ +import type { ToolResponse } from '@/tools/types' + +export interface JotformSubmissionsParams { + apiKey: string + formId: string + limit?: number + offset?: number + filter?: string + orderby?: string +} + +export interface JotformSubmissionsResponse extends ToolResponse { + output: { + resultSet: Array<{ + id: string + form_id: string + ip: string + created_at: string + updated_at: string + status: string + new: string + flag: string + answers: Record< + string, + { + name: string + order: string + text: string + type: string + answer: string | string[] | Record + prettyFormat?: string + } + > + }> + } +} + +export interface JotformGetFormParams { + apiKey: string + formId: string +} + +export interface JotformGetFormResponse extends ToolResponse { + output: { + id: string + username: string + title: string + height: string + status: string + created_at: string + updated_at: string + last_submission: string + new: string + count: string + type: string + favorite: string + archived: string + url: string + } +} + +export interface JotformListFormsParams { + apiKey: string + offset?: number + limit?: number + filter?: string + orderby?: string +} + +export interface JotformListFormsResponse extends ToolResponse { + output: { + forms: Array<{ + id: string + username: string + title: string + height: string + status: string + created_at: string + updated_at: string + last_submission: string + new: string + count: string + type: string + favorite: string + archived: string + url: string + }> + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 005e922cad..c743ce49ab 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1249,6 +1249,11 @@ import { typeformResponsesTool, typeformUpdateFormTool, } from '@/tools/typeform' +import { + jotformGetFormTool, + jotformListFormsTool, + jotformSubmissionsTool, +} from '@/tools/jotform' import type { ToolConfig } from '@/tools/types' import { falaiVideoTool, @@ -1585,6 +1590,9 @@ export const tools: Record = { typeform_create_form: typeformCreateFormTool, typeform_update_form: typeformUpdateFormTool, typeform_delete_form: typeformDeleteFormTool, + jotform_submissions: jotformSubmissionsTool, + jotform_get_form: jotformGetFormTool, + jotform_list_forms: jotformListFormsTool, youtube_search: youtubeSearchTool, youtube_video_details: youtubeVideoDetailsTool, youtube_channel_info: youtubeChannelInfoTool, diff --git a/apps/sim/triggers/jotform/index.ts b/apps/sim/triggers/jotform/index.ts new file mode 100644 index 0000000000..898766e6f2 --- /dev/null +++ b/apps/sim/triggers/jotform/index.ts @@ -0,0 +1 @@ +export { jotformWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/jotform/webhook.ts b/apps/sim/triggers/jotform/webhook.ts new file mode 100644 index 0000000000..db69b9da84 --- /dev/null +++ b/apps/sim/triggers/jotform/webhook.ts @@ -0,0 +1,98 @@ +import { JotformIcon } from '@/components/icons' +import type { TriggerConfig } from '@/triggers/types' + +export const jotformWebhookTrigger: TriggerConfig = { + id: 'jotform_webhook', + name: 'Jotform Webhook', + provider: 'jotform', + description: 'Trigger workflow when a Jotform submission is received', + version: '1.0.0', + icon: JotformIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + }, + { + id: 'formId', + title: 'Form ID', + type: 'short-input', + placeholder: 'Enter your Jotform form ID', + description: + 'The unique identifier for your Jotform. Find it in the form URL (e.g., https://form.jotform.com/241234567890 → Form ID is 241234567890).', + required: true, + mode: 'trigger', + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: [ + 'Copy the Webhook URL above', + 'Go to your Jotform form settings at https://www.jotform.com/myforms/', + 'Select your form and click on "Settings"', + 'Navigate to "Integrations" and search for "Webhooks"', + 'Add a new webhook and paste the URL copied from above', + 'Click "Complete Integration" to save', + 'Note: Jotform will send a POST request to your webhook URL with form submission data', + ] + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join(''), + mode: 'trigger', + }, + { + id: 'triggerSave', + title: '', + type: 'trigger-save', + hideFromPreview: true, + mode: 'trigger', + triggerId: 'jotform_webhook', + }, + ], + + outputs: { + 'webhook.data.payload.slug': { + type: 'string', + description: 'Submission slug (e.g., submit/253605100868051)', + }, + 'webhook.data.payload.event_id': { + type: 'string', + description: 'Unique event identifier for this submission', + }, + 'webhook.data.payload.submitDate': { + type: 'string', + description: 'Unix timestamp when the form was submitted', + }, + 'webhook.data.payload.submitSource': { + type: 'string', + description: 'Source of submission (e.g., form)', + }, + 'webhook.data.payload.timeToSubmit': { + type: 'string', + description: 'Time taken to submit the form in seconds', + }, + 'webhook.data.payload': { + type: 'json', + description: + 'Complete webhook payload from Jotform. Access form fields using their IDs (e.g., webhook.data.payload.q3_q3_email1 for email, webhook.data.payload.q2_q2_fullname0.first for first name). Field IDs vary by form structure.', + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 942ff0e018..33a008e719 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -63,6 +63,7 @@ import { jiraWebhookTrigger, jiraWorklogCreatedTrigger, } from '@/triggers/jira' +import { jotformWebhookTrigger } from '@/triggers/jotform' import { linearCommentCreatedTrigger, linearCommentUpdatedTrigger, @@ -136,6 +137,7 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { jira_issue_deleted: jiraIssueDeletedTrigger, jira_issue_commented: jiraIssueCommentedTrigger, jira_worklog_created: jiraWorklogCreatedTrigger, + jotform_webhook: jotformWebhookTrigger, linear_webhook: linearWebhookTrigger, linear_issue_created: linearIssueCreatedTrigger, linear_issue_updated: linearIssueUpdatedTrigger, diff --git a/helm/sim/templates/networkpolicy.yaml b/helm/sim/templates/networkpolicy.yaml index deac5a5dba..7ef8697417 100644 --- a/helm/sim/templates/networkpolicy.yaml +++ b/helm/sim/templates/networkpolicy.yaml @@ -141,6 +141,10 @@ spec: ports: - protocol: TCP port: 443 + # Allow custom egress rules + {{- with .Values.networkPolicy.egress }} + {{- toYaml . | nindent 2 }} + {{- end }} {{- end }} {{- if .Values.postgresql.enabled }}