Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ export function General() {
}
}

const handleAutoFocusOnClickChange = async (checked: boolean) => {
if (checked !== settings?.autoFocusOnClick && !updateSetting.isPending) {
await updateSetting.mutateAsync({ key: 'autoFocusOnClick', value: checked })
}
}

const handleSnapToGridChange = async (value: string) => {
const newValue = Number.parseInt(value, 10)
if (newValue !== settings?.snapToGridSize && !updateSetting.isPending) {
Expand Down Expand Up @@ -457,6 +463,36 @@ export function General() {
/>
</div>

<div className='flex items-center justify-between'>
<div className='flex items-center gap-1.5'>
<Label htmlFor='auto-focus-on-click'>Auto-focus on click</Label>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
type='button'
aria-label='About auto-focus on click'
className='inline-flex cursor-default text-[var(--text-muted)]'
>
<CircleInfo className='size-[14px]' />
</button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom' align='start'>
<p>Center the canvas on a block when you click it</p>
<Tooltip.Preview
src='/tooltips/auto-focus-on-click.mp4'
alt='Auto-focus on click example'
loop={true}
/>
</Tooltip.Content>
</Tooltip.Root>
</div>
<Switch
id='auto-focus-on-click'
checked={settings?.autoFocusOnClick ?? true}
onCheckedChange={handleAutoFocusOnClickChange}
/>
</div>

<div className='flex items-center justify-between'>
<div className='flex items-center gap-1.5'>
<Label htmlFor='error-notifications'>Canvas error notifications</Label>
Expand Down
33 changes: 28 additions & 5 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ import { isAnnotationOnlyBlock } from '@/executor/constants'
import { useCustomBlocks } from '@/hooks/queries/custom-blocks'
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
import { useFolderMap } from '@/hooks/queries/folders'
import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings'
import {
useAutoConnect,
useAutoFocusOnClick,
useSnapToGridSize,
} from '@/hooks/queries/general-settings'
import {
findLockedAncestorFolder,
isFolderOrAncestorLocked,
Expand Down Expand Up @@ -404,6 +408,8 @@ const WorkflowContent = React.memo(
const autoConnectRef = useRef(isAutoConnectEnabled)
autoConnectRef.current = isAutoConnectEnabled

const isAutoFocusOnClickEnabled = useAutoFocusOnClick()

// Panel open states for context menu
const isVariablesOpen = useVariablesModalStore((state) => state.isOpen)
const isChatOpen = useChatStore((state) => state.isChatOpen)
Expand Down Expand Up @@ -4320,8 +4326,9 @@ const WorkflowContent = React.memo(
/**
* Focus the clicked block: animate the camera so the card centers in
* the canvas frame. Plain clicks focus both regular cards and subflow
* containers; multi-select keeps the camera still.
* onNodeClick never fires after a drag.
* containers; multi-select keeps the camera still. Users who would
* rather keep their own framing turn auto-focus off in general
* settings. onNodeClick never fires after a drag.
*/
if (
!embedded &&
Expand All @@ -4330,11 +4337,27 @@ const WorkflowContent = React.memo(
node.type === 'noteBlock' ||
node.type === 'subflowNode')
) {
/**
* Marked whether or not the camera moves: with auto-focus on the
* click reframes the canvas, and with it off the click is the user
* deliberately keeping the framing they already have. Either way a
* later canvas re-init must not `fitView` over it.
*/
userFocusedWorkflowIdRef.current = activeWorkflowId ?? workflowIdParam
focusBlockInView(node)
if (isAutoFocusOnClickEnabled) {
focusBlockInView(node)
}
}
},
[activeWorkflowId, blocks, getNodes, embedded, focusBlockInView, workflowIdParam]
[
activeWorkflowId,
blocks,
getNodes,
embedded,
focusBlockInView,
isAutoFocusOnClickEnabled,
workflowIdParam,
]
)

/**
Expand Down
16 changes: 16 additions & 0 deletions apps/sim/hooks/queries/general-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface GeneralSettings {
errorNotificationsEnabled: boolean
snapToGridSize: number
showActionBar: boolean
/** Whether clicking a block on the canvas animates the camera to center it. */
autoFocusOnClick: boolean
/** Copilot tool ids the user picked "always allow" for. */
copilotAutoAllowedTools: string[]
/** Saved IANA timezone, or `null` when unset (the app falls back to the browser zone). */
Expand All @@ -57,6 +59,7 @@ export function mapGeneralSettingsResponse(data: UserSettingsApi): GeneralSettin
errorNotificationsEnabled: data.errorNotificationsEnabled,
snapToGridSize: data.snapToGridSize,
showActionBar: data.showActionBar,
autoFocusOnClick: data.autoFocusOnClick,
copilotAutoAllowedTools: data.copilotAutoAllowedTools ?? [],
timezone: data.timezone ?? null,
}
Expand Down Expand Up @@ -122,6 +125,19 @@ export function useShowActionBar(): boolean {
return data?.showActionBar ?? true
}

/**
* Whether the canvas camera animates to center a block when it is clicked.
*
* Scoped to clicks only. Arrow-key navigation and block creation deliberately
* keep following the camera regardless — both move selection to a block that
* may be off-screen, so suppressing the move would leave the user with no way
* to tell where the selection went.
*/
Comment thread
waleedlatif1 marked this conversation as resolved.
export function useAutoFocusOnClick(): boolean {
const { data } = useGeneralSettings()
return data?.autoFocusOnClick ?? true
}

export function useBillingUsageNotifications(): boolean {
const { data } = useGeneralSettings()
return data?.billingUsageNotificationsEnabled ?? true
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/api/contracts/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export const userSettingsSchema = z.object({
errorNotificationsEnabled: z.boolean().default(true),
snapToGridSize: z.number().min(0).max(50).default(0),
showActionBar: z.boolean().default(true),
/** Whether clicking a block on the canvas animates the camera to center it. */
autoFocusOnClick: z.boolean().default(true),
/** Copilot tool ids the user chose "always allow" for, so they are never prompted for them again. */
copilotAutoAllowedTools: z.array(z.string()).default([]),
/** IANA timezone for scheduling; `null` means the client falls back to the browser-detected zone. */
Expand All @@ -110,6 +112,7 @@ export const updateUserSettingsBodySchema = z.object({
errorNotificationsEnabled: z.boolean().optional(),
snapToGridSize: z.number().min(0).max(50).optional(),
showActionBar: z.boolean().optional(),
autoFocusOnClick: z.boolean().optional(),
copilotAutoAllowedTools: z.array(z.string()).optional(),
/** IANA timezone; explicit `null` resets to the browser-detected zone. */
timezone: ianaTimezoneSchema.nullable().optional(),
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/users/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const defaultUserSettings: UserSettingsApi = {
errorNotificationsEnabled: true,
snapToGridSize: 0,
showActionBar: true,
autoFocusOnClick: true,
copilotAutoAllowedTools: [],
timezone: null,
lastActiveWorkspaceId: null,
Expand All @@ -50,6 +51,7 @@ export async function getUserSettings(userId: string | null): Promise<UserSettin
errorNotificationsEnabled: settings.errorNotificationsEnabled,
snapToGridSize: settings.snapToGridSize,
showActionBar: settings.showActionBar,
autoFocusOnClick: settings.autoFocusOnClick,
copilotAutoAllowedTools: settings.copilotAutoAllowedTools,
timezone: settings.timezone,
lastActiveWorkspaceId: settings.lastActiveWorkspaceId,
Expand All @@ -76,6 +78,7 @@ export async function getUserSettings(userId: string | null): Promise<UserSettin
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
snapToGridSize: userSettings.snapToGridSize ?? 0,
showActionBar: userSettings.showActionBar ?? true,
autoFocusOnClick: userSettings.autoFocusOnClick ?? true,
copilotAutoAllowedTools: normalizeStringArray(userSettings.copilotAutoAllowedTools),
timezone: userSettings.timezone ?? null,
lastActiveWorkspaceId: userSettings.lastActiveWorkspaceId ?? null,
Expand Down
Binary file modified apps/sim/public/tooltips/auto-connect-on-drop.mp4
Binary file not shown.
Binary file added apps/sim/public/tooltips/auto-focus-on-click.mp4
Binary file not shown.
Binary file modified apps/sim/public/tooltips/canvas-error-notification.mp4
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/db/migrations/0290_settings_auto_focus_on_click.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- 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.
ALTER TABLE "settings" ADD COLUMN "auto_focus_on_click" boolean DEFAULT true NOT NULL;
Loading
Loading