refactor: use react-hook-form to validate Settings/DataModel/Field (#4916)
Closes #4295
This commit is contained in:
@ -124,7 +124,8 @@
|
|||||||
"commands": [
|
"commands": [
|
||||||
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'"
|
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'"
|
||||||
],
|
],
|
||||||
"port": 6006
|
"port": 6006,
|
||||||
|
"env": { "NODE_OPTIONS": "--max-old-space-size=5000" }
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"docs": { "scope": "ui-docs" },
|
"docs": { "scope": "ui-docs" },
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import { act, renderHook } from '@testing-library/react';
|
|||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { RelationMetadataType } from '~/generated/graphql';
|
||||||
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
query,
|
query,
|
||||||
@ -46,7 +45,6 @@ describe('useCreateOneRelationMetadataItem', () => {
|
|||||||
relationType: RelationMetadataType.OneToOne,
|
relationType: RelationMetadataType.OneToOne,
|
||||||
field: {
|
field: {
|
||||||
label: 'label',
|
label: 'label',
|
||||||
type: FieldMetadataType.Relation,
|
|
||||||
},
|
},
|
||||||
objectMetadataId: 'objectMetadataId',
|
objectMetadataId: 'objectMetadataId',
|
||||||
connect: {
|
connect: {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { formatFieldMetadataItemInput } from './formatFieldMetadataItemInput';
|
|||||||
|
|
||||||
export type FormatRelationMetadataInputParams = {
|
export type FormatRelationMetadataInputParams = {
|
||||||
relationType: RelationType;
|
relationType: RelationType;
|
||||||
field: Pick<Field, 'label' | 'icon' | 'description' | 'type'>;
|
field: Pick<Field, 'label' | 'icon' | 'description'>;
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
connect: {
|
connect: {
|
||||||
field: Pick<Field, 'label' | 'icon'>;
|
field: Pick<Field, 'label' | 'icon'>;
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
import { SafeParseSuccess } from 'zod';
|
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata';
|
import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||||
|
|
||||||
import { objectMetadataItemSchema } from '../objectMetadataItemSchema';
|
import { objectMetadataItemSchema } from '../objectMetadataItemSchema';
|
||||||
@ -11,13 +8,10 @@ describe('objectMetadataItemSchema', () => {
|
|||||||
const validObjectMetadataItem = mockedCompanyObjectMetadataItem;
|
const validObjectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const result = objectMetadataItemSchema.safeParse(validObjectMetadataItem);
|
const result = objectMetadataItemSchema.parse(validObjectMetadataItem);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(result.success).toBe(true);
|
expect(result).toEqual(validObjectMetadataItem);
|
||||||
expect((result as SafeParseSuccess<ObjectMetadataItem>).data).toEqual(
|
|
||||||
validObjectMetadataItem,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails for an invalid object metadata item', () => {
|
it('fails for an invalid object metadata item', () => {
|
||||||
|
|||||||
@ -1,6 +1,104 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
|
||||||
|
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
|
||||||
|
import {
|
||||||
|
FieldMetadataType,
|
||||||
|
RelationDefinitionType,
|
||||||
|
RelationMetadataType,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
|
||||||
|
|
||||||
// TODO: implement fieldMetadataItemSchema
|
export const fieldMetadataItemSchema = z.object({
|
||||||
export const fieldMetadataItemSchema: z.ZodType<FieldMetadataItem> = z.any();
|
__typename: z.literal('field').optional(),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
defaultValue: z.any().optional(),
|
||||||
|
description: z.string().trim().nullable().optional(),
|
||||||
|
fromRelationMetadata: z
|
||||||
|
.object({
|
||||||
|
__typename: z.literal('relation').optional(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
relationType: z.nativeEnum(RelationMetadataType),
|
||||||
|
toFieldMetadataId: z.string().uuid(),
|
||||||
|
toObjectMetadata: z.object({
|
||||||
|
__typename: z.literal('object').optional(),
|
||||||
|
dataSourceId: z.string().uuid(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
isRemote: z.boolean(),
|
||||||
|
isSystem: z.boolean(),
|
||||||
|
namePlural: z.string().trim().min(1),
|
||||||
|
nameSingular: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
icon: z.string().startsWith('Icon').trim().nullable(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
isCustom: z.boolean(),
|
||||||
|
isNullable: z.boolean(),
|
||||||
|
isSystem: z.boolean(),
|
||||||
|
label: metadataLabelSchema,
|
||||||
|
name: camelCaseStringSchema,
|
||||||
|
options: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
color: themeColorSchema,
|
||||||
|
id: z.string().uuid(),
|
||||||
|
label: z.string().trim().min(1),
|
||||||
|
position: z.number(),
|
||||||
|
value: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
relationDefinition: z
|
||||||
|
.object({
|
||||||
|
__typename: z.literal('RelationDefinition').optional(),
|
||||||
|
direction: z.nativeEnum(RelationDefinitionType),
|
||||||
|
sourceFieldMetadata: z.object({
|
||||||
|
__typename: z.literal('field').optional(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
sourceObjectMetadata: z.object({
|
||||||
|
__typename: z.literal('object').optional(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
namePlural: z.string().trim().min(1),
|
||||||
|
nameSingular: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
targetFieldMetadata: z.object({
|
||||||
|
__typename: z.literal('field').optional(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
targetObjectMetadata: z.object({
|
||||||
|
__typename: z.literal('object').optional(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
namePlural: z.string().trim().min(1),
|
||||||
|
nameSingular: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
toRelationMetadata: z
|
||||||
|
.object({
|
||||||
|
__typename: z.literal('relation').optional(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
relationType: z.nativeEnum(RelationMetadataType),
|
||||||
|
fromFieldMetadataId: z.string().uuid(),
|
||||||
|
fromObjectMetadata: z.object({
|
||||||
|
__typename: z.literal('object').optional(),
|
||||||
|
id: z.string().uuid(),
|
||||||
|
dataSourceId: z.string().uuid(),
|
||||||
|
isRemote: z.boolean(),
|
||||||
|
isSystem: z.boolean(),
|
||||||
|
namePlural: z.string().trim().min(1),
|
||||||
|
nameSingular: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
type: z.nativeEnum(FieldMetadataType),
|
||||||
|
updatedAt: z.string().datetime(),
|
||||||
|
}) satisfies z.ZodType<FieldMetadataItem>;
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const metadataLabelSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.regex(/^[a-zA-Z][a-zA-Z0-9 ()]*$/);
|
||||||
@ -2,6 +2,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||||
|
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
|
||||||
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
|
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
|
||||||
|
|
||||||
export const objectMetadataItemSchema = z.object({
|
export const objectMetadataItemSchema = z.object({
|
||||||
@ -18,8 +19,8 @@ export const objectMetadataItemSchema = z.object({
|
|||||||
isRemote: z.boolean(),
|
isRemote: z.boolean(),
|
||||||
isSystem: z.boolean(),
|
isSystem: z.boolean(),
|
||||||
labelIdentifierFieldMetadataId: z.string().uuid().nullable(),
|
labelIdentifierFieldMetadataId: z.string().uuid().nullable(),
|
||||||
labelPlural: z.string().trim().min(1),
|
labelPlural: metadataLabelSchema,
|
||||||
labelSingular: z.string().trim().min(1),
|
labelSingular: metadataLabelSchema,
|
||||||
namePlural: camelCaseStringSchema,
|
namePlural: camelCaseStringSchema,
|
||||||
nameSingular: camelCaseStringSchema,
|
nameSingular: camelCaseStringSchema,
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.string().datetime(),
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
|
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
|
||||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
|
||||||
import { TextArea } from '@/ui/input/components/TextArea';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
|
||||||
|
|
||||||
type SettingsObjectFieldFormSectionProps = {
|
|
||||||
disabled?: boolean;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
iconKey?: string;
|
|
||||||
onChange?: (
|
|
||||||
formValues: Partial<{
|
|
||||||
icon: string;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
}>,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledInputsContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsObjectFieldFormSection = ({
|
|
||||||
disabled,
|
|
||||||
name = '',
|
|
||||||
description = '',
|
|
||||||
iconKey = 'IconUsers',
|
|
||||||
onChange,
|
|
||||||
}: SettingsObjectFieldFormSectionProps) => (
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title="Name and description"
|
|
||||||
description="The name and description of this field"
|
|
||||||
/>
|
|
||||||
<StyledInputsContainer>
|
|
||||||
<IconPicker
|
|
||||||
disabled={disabled}
|
|
||||||
selectedIconKey={iconKey}
|
|
||||||
onChange={(value) => onChange?.({ icon: value.iconKey })}
|
|
||||||
variant="primary"
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
placeholder="Employees"
|
|
||||||
value={name}
|
|
||||||
onChange={(value) => {
|
|
||||||
if (!value || validateMetadataLabel(value)) {
|
|
||||||
onChange?.({ label: value });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</StyledInputsContainer>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Write a description"
|
|
||||||
minRows={4}
|
|
||||||
value={description}
|
|
||||||
onChange={(value) => onChange?.({ description: value })}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsObjectFieldFormSection } from '../SettingsObjectFieldFormSection';
|
|
||||||
|
|
||||||
const meta: Meta<typeof SettingsObjectFieldFormSection> = {
|
|
||||||
title: 'Modules/Settings/DataModel/SettingsObjectFieldFormSection',
|
|
||||||
component: SettingsObjectFieldFormSection,
|
|
||||||
decorators: [ComponentDecorator],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof SettingsObjectFieldFormSection>;
|
|
||||||
|
|
||||||
export const Default: Story = {};
|
|
||||||
|
|
||||||
export const WithDefaultValues: Story = {
|
|
||||||
args: {
|
|
||||||
iconKey: 'IconLink',
|
|
||||||
name: 'URL',
|
|
||||||
description: 'Lorem ipsum',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||||
|
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||||
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
|
||||||
|
export const settingsDataModelFieldAboutFormSchema =
|
||||||
|
fieldMetadataItemSchema.pick({
|
||||||
|
description: true,
|
||||||
|
icon: true,
|
||||||
|
label: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
type SettingsDataModelFieldAboutFormValues = z.infer<
|
||||||
|
typeof settingsDataModelFieldAboutFormSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
type SettingsDataModelFieldAboutFormProps = {
|
||||||
|
disabled?: boolean;
|
||||||
|
fieldMetadataItem?: FieldMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledInputsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsDataModelFieldAboutForm = ({
|
||||||
|
disabled,
|
||||||
|
fieldMetadataItem,
|
||||||
|
}: SettingsDataModelFieldAboutFormProps) => {
|
||||||
|
const { control } = useFormContext<SettingsDataModelFieldAboutFormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="icon"
|
||||||
|
control={control}
|
||||||
|
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<IconPicker
|
||||||
|
disabled={disabled}
|
||||||
|
selectedIconKey={value ?? ''}
|
||||||
|
onChange={({ iconKey }) => onChange(iconKey)}
|
||||||
|
variant="primary"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="label"
|
||||||
|
control={control}
|
||||||
|
defaultValue={fieldMetadataItem?.label}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<TextInput
|
||||||
|
placeholder="Employees"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
<Controller
|
||||||
|
name="description"
|
||||||
|
control={control}
|
||||||
|
defaultValue={fieldMetadataItem?.description}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<TextArea
|
||||||
|
placeholder="Write a description"
|
||||||
|
minRows={4}
|
||||||
|
value={value ?? undefined}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
|
||||||
|
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
|
||||||
|
import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||||
|
|
||||||
|
import { SettingsDataModelFieldAboutForm } from '../SettingsDataModelFieldAboutForm';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const meta: Meta<typeof SettingsDataModelFieldAboutForm> = {
|
||||||
|
title: 'Modules/Settings/DataModel/SettingsDataModelFieldAboutForm',
|
||||||
|
component: SettingsDataModelFieldAboutForm,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<StyledContainer>
|
||||||
|
<Story />
|
||||||
|
</StyledContainer>
|
||||||
|
),
|
||||||
|
FormProviderDecorator,
|
||||||
|
IconsProviderDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof SettingsDataModelFieldAboutForm>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const WithDefaultValues: Story = {
|
||||||
|
args: {
|
||||||
|
fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||||
|
({ name }) => name === 'name',
|
||||||
|
)!,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
|
||||||
import { useFieldMetadataForm } from '../useFieldMetadataForm';
|
import { useFieldMetadataForm } from '../useFieldMetadataForm';
|
||||||
|
|
||||||
describe('useFieldMetadataForm', () => {
|
describe('useFieldMetadataForm', () => {
|
||||||
@ -14,8 +16,6 @@ describe('useFieldMetadataForm', () => {
|
|||||||
|
|
||||||
expect(result.current.isInitialized).toBe(true);
|
expect(result.current.isInitialized).toBe(true);
|
||||||
expect(result.current.formValues).toEqual({
|
expect(result.current.formValues).toEqual({
|
||||||
icon: 'IconUsers',
|
|
||||||
label: '',
|
|
||||||
type: 'TEXT',
|
type: 'TEXT',
|
||||||
currency: { currencyCode: 'USD' },
|
currency: { currencyCode: 'USD' },
|
||||||
relation: {
|
relation: {
|
||||||
@ -45,7 +45,7 @@ describe('useFieldMetadataForm', () => {
|
|||||||
expect(result.current.hasSelectFormChanged).toBe(false);
|
expect(result.current.hasSelectFormChanged).toBe(false);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.handleFormChange({ label: 'New Label' });
|
result.current.handleFormChange({ type: FieldMetadataType.Number });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.hasFieldFormChanged).toBe(true);
|
expect(result.current.hasFieldFormChanged).toBe(true);
|
||||||
|
|||||||
@ -15,16 +15,11 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
|||||||
import { SettingsDataModelFieldSettingsFormValues } from '../components/SettingsDataModelFieldSettingsFormCard';
|
import { SettingsDataModelFieldSettingsFormValues } from '../components/SettingsDataModelFieldSettingsFormCard';
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
description?: string;
|
|
||||||
icon: string;
|
|
||||||
label: string;
|
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
type: SettingsSupportedFieldType;
|
type: SettingsSupportedFieldType;
|
||||||
} & SettingsDataModelFieldSettingsFormValues;
|
} & SettingsDataModelFieldSettingsFormValues;
|
||||||
|
|
||||||
export const fieldMetadataFormDefaultValues: FormValues = {
|
export const fieldMetadataFormDefaultValues: FormValues = {
|
||||||
icon: 'IconUsers',
|
|
||||||
label: '',
|
|
||||||
type: FieldMetadataType.Text,
|
type: FieldMetadataType.Text,
|
||||||
currency: { currencyCode: CurrencyCode.USD },
|
currency: { currencyCode: CurrencyCode.USD },
|
||||||
relation: {
|
relation: {
|
||||||
@ -43,9 +38,6 @@ const relationTargetFieldSchema = z.object({
|
|||||||
defaultValue: z.any(),
|
defaultValue: z.any(),
|
||||||
});
|
});
|
||||||
const fieldSchema = z.object({
|
const fieldSchema = z.object({
|
||||||
description: z.string().optional(),
|
|
||||||
icon: z.string().startsWith('Icon'),
|
|
||||||
label: z.string().min(1),
|
|
||||||
defaultValue: z.any(),
|
defaultValue: z.any(),
|
||||||
type: z.enum(
|
type: z.enum(
|
||||||
Object.values(FieldMetadataType) as [
|
Object.values(FieldMetadataType) as [
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { settingsDataModelFieldAboutFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||||
|
|
||||||
|
export const settingsFieldFormSchema = settingsDataModelFieldAboutFormSchema;
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { IconArchive, IconSettings } from 'twenty-ui';
|
import { IconArchive, IconSettings } from 'twenty-ui';
|
||||||
|
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';
|
||||||
@ -14,10 +17,11 @@ import { SaveAndCancelButtons } from '@/settings/components/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 { SettingsObjectFieldCurrencyFormValues } from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
|
||||||
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
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 { 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 { 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';
|
||||||
@ -31,6 +35,10 @@ import {
|
|||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
type SettingsDataModelFieldEditFormValues = z.infer<
|
||||||
|
typeof settingsFieldFormSchema
|
||||||
|
>;
|
||||||
|
|
||||||
const StyledSettingsObjectFieldTypeSelect = styled(
|
const StyledSettingsObjectFieldTypeSelect = styled(
|
||||||
SettingsDataModelFieldTypeSelect,
|
SettingsDataModelFieldTypeSelect,
|
||||||
)`
|
)`
|
||||||
@ -80,6 +88,11 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
[activeMetadataField, getRelationMetadata],
|
[activeMetadataField, getRelationMetadata],
|
||||||
) ?? {};
|
) ?? {};
|
||||||
|
|
||||||
|
const formConfig = useForm<SettingsDataModelFieldEditFormValues>({
|
||||||
|
mode: 'onTouched',
|
||||||
|
resolver: zodResolver(settingsFieldFormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formValues,
|
formValues,
|
||||||
handleFormChange,
|
handleFormChange,
|
||||||
@ -130,9 +143,6 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
if (!isFieldTypeSupported) return;
|
if (!isFieldTypeSupported) return;
|
||||||
|
|
||||||
initForm({
|
initForm({
|
||||||
icon: activeMetadataField.icon ?? undefined,
|
|
||||||
label: activeMetadataField.label,
|
|
||||||
description: activeMetadataField.description ?? undefined,
|
|
||||||
type: fieldType,
|
type: fieldType,
|
||||||
...(currencyDefaultValue ? { currency: currencyDefaultValue } : {}),
|
...(currencyDefaultValue ? { currency: currencyDefaultValue } : {}),
|
||||||
relation: {
|
relation: {
|
||||||
@ -163,7 +173,10 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
if (!isInitialized || !activeObjectMetadataItem || !activeMetadataField)
|
if (!isInitialized || !activeObjectMetadataItem || !activeMetadataField)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const canSave = isValid && hasFormChanged;
|
const canSave =
|
||||||
|
formConfig.formState.isValid &&
|
||||||
|
isValid &&
|
||||||
|
(formConfig.formState.isDirty || hasFormChanged);
|
||||||
|
|
||||||
const isLabelIdentifier = isLabelIdentifierField({
|
const isLabelIdentifier = isLabelIdentifierField({
|
||||||
fieldMetadataItem: activeMetadataField,
|
fieldMetadataItem: activeMetadataField,
|
||||||
@ -173,6 +186,9 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!validatedFormValues) return;
|
if (!validatedFormValues) return;
|
||||||
|
|
||||||
|
const formValues = formConfig.getValues();
|
||||||
|
const { dirtyFields } = formConfig.formState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
validatedFormValues.type === FieldMetadataType.Relation &&
|
validatedFormValues.type === FieldMetadataType.Relation &&
|
||||||
@ -186,17 +202,17 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
type: validatedFormValues.type,
|
type: validatedFormValues.type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
Object.keys(dirtyFields).length > 0 ||
|
||||||
hasFieldFormChanged ||
|
hasFieldFormChanged ||
|
||||||
hasSelectFormChanged ||
|
hasSelectFormChanged ||
|
||||||
hasMultiSelectFormChanged ||
|
hasMultiSelectFormChanged ||
|
||||||
hasDefaultValueChanged
|
hasDefaultValueChanged
|
||||||
) {
|
) {
|
||||||
await editMetadataField({
|
await editMetadataField({
|
||||||
description: validatedFormValues.description,
|
...formValues,
|
||||||
icon: validatedFormValues.icon,
|
|
||||||
id: activeMetadataField.id,
|
id: activeMetadataField.id,
|
||||||
label: validatedFormValues.label,
|
|
||||||
defaultValue: validatedFormValues.defaultValue,
|
defaultValue: validatedFormValues.defaultValue,
|
||||||
type: validatedFormValues.type,
|
type: validatedFormValues.type,
|
||||||
options:
|
options:
|
||||||
@ -225,77 +241,83 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
canPersistFieldMetadataItemUpdate(activeMetadataField);
|
canPersistFieldMetadataItemUpdate(activeMetadataField);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<SettingsPageContainer>
|
<FormProvider {...formConfig}>
|
||||||
<SettingsHeaderContainer>
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
<Breadcrumb
|
<SettingsPageContainer>
|
||||||
links={[
|
<SettingsHeaderContainer>
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
<Breadcrumb
|
||||||
{
|
links={[
|
||||||
children: activeObjectMetadataItem.labelPlural,
|
{ children: 'Objects', href: '/settings/objects' },
|
||||||
href: `/settings/objects/${objectSlug}`,
|
{
|
||||||
},
|
children: activeObjectMetadataItem.labelPlural,
|
||||||
{ children: activeMetadataField.label },
|
href: `/settings/objects/${objectSlug}`,
|
||||||
]}
|
},
|
||||||
/>
|
{ children: activeMetadataField.label },
|
||||||
{shouldDisplaySaveAndCancel && (
|
]}
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{shouldDisplaySaveAndCancel && (
|
||||||
</SettingsHeaderContainer>
|
<SaveAndCancelButtons
|
||||||
<SettingsObjectFieldFormSection
|
isSaveDisabled={!canSave}
|
||||||
disabled={!activeMetadataField.isCustom}
|
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||||
name={formValues.label}
|
onSave={handleSave}
|
||||||
description={formValues.description}
|
/>
|
||||||
iconKey={formValues.icon}
|
)}
|
||||||
onChange={handleFormChange}
|
</SettingsHeaderContainer>
|
||||||
/>
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title="Type and values"
|
|
||||||
description="The field's type and values."
|
|
||||||
/>
|
|
||||||
<StyledSettingsObjectFieldTypeSelect
|
|
||||||
disabled
|
|
||||||
onChange={handleFormChange}
|
|
||||||
value={formValues.type}
|
|
||||||
/>
|
|
||||||
<SettingsDataModelFieldSettingsFormCard
|
|
||||||
disableCurrencyForm
|
|
||||||
fieldMetadataItem={{
|
|
||||||
icon: formValues.icon,
|
|
||||||
id: activeMetadataField.id,
|
|
||||||
label: formValues.label,
|
|
||||||
name: activeMetadataField.name,
|
|
||||||
type: formValues.type,
|
|
||||||
}}
|
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
|
||||||
onChange={handleFormChange}
|
|
||||||
relationFieldMetadataItem={relationFieldMetadataItem}
|
|
||||||
values={{
|
|
||||||
currency: formValues.currency,
|
|
||||||
relation: formValues.relation,
|
|
||||||
select: formValues.select,
|
|
||||||
multiSelect: formValues.multiSelect,
|
|
||||||
defaultValue: formValues.defaultValue,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
{!isLabelIdentifier && (
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Danger zone" description="Disable this field" />
|
<H2Title
|
||||||
<Button
|
title="Name and description"
|
||||||
Icon={IconArchive}
|
description="The name and description of this field"
|
||||||
title="Disable"
|
/>
|
||||||
size="small"
|
<SettingsDataModelFieldAboutForm
|
||||||
onClick={handleDisable}
|
disabled={!activeMetadataField.isCustom}
|
||||||
|
fieldMetadataItem={activeMetadataField}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
<Section>
|
||||||
</SettingsPageContainer>
|
<H2Title
|
||||||
</SubMenuTopBarContainer>
|
title="Type and values"
|
||||||
|
description="The field's type and values."
|
||||||
|
/>
|
||||||
|
<StyledSettingsObjectFieldTypeSelect
|
||||||
|
disabled
|
||||||
|
onChange={handleFormChange}
|
||||||
|
value={formValues.type}
|
||||||
|
/>
|
||||||
|
<SettingsDataModelFieldSettingsFormCard
|
||||||
|
disableCurrencyForm
|
||||||
|
fieldMetadataItem={{
|
||||||
|
icon: formConfig.watch('icon'),
|
||||||
|
id: activeMetadataField.id,
|
||||||
|
label: formConfig.watch('label'),
|
||||||
|
name: activeMetadataField.name,
|
||||||
|
type: formValues.type,
|
||||||
|
}}
|
||||||
|
objectMetadataItem={activeObjectMetadataItem}
|
||||||
|
onChange={handleFormChange}
|
||||||
|
relationFieldMetadataItem={relationFieldMetadataItem}
|
||||||
|
values={{
|
||||||
|
currency: formValues.currency,
|
||||||
|
relation: formValues.relation,
|
||||||
|
select: formValues.select,
|
||||||
|
multiSelect: formValues.multiSelect,
|
||||||
|
defaultValue: formValues.defaultValue,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
{!isLabelIdentifier && (
|
||||||
|
<Section>
|
||||||
|
<H2Title title="Danger zone" description="Disable this field" />
|
||||||
|
<Button
|
||||||
|
Icon={IconArchive}
|
||||||
|
title="Disable"
|
||||||
|
size="small"
|
||||||
|
onClick={handleDisable}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Reference, useApolloClient } from '@apollo/client';
|
import { Reference, useApolloClient } from '@apollo/client';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import pick from 'lodash.pick';
|
||||||
import { IconSettings } from 'twenty-ui';
|
import { IconSettings } from 'twenty-ui';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
@ -15,10 +19,11 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|||||||
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 { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
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 { 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 { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
||||||
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';
|
||||||
@ -31,6 +36,10 @@ import { ViewType } from '@/views/types/ViewType';
|
|||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
|
type SettingsDataModelNewFieldFormValues = z.infer<
|
||||||
|
typeof settingsFieldFormSchema
|
||||||
|
>;
|
||||||
|
|
||||||
const StyledSettingsObjectFieldTypeSelect = styled(
|
const StyledSettingsObjectFieldTypeSelect = styled(
|
||||||
SettingsDataModelFieldTypeSelect,
|
SettingsDataModelFieldTypeSelect,
|
||||||
)`
|
)`
|
||||||
@ -57,10 +66,15 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
formValues,
|
formValues,
|
||||||
handleFormChange,
|
handleFormChange,
|
||||||
initForm,
|
initForm,
|
||||||
isValid: canSave,
|
isValid,
|
||||||
validatedFormValues,
|
validatedFormValues,
|
||||||
} = useFieldMetadataForm();
|
} = useFieldMetadataForm();
|
||||||
|
|
||||||
|
const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
|
||||||
|
mode: 'onTouched',
|
||||||
|
resolver: zodResolver(settingsFieldFormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeObjectMetadataItem) {
|
if (!activeObjectMetadataItem) {
|
||||||
navigate(AppPath.NotFound);
|
navigate(AppPath.NotFound);
|
||||||
@ -121,19 +135,18 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
|
|
||||||
if (!activeObjectMetadataItem) return null;
|
if (!activeObjectMetadataItem) return null;
|
||||||
|
|
||||||
|
const canSave = formConfig.formState.isValid && isValid;
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!validatedFormValues) return;
|
if (!validatedFormValues) return;
|
||||||
|
|
||||||
|
const formValues = formConfig.getValues();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (validatedFormValues.type === FieldMetadataType.Relation) {
|
if (validatedFormValues.type === FieldMetadataType.Relation) {
|
||||||
const createdRelation = await createOneRelationMetadata({
|
const createdRelation = await createOneRelationMetadata({
|
||||||
relationType: validatedFormValues.relation.type,
|
relationType: validatedFormValues.relation.type,
|
||||||
field: {
|
field: pick(formValues, ['icon', 'label', 'description']),
|
||||||
description: validatedFormValues.description,
|
|
||||||
icon: validatedFormValues.icon,
|
|
||||||
label: validatedFormValues.label,
|
|
||||||
type: validatedFormValues.type,
|
|
||||||
},
|
|
||||||
objectMetadataId: activeObjectMetadataItem.id,
|
objectMetadataId: activeObjectMetadataItem.id,
|
||||||
connect: {
|
connect: {
|
||||||
field: {
|
field: {
|
||||||
@ -223,9 +236,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
currencyCode: validatedFormValues.currency.currencyCode,
|
currencyCode: validatedFormValues.currency.currencyCode,
|
||||||
}
|
}
|
||||||
: validatedFormValues.defaultValue,
|
: validatedFormValues.defaultValue,
|
||||||
description: validatedFormValues.description,
|
...formValues,
|
||||||
icon: validatedFormValues.icon,
|
|
||||||
label: validatedFormValues.label ?? '',
|
|
||||||
objectMetadataId: activeObjectMetadataItem.id,
|
objectMetadataId: activeObjectMetadataItem.id,
|
||||||
type: validatedFormValues.type,
|
type: validatedFormValues.type,
|
||||||
options:
|
options:
|
||||||
@ -287,61 +298,65 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<SettingsPageContainer>
|
<FormProvider {...formConfig}>
|
||||||
<SettingsHeaderContainer>
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
<Breadcrumb
|
<SettingsPageContainer>
|
||||||
links={[
|
<SettingsHeaderContainer>
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
<Breadcrumb
|
||||||
{
|
links={[
|
||||||
children: activeObjectMetadataItem.labelPlural,
|
{ children: 'Objects', href: '/settings/objects' },
|
||||||
href: `/settings/objects/${objectSlug}`,
|
{
|
||||||
},
|
children: activeObjectMetadataItem.labelPlural,
|
||||||
{ children: 'New Field' },
|
href: `/settings/objects/${objectSlug}`,
|
||||||
]}
|
},
|
||||||
/>
|
{ children: 'New Field' },
|
||||||
{!activeObjectMetadataItem.isRemote && (
|
]}
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{!activeObjectMetadataItem.isRemote && (
|
||||||
</SettingsHeaderContainer>
|
<SaveAndCancelButtons
|
||||||
<SettingsObjectFieldFormSection
|
isSaveDisabled={!canSave}
|
||||||
iconKey={formValues.icon}
|
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||||
name={formValues.label}
|
onSave={handleSave}
|
||||||
description={formValues.description}
|
/>
|
||||||
onChange={handleFormChange}
|
)}
|
||||||
/>
|
</SettingsHeaderContainer>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Type and values"
|
title="Name and description"
|
||||||
description="The field's type and values."
|
description="The name and description of this field"
|
||||||
/>
|
/>
|
||||||
<StyledSettingsObjectFieldTypeSelect
|
<SettingsDataModelFieldAboutForm />
|
||||||
excludedFieldTypes={excludedFieldTypes}
|
</Section>
|
||||||
onChange={handleFormChange}
|
<Section>
|
||||||
value={formValues.type}
|
<H2Title
|
||||||
/>
|
title="Type and values"
|
||||||
<SettingsDataModelFieldSettingsFormCard
|
description="The field's type and values."
|
||||||
fieldMetadataItem={{
|
/>
|
||||||
icon: formValues.icon,
|
<StyledSettingsObjectFieldTypeSelect
|
||||||
label: formValues.label || 'Employees',
|
excludedFieldTypes={excludedFieldTypes}
|
||||||
type: formValues.type,
|
onChange={handleFormChange}
|
||||||
}}
|
value={formValues.type}
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
/>
|
||||||
onChange={handleFormChange}
|
<SettingsDataModelFieldSettingsFormCard
|
||||||
values={{
|
fieldMetadataItem={{
|
||||||
currency: formValues.currency,
|
icon: formConfig.watch('icon'),
|
||||||
relation: formValues.relation,
|
label: formConfig.watch('label') || 'Employees',
|
||||||
select: formValues.select,
|
type: formValues.type,
|
||||||
multiSelect: formValues.multiSelect,
|
}}
|
||||||
defaultValue: formValues.defaultValue,
|
objectMetadataItem={activeObjectMetadataItem}
|
||||||
}}
|
onChange={handleFormChange}
|
||||||
/>
|
values={{
|
||||||
</Section>
|
currency: formValues.currency,
|
||||||
</SettingsPageContainer>
|
relation: formValues.relation,
|
||||||
</SubMenuTopBarContainer>
|
select: formValues.select,
|
||||||
|
multiSelect: formValues.multiSelect,
|
||||||
|
defaultValue: formValues.defaultValue,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -53,7 +53,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
|
|||||||
createdAt: '2024-04-08T12:48:49.538Z',
|
createdAt: '2024-04-08T12:48:49.538Z',
|
||||||
updatedAt: '2024-04-08T12:48:49.538Z',
|
updatedAt: '2024-04-08T12:48:49.538Z',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
options: null,
|
|
||||||
relationDefinition: {
|
relationDefinition: {
|
||||||
__typename: 'RelationDefinition',
|
__typename: 'RelationDefinition',
|
||||||
direction: 'ONE_TO_MANY',
|
direction: 'ONE_TO_MANY',
|
||||||
@ -114,7 +113,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
|
|||||||
createdAt: '2024-04-08T12:48:49.538Z',
|
createdAt: '2024-04-08T12:48:49.538Z',
|
||||||
updatedAt: '2024-04-08T12:48:49.538Z',
|
updatedAt: '2024-04-08T12:48:49.538Z',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
options: null,
|
|
||||||
relationDefinition: null,
|
relationDefinition: null,
|
||||||
fromRelationMetadata: null,
|
fromRelationMetadata: null,
|
||||||
toRelationMetadata: null,
|
toRelationMetadata: null,
|
||||||
@ -137,7 +135,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
|
|||||||
createdAt: '2024-04-08T12:48:49.538Z',
|
createdAt: '2024-04-08T12:48:49.538Z',
|
||||||
updatedAt: '2024-04-08T12:48:49.538Z',
|
updatedAt: '2024-04-08T12:48:49.538Z',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
options: null,
|
|
||||||
relationDefinition: null,
|
relationDefinition: null,
|
||||||
fromRelationMetadata: null,
|
fromRelationMetadata: null,
|
||||||
toRelationMetadata: null,
|
toRelationMetadata: null,
|
||||||
@ -160,7 +157,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
|
|||||||
createdAt: '2024-04-08T12:48:49.538Z',
|
createdAt: '2024-04-08T12:48:49.538Z',
|
||||||
updatedAt: '2024-04-08T12:48:49.538Z',
|
updatedAt: '2024-04-08T12:48:49.538Z',
|
||||||
defaultValue: "''",
|
defaultValue: "''",
|
||||||
options: null,
|
|
||||||
relationDefinition: null,
|
relationDefinition: null,
|
||||||
fromRelationMetadata: null,
|
fromRelationMetadata: null,
|
||||||
toRelationMetadata: null,
|
toRelationMetadata: null,
|
||||||
@ -183,7 +179,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
|
|||||||
createdAt: '2024-04-08T12:48:49.538Z',
|
createdAt: '2024-04-08T12:48:49.538Z',
|
||||||
updatedAt: '2024-04-08T12:48:49.538Z',
|
updatedAt: '2024-04-08T12:48:49.538Z',
|
||||||
defaultValue: 'now',
|
defaultValue: 'now',
|
||||||
options: null,
|
|
||||||
relationDefinition: null,
|
relationDefinition: null,
|
||||||
fromRelationMetadata: null,
|
fromRelationMetadata: null,
|
||||||
toRelationMetadata: null,
|
toRelationMetadata: null,
|
||||||
@ -206,7 +201,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
|
|||||||
createdAt: '2024-04-08T12:48:49.538Z',
|
createdAt: '2024-04-08T12:48:49.538Z',
|
||||||
updatedAt: '2024-04-08T12:48:49.538Z',
|
updatedAt: '2024-04-08T12:48:49.538Z',
|
||||||
defaultValue: 'now',
|
defaultValue: 'now',
|
||||||
options: null,
|
|
||||||
relationDefinition: null,
|
relationDefinition: null,
|
||||||
fromRelationMetadata: null,
|
fromRelationMetadata: null,
|
||||||
toRelationMetadata: null,
|
toRelationMetadata: null,
|
||||||
@ -229,7 +223,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
|
|||||||
createdAt: '2024-04-08T12:48:49.538Z',
|
createdAt: '2024-04-08T12:48:49.538Z',
|
||||||
updatedAt: '2024-04-08T12:48:49.538Z',
|
updatedAt: '2024-04-08T12:48:49.538Z',
|
||||||
defaultValue: 'uuid',
|
defaultValue: 'uuid',
|
||||||
options: null,
|
|
||||||
relationDefinition: null,
|
relationDefinition: null,
|
||||||
fromRelationMetadata: null,
|
fromRelationMetadata: null,
|
||||||
toRelationMetadata: null,
|
toRelationMetadata: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user