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:
@@ -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}
|
||||
|
||||
+30
-2
@@ -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: [],
|
||||
},
|
||||
};
|
||||
|
||||
+20
-7
@@ -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>
|
||||
|
||||
+10
-9
@@ -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>
|
||||
|
||||
|
||||
+10
-9
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user