Skip to content

Commit 6262503

Browse files
feat(deployed-form): added deployed form input (#2679)
* feat(deployed-form): added deployed form input * styling consolidation, finishing touches on form * updated docs * remove unused files with knip * added more form fields * consolidated more test utils * remove unused/unneeded zustand stores, refactored stores for consistency * improvement(files): uncolorized plan name * feat(emcn): button-group * feat(emcn): tag input, tooltip shortcut * improvement(emcn): modal padding, api, chat, form * fix: deleted migrations * feat(form): added migrations * fix(emcn): tag input * fix: failing tests on build * add suplementary hover and fix bg color in date picker * fix: build errors --------- Co-authored-by: Emir Karabeg <[email protected]>
1 parent 6744043 commit 6262503

File tree

345 files changed

+17460
-7970
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

345 files changed

+17460
-7970
lines changed

.cursor/rules/sim-testing.mdc

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,57 @@
11
---
2-
description: Testing patterns with Vitest
2+
description: Testing patterns with Vitest and @sim/testing
33
globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
44
---
55

66
# Testing Patterns
77

8-
Use Vitest. Test files live next to source: `feature.ts` → `feature.test.ts`
8+
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
99

1010
## Structure
1111

1212
```typescript
1313
/**
14-
* Tests for [feature name]
15-
*
1614
* @vitest-environment node
1715
*/
16+
import { databaseMock, loggerMock } from '@sim/testing'
17+
import { describe, expect, it, vi } from 'vitest'
1818

19-
// 1. Mocks BEFORE imports
20-
vi.mock('@sim/db', () => ({ db: { select: vi.fn() } }))
19+
vi.mock('@sim/db', () => databaseMock)
2120
vi.mock('@sim/logger', () => loggerMock)
2221

23-
// 2. Imports AFTER mocks
24-
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
25-
import { createSession, loggerMock } from '@sim/testing'
2622
import { myFunction } from '@/lib/feature'
2723

2824
describe('myFunction', () => {
2925
beforeEach(() => vi.clearAllMocks())
30-
31-
it('should do something', () => {
32-
expect(myFunction()).toBe(expected)
33-
})
34-
35-
it.concurrent('runs in parallel', () => { ... })
26+
it.concurrent('isolated tests run in parallel', () => { ... })
3627
})
3728
```
3829

3930
## @sim/testing Package
4031

41-
```typescript
42-
// Factories - create test data
43-
import { createBlock, createWorkflow, createSession } from '@sim/testing'
32+
Always prefer over local mocks.
4433

45-
// Mocks - pre-configured mocks
46-
import { loggerMock, databaseMock, fetchMock } from '@sim/testing'
47-
48-
// Builders - fluent API for complex objects
49-
import { ExecutionBuilder, WorkflowBuilder } from '@sim/testing'
50-
```
34+
| Category | Utilities |
35+
|----------|-----------|
36+
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
37+
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
38+
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
39+
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
5140

5241
## Rules
5342

5443
1. `@vitest-environment node` directive at file top
55-
2. **Mocks before imports** - `vi.mock()` calls must come first
56-
3. Use `@sim/testing` factories over manual test data
57-
4. `it.concurrent` for independent tests (faster)
44+
2. `vi.mock()` calls before importing mocked modules
45+
3. `@sim/testing` utilities over local mocks
46+
4. `it.concurrent` for isolated tests (no shared mutable state)
5847
5. `beforeEach(() => vi.clearAllMocks())` to reset state
59-
6. Group related tests with nested `describe` blocks
60-
7. Test file naming: `*.test.ts` (not `*.spec.ts`)
48+
49+
## Hoisted Mocks
50+
51+
For mutable mock references:
52+
53+
```typescript
54+
const mockFn = vi.hoisted(() => vi.fn())
55+
vi.mock('@/lib/module', () => ({ myFunction: mockFn }))
56+
mockFn.mockResolvedValue({ data: 'test' })
57+
```

CLAUDE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,21 +173,21 @@ Use Vitest. Test files: `feature.ts` → `feature.test.ts`
173173
/**
174174
* @vitest-environment node
175175
*/
176+
import { databaseMock, loggerMock } from '@sim/testing'
177+
import { describe, expect, it, vi } from 'vitest'
176178

177-
// Mocks BEFORE imports
178-
vi.mock('@sim/db', () => ({ db: { select: vi.fn() } }))
179+
vi.mock('@sim/db', () => databaseMock)
180+
vi.mock('@sim/logger', () => loggerMock)
179181

180-
// Imports AFTER mocks
181-
import { describe, expect, it, vi } from 'vitest'
182-
import { createSession, loggerMock } from '@sim/testing'
182+
import { myFunction } from '@/lib/feature'
183183

184184
describe('feature', () => {
185185
beforeEach(() => vi.clearAllMocks())
186186
it.concurrent('runs in parallel', () => { ... })
187187
})
188188
```
189189

190-
Use `@sim/testing` factories over manual test data.
190+
Use `@sim/testing` mocks/factories over local test data. See `.cursor/rules/sim-testing.mdc` for details.
191191

192192
## Utils Rules
193193

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
title: Form Deployment
3+
---
4+
5+
import { Callout } from 'fumadocs-ui/components/callout'
6+
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
7+
8+
Deploy your workflow as an embeddable form that users can fill out on your website or share via link. Form submissions trigger your workflow with the `form` trigger type.
9+
10+
## Overview
11+
12+
Form deployment turns your workflow's Input Format into a responsive form that can be:
13+
- Shared via a direct link (e.g., `https://sim.ai/form/my-survey`)
14+
- Embedded in any website using an iframe
15+
16+
When a user submits the form, it triggers your workflow with the form data.
17+
18+
<Callout type="info">
19+
Forms derive their fields from your workflow's Start block Input Format. Each field becomes a form input with the appropriate type.
20+
</Callout>
21+
22+
## Creating a Form
23+
24+
1. Open your workflow and click **Deploy**
25+
2. Select the **Form** tab
26+
3. Configure:
27+
- **URL**: Unique identifier (e.g., `contact-form``sim.ai/form/contact-form`)
28+
- **Title**: Form heading
29+
- **Description**: Optional subtitle
30+
- **Form Fields**: Customize labels and descriptions for each field
31+
- **Authentication**: Public, password-protected, or email whitelist
32+
- **Thank You Message**: Shown after submission
33+
4. Click **Launch**
34+
35+
## Field Type Mapping
36+
37+
| Input Format Type | Form Field |
38+
|------------------|------------|
39+
| `string` | Text input |
40+
| `number` | Number input |
41+
| `boolean` | Toggle switch |
42+
| `object` | JSON editor |
43+
| `array` | JSON array editor |
44+
| `files` | File upload |
45+
46+
## Access Control
47+
48+
| Mode | Description |
49+
|------|-------------|
50+
| **Public** | Anyone with the link can submit |
51+
| **Password** | Users must enter a password |
52+
| **Email Whitelist** | Only specified emails/domains can submit |
53+
54+
For email whitelist:
55+
56+
- Domain: `@example.com` (all emails from domain)
57+
58+
## Embedding
59+
60+
### Direct Link
61+
62+
```
63+
https://sim.ai/form/your-identifier
64+
```
65+
66+
### Iframe
67+
68+
```html
69+
<iframe
70+
src="https://sim.ai/form/your-identifier"
71+
width="100%"
72+
height="600"
73+
frameborder="0"
74+
title="Form"
75+
></iframe>
76+
```
77+
78+
## API Submission
79+
80+
Submit forms programmatically:
81+
82+
<Tabs items={['cURL', 'TypeScript']}>
83+
<Tab value="cURL">
84+
```bash
85+
curl -X POST https://sim.ai/api/form/your-identifier \
86+
-H "Content-Type: application/json" \
87+
-d '{
88+
"formData": {
89+
"name": "John Doe",
90+
"email": "[email protected]"
91+
}
92+
}'
93+
```
94+
</Tab>
95+
<Tab value="TypeScript">
96+
```typescript
97+
const response = await fetch('https://sim.ai/api/form/your-identifier', {
98+
method: 'POST',
99+
headers: { 'Content-Type': 'application/json' },
100+
body: JSON.stringify({
101+
formData: {
102+
name: 'John Doe',
103+
104+
}
105+
})
106+
});
107+
108+
const result = await response.json();
109+
// { success: true, data: { executionId: '...' } }
110+
```
111+
</Tab>
112+
</Tabs>
113+
114+
### Protected Forms
115+
116+
For password-protected forms:
117+
```bash
118+
curl -X POST https://sim.ai/api/form/your-identifier \
119+
-H "Content-Type: application/json" \
120+
-d '{ "password": "secret", "formData": { "name": "John" } }'
121+
```
122+
123+
For email-protected forms:
124+
```bash
125+
curl -X POST https://sim.ai/api/form/your-identifier \
126+
-H "Content-Type: application/json" \
127+
-d '{ "email": "[email protected]", "formData": { "name": "John" } }'
128+
```
129+
130+
## Troubleshooting
131+
132+
**"No input fields configured"** - Add Input Format fields to your Start block.
133+
134+
**Form not loading in iframe** - Check your site's CSP allows iframes from `sim.ai`.
135+
136+
**Submissions failing** - Verify the identifier is correct and required fields are filled.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"pages": ["index", "basics", "api", "logging", "costs"]
2+
"pages": ["index", "basics", "api", "form", "logging", "costs"]
33
}

apps/docs/content/docs/en/triggers/start.mdx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Reference structured values downstream with expressions such as <code>&lt;start.
4444

4545
## How it behaves per entry point
4646

47-
<Tabs items={['Editor run', 'Deploy to API', 'Deploy to chat']}>
47+
<Tabs items={['Editor run', 'Deploy to API', 'Deploy to chat', 'Deploy to form']}>
4848
<Tab>
4949
When you click <strong>Run</strong> in the editor, the Start block renders the Input Format as a form. Default values make it easy to retest without retyping data. Submitting the form triggers the workflow immediately and the values become available on <code>&lt;start.fieldName&gt;</code> (for example <code>&lt;start.sampleField&gt;</code>).
5050

@@ -64,6 +64,13 @@ Reference structured values downstream with expressions such as <code>&lt;start.
6464

6565
If you launch chat with additional structured context (for example from an embed), it merges into the corresponding <code>&lt;start.fieldName&gt;</code> outputs, keeping downstream blocks consistent with API and manual runs.
6666
</Tab>
67+
<Tab>
68+
Form deployments render the Input Format as a standalone, embeddable form page. Each field becomes a form input with appropriate UI controls—text inputs for strings, number inputs for numbers, toggle switches for booleans, and file upload zones for files.
69+
70+
When a user submits the form, values become available on <code>&lt;start.fieldName&gt;</code> just like other entry points. The workflow executes with trigger type <code>form</code>, and submitters see a customizable thank-you message upon completion.
71+
72+
Forms can be embedded via iframe or shared as direct links, making them ideal for surveys, contact forms, and data collection workflows.
73+
</Tab>
6774
</Tabs>
6875

6976
## Referencing Start data downstream
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use client'
2+
3+
import { forwardRef, useState } from 'react'
4+
import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
5+
import { Button, type ButtonProps as EmcnButtonProps } from '@/components/emcn'
6+
import { cn } from '@/lib/core/utils/cn'
7+
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
8+
9+
export interface BrandedButtonProps extends Omit<EmcnButtonProps, 'variant' | 'size'> {
10+
/** Shows loading spinner and disables button */
11+
loading?: boolean
12+
/** Text to show when loading (appends "..." automatically) */
13+
loadingText?: string
14+
/** Show arrow animation on hover (default: true) */
15+
showArrow?: boolean
16+
/** Make button full width (default: true) */
17+
fullWidth?: boolean
18+
}
19+
20+
/**
21+
* Branded button for auth and status pages.
22+
* Automatically detects whitelabel customization and applies appropriate styling.
23+
*
24+
* @example
25+
* ```tsx
26+
* // Primary branded button with arrow
27+
* <BrandedButton onClick={handleSubmit}>Sign In</BrandedButton>
28+
*
29+
* // Loading state
30+
* <BrandedButton loading loadingText="Signing in">Sign In</BrandedButton>
31+
*
32+
* // Without arrow animation
33+
* <BrandedButton showArrow={false}>Continue</BrandedButton>
34+
* ```
35+
*/
36+
export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
37+
(
38+
{
39+
children,
40+
loading = false,
41+
loadingText,
42+
showArrow = true,
43+
fullWidth = true,
44+
className,
45+
disabled,
46+
onMouseEnter,
47+
onMouseLeave,
48+
...props
49+
},
50+
ref
51+
) => {
52+
const buttonClass = useBrandedButtonClass()
53+
const [isHovered, setIsHovered] = useState(false)
54+
55+
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
56+
setIsHovered(true)
57+
onMouseEnter?.(e)
58+
}
59+
60+
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
61+
setIsHovered(false)
62+
onMouseLeave?.(e)
63+
}
64+
65+
return (
66+
<Button
67+
ref={ref}
68+
variant='branded'
69+
size='branded'
70+
disabled={disabled || loading}
71+
onMouseEnter={handleMouseEnter}
72+
onMouseLeave={handleMouseLeave}
73+
className={cn(buttonClass, 'group', fullWidth && 'w-full', className)}
74+
{...props}
75+
>
76+
{loading ? (
77+
<span className='flex items-center gap-2'>
78+
<Loader2 className='h-4 w-4 animate-spin' />
79+
{loadingText ? `${loadingText}...` : children}
80+
</span>
81+
) : showArrow ? (
82+
<span className='flex items-center gap-1'>
83+
{children}
84+
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
85+
{isHovered ? (
86+
<ArrowRight className='h-4 w-4' aria-hidden='true' />
87+
) : (
88+
<ChevronRight className='h-4 w-4' aria-hidden='true' />
89+
)}
90+
</span>
91+
</span>
92+
) : (
93+
children
94+
)}
95+
</Button>
96+
)
97+
}
98+
)
99+
100+
BrandedButton.displayName = 'BrandedButton'

apps/sim/app/(auth)/components/sso-login-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function SSOLoginButton({
3434
}
3535

3636
const primaryBtnClasses = cn(
37-
primaryClassName || 'auth-button-gradient',
37+
primaryClassName || 'branded-button-gradient',
3838
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
3939
)
4040

0 commit comments

Comments
 (0)