From 1f333503eb08ce332edee56ba8e533aa104dd5cc Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 12 Sep 2025 20:03:55 +0200 Subject: [PATCH] Fix Entering special character in object's field name crashes the app (#14455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Capture d’écran 2025-09-12 à 17 27
51 --------- Co-authored-by: Charles Bochet --- .../validation-schemas/metadataLabelSchema.ts | 8 ++++--- .../validation-schemas/selectOptionsSchema.ts | 2 +- .../SettingsDataModelFieldIconLabelForm.tsx | 2 +- ...tingsDataModelFieldSelectFormOptionRow.tsx | 2 +- .../useSelectSettingsFormInitialValues.ts | 2 +- .../select/utils/generateNewSelectOption.ts | 2 +- .../SettingsDataModelFieldPreviewWidget.tsx | 2 +- .../SettingsDataModelObjectAboutForm.tsx | 2 +- .../forms/components/SettingsAIAgentForm.tsx | 2 +- .../compute-metadata-name-from-label.test.ts | 9 -------- .../computeMetadataNameFromLabel.test.ts | 21 +++++++++++++++++++ .../utils/computeMetadataNameFromLabel.ts | 9 ++++++++ ...=> computeMetadataNameFromLabelOrThrow.ts} | 2 +- ...tils.ts => computeOptionValueFromLabel.ts} | 0 14 files changed, 44 insertions(+), 21 deletions(-) delete mode 100644 packages/twenty-front/src/pages/settings/data-model/utils/__tests__/compute-metadata-name-from-label.test.ts create mode 100644 packages/twenty-front/src/pages/settings/data-model/utils/__tests__/computeMetadataNameFromLabel.test.ts create mode 100644 packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts rename packages/twenty-front/src/pages/settings/data-model/utils/{compute-metadata-name-from-label.utils.ts => computeMetadataNameFromLabelOrThrow.ts} (84%) rename packages/twenty-front/src/pages/settings/data-model/utils/{compute-option-value-from-label.utils.ts => computeOptionValueFromLabel.ts} (100%) diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts index d6b4d7e9a8..1e2a63af9c 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts @@ -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; } diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts index 32a45666ba..146ac96e39 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/selectOptionsSchema.ts @@ -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({ diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx index 70c514a3c6..801987b7ff 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx @@ -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[] = [], diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx index a16802b30a..8fe56568f9 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts index 3069ac5bff..cd6e880fcb 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues.ts @@ -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', diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/utils/generateNewSelectOption.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/utils/generateNewSelectOption.ts index bf8451d893..f3872e5172 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/utils/generateNewSelectOption.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/utils/generateNewSelectOption.ts @@ -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[], diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget.tsx index 2d56fe5eb9..6ac54e27fb 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx index 2345d21cf1..e28c5be4c0 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx @@ -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; 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 2bfdd443d5..3a1e2bd1ad 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 @@ -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` diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/__tests__/compute-metadata-name-from-label.test.ts b/packages/twenty-front/src/pages/settings/data-model/utils/__tests__/compute-metadata-name-from-label.test.ts deleted file mode 100644 index eb96732781..0000000000 --- a/packages/twenty-front/src/pages/settings/data-model/utils/__tests__/compute-metadata-name-from-label.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/__tests__/computeMetadataNameFromLabel.test.ts b/packages/twenty-front/src/pages/settings/data-model/utils/__tests__/computeMetadataNameFromLabel.test.ts new file mode 100644 index 0000000000..55673d3225 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/data-model/utils/__tests__/computeMetadataNameFromLabel.test.ts @@ -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(''); + }); +}); diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts new file mode 100644 index 0000000000..ec3f397a00 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts @@ -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 ''; + } +}; diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/compute-metadata-name-from-label.utils.ts b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabelOrThrow.ts similarity index 84% rename from packages/twenty-front/src/pages/settings/data-model/utils/compute-metadata-name-from-label.utils.ts rename to packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabelOrThrow.ts index 58d727bf90..766ffad4a7 100644 --- a/packages/twenty-front/src/pages/settings/data-model/utils/compute-metadata-name-from-label.utils.ts +++ b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabelOrThrow.ts @@ -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 === '') { diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/compute-option-value-from-label.utils.ts b/packages/twenty-front/src/pages/settings/data-model/utils/computeOptionValueFromLabel.ts similarity index 100% rename from packages/twenty-front/src/pages/settings/data-model/utils/compute-option-value-from-label.utils.ts rename to packages/twenty-front/src/pages/settings/data-model/utils/computeOptionValueFromLabel.ts