refactor: use react-hook-form for Field type config forms (#5326)

Closes #4295

Note: for the sake of an easier code review, I did not rename/move some
files and added "todo" comments instead so Github is able to match those
files with their previous version.
This commit is contained in:
Thaïs
2024-05-07 21:07:56 +02:00
committed by GitHub
parent b7a2e72c32
commit bb995d5488
34 changed files with 714 additions and 1068 deletions

View File

@ -1033,7 +1033,9 @@ export type User = {
id: Scalars['UUID']['output']; id: Scalars['UUID']['output'];
lastName: Scalars['String']['output']; lastName: Scalars['String']['output'];
passwordHash?: Maybe<Scalars['String']['output']>; passwordHash?: Maybe<Scalars['String']['output']>;
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
passwordResetToken?: Maybe<Scalars['String']['output']>; passwordResetToken?: Maybe<Scalars['String']['output']>;
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
passwordResetTokenExpiresAt?: Maybe<Scalars['DateTime']['output']>; passwordResetTokenExpiresAt?: Maybe<Scalars['DateTime']['output']>;
supportUserHash?: Maybe<Scalars['String']['output']>; supportUserHash?: Maybe<Scalars['String']['output']>;
updatedAt: Scalars['DateTime']['output']; updatedAt: Scalars['DateTime']['output'];

View File

@ -69,18 +69,7 @@ export const variables = {
disableMetadataField: { disableMetadataField: {
idToUpdate: fieldId, idToUpdate: fieldId,
updatePayload: { isActive: false, label: undefined }, updatePayload: { isActive: false, label: undefined },
}, }
editMetadataField: {
idToUpdate: '2c43466a-fe9e-4005-8d08-c5836067aa6c',
updatePayload: {
defaultValue: undefined,
description: null,
icon: undefined,
label: 'New label',
name: 'newLabel',
options: undefined,
},
},
}; };
const defaultResponseData = { const defaultResponseData = {

View File

@ -68,17 +68,6 @@ const mocks = [
}, },
})), })),
}, },
{
request: {
query: queries.activateMetadataField,
variables: variables.editMetadataField,
},
result: jest.fn(() => ({
data: {
updateOneField: responseData.default,
},
})),
},
]; ];
const Wrapper = ({ children }: { children: ReactNode }) => ( const Wrapper = ({ children }: { children: ReactNode }) => (
@ -149,22 +138,4 @@ describe('useFieldMetadataItem', () => {
}); });
}); });
}); });
it('should editMetadataField', async () => {
const { result } = renderHook(() => useFieldMetadataItem(), {
wrapper: Wrapper,
});
await act(async () => {
const res = await result.current.editMetadataField({
id: fieldMetadataItem.id,
label: 'New label',
type: FieldMetadataType.Text,
});
expect(res.data).toEqual({
updateOneField: responseData.default,
});
});
});
}); });

View File

@ -1,7 +1,3 @@
import { v4 } from 'uuid';
import { FieldMetadataOption } from '@/object-metadata/types/FieldMetadataOption';
import { getDefaultValueForBackend } from '@/object-metadata/utils/getDefaultValueForBackend';
import { Field } from '~/generated/graphql'; import { Field } from '~/generated/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
@ -26,49 +22,12 @@ export const useFieldMetadataItem = () => {
) => { ) => {
const formattedInput = formatFieldMetadataItemInput(input); const formattedInput = formatFieldMetadataItemInput(input);
const defaultValue = getDefaultValueForBackend(
input.defaultValue ?? formattedInput.defaultValue,
input.type,
);
return createOneFieldMetadataItem({ return createOneFieldMetadataItem({
...formattedInput, ...formattedInput,
defaultValue,
objectMetadataId: input.objectMetadataId, objectMetadataId: input.objectMetadataId,
type: input.type, type: input.type,
}); label: formattedInput.label ?? '',
}; name: formattedInput.name ?? '',
const editMetadataField = (
input: Pick<
Field,
| 'id'
| 'label'
| 'icon'
| 'description'
| 'defaultValue'
| 'type'
| 'options'
>,
) => {
// In Edit mode, all options need an id,
// so we generate an id for newly created options.
const inputOptions = input.options?.map((option: FieldMetadataOption) =>
option.id ? option : { ...option, id: v4() },
);
const formattedInput = formatFieldMetadataItemInput({
...input,
options: inputOptions,
});
const defaultValue = input.defaultValue ?? formattedInput.defaultValue;
return updateOneFieldMetadataItem({
fieldMetadataIdToUpdate: input.id,
updatePayload: {
...formattedInput,
defaultValue,
},
}); });
}; };
@ -92,6 +51,5 @@ export const useFieldMetadataItem = () => {
createMetadataField, createMetadataField,
disableMetadataField, disableMetadataField,
eraseMetadataField, eraseMetadataField,
editMetadataField,
}; };
}; };

View File

@ -12,7 +12,14 @@ import { FieldMetadataItem } from '../types/FieldMetadataItem';
export const useGetRelationMetadata = () => export const useGetRelationMetadata = () =>
useRecoilCallback( useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
({ fieldMetadataItem }: { fieldMetadataItem: FieldMetadataItem }) => { ({
fieldMetadataItem,
}: {
fieldMetadataItem: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
}) => {
if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null; if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null;
const relationMetadata = const relationMetadata =

View File

@ -77,7 +77,7 @@ describe('formatFieldMetadataItemInput', () => {
value: 'OPTION_2', value: 'OPTION_2',
}, },
], ],
defaultValue: 'OPTION_1', defaultValue: "'OPTION_1'",
}; };
const result = formatFieldMetadataItemInput(input); const result = formatFieldMetadataItemInput(input);
@ -140,7 +140,7 @@ describe('formatFieldMetadataItemInput', () => {
value: 'OPTION_2', value: 'OPTION_2',
}, },
], ],
defaultValue: ['OPTION_1', 'OPTION_2'], defaultValue: ["'OPTION_1'", "'OPTION_2'"],
}; };
const result = formatFieldMetadataItemInput(input); const result = formatFieldMetadataItemInput(input);

View File

@ -1,8 +0,0 @@
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
describe('validateMetadataLabel', () => {
it('should work as expected', () => {
const res = validateMetadataLabel('Pipeline Step');
expect(res).toBe(true);
});
});

View File

@ -1,6 +1,8 @@
import toSnakeCase from 'lodash.snakecase'; import toSnakeCase from 'lodash.snakecase';
import { Field, FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getDefaultValueForBackend } from '@/object-metadata/utils/getDefaultValueForBackend';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { formatMetadataLabelToMetadataNameOrThrows } from '~/pages/settings/data-model/utils/format-metadata-label-to-name.util'; import { formatMetadataLabelToMetadataNameOrThrows } from '~/pages/settings/data-model/utils/format-metadata-label-to-name.util';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -21,25 +23,24 @@ export const getOptionValueFromLabel = (label: string) => {
}; };
export const formatFieldMetadataItemInput = ( export const formatFieldMetadataItemInput = (
input: Pick< input: Partial<
Field, Pick<
'label' | 'icon' | 'description' | 'defaultValue' | 'options' FieldMetadataItem,
> & { type?: FieldMetadataType }, 'type' | 'label' | 'defaultValue' | 'icon' | 'description'
>
> & { options?: FieldMetadataOption[] },
) => { ) => {
const options = input.options as FieldMetadataOption[]; const options = input.options as FieldMetadataOption[] | undefined;
let defaultValue = input.defaultValue; let defaultValue = input.defaultValue;
if (input.type === FieldMetadataType.MultiSelect) { if (input.type === FieldMetadataType.MultiSelect) {
const defaultOptions = options?.filter((option) => option.isDefault); defaultValue = options
if (isDefined(defaultOptions)) { ?.filter((option) => option.isDefault)
defaultValue = defaultOptions.map( ?.map((defaultOption) => getOptionValueFromLabel(defaultOption.label));
(defaultOption) => `${getOptionValueFromLabel(defaultOption.label)}`,
);
}
} }
if (input.type === FieldMetadataType.Select) { if (input.type === FieldMetadataType.Select) {
const defaultOption = options?.find((option) => option.isDefault); const defaultOption = options?.find((option) => option.isDefault);
defaultValue = isDefined(defaultOption) defaultValue = isDefined(defaultOption)
? `${getOptionValueFromLabel(defaultOption.label)}` ? getOptionValueFromLabel(defaultOption.label)
: undefined; : undefined;
} }
@ -59,12 +60,17 @@ export const formatFieldMetadataItemInput = (
} }
} }
const label = input.label?.trim();
return { return {
defaultValue, defaultValue:
isDefined(defaultValue) && input.type
? getDefaultValueForBackend(defaultValue, input.type)
: undefined,
description: input.description?.trim() ?? null, description: input.description?.trim() ?? null,
icon: input.icon, icon: input.icon,
label: input.label.trim(), label,
name: formatMetadataLabelToMetadataNameOrThrows(input.label.trim()), name: label ? formatMetadataLabelToMetadataNameOrThrows(label) : undefined,
options: options?.map((option, index) => ({ options: options?.map((option, index) => ({
color: option.color, color: option.color,
id: option.id, id: option.id,

View File

@ -35,14 +35,14 @@ export const formatRelationMetadataInput = (
const { const {
description: fromDescription, description: fromDescription,
icon: fromIcon, icon: fromIcon,
label: fromLabel, label: fromLabel = '',
name: fromName, name: fromName = '',
} = formatFieldMetadataItemInput(fromField); } = formatFieldMetadataItemInput(fromField);
const { const {
description: toDescription, description: toDescription,
icon: toIcon, icon: toIcon,
label: toLabel, label: toLabel = '',
name: toName, name: toName = '',
} = formatFieldMetadataItemInput(toField); } = formatFieldMetadataItemInput(toField);
return { return {

View File

@ -13,7 +13,7 @@ export const getDefaultValueForBackend = (
currencyCode: `'${currencyDefaultValue.currencyCode}'` as CurrencyCode, currencyCode: `'${currencyDefaultValue.currencyCode}'` as CurrencyCode,
} satisfies FieldCurrencyValue; } satisfies FieldCurrencyValue;
} else if (fieldMetadataType === FieldMetadataType.Select) { } else if (fieldMetadataType === FieldMetadataType.Select) {
return `'${defaultValue}'`; return defaultValue ? `'${defaultValue}'` : null;
} else if (fieldMetadataType === FieldMetadataType.MultiSelect) { } else if (fieldMetadataType === FieldMetadataType.MultiSelect) {
return defaultValue.map((value: string) => `'${value}'`); return defaultValue.map((value: string) => `'${value}'`);
} }

View File

@ -1,4 +0,0 @@
const metadataLabelValidationPattern = /^[^0-9].*$/;
export const validateMetadataLabel = (value: string) =>
!!value.match(metadataLabelValidationPattern);

View File

@ -14,7 +14,7 @@ export const MultiSelectFieldDisplay = ({
const { fieldValues, fieldDefinition } = useMultiSelectField(); const { fieldValues, fieldDefinition } = useMultiSelectField();
const selectedOptions = fieldValues const selectedOptions = fieldValues
? fieldDefinition.metadata.options.filter((option) => ? fieldDefinition.metadata.options?.filter((option) =>
fieldValues.includes(option.value), fieldValues.includes(option.value),
) )
: []; : [];

View File

@ -6,7 +6,11 @@ import { useRelationField } from '../../hooks/useRelationField';
export const RelationFieldDisplay = () => { export const RelationFieldDisplay = () => {
const { fieldValue, fieldDefinition, maxWidth } = useRelationField(); const { fieldValue, fieldDefinition, maxWidth } = useRelationField();
if (!fieldValue || !fieldDefinition) return null; if (
!fieldValue ||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
)
return null;
return ( return (
<RecordChip <RecordChip

View File

@ -5,7 +5,7 @@ import { useSelectField } from '../../hooks/useSelectField';
export const SelectFieldDisplay = () => { export const SelectFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useSelectField(); const { fieldValue, fieldDefinition } = useSelectField();
const selectedOption = fieldDefinition.metadata.options.find( const selectedOption = fieldDefinition.metadata.options?.find(
(option) => option.value === fieldValue, (option) => option.value === fieldValue,
); );

View File

@ -1,17 +1,26 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconCheck, IconX } from 'twenty-ui'; import { IconCheck, IconX } from 'twenty-ui';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { Select } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from '@/ui/layout/card/components/CardContent';
type SettingsDataModelDefaultValueFormProps = { // TODO: rename to SettingsDataModelFieldBooleanForm and move to settings/data-model/fields/forms/components
className?: string;
disabled?: boolean;
onChange?: (defaultValue: SettingsDataModelDefaultValue) => void;
value?: SettingsDataModelDefaultValue;
};
export type SettingsDataModelDefaultValue = any; export const settingsDataModelFieldBooleanFormSchema = z.object({
defaultValue: z.boolean(),
});
type SettingsDataModelFieldBooleanFormValues = z.infer<
typeof settingsDataModelFieldBooleanFormSchema
>;
type SettingsDataModelFieldBooleanFormProps = {
className?: string;
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue'>;
};
const StyledContainer = styled(CardContent)` const StyledContainer = styled(CardContent)`
padding-bottom: ${({ theme }) => theme.spacing(3.5)}; padding-bottom: ${({ theme }) => theme.spacing(3.5)};
@ -26,34 +35,42 @@ const StyledLabel = styled.span`
margin-top: ${({ theme }) => theme.spacing(1)}; margin-top: ${({ theme }) => theme.spacing(1)};
`; `;
export const SettingsDataModelDefaultValueForm = ({ export const SettingsDataModelFieldBooleanForm = ({
className, className,
disabled, fieldMetadataItem,
onChange, }: SettingsDataModelFieldBooleanFormProps) => {
value, const { control } = useFormContext<SettingsDataModelFieldBooleanFormValues>();
}: SettingsDataModelDefaultValueFormProps) => {
const initialValue = fieldMetadataItem?.defaultValue ?? true;
return ( return (
<StyledContainer> <StyledContainer>
<StyledLabel>Default Value</StyledLabel> <StyledLabel>Default Value</StyledLabel>
<Select <Controller
className={className} name="defaultValue"
fullWidth control={control}
disabled={disabled} defaultValue={initialValue}
dropdownId="object-field-default-value-select" render={({ field: { onChange, value } }) => (
value={value} <Select
onChange={(value) => onChange?.(value)} className={className}
options={[ fullWidth
{ dropdownId="object-field-default-value-select"
value: true, value={value}
label: 'True', onChange={onChange}
Icon: IconCheck, options={[
}, {
{ value: true,
value: false, label: 'True',
label: 'False', Icon: IconCheck,
Icon: IconX, },
}, {
]} value: false,
label: 'False',
Icon: IconX,
},
]}
/>
)}
/> />
</StyledContainer> </StyledContainer>
); );

View File

@ -1,39 +1,66 @@
import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { Select } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from '@/ui/layout/card/components/CardContent';
import { SETTINGS_FIELD_CURRENCY_CODES } from '../constants/SettingsFieldCurrencyCodes'; // TODO: rename to SettingsDataModelFieldCurrencyForm and move to settings/data-model/fields/forms/components
export type SettingsObjectFieldCurrencyFormValues = { export const settingsDataModelFieldCurrencyFormSchema = z.object({
currencyCode: CurrencyCode; defaultValue: z.object({
}; currencyCode: z.nativeEnum(CurrencyCode),
}),
});
type SettingsObjectFieldCurrencyFormProps = { type SettingsDataModelFieldCurrencyFormValues = z.infer<
typeof settingsDataModelFieldCurrencyFormSchema
>;
type SettingsDataModelFieldCurrencyFormProps = {
disabled?: boolean; disabled?: boolean;
onChange: (values: Partial<SettingsObjectFieldCurrencyFormValues>) => void; fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue'>;
values: SettingsObjectFieldCurrencyFormValues;
}; };
export const SettingsObjectFieldCurrencyForm = ({ const OPTIONS = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
disabled, ([value, { label, Icon }]) => ({
onChange, label,
values, value: value as CurrencyCode,
}: SettingsObjectFieldCurrencyFormProps) => ( Icon,
<CardContent> }),
<Select
fullWidth
disabled={disabled}
label="Default Unit"
dropdownId="currency-unit-select"
value={values.currencyCode}
options={Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
([value, { label, Icon }]) => ({
label,
value: value as CurrencyCode,
Icon,
}),
)}
onChange={(value) => onChange({ currencyCode: value })}
/>
</CardContent>
); );
export const SettingsDataModelFieldCurrencyForm = ({
disabled,
fieldMetadataItem,
}: SettingsDataModelFieldCurrencyFormProps) => {
const { control } =
useFormContext<SettingsDataModelFieldCurrencyFormValues>();
const initialValue =
(fieldMetadataItem?.defaultValue?.currencyCode as CurrencyCode) ??
CurrencyCode.USD;
return (
<CardContent>
<Controller
name="defaultValue.currencyCode"
control={control}
defaultValue={initialValue}
render={({ field: { onChange, value } }) => (
<Select
fullWidth
disabled={disabled}
label="Default Unit"
dropdownId="currency-unit-select"
value={value}
options={OPTIONS}
onChange={onChange}
/>
)}
/>
</CardContent>
);
};

View File

@ -1,28 +1,46 @@
import { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useIcons } from 'twenty-ui'; import { useIcons } from 'twenty-ui';
import { z } from 'zod';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel'; import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
import { IconPicker } from '@/ui/input/components/IconPicker'; import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { Field } from '~/generated-metadata/graphql'; import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RELATION_TYPES } from '../constants/RelationTypes'; import { RELATION_TYPES } from '../constants/RelationTypes';
import { RelationType } from '../types/RelationType'; import { RelationType } from '../types/RelationType';
export type SettingsObjectFieldRelationFormValues = { // TODO: rename to SettingsDataModelFieldRelationForm and move to settings/data-model/fields/forms/components
field: Pick<Field, 'icon' | 'label'>;
objectMetadataId: string;
type: RelationType;
};
type SettingsObjectFieldRelationFormProps = { export const settingsDataModelFieldRelationFormSchema = z.object({
disableFieldEdition?: boolean; relation: z.object({
disableRelationEdition?: boolean; field: fieldMetadataItemSchema.pick({
onChange: (values: Partial<SettingsObjectFieldRelationFormValues>) => void; icon: true,
values: SettingsObjectFieldRelationFormValues; label: true,
}),
objectMetadataId: z.string().uuid(),
type: z.enum(
Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]],
),
}),
});
type SettingsDataModelFieldRelationFormValues = z.infer<
typeof settingsDataModelFieldRelationFormSchema
>;
type SettingsDataModelFieldRelationFormProps = {
fieldMetadataItem?: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -50,84 +68,119 @@ const StyledInputsContainer = styled.div`
width: 100%; width: 100%;
`; `;
export const SettingsObjectFieldRelationForm = ({ const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES)
disableFieldEdition, .filter(([value]) => 'ONE_TO_ONE' !== value)
disableRelationEdition, .map(([value, { label, Icon }]) => ({
onChange, label,
values, value: value as RelationType,
}: SettingsObjectFieldRelationFormProps) => { Icon,
}));
export const SettingsDataModelFieldRelationForm = ({
fieldMetadataItem,
}: SettingsDataModelFieldRelationFormProps) => {
const { control } =
useFormContext<SettingsDataModelFieldRelationFormValues>();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const { objectMetadataItems, findObjectMetadataItemById } = const { objectMetadataItems } = useFilteredObjectMetadataItems();
useFilteredObjectMetadataItems();
const getRelationMetadata = useGetRelationMetadata();
const {
relationFieldMetadataItem,
relationType,
relationObjectMetadataItem,
} =
useMemo(
() =>
fieldMetadataItem ? getRelationMetadata({ fieldMetadataItem }) : null,
[fieldMetadataItem, getRelationMetadata],
) ?? {};
const disableFieldEdition =
relationFieldMetadataItem && !relationFieldMetadataItem.isCustom;
const disableRelationEdition = !!relationFieldMetadataItem;
const selectedObjectMetadataItem = const selectedObjectMetadataItem =
(values.objectMetadataId relationObjectMetadataItem ?? objectMetadataItems[0];
? findObjectMetadataItemById(values.objectMetadataId)
: undefined) || objectMetadataItems[0];
return ( return (
<StyledContainer> <StyledContainer>
<StyledSelectsContainer> <StyledSelectsContainer>
<Select <Controller
label="Relation type" name="relation.type"
dropdownId="relation-type-select" control={control}
fullWidth defaultValue={relationType ?? RelationMetadataType.OneToMany}
disabled={disableRelationEdition} render={({ field: { onChange, value } }) => (
value={values.type} <Select
options={Object.entries(RELATION_TYPES) label="Relation type"
.filter(([value]) => 'ONE_TO_ONE' !== value) dropdownId="relation-type-select"
.map(([value, { label, Icon }]) => ({ fullWidth
label, disabled={disableRelationEdition}
value: value as RelationType, value={value}
Icon, options={RELATION_TYPE_OPTIONS}
}))} onChange={onChange}
onChange={(value) => onChange({ type: value })} />
)}
/> />
<Select <Controller
label="Object destination" name="relation.objectMetadataId"
dropdownId="object-destination-select" control={control}
fullWidth defaultValue={selectedObjectMetadataItem?.id}
disabled={disableRelationEdition} render={({ field: { onChange, value } }) => (
value={values.objectMetadataId} <Select
options={objectMetadataItems label="Object destination"
.filter((objectMetadataItem) => dropdownId="object-destination-select"
isObjectMetadataAvailableForRelation(objectMetadataItem), fullWidth
) disabled={disableRelationEdition}
.map((objectMetadataItem) => ({ value={value}
label: objectMetadataItem.labelPlural, options={objectMetadataItems
value: objectMetadataItem.id, .filter(isObjectMetadataAvailableForRelation)
Icon: getIcon(objectMetadataItem.icon), .map((objectMetadataItem) => ({
}))} label: objectMetadataItem.labelPlural,
onChange={(value) => onChange({ objectMetadataId: value })} value: objectMetadataItem.id,
Icon: getIcon(objectMetadataItem.icon),
}))}
onChange={onChange}
/>
)}
/> />
</StyledSelectsContainer> </StyledSelectsContainer>
<StyledInputsLabel> <StyledInputsLabel>
Field on {selectedObjectMetadataItem?.labelPlural} Field on {selectedObjectMetadataItem?.labelPlural}
</StyledInputsLabel> </StyledInputsLabel>
<StyledInputsContainer> <StyledInputsContainer>
<IconPicker <Controller
disabled={disableFieldEdition} name="relation.field.icon"
dropdownId="field-destination-icon-picker" control={control}
selectedIconKey={values.field.icon || undefined} defaultValue={
onChange={(value) => relationFieldMetadataItem?.icon ??
onChange({ relationObjectMetadataItem?.icon ??
field: { ...values.field, icon: value.iconKey }, 'IconUsers'
})
} }
variant="primary" render={({ field: { onChange, value } }) => (
<IconPicker
disabled={disableFieldEdition}
dropdownId="field-destination-icon-picker"
selectedIconKey={value ?? undefined}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
/>
)}
/> />
<TextInput <Controller
disabled={disableFieldEdition} name="relation.field.label"
placeholder="Field name" control={control}
value={values.field.label} defaultValue={relationFieldMetadataItem?.label}
onChange={(value) => { render={({ field: { onChange, value } }) => (
if (!value || validateMetadataLabel(value)) { <TextInput
onChange({ disabled={disableFieldEdition}
field: { ...values.field, label: value }, placeholder="Field name"
}); value={value}
} onChange={onChange}
}} fullWidth
fullWidth />
)}
/> />
</StyledInputsContainer> </StyledInputsContainer>
</StyledContainer> </StyledContainer>

View File

@ -1,8 +1,12 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DropResult } from '@hello-pangea/dnd'; import { DropResult } from '@hello-pangea/dnd';
import { IconPlus } from 'twenty-ui'; import { IconPlus } from 'twenty-ui';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsObjectFieldSelectFormOption } from '@/settings/data-model/types/SettingsObjectFieldSelectFormOption';
import { LightButton } from '@/ui/input/button/components/LightButton'; import { LightButton } from '@/ui/input/button/components/LightButton';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from '@/ui/layout/card/components/CardContent';
import { CardFooter } from '@/ui/layout/card/components/CardFooter'; import { CardFooter } from '@/ui/layout/card/components/CardFooter';
@ -12,18 +16,32 @@ import {
MAIN_COLOR_NAMES, MAIN_COLOR_NAMES,
ThemeColor, ThemeColor,
} from '@/ui/theme/constants/MainColorNames'; } from '@/ui/theme/constants/MainColorNames';
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
import { SettingsObjectFieldSelectFormOptionRow } from './SettingsObjectFieldSelectFormOptionRow'; import { SettingsObjectFieldSelectFormOptionRow } from './SettingsObjectFieldSelectFormOptionRow';
export type SettingsObjectFieldSelectFormValues = // TODO: rename to SettingsDataModelFieldSelectForm and move to settings/data-model/fields/forms/components
SettingsObjectFieldSelectFormOption[];
type SettingsObjectFieldSelectFormProps = { export const settingsDataModelFieldSelectFormSchema = z.object({
onChange: (values: SettingsObjectFieldSelectFormValues) => void; options: z
values: SettingsObjectFieldSelectFormValues; .array(
z.object({
color: themeColorSchema,
value: z.string(),
isDefault: z.boolean().optional(),
label: z.string().min(1),
}),
)
.min(1),
});
export type SettingsDataModelFieldSelectFormValues = z.infer<
typeof settingsDataModelFieldSelectFormSchema
>;
type SettingsDataModelFieldSelectFormProps = {
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue' | 'options'>;
isMultiSelect?: boolean; isMultiSelect?: boolean;
}; };
@ -58,12 +76,55 @@ const getNextColor = (currentColor: ThemeColor) => {
return MAIN_COLOR_NAMES[nextColorIndex]; return MAIN_COLOR_NAMES[nextColorIndex];
}; };
export const SettingsObjectFieldSelectForm = ({ const getDefaultValueOptionIndexes = (
onChange, fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue' | 'options'>,
values, ) =>
fieldMetadataItem?.options?.reduce<number[]>((result, option, index) => {
if (
Array.isArray(fieldMetadataItem?.defaultValue) &&
fieldMetadataItem?.defaultValue.includes(`'${option.value}'`)
) {
return [...result, index];
}
// Ensure default value is unique for simple Select field
if (
!result.length &&
fieldMetadataItem?.defaultValue === `'${option.value}'`
) {
return [index];
}
return result;
}, []);
const DEFAULT_OPTION: SettingsObjectFieldSelectFormOption = {
color: 'green',
label: 'Option 1',
value: v4(),
};
export const SettingsDataModelFieldSelectForm = ({
fieldMetadataItem,
isMultiSelect = false, isMultiSelect = false,
}: SettingsObjectFieldSelectFormProps) => { }: SettingsDataModelFieldSelectFormProps) => {
const handleDragEnd = (result: DropResult) => { const { control } = useFormContext<SettingsDataModelFieldSelectFormValues>();
const initialDefaultValueOptionIndexes =
getDefaultValueOptionIndexes(fieldMetadataItem);
const initialValue = fieldMetadataItem?.options
?.map((option, index) => ({
...option,
isDefault: initialDefaultValueOptionIndexes?.includes(index),
}))
.sort((optionA, optionB) => optionA.position - optionB.position);
const handleDragEnd = (
values: SettingsObjectFieldSelectFormOption[],
result: DropResult,
onChange: (options: SettingsObjectFieldSelectFormOption[]) => void,
) => {
if (!result.destination) return; if (!result.destination) return;
const nextOptions = moveArrayItem(values, { const nextOptions = moveArrayItem(values, {
@ -74,27 +135,7 @@ export const SettingsObjectFieldSelectForm = ({
onChange(nextOptions); onChange(nextOptions);
}; };
const handleDefaultValueChange = ( const findNewLabel = (values: SettingsObjectFieldSelectFormOption[]) => {
index: number,
option: SettingsObjectFieldSelectFormOption,
nextOption: SettingsObjectFieldSelectFormOption,
forceUniqueDefaultValue: boolean,
) => {
const computeUniqueDefaultValue =
forceUniqueDefaultValue && option.isDefault !== nextOption.isDefault;
const nextOptions = computeUniqueDefaultValue
? values.map((value) => ({
...value,
isDefault: false,
}))
: [...values];
nextOptions.splice(index, 1, nextOption);
onChange(nextOptions);
};
const findNewLabel = () => {
let optionIndex = values.length + 1; let optionIndex = values.length + 1;
while (optionIndex < 100) { while (optionIndex < 100) {
const newLabel = `Option ${optionIndex}`; const newLabel = `Option ${optionIndex}`;
@ -107,65 +148,75 @@ export const SettingsObjectFieldSelectForm = ({
}; };
return ( return (
<> <Controller
<StyledContainer> name="options"
<StyledLabel>Options</StyledLabel> control={control}
<DraggableList defaultValue={initialValue?.length ? initialValue : [DEFAULT_OPTION]}
onDragEnd={handleDragEnd} render={({ field: { onChange, value: options } }) => (
draggableItems={ <>
<> <StyledContainer>
{values.map((option, index) => ( <StyledLabel>Options</StyledLabel>
<DraggableItem <DraggableList
key={option.value} onDragEnd={(result) => handleDragEnd(options, result, onChange)}
draggableId={option.value} draggableItems={
index={index} <>
isDragDisabled={values.length === 1} {options.map((option, index) => (
itemComponent={ <DraggableItem
<SettingsObjectFieldSelectFormOptionRow
key={option.value} key={option.value}
isDefault={option.isDefault} draggableId={option.value}
onChange={(nextOption) => { index={index}
handleDefaultValueChange( isDragDisabled={options.length === 1}
index, itemComponent={
option, <SettingsObjectFieldSelectFormOptionRow
nextOption, key={option.value}
!isMultiSelect, isDefault={option.isDefault}
); onChange={(nextOption) => {
}} const nextOptions =
onRemove={ isMultiSelect || !nextOption.isDefault
values.length > 1 ? [...options]
? () => { : // Reset simple Select default option before setting the new one
const nextOptions = [...values]; options.map<SettingsObjectFieldSelectFormOption>(
nextOptions.splice(index, 1); (value) => ({ ...value, isDefault: false }),
onChange(nextOptions); );
} nextOptions.splice(index, 1, nextOption);
: undefined onChange(nextOptions);
}}
onRemove={
options.length > 1
? () => {
const nextOptions = [...options];
nextOptions.splice(index, 1);
onChange(nextOptions);
}
: undefined
}
option={option}
/>
} }
option={option}
/> />
} ))}
/> </>
))} }
</> />
} </StyledContainer>
/> <StyledFooter>
</StyledContainer> <StyledButton
<StyledFooter> title="Add option"
<StyledButton Icon={IconPlus}
title="Add option" onClick={() =>
Icon={IconPlus} onChange([
onClick={() => ...options,
onChange([ {
...values, color: getNextColor(options[options.length - 1].color),
{ label: findNewLabel(options),
color: getNextColor(values[values.length - 1].color), value: v4(),
label: findNewLabel(), },
value: v4(), ])
}, }
]) />
} </StyledFooter>
/> </>
</StyledFooter> )}
</> />
); );
}; };

View File

@ -1,20 +1,25 @@
import { useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { z } from 'zod';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelDefaultValueForm } from '@/settings/data-model/components/SettingsDataModelDefaultValue'; import {
SettingsDataModelFieldBooleanForm,
settingsDataModelFieldBooleanFormSchema,
} from '@/settings/data-model/components/SettingsDataModelDefaultValue';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import { import {
SettingsObjectFieldCurrencyForm, SettingsDataModelFieldCurrencyForm,
SettingsObjectFieldCurrencyFormValues, settingsDataModelFieldCurrencyFormSchema,
} from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm'; } from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
import { import {
SettingsObjectFieldRelationForm, SettingsDataModelFieldRelationForm,
SettingsObjectFieldRelationFormValues, settingsDataModelFieldRelationFormSchema,
} from '@/settings/data-model/components/SettingsObjectFieldRelationForm'; } from '@/settings/data-model/components/SettingsObjectFieldRelationForm';
import { import {
SettingsObjectFieldSelectForm, SettingsDataModelFieldSelectForm,
SettingsObjectFieldSelectFormValues, settingsDataModelFieldSelectFormSchema,
} from '@/settings/data-model/components/SettingsObjectFieldSelectForm'; } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
import { import {
@ -23,26 +28,44 @@ import {
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
export type SettingsDataModelFieldSettingsFormValues = { const booleanFieldFormSchema = z
currency: SettingsObjectFieldCurrencyFormValues; .object({ type: z.literal(FieldMetadataType.Boolean) })
relation: SettingsObjectFieldRelationFormValues; .merge(settingsDataModelFieldBooleanFormSchema);
select: SettingsObjectFieldSelectFormValues;
multiSelect: SettingsObjectFieldSelectFormValues; const currencyFieldFormSchema = z
defaultValue: any; .object({ type: z.literal(FieldMetadataType.Currency) })
}; .merge(settingsDataModelFieldCurrencyFormSchema);
const relationFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Relation) })
.merge(settingsDataModelFieldRelationFormSchema);
const selectFieldFormSchema = z
.object({
type: z.enum([FieldMetadataType.Select, FieldMetadataType.MultiSelect]),
})
.merge(settingsDataModelFieldSelectFormSchema);
export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
'type',
[
booleanFieldFormSchema,
currencyFieldFormSchema,
relationFieldFormSchema,
selectFieldFormSchema,
],
);
type SettingsDataModelFieldSettingsFormValues = z.infer<
typeof settingsDataModelFieldSettingsFormSchema
>;
type SettingsDataModelFieldSettingsFormCardProps = { type SettingsDataModelFieldSettingsFormCardProps = {
disableCurrencyForm?: boolean; disableCurrencyForm?: boolean;
onChange: (values: Partial<SettingsDataModelFieldSettingsFormValues>) => void; fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
relationFieldMetadataItem?: Pick< Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
FieldMetadataItem, relationFieldMetadataItem?: FieldMetadataItem;
'id' | 'isCustom' | 'name' } & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
>;
values: SettingsDataModelFieldSettingsFormValues;
} & Pick<
SettingsDataModelFieldPreviewCardProps,
'fieldMetadataItem' | 'objectMetadataItem'
>;
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
display: grid; display: grid;
@ -81,18 +104,23 @@ export const SettingsDataModelFieldSettingsFormCard = ({
disableCurrencyForm, disableCurrencyForm,
fieldMetadataItem, fieldMetadataItem,
objectMetadataItem, objectMetadataItem,
onChange,
relationFieldMetadataItem, relationFieldMetadataItem,
values,
}: SettingsDataModelFieldSettingsFormCardProps) => { }: SettingsDataModelFieldSettingsFormCardProps) => {
const { watch: watchFormValue } =
useFormContext<SettingsDataModelFieldSettingsFormValues>();
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems(); const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
if (!previewableTypes.includes(fieldMetadataItem.type)) return null; if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
const relationObjectMetadataItem = findObjectMetadataItemById( const relationObjectMetadataId = watchFormValue('relation.objectMetadataId');
values.relation.objectMetadataId, const relationObjectMetadataItem = relationObjectMetadataId
); ? findObjectMetadataItemById(relationObjectMetadataId)
const relationTypeConfig = RELATION_TYPES[values.relation.type]; : undefined;
const relationType = watchFormValue('relation.type');
const relationTypeConfig = relationType
? RELATION_TYPES[relationType]
: undefined;
return ( return (
<SettingsDataModelPreviewFormCard <SettingsDataModelPreviewFormCard
@ -103,14 +131,11 @@ export const SettingsDataModelFieldSettingsFormCard = ({
shrink={fieldMetadataItem.type === FieldMetadataType.Relation} shrink={fieldMetadataItem.type === FieldMetadataType.Relation}
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}
relationObjectMetadataItem={relationObjectMetadataItem} relationObjectMetadataItem={relationObjectMetadataItem}
selectOptions={ selectOptions={watchFormValue('options')}
fieldMetadataItem.type === FieldMetadataType.MultiSelect
? values.multiSelect
: values.select
}
/> />
{fieldMetadataItem.type === FieldMetadataType.Relation && {fieldMetadataItem.type === FieldMetadataType.Relation &&
!!relationObjectMetadataItem && ( !!relationObjectMetadataItem &&
!!relationTypeConfig && (
<> <>
<StyledRelationImage <StyledRelationImage
src={relationTypeConfig.imageSrc} src={relationTypeConfig.imageSrc}
@ -119,11 +144,11 @@ export const SettingsDataModelFieldSettingsFormCard = ({
/> />
<StyledFieldPreviewCard <StyledFieldPreviewCard
fieldMetadataItem={{ fieldMetadataItem={{
icon: values.relation.field.icon, ...relationFieldMetadataItem,
label: values.relation.field.label || 'Field name', icon: watchFormValue('relation.field.icon'),
label:
watchFormValue('relation.field.label') || 'Field name',
type: FieldMetadataType.Relation, type: FieldMetadataType.Relation,
name: relationFieldMetadataItem?.name,
id: relationFieldMetadataItem?.id,
}} }}
shrink shrink
objectMetadataItem={relationObjectMetadataItem} objectMetadataItem={relationObjectMetadataItem}
@ -134,49 +159,25 @@ export const SettingsDataModelFieldSettingsFormCard = ({
</StyledPreviewContent> </StyledPreviewContent>
} }
form={ form={
fieldMetadataItem.type === FieldMetadataType.Currency ? ( fieldMetadataItem.type === FieldMetadataType.Boolean ? (
<SettingsObjectFieldCurrencyForm <SettingsDataModelFieldBooleanForm
fieldMetadataItem={fieldMetadataItem}
/>
) : fieldMetadataItem.type === FieldMetadataType.Currency ? (
<SettingsDataModelFieldCurrencyForm
disabled={disableCurrencyForm} disabled={disableCurrencyForm}
values={values.currency} fieldMetadataItem={fieldMetadataItem}
onChange={(nextCurrencyValues) =>
onChange({
currency: { ...values.currency, ...nextCurrencyValues },
})
}
/> />
) : fieldMetadataItem.type === FieldMetadataType.Relation ? ( ) : fieldMetadataItem.type === FieldMetadataType.Relation ? (
<SettingsObjectFieldRelationForm <SettingsDataModelFieldRelationForm
disableFieldEdition={ fieldMetadataItem={fieldMetadataItem}
relationFieldMetadataItem && !relationFieldMetadataItem.isCustom
}
disableRelationEdition={!!relationFieldMetadataItem}
values={values.relation}
onChange={(nextRelationValues) =>
onChange({
relation: { ...values.relation, ...nextRelationValues },
})
}
/> />
) : fieldMetadataItem.type === FieldMetadataType.Select ? ( ) : fieldMetadataItem.type === FieldMetadataType.Select ||
<SettingsObjectFieldSelectForm fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
values={values.select} <SettingsDataModelFieldSelectForm
onChange={(nextSelectValues) => fieldMetadataItem={fieldMetadataItem}
onChange({ select: nextSelectValues }) isMultiSelect={
} fieldMetadataItem.type === FieldMetadataType.MultiSelect
/>
) : fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
<SettingsObjectFieldSelectForm
values={values.multiSelect}
onChange={(nextMultiSelectValues) =>
onChange({ multiSelect: nextMultiSelectValues })
}
isMultiSelect={true}
/>
) : fieldMetadataItem.type === FieldMetadataType.Boolean ? (
<SettingsDataModelDefaultValueForm
value={values.defaultValue}
onChange={(nextValueDefaultValue) =>
onChange({ defaultValue: nextValueDefaultValue })
} }
/> />
) : undefined ) : undefined

View File

@ -1,38 +1,47 @@
import { Controller, useFormContext } from 'react-hook-form';
import omit from 'lodash.omit'; import omit from 'lodash.omit';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { import {
SETTINGS_FIELD_TYPE_CONFIGS, SETTINGS_FIELD_TYPE_CONFIGS,
SettingsFieldTypeConfig, SettingsFieldTypeConfig,
} from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
import { Select, SelectOption } from '@/ui/input/components/Select'; import { Select, SelectOption } from '@/ui/input/components/Select';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const settingsDataModelFieldTypeFormSchema = z.object({
type: z.enum(
Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [
SettingsSupportedFieldType,
...SettingsSupportedFieldType[],
],
),
});
type SettingsDataModelFieldTypeFormValues = z.infer<
typeof settingsDataModelFieldTypeFormSchema
>;
type SettingsDataModelFieldTypeSelectProps = { type SettingsDataModelFieldTypeSelectProps = {
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
excludedFieldTypes?: SettingsSupportedFieldType[]; excludedFieldTypes?: SettingsSupportedFieldType[];
onChange?: ({ fieldMetadataItem?: FieldMetadataItem;
type,
defaultValue,
}: {
type: SettingsSupportedFieldType;
defaultValue: any;
}) => void;
value?: SettingsSupportedFieldType;
}; };
export const SettingsDataModelFieldTypeSelect = ({ export const SettingsDataModelFieldTypeSelect = ({
className, className,
disabled, disabled,
excludedFieldTypes = [], excludedFieldTypes = [],
onChange, fieldMetadataItem,
value,
}: SettingsDataModelFieldTypeSelectProps) => { }: SettingsDataModelFieldTypeSelectProps) => {
const fieldTypeConfigs = omit( const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>();
SETTINGS_FIELD_TYPE_CONFIGS,
excludedFieldTypes, const fieldTypeConfigs: Partial<
); Record<SettingsSupportedFieldType, SettingsFieldTypeConfig>
> = omit(SETTINGS_FIELD_TYPE_CONFIGS, excludedFieldTypes);
const fieldTypeOptions = Object.entries<SettingsFieldTypeConfig>( const fieldTypeOptions = Object.entries<SettingsFieldTypeConfig>(
fieldTypeConfigs, fieldTypeConfigs,
).map<SelectOption<SettingsSupportedFieldType>>(([key, dataTypeConfig]) => ({ ).map<SelectOption<SettingsSupportedFieldType>>(([key, dataTypeConfig]) => ({
@ -42,19 +51,25 @@ export const SettingsDataModelFieldTypeSelect = ({
})); }));
return ( return (
<Select <Controller
className={className} name="type"
fullWidth control={control}
disabled={disabled} defaultValue={
dropdownId="object-field-type-select" fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs
value={value} ? (fieldMetadataItem.type as SettingsSupportedFieldType)
onChange={(value) => : undefined
onChange?.({
type: value,
defaultValue: value === FieldMetadataType.Boolean ? false : undefined,
})
} }
options={fieldTypeOptions} render={({ field: { onChange, value } }) => (
<Select
className={className}
fullWidth
disabled={disabled}
dropdownId="object-field-type-select"
value={value}
onChange={onChange}
options={fieldTypeOptions}
/>
)}
/> />
); );
}; };

View File

@ -1,12 +1,8 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui'; import { ComponentDecorator } from 'twenty-ui';
import { fieldMetadataFormDefaultValues } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@ -22,14 +18,6 @@ const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
({ type }) => type === FieldMetadataType.Text, ({ type }) => type === FieldMetadataType.Text,
)!; )!;
const defaultValues = {
currency: fieldMetadataFormDefaultValues.currency,
relation: fieldMetadataFormDefaultValues.relation,
select: fieldMetadataFormDefaultValues.select,
multiSelect: fieldMetadataFormDefaultValues.multiSelect,
defaultValue: fieldMetadataFormDefaultValues.defaultValue,
};
const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = { const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = {
title: title:
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldSettingsFormCard', 'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldSettingsFormCard',
@ -39,12 +27,11 @@ const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = {
ComponentDecorator, ComponentDecorator,
ObjectMetadataItemsDecorator, ObjectMetadataItemsDecorator,
SnackBarDecorator, SnackBarDecorator,
FormProviderDecorator,
], ],
args: { args: {
fieldMetadataItem, fieldMetadataItem,
objectMetadataItem: mockedCompanyObjectMetadataItem, objectMetadataItem: mockedCompanyObjectMetadataItem,
onChange: fn(),
values: defaultValues,
}, },
parameters: { parameters: {
container: { width: 512 }, container: { width: 512 },
@ -57,24 +44,14 @@ type Story = StoryObj<typeof SettingsDataModelFieldSettingsFormCard>;
export const Default: Story = {}; export const Default: Story = {};
const relationFieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
({ name }) => name === 'company',
)!;
export const WithRelationForm: Story = { export const WithRelationForm: Story = {
args: { args: {
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
({ name }) => name === 'people', ({ name }) => name === 'people',
), ),
relationFieldMetadataItem, relationFieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
values: { ({ name }) => name === 'company',
...defaultValues, )!,
relation: {
field: relationFieldMetadataItem,
objectMetadataId: mockedPersonObjectMetadataItem.id,
type: RelationMetadataType.OneToMany,
},
},
}, },
}; };
@ -85,34 +62,5 @@ export const WithSelectForm: Story = {
icon: 'IconBuildingFactory2', icon: 'IconBuildingFactory2',
type: FieldMetadataType.Select, type: FieldMetadataType.Select,
}, },
values: {
...defaultValues,
select: [
{
color: 'pink',
isDefault: true,
label: '💊 Health',
value: 'HEALTH',
},
{
color: 'purple',
label: '🏭 Industry',
value: 'INDUSTRY',
},
{ color: 'sky', label: '🤖 SaaS', value: 'SAAS' },
{
color: 'turquoise',
label: '🌿 Green tech',
value: 'GREEN_TECH',
},
{
color: 'yellow',
label: '🚲 Mobility',
value: 'MOBILITY',
},
{ color: 'green', label: '🌏 NGO', value: 'NGO' },
],
defaultValue: undefined,
},
}, },
}; };

View File

@ -1,5 +1,5 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test'; import { expect, userEvent, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui'; import { ComponentDecorator } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -12,10 +12,6 @@ const meta: Meta<typeof SettingsDataModelFieldTypeSelect> = {
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect', 'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect',
component: SettingsDataModelFieldTypeSelect, component: SettingsDataModelFieldTypeSelect,
decorators: [ComponentDecorator], decorators: [ComponentDecorator],
args: {
onChange: fn(),
value: FieldMetadataType.Text,
},
parameters: { parameters: {
container: { width: 512 }, container: { width: 512 },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -1,55 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { FieldMetadataType } from '~/generated/graphql';
import { useFieldMetadataForm } from '../useFieldMetadataForm';
describe('useFieldMetadataForm', () => {
it('should initialize with default values', () => {
const { result } = renderHook(() => useFieldMetadataForm());
expect(result.current.isInitialized).toBe(false);
act(() => {
result.current.initForm({});
});
expect(result.current.isInitialized).toBe(true);
expect(result.current.formValues).toEqual({
type: 'TEXT',
currency: { currencyCode: 'USD' },
relation: {
type: 'ONE_TO_MANY',
objectMetadataId: '',
field: { label: '' },
},
defaultValue: null,
select: [
{ color: 'green', label: 'Option 1', value: expect.any(String) },
],
multiSelect: [
{ color: 'green', label: 'Option 1', value: expect.any(String) },
],
});
});
it('should handle form changes', () => {
const { result } = renderHook(() => useFieldMetadataForm());
act(() => {
result.current.initForm({});
});
expect(result.current.hasFieldFormChanged).toBe(false);
expect(result.current.hasRelationFormChanged).toBe(false);
expect(result.current.hasSelectFormChanged).toBe(false);
act(() => {
result.current.handleFormChange({ type: FieldMetadataType.Number });
});
expect(result.current.hasFieldFormChanged).toBe(true);
expect(result.current.hasRelationFormChanged).toBe(false);
expect(result.current.hasSelectFormChanged).toBe(false);
});
});

View File

@ -1,262 +0,0 @@
import { useState } from 'react';
import { DeepPartial } from 'react-hook-form';
import { v4 } from 'uuid';
import { z } from 'zod';
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { SettingsDataModelFieldSettingsFormValues } from '../components/SettingsDataModelFieldSettingsFormCard';
type FormValues = {
defaultValue: any;
type: SettingsSupportedFieldType;
} & SettingsDataModelFieldSettingsFormValues;
export const fieldMetadataFormDefaultValues: FormValues = {
type: FieldMetadataType.Text,
currency: { currencyCode: CurrencyCode.USD },
relation: {
type: RelationMetadataType.OneToMany,
objectMetadataId: '',
field: { label: '' },
},
defaultValue: null,
select: [{ color: 'green', label: 'Option 1', value: v4() }],
multiSelect: [{ color: 'green', label: 'Option 1', value: v4() }],
};
const relationTargetFieldSchema = z.object({
description: z.string().optional(),
icon: z.string().startsWith('Icon'),
label: z.string().min(1),
defaultValue: z.any(),
});
const fieldSchema = z.object({
defaultValue: z.any(),
type: z.enum(
Object.values(FieldMetadataType) as [
FieldMetadataType,
...FieldMetadataType[],
],
),
});
const currencySchema = fieldSchema.merge(
z.object({
type: z.literal(FieldMetadataType.Currency),
currency: z.object({
currencyCode: z.nativeEnum(CurrencyCode),
}),
}),
);
const relationSchema = fieldSchema.merge(
z.object({
type: z.literal(FieldMetadataType.Relation),
relation: z.object({
field: relationTargetFieldSchema,
objectMetadataId: z.string().uuid(),
type: z.enum([
RelationMetadataType.OneToMany,
RelationMetadataType.OneToOne,
'MANY_TO_ONE',
]),
}),
}),
);
const selectSchema = fieldSchema.merge(
z.object({
type: z.literal(FieldMetadataType.Select),
select: z
.array(
z.object({
color: themeColorSchema,
id: z.string().optional(),
isDefault: z.boolean().optional(),
label: z.string().min(1),
}),
)
.nonempty(),
}),
);
const multiSelectSchema = fieldSchema.merge(
z.object({
type: z.literal(FieldMetadataType.MultiSelect),
multiSelect: z
.array(
z.object({
color: themeColorSchema,
id: z.string().optional(),
isDefault: z.boolean().optional(),
label: z.string().min(1),
}),
)
.nonempty(),
}),
);
const {
Currency: _Currency,
Relation: _Relation,
Select: _Select,
MultiSelect: _MultiSelect,
...otherFieldTypes
} = FieldMetadataType;
type OtherFieldType = Exclude<
FieldMetadataType,
| FieldMetadataType.Currency
| FieldMetadataType.Relation
| FieldMetadataType.Select
| FieldMetadataType.MultiSelect
>;
const otherFieldTypesSchema = fieldSchema.merge(
z.object({
type: z.enum(
Object.values(otherFieldTypes) as [OtherFieldType, ...OtherFieldType[]],
),
}),
);
const schema = z.discriminatedUnion('type', [
currencySchema,
relationSchema,
selectSchema,
multiSelectSchema,
otherFieldTypesSchema,
]);
type PartialFormValues = Partial<Omit<FormValues, 'relation'>> &
DeepPartial<Pick<FormValues, 'relation'>>;
export const useFieldMetadataForm = () => {
const [isInitialized, setIsInitialized] = useState(false);
const [initialFormValues, setInitialFormValues] = useState<FormValues>(
fieldMetadataFormDefaultValues,
);
const [formValues, setFormValues] = useState<FormValues>(
fieldMetadataFormDefaultValues,
);
const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false);
const [hasCurrencyFormChanged, setHasCurrencyFormChanged] = useState(false);
const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false);
const [hasSelectFormChanged, setHasSelectFormChanged] = useState(false);
const [hasMultiSelectFormChanged, setHasMultiSelectFormChanged] =
useState(false);
const [hasDefaultValueChanged, setHasDefaultValueFormChanged] =
useState(false);
const [validationResult, setValidationResult] = useState(
schema.safeParse(formValues),
);
const mergePartialValues = (
previousValues: FormValues,
nextValues: PartialFormValues,
): FormValues => ({
...previousValues,
...nextValues,
currency: { ...previousValues.currency, ...nextValues.currency },
relation: {
...previousValues.relation,
...nextValues.relation,
field: {
...previousValues.relation?.field,
...nextValues.relation?.field,
},
},
});
const initForm = (lazyInitialFormValues: PartialFormValues) => {
if (isInitialized) return;
const mergedFormValues = mergePartialValues(
initialFormValues,
lazyInitialFormValues,
);
setInitialFormValues(mergedFormValues);
setFormValues(mergedFormValues);
setValidationResult(schema.safeParse(mergedFormValues));
setIsInitialized(true);
};
const handleFormChange = (values: PartialFormValues) => {
const nextFormValues = mergePartialValues(formValues, values);
setFormValues(nextFormValues);
setValidationResult(schema.safeParse(nextFormValues));
const {
currency: initialCurrencyFormValues,
relation: initialRelationFormValues,
select: initialSelectFormValues,
multiSelect: initialMultiSelectFormValues,
defaultValue: initialDefaultValue,
...initialFieldFormValues
} = initialFormValues;
const {
currency: nextCurrencyFormValues,
relation: nextRelationFormValues,
select: nextSelectFormValues,
multiSelect: nextMultiSelectFormValues,
defaultValue: nextDefaultValue,
...nextFieldFormValues
} = nextFormValues;
setHasFieldFormChanged(
!isDeeplyEqual(initialFieldFormValues, nextFieldFormValues),
);
setHasCurrencyFormChanged(
nextFieldFormValues.type === FieldMetadataType.Currency &&
!isDeeplyEqual(initialCurrencyFormValues, nextCurrencyFormValues),
);
setHasRelationFormChanged(
nextFieldFormValues.type === FieldMetadataType.Relation &&
!isDeeplyEqual(initialRelationFormValues, nextRelationFormValues),
);
setHasSelectFormChanged(
nextFieldFormValues.type === FieldMetadataType.Select &&
!isDeeplyEqual(initialSelectFormValues, nextSelectFormValues),
);
setHasMultiSelectFormChanged(
nextFieldFormValues.type === FieldMetadataType.MultiSelect &&
!isDeeplyEqual(initialMultiSelectFormValues, nextMultiSelectFormValues),
);
setHasDefaultValueFormChanged(
nextFieldFormValues.type === FieldMetadataType.Boolean &&
!isDeeplyEqual(initialDefaultValue, nextDefaultValue),
);
};
return {
formValues,
handleFormChange,
hasFieldFormChanged,
hasFormChanged:
hasFieldFormChanged ||
hasCurrencyFormChanged ||
hasRelationFormChanged ||
hasSelectFormChanged ||
hasMultiSelectFormChanged ||
hasDefaultValueChanged,
hasRelationFormChanged,
hasSelectFormChanged,
hasMultiSelectFormChanged,
hasDefaultValueChanged,
initForm,
isInitialized,
isValid: validationResult.success,
validatedFormValues: validationResult.success
? validationResult.data
: undefined,
};
};

View File

@ -1,3 +1,11 @@
import { settingsDataModelFieldAboutFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm'; import { z } from 'zod';
export const settingsFieldFormSchema = settingsDataModelFieldAboutFormSchema; import { settingsDataModelFieldAboutFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
import { settingsDataModelFieldSettingsFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { settingsDataModelFieldTypeFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
export const settingsFieldFormSchema = z
.object({})
.merge(settingsDataModelFieldAboutFormSchema)
.merge(settingsDataModelFieldTypeFormSchema)
.and(settingsDataModelFieldSettingsFormSchema);

View File

@ -8,7 +8,7 @@ import { FieldDisplay } from '@/object-record/record-field/components/FieldDispl
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput'; import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput';
import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput'; import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput';
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm'; import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect'; import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
import { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect'; import { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview'; import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview';
@ -24,7 +24,7 @@ export type SettingsDataModelFieldPreviewProps = {
}; };
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
relationObjectMetadataItem?: ObjectMetadataItem; relationObjectMetadataItem?: ObjectMetadataItem;
selectOptions?: SettingsObjectFieldSelectFormValues; selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
shrink?: boolean; shrink?: boolean;
withFieldLabel?: boolean; withFieldLabel?: boolean;
}; };

View File

@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty'; import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm'; import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
import { getFieldDefaultPreviewValue } from '@/settings/data-model/utils/getFieldDefaultPreviewValue'; import { getFieldDefaultPreviewValue } from '@/settings/data-model/utils/getFieldDefaultPreviewValue';
import { getFieldPreviewValueFromRecord } from '@/settings/data-model/utils/getFieldPreviewValueFromRecord'; import { getFieldPreviewValueFromRecord } from '@/settings/data-model/utils/getFieldPreviewValueFromRecord';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -16,7 +16,7 @@ type UseFieldPreviewParams = {
}; };
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
relationObjectMetadataItem?: ObjectMetadataItem; relationObjectMetadataItem?: ObjectMetadataItem;
selectOptions?: SettingsObjectFieldSelectFormValues; selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
}; };
export const useFieldPreview = ({ export const useFieldPreview = ({

View File

@ -3,7 +3,6 @@ import styled from '@emotion/styled';
import { z } from 'zod'; import { z } from 'zod';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { IconPicker } from '@/ui/input/components/IconPicker'; import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextArea } from '@/ui/input/components/TextArea'; import { TextArea } from '@/ui/input/components/TextArea';
@ -93,11 +92,7 @@ export const SettingsDataModelObjectAboutForm = ({
label={label} label={label}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
onChange={(value) => { onChange={onChange}
if (!value || validateMetadataLabel(value)) {
onChange?.(value);
}
}}
disabled={disabled} disabled={disabled}
fullWidth fullWidth
/> />

View File

@ -1,5 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm'; import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
import { import {
mockedCompanyObjectMetadataItem, mockedCompanyObjectMetadataItem,
mockedOpportunityObjectMetadataItem, mockedOpportunityObjectMetadataItem,
@ -16,8 +16,19 @@ describe('getFieldDefaultPreviewValue', () => {
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
({ name }) => name === 'stage', ({ name }) => name === 'stage',
)!; )!;
const selectOptions: SettingsObjectFieldSelectFormValues = const selectOptions: SettingsDataModelFieldSelectFormValues['options'] = [
fieldMetadataItem.options ?? []; {
color: 'purple',
label: '🏭 Industry',
value: 'INDUSTRY',
},
{
color: 'pink',
isDefault: true,
label: '💊 Health',
value: 'HEALTH',
},
];
// When // When
const result = getFieldDefaultPreviewValue({ const result = getFieldDefaultPreviewValue({
@ -27,7 +38,7 @@ describe('getFieldDefaultPreviewValue', () => {
}); });
// Then // Then
expect(result).toEqual(selectOptions[0].value); expect(result).toEqual(selectOptions[1].value);
}); });
it('returns the first select option if no default option was found', () => { it('returns the first select option if no default option was found', () => {
@ -36,8 +47,18 @@ describe('getFieldDefaultPreviewValue', () => {
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
({ name }) => name === 'stage', ({ name }) => name === 'stage',
)!; )!;
const selectOptions: SettingsObjectFieldSelectFormValues = const selectOptions: SettingsDataModelFieldSelectFormValues['options'] = [
fieldMetadataItem.options ?? []; {
color: 'purple' as const,
label: '🏭 Industry',
value: 'INDUSTRY',
},
{
color: 'pink' as const,
label: '💊 Health',
value: 'HEALTH',
},
];
// When // When
const result = getFieldDefaultPreviewValue({ const result = getFieldDefaultPreviewValue({

View File

@ -1,5 +1,5 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm'; import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
import { import {
mockedCompanyObjectMetadataItem, mockedCompanyObjectMetadataItem,
mockedOpportunityObjectMetadataItem, mockedOpportunityObjectMetadataItem,
@ -20,8 +20,34 @@ describe('getFieldPreviewValueFromRecord', () => {
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
({ name }) => name === 'stage', ({ name }) => name === 'stage',
)!; )!;
const selectOptions: SettingsObjectFieldSelectFormValues = const selectOptions: SettingsDataModelFieldSelectFormValues['options'] = [
fieldMetadataItem.options ?? []; {
color: 'red',
label: 'New',
value: 'NEW',
},
{
color: 'purple',
label: 'Screening',
value: 'SCREENING',
},
{
color: 'sky',
label: 'Meeting',
value: 'MEETING',
isDefault: true,
},
{
color: 'turquoise',
label: 'Proposal',
value: 'PROPOSAL',
},
{
color: 'yellow',
label: 'Customer',
value: 'CUSTOMER',
},
];
// When // When
const result = getFieldPreviewValueFromRecord({ const result = getFieldPreviewValueFromRecord({
@ -44,8 +70,24 @@ describe('getFieldPreviewValueFromRecord', () => {
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
({ name }) => name === 'stage', ({ name }) => name === 'stage',
)!; )!;
const selectOptions: SettingsObjectFieldSelectFormValues = const selectOptions: SettingsDataModelFieldSelectFormValues['options'] = [
fieldMetadataItem.options ?? []; {
color: 'purple',
label: '🏭 Industry',
value: 'INDUSTRY',
},
{
color: 'pink',
isDefault: true,
label: '💊 Health',
value: 'HEALTH',
},
{
color: 'turquoise',
label: '🌿 Green tech',
value: 'GREEN_TECH',
},
];
// When // When
const result = getFieldPreviewValueFromRecord({ const result = getFieldPreviewValueFromRecord({

View File

@ -2,7 +2,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm'; import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -19,7 +19,7 @@ export const getFieldDefaultPreviewValue = ({
}; };
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
relationObjectMetadataItem?: ObjectMetadataItem; relationObjectMetadataItem?: ObjectMetadataItem;
selectOptions?: SettingsObjectFieldSelectFormValues; selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
}) => { }) => {
if ( if (
fieldMetadataItem.type === FieldMetadataType.Select && fieldMetadataItem.type === FieldMetadataType.Select &&

View File

@ -1,6 +1,6 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm'; import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getFieldPreviewValueFromRecord = ({ export const getFieldPreviewValueFromRecord = ({
@ -10,7 +10,7 @@ export const getFieldPreviewValueFromRecord = ({
}: { }: {
record: ObjectRecord; record: ObjectRecord;
fieldMetadataItem: Pick<FieldMetadataItem, 'name' | 'type'>; fieldMetadataItem: Pick<FieldMetadataItem, 'name' | 'type'>;
selectOptions?: SettingsObjectFieldSelectFormValues; selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
}) => { }) => {
const recordFieldValue = record[fieldMetadataItem.name]; const recordFieldValue = record[fieldMetadataItem.name];

View File

@ -4,25 +4,27 @@ import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import omit from 'lodash.omit';
import pick from 'lodash.pick';
import { IconArchive, IconSettings } from 'twenty-ui'; import { IconArchive, IconSettings } from 'twenty-ui';
import { v4 } from 'uuid';
import { z } from 'zod'; import { z } from 'zod';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdateOneFieldMetadataItem';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFieldCurrencyFormValues } from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm'; import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect'; import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
import { useFieldMetadataForm } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { H2Title } from '@/ui/display/typography/components/H2Title'; import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -30,10 +32,7 @@ import { Button } from '@/ui/input/button/components/Button';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { import { FieldMetadataType } from '~/generated-metadata/graphql';
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
type SettingsDataModelFieldEditFormValues = z.infer< type SettingsDataModelFieldEditFormValues = z.infer<
typeof settingsFieldFormSchema typeof settingsFieldFormSchema
@ -66,117 +65,38 @@ export const SettingsObjectFieldEdit = () => {
const activeObjectMetadataItem = const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug); findActiveObjectMetadataItemBySlug(objectSlug);
const { disableMetadataField, editMetadataField } = useFieldMetadataItem(); const { disableMetadataField } = useFieldMetadataItem();
const activeMetadataField = activeObjectMetadataItem?.fields.find( const activeMetadataField = activeObjectMetadataItem?.fields.find(
(metadataField) => (metadataField) =>
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug, metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
); );
const getRelationMetadata = useGetRelationMetadata(); const getRelationMetadata = useGetRelationMetadata();
const { const { relationFieldMetadataItem } =
relationFieldMetadataItem,
relationObjectMetadataItem,
relationType,
} =
useMemo( useMemo(
() => () =>
activeMetadataField activeMetadataField
? getRelationMetadata({ ? getRelationMetadata({ fieldMetadataItem: activeMetadataField })
fieldMetadataItem: activeMetadataField,
})
: null, : null,
[activeMetadataField, getRelationMetadata], [activeMetadataField, getRelationMetadata],
) ?? {}; ) ?? {};
const { updateOneFieldMetadataItem } = useUpdateOneFieldMetadataItem();
const formConfig = useForm<SettingsDataModelFieldEditFormValues>({ const formConfig = useForm<SettingsDataModelFieldEditFormValues>({
mode: 'onTouched', mode: 'onTouched',
resolver: zodResolver(settingsFieldFormSchema), resolver: zodResolver(settingsFieldFormSchema),
}); });
const {
formValues,
handleFormChange,
hasFieldFormChanged,
hasDefaultValueChanged,
hasFormChanged,
hasRelationFormChanged,
hasSelectFormChanged,
hasMultiSelectFormChanged,
initForm,
isInitialized,
isValid,
validatedFormValues,
} = useFieldMetadataForm();
useEffect(() => { useEffect(() => {
if (!activeObjectMetadataItem || !activeMetadataField) { if (!activeObjectMetadataItem || !activeMetadataField) {
navigate(AppPath.NotFound); navigate(AppPath.NotFound);
return;
} }
}, [activeMetadataField, activeObjectMetadataItem, navigate]);
const { defaultValue } = activeMetadataField; if (!activeObjectMetadataItem || !activeMetadataField) return null;
const currencyDefaultValue = const canSave = formConfig.formState.isValid && formConfig.formState.isDirty;
activeMetadataField.type === FieldMetadataType.Currency
? (defaultValue as SettingsObjectFieldCurrencyFormValues | undefined)
: undefined;
const selectOptions = activeMetadataField.options?.map((option) => ({
...option,
isDefault: defaultValue === `'${option.value}'`,
}));
selectOptions?.sort(
(optionA, optionB) => optionA.position - optionB.position,
);
const multiSelectOptions = activeMetadataField.options?.map((option) => ({
...option,
isDefault: defaultValue?.includes(`'${option.value}'`) || false,
}));
multiSelectOptions?.sort(
(optionA, optionB) => optionA.position - optionB.position,
);
const fieldType = activeMetadataField.type;
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType);
if (!isFieldTypeSupported) return;
initForm({
type: fieldType,
...(currencyDefaultValue ? { currency: currencyDefaultValue } : {}),
relation: {
field: {
icon: relationFieldMetadataItem?.icon,
label: relationFieldMetadataItem?.label || '',
},
objectMetadataId: relationObjectMetadataItem?.id || '',
type: relationType || RelationMetadataType.OneToMany,
},
defaultValue: activeMetadataField.defaultValue,
...(selectOptions?.length ? { select: selectOptions } : {}),
...(multiSelectOptions?.length
? { multiSelect: multiSelectOptions }
: {}),
});
}, [
activeMetadataField,
activeObjectMetadataItem,
initForm,
navigate,
relationFieldMetadataItem?.icon,
relationFieldMetadataItem?.label,
relationObjectMetadataItem?.id,
relationType,
]);
if (!isInitialized || !activeObjectMetadataItem || !activeMetadataField)
return null;
const canSave =
formConfig.formState.isValid &&
isValid &&
(formConfig.formState.isDirty || hasFormChanged);
const isLabelIdentifier = isLabelIdentifierField({ const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem: activeMetadataField, fieldMetadataItem: activeMetadataField,
@ -184,43 +104,37 @@ export const SettingsObjectFieldEdit = () => {
}); });
const handleSave = async () => { const handleSave = async () => {
if (!validatedFormValues) return;
const formValues = formConfig.getValues(); const formValues = formConfig.getValues();
const { dirtyFields } = formConfig.formState; const { dirtyFields } = formConfig.formState;
try { try {
if ( if (
validatedFormValues.type === FieldMetadataType.Relation && formValues.type === FieldMetadataType.Relation &&
isNonEmptyString(relationFieldMetadataItem?.id) && isNonEmptyString(relationFieldMetadataItem?.id) &&
hasRelationFormChanged 'relation' in dirtyFields
) { ) {
await editMetadataField({ await updateOneFieldMetadataItem({
icon: validatedFormValues.relation.field.icon, fieldMetadataIdToUpdate: relationFieldMetadataItem.id,
id: relationFieldMetadataItem?.id, updatePayload: formValues.relation.field,
label: validatedFormValues.relation.field.label,
type: validatedFormValues.type,
}); });
} }
if ( const otherDirtyFields = omit(dirtyFields, 'relation');
Object.keys(dirtyFields).length > 0 ||
hasFieldFormChanged || if (Object.keys(otherDirtyFields).length > 0) {
hasSelectFormChanged || const formattedInput = pick(
hasMultiSelectFormChanged || formatFieldMetadataItemInput(formValues),
hasDefaultValueChanged Object.keys(otherDirtyFields),
) { );
await editMetadataField({
...formValues, const options = formattedInput.options?.map((option) => ({
id: activeMetadataField.id, ...option,
defaultValue: validatedFormValues.defaultValue, id: option.id ?? v4(),
type: validatedFormValues.type, }));
options:
validatedFormValues.type === FieldMetadataType.Select await updateOneFieldMetadataItem({
? validatedFormValues.select fieldMetadataIdToUpdate: activeMetadataField.id,
: validatedFormValues.type === FieldMetadataType.MultiSelect updatePayload: { ...formattedInput, options },
? validatedFormValues.multiSelect
: undefined,
}); });
} }
@ -281,28 +195,13 @@ export const SettingsObjectFieldEdit = () => {
/> />
<StyledSettingsObjectFieldTypeSelect <StyledSettingsObjectFieldTypeSelect
disabled disabled
onChange={handleFormChange} fieldMetadataItem={activeMetadataField}
value={formValues.type}
/> />
<SettingsDataModelFieldSettingsFormCard <SettingsDataModelFieldSettingsFormCard
disableCurrencyForm disableCurrencyForm
fieldMetadataItem={{ fieldMetadataItem={activeMetadataField}
icon: formConfig.watch('icon'),
id: activeMetadataField.id,
label: formConfig.watch('label'),
name: activeMetadataField.name,
type: formValues.type,
}}
objectMetadataItem={activeObjectMetadataItem} objectMetadataItem={activeObjectMetadataItem}
onChange={handleFormChange}
relationFieldMetadataItem={relationFieldMetadataItem} relationFieldMetadataItem={relationFieldMetadataItem}
values={{
currency: formValues.currency,
relation: formValues.relation,
select: formValues.select,
multiSelect: formValues.multiSelect,
defaultValue: formValues.defaultValue,
}}
/> />
</Section> </Section>
{!isLabelIdentifier && ( {!isLabelIdentifier && (

View File

@ -22,7 +22,6 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm'; import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect'; import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
import { useFieldMetadataForm } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
@ -51,25 +50,14 @@ export const SettingsObjectNewFieldStep2 = () => {
const { objectSlug = '' } = useParams(); const { objectSlug = '' } = useParams();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { const { findActiveObjectMetadataItemBySlug, findObjectMetadataItemById } =
findActiveObjectMetadataItemBySlug, useFilteredObjectMetadataItems();
findObjectMetadataItemById,
findObjectMetadataItemByNamePlural,
} = useFilteredObjectMetadataItems();
const activeObjectMetadataItem = const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug); findActiveObjectMetadataItemBySlug(objectSlug);
const { createMetadataField } = useFieldMetadataItem(); const { createMetadataField } = useFieldMetadataItem();
const cache = useApolloClient().cache; const cache = useApolloClient().cache;
const {
formValues,
handleFormChange,
initForm,
isValid,
validatedFormValues,
} = useFieldMetadataForm();
const formConfig = useForm<SettingsDataModelNewFieldFormValues>({ const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
mode: 'onTouched', mode: 'onTouched',
resolver: zodResolver(settingsFieldFormSchema), resolver: zodResolver(settingsFieldFormSchema),
@ -78,23 +66,8 @@ export const SettingsObjectNewFieldStep2 = () => {
useEffect(() => { useEffect(() => {
if (!activeObjectMetadataItem) { if (!activeObjectMetadataItem) {
navigate(AppPath.NotFound); navigate(AppPath.NotFound);
return;
} }
}, [activeObjectMetadataItem, navigate]);
initForm({
relation: {
field: { icon: activeObjectMetadataItem.icon },
objectMetadataId:
findObjectMetadataItemByNamePlural('people')?.id || '',
},
});
}, [
activeObjectMetadataItem,
findObjectMetadataItemByNamePlural,
initForm,
navigate,
]);
const [objectViews, setObjectViews] = useState<View[]>([]); const [objectViews, setObjectViews] = useState<View[]>([]);
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]); const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
@ -116,12 +89,16 @@ export const SettingsObjectNewFieldStep2 = () => {
}, },
}); });
const relationObjectMetadataId = formConfig.watch(
'relation.objectMetadataId',
);
useFindManyRecords<View>({ useFindManyRecords<View>({
objectNameSingular: CoreObjectNameSingular.View, objectNameSingular: CoreObjectNameSingular.View,
skip: !formValues.relation?.objectMetadataId, skip: !relationObjectMetadataId,
filter: { filter: {
type: { eq: ViewType.Table }, type: { eq: ViewType.Table },
objectMetadataId: { eq: formValues.relation?.objectMetadataId }, objectMetadataId: { eq: relationObjectMetadataId },
}, },
onCompleted: async (views) => { onCompleted: async (views) => {
if (isUndefinedOrNull(views)) return; if (isUndefinedOrNull(views)) return;
@ -135,37 +112,40 @@ export const SettingsObjectNewFieldStep2 = () => {
if (!activeObjectMetadataItem) return null; if (!activeObjectMetadataItem) return null;
const canSave = formConfig.formState.isValid && isValid; const canSave = formConfig.formState.isValid;
const handleSave = async () => { const handleSave = async () => {
if (!validatedFormValues) return;
const formValues = formConfig.getValues(); const formValues = formConfig.getValues();
try { try {
if (validatedFormValues.type === FieldMetadataType.Relation) { if (
formValues.type === FieldMetadataType.Relation &&
'relation' in formValues
) {
const { relation: relationFormValues, ...fieldFormValues } = formValues;
const createdRelation = await createOneRelationMetadata({ const createdRelation = await createOneRelationMetadata({
relationType: validatedFormValues.relation.type, relationType: relationFormValues.type,
field: pick(formValues, ['icon', 'label', 'description']), field: pick(fieldFormValues, ['icon', 'label', 'description']),
objectMetadataId: activeObjectMetadataItem.id, objectMetadataId: activeObjectMetadataItem.id,
connect: { connect: {
field: { field: {
icon: validatedFormValues.relation.field.icon, icon: relationFormValues.field.icon,
label: validatedFormValues.relation.field.label, label: relationFormValues.field.label,
}, },
objectMetadataId: validatedFormValues.relation.objectMetadataId, objectMetadataId: relationFormValues.objectMetadataId,
}, },
}); });
const relationObjectMetadataItem = findObjectMetadataItemById( const relationObjectMetadataItem = findObjectMetadataItemById(
validatedFormValues.relation.objectMetadataId, relationFormValues.objectMetadataId,
); );
objectViews.map(async (view) => { objectViews.map(async (view) => {
const viewFieldToCreate = { const viewFieldToCreate = {
viewId: view.id, viewId: view.id,
fieldMetadataId: fieldMetadataId:
validatedFormValues.relation.type === 'MANY_TO_ONE' relationFormValues.type === 'MANY_TO_ONE'
? createdRelation.data?.createOneRelation.toFieldMetadataId ? createdRelation.data?.createOneRelation.toFieldMetadataId
: createdRelation.data?.createOneRelation.fromFieldMetadataId, : createdRelation.data?.createOneRelation.fromFieldMetadataId,
position: activeObjectMetadataItem.fields.length, position: activeObjectMetadataItem.fields.length,
@ -198,7 +178,7 @@ export const SettingsObjectNewFieldStep2 = () => {
const viewFieldToCreate = { const viewFieldToCreate = {
viewId: view.id, viewId: view.id,
fieldMetadataId: fieldMetadataId:
validatedFormValues.relation.type === 'MANY_TO_ONE' relationFormValues.type === 'MANY_TO_ONE'
? createdRelation.data?.createOneRelation.fromFieldMetadataId ? createdRelation.data?.createOneRelation.fromFieldMetadataId
: createdRelation.data?.createOneRelation.toFieldMetadataId, : createdRelation.data?.createOneRelation.toFieldMetadataId,
position: relationObjectMetadataItem?.fields.length, position: relationObjectMetadataItem?.fields.length,
@ -229,22 +209,17 @@ export const SettingsObjectNewFieldStep2 = () => {
}); });
} else { } else {
const createdMetadataField = await createMetadataField({ const createdMetadataField = await createMetadataField({
defaultValue:
validatedFormValues.type === FieldMetadataType.Currency
? {
amountMicros: null,
currencyCode: validatedFormValues.currency.currencyCode,
}
: validatedFormValues.defaultValue,
...formValues, ...formValues,
objectMetadataId: activeObjectMetadataItem.id, defaultValue:
type: validatedFormValues.type, formValues.type === FieldMetadataType.Currency
options: ? {
validatedFormValues.type === FieldMetadataType.Select ...formValues.defaultValue,
? validatedFormValues.select amountMicros: null,
: validatedFormValues.type === FieldMetadataType.MultiSelect }
? validatedFormValues.multiSelect : 'defaultValue' in formValues
? formValues.defaultValue
: undefined, : undefined,
objectMetadataId: activeObjectMetadataItem.id,
}); });
objectViews.map(async (view) => { objectViews.map(async (view) => {
@ -335,24 +310,14 @@ export const SettingsObjectNewFieldStep2 = () => {
/> />
<StyledSettingsObjectFieldTypeSelect <StyledSettingsObjectFieldTypeSelect
excludedFieldTypes={excludedFieldTypes} excludedFieldTypes={excludedFieldTypes}
onChange={handleFormChange}
value={formValues.type}
/> />
<SettingsDataModelFieldSettingsFormCard <SettingsDataModelFieldSettingsFormCard
fieldMetadataItem={{ fieldMetadataItem={{
icon: formConfig.watch('icon'), icon: formConfig.watch('icon'),
label: formConfig.watch('label') || 'Employees', label: formConfig.watch('label') || 'Employees',
type: formValues.type, type: formConfig.watch('type'),
}} }}
objectMetadataItem={activeObjectMetadataItem} objectMetadataItem={activeObjectMetadataItem}
onChange={handleFormChange}
values={{
currency: formValues.currency,
relation: formValues.relation,
select: formValues.select,
multiSelect: formValues.multiSelect,
defaultValue: formValues.defaultValue,
}}
/> />
</Section> </Section>
</SettingsPageContainer> </SettingsPageContainer>