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:
@ -1033,7 +1033,9 @@ export type User = {
|
||||
id: Scalars['UUID']['output'];
|
||||
lastName: 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']>;
|
||||
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
|
||||
passwordResetTokenExpiresAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
supportUserHash?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
|
||||
@ -69,18 +69,7 @@ export const variables = {
|
||||
disableMetadataField: {
|
||||
idToUpdate: fieldId,
|
||||
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 = {
|
||||
|
||||
@ -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 }) => (
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
@ -26,49 +22,12 @@ export const useFieldMetadataItem = () => {
|
||||
) => {
|
||||
const formattedInput = formatFieldMetadataItemInput(input);
|
||||
|
||||
const defaultValue = getDefaultValueForBackend(
|
||||
input.defaultValue ?? formattedInput.defaultValue,
|
||||
input.type,
|
||||
);
|
||||
|
||||
return createOneFieldMetadataItem({
|
||||
...formattedInput,
|
||||
defaultValue,
|
||||
objectMetadataId: input.objectMetadataId,
|
||||
type: input.type,
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
label: formattedInput.label ?? '',
|
||||
name: formattedInput.name ?? '',
|
||||
});
|
||||
};
|
||||
|
||||
@ -92,6 +51,5 @@ export const useFieldMetadataItem = () => {
|
||||
createMetadataField,
|
||||
disableMetadataField,
|
||||
eraseMetadataField,
|
||||
editMetadataField,
|
||||
};
|
||||
};
|
||||
|
||||
@ -12,7 +12,14 @@ import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
export const useGetRelationMetadata = () =>
|
||||
useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({ fieldMetadataItem }: { fieldMetadataItem: FieldMetadataItem }) => {
|
||||
({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
|
||||
>;
|
||||
}) => {
|
||||
if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null;
|
||||
|
||||
const relationMetadata =
|
||||
|
||||
@ -77,7 +77,7 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
value: 'OPTION_2',
|
||||
},
|
||||
],
|
||||
defaultValue: 'OPTION_1',
|
||||
defaultValue: "'OPTION_1'",
|
||||
};
|
||||
|
||||
const result = formatFieldMetadataItemInput(input);
|
||||
@ -140,7 +140,7 @@ describe('formatFieldMetadataItemInput', () => {
|
||||
value: 'OPTION_2',
|
||||
},
|
||||
],
|
||||
defaultValue: ['OPTION_1', 'OPTION_2'],
|
||||
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
|
||||
};
|
||||
|
||||
const result = formatFieldMetadataItemInput(input);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,8 @@
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
@ -21,25 +23,24 @@ export const getOptionValueFromLabel = (label: string) => {
|
||||
};
|
||||
|
||||
export const formatFieldMetadataItemInput = (
|
||||
input: Pick<
|
||||
Field,
|
||||
'label' | 'icon' | 'description' | 'defaultValue' | 'options'
|
||||
> & { type?: FieldMetadataType },
|
||||
input: Partial<
|
||||
Pick<
|
||||
FieldMetadataItem,
|
||||
'type' | 'label' | 'defaultValue' | 'icon' | 'description'
|
||||
>
|
||||
> & { options?: FieldMetadataOption[] },
|
||||
) => {
|
||||
const options = input.options as FieldMetadataOption[];
|
||||
const options = input.options as FieldMetadataOption[] | undefined;
|
||||
let defaultValue = input.defaultValue;
|
||||
if (input.type === FieldMetadataType.MultiSelect) {
|
||||
const defaultOptions = options?.filter((option) => option.isDefault);
|
||||
if (isDefined(defaultOptions)) {
|
||||
defaultValue = defaultOptions.map(
|
||||
(defaultOption) => `${getOptionValueFromLabel(defaultOption.label)}`,
|
||||
);
|
||||
}
|
||||
defaultValue = options
|
||||
?.filter((option) => option.isDefault)
|
||||
?.map((defaultOption) => getOptionValueFromLabel(defaultOption.label));
|
||||
}
|
||||
if (input.type === FieldMetadataType.Select) {
|
||||
const defaultOption = options?.find((option) => option.isDefault);
|
||||
defaultValue = isDefined(defaultOption)
|
||||
? `${getOptionValueFromLabel(defaultOption.label)}`
|
||||
? getOptionValueFromLabel(defaultOption.label)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@ -59,12 +60,17 @@ export const formatFieldMetadataItemInput = (
|
||||
}
|
||||
}
|
||||
|
||||
const label = input.label?.trim();
|
||||
|
||||
return {
|
||||
defaultValue,
|
||||
defaultValue:
|
||||
isDefined(defaultValue) && input.type
|
||||
? getDefaultValueForBackend(defaultValue, input.type)
|
||||
: undefined,
|
||||
description: input.description?.trim() ?? null,
|
||||
icon: input.icon,
|
||||
label: input.label.trim(),
|
||||
name: formatMetadataLabelToMetadataNameOrThrows(input.label.trim()),
|
||||
label,
|
||||
name: label ? formatMetadataLabelToMetadataNameOrThrows(label) : undefined,
|
||||
options: options?.map((option, index) => ({
|
||||
color: option.color,
|
||||
id: option.id,
|
||||
|
||||
@ -35,14 +35,14 @@ export const formatRelationMetadataInput = (
|
||||
const {
|
||||
description: fromDescription,
|
||||
icon: fromIcon,
|
||||
label: fromLabel,
|
||||
name: fromName,
|
||||
label: fromLabel = '',
|
||||
name: fromName = '',
|
||||
} = formatFieldMetadataItemInput(fromField);
|
||||
const {
|
||||
description: toDescription,
|
||||
icon: toIcon,
|
||||
label: toLabel,
|
||||
name: toName,
|
||||
label: toLabel = '',
|
||||
name: toName = '',
|
||||
} = formatFieldMetadataItemInput(toField);
|
||||
|
||||
return {
|
||||
|
||||
@ -13,7 +13,7 @@ export const getDefaultValueForBackend = (
|
||||
currencyCode: `'${currencyDefaultValue.currencyCode}'` as CurrencyCode,
|
||||
} satisfies FieldCurrencyValue;
|
||||
} else if (fieldMetadataType === FieldMetadataType.Select) {
|
||||
return `'${defaultValue}'`;
|
||||
return defaultValue ? `'${defaultValue}'` : null;
|
||||
} else if (fieldMetadataType === FieldMetadataType.MultiSelect) {
|
||||
return defaultValue.map((value: string) => `'${value}'`);
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
const metadataLabelValidationPattern = /^[^0-9].*$/;
|
||||
|
||||
export const validateMetadataLabel = (value: string) =>
|
||||
!!value.match(metadataLabelValidationPattern);
|
||||
@ -14,7 +14,7 @@ export const MultiSelectFieldDisplay = ({
|
||||
const { fieldValues, fieldDefinition } = useMultiSelectField();
|
||||
|
||||
const selectedOptions = fieldValues
|
||||
? fieldDefinition.metadata.options.filter((option) =>
|
||||
? fieldDefinition.metadata.options?.filter((option) =>
|
||||
fieldValues.includes(option.value),
|
||||
)
|
||||
: [];
|
||||
|
||||
@ -6,7 +6,11 @@ import { useRelationField } from '../../hooks/useRelationField';
|
||||
export const RelationFieldDisplay = () => {
|
||||
const { fieldValue, fieldDefinition, maxWidth } = useRelationField();
|
||||
|
||||
if (!fieldValue || !fieldDefinition) return null;
|
||||
if (
|
||||
!fieldValue ||
|
||||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<RecordChip
|
||||
|
||||
@ -5,7 +5,7 @@ import { useSelectField } from '../../hooks/useSelectField';
|
||||
export const SelectFieldDisplay = () => {
|
||||
const { fieldValue, fieldDefinition } = useSelectField();
|
||||
|
||||
const selectedOption = fieldDefinition.metadata.options.find(
|
||||
const selectedOption = fieldDefinition.metadata.options?.find(
|
||||
(option) => option.value === fieldValue,
|
||||
);
|
||||
|
||||
|
||||
@ -1,17 +1,26 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
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 { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
type SettingsDataModelDefaultValueFormProps = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
onChange?: (defaultValue: SettingsDataModelDefaultValue) => void;
|
||||
value?: SettingsDataModelDefaultValue;
|
||||
};
|
||||
// TODO: rename to SettingsDataModelFieldBooleanForm and move to settings/data-model/fields/forms/components
|
||||
|
||||
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)`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3.5)};
|
||||
@ -26,22 +35,28 @@ const StyledLabel = styled.span`
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelDefaultValueForm = ({
|
||||
export const SettingsDataModelFieldBooleanForm = ({
|
||||
className,
|
||||
disabled,
|
||||
onChange,
|
||||
value,
|
||||
}: SettingsDataModelDefaultValueFormProps) => {
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldBooleanFormProps) => {
|
||||
const { control } = useFormContext<SettingsDataModelFieldBooleanFormValues>();
|
||||
|
||||
const initialValue = fieldMetadataItem?.defaultValue ?? true;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLabel>Default Value</StyledLabel>
|
||||
<Controller
|
||||
name="defaultValue"
|
||||
control={control}
|
||||
defaultValue={initialValue}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
className={className}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-default-value-select"
|
||||
value={value}
|
||||
onChange={(value) => onChange?.(value)}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{
|
||||
value: true,
|
||||
@ -55,6 +70,8 @@ export const SettingsDataModelDefaultValueForm = ({
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
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 = {
|
||||
currencyCode: CurrencyCode;
|
||||
};
|
||||
export const settingsDataModelFieldCurrencyFormSchema = z.object({
|
||||
defaultValue: z.object({
|
||||
currencyCode: z.nativeEnum(CurrencyCode),
|
||||
}),
|
||||
});
|
||||
|
||||
type SettingsObjectFieldCurrencyFormProps = {
|
||||
type SettingsDataModelFieldCurrencyFormValues = z.infer<
|
||||
typeof settingsDataModelFieldCurrencyFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldCurrencyFormProps = {
|
||||
disabled?: boolean;
|
||||
onChange: (values: Partial<SettingsObjectFieldCurrencyFormValues>) => void;
|
||||
values: SettingsObjectFieldCurrencyFormValues;
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue'>;
|
||||
};
|
||||
|
||||
export const SettingsObjectFieldCurrencyForm = ({
|
||||
disabled,
|
||||
onChange,
|
||||
values,
|
||||
}: SettingsObjectFieldCurrencyFormProps) => (
|
||||
<CardContent>
|
||||
<Select
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
label="Default Unit"
|
||||
dropdownId="currency-unit-select"
|
||||
value={values.currencyCode}
|
||||
options={Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
||||
const OPTIONS = Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
|
||||
([value, { label, Icon }]) => ({
|
||||
label,
|
||||
value: value as CurrencyCode,
|
||||
Icon,
|
||||
}),
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
onChange={(value) => onChange({ currencyCode: value })}
|
||||
/>
|
||||
</CardContent>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,28 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { useIcons } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
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 { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
|
||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
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 { RelationType } from '../types/RelationType';
|
||||
|
||||
export type SettingsObjectFieldRelationFormValues = {
|
||||
field: Pick<Field, 'icon' | 'label'>;
|
||||
objectMetadataId: string;
|
||||
type: RelationType;
|
||||
};
|
||||
// TODO: rename to SettingsDataModelFieldRelationForm and move to settings/data-model/fields/forms/components
|
||||
|
||||
type SettingsObjectFieldRelationFormProps = {
|
||||
disableFieldEdition?: boolean;
|
||||
disableRelationEdition?: boolean;
|
||||
onChange: (values: Partial<SettingsObjectFieldRelationFormValues>) => void;
|
||||
values: SettingsObjectFieldRelationFormValues;
|
||||
export const settingsDataModelFieldRelationFormSchema = z.object({
|
||||
relation: z.object({
|
||||
field: fieldMetadataItemSchema.pick({
|
||||
icon: true,
|
||||
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`
|
||||
@ -50,85 +68,120 @@ const StyledInputsContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsObjectFieldRelationForm = ({
|
||||
disableFieldEdition,
|
||||
disableRelationEdition,
|
||||
onChange,
|
||||
values,
|
||||
}: SettingsObjectFieldRelationFormProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { objectMetadataItems, findObjectMetadataItemById } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const selectedObjectMetadataItem =
|
||||
(values.objectMetadataId
|
||||
? findObjectMetadataItemById(values.objectMetadataId)
|
||||
: undefined) || objectMetadataItems[0];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSelectsContainer>
|
||||
<Select
|
||||
label="Relation type"
|
||||
dropdownId="relation-type-select"
|
||||
fullWidth
|
||||
disabled={disableRelationEdition}
|
||||
value={values.type}
|
||||
options={Object.entries(RELATION_TYPES)
|
||||
const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES)
|
||||
.filter(([value]) => 'ONE_TO_ONE' !== value)
|
||||
.map(([value, { label, Icon }]) => ({
|
||||
label,
|
||||
value: value as RelationType,
|
||||
Icon,
|
||||
}))}
|
||||
onChange={(value) => onChange({ type: value })}
|
||||
}));
|
||||
|
||||
export const SettingsDataModelFieldRelationForm = ({
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldRelationFormProps) => {
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldRelationFormValues>();
|
||||
const { getIcon } = useIcons();
|
||||
const { objectMetadataItems } = 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 =
|
||||
relationObjectMetadataItem ?? objectMetadataItems[0];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSelectsContainer>
|
||||
<Controller
|
||||
name="relation.type"
|
||||
control={control}
|
||||
defaultValue={relationType ?? RelationMetadataType.OneToMany}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
label="Relation type"
|
||||
dropdownId="relation-type-select"
|
||||
fullWidth
|
||||
disabled={disableRelationEdition}
|
||||
value={value}
|
||||
options={RELATION_TYPE_OPTIONS}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="relation.objectMetadataId"
|
||||
control={control}
|
||||
defaultValue={selectedObjectMetadataItem?.id}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
label="Object destination"
|
||||
dropdownId="object-destination-select"
|
||||
fullWidth
|
||||
disabled={disableRelationEdition}
|
||||
value={values.objectMetadataId}
|
||||
value={value}
|
||||
options={objectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
isObjectMetadataAvailableForRelation(objectMetadataItem),
|
||||
)
|
||||
.filter(isObjectMetadataAvailableForRelation)
|
||||
.map((objectMetadataItem) => ({
|
||||
label: objectMetadataItem.labelPlural,
|
||||
value: objectMetadataItem.id,
|
||||
Icon: getIcon(objectMetadataItem.icon),
|
||||
}))}
|
||||
onChange={(value) => onChange({ objectMetadataId: value })}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledSelectsContainer>
|
||||
<StyledInputsLabel>
|
||||
Field on {selectedObjectMetadataItem?.labelPlural}
|
||||
</StyledInputsLabel>
|
||||
<StyledInputsContainer>
|
||||
<Controller
|
||||
name="relation.field.icon"
|
||||
control={control}
|
||||
defaultValue={
|
||||
relationFieldMetadataItem?.icon ??
|
||||
relationObjectMetadataItem?.icon ??
|
||||
'IconUsers'
|
||||
}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<IconPicker
|
||||
disabled={disableFieldEdition}
|
||||
dropdownId="field-destination-icon-picker"
|
||||
selectedIconKey={values.field.icon || undefined}
|
||||
onChange={(value) =>
|
||||
onChange({
|
||||
field: { ...values.field, icon: value.iconKey },
|
||||
})
|
||||
}
|
||||
selectedIconKey={value ?? undefined}
|
||||
onChange={({ iconKey }) => onChange(iconKey)}
|
||||
variant="primary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="relation.field.label"
|
||||
control={control}
|
||||
defaultValue={relationFieldMetadataItem?.label}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
disabled={disableFieldEdition}
|
||||
placeholder="Field name"
|
||||
value={values.field.label}
|
||||
onChange={(value) => {
|
||||
if (!value || validateMetadataLabel(value)) {
|
||||
onChange({
|
||||
field: { ...values.field, label: value },
|
||||
});
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { DropResult } from '@hello-pangea/dnd';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
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 { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
import { CardFooter } from '@/ui/layout/card/components/CardFooter';
|
||||
@ -12,18 +16,32 @@ import {
|
||||
MAIN_COLOR_NAMES,
|
||||
ThemeColor,
|
||||
} from '@/ui/theme/constants/MainColorNames';
|
||||
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
|
||||
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
|
||||
|
||||
import { SettingsObjectFieldSelectFormOptionRow } from './SettingsObjectFieldSelectFormOptionRow';
|
||||
|
||||
export type SettingsObjectFieldSelectFormValues =
|
||||
SettingsObjectFieldSelectFormOption[];
|
||||
// TODO: rename to SettingsDataModelFieldSelectForm and move to settings/data-model/fields/forms/components
|
||||
|
||||
type SettingsObjectFieldSelectFormProps = {
|
||||
onChange: (values: SettingsObjectFieldSelectFormValues) => void;
|
||||
values: SettingsObjectFieldSelectFormValues;
|
||||
export const settingsDataModelFieldSelectFormSchema = z.object({
|
||||
options: z
|
||||
.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;
|
||||
};
|
||||
|
||||
@ -58,12 +76,55 @@ const getNextColor = (currentColor: ThemeColor) => {
|
||||
return MAIN_COLOR_NAMES[nextColorIndex];
|
||||
};
|
||||
|
||||
export const SettingsObjectFieldSelectForm = ({
|
||||
onChange,
|
||||
values,
|
||||
const getDefaultValueOptionIndexes = (
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue' | 'options'>,
|
||||
) =>
|
||||
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,
|
||||
}: SettingsObjectFieldSelectFormProps) => {
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
}: SettingsDataModelFieldSelectFormProps) => {
|
||||
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;
|
||||
|
||||
const nextOptions = moveArrayItem(values, {
|
||||
@ -74,27 +135,7 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
onChange(nextOptions);
|
||||
};
|
||||
|
||||
const handleDefaultValueChange = (
|
||||
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 = () => {
|
||||
const findNewLabel = (values: SettingsObjectFieldSelectFormOption[]) => {
|
||||
let optionIndex = values.length + 1;
|
||||
while (optionIndex < 100) {
|
||||
const newLabel = `Option ${optionIndex}`;
|
||||
@ -107,35 +148,43 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="options"
|
||||
control={control}
|
||||
defaultValue={initialValue?.length ? initialValue : [DEFAULT_OPTION]}
|
||||
render={({ field: { onChange, value: options } }) => (
|
||||
<>
|
||||
<StyledContainer>
|
||||
<StyledLabel>Options</StyledLabel>
|
||||
<DraggableList
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragEnd={(result) => handleDragEnd(options, result, onChange)}
|
||||
draggableItems={
|
||||
<>
|
||||
{values.map((option, index) => (
|
||||
{options.map((option, index) => (
|
||||
<DraggableItem
|
||||
key={option.value}
|
||||
draggableId={option.value}
|
||||
index={index}
|
||||
isDragDisabled={values.length === 1}
|
||||
isDragDisabled={options.length === 1}
|
||||
itemComponent={
|
||||
<SettingsObjectFieldSelectFormOptionRow
|
||||
key={option.value}
|
||||
isDefault={option.isDefault}
|
||||
onChange={(nextOption) => {
|
||||
handleDefaultValueChange(
|
||||
index,
|
||||
option,
|
||||
nextOption,
|
||||
!isMultiSelect,
|
||||
const nextOptions =
|
||||
isMultiSelect || !nextOption.isDefault
|
||||
? [...options]
|
||||
: // Reset simple Select default option before setting the new one
|
||||
options.map<SettingsObjectFieldSelectFormOption>(
|
||||
(value) => ({ ...value, isDefault: false }),
|
||||
);
|
||||
nextOptions.splice(index, 1, nextOption);
|
||||
onChange(nextOptions);
|
||||
}}
|
||||
onRemove={
|
||||
values.length > 1
|
||||
options.length > 1
|
||||
? () => {
|
||||
const nextOptions = [...values];
|
||||
const nextOptions = [...options];
|
||||
nextOptions.splice(index, 1);
|
||||
onChange(nextOptions);
|
||||
}
|
||||
@ -156,10 +205,10 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
Icon={IconPlus}
|
||||
onClick={() =>
|
||||
onChange([
|
||||
...values,
|
||||
...options,
|
||||
{
|
||||
color: getNextColor(values[values.length - 1].color),
|
||||
label: findNewLabel(),
|
||||
color: getNextColor(options[options.length - 1].color),
|
||||
label: findNewLabel(options),
|
||||
value: v4(),
|
||||
},
|
||||
])
|
||||
@ -167,5 +216,7 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
/>
|
||||
</StyledFooter>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,20 +1,25 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
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 {
|
||||
SettingsObjectFieldCurrencyForm,
|
||||
SettingsObjectFieldCurrencyFormValues,
|
||||
SettingsDataModelFieldCurrencyForm,
|
||||
settingsDataModelFieldCurrencyFormSchema,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
|
||||
import {
|
||||
SettingsObjectFieldRelationForm,
|
||||
SettingsObjectFieldRelationFormValues,
|
||||
SettingsDataModelFieldRelationForm,
|
||||
settingsDataModelFieldRelationFormSchema,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldRelationForm';
|
||||
import {
|
||||
SettingsObjectFieldSelectForm,
|
||||
SettingsObjectFieldSelectFormValues,
|
||||
SettingsDataModelFieldSelectForm,
|
||||
settingsDataModelFieldSelectFormSchema,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||
import {
|
||||
@ -23,26 +28,44 @@ import {
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type SettingsDataModelFieldSettingsFormValues = {
|
||||
currency: SettingsObjectFieldCurrencyFormValues;
|
||||
relation: SettingsObjectFieldRelationFormValues;
|
||||
select: SettingsObjectFieldSelectFormValues;
|
||||
multiSelect: SettingsObjectFieldSelectFormValues;
|
||||
defaultValue: any;
|
||||
};
|
||||
const booleanFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Boolean) })
|
||||
.merge(settingsDataModelFieldBooleanFormSchema);
|
||||
|
||||
const currencyFieldFormSchema = z
|
||||
.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 = {
|
||||
disableCurrencyForm?: boolean;
|
||||
onChange: (values: Partial<SettingsDataModelFieldSettingsFormValues>) => void;
|
||||
relationFieldMetadataItem?: Pick<
|
||||
FieldMetadataItem,
|
||||
'id' | 'isCustom' | 'name'
|
||||
>;
|
||||
values: SettingsDataModelFieldSettingsFormValues;
|
||||
} & Pick<
|
||||
SettingsDataModelFieldPreviewCardProps,
|
||||
'fieldMetadataItem' | 'objectMetadataItem'
|
||||
>;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||
relationFieldMetadataItem?: FieldMetadataItem;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
@ -81,18 +104,23 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
disableCurrencyForm,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
onChange,
|
||||
relationFieldMetadataItem,
|
||||
values,
|
||||
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldSettingsFormValues>();
|
||||
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
||||
|
||||
if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
|
||||
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
values.relation.objectMetadataId,
|
||||
);
|
||||
const relationTypeConfig = RELATION_TYPES[values.relation.type];
|
||||
const relationObjectMetadataId = watchFormValue('relation.objectMetadataId');
|
||||
const relationObjectMetadataItem = relationObjectMetadataId
|
||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
const relationType = watchFormValue('relation.type');
|
||||
const relationTypeConfig = relationType
|
||||
? RELATION_TYPES[relationType]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
@ -103,14 +131,11 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
shrink={fieldMetadataItem.type === FieldMetadataType.Relation}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
selectOptions={
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
||||
? values.multiSelect
|
||||
: values.select
|
||||
}
|
||||
selectOptions={watchFormValue('options')}
|
||||
/>
|
||||
{fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
!!relationObjectMetadataItem && (
|
||||
!!relationObjectMetadataItem &&
|
||||
!!relationTypeConfig && (
|
||||
<>
|
||||
<StyledRelationImage
|
||||
src={relationTypeConfig.imageSrc}
|
||||
@ -119,11 +144,11 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
/>
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={{
|
||||
icon: values.relation.field.icon,
|
||||
label: values.relation.field.label || 'Field name',
|
||||
...relationFieldMetadataItem,
|
||||
icon: watchFormValue('relation.field.icon'),
|
||||
label:
|
||||
watchFormValue('relation.field.label') || 'Field name',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: relationFieldMetadataItem?.name,
|
||||
id: relationFieldMetadataItem?.id,
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={relationObjectMetadataItem}
|
||||
@ -134,49 +159,25 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
</StyledPreviewContent>
|
||||
}
|
||||
form={
|
||||
fieldMetadataItem.type === FieldMetadataType.Currency ? (
|
||||
<SettingsObjectFieldCurrencyForm
|
||||
fieldMetadataItem.type === FieldMetadataType.Boolean ? (
|
||||
<SettingsDataModelFieldBooleanForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Currency ? (
|
||||
<SettingsDataModelFieldCurrencyForm
|
||||
disabled={disableCurrencyForm}
|
||||
values={values.currency}
|
||||
onChange={(nextCurrencyValues) =>
|
||||
onChange({
|
||||
currency: { ...values.currency, ...nextCurrencyValues },
|
||||
})
|
||||
}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Relation ? (
|
||||
<SettingsObjectFieldRelationForm
|
||||
disableFieldEdition={
|
||||
relationFieldMetadataItem && !relationFieldMetadataItem.isCustom
|
||||
}
|
||||
disableRelationEdition={!!relationFieldMetadataItem}
|
||||
values={values.relation}
|
||||
onChange={(nextRelationValues) =>
|
||||
onChange({
|
||||
relation: { ...values.relation, ...nextRelationValues },
|
||||
})
|
||||
}
|
||||
<SettingsDataModelFieldRelationForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Select ? (
|
||||
<SettingsObjectFieldSelectForm
|
||||
values={values.select}
|
||||
onChange={(nextSelectValues) =>
|
||||
onChange({ select: nextSelectValues })
|
||||
}
|
||||
/>
|
||||
) : 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 })
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Select ||
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
|
||||
<SettingsDataModelFieldSelectForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
isMultiSelect={
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
||||
}
|
||||
/>
|
||||
) : undefined
|
||||
|
||||
@ -1,38 +1,47 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import omit from 'lodash.omit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import {
|
||||
SETTINGS_FIELD_TYPE_CONFIGS,
|
||||
SettingsFieldTypeConfig,
|
||||
} from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
||||
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
||||
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 = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
excludedFieldTypes?: SettingsSupportedFieldType[];
|
||||
onChange?: ({
|
||||
type,
|
||||
defaultValue,
|
||||
}: {
|
||||
type: SettingsSupportedFieldType;
|
||||
defaultValue: any;
|
||||
}) => void;
|
||||
value?: SettingsSupportedFieldType;
|
||||
fieldMetadataItem?: FieldMetadataItem;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldTypeSelect = ({
|
||||
className,
|
||||
disabled,
|
||||
excludedFieldTypes = [],
|
||||
onChange,
|
||||
value,
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldTypeSelectProps) => {
|
||||
const fieldTypeConfigs = omit(
|
||||
SETTINGS_FIELD_TYPE_CONFIGS,
|
||||
excludedFieldTypes,
|
||||
);
|
||||
const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>();
|
||||
|
||||
const fieldTypeConfigs: Partial<
|
||||
Record<SettingsSupportedFieldType, SettingsFieldTypeConfig>
|
||||
> = omit(SETTINGS_FIELD_TYPE_CONFIGS, excludedFieldTypes);
|
||||
|
||||
const fieldTypeOptions = Object.entries<SettingsFieldTypeConfig>(
|
||||
fieldTypeConfigs,
|
||||
).map<SelectOption<SettingsSupportedFieldType>>(([key, dataTypeConfig]) => ({
|
||||
@ -42,19 +51,25 @@ export const SettingsDataModelFieldTypeSelect = ({
|
||||
}));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
defaultValue={
|
||||
fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs
|
||||
? (fieldMetadataItem.type as SettingsSupportedFieldType)
|
||||
: undefined
|
||||
}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
className={className}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-type-select"
|
||||
value={value}
|
||||
onChange={(value) =>
|
||||
onChange?.({
|
||||
type: value,
|
||||
defaultValue: value === FieldMetadataType.Boolean ? false : undefined,
|
||||
})
|
||||
}
|
||||
onChange={onChange}
|
||||
options={fieldTypeOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { fieldMetadataFormDefaultValues } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
@ -22,14 +18,6 @@ const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ 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> = {
|
||||
title:
|
||||
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldSettingsFormCard',
|
||||
@ -39,12 +27,11 @@ const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = {
|
||||
ComponentDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
FormProviderDecorator,
|
||||
],
|
||||
args: {
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
onChange: fn(),
|
||||
values: defaultValues,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
@ -57,24 +44,14 @@ type Story = StoryObj<typeof SettingsDataModelFieldSettingsFormCard>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
const relationFieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!;
|
||||
|
||||
export const WithRelationForm: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'people',
|
||||
),
|
||||
relationFieldMetadataItem,
|
||||
values: {
|
||||
...defaultValues,
|
||||
relation: {
|
||||
field: relationFieldMetadataItem,
|
||||
objectMetadataId: mockedPersonObjectMetadataItem.id,
|
||||
type: RelationMetadataType.OneToMany,
|
||||
},
|
||||
},
|
||||
relationFieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!,
|
||||
},
|
||||
};
|
||||
|
||||
@ -85,34 +62,5 @@ export const WithSelectForm: Story = {
|
||||
icon: 'IconBuildingFactory2',
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@ -12,10 +12,6 @@ const meta: Meta<typeof SettingsDataModelFieldTypeSelect> = {
|
||||
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect',
|
||||
component: SettingsDataModelFieldTypeSelect,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
onChange: fn(),
|
||||
value: FieldMetadataType.Text,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
msw: graphqlMocks,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -8,7 +8,7 @@ import { FieldDisplay } from '@/object-record/record-field/components/FieldDispl
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
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 { 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 { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
|
||||
import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview';
|
||||
@ -24,7 +24,7 @@ export type SettingsDataModelFieldPreviewProps = {
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
|
||||
shrink?: boolean;
|
||||
withFieldLabel?: boolean;
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
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 { getFieldPreviewValueFromRecord } from '@/settings/data-model/utils/getFieldPreviewValueFromRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@ -16,7 +16,7 @@ type UseFieldPreviewParams = {
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
|
||||
};
|
||||
|
||||
export const useFieldPreview = ({
|
||||
|
||||
@ -3,7 +3,6 @@ import styled from '@emotion/styled';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
|
||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
@ -93,11 +92,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
if (!value || validateMetadataLabel(value)) {
|
||||
onChange?.(value);
|
||||
}
|
||||
}}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import {
|
||||
mockedCompanyObjectMetadataItem,
|
||||
mockedOpportunityObjectMetadataItem,
|
||||
@ -16,8 +16,19 @@ describe('getFieldDefaultPreviewValue', () => {
|
||||
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'stage',
|
||||
)!;
|
||||
const selectOptions: SettingsObjectFieldSelectFormValues =
|
||||
fieldMetadataItem.options ?? [];
|
||||
const selectOptions: SettingsDataModelFieldSelectFormValues['options'] = [
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
];
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
@ -27,7 +38,7 @@ describe('getFieldDefaultPreviewValue', () => {
|
||||
});
|
||||
|
||||
// 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', () => {
|
||||
@ -36,8 +47,18 @@ describe('getFieldDefaultPreviewValue', () => {
|
||||
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'stage',
|
||||
)!;
|
||||
const selectOptions: SettingsObjectFieldSelectFormValues =
|
||||
fieldMetadataItem.options ?? [];
|
||||
const selectOptions: SettingsDataModelFieldSelectFormValues['options'] = [
|
||||
{
|
||||
color: 'purple' as const,
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
color: 'pink' as const,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
];
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import {
|
||||
mockedCompanyObjectMetadataItem,
|
||||
mockedOpportunityObjectMetadataItem,
|
||||
@ -20,8 +20,34 @@ describe('getFieldPreviewValueFromRecord', () => {
|
||||
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'stage',
|
||||
)!;
|
||||
const selectOptions: SettingsObjectFieldSelectFormValues =
|
||||
fieldMetadataItem.options ?? [];
|
||||
const selectOptions: SettingsDataModelFieldSelectFormValues['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
|
||||
const result = getFieldPreviewValueFromRecord({
|
||||
@ -44,8 +70,24 @@ describe('getFieldPreviewValueFromRecord', () => {
|
||||
const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'stage',
|
||||
)!;
|
||||
const selectOptions: SettingsObjectFieldSelectFormValues =
|
||||
fieldMetadataItem.options ?? [];
|
||||
const selectOptions: SettingsDataModelFieldSelectFormValues['options'] = [
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
{
|
||||
color: 'turquoise',
|
||||
label: '🌿 Green tech',
|
||||
value: 'GREEN_TECH',
|
||||
},
|
||||
];
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValueFromRecord({
|
||||
|
||||
@ -2,7 +2,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
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 { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -19,7 +19,7 @@ export const getFieldDefaultPreviewValue = ({
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
|
||||
}) => {
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Select &&
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
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';
|
||||
|
||||
export const getFieldPreviewValueFromRecord = ({
|
||||
@ -10,7 +10,7 @@ export const getFieldPreviewValueFromRecord = ({
|
||||
}: {
|
||||
record: ObjectRecord;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'name' | 'type'>;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
|
||||
}) => {
|
||||
const recordFieldValue = record[fieldMetadataItem.name];
|
||||
|
||||
|
||||
@ -4,25 +4,27 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import omit from 'lodash.omit';
|
||||
import pick from 'lodash.pick';
|
||||
import { IconArchive, IconSettings } from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdateOneFieldMetadataItem';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput';
|
||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
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 { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
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 { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
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 { Section } from '@/ui/layout/section/components/Section';
|
||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsDataModelFieldEditFormValues = z.infer<
|
||||
typeof settingsFieldFormSchema
|
||||
@ -66,117 +65,38 @@ export const SettingsObjectFieldEdit = () => {
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
|
||||
const { disableMetadataField, editMetadataField } = useFieldMetadataItem();
|
||||
const { disableMetadataField } = useFieldMetadataItem();
|
||||
const activeMetadataField = activeObjectMetadataItem?.fields.find(
|
||||
(metadataField) =>
|
||||
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
|
||||
);
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const {
|
||||
relationFieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationType,
|
||||
} =
|
||||
const { relationFieldMetadataItem } =
|
||||
useMemo(
|
||||
() =>
|
||||
activeMetadataField
|
||||
? getRelationMetadata({
|
||||
fieldMetadataItem: activeMetadataField,
|
||||
})
|
||||
? getRelationMetadata({ fieldMetadataItem: activeMetadataField })
|
||||
: null,
|
||||
[activeMetadataField, getRelationMetadata],
|
||||
) ?? {};
|
||||
|
||||
const { updateOneFieldMetadataItem } = useUpdateOneFieldMetadataItem();
|
||||
|
||||
const formConfig = useForm<SettingsDataModelFieldEditFormValues>({
|
||||
mode: 'onTouched',
|
||||
resolver: zodResolver(settingsFieldFormSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
formValues,
|
||||
handleFormChange,
|
||||
hasFieldFormChanged,
|
||||
hasDefaultValueChanged,
|
||||
hasFormChanged,
|
||||
hasRelationFormChanged,
|
||||
hasSelectFormChanged,
|
||||
hasMultiSelectFormChanged,
|
||||
initForm,
|
||||
isInitialized,
|
||||
isValid,
|
||||
validatedFormValues,
|
||||
} = useFieldMetadataForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem || !activeMetadataField) {
|
||||
navigate(AppPath.NotFound);
|
||||
return;
|
||||
}
|
||||
}, [activeMetadataField, activeObjectMetadataItem, navigate]);
|
||||
|
||||
const { defaultValue } = activeMetadataField;
|
||||
if (!activeObjectMetadataItem || !activeMetadataField) return null;
|
||||
|
||||
const currencyDefaultValue =
|
||||
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 canSave = formConfig.formState.isValid && formConfig.formState.isDirty;
|
||||
|
||||
const isLabelIdentifier = isLabelIdentifierField({
|
||||
fieldMetadataItem: activeMetadataField,
|
||||
@ -184,43 +104,37 @@ export const SettingsObjectFieldEdit = () => {
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validatedFormValues) return;
|
||||
|
||||
const formValues = formConfig.getValues();
|
||||
const { dirtyFields } = formConfig.formState;
|
||||
|
||||
try {
|
||||
if (
|
||||
validatedFormValues.type === FieldMetadataType.Relation &&
|
||||
formValues.type === FieldMetadataType.Relation &&
|
||||
isNonEmptyString(relationFieldMetadataItem?.id) &&
|
||||
hasRelationFormChanged
|
||||
'relation' in dirtyFields
|
||||
) {
|
||||
await editMetadataField({
|
||||
icon: validatedFormValues.relation.field.icon,
|
||||
id: relationFieldMetadataItem?.id,
|
||||
label: validatedFormValues.relation.field.label,
|
||||
type: validatedFormValues.type,
|
||||
await updateOneFieldMetadataItem({
|
||||
fieldMetadataIdToUpdate: relationFieldMetadataItem.id,
|
||||
updatePayload: formValues.relation.field,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(dirtyFields).length > 0 ||
|
||||
hasFieldFormChanged ||
|
||||
hasSelectFormChanged ||
|
||||
hasMultiSelectFormChanged ||
|
||||
hasDefaultValueChanged
|
||||
) {
|
||||
await editMetadataField({
|
||||
...formValues,
|
||||
id: activeMetadataField.id,
|
||||
defaultValue: validatedFormValues.defaultValue,
|
||||
type: validatedFormValues.type,
|
||||
options:
|
||||
validatedFormValues.type === FieldMetadataType.Select
|
||||
? validatedFormValues.select
|
||||
: validatedFormValues.type === FieldMetadataType.MultiSelect
|
||||
? validatedFormValues.multiSelect
|
||||
: undefined,
|
||||
const otherDirtyFields = omit(dirtyFields, 'relation');
|
||||
|
||||
if (Object.keys(otherDirtyFields).length > 0) {
|
||||
const formattedInput = pick(
|
||||
formatFieldMetadataItemInput(formValues),
|
||||
Object.keys(otherDirtyFields),
|
||||
);
|
||||
|
||||
const options = formattedInput.options?.map((option) => ({
|
||||
...option,
|
||||
id: option.id ?? v4(),
|
||||
}));
|
||||
|
||||
await updateOneFieldMetadataItem({
|
||||
fieldMetadataIdToUpdate: activeMetadataField.id,
|
||||
updatePayload: { ...formattedInput, options },
|
||||
});
|
||||
}
|
||||
|
||||
@ -281,28 +195,13 @@ export const SettingsObjectFieldEdit = () => {
|
||||
/>
|
||||
<StyledSettingsObjectFieldTypeSelect
|
||||
disabled
|
||||
onChange={handleFormChange}
|
||||
value={formValues.type}
|
||||
fieldMetadataItem={activeMetadataField}
|
||||
/>
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
disableCurrencyForm
|
||||
fieldMetadataItem={{
|
||||
icon: formConfig.watch('icon'),
|
||||
id: activeMetadataField.id,
|
||||
label: formConfig.watch('label'),
|
||||
name: activeMetadataField.name,
|
||||
type: formValues.type,
|
||||
}}
|
||||
fieldMetadataItem={activeMetadataField}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
onChange={handleFormChange}
|
||||
relationFieldMetadataItem={relationFieldMetadataItem}
|
||||
values={{
|
||||
currency: formValues.currency,
|
||||
relation: formValues.relation,
|
||||
select: formValues.select,
|
||||
multiSelect: formValues.multiSelect,
|
||||
defaultValue: formValues.defaultValue,
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
{!isLabelIdentifier && (
|
||||
|
||||
@ -22,7 +22,6 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
|
||||
import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
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 { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
@ -51,25 +50,14 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
const { objectSlug = '' } = useParams();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const {
|
||||
findActiveObjectMetadataItemBySlug,
|
||||
findObjectMetadataItemById,
|
||||
findObjectMetadataItemByNamePlural,
|
||||
} = useFilteredObjectMetadataItems();
|
||||
const { findActiveObjectMetadataItemBySlug, findObjectMetadataItemById } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
const { createMetadataField } = useFieldMetadataItem();
|
||||
const cache = useApolloClient().cache;
|
||||
|
||||
const {
|
||||
formValues,
|
||||
handleFormChange,
|
||||
initForm,
|
||||
isValid,
|
||||
validatedFormValues,
|
||||
} = useFieldMetadataForm();
|
||||
|
||||
const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
|
||||
mode: 'onTouched',
|
||||
resolver: zodResolver(settingsFieldFormSchema),
|
||||
@ -78,23 +66,8 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem) {
|
||||
navigate(AppPath.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
initForm({
|
||||
relation: {
|
||||
field: { icon: activeObjectMetadataItem.icon },
|
||||
objectMetadataId:
|
||||
findObjectMetadataItemByNamePlural('people')?.id || '',
|
||||
},
|
||||
});
|
||||
}, [
|
||||
activeObjectMetadataItem,
|
||||
findObjectMetadataItemByNamePlural,
|
||||
initForm,
|
||||
|
||||
navigate,
|
||||
]);
|
||||
}, [activeObjectMetadataItem, navigate]);
|
||||
|
||||
const [objectViews, setObjectViews] = useState<View[]>([]);
|
||||
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
|
||||
@ -116,12 +89,16 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const relationObjectMetadataId = formConfig.watch(
|
||||
'relation.objectMetadataId',
|
||||
);
|
||||
|
||||
useFindManyRecords<View>({
|
||||
objectNameSingular: CoreObjectNameSingular.View,
|
||||
skip: !formValues.relation?.objectMetadataId,
|
||||
skip: !relationObjectMetadataId,
|
||||
filter: {
|
||||
type: { eq: ViewType.Table },
|
||||
objectMetadataId: { eq: formValues.relation?.objectMetadataId },
|
||||
objectMetadataId: { eq: relationObjectMetadataId },
|
||||
},
|
||||
onCompleted: async (views) => {
|
||||
if (isUndefinedOrNull(views)) return;
|
||||
@ -135,37 +112,40 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
|
||||
if (!activeObjectMetadataItem) return null;
|
||||
|
||||
const canSave = formConfig.formState.isValid && isValid;
|
||||
const canSave = formConfig.formState.isValid;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validatedFormValues) return;
|
||||
|
||||
const formValues = formConfig.getValues();
|
||||
|
||||
try {
|
||||
if (validatedFormValues.type === FieldMetadataType.Relation) {
|
||||
if (
|
||||
formValues.type === FieldMetadataType.Relation &&
|
||||
'relation' in formValues
|
||||
) {
|
||||
const { relation: relationFormValues, ...fieldFormValues } = formValues;
|
||||
|
||||
const createdRelation = await createOneRelationMetadata({
|
||||
relationType: validatedFormValues.relation.type,
|
||||
field: pick(formValues, ['icon', 'label', 'description']),
|
||||
relationType: relationFormValues.type,
|
||||
field: pick(fieldFormValues, ['icon', 'label', 'description']),
|
||||
objectMetadataId: activeObjectMetadataItem.id,
|
||||
connect: {
|
||||
field: {
|
||||
icon: validatedFormValues.relation.field.icon,
|
||||
label: validatedFormValues.relation.field.label,
|
||||
icon: relationFormValues.field.icon,
|
||||
label: relationFormValues.field.label,
|
||||
},
|
||||
objectMetadataId: validatedFormValues.relation.objectMetadataId,
|
||||
objectMetadataId: relationFormValues.objectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
validatedFormValues.relation.objectMetadataId,
|
||||
relationFormValues.objectMetadataId,
|
||||
);
|
||||
|
||||
objectViews.map(async (view) => {
|
||||
const viewFieldToCreate = {
|
||||
viewId: view.id,
|
||||
fieldMetadataId:
|
||||
validatedFormValues.relation.type === 'MANY_TO_ONE'
|
||||
relationFormValues.type === 'MANY_TO_ONE'
|
||||
? createdRelation.data?.createOneRelation.toFieldMetadataId
|
||||
: createdRelation.data?.createOneRelation.fromFieldMetadataId,
|
||||
position: activeObjectMetadataItem.fields.length,
|
||||
@ -198,7 +178,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
const viewFieldToCreate = {
|
||||
viewId: view.id,
|
||||
fieldMetadataId:
|
||||
validatedFormValues.relation.type === 'MANY_TO_ONE'
|
||||
relationFormValues.type === 'MANY_TO_ONE'
|
||||
? createdRelation.data?.createOneRelation.fromFieldMetadataId
|
||||
: createdRelation.data?.createOneRelation.toFieldMetadataId,
|
||||
position: relationObjectMetadataItem?.fields.length,
|
||||
@ -229,22 +209,17 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
});
|
||||
} else {
|
||||
const createdMetadataField = await createMetadataField({
|
||||
defaultValue:
|
||||
validatedFormValues.type === FieldMetadataType.Currency
|
||||
? {
|
||||
amountMicros: null,
|
||||
currencyCode: validatedFormValues.currency.currencyCode,
|
||||
}
|
||||
: validatedFormValues.defaultValue,
|
||||
...formValues,
|
||||
objectMetadataId: activeObjectMetadataItem.id,
|
||||
type: validatedFormValues.type,
|
||||
options:
|
||||
validatedFormValues.type === FieldMetadataType.Select
|
||||
? validatedFormValues.select
|
||||
: validatedFormValues.type === FieldMetadataType.MultiSelect
|
||||
? validatedFormValues.multiSelect
|
||||
defaultValue:
|
||||
formValues.type === FieldMetadataType.Currency
|
||||
? {
|
||||
...formValues.defaultValue,
|
||||
amountMicros: null,
|
||||
}
|
||||
: 'defaultValue' in formValues
|
||||
? formValues.defaultValue
|
||||
: undefined,
|
||||
objectMetadataId: activeObjectMetadataItem.id,
|
||||
});
|
||||
|
||||
objectViews.map(async (view) => {
|
||||
@ -335,24 +310,14 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
/>
|
||||
<StyledSettingsObjectFieldTypeSelect
|
||||
excludedFieldTypes={excludedFieldTypes}
|
||||
onChange={handleFormChange}
|
||||
value={formValues.type}
|
||||
/>
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
fieldMetadataItem={{
|
||||
icon: formConfig.watch('icon'),
|
||||
label: formConfig.watch('label') || 'Employees',
|
||||
type: formValues.type,
|
||||
type: formConfig.watch('type'),
|
||||
}}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
onChange={handleFormChange}
|
||||
values={{
|
||||
currency: formValues.currency,
|
||||
relation: formValues.relation,
|
||||
select: formValues.select,
|
||||
multiSelect: formValues.multiSelect,
|
||||
defaultValue: formValues.defaultValue,
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
|
||||
Reference in New Issue
Block a user