Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions apps/sim/blocks/blocks/jotform.ts
Original file line number Diff line number Diff line change
@@ -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<JotformSubmissionsResponse> = {
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'],
},
}
2 changes: 2 additions & 0 deletions apps/sim/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -198,6 +199,7 @@ export const registry: Record<string, BlockConfig> = {
intercom: IntercomBlock,
jina: JinaBlock,
jira: JiraBlock,
jotform: JotformBlock,
kalshi: KalshiBlock,
knowledge: KnowledgeBlock,
linear: LinearBlock,
Expand Down
32 changes: 32 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,38 @@ export function TypeformIcon(props: SVGProps<SVGSVGElement>) {
)
}

export function JotformIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<rect
x='3'
y='3'
width='18'
height='18'
rx='2'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M7 8H17M7 12H17M7 16H13'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}

export function DocumentIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
Expand Down
24 changes: 24 additions & 0 deletions apps/sim/lib/webhooks/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,30 @@ export async function parseWebhookBody(
body = Object.fromEntries(formData.entries())
logger.debug(`[${requestId}] Parsed form-encoded webhook data (direct fields)`)
}
} else if (contentType.includes('multipart/form-data')) {
try {
const formData = await request.formData()
const rawRequest = formData.get('rawRequest')

if (rawRequest && typeof rawRequest === 'string') {
body = JSON.parse(rawRequest)
logger.debug(`[${requestId}] Parsed multipart/form-data webhook with rawRequest field`)
} else {
const formObject: Record<string, any> = {}
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`)
Expand Down
74 changes: 74 additions & 0 deletions apps/sim/tools/jotform/get_form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { JotformGetFormParams, JotformGetFormResponse } from '@/tools/jotform/types'
import type { ToolConfig } from '@/tools/types'

export const getFormTool: ToolConfig<JotformGetFormParams, JotformGetFormResponse> = {
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',
},
},
}
7 changes: 7 additions & 0 deletions apps/sim/tools/jotform/index.ts
Original file line number Diff line number Diff line change
@@ -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
Loading