Skip to content

Commit b8b2057

Browse files
authored
improvement(ui): modal style standardization, select drop improvement, duplication selection fixes (#2871)
* improvement(ui): modal style standardization, select drop improvement * consolidation, fixed canvas issues * more
1 parent 4b8534e commit b8b2057

File tree

22 files changed

+255
-228
lines changed

22 files changed

+255
-228
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const ActionBar = memo(
4848
collaborativeBatchToggleBlockEnabled,
4949
collaborativeBatchToggleBlockHandles,
5050
} = useCollaborativeWorkflow()
51-
const { activeWorkflowId } = useWorkflowRegistry()
51+
const { activeWorkflowId, setPendingSelection } = useWorkflowRegistry()
5252
const blocks = useWorkflowStore((state) => state.blocks)
5353
const subBlockStore = useSubBlockStore()
5454

@@ -68,13 +68,15 @@ export const ActionBar = memo(
6868
subBlockValues,
6969
})
7070

71+
setPendingSelection([newId])
7172
collaborativeBatchAddBlocks([block], [], {}, {}, { [newId]: filteredValues })
7273
}, [
7374
blockId,
7475
blocks,
7576
activeWorkflowId,
7677
subBlockStore.workflowValues,
7778
collaborativeBatchAddBlocks,
79+
setPendingSelection,
7880
])
7981

8082
/**

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ import ReactMarkdown from 'react-markdown'
33
import type { NodeProps } from 'reactflow'
44
import remarkGfm from 'remark-gfm'
55
import { cn } from '@/lib/core/utils/cn'
6+
import { BLOCK_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
67
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
78
import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar'
89
import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
9-
import {
10-
BLOCK_DIMENSIONS,
11-
useBlockDimensions,
12-
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
10+
import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
1311
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1412
import type { WorkflowBlockProps } from '../workflow-block/types'
1513

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

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import {
1111
ButtonGroupItem,
1212
Checkbox,
1313
Code,
14-
Combobox,
15-
type ComboboxOption,
1614
Input,
1715
Label,
1816
TagInput,
@@ -271,14 +269,6 @@ export function A2aDeploy({
271269
onNeedsRepublishChange?.(!!needsRepublish)
272270
}, [needsRepublish, onNeedsRepublishChange])
273271

274-
const authSchemeOptions: ComboboxOption[] = useMemo(
275-
() => [
276-
{ label: 'API Key', value: 'apiKey' },
277-
{ label: 'None (Public)', value: 'none' },
278-
],
279-
[]
280-
)
281-
282272
const canSave = name.trim().length > 0 && description.trim().length > 0
283273
useEffect(() => {
284274
onCanSaveChange?.(canSave)
@@ -758,17 +748,18 @@ console.log(data);`
758748
/>
759749
</div>
760750

761-
{/* Authentication */}
751+
{/* Access */}
762752
<div>
763753
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
764-
Authentication
754+
Access
765755
</Label>
766-
<Combobox
767-
options={authSchemeOptions}
756+
<ButtonGroup
768757
value={authScheme}
769-
onChange={(v) => setAuthScheme(v as AuthScheme)}
770-
placeholder='Select authentication...'
771-
/>
758+
onValueChange={(value) => setAuthScheme(value as AuthScheme)}
759+
>
760+
<ButtonGroupItem value='apiKey'>API Key</ButtonGroupItem>
761+
<ButtonGroupItem value='none'>Public</ButtonGroupItem>
762+
</ButtonGroup>
772763
<p className='mt-[6.5px] text-[11px] text-[var(--text-secondary)]'>
773764
{authScheme === 'none'
774765
? 'Anyone can call this agent without authentication'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ export function ChatDeploy({
424424
>
425425
Cancel
426426
</Button>
427-
<Button variant='default' onClick={handleDelete} disabled={isDeleting}>
427+
<Button variant='destructive' onClick={handleDelete} disabled={isDeleting}>
428428
{isDeleting ? 'Deleting...' : 'Delete'}
429429
</Button>
430430
</ModalFooter>

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { cn } from '@/lib/core/utils/cn'
77
import { getBaseUrl } from '@/lib/core/utils/urls'
88
import { createMcpToolId } from '@/lib/mcp/utils'
99
import { getProviderIdFromServiceId } from '@/lib/oauth'
10+
import { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
1011
import {
1112
buildCanonicalIndex,
1213
evaluateSubBlockCondition,
@@ -28,11 +29,7 @@ import {
2829
shouldSkipBlockRender,
2930
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils'
3031
import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
31-
import {
32-
BLOCK_DIMENSIONS,
33-
HANDLE_POSITIONS,
34-
useBlockDimensions,
35-
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
32+
import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
3633
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
3734
import { getDependsOnFields } from '@/blocks/utils'
3835
import { useKnowledgeBase } from '@/hooks/kb/use-knowledge'
Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
export {
2-
clearDragHighlights,
3-
computeClampedPositionUpdates,
4-
computeParentUpdateEntries,
5-
getClampedPositionForNode,
6-
isInEditableElement,
7-
resolveParentChildSelectionConflicts,
8-
validateTriggerPaste,
9-
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers'
101
export { useFloatBoundarySync, useFloatDrag, useFloatResize } from './float'
2+
export { useAccessibleReferencePrefixes } from './use-accessible-reference-prefixes'
113
export { useAutoLayout } from './use-auto-layout'
12-
export { BLOCK_DIMENSIONS, useBlockDimensions } from './use-block-dimensions'
4+
export { useBlockDimensions } from './use-block-dimensions'
5+
export { useBlockOutputFields } from './use-block-output-fields'
136
export { useBlockVisual } from './use-block-visual'
7+
export { useCanvasContextMenu } from './use-canvas-context-menu'
148
export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow'
15-
export { calculateContainerDimensions, useNodeUtilities } from './use-node-utilities'
9+
export { useNodeUtilities } from './use-node-utilities'
1610
export { usePreventZoom } from './use-prevent-zoom'
1711
export { useScrollManagement } from './use-scroll-management'
12+
export { useShiftSelectionLock } from './use-shift-selection-lock'
13+
export { useWand, type WandConfig } from './use-wand'
1814
export { useWorkflowExecution } from './use-workflow-execution'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ import { useEffect, useRef } from 'react'
22
import { useUpdateNodeInternals } from 'reactflow'
33
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
44

5-
// Re-export for backwards compatibility
6-
export { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
7-
85
interface BlockDimensions {
96
width: number
107
height: number

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts

Lines changed: 5 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,107 +2,15 @@ import { useCallback } from 'react'
22
import { createLogger } from '@sim/logger'
33
import { useReactFlow } from 'reactflow'
44
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
5-
import { getBlock } from '@/blocks/registry'
5+
import {
6+
calculateContainerDimensions,
7+
clampPositionToContainer,
8+
estimateBlockDimensions,
9+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils'
610
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
711

812
const logger = createLogger('NodeUtilities')
913

10-
/**
11-
* Estimates block dimensions based on block type.
12-
* Uses subblock count to estimate height for blocks that haven't been measured yet.
13-
*
14-
* @param blockType - The type of block (e.g., 'condition', 'agent')
15-
* @returns Estimated width and height for the block
16-
*/
17-
export function estimateBlockDimensions(blockType: string): { width: number; height: number } {
18-
const blockConfig = getBlock(blockType)
19-
const subBlockCount = blockConfig?.subBlocks?.length ?? 3
20-
// Many subblocks are conditionally rendered (advanced mode, provider-specific, etc.)
21-
// Use roughly half the config count as a reasonable estimate, capped between 3-7 rows
22-
const estimatedRows = Math.max(3, Math.min(Math.ceil(subBlockCount / 2), 7))
23-
const hasErrorRow = blockType !== 'starter' && blockType !== 'response' ? 1 : 0
24-
25-
const height =
26-
BLOCK_DIMENSIONS.HEADER_HEIGHT +
27-
BLOCK_DIMENSIONS.WORKFLOW_CONTENT_PADDING +
28-
(estimatedRows + hasErrorRow) * BLOCK_DIMENSIONS.WORKFLOW_ROW_HEIGHT
29-
30-
return {
31-
width: BLOCK_DIMENSIONS.FIXED_WIDTH,
32-
height: Math.max(height, BLOCK_DIMENSIONS.MIN_HEIGHT),
33-
}
34-
}
35-
36-
/**
37-
* Clamps a position to keep a block fully inside a container's content area.
38-
* Content area starts after the header and padding, and ends before the right/bottom padding.
39-
*
40-
* @param position - Raw position relative to container origin
41-
* @param containerDimensions - Container width and height
42-
* @param blockDimensions - Block width and height
43-
* @returns Clamped position that keeps block inside content area
44-
*/
45-
export function clampPositionToContainer(
46-
position: { x: number; y: number },
47-
containerDimensions: { width: number; height: number },
48-
blockDimensions: { width: number; height: number }
49-
): { x: number; y: number } {
50-
const { width: containerWidth, height: containerHeight } = containerDimensions
51-
const { width: blockWidth, height: blockHeight } = blockDimensions
52-
53-
// Content area bounds (where blocks can be placed)
54-
const minX = CONTAINER_DIMENSIONS.LEFT_PADDING
55-
const minY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING
56-
const maxX = containerWidth - CONTAINER_DIMENSIONS.RIGHT_PADDING - blockWidth
57-
const maxY = containerHeight - CONTAINER_DIMENSIONS.BOTTOM_PADDING - blockHeight
58-
59-
return {
60-
x: Math.max(minX, Math.min(position.x, Math.max(minX, maxX))),
61-
y: Math.max(minY, Math.min(position.y, Math.max(minY, maxY))),
62-
}
63-
}
64-
65-
/**
66-
* Calculates container dimensions based on child block positions.
67-
* Single source of truth for container sizing - ensures consistency between
68-
* live drag updates and final dimension calculations.
69-
*
70-
* @param childPositions - Array of child positions with their dimensions
71-
* @returns Calculated width and height for the container
72-
*/
73-
export function calculateContainerDimensions(
74-
childPositions: Array<{ x: number; y: number; width: number; height: number }>
75-
): { width: number; height: number } {
76-
if (childPositions.length === 0) {
77-
return {
78-
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
79-
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
80-
}
81-
}
82-
83-
let maxRight = 0
84-
let maxBottom = 0
85-
86-
for (const child of childPositions) {
87-
maxRight = Math.max(maxRight, child.x + child.width)
88-
maxBottom = Math.max(maxBottom, child.y + child.height)
89-
}
90-
91-
const width = Math.max(
92-
CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
93-
CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
94-
)
95-
const height = Math.max(
96-
CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
97-
CONTAINER_DIMENSIONS.HEADER_HEIGHT +
98-
CONTAINER_DIMENSIONS.TOP_PADDING +
99-
maxBottom +
100-
CONTAINER_DIMENSIONS.BOTTOM_PADDING
101-
)
102-
103-
return { width, height }
104-
}
105-
10614
/**
10715
* Hook providing utilities for node position, hierarchy, and dimension calculations
10816
*/
@@ -138,15 +46,13 @@ export function useNodeUtilities(blocks: Record<string, any>) {
13846
}
13947
}
14048

141-
// Prefer deterministic height published by the block component; fallback to estimate
14249
if (block.height) {
14350
return {
14451
width: BLOCK_DIMENSIONS.FIXED_WIDTH,
14552
height: Math.max(block.height, BLOCK_DIMENSIONS.MIN_HEIGHT),
14653
}
14754
}
14855

149-
// Use shared estimation utility for blocks without measured height
15056
return estimateBlockDimensions(block.type)
15157
},
15258
[blocks, isContainerType]
@@ -230,8 +136,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
230136

231137
const parentPos = getNodeAbsolutePosition(parentId)
232138

233-
// Child positions are stored relative to the content area (after header and padding)
234-
// Add these offsets when calculating absolute position
235139
const headerHeight = 50
236140
const leftPadding = 16
237141
const topPadding = 16
@@ -314,7 +218,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
314218
})
315219
.map((n) => ({
316220
loopId: n.id,
317-
// Return absolute position so callers can compute relative placement correctly
318221
loopPosition: getNodeAbsolutePosition(n.id),
319222
dimensions: {
320223
width: n.data?.width || CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
@@ -449,7 +352,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
449352
return absPos
450353
}
451354

452-
// Use known defaults per node type without type casting
453355
const isSubflow = node.type === 'subflowNode'
454356
const width = isSubflow
455357
? typeof node.data?.width === 'number'
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useCallback, useEffect, useState } from 'react'
2+
3+
interface UseShiftSelectionLockProps {
4+
isHandMode: boolean
5+
}
6+
7+
interface UseShiftSelectionLockResult {
8+
/** Whether a shift-selection is currently active (locked in until mouseup) */
9+
isShiftSelecting: boolean
10+
/** Handler to attach to canvas mousedown */
11+
handleCanvasMouseDown: (event: React.MouseEvent) => void
12+
/** Computed ReactFlow props based on current selection state */
13+
selectionProps: {
14+
selectionOnDrag: boolean
15+
panOnDrag: [number, number] | false
16+
selectionKeyCode: string | null
17+
}
18+
}
19+
20+
/**
21+
* Locks shift-selection mode from mousedown to mouseup.
22+
* Prevents selection from canceling when shift is released mid-drag.
23+
*/
24+
export function useShiftSelectionLock({
25+
isHandMode,
26+
}: UseShiftSelectionLockProps): UseShiftSelectionLockResult {
27+
const [isShiftSelecting, setIsShiftSelecting] = useState(false)
28+
29+
const handleCanvasMouseDown = useCallback(
30+
(event: React.MouseEvent) => {
31+
if (!event.shiftKey) return
32+
33+
const target = event.target as HTMLElement | null
34+
const isPaneTarget = Boolean(target?.closest('.react-flow__pane, .react-flow__selectionpane'))
35+
36+
if (isPaneTarget && isHandMode) {
37+
setIsShiftSelecting(true)
38+
}
39+
40+
if (isPaneTarget) {
41+
event.preventDefault()
42+
window.getSelection()?.removeAllRanges()
43+
}
44+
},
45+
[isHandMode]
46+
)
47+
48+
useEffect(() => {
49+
if (!isShiftSelecting) return
50+
51+
const handleMouseUp = () => setIsShiftSelecting(false)
52+
window.addEventListener('mouseup', handleMouseUp)
53+
return () => window.removeEventListener('mouseup', handleMouseUp)
54+
}, [isShiftSelecting])
55+
56+
const selectionProps = {
57+
selectionOnDrag: !isHandMode || isShiftSelecting,
58+
panOnDrag: (isHandMode && !isShiftSelecting ? [0, 1] : false) as [number, number] | false,
59+
selectionKeyCode: isShiftSelecting ? null : 'Shift',
60+
}
61+
62+
return { isShiftSelecting, handleCanvasMouseDown, selectionProps }
63+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './auto-layout-utils'
22
export * from './block-ring-utils'
3+
export * from './node-position-utils'
4+
export * from './workflow-canvas-helpers'
35
export * from './workflow-execution-utils'

0 commit comments

Comments
 (0)