Added new view to select types for objects (#6700)
Issue #6496 Hi team, Is this the right approach for handling type selection with states and conditional rendering, or should these be managed on separate pages altogether? Please let me know Ill make changes accordingly :) I’m also working on styling the buttons according to the Figma design and will be moving constants like categoryDescriptions and categories to the constants folder. Thanks for your guidance! https://github.com/user-attachments/assets/452bea9f-4d0a-4472-9941-421b54cda47f --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,104 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||
import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
export const settingsDataModelFieldAboutFormSchema = (
|
||||
existingLabels?: string[],
|
||||
) => {
|
||||
return fieldMetadataItemSchema(existingLabels || []).pick({
|
||||
description: true,
|
||||
icon: true,
|
||||
label: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Correctly infer the type from the returned schema
|
||||
type SettingsDataModelFieldAboutFormValues = z.infer<
|
||||
ReturnType<typeof settingsDataModelFieldAboutFormSchema>
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldAboutFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem?: FieldMetadataItem;
|
||||
maxLength?: number;
|
||||
};
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const LABEL = 'label';
|
||||
|
||||
export const SettingsDataModelFieldAboutForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
maxLength,
|
||||
}: SettingsDataModelFieldAboutFormProps) => {
|
||||
const {
|
||||
control,
|
||||
trigger,
|
||||
formState: { errors },
|
||||
} = 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={(e) => {
|
||||
onChange(e);
|
||||
trigger(LABEL);
|
||||
}}
|
||||
error={getErrorMessageFromError(errors.label?.message)}
|
||||
disabled={disabled}
|
||||
maxLength={maxLength}
|
||||
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 { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
|
||||
export const settingsDataModelFieldDescriptionFormSchema = () => {
|
||||
return fieldMetadataItemSchema([]).pick({
|
||||
description: true,
|
||||
});
|
||||
};
|
||||
|
||||
type SettingsDataModelFieldDescriptionFormValues = z.infer<
|
||||
ReturnType<typeof settingsDataModelFieldDescriptionFormSchema>
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldDescriptionFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem?: FieldMetadataItem;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldDescriptionForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldDescriptionFormProps) => {
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldDescriptionFormValues>();
|
||||
|
||||
return (
|
||||
<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,84 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||
import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
export const settingsDataModelFieldIconLabelFormSchema = (
|
||||
existingLabels?: string[],
|
||||
) => {
|
||||
return fieldMetadataItemSchema(existingLabels || []).pick({
|
||||
icon: true,
|
||||
label: true,
|
||||
});
|
||||
};
|
||||
|
||||
type SettingsDataModelFieldIconLabelFormValues = z.infer<
|
||||
ReturnType<typeof settingsDataModelFieldIconLabelFormSchema>
|
||||
>;
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type SettingsDataModelFieldIconLabelFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem?: FieldMetadataItem;
|
||||
maxLength?: number;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldIconLabelForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
maxLength,
|
||||
}: SettingsDataModelFieldIconLabelFormProps) => {
|
||||
const {
|
||||
control,
|
||||
trigger,
|
||||
formState: { errors },
|
||||
} = useFormContext<SettingsDataModelFieldIconLabelFormValues>();
|
||||
|
||||
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={(e) => {
|
||||
onChange(e);
|
||||
trigger('label');
|
||||
}}
|
||||
error={getErrorMessageFromError(errors.label?.message)}
|
||||
disabled={disabled}
|
||||
maxLength={maxLength}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
);
|
||||
};
|
||||
@ -1,8 +1,10 @@
|
||||
import omit from 'lodash.omit';
|
||||
import styled from '@emotion/styled';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SETTINGS_FIELD_TYPE_CATEGORIES } from '@/settings/data-model/constants/SettingsFieldTypeCategories';
|
||||
import { SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS } from '@/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions';
|
||||
import {
|
||||
SETTINGS_FIELD_TYPE_CONFIGS,
|
||||
SettingsFieldTypeConfig,
|
||||
@ -11,7 +13,12 @@ import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/field
|
||||
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
|
||||
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
|
||||
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { Section } from '@react-email/components';
|
||||
import { useState } from 'react';
|
||||
import { H2Title, IconChevronRight, IconSearch } from 'twenty-ui';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const settingsDataModelFieldTypeFormSchema = z.object({
|
||||
@ -23,39 +30,76 @@ export const settingsDataModelFieldTypeFormSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
type SettingsDataModelFieldTypeFormValues = z.infer<
|
||||
export type SettingsDataModelFieldTypeFormValues = z.infer<
|
||||
typeof settingsDataModelFieldTypeFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldTypeSelectProps = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
excludedFieldTypes?: SettingsSupportedFieldType[];
|
||||
fieldMetadataItem?: Pick<
|
||||
FieldMetadataItem,
|
||||
'defaultValue' | 'options' | 'type'
|
||||
>;
|
||||
onFieldTypeSelect: () => void;
|
||||
};
|
||||
|
||||
const StyledTypeSelectContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: inherit;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)<{ isActive: boolean }>`
|
||||
background: ${({ theme, isActive }) =>
|
||||
isActive ? theme.background.quaternary : theme.background.secondary};
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
`;
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
position: relative;
|
||||
width: calc(50% - ${({ theme }) => theme.spacing(1)});
|
||||
`;
|
||||
|
||||
const StyledRightChevron = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(2)};
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
const StyledSearchInput = styled(TextInput)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldTypeSelect = ({
|
||||
className,
|
||||
disabled,
|
||||
excludedFieldTypes = [],
|
||||
fieldMetadataItem,
|
||||
onFieldTypeSelect,
|
||||
}: SettingsDataModelFieldTypeSelectProps) => {
|
||||
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]) => ({
|
||||
Icon: dataTypeConfig.Icon,
|
||||
label: dataTypeConfig.label,
|
||||
value: key as SettingsSupportedFieldType,
|
||||
}));
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const theme = useTheme();
|
||||
const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig>(
|
||||
SETTINGS_FIELD_TYPE_CONFIGS,
|
||||
).filter(
|
||||
([key, config]) =>
|
||||
!excludedFieldTypes.includes(key as SettingsSupportedFieldType) &&
|
||||
config.label.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
const { resetDefaultValueField: resetBooleanDefaultValueField } =
|
||||
useBooleanSettingsFormInitialValues({ fieldMetadataItem });
|
||||
@ -66,8 +110,6 @@ export const SettingsDataModelFieldTypeSelect = ({
|
||||
const { resetDefaultValueField: resetSelectDefaultValueField } =
|
||||
useSelectSettingsFormInitialValues({ fieldMetadataItem });
|
||||
|
||||
// Reset defaultValue on type change with a valid value for the selected type
|
||||
// so the form does not become invalid.
|
||||
const resetDefaultValueField = (nextValue: SettingsSupportedFieldType) => {
|
||||
switch (nextValue) {
|
||||
case FieldMetadataType.Boolean:
|
||||
@ -95,18 +137,49 @@ export const SettingsDataModelFieldTypeSelect = ({
|
||||
: FieldMetadataType.Text
|
||||
}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
className={className}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-type-select"
|
||||
value={value}
|
||||
onChange={(nextValue) => {
|
||||
onChange(nextValue);
|
||||
resetDefaultValueField(nextValue);
|
||||
}}
|
||||
options={fieldTypeOptions}
|
||||
/>
|
||||
<StyledTypeSelectContainer className={className}>
|
||||
<Section>
|
||||
<StyledSearchInput
|
||||
LeftIcon={IconSearch}
|
||||
placeholder="Search a type"
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
/>
|
||||
</Section>
|
||||
{SETTINGS_FIELD_TYPE_CATEGORIES.map((category) => (
|
||||
<Section key={category}>
|
||||
<H2Title
|
||||
title={category}
|
||||
description={
|
||||
SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS[category]
|
||||
}
|
||||
/>
|
||||
<StyledContainer>
|
||||
{fieldTypeConfigs
|
||||
.filter(([, config]) => config.category === category)
|
||||
.map(([key, config]) => (
|
||||
<StyledButtonContainer>
|
||||
<StyledButton
|
||||
key={key}
|
||||
onClick={() => {
|
||||
onChange(key as SettingsSupportedFieldType);
|
||||
resetDefaultValueField(
|
||||
key as SettingsSupportedFieldType,
|
||||
);
|
||||
onFieldTypeSelect();
|
||||
}}
|
||||
title={config.label}
|
||||
Icon={config.Icon}
|
||||
size="small"
|
||||
isActive={value === key}
|
||||
/>
|
||||
<StyledRightChevron size={theme.icon.size.md} />
|
||||
</StyledButtonContainer>
|
||||
))}
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
))}
|
||||
</StyledTypeSelectContainer>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
|
||||
|
||||
import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||
import { SettingsDataModelFieldDescriptionForm } from '../SettingsDataModelFieldDescriptionForm';
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldDescriptionForm> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsDataModelFieldDescriptionForm',
|
||||
component: SettingsDataModelFieldDescriptionForm,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ flex: 1 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
FormProviderDecorator,
|
||||
ComponentDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldDescriptionForm>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithFieldMetadataItem: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||
({ description }) => description === 'description',
|
||||
)!,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
@ -4,17 +4,17 @@ 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';
|
||||
import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||
import { SettingsDataModelFieldIconLabelForm } from '../SettingsDataModelFieldIconLabelForm';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldAboutForm> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsDataModelFieldAboutForm',
|
||||
component: SettingsDataModelFieldAboutForm,
|
||||
const meta: Meta<typeof SettingsDataModelFieldIconLabelForm> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsDataModelFieldIconLabelForm',
|
||||
component: SettingsDataModelFieldIconLabelForm,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StyledContainer>
|
||||
@ -28,11 +28,11 @@ const meta: Meta<typeof SettingsDataModelFieldAboutForm> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldAboutForm>;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldIconLabelForm>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithDefaultValues: Story = {
|
||||
export const WithFieldMetadataItem: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'name',
|
||||
@ -45,3 +45,9 @@ export const Disabled: Story = {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithMaxLength: Story = {
|
||||
args: {
|
||||
maxLength: 50,
|
||||
},
|
||||
};
|
||||
@ -24,12 +24,6 @@ type Story = StoryObj<typeof SettingsDataModelFieldTypeSelect>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOpenSelect: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { settingsDataModelFieldAboutFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||
import { settingsDataModelFieldDescriptionFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm';
|
||||
import { settingsDataModelFieldIconLabelFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm';
|
||||
import { settingsDataModelFieldSettingsFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
import { settingsDataModelFieldTypeFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
|
||||
|
||||
export const settingsFieldFormSchema = (existingLabels?: string[]) => {
|
||||
return z
|
||||
.object({})
|
||||
.merge(settingsDataModelFieldAboutFormSchema(existingLabels))
|
||||
.merge(settingsDataModelFieldIconLabelFormSchema(existingLabels))
|
||||
.merge(settingsDataModelFieldDescriptionFormSchema())
|
||||
.merge(settingsDataModelFieldTypeFormSchema)
|
||||
.and(settingsDataModelFieldSettingsFormSchema);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user