Skip to content

Commit 489d55b

Browse files
committed
feat(canvas): add a setting to turn off auto-focus when clicking blocks
Clicking a block animates the camera to center it, which zooms in far enough that you lose sight of the rest of the workflow. Add an "Auto-focus on click" preference (on by default, so existing behavior is unchanged) that keeps the camera still on click. Also re-record the auto-connect and canvas-error-notification tooltip previews and re-encode all three at a smaller size.
1 parent 44524d1 commit 489d55b

13 files changed

Lines changed: 19184 additions & 3 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ export function General() {
217217
}
218218
}
219219

220+
const handleAutoFocusOnClickChange = async (checked: boolean) => {
221+
if (checked !== settings?.autoFocusOnClick && !updateSetting.isPending) {
222+
await updateSetting.mutateAsync({ key: 'autoFocusOnClick', value: checked })
223+
}
224+
}
225+
220226
const handleSnapToGridChange = async (value: string) => {
221227
const newValue = Number.parseInt(value, 10)
222228
if (newValue !== settings?.snapToGridSize && !updateSetting.isPending) {
@@ -457,6 +463,36 @@ export function General() {
457463
/>
458464
</div>
459465

466+
<div className='flex items-center justify-between'>
467+
<div className='flex items-center gap-1.5'>
468+
<Label htmlFor='auto-focus-on-click'>Auto-focus on click</Label>
469+
<Tooltip.Root>
470+
<Tooltip.Trigger asChild>
471+
<button
472+
type='button'
473+
aria-label='About auto-focus on click'
474+
className='inline-flex cursor-default text-[var(--text-muted)]'
475+
>
476+
<CircleInfo className='size-[14px]' />
477+
</button>
478+
</Tooltip.Trigger>
479+
<Tooltip.Content side='bottom' align='start'>
480+
<p>Center the canvas on a block when you click it</p>
481+
<Tooltip.Preview
482+
src='/tooltips/auto-focus-on-click.mp4'
483+
alt='Auto-focus on click example'
484+
loop={true}
485+
/>
486+
</Tooltip.Content>
487+
</Tooltip.Root>
488+
</div>
489+
<Switch
490+
id='auto-focus-on-click'
491+
checked={settings?.autoFocusOnClick ?? true}
492+
onCheckedChange={handleAutoFocusOnClickChange}
493+
/>
494+
</div>
495+
460496
<div className='flex items-center justify-between'>
461497
<div className='flex items-center gap-1.5'>
462498
<Label htmlFor='error-notifications'>Canvas error notifications</Label>

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ import { isAnnotationOnlyBlock } from '@/executor/constants'
113113
import { useCustomBlocks } from '@/hooks/queries/custom-blocks'
114114
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
115115
import { useFolderMap } from '@/hooks/queries/folders'
116-
import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings'
116+
import {
117+
useAutoConnect,
118+
useAutoFocusOnClick,
119+
useSnapToGridSize,
120+
} from '@/hooks/queries/general-settings'
117121
import {
118122
findLockedAncestorFolder,
119123
isFolderOrAncestorLocked,
@@ -404,6 +408,8 @@ const WorkflowContent = React.memo(
404408
const autoConnectRef = useRef(isAutoConnectEnabled)
405409
autoConnectRef.current = isAutoConnectEnabled
406410

411+
const isAutoFocusOnClickEnabled = useAutoFocusOnClick()
412+
407413
// Panel open states for context menu
408414
const isVariablesOpen = useVariablesModalStore((state) => state.isOpen)
409415
const isChatOpen = useChatStore((state) => state.isChatOpen)
@@ -4320,12 +4326,14 @@ const WorkflowContent = React.memo(
43204326
/**
43214327
* Focus the clicked block: animate the camera so the card centers in
43224328
* the canvas frame. Plain clicks focus both regular cards and subflow
4323-
* containers; multi-select keeps the camera still.
4329+
* containers; multi-select keeps the camera still. Users who would
4330+
* rather keep their own framing turn this off in general settings.
43244331
* onNodeClick never fires after a drag.
43254332
*/
43264333
if (
43274334
!embedded &&
43284335
!isMultiSelect &&
4336+
isAutoFocusOnClickEnabled &&
43294337
(node.type === 'workflowBlock' ||
43304338
node.type === 'noteBlock' ||
43314339
node.type === 'subflowNode')
@@ -4334,7 +4342,15 @@ const WorkflowContent = React.memo(
43344342
focusBlockInView(node)
43354343
}
43364344
},
4337-
[activeWorkflowId, blocks, getNodes, embedded, focusBlockInView, workflowIdParam]
4345+
[
4346+
activeWorkflowId,
4347+
blocks,
4348+
getNodes,
4349+
embedded,
4350+
focusBlockInView,
4351+
isAutoFocusOnClickEnabled,
4352+
workflowIdParam,
4353+
]
43384354
)
43394355

43404356
/**

apps/sim/hooks/queries/general-settings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export interface GeneralSettings {
3636
errorNotificationsEnabled: boolean
3737
snapToGridSize: number
3838
showActionBar: boolean
39+
/** Whether clicking a block on the canvas animates the camera to center it. */
40+
autoFocusOnClick: boolean
3941
/** Copilot tool ids the user picked "always allow" for. */
4042
copilotAutoAllowedTools: string[]
4143
/** Saved IANA timezone, or `null` when unset (the app falls back to the browser zone). */
@@ -57,6 +59,7 @@ export function mapGeneralSettingsResponse(data: UserSettingsApi): GeneralSettin
5759
errorNotificationsEnabled: data.errorNotificationsEnabled,
5860
snapToGridSize: data.snapToGridSize,
5961
showActionBar: data.showActionBar,
62+
autoFocusOnClick: data.autoFocusOnClick,
6063
copilotAutoAllowedTools: data.copilotAutoAllowedTools ?? [],
6164
timezone: data.timezone ?? null,
6265
}
@@ -122,6 +125,15 @@ export function useShowActionBar(): boolean {
122125
return data?.showActionBar ?? true
123126
}
124127

128+
/**
129+
* Whether the canvas camera animates to center a block when it is clicked or
130+
* reached with arrow-key navigation.
131+
*/
132+
export function useAutoFocusOnClick(): boolean {
133+
const { data } = useGeneralSettings()
134+
return data?.autoFocusOnClick ?? true
135+
}
136+
125137
export function useBillingUsageNotifications(): boolean {
126138
const { data } = useGeneralSettings()
127139
return data?.billingUsageNotificationsEnabled ?? true

apps/sim/lib/api/contracts/user.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export const userSettingsSchema = z.object({
9090
errorNotificationsEnabled: z.boolean().default(true),
9191
snapToGridSize: z.number().min(0).max(50).default(0),
9292
showActionBar: z.boolean().default(true),
93+
/** Whether clicking a block on the canvas animates the camera to center it. */
94+
autoFocusOnClick: z.boolean().default(true),
9395
/** Copilot tool ids the user chose "always allow" for, so they are never prompted for them again. */
9496
copilotAutoAllowedTools: z.array(z.string()).default([]),
9597
/** IANA timezone for scheduling; `null` means the client falls back to the browser-detected zone. */
@@ -110,6 +112,7 @@ export const updateUserSettingsBodySchema = z.object({
110112
errorNotificationsEnabled: z.boolean().optional(),
111113
snapToGridSize: z.number().min(0).max(50).optional(),
112114
showActionBar: z.boolean().optional(),
115+
autoFocusOnClick: z.boolean().optional(),
113116
copilotAutoAllowedTools: z.array(z.string()).optional(),
114117
/** IANA timezone; explicit `null` resets to the browser-detected zone. */
115118
timezone: ianaTimezoneSchema.nullable().optional(),

apps/sim/lib/users/queries.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const defaultUserSettings: UserSettingsApi = {
2424
errorNotificationsEnabled: true,
2525
snapToGridSize: 0,
2626
showActionBar: true,
27+
autoFocusOnClick: true,
2728
copilotAutoAllowedTools: [],
2829
timezone: null,
2930
lastActiveWorkspaceId: null,
@@ -50,6 +51,7 @@ export async function getUserSettings(userId: string | null): Promise<UserSettin
5051
errorNotificationsEnabled: settings.errorNotificationsEnabled,
5152
snapToGridSize: settings.snapToGridSize,
5253
showActionBar: settings.showActionBar,
54+
autoFocusOnClick: settings.autoFocusOnClick,
5355
copilotAutoAllowedTools: settings.copilotAutoAllowedTools,
5456
timezone: settings.timezone,
5557
lastActiveWorkspaceId: settings.lastActiveWorkspaceId,
@@ -76,6 +78,7 @@ export async function getUserSettings(userId: string | null): Promise<UserSettin
7678
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
7779
snapToGridSize: userSettings.snapToGridSize ?? 0,
7880
showActionBar: userSettings.showActionBar ?? true,
81+
autoFocusOnClick: userSettings.autoFocusOnClick ?? true,
7982
copilotAutoAllowedTools: normalizeStringArray(userSettings.copilotAutoAllowedTools),
8083
timezone: userSettings.timezone ?? null,
8184
lastActiveWorkspaceId: userSettings.lastActiveWorkspaceId ?? null,
-55.5 KB
Binary file not shown.
16 KB
Binary file not shown.
-39.9 KB
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- migration-safe: an additive column with a non-null default, so old and new app versions read and write these rows throughout the deploy. Defaulting to true preserves the camera-follows-click behavior every released version already ships.
2+
ALTER TABLE "settings" ADD COLUMN "auto_focus_on_click" boolean DEFAULT true NOT NULL;

0 commit comments

Comments
 (0)