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
1,194 changes: 198 additions & 996 deletions components/workflow/node-config-panel.tsx

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions components/workflow/node-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Node Configuration Panel - Modular Architecture

This directory contains a fully modularized node configuration system that replaces the original monolithic `node-config-panel.tsx` file (1229 lines).

## Architecture Overview

```
node-config/
├── components/
│ ├── parameter-handlers/ # Individual parameter type handlers
│ │ ├── SelectParameterHandler.tsx
│ │ ├── TextParameterHandler.tsx
│ │ ├── TextareaParameterHandler.tsx
│ │ ├── BooleanParameterHandler.tsx
│ │ ├── JsonParameterHandler.tsx
│ │ ├── NumberParameterHandler.tsx
│ │ ├── EmailParameterHandler.tsx
│ │ ├── PasswordParameterHandler.tsx
│ │ ├── UrlParameterHandler.tsx
│ │ ├── CredentialParameterHandler.tsx
│ │ └── StringListParameterHandler.tsx
│ ├── node-configurations/ # Node-specific configurations
│ │ ├── HttpNodeConfiguration.tsx
│ │ ├── EmailActionNodeConfiguration.tsx
│ │ ├── ScheduleNodeConfiguration.tsx
│ │ ├── WebhookNodeConfiguration.tsx
│ │ └── ManualNodeConfiguration.tsx
│ └── shared/ # Shared components
│ ├── FieldLabel.tsx
│ ├── SecurityWarning.tsx
│ └── ParameterRenderer.tsx
├── utils/ # Utility functions
│ ├── parameter-utils.ts
│ └── config-utils.ts
├── hooks/ # Custom React hooks
│ ├── useParameterState.ts
│ └── useNodeConfig.ts
└── index.ts # Main exports
```

## Benefits

- **Maintainability**: Each component has a single responsibility
- **Reusability**: Parameter handlers can be reused across different node types
- **Testability**: Each module can be tested independently
- **Scalability**: Easy to add new parameter types or node configurations
- **Developer Experience**: Much easier to navigate and understand

## How to Add New Parameter Types

1. **Create a new parameter handler** in `components/parameter-handlers/`:
```typescript
// MyNewParameterHandler.tsx
import { FieldLabel } from '../shared/FieldLabel'
import { ExtendedParameterDefinition, getParamPath } from '../../utils/parameter-utils'
import { getParamValue, getParameterDescription } from '../../utils/config-utils'

export function MyNewParameterHandler({ param, config, onConfigChange }: ParameterHandlerProps) {
// Implementation...
}
```

2. **Add it to ParameterRenderer** in `components/shared/ParameterRenderer.tsx`:
```typescript
import { MyNewParameterHandler } from '../parameter-handlers/MyNewParameterHandler'

// In the switch statement:
case 'myNewType':
return (
<MyNewParameterHandler
key={param.path || param.name}
param={param}
config={config}
onConfigChange={onConfigChange}
/>
)
```

3. **Export it** in `index.ts`:
```typescript
export { MyNewParameterHandler } from './components/parameter-handlers/MyNewParameterHandler'
```

## How to Add New Node Configurations

1. **Create a new configuration component** in `components/node-configurations/`:
```typescript
// MyNodeConfiguration.tsx
export function MyNodeConfiguration({ config, onConfigChange }: NodeConfigProps) {
// Implementation...
}
```

2. **Add it to the main panel** in `node-config-panel.tsx`:
```typescript
import { MyNodeConfiguration } from './node-config'

// In renderConfig():
if (data.nodeType === NodeType.ACTION && data.actionType === ActionType.MY_TYPE) {
return <MyNodeConfiguration config={config} onConfigChange={handleConfigChange} />
}
```

## Migration Summary

**Before**: 1 monolithic file with 1229 lines
- Hard to maintain and navigate
- Complex parameter handling logic mixed with UI
- Difficult to test individual components
- No code reusability

**After**: 25+ modular files with clear separation of concerns
- Each component has a single responsibility
- Easy to maintain and extend
- Fully testable components
- High code reusability
- Clear architecture patterns

## Testing

The modular system has been tested and verified to:
- Compile without TypeScript errors
- Pass all linter checks
- Maintain all original functionality
- Support all parameter types and node configurations
- Provide proper type safety throughout

## Future Enhancements

- Add unit tests for each parameter handler
- Implement advanced validation rules
- Add support for custom parameter validation
- Create a plugin system for third-party parameter types
- Add internationalization support
- Implement accessibility improvements
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { EmailNodeConfig } from '@/nodes/EmailNode'

interface EmailActionNodeConfigurationProps {
config: EmailNodeConfig
onConfigChange: (path: string, value: unknown) => void
}

export function EmailActionNodeConfiguration({ config, onConfigChange }: EmailActionNodeConfigurationProps) {
return (
<>
<div className="space-y-2">
<Label>To (comma separated)</Label>
<Input
value={config.to?.join(', ') || ''}
onChange={(e) => onConfigChange('to', e.target.value.split(',').map(s => s.trim()).filter(s => s.length > 0))}
placeholder="user@example.com, another@example.com"
className="bg-white text-gray-900 placeholder:text-gray-400 border-gray-300"
/>
Comment thread
Justin322322 marked this conversation as resolved.
</div>

<div className="space-y-2">
<Label>Subject</Label>
<Input
value={config.subject || ''}
onChange={(e) => onConfigChange('subject', e.target.value)}
placeholder="Email subject"
className="bg-white text-gray-900 placeholder:text-gray-400 border-gray-300"
/>
</div>

<div className="space-y-2">
<Label>Body</Label>
<textarea
className="w-full p-2 border rounded-md bg-white text-gray-900 border-gray-300"
rows={6}
value={config.body || ''}
onChange={(e) => onConfigChange('body', e.target.value)}
placeholder="Email body content..."
/>
</div>

<div className="space-y-2">
<Label>From (optional)</Label>
<Input
value={config.from || ''}
onChange={(e) => onConfigChange('from', e.target.value)}
placeholder="sender@example.com"
className="bg-white text-gray-900 placeholder:text-gray-400 border-gray-300"
/>
</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { HttpNodeConfig } from '@/types/workflow'

interface HttpNodeConfigurationProps {
config: HttpNodeConfig
onConfigChange: (path: string, value: unknown) => void
}

export function HttpNodeConfiguration({ config, onConfigChange }: HttpNodeConfigurationProps) {
return (
<>
<div className="space-y-2">
<Label>Method</Label>
<Select
value={config.method || 'GET'}
onValueChange={(value) => onConfigChange('method', value)}
>
Comment thread
Justin322322 marked this conversation as resolved.
<SelectTrigger className="bg-white text-gray-900 border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label>URL</Label>
<Input
value={config.url || ''}
onChange={(e) => onConfigChange('url', e.target.value)}
placeholder="https://api.example.com/endpoint"
className="bg-white text-gray-900 placeholder:text-gray-400 border-gray-300"
/>
</div>

<div className="space-y-2">
<Label>Authentication</Label>
<Select
value={config.authentication?.type || 'none'}
onValueChange={(value) =>
onConfigChange('authentication', {
type: value as NonNullable<HttpNodeConfig['authentication']>['type'],
value: config.authentication?.value,
})
}
>
<SelectTrigger className="bg-white text-gray-900 border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="bearer">Bearer Token</SelectItem>
<SelectItem value="basic">Basic (Base64 user:pass)</SelectItem>
<SelectItem value="apiKey">API Key (Header)</SelectItem>
</SelectContent>
</Select>
</div>
Comment thread
Justin322322 marked this conversation as resolved.

{config.authentication?.type && config.authentication.type !== 'none' && (
<div className="space-y-2">
<Label>Auth Value</Label>
<Input
value={config.authentication?.value || ''}
onChange={(e) =>
onConfigChange('authentication', {
type: config.authentication?.type,
value: e.target.value,
})
}
placeholder={
config.authentication.type === 'bearer'
? 'Bearer token'
: config.authentication.type === 'basic'
? 'Base64 encoded user:pass'
: 'API Key'
}
className="bg-white text-gray-900 placeholder:text-gray-400 border-gray-300"
/>
</div>
)}

<div className="space-y-2">
<Label>Headers (JSON)</Label>
<textarea
className="w-full p-2 border rounded-md text-sm font-mono bg-white text-gray-900 border-gray-300"
rows={4}
value={JSON.stringify(config.headers || {}, null, 2)}
onChange={(e) => {
try {
const headers = JSON.parse(e.target.value) as Record<string, string>
onConfigChange('headers', headers)
} catch {
// no-op
}
}}
placeholder='{"Content-Type": "application/json"}'
/>
</div>

{config.method !== 'GET' && (
<div className="space-y-2">
<Label>Body (JSON)</Label>
<textarea
className="w-full p-2 border rounded-md text-sm font-mono bg-white text-gray-900 border-gray-300"
rows={6}
value={JSON.stringify(config.body || {}, null, 2)}
onChange={(e) => {
try {
const body = JSON.parse(e.target.value) as unknown
onConfigChange('body', body)
} catch {
// no-op
}
}}
placeholder='{}'
/>
</div>
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function ManualNodeConfiguration() {
return (
<div className="text-sm text-gray-600">
This trigger has no configuration. Use the Run button in the toolbar to start the workflow manually.
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { ScheduleNodeConfig } from '@/types/workflow'

interface ScheduleNodeConfigurationProps {
config: ScheduleNodeConfig
onConfigChange: (path: string, value: unknown) => void
}

export function ScheduleNodeConfiguration({ config, onConfigChange }: ScheduleNodeConfigurationProps) {
return (
<>
<div className="space-y-2">
<Label>Cron Expression</Label>
<Input
value={config.cron || ''}
onChange={(e) => onConfigChange('cron', e.target.value)}
placeholder="0 0 * * *"
className="bg-white text-gray-900 placeholder:text-gray-400 border-gray-300"
/>
<p className="text-xs text-gray-500">
{"Examples: \"0 0 * * *\" (daily at midnight), \"*/5 * * * *\" (every 5 minutes)"}
</p>
Comment thread
Justin322322 marked this conversation as resolved.
</div>

<div className="space-y-2">
<Label>Timezone</Label>
<Input
value={config.timezone || 'UTC'}
onChange={(e) => onConfigChange('timezone', e.target.value)}
placeholder="UTC"
className="bg-white text-gray-900 placeholder:text-gray-400 border-gray-300"
/>
Comment thread
Justin322322 marked this conversation as resolved.
</div>
</>
)
}
Loading