Skip to content

Commit e40e7b2

Browse files
feat: added the initial version of workflow canvas component (#510)
* feat: added the initial version of workflow canvas component * chore: replaced interface with type * chore: fixed all the pr comments --------- Co-authored-by: vinit <vinit.khandal@juspay.in>
1 parent 5036d11 commit e40e7b2

22 files changed

+2133
-4
lines changed

apps/site/src/demos/SidebarDemo.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
Settings,
3333
TrendingUp,
3434
Upload,
35+
Workflow,
3536
} from 'lucide-react'
3637
import { FOUNDATION_THEME } from '../../../../packages/blend/lib/tokens'
3738
import { Sidebar } from '../../../../packages/blend/lib/components/Sidebar'
@@ -92,6 +93,7 @@ import SearchInputDemo from './SearchInputDemo'
9293
import VirtualListDemo from './VirtualListDemo'
9394
import UploadDemo from './UploadDemo'
9495
import CodeBlockDemo from './CodeBlockDemo'
96+
import WorkflowCanvasDemo from './WorkflowCanvasDemo'
9597

9698
const SidebarDemo = () => {
9799
const [activeComponent, setActiveComponent] = useState<
@@ -148,6 +150,7 @@ const SidebarDemo = () => {
148150
| 'virtualList'
149151
| 'upload'
150152
| 'codeBlock'
153+
| 'workflowCanvas'
151154
>('dataRangePicker')
152155

153156
const [activeTenant, setActiveTenant] = useState<string>('Juspay')
@@ -406,6 +409,8 @@ const SidebarDemo = () => {
406409
return <UploadDemo />
407410
case 'codeBlock':
408411
return <CodeBlockDemo />
412+
case 'workflowCanvas':
413+
return <WorkflowCanvasDemo />
409414
default:
410415
return (
411416
<div className="p-8">
@@ -836,6 +841,14 @@ const SidebarDemo = () => {
836841
),
837842
onClick: () => setActiveComponent('codeBlock'),
838843
},
844+
{
845+
label: 'Workflow Canvas',
846+
leftSlot: (
847+
<Workflow style={{ width: '16px', height: '16px' }} />
848+
),
849+
isSelected: activeComponent === 'workflowCanvas',
850+
onClick: () => setActiveComponent('workflowCanvas'),
851+
},
839852
],
840853
},
841854
{
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { useState, useCallback } from 'react'
2+
import {
3+
WorkflowCanvas,
4+
type BlendNode,
5+
type BlendEdge,
6+
ThemeProvider,
7+
} from '../../../../packages/blend/lib/main'
8+
import {
9+
Zap,
10+
Filter,
11+
Settings,
12+
Database,
13+
Mail,
14+
CheckCircle,
15+
XCircle,
16+
} from 'lucide-react'
17+
18+
const WorkflowCanvasDemo = () => {
19+
const [nodes, setNodes] = useState<BlendNode[]>([
20+
{
21+
id: '1',
22+
type: 'input',
23+
position: { x: 50, y: 200 },
24+
data: {
25+
label: 'Webhook Trigger',
26+
description: 'HTTP POST',
27+
icon: <Zap size={20} />,
28+
},
29+
},
30+
{
31+
id: '2',
32+
type: 'default',
33+
position: { x: 300, y: 120 },
34+
data: {
35+
label: 'Validate Request',
36+
description: 'Check payload',
37+
icon: <Filter size={20} />,
38+
},
39+
},
40+
{
41+
id: '3',
42+
type: 'default',
43+
position: { x: 300, y: 280 },
44+
data: {
45+
label: 'Transform Data',
46+
description: 'Map fields',
47+
icon: <Settings size={20} />,
48+
},
49+
},
50+
{
51+
id: '4',
52+
type: 'default',
53+
position: { x: 550, y: 120 },
54+
data: {
55+
label: 'Save to DB',
56+
description: 'PostgreSQL',
57+
icon: <Database size={20} />,
58+
},
59+
},
60+
{
61+
id: '5',
62+
type: 'default',
63+
position: { x: 550, y: 280 },
64+
data: {
65+
label: 'Send Email',
66+
description: 'Notify user',
67+
icon: <Mail size={20} />,
68+
},
69+
},
70+
{
71+
id: '6',
72+
type: 'output',
73+
position: { x: 800, y: 120 },
74+
data: {
75+
label: 'Completed',
76+
description: '200 OK',
77+
icon: <CheckCircle size={20} />,
78+
},
79+
},
80+
{
81+
id: '7',
82+
type: 'output',
83+
position: { x: 800, y: 280 },
84+
data: {
85+
label: 'Failed',
86+
description: '500 Error',
87+
icon: <XCircle size={20} />,
88+
},
89+
},
90+
])
91+
92+
const [edges, setEdges] = useState<BlendEdge[]>([
93+
{
94+
id: 'e1-2',
95+
source: '1',
96+
target: '2',
97+
data: { label: 'validated' },
98+
},
99+
{
100+
id: 'e1-3',
101+
source: '1',
102+
target: '3',
103+
data: { label: 'transformed' },
104+
},
105+
{
106+
id: 'e2-4',
107+
source: '2',
108+
target: '4',
109+
data: { label: 'saved' },
110+
},
111+
{
112+
id: 'e3-5',
113+
source: '3',
114+
target: '5',
115+
data: { label: 'sent' },
116+
},
117+
{
118+
id: 'e4-6',
119+
source: '4',
120+
target: '6',
121+
data: { label: 'success' },
122+
},
123+
{
124+
id: 'e5-7',
125+
source: '5',
126+
target: '7',
127+
data: { label: 'failed' },
128+
},
129+
])
130+
131+
const handleNodesChange = useCallback((updatedNodes: BlendNode[]) => {
132+
setNodes(updatedNodes)
133+
}, [])
134+
135+
const handleEdgesChange = useCallback((updatedEdges: BlendEdge[]) => {
136+
setEdges(updatedEdges)
137+
}, [])
138+
139+
const handleConnect = useCallback((connection: any) => {
140+
const newEdge: BlendEdge = {
141+
id: `e${connection.source}-${connection.target}`,
142+
source: connection.source,
143+
target: connection.target,
144+
data: { label: 'connected' },
145+
}
146+
setEdges((eds) => [...eds, newEdge])
147+
}, [])
148+
149+
return (
150+
<ThemeProvider>
151+
<div
152+
style={{ width: '100%', height: '100%', paddingBottom: '32px' }}
153+
>
154+
<WorkflowCanvas
155+
nodes={nodes}
156+
edges={edges}
157+
onNodesChange={handleNodesChange}
158+
onEdgesChange={handleEdgesChange}
159+
onConnect={handleConnect}
160+
height={750}
161+
fitView={true}
162+
showControls={true}
163+
showMinimap={true}
164+
showBackground={true}
165+
nodesDraggable={true}
166+
nodesConnectable={true}
167+
elementsSelectable={true}
168+
/>
169+
</div>
170+
</ThemeProvider>
171+
)
172+
}
173+
174+
export default WorkflowCanvasDemo
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# WorkflowCanvas Component
2+
3+
A powerful workflow visualization component built on top of React Flow, providing a token-based themeable canvas for creating node-based workflows.
4+
5+
## Architecture
6+
7+
The component follows a **component-specific CSS approach** for better maintainability and tree-shaking:
8+
9+
```
10+
WorkflowCanvas/
11+
├── WorkflowCanvas.tsx # Main component
12+
├── WorkflowCanvas.css # Component-specific CSS overrides
13+
├── workflow.tokens.ts # Token definitions for theming
14+
├── types.ts # TypeScript type definitions
15+
├── nodes/ # Custom node components
16+
├── edges/ # Custom edge components
17+
└── WorkflowControls.tsx # Control panel component
18+
```
19+
20+
## CSS Scoping Strategy
21+
22+
### Problem Solved
23+
24+
Previously, the component used `!important` flags to override ReactFlow's default styles globally, which caused issues when:
25+
26+
1. The consuming app already uses ReactFlow elsewhere
27+
2. Multiple WorkflowCanvas instances existed in the same app
28+
3. The consuming app had different styling requirements for ReactFlow
29+
30+
### Solution
31+
32+
The component now uses **scoped CSS** with the `.blend-workflow-canvas` class:
33+
34+
```css
35+
/* Only affects WorkflowCanvas instances, not other ReactFlow usage */
36+
.blend-workflow-canvas .react-flow__node {
37+
cursor: pointer;
38+
padding: 0;
39+
border: none;
40+
/* ... */
41+
}
42+
```
43+
44+
**Benefits:**
45+
46+
- ✅ No `!important` flags needed
47+
- ✅ Doesn't affect other ReactFlow instances in the app
48+
- ✅ Clean separation of concerns
49+
- ✅ Better CSS specificity management
50+
51+
## ReactFlow CSS Import
52+
53+
**Important:** This component **imports** `reactflow/dist/style.css` internally to ensure proper functionality.
54+
55+
### CSS Architecture
56+
57+
The component uses a hybrid approach:
58+
59+
1. **ReactFlow Base CSS**: Imported globally for core functionality
60+
2. **Scoped Overrides**: Custom styles in `WorkflowCanvas.css` scoped to `.blend-workflow-canvas`
61+
62+
This means:
63+
64+
- ✅ ReactFlow's base styles are available (required for proper rendering)
65+
- ✅ Our custom node/edge styles only affect WorkflowCanvas instances
66+
- ✅ Other ReactFlow instances in your app won't be affected by our overrides
67+
68+
### For Consuming Applications
69+
70+
No additional CSS imports needed! Just use the component:
71+
72+
```tsx
73+
import { WorkflowCanvas } from '@juspay/blend-design-system'
74+
75+
// WorkflowCanvas handles all CSS internally
76+
;<WorkflowCanvas nodes={nodes} edges={edges} />
77+
```
78+
79+
**Note**: If you're already using ReactFlow elsewhere in your app and have imported `reactflow/dist/style.css`, there's no conflict - the CSS will simply be imported once.
80+
81+
## Usage Example
82+
83+
```tsx
84+
import { WorkflowCanvas } from '@juspay/blend-design-system'
85+
import type { BlendNode, BlendEdge } from '@juspay/blend-design-system'
86+
87+
const MyWorkflow = () => {
88+
const nodes: BlendNode[] = [
89+
{
90+
id: '1',
91+
type: 'input',
92+
position: { x: 0, y: 0 },
93+
data: { label: 'Start' },
94+
},
95+
]
96+
97+
const edges: BlendEdge[] = []
98+
99+
return (
100+
<WorkflowCanvas
101+
nodes={nodes}
102+
edges={edges}
103+
height="600px"
104+
width="100%"
105+
showControls
106+
showMinimap
107+
/>
108+
)
109+
}
110+
```
111+
112+
## Styling Philosophy
113+
114+
1. **Token-based theming**: All colors, sizes, and spacing use the theme token system
115+
2. **Scoped overrides**: CSS overrides are scoped to `.blend-workflow-canvas`
116+
3. **No global pollution**: Doesn't affect other ReactFlow instances
117+
4. **Component-specific CSS**: Keeps styles colocated with the component
118+
119+
## Migration Guide
120+
121+
If you were using an older version with global CSS conflicts:
122+
123+
### Before
124+
125+
```tsx
126+
// Old version had global !important overrides
127+
<WorkflowCanvas className="my-custom-class" {...props} />
128+
// ^ This would conflict with other ReactFlow instances
129+
```
130+
131+
### After
132+
133+
```tsx
134+
// New version uses scoped CSS
135+
<WorkflowCanvas {...props} />
136+
// ^ Works alongside other ReactFlow instances without conflicts
137+
```
138+
139+
**Note:** The `className` prop has been removed as it's not needed with the token system.
140+
141+
## Technical Details
142+
143+
### Removed `className` Prop
144+
145+
The component previously accepted a `className` prop but never used it meaningfully. Since the component uses:
146+
147+
- Token-based theming via `useResponsiveTokens`
148+
- Scoped CSS via `.blend-workflow-canvas`
149+
- Styled-components for dynamic styling
150+
151+
...custom className overrides were not compatible with the design system's approach and have been removed.
152+
153+
### CSS Specificity
154+
155+
The scoped CSS uses natural specificity without `!important`:
156+
157+
```css
158+
/* Specificity: 0-2-0 (2 classes) */
159+
.blend-workflow-canvas .react-flow__node {
160+
}
161+
162+
/* This is higher than ReactFlow's base styles */
163+
.react-flow__node {
164+
} /* Specificity: 0-1-0 */
165+
```
166+
167+
This ensures our styles take precedence while remaining maintainable and predictable.

0 commit comments

Comments
 (0)