[REFACTOR] Split in two distinct forms Settings Object Model page (#10653)
# Introduction This PR contains around ~+300 tests + snapshot additions Please check both object model creation and edition Closes https://github.com/twentyhq/core-team-issues/issues/355 Refactored into two agnostic forms the Object Model settings page for instance `/settings/objects/notes#settings`. ## `SettingsDataModelObjectAboutForm` Added a new abstraction `SettingsUpdateDataModelObjectAboutForm` to wrap `SettingsDataModelObjectAboutForm` in an `update` context  Schema: ```ts const requiredFormFields = objectMetadataItemSchema.pick({ description: true, icon: true, labelPlural: true, labelSingular: true, }); const optionalFormFields = objectMetadataItemSchema .pick({ nameSingular: true, namePlural: true, isLabelSyncedWithName: true, }) .partial(); export const settingsDataModelObjectAboutFormSchema = requiredFormFields.merge(optionalFormFields); ``` ## `SettingsDataModelObjectSettingsFormCard` Update on change  Schema: ```ts export const settingsDataModelObjectIdentifiersFormSchema = objectMetadataItemSchema.pick({ labelIdentifierFieldMetadataId: true, imageIdentifierFieldMetadataId: true, }); ``` ## Error management and validation schema Improved the frontend validation form in order to attest that: - Names are in camelCase - Names are differents - Names are not empty string ***SHOULD BE DONE SERVER SIDE TOO*** ( will in a next PR, atm it literally breaks any workspace ) - Labels are differents - Labels aren't empty strings Hide the error messages as we need to decide what kind of styling we want for our errors with forms ( Example with error labels ) 
This commit is contained in:
@ -0,0 +1,2 @@
|
|||||||
|
export const SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE =
|
||||||
|
true;
|
||||||
@ -11,8 +11,8 @@ import {
|
|||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
type SettingsObjectFieldInactiveActionDropdownProps = {
|
type SettingsObjectFieldInactiveActionDropdownProps = {
|
||||||
isCustomField?: boolean;
|
isCustomField?: boolean;
|
||||||
|
|||||||
@ -0,0 +1,106 @@
|
|||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE } from '@/settings/constants/SettingsObjectModel';
|
||||||
|
import { SettingsDataModelObjectAboutForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||||
|
import {
|
||||||
|
SettingsDataModelObjectAboutFormValues,
|
||||||
|
settingsDataModelObjectAboutFormSchema,
|
||||||
|
} from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import { ZodError } from 'zod';
|
||||||
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
|
import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState';
|
||||||
|
|
||||||
|
type SettingsUpdateDataModelObjectAboutFormProps = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsUpdateDataModelObjectAboutForm = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
}: SettingsUpdateDataModelObjectAboutFormProps) => {
|
||||||
|
const navigate = useNavigateSettings();
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const setUpdatedObjectNamePlural = useSetRecoilState(
|
||||||
|
updatedObjectNamePluralState,
|
||||||
|
);
|
||||||
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
const {
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
isLabelSyncedWithName,
|
||||||
|
labelPlural,
|
||||||
|
labelSingular,
|
||||||
|
namePlural,
|
||||||
|
nameSingular,
|
||||||
|
} = objectMetadataItem;
|
||||||
|
const formConfig = useForm<SettingsDataModelObjectAboutFormValues>({
|
||||||
|
mode: 'onTouched',
|
||||||
|
resolver: zodResolver(settingsDataModelObjectAboutFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
description,
|
||||||
|
icon: icon ?? undefined,
|
||||||
|
isLabelSyncedWithName: isDefined(isLabelSyncedWithName)
|
||||||
|
? isLabelSyncedWithName
|
||||||
|
: SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE,
|
||||||
|
labelPlural,
|
||||||
|
labelSingular,
|
||||||
|
namePlural,
|
||||||
|
nameSingular,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = async (
|
||||||
|
formValues: SettingsDataModelObjectAboutFormValues,
|
||||||
|
) => {
|
||||||
|
if (!(Object.keys(formConfig.formState.dirtyFields).length > 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectNamePluralForRedirection =
|
||||||
|
formValues.namePlural ?? objectMetadataItem.namePlural;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setUpdatedObjectNamePlural(objectNamePluralForRedirection);
|
||||||
|
|
||||||
|
await updateOneObjectMetadataItem({
|
||||||
|
idToUpdate: objectMetadataItem.id,
|
||||||
|
updatePayload: formValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
formConfig.reset(undefined, { keepValues: true });
|
||||||
|
|
||||||
|
navigate(SettingsPath.ObjectDetail, {
|
||||||
|
objectNamePlural: objectNamePluralForRedirection,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
enqueueSnackBar(error.issues[0].message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
enqueueSnackBar((error as Error).message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
<FormProvider {...formConfig}>
|
||||||
|
<SettingsDataModelObjectAboutForm
|
||||||
|
onNewDirtyField={() => formConfig.handleSubmit(handleSave)()}
|
||||||
|
disableEdition={!objectMetadataItem.isCustom}
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,39 +1,14 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
|
||||||
import { Button, H2Title, IconArchive, Section } from 'twenty-ui';
|
import { Button, H2Title, IconArchive, Section } from 'twenty-ui';
|
||||||
import { ZodError, z } from 'zod';
|
|
||||||
|
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import {
|
import { SettingsUpdateDataModelObjectAboutForm } from '@/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm';
|
||||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
|
||||||
SettingsDataModelObjectAboutForm,
|
|
||||||
settingsDataModelObjectAboutFormSchema,
|
|
||||||
} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
|
||||||
import { settingsDataModelObjectIdentifiersFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
|
|
||||||
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
||||||
import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import pick from 'lodash.pick';
|
|
||||||
import { useSetRecoilState } from 'recoil';
|
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState';
|
|
||||||
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
|
||||||
|
|
||||||
const objectEditFormSchema = z
|
|
||||||
.object({})
|
|
||||||
.merge(settingsDataModelObjectAboutFormSchema)
|
|
||||||
.merge(settingsDataModelObjectIdentifiersFormSchema);
|
|
||||||
|
|
||||||
type SettingsDataModelObjectEditFormValues = z.infer<
|
|
||||||
typeof objectEditFormSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
type ObjectSettingsProps = {
|
type ObjectSettingsProps = {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
@ -52,101 +27,7 @@ const StyledFormSection = styled(Section)`
|
|||||||
export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
|
export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const navigate = useNavigateSettings();
|
const navigate = useNavigateSettings();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
|
||||||
const setUpdatedObjectNamePlural = useSetRecoilState(
|
|
||||||
updatedObjectNamePluralState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
|
||||||
const formConfig = useForm<SettingsDataModelObjectEditFormValues>({
|
|
||||||
mode: 'onTouched',
|
|
||||||
resolver: zodResolver(objectEditFormSchema),
|
|
||||||
});
|
|
||||||
const { isDirty } = formConfig.formState;
|
|
||||||
|
|
||||||
const getUpdatePayload = (
|
|
||||||
formValues: SettingsDataModelObjectEditFormValues,
|
|
||||||
) => {
|
|
||||||
let values = formValues;
|
|
||||||
const dirtyFieldKeys = Object.keys(
|
|
||||||
formConfig.formState.dirtyFields,
|
|
||||||
) as (keyof SettingsDataModelObjectEditFormValues)[];
|
|
||||||
const shouldComputeNamesFromLabels: boolean = dirtyFieldKeys.includes(
|
|
||||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
|
||||||
)
|
|
||||||
? (formValues.isLabelSyncedWithName as boolean)
|
|
||||||
: objectMetadataItem.isLabelSyncedWithName;
|
|
||||||
|
|
||||||
if (shouldComputeNamesFromLabels) {
|
|
||||||
values = {
|
|
||||||
...values,
|
|
||||||
...(values.labelSingular && dirtyFieldKeys.includes('labelSingular')
|
|
||||||
? {
|
|
||||||
nameSingular: computeMetadataNameFromLabel(
|
|
||||||
formValues.labelSingular,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(values.labelPlural && dirtyFieldKeys.includes('labelPlural')
|
|
||||||
? {
|
|
||||||
namePlural: computeMetadataNameFromLabel(formValues.labelPlural),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return settingsUpdateObjectInputSchema.parse(
|
|
||||||
pick(values, [
|
|
||||||
...dirtyFieldKeys,
|
|
||||||
...(shouldComputeNamesFromLabels &&
|
|
||||||
dirtyFieldKeys.includes('labelPlural')
|
|
||||||
? ['namePlural']
|
|
||||||
: []),
|
|
||||||
...(shouldComputeNamesFromLabels &&
|
|
||||||
dirtyFieldKeys.includes('labelSingular')
|
|
||||||
? ['nameSingular']
|
|
||||||
: []),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async (
|
|
||||||
formValues: SettingsDataModelObjectEditFormValues,
|
|
||||||
) => {
|
|
||||||
if (!isDirty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const updatePayload = getUpdatePayload(formValues);
|
|
||||||
const objectNamePluralForRedirection =
|
|
||||||
updatePayload.namePlural ?? objectMetadataItem.namePlural;
|
|
||||||
|
|
||||||
setUpdatedObjectNamePlural(objectNamePluralForRedirection);
|
|
||||||
|
|
||||||
await updateOneObjectMetadataItem({
|
|
||||||
idToUpdate: objectMetadataItem.id,
|
|
||||||
updatePayload,
|
|
||||||
});
|
|
||||||
|
|
||||||
formConfig.reset(undefined, { keepValues: true });
|
|
||||||
|
|
||||||
navigate(SettingsPath.ObjectDetail, {
|
|
||||||
objectNamePlural: objectNamePluralForRedirection,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ZodError) {
|
|
||||||
enqueueSnackBar(error.issues[0].message, {
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
enqueueSnackBar((error as Error).message, {
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisable = async () => {
|
const handleDisable = async () => {
|
||||||
await updateOneObjectMetadataItem({
|
await updateOneObjectMetadataItem({
|
||||||
idToUpdate: objectMetadataItem.id,
|
idToUpdate: objectMetadataItem.id,
|
||||||
@ -157,49 +38,42 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordFieldValueSelectorContextProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
<FormProvider {...formConfig}>
|
<StyledContentContainer>
|
||||||
<StyledContentContainer>
|
<StyledFormSection>
|
||||||
<StyledFormSection>
|
<H2Title
|
||||||
|
title={t`About`}
|
||||||
|
description={t`Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms.`}
|
||||||
|
/>
|
||||||
|
<SettingsUpdateDataModelObjectAboutForm
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
/>
|
||||||
|
</StyledFormSection>
|
||||||
|
<StyledFormSection>
|
||||||
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title={t`About`}
|
title={t`Options`}
|
||||||
description={t`Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms.`}
|
description={t`Choose the fields that will identify your records`}
|
||||||
/>
|
/>
|
||||||
<SettingsDataModelObjectAboutForm
|
<SettingsDataModelObjectSettingsFormCard
|
||||||
disableEdition={!objectMetadataItem.isCustom}
|
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
onBlur={() => {
|
|
||||||
formConfig.handleSubmit(handleSave)();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</StyledFormSection>
|
</Section>
|
||||||
<StyledFormSection>
|
</StyledFormSection>
|
||||||
<Section>
|
<StyledFormSection>
|
||||||
<H2Title
|
<Section>
|
||||||
title={t`Options`}
|
<H2Title
|
||||||
description={t`Choose the fields that will identify your records`}
|
title={t`Danger zone`}
|
||||||
/>
|
description={t`Deactivate object`}
|
||||||
<SettingsDataModelObjectSettingsFormCard
|
/>
|
||||||
onBlur={() => formConfig.handleSubmit(handleSave)()}
|
<Button
|
||||||
objectMetadataItem={objectMetadataItem}
|
Icon={IconArchive}
|
||||||
/>
|
title={t`Deactivate`}
|
||||||
</Section>
|
size="small"
|
||||||
</StyledFormSection>
|
onClick={handleDisable}
|
||||||
<StyledFormSection>
|
/>
|
||||||
<Section>
|
</Section>
|
||||||
<H2Title
|
</StyledFormSection>
|
||||||
title={t`Danger zone`}
|
</StyledContentContainer>
|
||||||
description={t`Deactivate object`}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
Icon={IconArchive}
|
|
||||||
title={t`Deactivate`}
|
|
||||||
size="small"
|
|
||||||
onClick={handleDisable}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
</StyledFormSection>
|
|
||||||
</StyledContentContainer>
|
|
||||||
</FormProvider>
|
|
||||||
</RecordFieldValueSelectorContextProvider>
|
</RecordFieldValueSelectorContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
|
||||||
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
||||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||||
|
import { SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE } from '@/settings/constants/SettingsObjectModel';
|
||||||
import { OBJECT_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/ObjectNameMaximumLength';
|
import { OBJECT_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/ObjectNameMaximumLength';
|
||||||
|
import { SettingsDataModelObjectAboutFormValues } from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema';
|
||||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||||
import { TextArea } from '@/ui/input/components/TextArea';
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
@ -19,34 +20,13 @@ import {
|
|||||||
IconRefresh,
|
IconRefresh,
|
||||||
TooltipDelay,
|
TooltipDelay,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { StringKeyOf } from 'type-fest';
|
||||||
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
export const settingsDataModelObjectAboutFormSchema = objectMetadataItemSchema
|
|
||||||
.pick({
|
|
||||||
description: true,
|
|
||||||
icon: true,
|
|
||||||
labelPlural: true,
|
|
||||||
labelSingular: true,
|
|
||||||
})
|
|
||||||
.merge(
|
|
||||||
objectMetadataItemSchema
|
|
||||||
.pick({
|
|
||||||
nameSingular: true,
|
|
||||||
namePlural: true,
|
|
||||||
isLabelSyncedWithName: true,
|
|
||||||
})
|
|
||||||
.partial(),
|
|
||||||
);
|
|
||||||
|
|
||||||
type SettingsDataModelObjectAboutFormValues = z.infer<
|
|
||||||
typeof settingsDataModelObjectAboutFormSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
type SettingsDataModelObjectAboutFormProps = {
|
type SettingsDataModelObjectAboutFormProps = {
|
||||||
disableEdition?: boolean;
|
disableEdition?: boolean;
|
||||||
objectMetadataItem?: ObjectMetadataItem;
|
objectMetadataItem?: ObjectMetadataItem;
|
||||||
onBlur?: () => void;
|
onNewDirtyField?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledInputsContainer = styled.div`
|
const StyledInputsContainer = styled.div`
|
||||||
@ -92,21 +72,20 @@ const infoCircleElementId = 'info-circle-id';
|
|||||||
export const IS_LABEL_SYNCED_WITH_NAME_LABEL = 'isLabelSyncedWithName';
|
export const IS_LABEL_SYNCED_WITH_NAME_LABEL = 'isLabelSyncedWithName';
|
||||||
|
|
||||||
export const SettingsDataModelObjectAboutForm = ({
|
export const SettingsDataModelObjectAboutForm = ({
|
||||||
disableEdition,
|
disableEdition = false,
|
||||||
|
onNewDirtyField,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
onBlur,
|
|
||||||
}: SettingsDataModelObjectAboutFormProps) => {
|
}: SettingsDataModelObjectAboutFormProps) => {
|
||||||
const { t } = useLingui();
|
|
||||||
|
|
||||||
const { control, watch, setValue } =
|
const { control, watch, setValue } =
|
||||||
useFormContext<SettingsDataModelObjectAboutFormValues>();
|
useFormContext<SettingsDataModelObjectAboutFormValues>();
|
||||||
|
const { t } = useLingui();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const isLabelSyncedWithName =
|
const isLabelSyncedWithName =
|
||||||
watch(IS_LABEL_SYNCED_WITH_NAME_LABEL) ??
|
watch(IS_LABEL_SYNCED_WITH_NAME_LABEL) ??
|
||||||
(isDefined(objectMetadataItem)
|
(isDefined(objectMetadataItem)
|
||||||
? objectMetadataItem.isLabelSyncedWithName
|
? objectMetadataItem.isLabelSyncedWithName
|
||||||
: true);
|
: SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE);
|
||||||
const labelSingular = watch('labelSingular');
|
const labelSingular = watch('labelSingular');
|
||||||
const labelPlural = watch('labelPlural');
|
const labelPlural = watch('labelPlural');
|
||||||
watch('nameSingular');
|
watch('nameSingular');
|
||||||
@ -117,30 +96,34 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
? t`Deactivate "Synchronize Objects Labels and API Names" to set a custom API name`
|
? t`Deactivate "Synchronize Objects Labels and API Names" to set a custom API name`
|
||||||
: t`Input must be in camel case and cannot start with a number`;
|
: t`Input must be in camel case and cannot start with a number`;
|
||||||
|
|
||||||
const fillLabelPlural = (labelSingular: string) => {
|
const fillLabelPlural = (labelSingular: string | undefined) => {
|
||||||
const newLabelPluralValue = isDefined(labelSingular)
|
if (!isDefined(labelSingular)) return;
|
||||||
? plural(labelSingular)
|
|
||||||
: '';
|
const labelPluralFromSingularLabel = plural(labelSingular);
|
||||||
setValue('labelPlural', newLabelPluralValue, {
|
setValue('labelPlural', labelPluralFromSingularLabel, {
|
||||||
shouldDirty: isDefined(labelSingular) ? true : false,
|
shouldDirty: true,
|
||||||
});
|
});
|
||||||
if (isLabelSyncedWithName === true) {
|
if (isLabelSyncedWithName === true) {
|
||||||
fillNamePluralFromLabelPlural(newLabelPluralValue);
|
fillNamePluralFromLabelPlural(labelPluralFromSingularLabel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillNameSingularFromLabelSingular = (labelSingular: string) => {
|
const fillNameSingularFromLabelSingular = (
|
||||||
isDefined(labelSingular) &&
|
labelSingular: string | undefined,
|
||||||
setValue('nameSingular', computeMetadataNameFromLabel(labelSingular), {
|
) => {
|
||||||
shouldDirty: true,
|
if (!isDefined(labelSingular)) return;
|
||||||
});
|
|
||||||
|
setValue('nameSingular', computeMetadataNameFromLabel(labelSingular), {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillNamePluralFromLabelPlural = (labelPlural: string) => {
|
const fillNamePluralFromLabelPlural = (labelPlural: string | undefined) => {
|
||||||
isDefined(labelPlural) &&
|
if (!isDefined(labelPlural)) return;
|
||||||
setValue('namePlural', computeMetadataNameFromLabel(labelPlural), {
|
|
||||||
shouldDirty: true,
|
setValue('namePlural', computeMetadataNameFromLabel(labelPlural), {
|
||||||
});
|
shouldDirty: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -158,7 +141,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
selectedIconKey={value}
|
selectedIconKey={value}
|
||||||
onChange={({ iconKey }) => {
|
onChange={({ iconKey }) => {
|
||||||
onChange(iconKey);
|
onChange(iconKey);
|
||||||
onBlur?.();
|
onNewDirtyField?.();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -168,9 +151,12 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
key={`object-labelSingular-text-input`}
|
key={`object-labelSingular-text-input`}
|
||||||
name={'labelSingular'}
|
name={'labelSingular'}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={objectMetadataItem?.labelSingular}
|
defaultValue={objectMetadataItem?.labelSingular ?? ''}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value }, formState: { errors } }) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
// TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders
|
||||||
|
noErrorHelper={true}
|
||||||
|
error={errors.labelSingular?.message}
|
||||||
label={t`Singular`}
|
label={t`Singular`}
|
||||||
placeholder={'Listing'}
|
placeholder={'Listing'}
|
||||||
value={value}
|
value={value}
|
||||||
@ -181,7 +167,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
fillNameSingularFromLabelSingular(value);
|
fillNameSingularFromLabelSingular(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={onBlur}
|
onBlur={() => onNewDirtyField?.()}
|
||||||
disabled={disableEdition}
|
disabled={disableEdition}
|
||||||
fullWidth
|
fullWidth
|
||||||
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
@ -192,9 +178,12 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
key={`object-labelPlural-text-input`}
|
key={`object-labelPlural-text-input`}
|
||||||
name={'labelPlural'}
|
name={'labelPlural'}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={objectMetadataItem?.labelPlural}
|
defaultValue={objectMetadataItem?.labelPlural ?? ''}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value }, formState: { errors } }) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
// TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders
|
||||||
|
noErrorHelper={true}
|
||||||
|
error={errors.labelPlural?.message}
|
||||||
label={t`Plural`}
|
label={t`Plural`}
|
||||||
placeholder={t`Listings`}
|
placeholder={t`Listings`}
|
||||||
value={value}
|
value={value}
|
||||||
@ -204,6 +193,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
fillNamePluralFromLabelPlural(value);
|
fillNamePluralFromLabelPlural(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onBlur={() => onNewDirtyField?.()}
|
||||||
disabled={disableEdition}
|
disabled={disableEdition}
|
||||||
fullWidth
|
fullWidth
|
||||||
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
@ -214,7 +204,6 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="description"
|
name="description"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={objectMetadataItem?.description ?? null}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder={t`Write a description`}
|
placeholder={t`Write a description`}
|
||||||
@ -222,7 +211,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
value={value ?? undefined}
|
value={value ?? undefined}
|
||||||
onChange={(nextValue) => onChange(nextValue ?? null)}
|
onChange={(nextValue) => onChange(nextValue ?? null)}
|
||||||
disabled={disableEdition}
|
disabled={disableEdition}
|
||||||
onBlur={onBlur}
|
onBlur={() => onNewDirtyField?.()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -232,28 +221,30 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
label: t`API Name (Singular)`,
|
label: t`API Name (Singular)`,
|
||||||
fieldName: 'nameSingular' as const,
|
fieldName:
|
||||||
|
'nameSingular' as const satisfies StringKeyOf<ObjectMetadataItem>,
|
||||||
placeholder: `listing`,
|
placeholder: `listing`,
|
||||||
defaultValue: objectMetadataItem?.nameSingular,
|
defaultValue: objectMetadataItem?.nameSingular ?? '',
|
||||||
disableEdition: disableEdition || isLabelSyncedWithName,
|
disableEdition: disableEdition || isLabelSyncedWithName,
|
||||||
tooltip: apiNameTooltipText,
|
tooltip: apiNameTooltipText,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t`API Name (Plural)`,
|
label: t`API Name (Plural)`,
|
||||||
fieldName: 'namePlural' as const,
|
fieldName:
|
||||||
|
'namePlural' as const satisfies StringKeyOf<ObjectMetadataItem>,
|
||||||
placeholder: `listings`,
|
placeholder: `listings`,
|
||||||
defaultValue: objectMetadataItem?.namePlural,
|
defaultValue: objectMetadataItem?.namePlural ?? '',
|
||||||
disableEdition: disableEdition || isLabelSyncedWithName,
|
disableEdition: disableEdition || isLabelSyncedWithName,
|
||||||
tooltip: apiNameTooltipText,
|
tooltip: apiNameTooltipText,
|
||||||
},
|
},
|
||||||
].map(
|
].map(
|
||||||
({
|
({
|
||||||
defaultValue,
|
|
||||||
fieldName,
|
fieldName,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
disableEdition,
|
disableEdition,
|
||||||
tooltip,
|
tooltip,
|
||||||
|
defaultValue,
|
||||||
}) => (
|
}) => (
|
||||||
<AdvancedSettingsWrapper key={`object-${fieldName}-text-input`}>
|
<AdvancedSettingsWrapper key={`object-${fieldName}-text-input`}>
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
@ -261,7 +252,10 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
name={fieldName}
|
name={fieldName}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
formState: { errors },
|
||||||
|
}) => (
|
||||||
<>
|
<>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={label}
|
label={label}
|
||||||
@ -271,7 +265,10 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
disabled={disableEdition}
|
disabled={disableEdition}
|
||||||
fullWidth
|
fullWidth
|
||||||
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
onBlur={onBlur}
|
onBlur={() => onNewDirtyField?.()}
|
||||||
|
error={errors[fieldName]?.message}
|
||||||
|
// TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders
|
||||||
|
noErrorHelper={true}
|
||||||
RightIcon={() =>
|
RightIcon={() =>
|
||||||
tooltip && (
|
tooltip && (
|
||||||
<>
|
<>
|
||||||
@ -305,7 +302,10 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name={IS_LABEL_SYNCED_WITH_NAME_LABEL}
|
name={IS_LABEL_SYNCED_WITH_NAME_LABEL}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={objectMetadataItem?.isLabelSyncedWithName ?? true}
|
defaultValue={
|
||||||
|
objectMetadataItem?.isLabelSyncedWithName ??
|
||||||
|
SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE
|
||||||
|
}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Card rounded>
|
<Card rounded>
|
||||||
<SettingsOptionCardContentToggle
|
<SettingsOptionCardContentToggle
|
||||||
@ -324,7 +324,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
fillNamePluralFromLabelPlural(labelPlural);
|
fillNamePluralFromLabelPlural(labelPlural);
|
||||||
fillNameSingularFromLabelSingular(labelSingular);
|
fillNameSingularFromLabelSingular(labelSingular);
|
||||||
}
|
}
|
||||||
onBlur?.();
|
onNewDirtyField?.();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { IconCircleOff, useIcons } from 'twenty-ui';
|
import { IconCircleOff, useIcons } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { ZodError, isDirty, z } from 'zod';
|
||||||
|
|
||||||
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
|
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
|
||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems';
|
import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems';
|
||||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
|
|
||||||
export const settingsDataModelObjectIdentifiersFormSchema =
|
export const settingsDataModelObjectIdentifiersFormSchema =
|
||||||
@ -24,7 +28,6 @@ export type SettingsDataModelObjectIdentifiers =
|
|||||||
keyof SettingsDataModelObjectIdentifiersFormValues;
|
keyof SettingsDataModelObjectIdentifiersFormValues;
|
||||||
type SettingsDataModelObjectIdentifiersFormProps = {
|
type SettingsDataModelObjectIdentifiersFormProps = {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
onBlur: () => void;
|
|
||||||
};
|
};
|
||||||
const LABEL_IDENTIFIER_FIELD_METADATA_ID: SettingsDataModelObjectIdentifiers =
|
const LABEL_IDENTIFIER_FIELD_METADATA_ID: SettingsDataModelObjectIdentifiers =
|
||||||
'labelIdentifierFieldMetadataId';
|
'labelIdentifierFieldMetadataId';
|
||||||
@ -38,10 +41,41 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
export const SettingsDataModelObjectIdentifiersForm = ({
|
export const SettingsDataModelObjectIdentifiersForm = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
onBlur,
|
|
||||||
}: SettingsDataModelObjectIdentifiersFormProps) => {
|
}: SettingsDataModelObjectIdentifiersFormProps) => {
|
||||||
const { control } =
|
const formConfig = useForm<SettingsDataModelObjectIdentifiersFormValues>({
|
||||||
useFormContext<SettingsDataModelObjectIdentifiersFormValues>();
|
mode: 'onTouched',
|
||||||
|
resolver: zodResolver(settingsDataModelObjectIdentifiersFormSchema),
|
||||||
|
});
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
|
||||||
|
const handleSave = async (
|
||||||
|
formValues: SettingsDataModelObjectIdentifiersFormValues,
|
||||||
|
) => {
|
||||||
|
if (!isDirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateOneObjectMetadataItem({
|
||||||
|
idToUpdate: objectMetadataItem.id,
|
||||||
|
updatePayload: formValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
formConfig.reset(undefined, { keepValues: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
enqueueSnackBar(error.issues[0].message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
enqueueSnackBar((error as Error).message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const labelIdentifierFieldOptions = useMemo(
|
const labelIdentifierFieldOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -84,7 +118,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
name={fieldName}
|
name={fieldName}
|
||||||
control={control}
|
control={formConfig.control}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Select
|
<Select
|
||||||
@ -97,7 +131,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onChange(value);
|
onChange(value);
|
||||||
onBlur();
|
formConfig.handleSubmit(handleSave)();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { Card, CardContent } from 'twenty-ui';
|
|||||||
|
|
||||||
type SettingsDataModelObjectSettingsFormCardProps = {
|
type SettingsDataModelObjectSettingsFormCardProps = {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
onBlur: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||||
@ -35,7 +34,6 @@ const StyledObjectSummaryCardContent = styled(CardContent)`
|
|||||||
|
|
||||||
export const SettingsDataModelObjectSettingsFormCard = ({
|
export const SettingsDataModelObjectSettingsFormCard = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
onBlur,
|
|
||||||
}: SettingsDataModelObjectSettingsFormCardProps) => {
|
}: SettingsDataModelObjectSettingsFormCardProps) => {
|
||||||
const labelIdentifierFieldMetadataItem = useMemo(() => {
|
const labelIdentifierFieldMetadataItem = useMemo(() => {
|
||||||
return getLabelIdentifierFieldMetadataItem({
|
return getLabelIdentifierFieldMetadataItem({
|
||||||
@ -70,7 +68,6 @@ export const SettingsDataModelObjectSettingsFormCard = ({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<SettingsDataModelObjectIdentifiersForm
|
<SettingsDataModelObjectIdentifiersForm
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -0,0 +1,159 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`settingsDataModelObjectAboutFormSchema fails when isLabelSyncedWithName is not a boolean 1`] = `
|
||||||
|
[ZodError: [
|
||||||
|
{
|
||||||
|
"code": "invalid_type",
|
||||||
|
"expected": "boolean",
|
||||||
|
"received": "string",
|
||||||
|
"path": [
|
||||||
|
"isLabelSyncedWithName"
|
||||||
|
],
|
||||||
|
"message": "Expected boolean, received string"
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`settingsDataModelObjectAboutFormSchema fails when labels are empty strings 1`] = `
|
||||||
|
[ZodError: [
|
||||||
|
{
|
||||||
|
"code": "too_small",
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "string",
|
||||||
|
"inclusive": true,
|
||||||
|
"exact": false,
|
||||||
|
"message": "String must contain at least 1 character(s)",
|
||||||
|
"path": [
|
||||||
|
"labelSingular"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "too_small",
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "string",
|
||||||
|
"inclusive": true,
|
||||||
|
"exact": false,
|
||||||
|
"message": "String must contain at least 1 character(s)",
|
||||||
|
"path": [
|
||||||
|
"labelPlural"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "Singular and plural labels must be different",
|
||||||
|
"path": [
|
||||||
|
"labelPlural"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "Singular and plural labels must be different",
|
||||||
|
"path": [
|
||||||
|
"labelSingular"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`settingsDataModelObjectAboutFormSchema fails when names are not in camelCase 1`] = `
|
||||||
|
[ZodError: [
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "String should be camel case",
|
||||||
|
"path": [
|
||||||
|
"namePlural"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "String should be camel case",
|
||||||
|
"path": [
|
||||||
|
"nameSingular"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`settingsDataModelObjectAboutFormSchema fails when required fields are missing 1`] = `
|
||||||
|
[ZodError: [
|
||||||
|
{
|
||||||
|
"code": "invalid_type",
|
||||||
|
"expected": "string",
|
||||||
|
"received": "undefined",
|
||||||
|
"path": [
|
||||||
|
"labelSingular"
|
||||||
|
],
|
||||||
|
"message": "Required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "invalid_type",
|
||||||
|
"expected": "string",
|
||||||
|
"received": "undefined",
|
||||||
|
"path": [
|
||||||
|
"labelPlural"
|
||||||
|
],
|
||||||
|
"message": "Required"
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`settingsDataModelObjectAboutFormSchema fails when singular and plural labels are the same 1`] = `
|
||||||
|
[ZodError: [
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "Singular and plural labels must be different",
|
||||||
|
"path": [
|
||||||
|
"labelPlural"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "Singular and plural labels must be different",
|
||||||
|
"path": [
|
||||||
|
"labelSingular"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`settingsDataModelObjectAboutFormSchema fails when singular and plural names are the same 1`] = `
|
||||||
|
[ZodError: [
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "Singular and plural names must be different",
|
||||||
|
"path": [
|
||||||
|
"nameSingular"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "custom",
|
||||||
|
"message": "Singular and plural names must be different",
|
||||||
|
"path": [
|
||||||
|
"namePlural"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`settingsDataModelObjectAboutFormSchema fails with invalid types for optional fields 1`] = `
|
||||||
|
[ZodError: [
|
||||||
|
{
|
||||||
|
"code": "invalid_type",
|
||||||
|
"expected": "string",
|
||||||
|
"received": "number",
|
||||||
|
"path": [
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"message": "Expected string, received number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "invalid_type",
|
||||||
|
"expected": "string",
|
||||||
|
"received": "boolean",
|
||||||
|
"path": [
|
||||||
|
"icon"
|
||||||
|
],
|
||||||
|
"message": "Expected string, received boolean"
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
`;
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { SafeParseSuccess } from 'zod';
|
|
||||||
|
|
||||||
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
import { settingsCreateObjectInputSchema } from '../settingsCreateObjectInputSchema';
|
|
||||||
|
|
||||||
describe('settingsCreateObjectInputSchema', () => {
|
|
||||||
it('validates a valid input and adds name properties', () => {
|
|
||||||
// Given
|
|
||||||
const validInput = {
|
|
||||||
description: 'A valid description',
|
|
||||||
icon: 'IconPlus',
|
|
||||||
labelPlural: ' Labels ',
|
|
||||||
labelSingular: 'Label ',
|
|
||||||
namePlural: 'namePlural',
|
|
||||||
nameSingular: 'nameSingular',
|
|
||||||
isLabelSyncedWithName: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// When
|
|
||||||
const result = settingsCreateObjectInputSchema.safeParse(validInput);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect((result as SafeParseSuccess<CreateObjectInput>).data).toEqual({
|
|
||||||
description: validInput.description,
|
|
||||||
icon: validInput.icon,
|
|
||||||
labelPlural: 'Labels',
|
|
||||||
labelSingular: 'Label',
|
|
||||||
namePlural: 'namePlural',
|
|
||||||
nameSingular: 'nameSingular',
|
|
||||||
isLabelSyncedWithName: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails for an invalid input', () => {
|
|
||||||
// Given
|
|
||||||
const invalidInput = {
|
|
||||||
description: 123,
|
|
||||||
icon: true,
|
|
||||||
labelPlural: [],
|
|
||||||
labelSingular: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
// When
|
|
||||||
const result = settingsCreateObjectInputSchema.safeParse(invalidInput);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
import {
|
||||||
|
SettingsDataModelObjectAboutFormValues,
|
||||||
|
settingsDataModelObjectAboutFormSchema,
|
||||||
|
} from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema';
|
||||||
|
import { EachTestingContext } from '~/types/EachTestingContext';
|
||||||
|
|
||||||
|
describe('settingsDataModelObjectAboutFormSchema', () => {
|
||||||
|
const validInput: SettingsDataModelObjectAboutFormValues = {
|
||||||
|
description: 'A valid description',
|
||||||
|
icon: 'IconName',
|
||||||
|
labelPlural: 'Labels Plural',
|
||||||
|
labelSingular: 'Label Singular',
|
||||||
|
namePlural: 'labelsPlural',
|
||||||
|
nameSingular: 'labelSingular',
|
||||||
|
isLabelSyncedWithName: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const passingTestsUseCase: EachTestingContext<{
|
||||||
|
input: SettingsDataModelObjectAboutFormValues;
|
||||||
|
expectedSuccess: true;
|
||||||
|
}>[] = [
|
||||||
|
{
|
||||||
|
title: 'validates a complete valid input',
|
||||||
|
context: {
|
||||||
|
input: validInput,
|
||||||
|
expectedSuccess: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'validates input with optional fields omitted',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
labelPlural: 'Labels Plural',
|
||||||
|
labelSingular: 'Label Singular',
|
||||||
|
namePlural: 'labelsPlural',
|
||||||
|
nameSingular: 'labelSingular',
|
||||||
|
isLabelSyncedWithName: false,
|
||||||
|
},
|
||||||
|
expectedSuccess: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'validates input with trimmed labels',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
...validInput,
|
||||||
|
labelPlural: ' Labels Plural ',
|
||||||
|
labelSingular: ' Label Singular ',
|
||||||
|
},
|
||||||
|
expectedSuccess: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const failsValidationTestsUseCase: EachTestingContext<{
|
||||||
|
input: Partial<Record<keyof SettingsDataModelObjectAboutFormValues, any>>;
|
||||||
|
expectedSuccess: false;
|
||||||
|
}>[] = [
|
||||||
|
{
|
||||||
|
title: 'fails when required fields are missing',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
description: 'Only description',
|
||||||
|
labelPlural: undefined,
|
||||||
|
labelSingular: undefined,
|
||||||
|
},
|
||||||
|
expectedSuccess: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'fails when names are not in camelCase',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
...validInput,
|
||||||
|
namePlural: 'Labels_Plural',
|
||||||
|
nameSingular: 'Label-Singular',
|
||||||
|
},
|
||||||
|
expectedSuccess: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'fails when labels are empty strings',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
...validInput,
|
||||||
|
labelPlural: '',
|
||||||
|
labelSingular: '',
|
||||||
|
},
|
||||||
|
expectedSuccess: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'fails when singular and plural labels are the same',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
...validInput,
|
||||||
|
labelPlural: 'Same Label',
|
||||||
|
labelSingular: 'Same Label',
|
||||||
|
},
|
||||||
|
expectedSuccess: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'fails when singular and plural names are the same',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
...validInput,
|
||||||
|
namePlural: 'sameName',
|
||||||
|
nameSingular: 'sameName',
|
||||||
|
},
|
||||||
|
expectedSuccess: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'fails when isLabelSyncedWithName is not a boolean',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
...validInput,
|
||||||
|
isLabelSyncedWithName: 'true',
|
||||||
|
},
|
||||||
|
expectedSuccess: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'fails with invalid types for optional fields',
|
||||||
|
context: {
|
||||||
|
input: {
|
||||||
|
...validInput,
|
||||||
|
description: 123,
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
|
expectedSuccess: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each([...passingTestsUseCase, ...failsValidationTestsUseCase])(
|
||||||
|
'$title',
|
||||||
|
({ context: { expectedSuccess, input } }) => {
|
||||||
|
const result = settingsDataModelObjectAboutFormSchema.safeParse({
|
||||||
|
...validInput,
|
||||||
|
...input,
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(expectedSuccess);
|
||||||
|
if (!expectedSuccess) {
|
||||||
|
expect(result.error).toMatchSnapshot();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { SafeParseSuccess } from 'zod';
|
|
||||||
|
|
||||||
import { UpdateObjectPayload } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
import { settingsUpdateObjectInputSchema } from '../settingsUpdateObjectInputSchema';
|
|
||||||
|
|
||||||
describe('settingsUpdateObjectInputSchema', () => {
|
|
||||||
it('validates a valid input and adds name properties', () => {
|
|
||||||
// Given
|
|
||||||
const validInput = {
|
|
||||||
description: 'A valid description',
|
|
||||||
icon: 'IconName',
|
|
||||||
labelPlural: 'Labels Plural ',
|
|
||||||
labelSingular: ' Label Singular',
|
|
||||||
namePlural: 'namePlural',
|
|
||||||
nameSingular: 'nameSingular',
|
|
||||||
labelIdentifierFieldMetadataId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
|
||||||
};
|
|
||||||
|
|
||||||
// When
|
|
||||||
const result = settingsUpdateObjectInputSchema.safeParse(validInput);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect((result as SafeParseSuccess<UpdateObjectPayload>).data).toEqual({
|
|
||||||
description: validInput.description,
|
|
||||||
icon: validInput.icon,
|
|
||||||
labelIdentifierFieldMetadataId: validInput.labelIdentifierFieldMetadataId,
|
|
||||||
labelPlural: 'Labels Plural',
|
|
||||||
labelSingular: 'Label Singular',
|
|
||||||
namePlural: 'namePlural',
|
|
||||||
nameSingular: 'nameSingular',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails for an invalid input', () => {
|
|
||||||
// Given
|
|
||||||
const invalidInput = {
|
|
||||||
description: 123,
|
|
||||||
icon: true,
|
|
||||||
labelPlural: [],
|
|
||||||
labelSingular: {},
|
|
||||||
labelIdentifierFieldMetadataId: 'invalid uuid',
|
|
||||||
};
|
|
||||||
|
|
||||||
// When
|
|
||||||
const result = settingsUpdateObjectInputSchema.safeParse(invalidInput);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
|
||||||
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
|
||||||
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
|
||||||
|
|
||||||
export const settingsCreateObjectInputSchema =
|
|
||||||
settingsDataModelObjectAboutFormSchema.transform<CreateObjectInput>(
|
|
||||||
(values) => ({
|
|
||||||
...values,
|
|
||||||
nameSingular:
|
|
||||||
values.nameSingular ??
|
|
||||||
computeMetadataNameFromLabel(values.labelSingular),
|
|
||||||
namePlural:
|
|
||||||
values.namePlural ?? computeMetadataNameFromLabel(values.labelPlural),
|
|
||||||
isLabelSyncedWithName: values.isLabelSyncedWithName ?? true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { ZodType, z } from 'zod';
|
||||||
|
import { ReadonlyKeysArray } from '~/types/ReadonlyKeysArray';
|
||||||
|
import { zodNonEmptyString } from '~/types/ZodNonEmptyString';
|
||||||
|
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
|
||||||
|
|
||||||
|
type ZodTypeSettingsDataModelFormFields = ZodType<
|
||||||
|
Pick<
|
||||||
|
ObjectMetadataItem,
|
||||||
|
| 'labelSingular'
|
||||||
|
| 'labelPlural'
|
||||||
|
| 'description'
|
||||||
|
| 'icon'
|
||||||
|
| 'namePlural'
|
||||||
|
| 'nameSingular'
|
||||||
|
| 'isLabelSyncedWithName'
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
const settingsDataModelFormFieldsSchema = z.object({
|
||||||
|
description: z.string().nullish(),
|
||||||
|
icon: z.string().optional(),
|
||||||
|
labelSingular: zodNonEmptyString,
|
||||||
|
labelPlural: zodNonEmptyString,
|
||||||
|
namePlural: zodNonEmptyString.and(camelCaseStringSchema),
|
||||||
|
nameSingular: zodNonEmptyString.and(camelCaseStringSchema),
|
||||||
|
isLabelSyncedWithName: z.boolean(),
|
||||||
|
}) satisfies ZodTypeSettingsDataModelFormFields;
|
||||||
|
|
||||||
|
export const settingsDataModelObjectAboutFormSchema =
|
||||||
|
settingsDataModelFormFieldsSchema.superRefine(
|
||||||
|
({ labelPlural, labelSingular, namePlural, nameSingular }, ctx) => {
|
||||||
|
const labelsAreDifferent =
|
||||||
|
labelPlural.trim().toLowerCase() !== labelSingular.trim().toLowerCase();
|
||||||
|
if (!labelsAreDifferent) {
|
||||||
|
const labelFields: ReadonlyKeysArray<ObjectMetadataItem> = [
|
||||||
|
'labelPlural',
|
||||||
|
'labelSingular',
|
||||||
|
];
|
||||||
|
labelFields.forEach((field) =>
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t`Singular and plural labels must be different`,
|
||||||
|
path: [field],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameAreDifferent =
|
||||||
|
nameSingular.toLowerCase() !== namePlural.toLowerCase();
|
||||||
|
if (!nameAreDifferent) {
|
||||||
|
const nameFields: ReadonlyKeysArray<ObjectMetadataItem> = [
|
||||||
|
'nameSingular',
|
||||||
|
'namePlural',
|
||||||
|
];
|
||||||
|
nameFields.forEach((field) =>
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t`Singular and plural names must be different`,
|
||||||
|
path: [field],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
export type SettingsDataModelObjectAboutFormValues = z.infer<
|
||||||
|
typeof settingsDataModelObjectAboutFormSchema
|
||||||
|
>;
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
|
|
||||||
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
|
||||||
|
|
||||||
export const settingsUpdateObjectInputSchema =
|
|
||||||
settingsDataModelObjectAboutFormSchema
|
|
||||||
.merge(
|
|
||||||
objectMetadataItemSchema.pick({
|
|
||||||
imageIdentifierFieldMetadataId: true,
|
|
||||||
isActive: true,
|
|
||||||
labelIdentifierFieldMetadataId: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.partial();
|
|
||||||
@ -1,16 +1,16 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { H2Title, Section } from 'twenty-ui';
|
import { H2Title, Section } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
|
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE } from '@/settings/constants/SettingsObjectModel';
|
||||||
|
import { SettingsDataModelObjectAboutForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||||
import {
|
import {
|
||||||
SettingsDataModelObjectAboutForm,
|
SettingsDataModelObjectAboutFormValues,
|
||||||
settingsDataModelObjectAboutFormSchema,
|
settingsDataModelObjectAboutFormSchema,
|
||||||
} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
} from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema';
|
||||||
import { settingsCreateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsCreateObjectInputSchema';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
@ -19,10 +19,6 @@ import { useLingui } from '@lingui/react/macro';
|
|||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
const newObjectFormSchema = settingsDataModelObjectAboutFormSchema;
|
|
||||||
|
|
||||||
type SettingsDataModelNewObjectFormValues = z.infer<typeof newObjectFormSchema>;
|
|
||||||
|
|
||||||
export const SettingsNewObject = () => {
|
export const SettingsNewObject = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const navigate = useNavigateSettings();
|
const navigate = useNavigateSettings();
|
||||||
@ -30,21 +26,23 @@ export const SettingsNewObject = () => {
|
|||||||
|
|
||||||
const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem();
|
const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem();
|
||||||
|
|
||||||
const formConfig = useForm<SettingsDataModelNewObjectFormValues>({
|
const formConfig = useForm<SettingsDataModelObjectAboutFormValues>({
|
||||||
mode: 'onTouched',
|
mode: 'onSubmit',
|
||||||
resolver: zodResolver(newObjectFormSchema),
|
resolver: zodResolver(settingsDataModelObjectAboutFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
isLabelSyncedWithName:
|
||||||
|
SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isValid, isSubmitting } = formConfig.formState;
|
const { isValid, isSubmitting } = formConfig.formState;
|
||||||
const canSave = isValid && !isSubmitting;
|
const canSave = isValid && !isSubmitting;
|
||||||
|
|
||||||
const handleSave = async (
|
const handleSave = async (
|
||||||
formValues: SettingsDataModelNewObjectFormValues,
|
formValues: SettingsDataModelObjectAboutFormValues,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { data: response } = await createOneObjectMetadataItem(
|
const { data: response } = await createOneObjectMetadataItem(formValues);
|
||||||
settingsCreateObjectInputSchema.parse(formValues),
|
|
||||||
);
|
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
response ? SettingsPath.ObjectDetail : SettingsPath.Objects,
|
response ? SettingsPath.ObjectDetail : SettingsPath.Objects,
|
||||||
@ -53,6 +51,8 @@ export const SettingsNewObject = () => {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
enqueueSnackBar((error as Error).message, {
|
enqueueSnackBar((error as Error).message, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
@ -90,7 +90,9 @@ export const SettingsNewObject = () => {
|
|||||||
title={t`About`}
|
title={t`About`}
|
||||||
description={t`Define the name and description of your object`}
|
description={t`Define the name and description of your object`}
|
||||||
/>
|
/>
|
||||||
<SettingsDataModelObjectAboutForm />
|
<SettingsDataModelObjectAboutForm
|
||||||
|
onNewDirtyField={() => formConfig.trigger()}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
|
|||||||
1
packages/twenty-front/src/types/ReadonlyKeysArray.ts
Normal file
1
packages/twenty-front/src/types/ReadonlyKeysArray.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type ReadonlyKeysArray<T> = readonly (keyof T)[];
|
||||||
3
packages/twenty-front/src/types/ZodNonEmptyString.ts
Normal file
3
packages/twenty-front/src/types/ZodNonEmptyString.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const zodNonEmptyString = z.string().min(1);
|
||||||
Reference in New Issue
Block a user