[Fix] Forbid default value removal for non-nullable select field (#16465)

Permanently fixes https://github.com/twentyhq/private-issues/issues/389

On a non-nullable select field, default value should not be removable,
otherwise users won't be able to create new records from the interface.
This PR enforces this in FE and BE.


https://github.com/user-attachments/assets/1dfa2bcc-b9df-4a00-9915-679d36ec1b25
This commit is contained in:
Marie
2025-12-11 00:53:48 +01:00
committed by GitHub
parent bed5270f3c
commit 7a7f36d38c
3 changed files with 62 additions and 35 deletions
@@ -17,6 +17,7 @@ import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { toSpliced } from '~/utils/array/toSpliced';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
import { useTheme } from '@emotion/react';
@@ -119,6 +120,10 @@ export const SettingsDataModelFieldSelectForm = ({
useSelectSettingsFormInitialValues({
fieldMetadataId: existingFieldMetadataId,
});
const { fieldMetadataItem } = useFieldMetadataItemById(
existingFieldMetadataId,
);
const isNullable = fieldMetadataItem?.isNullable;
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
@@ -340,6 +345,7 @@ export const SettingsDataModelFieldSelectForm = ({
onChange(nextOptions);
}}
isDefault={isOptionDefaultValue(option.value)}
fieldIsNullable={!!isNullable}
onSetAsDefault={() => {
if (disabled) {
return;
@@ -32,6 +32,7 @@ type SettingsDataModelFieldSelectFormOptionRowProps = {
onInputEnter?: () => void;
option: FieldMetadataItemOption;
isNewRow?: boolean;
fieldIsNullable?: boolean;
};
const StyledRow = styled.div`
@@ -76,6 +77,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
onInputEnter,
option,
isNewRow,
fieldIsNullable,
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
const theme = useTheme();
const SELECT_COLOR_DROPDOWN_ID = `select-color-dropdown-${option.id}`;
@@ -84,6 +86,8 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
const { closeDropdown: closeColorDropdown } = useCloseDropdown();
const { closeDropdown: closeActionsDropdown } = useCloseDropdown();
const shouldForbidRemoveAsDefault = isDefault && !fieldIsNullable;
const handleInputEnter = () => {
onInputEnter?.();
};
@@ -157,43 +161,49 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
dropdownId={SELECT_ACTIONS_DROPDOWN_ID}
dropdownPlacement="right-start"
clickableComponent={
<StyledLightIconButton accent="tertiary" Icon={IconDotsVertical} />
<StyledLightIconButton
accent="tertiary"
Icon={IconDotsVertical}
disabled={shouldForbidRemoveAsDefault}
/>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
{isDefault ? (
<MenuItem
LeftIcon={IconX}
text={t`Remove as default`}
onClick={() => {
onRemoveAsDefault?.();
closeActionsDropdown(SELECT_ACTIONS_DROPDOWN_ID);
}}
/>
) : (
<MenuItem
LeftIcon={IconCheck}
text={t`Set as default`}
onClick={() => {
onSetAsDefault?.();
closeActionsDropdown(SELECT_ACTIONS_DROPDOWN_ID);
}}
/>
)}
{!!onRemove && !isDefault && (
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text={t`Remove option`}
onClick={() => {
onRemove();
closeActionsDropdown(SELECT_ACTIONS_DROPDOWN_ID);
}}
/>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
shouldForbidRemoveAsDefault ? null : (
<DropdownContent>
<DropdownMenuItemsContainer>
{isDefault ? (
<MenuItem
LeftIcon={IconX}
text={t`Remove as default`}
onClick={() => {
onRemoveAsDefault?.();
closeActionsDropdown(SELECT_ACTIONS_DROPDOWN_ID);
}}
/>
) : (
<MenuItem
LeftIcon={IconCheck}
text={t`Set as default`}
onClick={() => {
onSetAsDefault?.();
closeActionsDropdown(SELECT_ACTIONS_DROPDOWN_ID);
}}
/>
)}
{!!onRemove && !isDefault && (
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text={t`Remove option`}
onClick={() => {
onRemove();
closeActionsDropdown(SELECT_ACTIONS_DROPDOWN_ID);
}}
/>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
)
}
/>
</StyledRow>
@@ -158,6 +158,17 @@ export class FlatFieldMetadataValidatorService {
});
}
if (
flatFieldMetadataToValidate.isNullable === false &&
flatFieldMetadataToValidate.defaultValue === null
) {
validationResult.errors.push({
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: 'Default value cannot be null for non-nullable fields',
userFriendlyMessage: msg`Default value cannot be null.`,
});
}
const fieldMetadataTypeValidationErrors =
this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities(
{