chore: Simpler autofocus of function call (#39946)

## Description

Improves the autofocus logic as per
https://github.com/appsmithorg/appsmith/pull/39916#discussion_r2014507020



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- When adding a new function field, it now automatically receives focus,
making the configuration process smoother and more intuitive.
- The transition for adding fields has been streamlined to improve user
interaction without distractions from unnecessary scrolling behavior.
- New state management for tracking the most recently added function
field enhances user experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Hetu Nandu 2025-03-27 12:28:30 +05:30 committed by GitHub
parent afad67b434
commit 3f696facef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 22 additions and 40 deletions

View File

@ -1,5 +1,5 @@
import { Button, Text } from "@appsmith/ads"; import { Button, Text } from "@appsmith/ads";
import { default as React, useCallback, useRef, useEffect } from "react"; import React, { useCallback, useState } from "react";
import type { FieldArrayFieldsProps } from "redux-form"; import type { FieldArrayFieldsProps } from "redux-form";
import styled from "styled-components"; import styled from "styled-components";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
@ -29,17 +29,19 @@ export const FunctionCallingConfigForm = ({
fields, fields,
formName, formName,
}: FunctionCallingConfigFormProps) => { }: FunctionCallingConfigFormProps) => {
const latestFieldRef = useRef<HTMLDivElement>(null); const [newlyAddedId, setNewlyAddedId] = useState<string | null>(null);
const previousFieldsLength = useRef(fields.length);
const handleAddFunctionButtonClick = useCallback(() => { const handleAddFunctionButtonClick = useCallback(() => {
const id = uuid();
fields.push({ fields.push({
id: uuid(), id,
description: "", description: "",
entityId: "", entityId: "",
isApprovalRequired: false, isApprovalRequired: false,
entityType: "Query", entityType: "Query",
}); });
setNewlyAddedId(id);
}, [fields]); }, [fields]);
const handleRemoveToolButtonClick = useCallback( const handleRemoveToolButtonClick = useCallback(
@ -49,38 +51,6 @@ export const FunctionCallingConfigForm = ({
[fields], [fields],
); );
useEffect(
function handleAddFunction() {
// Only scroll and focus if a new field was added (length increased)
if (
fields.length > previousFieldsLength.current &&
latestFieldRef.current
) {
// Scroll the new field into view
latestFieldRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
// Focus the menu button in the latest field
// The menu button is rendered by the Menu component from @appsmith/ads
const menuButton = latestFieldRef.current.querySelector(
"button.rc-select-selector",
);
if (menuButton) {
// Use setTimeout to ensure the button is fully rendered
setTimeout(() => {
(menuButton as HTMLButtonElement).focus();
}, 100);
}
}
previousFieldsLength.current = fields.length;
},
[fields.length],
);
return ( return (
<> <>
<Header> <Header>
@ -108,14 +78,21 @@ export const FunctionCallingConfigForm = ({
) : ( ) : (
<ConfigItems> <ConfigItems>
{fields.map((field, index) => { {fields.map((field, index) => {
const isLastField = index === fields.length - 1; const fieldValue = fields.get(index);
const isNewlyAdded = fieldValue.id === newlyAddedId;
// Reset the newly added ID after rendering to ensure focus only happens once
if (isNewlyAdded) {
setTimeout(() => setNewlyAddedId(null), 100);
}
return ( return (
<div key={field} ref={isLastField ? latestFieldRef : undefined}> <div key={field}>
<FunctionCallingConfigToolField <FunctionCallingConfigToolField
fieldPath={field} fieldPath={field}
formName={formName} formName={formName}
index={index} index={index}
isLastAdded={isNewlyAdded}
onRemove={handleRemoveToolButtonClick} onRemove={handleRemoveToolButtonClick}
/> />
</div> </div>

View File

@ -12,6 +12,7 @@ interface FunctionCallingConfigToolFieldProps {
formName: string; formName: string;
index: number; index: number;
onRemove: (index: number) => void; onRemove: (index: number) => void;
isLastAdded?: boolean;
} }
const ConfigItemRoot = styled.div` const ConfigItemRoot = styled.div`
@ -39,6 +40,7 @@ const ConfigItemSelectWrapper = styled.div`
export const FunctionCallingConfigToolField = ({ export const FunctionCallingConfigToolField = ({
index, index,
isLastAdded,
onRemove, onRemove,
...props ...props
}: FunctionCallingConfigToolFieldProps) => { }: FunctionCallingConfigToolFieldProps) => {
@ -100,6 +102,7 @@ export const FunctionCallingConfigToolField = ({
<ConfigItemRow> <ConfigItemRow>
<ConfigItemSelectWrapper> <ConfigItemSelectWrapper>
<FunctionCallingMenuField <FunctionCallingMenuField
autoFocus={isLastAdded}
name={`${props.fieldPath}.entityId`} name={`${props.fieldPath}.entityId`}
onValueChange={handleFunctionChange} onValueChange={handleFunctionChange}
/> />

View File

@ -1,3 +1,4 @@
import React, { useCallback, useMemo } from "react";
import { import {
Button, Button,
Menu, Menu,
@ -12,7 +13,6 @@ import {
MenuSubTrigger, MenuSubTrigger,
MenuSubContent, MenuSubContent,
} from "@appsmith/ads"; } from "@appsmith/ads";
import React, { useCallback, useMemo } from "react";
import { Field, type WrappedFieldProps, type BaseFieldProps } from "redux-form"; import { Field, type WrappedFieldProps, type BaseFieldProps } from "redux-form";
import type { import type {
FunctionCallingEntityType, FunctionCallingEntityType,
@ -35,6 +35,7 @@ interface FunctionCallingMenuFieldProps {
value: string, value: string,
entityType: FunctionCallingEntityType, entityType: FunctionCallingEntityType,
) => void; ) => void;
autoFocus?: boolean;
} }
interface FieldRenderProps interface FieldRenderProps
@ -82,7 +83,7 @@ const getSelectedValueInfo = (
}; };
const FunctionCallingMenuFieldRender = (props: FieldRenderProps) => { const FunctionCallingMenuFieldRender = (props: FieldRenderProps) => {
const { children, disabled, input, onValueChange } = props; const { autoFocus, children, disabled, input, onValueChange } = props;
const options = useSelector(selectEntityOptions); const options = useSelector(selectEntityOptions);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -154,6 +155,7 @@ const FunctionCallingMenuFieldRender = (props: FieldRenderProps) => {
<Menu> <Menu>
<MenuTrigger> <MenuTrigger>
<Button <Button
autoFocus={autoFocus}
className="rc-select-selector" className="rc-select-selector"
disabled={disabled} disabled={disabled}
endIcon="arrow-down-s-line" endIcon="arrow-down-s-line"