Skip to content
Merged
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
29 changes: 29 additions & 0 deletions apps/docs/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9140,6 +9140,35 @@ export function FlowiseIcon(props: SVGProps<SVGSVGElement>) {
)
}

export function JotformIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='147 132 306 336'
fill='none'
role='img'
xmlns='http://www.w3.org/2000/svg'
>
<path
fill='#0A1551'
d='M231.287 450.612C237.601 456.733 233.139 467.221 224.173 467.221H168.06C156.989 467.221 147.98 458.488 147.98 447.756V393.358C147.98 384.666 158.799 380.341 165.113 386.462L231.287 450.612Z'
/>
<path
fill='#FFB629'
d='M319.003 454.845C302.393 438.343 302.394 411.589 319.003 395.088L378.947 335.535C395.557 319.033 422.486 319.033 439.096 335.535C455.705 352.036 455.705 378.79 439.096 395.292L379.152 454.845C362.542 471.346 335.613 471.346 319.003 454.845Z'
/>
<path
fill='#0099FF'
d='M160.64 305.204C144.031 288.703 144.031 261.949 160.64 245.447L261.52 145.155C278.129 128.653 305.059 128.653 321.669 145.155C338.278 161.656 338.278 188.41 321.669 204.912L220.789 305.204C204.179 321.705 177.25 321.705 160.64 305.204Z'
/>
<path
fill='#FF6100'
d='M243.108 376.686C226.498 360.185 226.498 333.43 243.108 316.929L379.414 181.511C396.024 165.009 422.953 165.009 439.563 181.511C456.173 198.012 456.173 224.766 439.563 241.268L303.256 376.686C286.647 393.187 259.717 393.187 243.108 376.686Z'
/>
</svg>
)
}

export function JupyterIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 44 51' xmlns='http://www.w3.org/2000/svg'>
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import {
JinaAIIcon,
JiraIcon,
JiraServiceManagementIcon,
JotformIcon,
JupyterIcon,
KalshiIcon,
KetchIcon,
Expand Down Expand Up @@ -402,6 +403,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
jina: JinaAIIcon,
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
jotform: JotformIcon,
jsm: JiraServiceManagementIcon,
jupyter: JupyterIcon,
kalshi: KalshiIcon,
Expand Down
1,176 changes: 1,176 additions & 0 deletions apps/docs/content/docs/en/integrations/jotform.mdx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/docs/content/docs/en/integrations/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"jina",
"jira",
"jira_service_management",
"jotform",
"jupyter",
"kalshi",
"ketch",
Expand Down
202 changes: 202 additions & 0 deletions apps/sim/blocks/blocks/jotform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { JotformBlock } from '@/blocks/blocks/jotform'

/**
* Every assertion here runs against `{ ...inputs, ...buildParams(inputs) }`, the
* shape the generic tool handler actually forwards. A key the mapper omits is
* *not* dropped by that merge — the raw subBlock value survives — so asserting
* on the mapper's return alone would prove nothing about what the tool receives.
*/
describe('JotformBlock', () => {
const buildParams = JotformBlock.tools.config.params!
const selectTool = JotformBlock.tools.config.tool!

const operationIds =
JotformBlock.subBlocks
.find((subBlock) => subBlock.id === 'operation')
?.options?.map((option) => (option as { id: string }).id) ?? []

it('maps every dropdown operation onto a registered tool', () => {
expect(operationIds).toHaveLength(43)
expect(new Set(operationIds.map((id) => selectTool({ operation: id })))).toEqual(
new Set(JotformBlock.tools.access)
)
})

it('declares an input for every subblock', () => {
const inputIds = new Set(Object.keys(JotformBlock.inputs))
const missing = JotformBlock.subBlocks
.map((subBlock) => subBlock.id)
.filter((id) => !inputIds.has(id))

expect(missing).toEqual([])
})

it('gives every subblock a unique id', () => {
const ids = JotformBlock.subBlocks.map((subBlock) => subBlock.id)
expect(new Set(ids).size).toBe(ids.length)
})

/**
* `properties`, `text`, `order`, `name`, `title`, and `fields` are tool params on
* one operation and would be meaningless on another. Naming a subblock after one
* of them would leak its stale value into every other operation through the raw
* merge, so each is deliberately carried by a prefixed subblock instead.
*/
it('never names a subblock after a param another operation owns', () => {
const contested = ['properties', 'questions', 'emails', 'text', 'order', 'name', 'title']
const collisions = JotformBlock.subBlocks
.map((subBlock) => subBlock.id)
.filter((id) => contested.includes(id))

expect(collisions).toEqual([])
})

it('renames the create-form subblocks onto the tool params', () => {
const inputs = {
operation: 'create_form',
apiKey: 'key',
newFormQuestions: '[{"type":"control_email","text":"Email","order":"1","name":"email"}]',
newFormProperties: '{"title":"Contact Us"}',
newFormEmails: '[{"type":"notification","to":"team@example.com"}]',
}
const finalInputs = { ...inputs, ...buildParams(inputs) }

expect(finalInputs.questions).toBe(inputs.newFormQuestions)
expect(finalInputs.properties).toBe(inputs.newFormProperties)
expect(finalInputs.emails).toBe(inputs.newFormEmails)
})

it('renames the question subblocks onto the tool params', () => {
const inputs = {
operation: 'create_question',
apiKey: 'key',
formId: '2315',
questionType: 'control_email',
questionText: 'Your email',
questionOrder: '2',
questionName: 'yourEmail',
}
const finalInputs = { ...inputs, ...buildParams(inputs) }

expect(finalInputs.text).toBe('Your email')
expect(finalInputs.order).toBe('2')
expect(finalInputs.name).toBe('yourEmail')
expect(finalInputs.questionType).toBe('control_email')
})

it('renames the report subblocks onto the tool params', () => {
const inputs = {
operation: 'create_report',
apiKey: 'key',
formId: '2315',
reportTitle: 'Weekly responses',
reportType: 'csv',
reportFields: 'ip,dt,3,4',
}
const finalInputs = { ...inputs, ...buildParams(inputs) }

expect(finalInputs.title).toBe('Weekly responses')
expect(finalInputs.listType).toBe('csv')
expect(finalInputs.fields).toBe('ip,dt,3,4')
})

it('renames the bulk subblocks onto the tool params', () => {
const submissions = {
operation: 'create_submissions',
apiKey: 'key',
formId: '2315',
bulkSubmissions: '[{"1":{"text":"a"}}]',
}
expect({ ...submissions, ...buildParams(submissions) }.submissions).toBe(
submissions.bulkSubmissions
)

const questions = {
operation: 'create_questions',
apiKey: 'key',
formId: '2315',
bulkQuestions: '[{"type":"control_head"}]',
}
expect({ ...questions, ...buildParams(questions) }.questions).toBe(questions.bulkQuestions)
})

/**
* `create_form` and `create_questions` both feed a `questions` tool param from
* different subblocks. A leftover value from one must not arrive as the other.
*/
it('keeps the two questions sources from bleeding into each other', () => {
const inputs = {
operation: 'create_questions',
apiKey: 'key',
formId: '2315',
bulkQuestions: '[{"type":"control_head"}]',
newFormQuestions: '[{"type":"control_email"}]',
}

expect({ ...inputs, ...buildParams(inputs) }.questions).toBe(inputs.bulkQuestions)
})

it('renames the history subblocks onto the tool params', () => {
const inputs = {
operation: 'get_history',
apiKey: 'key',
historyAction: 'formCreation',
historySortBy: 'ASC',
historyStartDate: '01/01/2026',
historyEndDate: '02/01/2026',
}
const finalInputs = { ...inputs, ...buildParams(inputs) }

expect(finalInputs.action).toBe('formCreation')
expect(finalInputs.sortBy).toBe('ASC')
expect(finalInputs.startDate).toBe('01/01/2026')
expect(finalInputs.endDate).toBe('02/01/2026')
})

/**
* A leftover value from a previously selected operation still reaches the mapper.
* The rename must not fire for the operation that does not own it, or a stale
* report title would arrive as a question label.
*/
it('leaves renames untouched for operations that do not own them', () => {
const inputs = {
operation: 'list_form_submissions',
apiKey: 'key',
formId: '2315',
reportTitle: 'Left over from an earlier operation',
questionText: 'Also left over',
}
const finalInputs = { ...inputs, ...buildParams(inputs) }

expect(finalInputs.title).toBeUndefined()
expect(finalInputs.text).toBeUndefined()
expect(finalInputs.listType).toBeUndefined()
})

it('marks the identifier each operation addresses as required', () => {
const requiredFor = (id: string) => {
const subBlock = JotformBlock.subBlocks.find((candidate) => candidate.id === id)
const required = subBlock?.required as { field: string; value: string | string[] } | undefined
const value = required?.value ?? []
return Array.isArray(value) ? value : [value]
}

expect(requiredFor('formId')).toContain('list_form_submissions')
expect(requiredFor('submissionId')).toEqual([
'get_submission',
'update_submission',
'delete_submission',
])
expect(requiredFor('questionId')).toEqual([
'get_question',
'update_question',
'delete_question',
])
expect(requiredFor('reportId')).toEqual(['get_report', 'delete_report'])
expect(requiredFor('webhookId')).toEqual(['delete_webhook'])
})
})
Loading
Loading