From 454114932ace23cfe93da6078530c48a3b3c8a4a Mon Sep 17 00:00:00 2001 From: Ranjeet Baraik <84462743+anadi45@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:06:27 +0530 Subject: [PATCH] fix: ai settings page crash (#15455) Fixes - https://github.com/twentyhq/twenty/issues/14995 --------- Co-authored-by: prastoin Co-authored-by: Lucas Bordeau --- .../modules/ui/input/components/Select.tsx | 49 +++++++++++++------ .../ui/input/components/SelectControl.tsx | 4 +- .../components/__stories__/Select.stories.tsx | 32 +++++++++++- .../components/SettingsAIRouterSettings.tsx | 27 +++++++--- .../components/SettingsAgentSettingsTab.tsx | 19 +++---- .../forms/components/SettingsAIAgentForm.tsx | 19 +++---- 6 files changed, 107 insertions(+), 43 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/input/components/Select.tsx b/packages/twenty-front/src/modules/ui/input/components/Select.tsx index b527db314a..79b3c8c274 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Select.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Select.tsx @@ -17,6 +17,7 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; import { type IconComponent } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; @@ -94,10 +95,25 @@ export const Select = ({ const [searchInputValue, setSearchInputValue] = useState(''); - const selectedOption = - options.find(({ value: key }) => key === value) || - emptyOption || - options[0]; + const selectedOption = useMemo(() => { + const fromMatchingOption = options.find( + ({ value: optionValue }) => optionValue === value, + ); + + if (isDefined(fromMatchingOption)) { + return fromMatchingOption; + } + + if (isDefined(emptyOption)) { + return emptyOption; + } + + if (options.length > 0) { + return options[0]; + } + + return null; + }, [emptyOption, options, value]); const filteredOptions = useMemo( () => @@ -132,11 +148,15 @@ export const Select = ({ const { setSelectedItemId } = useSelectableList(dropdownId); const handleDropdownOpen = () => { - if (selectedOption && !searchInputValue) { + if (isDefined(selectedOption) && !isNonEmptyString(searchInputValue)) { setSelectedItemId(selectedOption.label); } }; + if (!isDefined(selectedOption)) { + return <>; + } + return ( ({ onBlur={onBlur} ref={selectContainerRef} > - {!!label && {label}} + {isNonEmptyString(label) && {label}} {isDisabled ? ( ({ } dropdownComponents={ - {!!withSearchInput && ( + {withSearchInput === true && ( setSearchInputValue(event.target.value)} /> )} - {!!withSearchInput && !!filteredOptions.length && ( + {withSearchInput === true && isNonEmptyArray(filteredOptions) && ( )} - {!!filteredOptions.length && ( + {isNonEmptyArray(filteredOptions) && ( ({ )} - {!!callToActionButton && !!filteredOptions.length && ( - - )} - {!!callToActionButton && ( + {isDefined(callToActionButton) && + isNonEmptyArray(filteredOptions) && } + {isDefined(callToActionButton) && ( ({ } /> )} - {!!description && {description}} + {isNonEmptyString(description) && ( + {description} + )} ); }; diff --git a/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx b/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx index 574dc05998..baa521b0bb 100644 --- a/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/SelectControl.tsx @@ -80,12 +80,12 @@ export const SelectControl = ({ return ( - {isDefined(selectedOption.Icon) ? ( + {isDefined(selectedOption?.Icon) ? ( ; @@ -67,3 +67,31 @@ export const CallToActionButton: Story = { }, }, }; + +export const WithLabel: Story = { + args: { + label: 'Test label', + }, +}; + +export const WithDescription: Story = { + args: { + description: 'Test description', + }, +}; + +export const WithNullOption: Story = { + args: { + options: [ + { value: 'a', label: 'Option A' }, + { value: 'b', label: 'Option B' }, + { value: null, label: 'Option C' }, + ], + }, +}; + +export const WithoutOptions: Story = { + args: { + options: [], + }, +}; diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx index 518c699511..321af7e091 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx @@ -23,6 +23,12 @@ const StyledSelectContainer = styled.div` max-width: 120px; `; +const StyledErrorMessage = styled.div` + color: ${({ theme }) => theme.color.red}; + font-size: ${({ theme }) => theme.font.size.sm}; + margin-top: ${({ theme }) => theme.spacing(1)}; +`; + export const SettingsAIRouterSettings = () => { const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( @@ -31,6 +37,7 @@ export const SettingsAIRouterSettings = () => { const [updateWorkspace] = useUpdateWorkspaceMutation(); const modelOptions = useAiModelOptions(); + const noModelsAvailable = modelOptions.length === 0; const handleModelChange = async (value: string) => { if (!currentWorkspace?.id) { @@ -90,13 +97,19 @@ export const SettingsAIRouterSettings = () => { - + )} diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx index 0f4169cd0c..f656c1a5c2 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx @@ -110,18 +110,19 @@ export const SettingsAgentSettingsTab = ({ - onFieldChange('modelId', value)} + options={modelOptions} + disabled={noModelsAvailable || disabled} + /> )} diff --git a/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx b/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx index 0d8847716b..41e5524ad7 100644 --- a/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx +++ b/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx @@ -98,18 +98,19 @@ export const SettingsAIAgentForm = ({ - onFieldChange('modelId', value)} + options={modelOptions} + disabled={noModelsAvailable || disabled} + /> )}