diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started.mdx index fb30871177b43..f30501de0334b 100644 --- a/packages/twenty-docs/l/tr/developers/extend/apps/getting-started.mdx +++ b/packages/twenty-docs/l/tr/developers/extend/apps/getting-started.mdx @@ -13,9 +13,9 @@ Uygulamalar, Twenty'yi özel nesneler, alanlar, mantık işlevleri, Yapay Zeka y * Veri modelinizi şekillendirmek için özel nesneler, alanlar, görünümler ve gezinti öğeleri * HTTP rotaları, cron zamanlamaları veya veritabanı olayları tarafından tetiklenen mantık işlevleri -* Twenty'nin UI'si içinde doğrudan görüntülenen ön bileşenler +* Twenty'nin UI'si içinde doğrudan görüntülenen ön uç bileşenleri * Twenty'nin yapay zeka ajanlarını genişleten beceriler -* Bir uygulamayı birden çok çalışma alanına dağıtma +* Bir uygulamayı birden çok çalışma alanına dağıtın ## Ön Gereksinimler diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/TextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/TextFieldInput.tsx index 8b7d7757cd34c..0c0f8414f98e6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/TextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/TextFieldInput.tsx @@ -5,8 +5,13 @@ import { useTextField } from '@/object-record/record-field/ui/meta-types/hooks/u import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState'; import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useLingui } from '@lingui/react/macro'; import { useContext } from 'react'; import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly'; @@ -21,8 +26,46 @@ export const TextFieldInput = () => { RecordFieldComponentInstanceContext, ); + const { enqueueErrorSnackBar } = useSnackBar(); + const { t } = useLingui(); + + const setRecordFieldInputIsFieldInError = useSetAtomComponentState( + recordFieldInputIsFieldInErrorComponentState, + ); + + const validationPattern = + fieldDefinition.metadata.settings?.validationPattern; + const customValidationErrorMessage = + fieldDefinition.metadata.settings?.validationErrorMessage; + + const isTextValid = (text: string) => { + const trimmedText = text.trim(); + + if (!isNonEmptyString(validationPattern) || trimmedText === '') { + return true; + } + + return new RegExp(validationPattern).test(trimmedText); + }; + + const validateAndNotifyOnError = (text: string) => { + const isValid = isTextValid(text); + + if (!isValid) { + enqueueErrorSnackBar({ + message: isNonEmptyString(customValidationErrorMessage) + ? customValidationErrorMessage + : t`Value does not match the required format`, + }); + } + + return isValid; + }; + const handleEnter = (newText: string) => { - onEnter?.({ newValue: newText.trim() }); + if (validateAndNotifyOnError(newText)) { + onEnter?.({ newValue: newText.trim() }); + } }; const handleEscape = (newText: string) => { @@ -33,21 +76,28 @@ export const TextFieldInput = () => { event: MouseEvent | TouchEvent, newText: string, ) => { - onClickOutside?.({ - newValue: newText.trim(), - event, - }); + if (validateAndNotifyOnError(newText)) { + onClickOutside?.({ + newValue: newText.trim(), + event, + }); + } }; const handleTab = (newText: string) => { - onTab?.({ newValue: newText.trim() }); + if (validateAndNotifyOnError(newText)) { + onTab?.({ newValue: newText.trim() }); + } }; const handleShiftTab = (newText: string) => { - onShiftTab?.({ newValue: newText.trim() }); + if (validateAndNotifyOnError(newText)) { + onShiftTab?.({ newValue: newText.trim() }); + } }; const handleChange = (newText: string) => { + setRecordFieldInputIsFieldInError(!isTextValid(newText)); setDraftValue(turnIntoUndefinedIfWhitespacesOnly(newText)); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts index b51dabdf9b677..2a7e09bb3d6f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/types/FieldMetadata.ts @@ -31,6 +31,8 @@ export type FieldTextMetadata = BaseFieldMetadata & { placeHolder: string; settings?: { displayedMaxRows?: number; + validationPattern?: string; + validationErrorMessage?: string; }; }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx index 7e05ada426952..58f196edfe616 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx @@ -1,11 +1,25 @@ import { Controller, useFormContext } from 'react-hook-form'; import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { Separator } from '@/settings/components/Separator'; +import { + StyledSettingsCardContent, + StyledSettingsCardDescription, + StyledSettingsCardIcon, + StyledSettingsCardTextContainer, + StyledSettingsCardTitle, +} from '@/settings/components/SettingsOptions/SettingsCardContentBase'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; +import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; +import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer'; import { TEXT_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/components/text/constants/TextDataModelSelectOptions'; +import { TextInput } from '@/ui/input/components/TextInput'; import { Select } from '@/ui/input/components/Select'; import { useLingui } from '@lingui/react/macro'; -import { IconTextWrap } from 'twenty-ui/display'; +import { styled } from '@linaria/react'; +import { useState } from 'react'; +import { IconCode, IconTextWrap } from 'twenty-ui/display'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; import { z } from 'zod'; type SettingsDataModelFieldTextFormProps = { @@ -13,9 +27,25 @@ type SettingsDataModelFieldTextFormProps = { existingFieldMetadataId: string; }; -const textFieldDefaultValueSchema = z.object({ - displayedMaxRows: z.number().nullable(), -}); +const textFieldDefaultValueSchema = z + .object({ + displayedMaxRows: z.number().nullable(), + validationPattern: z.string().optional(), + validationErrorMessage: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.validationPattern) { + try { + new RegExp(data.validationPattern); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['validationPattern'], + message: 'Invalid regular expression', + }); + } + } + }); export const settingsDataModelFieldTextFormSchema = z.object({ settings: textFieldDefaultValueSchema, @@ -25,6 +55,21 @@ export type SettingsDataModelFieldTextFormValues = z.infer< typeof settingsDataModelFieldTextFormSchema >; +const StyledValidationContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; + margin-top: ${themeCssVariables.spacing[2]}; + width: 100%; +`; + +const StyledInputRow = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[1]}; + width: 100%; +`; + export const SettingsDataModelFieldTextForm = ({ disabled, existingFieldMetadataId, @@ -34,36 +79,122 @@ export const SettingsDataModelFieldTextForm = ({ const { fieldMetadataItem: existingFieldMetadataItem } = useFieldMetadataItemById(existingFieldMetadataId); - const { control } = useFormContext(); + const { + control, + formState: { errors }, + } = useFormContext(); + const patternError = errors.settings?.validationPattern?.message; + + const [validationEnabled, setValidationEnabled] = useState( + !!existingFieldMetadataItem?.settings?.validationPattern, + ); + return ( { const displayedMaxRows = value?.displayedMaxRows ?? 0; + const validationPattern = value?.validationPattern ?? ''; + const validationErrorMessage = value?.validationErrorMessage ?? ''; + + const handleToggle = (enabled: boolean) => { + setValidationEnabled(enabled); + if (!enabled) { + onChange({ + ...value, + validationPattern: '', + validationErrorMessage: '', + }); + } + }; return ( - - - dropdownId="text-wrap" - value={displayedMaxRows} - onChange={(value) => onChange({ displayedMaxRows: value })} + <> + + + dropdownId="text-wrap" + value={displayedMaxRows} + onChange={(newValue) => + onChange({ + ...value, + displayedMaxRows: newValue, + }) + } + disabled={disabled} + options={TEXT_DATA_MODEL_SELECT_OPTIONS.map((option) => ({ + ...option, + label: t(option.label), + }))} + selectSizeVariant="small" + /> + + + ({ - ...option, - label: t(option.label), - }))} - selectSizeVariant="small" /> - + {validationEnabled && ( + + + + + + {t`Pattern & error message`} + + {t`Regex the field value must match`} + + + + + onChange({ + ...value, + validationPattern: newValue, + }) + } + placeholder="^[A-Z0-9]+$" + disabled={disabled} + error={patternError} + fullWidth + /> + + + + onChange({ + ...value, + validationErrorMessage: newValue, + }) + } + placeholder={t`Custom error message (optional)`} + disabled={disabled} + fullWidth + /> + + + + + )} + ); }} /> diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts index 43a3ac9cd7688..952be375ce0ee 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts @@ -189,7 +189,11 @@ export class DataArgProcessorService { return validateNumberFieldOrThrow(value, key); } case FieldMetadataType.TEXT: { - const validatedValue = validateTextFieldOrThrow(value, key); + const validatedValue = validateTextFieldOrThrow( + value, + key, + fieldMetadata.settings as FieldMetadataSettingsMapping[FieldMetadataType.TEXT], + ); return transformTextField(validatedValue); } diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-text-field-or-throw.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-text-field-or-throw.util.spec.ts index f5f4ffef8fb92..e48f1fa8f51a8 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-text-field-or-throw.util.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/__tests__/validate-text-field-or-throw.util.spec.ts @@ -19,6 +19,38 @@ describe('validateTextFieldOrThrow', () => { expect(result).toBe('hello world'); }); + + it('should return the string when it matches the validation pattern', () => { + const result = validateTextFieldOrThrow('ABC123', 'testField', { + validationPattern: '^[A-Z]{3}[0-9]{3}$', + }); + + expect(result).toBe('ABC123'); + }); + + it('should not enforce the pattern when value is an empty string', () => { + const result = validateTextFieldOrThrow('', 'testField', { + validationPattern: '^[A-Z]{3}[0-9]{3}$', + }); + + expect(result).toBe(''); + }); + + it('should not enforce the pattern when value is whitespace only', () => { + const result = validateTextFieldOrThrow(' ', 'testField', { + validationPattern: '^[A-Z]{3}[0-9]{3}$', + }); + + expect(result).toBe(' '); + }); + + it('should not enforce the pattern when validationPattern is an empty string', () => { + const result = validateTextFieldOrThrow('anything', 'testField', { + validationPattern: '', + }); + + expect(result).toBe('anything'); + }); }); describe('invalid inputs', () => { @@ -65,5 +97,15 @@ describe('validateTextFieldOrThrow', () => { 'Invalid string value { key: \'value\' } for text field "testField"', ); }); + + it('should throw when value does not match the validation pattern', () => { + expect(() => + validateTextFieldOrThrow('not-a-match', 'testField', { + validationPattern: '^[A-Z]{3}[0-9]{3}$', + }), + ).toThrow( + 'Field "testField" value does not match pattern ^[A-Z]{3}[0-9]{3}$', + ); + }); }); }); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts index ba55c50958c48..73de9633306e3 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util.ts @@ -1,7 +1,11 @@ import { inspect } from 'util'; import { msg } from '@lingui/core/macro'; -import { isNull } from '@sniptt/guards'; +import { isNonEmptyString, isNull } from '@sniptt/guards'; +import { + type FieldMetadataSettingsMapping, + type FieldMetadataType, +} from 'twenty-shared/types'; import { CommonQueryRunnerException, @@ -11,6 +15,7 @@ import { export const validateTextFieldOrThrow = ( value: unknown, fieldName: string, + settings?: FieldMetadataSettingsMapping[FieldMetadataType.TEXT] | null, ): string | null => { if (typeof value !== 'string' && !isNull(value)) { const inspectedValue = inspect(value); @@ -22,5 +27,31 @@ export const validateTextFieldOrThrow = ( ); } + if ( + typeof value !== 'string' || + value.trim() === '' || + !isNonEmptyString(settings?.validationPattern) + ) { + return value; + } + + const doesValueMatchPattern = new RegExp(settings.validationPattern).test( + value, + ); + + if (!doesValueMatchPattern) { + const customErrorMessage = settings.validationErrorMessage; + + throw new CommonQueryRunnerException( + `Field "${fieldName}" value does not match pattern ${settings.validationPattern}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + { + userFriendlyMessage: isNonEmptyString(customErrorMessage) + ? msg`${customErrorMessage}` + : msg`Value does not match the required format`, + }, + ); + } + return value; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts index 8b5bf345123b0..690fd34e534f0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts @@ -15,6 +15,7 @@ import { validateFilesFlatFieldMetadata } from 'src/engine/metadata-modules/flat import { validateMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-metadata.util'; import { validateMorphRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-flat-field-metadata.util'; import { validatePositionFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-position-flat-field-metadata.util'; +import { validateTextFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-text-flat-field-metadata.util'; import { validateTsVectorFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-ts-vector-flat-field-metadata.util'; const DEFAULT_NO_VALIDATION = (): FlatFieldMetadataValidationError[] => []; @@ -72,7 +73,7 @@ export class FlatFieldMetadataTypeValidatorService { POSITION: validatePositionFlatFieldMetadata, RAW_JSON: DEFAULT_NO_VALIDATION, RICH_TEXT: DEFAULT_NO_VALIDATION, - TEXT: DEFAULT_NO_VALIDATION, + TEXT: validateTextFlatFieldMetadata, TS_VECTOR: validateTsVectorFlatFieldMetadata, UUID: DEFAULT_NO_VALIDATION, MORPH_RELATION: validateMorphRelationFlatFieldMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/__tests__/validate-text-flat-field-metadata.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/__tests__/validate-text-flat-field-metadata.util.spec.ts new file mode 100644 index 0000000000000..f3d35527c58fb --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/__tests__/validate-text-flat-field-metadata.util.spec.ts @@ -0,0 +1,72 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; +import { validateTextFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-text-flat-field-metadata.util'; +import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; + +const createFlatEntityToValidate = ( + overrides: Partial> = {}, +): UniversalFlatFieldMetadata => + ({ + type: FieldMetadataType.TEXT, + name: 'testTextField', + label: 'Test Text Field', + universalSettings: {}, + isUnique: false, + ...overrides, + }) as UniversalFlatFieldMetadata; + +const callValidator = ( + flatEntityToValidate: UniversalFlatFieldMetadata, +) => + validateTextFlatFieldMetadata({ + flatEntityToValidate, + } as Parameters[0]); + +const stripUserFriendlyMessage = (errors: FlatFieldMetadataValidationError[]) => + errors.map(({ userFriendlyMessage: _, ...rest }) => rest); + +describe('validateTextFlatFieldMetadata', () => { + it('should return no errors when no validation pattern is set', () => { + const errors = callValidator(createFlatEntityToValidate()); + + expect(errors).toMatchInlineSnapshot('[]'); + }); + + it('should return no errors for a valid validation pattern', () => { + const errors = callValidator( + createFlatEntityToValidate({ + universalSettings: { validationPattern: '^[A-Z]{3}[0-9]{3}$' }, + }), + ); + + expect(errors).toMatchInlineSnapshot('[]'); + }); + + it('should return no errors when the validation pattern is an empty string', () => { + const errors = callValidator( + createFlatEntityToValidate({ + universalSettings: { validationPattern: '' }, + }), + ); + + expect(errors).toMatchInlineSnapshot('[]'); + }); + + it('should return an error when the validation pattern is not a valid regular expression', () => { + const errors = callValidator( + createFlatEntityToValidate({ + universalSettings: { validationPattern: '[unclosed' }, + }), + ); + + expect(stripUserFriendlyMessage(errors)).toMatchInlineSnapshot(` +[ + { + "code": "INVALID_FIELD_INPUT", + "message": "validationPattern is not a valid regular expression: [unclosed", + }, +] +`); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-text-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-text-flat-field-metadata.util.ts new file mode 100644 index 0000000000000..85d4978f354b2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-text-flat-field-metadata.util.ts @@ -0,0 +1,29 @@ +import { msg } from '@lingui/core/macro'; +import { isNonEmptyString } from '@sniptt/guards'; +import { type FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type'; +import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; + +export const validateTextFlatFieldMetadata = ({ + flatEntityToValidate, +}: FlatFieldMetadataTypeValidationArgs): FlatFieldMetadataValidationError[] => { + const errors: FlatFieldMetadataValidationError[] = []; + + const pattern = flatEntityToValidate?.universalSettings?.validationPattern; + + if (isNonEmptyString(pattern)) { + try { + new RegExp(pattern); + } catch { + errors.push({ + code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + message: `validationPattern is not a valid regular expression: ${pattern}`, + userFriendlyMessage: msg`The validation pattern is not a valid regular expression`, + }); + } + } + + return errors; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts index f0a4c186b6036..473b79ddf5488 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts @@ -127,6 +127,8 @@ type SettingsExpectedResult = } | { displayedMaxRows?: number | undefined; + validationPattern?: string | undefined; + validationErrorMessage?: string | undefined; __JsonbPropertyBrand__?: undefined; } | null; diff --git a/packages/twenty-shared/src/types/FieldMetadataSettings.ts b/packages/twenty-shared/src/types/FieldMetadataSettings.ts index cee9334ef20cf..b6fce229ae4b9 100644 --- a/packages/twenty-shared/src/types/FieldMetadataSettings.ts +++ b/packages/twenty-shared/src/types/FieldMetadataSettings.ts @@ -28,6 +28,8 @@ type FieldMetadataNumberSettings = { type FieldMetadataTextSettings = { displayedMaxRows?: number; + validationPattern?: string; + validationErrorMessage?: string; }; type FieldMetadataDateSettings = {