-
Notifications
You must be signed in to change notification settings - Fork 3.5k
feat(loop-block): added a loop block to include looping logic #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
5d86d5c
214a1e0
fdc7989
555d4fa
e8cd888
720ba2f
d1e756c
a97e546
69a1dd7
5a09db7
a1b8380
37fa527
2ead645
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,17 @@ | ||||||||||||||||||||
| import { memo, useMemo } from 'react' | ||||||||||||||||||||
| import { memo, useMemo, useRef } from 'react' | ||||||||||||||||||||
| import { Handle, NodeProps, Position, useReactFlow } from 'reactflow' | ||||||||||||||||||||
| import { Trash2 } from 'lucide-react' | ||||||||||||||||||||
| import { StartIcon } from '@/components/icons' | ||||||||||||||||||||
| import { Card } from '@/components/ui/card' | ||||||||||||||||||||
| import { Button } from '@/components/ui/button' | ||||||||||||||||||||
| import { cn } from '@/lib/utils' | ||||||||||||||||||||
| import { useWorkflowStore } from '@/stores/workflows/workflow/store' | ||||||||||||||||||||
| import { LoopConfigBadges } from './components/loop-config-badges' | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => { | ||||||||||||||||||||
| const { getNodes } = useReactFlow(); | ||||||||||||||||||||
| const blockRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Determine nesting level by counting parents | ||||||||||||||||||||
| const nestingLevel = useMemo(() => { | ||||||||||||||||||||
|
|
@@ -25,11 +28,10 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => { | |||||||||||||||||||
| return level; | ||||||||||||||||||||
| }, [id, data?.parentId, getNodes]); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Generate different border styles based on nesting level | ||||||||||||||||||||
| const getBorderStyle = () => { | ||||||||||||||||||||
| // Generate different background styles based on nesting level | ||||||||||||||||||||
| const getNestedStyles = () => { | ||||||||||||||||||||
| // Base styles | ||||||||||||||||||||
| const styles = { | ||||||||||||||||||||
| border: '1px solid rgba(148, 163, 184, 0.6)', | ||||||||||||||||||||
| const styles: Record<string, string> = { | ||||||||||||||||||||
| backgroundColor: data?.state === 'valid' ? 'rgba(34,197,94,0.05)' : 'transparent', | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
Comment on lines
+103
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The backgroundColor style is duplicated in both getNestedStyles and the Card className (line 60). Consider consolidating the valid state styling to one location |
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -39,102 +41,89 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => { | |||||||||||||||||||
| const colors = ['#e2e8f0', '#cbd5e1', '#94a3b8', '#64748b', '#475569']; | ||||||||||||||||||||
| const colorIndex = (nestingLevel - 1) % colors.length; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| styles.border = `2px solid ${colors[colorIndex]}`; | ||||||||||||||||||||
| styles.backgroundColor = `${colors[colorIndex]}30`; // Slightly more visible background | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return styles; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const borderStyle = getBorderStyle(); | ||||||||||||||||||||
| const nestedStyles = getNestedStyles(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return ( | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className={cn( | ||||||||||||||||||||
| 'relative group-node group', | ||||||||||||||||||||
| data?.state === 'valid' && 'border-[#2FB3FF] bg-[rgba(34,197,94,0.05)]', | ||||||||||||||||||||
| )} | ||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| width: data.width || 800, | ||||||||||||||||||||
| height: data.height || 1000, | ||||||||||||||||||||
| borderRadius: '8px', | ||||||||||||||||||||
| position: 'relative', | ||||||||||||||||||||
| overflow: 'visible', | ||||||||||||||||||||
| ...borderStyle, | ||||||||||||||||||||
| pointerEvents: 'all', | ||||||||||||||||||||
| transition: 'width 0.2s ease-out, height 0.2s ease-out, border-color 0.2s ease-in-out, background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out', | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| data-node-id={id} | ||||||||||||||||||||
| data-type="loopNode" | ||||||||||||||||||||
| data-nesting-level={nestingLevel} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| {/* Critical drag handle that controls only the loop node movement */} | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="absolute top-0 left-0 right-0 h-10 workflow-drag-handle cursor-move z-10" | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Nesting level indicator */} | ||||||||||||||||||||
| {nestingLevel > 0 && ( | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="absolute top-2 left-2 px-2 py-0.5 text-xs rounded-md bg-background/80 border border-border shadow-sm z-10" | ||||||||||||||||||||
| style={{ pointerEvents: 'none' }} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| Nested: L{nestingLevel} | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| )} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Custom visible resize handle */} | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="absolute bottom-2 right-2 w-8 h-8 flex items-center justify-center z-20 text-muted-foreground cursor-se-resize" | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Child nodes container - Set pointerEvents: none to allow events to reach edges */} | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="p-4 h-[calc(100%-10px)]" | ||||||||||||||||||||
| data-dragarea="true" | ||||||||||||||||||||
| <div className="relative group"> | ||||||||||||||||||||
| <Card | ||||||||||||||||||||
| ref={blockRef} | ||||||||||||||||||||
| className={cn( | ||||||||||||||||||||
| ' select-none relative cursor-default', | ||||||||||||||||||||
| 'transition-ring transition-block-bg', | ||||||||||||||||||||
| 'z-[20]', | ||||||||||||||||||||
| data?.state === 'valid' && 'ring-2 ring-[#2FB3FF] bg-[rgba(34,197,94,0.05)]', | ||||||||||||||||||||
| nestingLevel > 0 && `border border-[0.5px] ${nestingLevel % 2 === 0 ? 'border-slate-300/60' : 'border-slate-400/60'}` | ||||||||||||||||||||
| )} | ||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| width: data.width || 500, | ||||||||||||||||||||
| height: data.height || 300, | ||||||||||||||||||||
| position: 'relative', | ||||||||||||||||||||
| minHeight: '100%', | ||||||||||||||||||||
| pointerEvents: 'none', | ||||||||||||||||||||
| overflow: 'visible', | ||||||||||||||||||||
| ...nestedStyles, | ||||||||||||||||||||
| pointerEvents: 'all', | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| data-node-id={id} | ||||||||||||||||||||
| data-type="loopNode" | ||||||||||||||||||||
| data-nesting-level={nestingLevel} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| {/* Delete button - now always visible */} | ||||||||||||||||||||
| {/* Critical drag handle that controls only the loop node movement */} | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="absolute top-3 right-3 w-7 h-7 flex items-center justify-center rounded-full bg-background/90 hover:bg-red-100 border border-border cursor-pointer z-20 shadow-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200" | ||||||||||||||||||||
| onClick={(e) => { | ||||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||||
| useWorkflowStore.getState().removeBlock(id); | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} // Re-enable pointer events for this button | ||||||||||||||||||||
| className="absolute top-0 left-0 right-0 h-10 workflow-drag-handle cursor-move z-10" | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Custom visible resize handle */} | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="absolute bottom-2 right-2 w-8 h-8 flex items-center justify-center z-20 text-muted-foreground cursor-se-resize" | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| <Trash2 size={14} className="text-muted-foreground hover:text-red-500" /> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Loop Start Block - positioned at left middle */} | ||||||||||||||||||||
| {/* Child nodes container - Set pointerEvents: none to allow events to reach edges */} | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="absolute top-1/2 left-8 w-12 transform -translate-y-1/2" | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} // Re-enable pointer events | ||||||||||||||||||||
| className="p-4 h-[calc(100%-10px)]" | ||||||||||||||||||||
| data-dragarea="true" | ||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| position: 'relative', | ||||||||||||||||||||
| minHeight: '100%', | ||||||||||||||||||||
| pointerEvents: 'none', | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| {/* Delete button - styled like in action-bar.tsx */} | ||||||||||||||||||||
| <Button | ||||||||||||||||||||
| variant="ghost" | ||||||||||||||||||||
| size="sm" | ||||||||||||||||||||
| onClick={(e) => { | ||||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||||
| useWorkflowStore.getState().removeBlock(id); | ||||||||||||||||||||
| }} | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Directly accessing store.getState() in event handler could miss store updates. Consider using a store hook instead
Suggested change
|
||||||||||||||||||||
| className="absolute top-2 right-2 z-20 text-gray-500 hover:text-red-600 opacity-0 group-hover:opacity-100 transition-opacity duration-200" | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} | ||||||||||||||||||||
| > | ||||||||||||||||||||
| <Trash2 className="h-4 w-4" /> | ||||||||||||||||||||
| </Button> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Loop Start Block */} | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="bg-white border border-border rounded-md p-2 h-12 relative hover:bg-slate-50 transition-colors flex items-center justify-center" | ||||||||||||||||||||
| data-parent-id={id} | ||||||||||||||||||||
| className="absolute top-1/2 left-8 w-10 bg-[#2FB3FF] rounded-md p-2 h-10 flex items-center justify-center transform -translate-y-1/2" | ||||||||||||||||||||
| style={{ pointerEvents: 'auto' }} | ||||||||||||||||||||
| data-parent-id={id} | ||||||||||||||||||||
| data-node-role="loop-start" | ||||||||||||||||||||
| data-extent="parent" | ||||||||||||||||||||
| > | ||||||||||||||||||||
| <div | ||||||||||||||||||||
| className="bg-[#2FB3FF] rounded-full p-1.5 flex items-center justify-center" | ||||||||||||||||||||
| style={{ zIndex: 1}}> | ||||||||||||||||||||
| <StartIcon className="text-white w-6 h-6" /> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <StartIcon className="text-white w-6 h-6" /> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <Handle | ||||||||||||||||||||
| type="source" | ||||||||||||||||||||
| position={Position.Right} | ||||||||||||||||||||
| id="loop-start-source" | ||||||||||||||||||||
| className="!w-[7px] !h-4 !bg-[#2FB3FF] dark:!bg-[#2FB3FF]! !border-none !z-[30] group-hover:!shadow-[0_0_0_3px_rgba(64,224,208,0.15)] hover:!w-[10px] hover:!right-[-10px] hover:!rounded-r-full !cursor-crosshair transition-[colors] duration-150" | ||||||||||||||||||||
| className="!w-[6px] !h-4 !bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none !z-[30] hover:!w-[10px] hover:!right-[-10px] hover:!rounded-r-full hover:!rounded-l-none !cursor-crosshair -[colors] duration-150" | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. syntax: There appears to be a typo in the className: '-[colors]' should likely be 'transition-[colors]'
Suggested change
|
||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| right: "-6px", | ||||||||||||||||||||
| top: "50%", | ||||||||||||||||||||
|
|
@@ -145,37 +134,37 @@ export const LoopNodeComponent = memo(({ data, selected, id }: NodeProps) => { | |||||||||||||||||||
| /> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Input handle on left middle */} | ||||||||||||||||||||
| <Handle | ||||||||||||||||||||
| type="target" | ||||||||||||||||||||
| position={Position.Left} | ||||||||||||||||||||
| className="!w-[10px] !h-5 !bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none !z-[30] group-hover:!shadow-[0_0_0_3px_rgba(156,163,175,0.15)] hover:!w-[10px] hover:!left-[-10px] hover:!rounded-l-full hover:!rounded-r-none !cursor-crosshair transition-[colors] duration-150" | ||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| left: "-6px", | ||||||||||||||||||||
| top: "50%", | ||||||||||||||||||||
| transform: "translateY(-50%)", | ||||||||||||||||||||
| pointerEvents: 'auto' | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| {/* Input handle on left middle */} | ||||||||||||||||||||
| <Handle | ||||||||||||||||||||
| type="target" | ||||||||||||||||||||
| position={Position.Left} | ||||||||||||||||||||
| className="!w-[7px] !h-5 !bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none !z-[30] hover:!w-[10px] hover:!left-[-10px] hover:!rounded-l-full hover:!rounded-r-none !cursor-crosshair transition-[colors] duration-150" | ||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| left: "-7px", | ||||||||||||||||||||
| top: "50%", | ||||||||||||||||||||
| transform: "translateY(-50%)", | ||||||||||||||||||||
| pointerEvents: 'auto' | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| /> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Output handle on right middle */} | ||||||||||||||||||||
| <Handle | ||||||||||||||||||||
| type="source" | ||||||||||||||||||||
| position={Position.Right} | ||||||||||||||||||||
| className="!w-[10px] !h-5 !bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none !z-[30] group-hover:!shadow-[0_0_0_3px_rgba(156,163,175,0.15)] hover:!w-[10px] hover:!right-[-10px] hover:!rounded-r-full hover:!rounded-l-none !cursor-crosshair transition-[colors] duration-150" | ||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| right: "-6px", | ||||||||||||||||||||
| top: "50%", | ||||||||||||||||||||
| transform: "translateY(-50%)", | ||||||||||||||||||||
| pointerEvents: 'auto' | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| id="loop-end-source" | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| {/* Output handle on right middle */} | ||||||||||||||||||||
| <Handle | ||||||||||||||||||||
| type="source" | ||||||||||||||||||||
| position={Position.Right} | ||||||||||||||||||||
| className="!w-[7px] !h-5 !bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none !z-[30] hover:!w-[10px] hover:!right-[-10px] hover:!rounded-r-full hover:!rounded-l-none !cursor-crosshair transition-[colors] duration-150" | ||||||||||||||||||||
| style={{ | ||||||||||||||||||||
| right: "-7px", | ||||||||||||||||||||
| top: "50%", | ||||||||||||||||||||
| transform: "translateY(-50%)", | ||||||||||||||||||||
| pointerEvents: 'auto' | ||||||||||||||||||||
| }} | ||||||||||||||||||||
| id="loop-end-source" | ||||||||||||||||||||
| /> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| {/* Loop Configuration Badges */} | ||||||||||||||||||||
| <LoopConfigBadges nodeId={id} data={data} /> | ||||||||||||||||||||
| {/* Loop Configuration Badges */} | ||||||||||||||||||||
| <LoopConfigBadges nodeId={id} data={data} /> | ||||||||||||||||||||
| </Card> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| }) | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: collection field should have a default value that matches the expected type (array/object) for forEach loops