fix: ai settings page crash (#15455)

Fixes - https://github.com/twentyhq/twenty/issues/14995

---------

Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Ranjeet Baraik
2025-11-03 21:06:27 +05:30
committed by GitHub
parent a5e53d74dc
commit 454114932a
6 changed files with 107 additions and 43 deletions
@@ -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 = <Value extends SelectValue>({
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 = <Value extends SelectValue>({
const { setSelectedItemId } = useSelectableList(dropdownId);
const handleDropdownOpen = () => {
if (selectedOption && !searchInputValue) {
if (isDefined(selectedOption) && !isNonEmptyString(searchInputValue)) {
setSelectedItemId(selectedOption.label);
}
};
if (!isDefined(selectedOption)) {
return <></>;
}
return (
<StyledContainer
className={className}
@@ -145,7 +165,7 @@ export const Select = <Value extends SelectValue>({
onBlur={onBlur}
ref={selectContainerRef}
>
{!!label && <StyledLabel>{label}</StyledLabel>}
{isNonEmptyString(label) && <StyledLabel>{label}</StyledLabel>}
{isDisabled ? (
<SelectControl
selectedOption={selectedOption}
@@ -169,17 +189,17 @@ export const Select = <Value extends SelectValue>({
}
dropdownComponents={
<DropdownContent widthInPixels={dropDownMenuWidth}>
{!!withSearchInput && (
{withSearchInput === true && (
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
onChange={(event) => setSearchInputValue(event.target.value)}
/>
)}
{!!withSearchInput && !!filteredOptions.length && (
{withSearchInput === true && isNonEmptyArray(filteredOptions) && (
<DropdownMenuSeparator />
)}
{!!filteredOptions.length && (
{isNonEmptyArray(filteredOptions) && (
<DropdownMenuItemsContainer hasMaxHeight>
<SelectableList
selectableListInstanceId={dropdownId}
@@ -213,10 +233,9 @@ export const Select = <Value extends SelectValue>({
</SelectableList>
</DropdownMenuItemsContainer>
)}
{!!callToActionButton && !!filteredOptions.length && (
<DropdownMenuSeparator />
)}
{!!callToActionButton && (
{isDefined(callToActionButton) &&
isNonEmptyArray(filteredOptions) && <DropdownMenuSeparator />}
{isDefined(callToActionButton) && (
<DropdownMenuItemsContainer hasMaxHeight scrollable={false}>
<MenuItem
onClick={callToActionButton.onClick}
@@ -229,7 +248,9 @@ export const Select = <Value extends SelectValue>({
}
/>
)}
{!!description && <StyledDescription>{description}</StyledDescription>}
{isNonEmptyString(description) && (
<StyledDescription>{description}</StyledDescription>
)}
</StyledContainer>
);
};
@@ -80,12 +80,12 @@ export const SelectControl = ({
return (
<StyledControlContainer
disabled={isDisabled}
hasIcon={isDefined(selectedOption.Icon)}
hasIcon={isDefined(selectedOption?.Icon)}
selectSizeVariant={selectSizeVariant}
textAccent={textAccent}
hasRightElement={hasRightElement}
>
{isDefined(selectedOption.Icon) ? (
{isDefined(selectedOption?.Icon) ? (
<selectedOption.Icon
color={isDisabled ? theme.font.color.light : theme.font.color.primary}
size={theme.icon.size.md}
@@ -2,9 +2,9 @@ import { type Meta, type StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/test';
import { useState } from 'react';
import { Select, type SelectProps } from '../Select';
import { ComponentDecorator } from 'twenty-ui/testing';
import { IconPlus } from 'twenty-ui/display';
import { ComponentDecorator } from 'twenty-ui/testing';
import { Select, type SelectProps } from '../Select';
type RenderProps = SelectProps<string | number | boolean | null>;
@@ -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: [],
},
};
@@ -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 = () => {
</StyledSettingsOptionCardDescription>
</div>
<StyledSelectContainer>
<Select
dropdownId="router-model-select"
value={currentWorkspace?.routerModel || 'auto'}
onChange={handleModelChange}
options={modelOptions}
selectSizeVariant="small"
/>
{noModelsAvailable ? (
<StyledErrorMessage>
{t`No models available. Please configure AI models in your workspace settings.`}
</StyledErrorMessage>
) : (
<Select
dropdownId="router-model-select"
value={currentWorkspace?.routerModel || 'auto'}
onChange={handleModelChange}
options={modelOptions}
selectSizeVariant="small"
/>
)}
</StyledSelectContainer>
</StyledSettingsOptionCardContent>
</Card>
@@ -110,18 +110,19 @@ export const SettingsAgentSettingsTab = ({
</StyledFormContainer>
<StyledFormContainer>
<Select
dropdownId="ai-model-select"
label={t`AI Model`}
value={formValues.modelId}
onChange={(value) => onFieldChange('modelId', value)}
options={modelOptions}
disabled={noModelsAvailable || disabled}
/>
{noModelsAvailable && (
{noModelsAvailable ? (
<StyledErrorMessage>
{t`No models available. Please configure AI models in your workspace settings.`}
</StyledErrorMessage>
) : (
<Select
dropdownId="ai-model-select"
label={t`AI Model`}
value={formValues.modelId}
onChange={(value) => onFieldChange('modelId', value)}
options={modelOptions}
disabled={noModelsAvailable || disabled}
/>
)}
</StyledFormContainer>
@@ -98,18 +98,19 @@ export const SettingsAIAgentForm = ({
</StyledFormContainer>
<StyledFormContainer>
<Select
dropdownId="ai-model-select"
label={t`AI Model`}
value={formValues.modelId}
onChange={(value) => onFieldChange('modelId', value)}
options={modelOptions}
disabled={noModelsAvailable || disabled}
/>
{noModelsAvailable && (
{noModelsAvailable ? (
<StyledErrorMessage>
{t`No models available. Please configure AI models in your workspace settings.`}
</StyledErrorMessage>
) : (
<Select
dropdownId="ai-model-select"
label={t`AI Model`}
value={formValues.modelId}
onChange={(value) => onFieldChange('modelId', value)}
options={modelOptions}
disabled={noModelsAvailable || disabled}
/>
)}
</StyledFormContainer>