Skip to content

Commit e59e2fd

Browse files
committed
feat(workflow-controls): added action bar for picker/hand/undo/redo/zoom workflow controls, added general setting to disable
1 parent 9a16e7c commit e59e2fd

File tree

28 files changed

+10133
-70
lines changed

28 files changed

+10133
-70
lines changed

apps/sim/app/api/users/me/settings/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const SettingsSchema = z.object({
2727
superUserModeEnabled: z.boolean().optional(),
2828
errorNotificationsEnabled: z.boolean().optional(),
2929
snapToGridSize: z.number().min(0).max(50).optional(),
30+
showActionBar: z.boolean().optional(),
3031
})
3132

3233
const defaultSettings = {
@@ -39,6 +40,7 @@ const defaultSettings = {
3940
superUserModeEnabled: false,
4041
errorNotificationsEnabled: true,
4142
snapToGridSize: 0,
43+
showActionBar: true,
4244
}
4345

4446
export async function GET() {
@@ -73,6 +75,7 @@ export async function GET() {
7375
superUserModeEnabled: userSettings.superUserModeEnabled ?? true,
7476
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
7577
snapToGridSize: userSettings.snapToGridSize ?? 0,
78+
showActionBar: userSettings.showActionBar ?? true,
7679
},
7780
},
7881
{ status: 200 }

apps/sim/app/playground/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ import {
2121
Combobox,
2222
Connections,
2323
Copy,
24+
Cursor,
2425
DatePicker,
2526
DocumentAttachment,
2627
Duplicate,
28+
Expand,
2729
Eye,
2830
FolderCode,
2931
FolderPlus,
32+
Hand,
3033
HexSimple,
3134
Input,
3235
Key as KeyIcon,
@@ -979,11 +982,14 @@ export default function PlaygroundPage() {
979982
{ Icon: ChevronDown, name: 'ChevronDown' },
980983
{ Icon: Connections, name: 'Connections' },
981984
{ Icon: Copy, name: 'Copy' },
985+
{ Icon: Cursor, name: 'Cursor' },
982986
{ Icon: DocumentAttachment, name: 'DocumentAttachment' },
983987
{ Icon: Duplicate, name: 'Duplicate' },
988+
{ Icon: Expand, name: 'Expand' },
984989
{ Icon: Eye, name: 'Eye' },
985990
{ Icon: FolderCode, name: 'FolderCode' },
986991
{ Icon: FolderPlus, name: 'FolderPlus' },
992+
{ Icon: Hand, name: 'Hand' },
987993
{ Icon: HexSimple, name: 'HexSimple' },
988994
{ Icon: KeyIcon, name: 'Key' },
989995
{ Icon: Layout, name: 'Layout' },
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
'use client'
2+
3+
import { useRef, useState } from 'react'
4+
import { useReactFlow } from 'reactflow'
5+
import {
6+
Button,
7+
Cursor,
8+
Expand,
9+
Hand,
10+
Popover,
11+
PopoverAnchor,
12+
PopoverContent,
13+
PopoverItem,
14+
Redo,
15+
Tooltip,
16+
Undo,
17+
ZoomIn,
18+
ZoomOut,
19+
} from '@/components/emcn'
20+
import { useSession } from '@/lib/auth/auth-client'
21+
import { useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
22+
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
23+
import { useCanvasModeStore } from '@/stores/canvas-mode'
24+
import { useGeneralStore } from '@/stores/settings/general'
25+
import { useUndoRedoStore } from '@/stores/undo-redo'
26+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
27+
28+
export function ActionBar() {
29+
const { zoomIn, zoomOut, fitView } = useReactFlow()
30+
const { mode, setMode } = useCanvasModeStore()
31+
const { undo, redo } = useCollaborativeWorkflow()
32+
const showActionBar = useGeneralStore((s) => s.showActionBar)
33+
const updateSetting = useUpdateGeneralSetting()
34+
35+
const { activeWorkflowId } = useWorkflowRegistry()
36+
const { data: session } = useSession()
37+
const userId = session?.user?.id || 'unknown'
38+
const stacks = useUndoRedoStore((s) => s.stacks)
39+
const key = activeWorkflowId && userId ? `${activeWorkflowId}:${userId}` : ''
40+
const stack = (key && stacks[key]) || { undo: [], redo: [] }
41+
const canUndo = stack.undo.length > 0
42+
const canRedo = stack.redo.length > 0
43+
44+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
45+
const menuRef = useRef<HTMLDivElement>(null)
46+
47+
const handleContextMenu = (e: React.MouseEvent) => {
48+
e.preventDefault()
49+
setContextMenu({ x: e.clientX, y: e.clientY })
50+
}
51+
52+
const handleHide = async () => {
53+
setContextMenu(null)
54+
await updateSetting.mutateAsync({ key: 'showActionBar', value: false })
55+
}
56+
57+
if (!showActionBar) {
58+
return null
59+
}
60+
61+
return (
62+
<>
63+
<div
64+
className='fixed bottom-[calc(var(--terminal-height)+12px)] left-[calc(var(--sidebar-width)+12px)] z-10 flex h-[36px] items-center gap-[2px] rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)] p-[4px] shadow-sm transition-[left,bottom] duration-100 ease-out'
65+
onContextMenu={handleContextMenu}
66+
>
67+
<Tooltip.Root>
68+
<Tooltip.Trigger asChild>
69+
<Button
70+
variant={mode === 'hand' ? 'secondary' : 'ghost'}
71+
className='h-[28px] w-[28px] p-0'
72+
onClick={() => setMode('hand')}
73+
>
74+
<Hand className='h-[16px] w-[16px]' />
75+
</Button>
76+
</Tooltip.Trigger>
77+
<Tooltip.Content side='top'>Hand tool</Tooltip.Content>
78+
</Tooltip.Root>
79+
80+
<Tooltip.Root>
81+
<Tooltip.Trigger asChild>
82+
<Button
83+
variant={mode === 'cursor' ? 'secondary' : 'ghost'}
84+
className='h-[28px] w-[28px] p-0'
85+
onClick={() => setMode('cursor')}
86+
>
87+
<Cursor className='h-[16px] w-[16px]' />
88+
</Button>
89+
</Tooltip.Trigger>
90+
<Tooltip.Content side='top'>Move</Tooltip.Content>
91+
</Tooltip.Root>
92+
93+
<div className='mx-[4px] h-[20px] w-[1px] bg-[var(--border)]' />
94+
95+
<Tooltip.Root>
96+
<Tooltip.Trigger asChild>
97+
<Button
98+
variant='ghost'
99+
className='h-[28px] w-[28px] p-0'
100+
onClick={undo}
101+
disabled={!canUndo}
102+
>
103+
<Undo className='h-[16px] w-[16px]' />
104+
</Button>
105+
</Tooltip.Trigger>
106+
<Tooltip.Content side='top'>
107+
<Tooltip.Shortcut keys='⌘Z'>Undo</Tooltip.Shortcut>
108+
</Tooltip.Content>
109+
</Tooltip.Root>
110+
111+
<Tooltip.Root>
112+
<Tooltip.Trigger asChild>
113+
<Button
114+
variant='ghost'
115+
className='h-[28px] w-[28px] p-0'
116+
onClick={redo}
117+
disabled={!canRedo}
118+
>
119+
<Redo className='h-[16px] w-[16px]' />
120+
</Button>
121+
</Tooltip.Trigger>
122+
<Tooltip.Content side='top'>
123+
<Tooltip.Shortcut keys='⌘⇧Z'>Redo</Tooltip.Shortcut>
124+
</Tooltip.Content>
125+
</Tooltip.Root>
126+
127+
<div className='mx-[4px] h-[20px] w-[1px] bg-[var(--border)]' />
128+
129+
<Tooltip.Root>
130+
<Tooltip.Trigger asChild>
131+
<Button variant='ghost' className='h-[28px] w-[28px] p-0' onClick={() => zoomOut()}>
132+
<ZoomOut className='h-[16px] w-[16px]' />
133+
</Button>
134+
</Tooltip.Trigger>
135+
<Tooltip.Content side='top'>Zoom out</Tooltip.Content>
136+
</Tooltip.Root>
137+
138+
<Tooltip.Root>
139+
<Tooltip.Trigger asChild>
140+
<Button variant='ghost' className='h-[28px] w-[28px] p-0' onClick={() => zoomIn()}>
141+
<ZoomIn className='h-[16px] w-[16px]' />
142+
</Button>
143+
</Tooltip.Trigger>
144+
<Tooltip.Content side='top'>Zoom in</Tooltip.Content>
145+
</Tooltip.Root>
146+
147+
<Tooltip.Root>
148+
<Tooltip.Trigger asChild>
149+
<Button
150+
variant='ghost'
151+
className='h-[28px] w-[28px] p-0'
152+
onClick={() => fitView({ padding: 0.3, duration: 300 })}
153+
>
154+
<Expand className='h-[16px] w-[16px]' />
155+
</Button>
156+
</Tooltip.Trigger>
157+
<Tooltip.Content side='top'>Zoom to fit</Tooltip.Content>
158+
</Tooltip.Root>
159+
</div>
160+
161+
<Popover
162+
open={contextMenu !== null}
163+
onOpenChange={(open) => !open && setContextMenu(null)}
164+
variant='secondary'
165+
size='sm'
166+
colorScheme='inverted'
167+
>
168+
<PopoverAnchor
169+
style={{
170+
position: 'fixed',
171+
left: `${contextMenu?.x ?? 0}px`,
172+
top: `${contextMenu?.y ?? 0}px`,
173+
width: '1px',
174+
height: '1px',
175+
}}
176+
/>
177+
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
178+
<PopoverItem onClick={handleHide}>Hide canvas controls</PopoverItem>
179+
</PopoverContent>
180+
</Popover>
181+
</>
182+
)
183+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ActionBar } from './action-bar'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { ActionBar } from './action-bar'
12
export { CommandList } from './command-list/command-list'
23
export { Cursors } from './cursors/cursors'
34
export { DiffControls } from './diff-controls/diff-controls'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5-
import { Maximize2 } from 'lucide-react'
65
import {
76
Button,
87
ButtonGroup,
98
ButtonGroupItem,
9+
Expand,
1010
Label,
1111
Modal,
1212
ModalBody,
@@ -222,7 +222,7 @@ export function GeneralDeploy({
222222
onClick={() => setShowExpandedPreview(true)}
223223
className='absolute right-[8px] bottom-[8px] z-10 h-[28px] w-[28px] cursor-pointer border border-[var(--border)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-3)]'
224224
>
225-
<Maximize2 className='h-[14px] w-[14px]' />
225+
<Expand className='h-[14px] w-[14px]' />
226226
</Button>
227227
</Tooltip.Trigger>
228228
<Tooltip.Content side='top'>See preview</Tooltip.Content>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ export { Copilot } from './copilot/copilot'
22
export { Deploy } from './deploy/deploy'
33
export { Editor } from './editor/editor'
44
export { Toolbar } from './toolbar/toolbar'
5-
export { WorkflowControls } from './workflow-controls/workflow-controls'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/workflow-controls/workflow-controls.tsx

Lines changed: 0 additions & 51 deletions
This file was deleted.

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,9 +495,6 @@ export function Panel() {
495495
Editor
496496
</Button>
497497
</div>
498-
499-
{/* Workflow Controls (Undo/Redo) */}
500-
{/* <WorkflowControls /> */}
501498
</div>
502499

503500
{/* Tab Content - Keep all tabs mounted but hidden to preserve state */}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/b
2424
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
2525
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
2626
import {
27+
ActionBar,
2728
CommandList,
2829
DiffControls,
2930
Notifications,
@@ -64,6 +65,7 @@ import { isAnnotationOnlyBlock } from '@/executor/constants'
6465
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
6566
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
6667
import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
68+
import { useCanvasModeStore } from '@/stores/canvas-mode'
6769
import { useChatStore } from '@/stores/chat/store'
6870
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
6971
import { useExecutionStore } from '@/stores/execution'
@@ -211,6 +213,8 @@ const WorkflowContent = React.memo(() => {
211213
const [isShiftPressed, setIsShiftPressed] = useState(false)
212214
const [isSelectionDragActive, setIsSelectionDragActive] = useState(false)
213215
const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false)
216+
const canvasMode = useCanvasModeStore((state) => state.mode)
217+
const isHandMode = canvasMode === 'hand'
214218
const [oauthModal, setOauthModal] = useState<{
215219
provider: OAuthProvider
216220
serviceId: string
@@ -3327,9 +3331,9 @@ const WorkflowContent = React.memo(() => {
33273331
onPointerMove={handleCanvasPointerMove}
33283332
onPointerLeave={handleCanvasPointerLeave}
33293333
elementsSelectable={true}
3330-
selectionOnDrag={isShiftPressed || isSelectionDragActive}
3334+
selectionOnDrag={!isHandMode || isSelectionDragActive}
33313335
selectionMode={SelectionMode.Partial}
3332-
panOnDrag={isShiftPressed || isSelectionDragActive ? false : [0, 1]}
3336+
panOnDrag={isHandMode ? [0, 1] : false}
33333337
onSelectionStart={onSelectionStart}
33343338
onSelectionEnd={onSelectionEnd}
33353339
multiSelectionKeyCode={['Meta', 'Control', 'Shift']}
@@ -3358,6 +3362,8 @@ const WorkflowContent = React.memo(() => {
33583362

33593363
<Cursors />
33603364

3365+
<ActionBar />
3366+
33613367
<Suspense fallback={null}>
33623368
<LazyChat />
33633369
</Suspense>

0 commit comments

Comments
 (0)