diff --git a/packages/twenty-front/src/modules/settings/constants/SettingsObjectModel.ts b/packages/twenty-front/src/modules/settings/constants/SettingsObjectModel.ts new file mode 100644 index 000000000..ba4fc3e12 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/constants/SettingsObjectModel.ts @@ -0,0 +1,2 @@ +export const SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE = + true; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx index 442128035..75ddcbb15 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx @@ -11,8 +11,8 @@ import { import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { t } from '@lingui/core/macro'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; type SettingsObjectFieldInactiveActionDropdownProps = { isCustomField?: boolean; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx new file mode 100644 index 000000000..9a4d68813 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx @@ -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({ + 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 + + formConfig.handleSubmit(handleSave)()} + disableEdition={!objectMetadataItem.isCustom} + objectMetadataItem={objectMetadataItem} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx index 0a78b2806..944ef218b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx @@ -1,39 +1,14 @@ -/* eslint-disable react/jsx-props-no-spreading */ 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 { ZodError, z } from 'zod'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; -import { - 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 { SettingsUpdateDataModelObjectAboutForm } from '@/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm'; 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 { 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 { useLingui } from '@lingui/react/macro'; -import pick from 'lodash.pick'; -import { useSetRecoilState } from 'recoil'; 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 = { objectMetadataItem: ObjectMetadataItem; @@ -52,101 +27,7 @@ const StyledFormSection = styled(Section)` export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { const { t } = useLingui(); const navigate = useNavigateSettings(); - const { enqueueSnackBar } = useSnackBar(); - const setUpdatedObjectNamePlural = useSetRecoilState( - updatedObjectNamePluralState, - ); - const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); - - const formConfig = useForm({ - 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 () => { await updateOneObjectMetadataItem({ idToUpdate: objectMetadataItem.id, @@ -157,49 +38,42 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { return ( - - - + + + + + + +
- { - formConfig.handleSubmit(handleSave)(); - }} /> - - -
- - formConfig.handleSubmit(handleSave)()} - objectMetadataItem={objectMetadataItem} - /> -
-
- -
- -
-
- - +
+
+ +
+ +
+
+
); }; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx index 3aa8aad43..b053eb5a5 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx @@ -1,8 +1,9 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper'; 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 { SettingsDataModelObjectAboutFormValues } from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { TextArea } from '@/ui/input/components/TextArea'; import { TextInput } from '@/ui/input/components/TextInput'; @@ -19,34 +20,13 @@ import { IconRefresh, TooltipDelay, } 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'; -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 = { disableEdition?: boolean; objectMetadataItem?: ObjectMetadataItem; - onBlur?: () => void; + onNewDirtyField?: () => void; }; const StyledInputsContainer = styled.div` @@ -92,21 +72,20 @@ const infoCircleElementId = 'info-circle-id'; export const IS_LABEL_SYNCED_WITH_NAME_LABEL = 'isLabelSyncedWithName'; export const SettingsDataModelObjectAboutForm = ({ - disableEdition, + disableEdition = false, + onNewDirtyField, objectMetadataItem, - onBlur, }: SettingsDataModelObjectAboutFormProps) => { - const { t } = useLingui(); - const { control, watch, setValue } = useFormContext(); + const { t } = useLingui(); const theme = useTheme(); const isLabelSyncedWithName = watch(IS_LABEL_SYNCED_WITH_NAME_LABEL) ?? (isDefined(objectMetadataItem) ? objectMetadataItem.isLabelSyncedWithName - : true); + : SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE); const labelSingular = watch('labelSingular'); const labelPlural = watch('labelPlural'); watch('nameSingular'); @@ -117,30 +96,34 @@ export const SettingsDataModelObjectAboutForm = ({ ? 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`; - const fillLabelPlural = (labelSingular: string) => { - const newLabelPluralValue = isDefined(labelSingular) - ? plural(labelSingular) - : ''; - setValue('labelPlural', newLabelPluralValue, { - shouldDirty: isDefined(labelSingular) ? true : false, + const fillLabelPlural = (labelSingular: string | undefined) => { + if (!isDefined(labelSingular)) return; + + const labelPluralFromSingularLabel = plural(labelSingular); + setValue('labelPlural', labelPluralFromSingularLabel, { + shouldDirty: true, }); if (isLabelSyncedWithName === true) { - fillNamePluralFromLabelPlural(newLabelPluralValue); + fillNamePluralFromLabelPlural(labelPluralFromSingularLabel); } }; - const fillNameSingularFromLabelSingular = (labelSingular: string) => { - isDefined(labelSingular) && - setValue('nameSingular', computeMetadataNameFromLabel(labelSingular), { - shouldDirty: true, - }); + const fillNameSingularFromLabelSingular = ( + labelSingular: string | undefined, + ) => { + if (!isDefined(labelSingular)) return; + + setValue('nameSingular', computeMetadataNameFromLabel(labelSingular), { + shouldDirty: true, + }); }; - const fillNamePluralFromLabelPlural = (labelPlural: string) => { - isDefined(labelPlural) && - setValue('namePlural', computeMetadataNameFromLabel(labelPlural), { - shouldDirty: true, - }); + const fillNamePluralFromLabelPlural = (labelPlural: string | undefined) => { + if (!isDefined(labelPlural)) return; + + setValue('namePlural', computeMetadataNameFromLabel(labelPlural), { + shouldDirty: true, + }); }; return ( @@ -158,7 +141,7 @@ export const SettingsDataModelObjectAboutForm = ({ selectedIconKey={value} onChange={({ iconKey }) => { onChange(iconKey); - onBlur?.(); + onNewDirtyField?.(); }} /> )} @@ -168,9 +151,12 @@ export const SettingsDataModelObjectAboutForm = ({ key={`object-labelSingular-text-input`} name={'labelSingular'} control={control} - defaultValue={objectMetadataItem?.labelSingular} - render={({ field: { onChange, value } }) => ( + defaultValue={objectMetadataItem?.labelSingular ?? ''} + render={({ field: { onChange, value }, formState: { errors } }) => ( onNewDirtyField?.()} disabled={disableEdition} fullWidth maxLength={OBJECT_NAME_MAXIMUM_LENGTH} @@ -192,9 +178,12 @@ export const SettingsDataModelObjectAboutForm = ({ key={`object-labelPlural-text-input`} name={'labelPlural'} control={control} - defaultValue={objectMetadataItem?.labelPlural} - render={({ field: { onChange, value } }) => ( + defaultValue={objectMetadataItem?.labelPlural ?? ''} + render={({ field: { onChange, value }, formState: { errors } }) => ( onNewDirtyField?.()} disabled={disableEdition} fullWidth maxLength={OBJECT_NAME_MAXIMUM_LENGTH} @@ -214,7 +204,6 @@ export const SettingsDataModelObjectAboutForm = ({ (