Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) => {
Expand All @@ -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));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type FieldTextMetadata = BaseFieldMetadata & {
placeHolder: string;
settings?: {
displayedMaxRows?: number;
validationPattern?: string;
validationErrorMessage?: string;
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
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 = {
disabled?: boolean;
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,
Expand All @@ -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,
Expand All @@ -34,36 +79,122 @@ export const SettingsDataModelFieldTextForm = ({
const { fieldMetadataItem: existingFieldMetadataItem } =
useFieldMetadataItemById(existingFieldMetadataId);

const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
const {
control,
formState: { errors },
} = useFormContext<SettingsDataModelFieldTextFormValues>();
const patternError = errors.settings?.validationPattern?.message;

const [validationEnabled, setValidationEnabled] = useState(
!!existingFieldMetadataItem?.settings?.validationPattern,
);

return (
<Controller
name="settings"
defaultValue={{
displayedMaxRows:
existingFieldMetadataItem?.settings?.displayedMaxRows || 0,
validationPattern:
existingFieldMetadataItem?.settings?.validationPattern ?? '',
validationErrorMessage:
existingFieldMetadataItem?.settings?.validationErrorMessage ?? '',
}}
control={control}
render={({ field: { onChange, value } }) => {
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 (
<SettingsOptionCardContentSelect
Icon={IconTextWrap}
title={t`Wrap on record pages`}
description={t`Display text on multiple lines`}
>
<Select<number>
dropdownId="text-wrap"
value={displayedMaxRows}
onChange={(value) => onChange({ displayedMaxRows: value })}
<>
<SettingsOptionCardContentSelect
Icon={IconTextWrap}
title={t`Wrap on record pages`}
description={t`Display text on multiple lines`}
>
<Select<number>
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"
/>
</SettingsOptionCardContentSelect>
<Separator />
<SettingsOptionCardContentToggle
Icon={IconCode}
title={t`Validation pattern`}
description={t`Require values to match a regex`}
checked={validationEnabled}
onChange={handleToggle}
disabled={disabled}
options={TEXT_DATA_MODEL_SELECT_OPTIONS.map((option) => ({
...option,
label: t(option.label),
}))}
selectSizeVariant="small"
/>
</SettingsOptionCardContentSelect>
{validationEnabled && (
<StyledSettingsCardContent alignItems="flex-start">
<StyledSettingsCardIcon>
<SettingsOptionIconCustomizer Icon={IconCode} />
</StyledSettingsCardIcon>
<StyledSettingsCardTextContainer>
<StyledSettingsCardTitle>{t`Pattern & error message`}</StyledSettingsCardTitle>
<StyledSettingsCardDescription>
{t`Regex the field value must match`}
</StyledSettingsCardDescription>
<StyledValidationContainer>
<StyledInputRow>
<TextInput
value={validationPattern}
onChange={(newValue) =>
onChange({
...value,
validationPattern: newValue,
})
}
placeholder="^[A-Z0-9]+$"
disabled={disabled}
error={patternError}
fullWidth
/>
</StyledInputRow>
<StyledInputRow>
<TextInput
value={validationErrorMessage}
onChange={(newValue) =>
onChange({
...value,
validationErrorMessage: newValue,
})
}
placeholder={t`Custom error message (optional)`}
disabled={disabled}
fullWidth
/>
</StyledInputRow>
</StyledValidationContainer>
</StyledSettingsCardTextContainer>
</StyledSettingsCardContent>
)}
</>
);
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading