Fix Entering special character in object's field name crashes the app (#14455)

Fixes https://github.com/twentyhq/twenty/issues/14289.

We should leave the error management to the form validation, and only
throw in the frame of this validation where it will be properly handled.
This is enough to prevent form submission with an undesired value.
So we can use an empty metadata name everywhere else on the page to
prevent crash.
<img width="708" height="229" alt="Capture d’écran 2025-09-12 à 17 27
51"
src="https://github.com/user-attachments/assets/f257c220-674e-49fc-b560-5ad585ae6c5f"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2025-09-12 20:03:55 +02:00
committed by GitHub
parent a4cf534130
commit 1f333503eb
14 changed files with 44 additions and 21 deletions
@@ -1,8 +1,8 @@
import { errors } from '@/settings/data-model/fields/forms/utils/errorMessages';
import { z } from 'zod';
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabelOrThrow';
import { METADATA_LABEL_VALID_PATTERN } from '~/pages/settings/data-model/constants/MetadataLabelValidPattern';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
export const metadataLabelSchema = (existingLabels?: string[]) => {
return z
.string()
@@ -12,7 +12,7 @@ export const metadataLabelSchema = (existingLabels?: string[]) => {
.refine(
(label) => {
try {
computeMetadataNameFromLabel(label);
computeMetadataNameFromLabelOrThrow(label);
return true;
} catch {
return false;
@@ -28,7 +28,9 @@ export const metadataLabelSchema = (existingLabels?: string[]) => {
if (!existingLabels || !label?.length) {
return true;
}
return !existingLabels.includes(computeMetadataNameFromLabel(label));
return !existingLabels.includes(
computeMetadataNameFromLabelOrThrow(label),
);
} catch {
return false;
}
@@ -2,7 +2,7 @@ import { z } from 'zod';
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { themeColorSchema } from 'twenty-ui/theme';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/computeOptionValueFromLabel';
const selectOptionSchema = z
.object({
@@ -22,7 +22,7 @@ import {
TooltipDelay,
} from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel';
export const settingsDataModelFieldIconLabelFormSchema = (
existingOtherLabels: string[] = [],
@@ -20,7 +20,7 @@ import {
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem, MenuItemSelectColor } from 'twenty-ui/navigation';
import { MAIN_COLOR_NAMES } from 'twenty-ui/theme';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/computeOptionValueFromLabel';
type SettingsDataModelFieldSelectFormOptionRowProps = {
className?: string;
@@ -5,7 +5,7 @@ import { v4 } from 'uuid';
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { type SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/computeOptionValueFromLabel';
const DEFAULT_OPTION: FieldMetadataItemOption = {
color: 'green',
@@ -2,8 +2,8 @@ import { v4 } from 'uuid';
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { generateNewSelectOptionLabel } from '@/settings/data-model/fields/forms/select/utils/generateNewSelectOptionLabel';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
import { getNextThemeColor } from 'twenty-ui/theme';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/computeOptionValueFromLabel';
export const generateNewSelectOption = (
options: FieldMetadataItemOption[],
@@ -5,7 +5,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { SettingsDataModelFieldPreview } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview';
import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary';
import { Card, CardContent } from 'twenty-ui/layout';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel';
type SettingsDataModelFieldPreviewWidgetProps = {
className?: string;
@@ -20,7 +20,7 @@ import {
} from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { type StringKeyOf } from 'type-fest';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel';
type SettingsDataModelObjectAboutFormProps = {
disableEdition?: boolean;
@@ -8,7 +8,7 @@ import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import { isDefined } from 'twenty-shared/utils';
import { useGetRolesQuery } from '~/generated-metadata/graphql';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel';
import { type SettingsAIAgentFormValues } from '../../hooks/useSettingsAgentFormState';
const StyledFormContainer = styled.div`
@@ -1,9 +0,0 @@
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
describe('computeMetadataNameFromLabel', () => {
it('computes name for label with non-latin char', () => {
const label = 'λλλ!';
expect(computeMetadataNameFromLabel(label)).toEqual('lll');
});
});
@@ -0,0 +1,21 @@
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel';
describe('computeMetadataNameFromLabel', () => {
it('computes name for label with non-latin char', () => {
const label = 'λλλ!';
expect(computeMetadataNameFromLabel(label)).toEqual('lll');
});
it('returns empty string for empty label', () => {
const label = '';
expect(computeMetadataNameFromLabel(label)).toEqual('');
});
it('returns empty string for invalid label', () => {
const label = '/';
expect(computeMetadataNameFromLabel(label)).toEqual('');
});
});
@@ -0,0 +1,9 @@
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabelOrThrow';
export const computeMetadataNameFromLabel = (label: string): string => {
try {
return computeMetadataNameFromLabelOrThrow(label);
} catch {
return '';
}
};
@@ -1,7 +1,7 @@
import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration';
export const computeMetadataNameFromLabel = (label: string): string => {
export const computeMetadataNameFromLabelOrThrow = (label: string): string => {
const prefixedLabel = /^\d/.test(label) ? `n${label}` : label;
if (prefixedLabel === '') {