[BUG] Record settings not saved (#9762)

# Introduction
By initially fixing this Fixes #9381, discovered other behavior that
have been fix.
Overall we encountered a bug that corrupts a workspace and make the
browser + api crash
This issue https://github.com/twentyhq/core-team-issues/issues/25
suggests a refactor that has final save button instead of auto-save

## `labelIdentifierFieldMetadataId` form default value
The default value resulted in being undefined, resulting in react hook
form `labelIdentifierFieldMetadataId` is required field error.

### Fix
Setting default value fallback to `null`  as field is `nullable`

## `SettingsDataModelObjectSettingsFormCard` never triggers form
Unless I'm mistaken in production touching any fields within
`SettingsDataModelObjectSettingsFormCard` would never trigger form
submission until you also modify `SettingsDataModelObjectAboutForm`
fields

### Fix
Provide and apply `onblur` that triggers the form on both
`SettingsDataModelObjectSettingsFormCard` inputs

## Wrong default `labelIdentifierFieldMetadataItem` on first page render
When landing on the page for the first time, if a custom
`labelIdentifierFieldMetadataItem` has been set it won't be computed
within the `PreviewCard`.
Occurs when `labelIdentifierFieldMetadataId` form default value is
undefined, due to `any` injection.

### Fix
In the `getLabelIdentifierFieldMetadataItem` check the
`labelIdentifierFieldMetadataIdFormValue` definition, if undefined
fallback to current `objectMetadata` identifier

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Paul Rastoin
2025-01-22 16:32:57 +01:00
committed by GitHub
parent 80c9ebfd4e
commit 8ab01ebef4
22 changed files with 161 additions and 89 deletions

View File

@ -28,6 +28,7 @@ export const useFieldPreviewValue = ({
relationObjectMetadataItem: relationObjectMetadataItem ?? {
fields: [],
labelSingular: '',
labelIdentifierFieldMetadataId: '20202020-1000-4629-87e5-9a1fae1cc2fd',
nameSingular: CoreObjectNameSingular.Company,
},
skip:

View File

@ -23,7 +23,6 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import styled from '@emotion/styled';
import isEmpty from 'lodash.isempty';
import pick from 'lodash.pick';
import { useSetRecoilState } from 'recoil';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@ -70,6 +69,7 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
mode: 'onTouched',
resolver: zodResolver(objectEditFormSchema),
});
const { isDirty } = formConfig.formState;
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
@ -124,7 +124,7 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
const handleSave = async (
formValues: SettingsDataModelObjectEditFormValues,
) => {
if (isEmpty(formConfig.formState.dirtyFields) === true) {
if (!isDirty) {
return;
}
try {
@ -202,6 +202,7 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
description="Choose the fields that will identify your records"
/>
<SettingsDataModelObjectSettingsFormCard
onBlur={() => formConfig.handleSubmit(handleSave)()}
objectMetadataItem={objectMetadataItem}
/>
</Section>

View File

@ -219,6 +219,7 @@ export const SettingsDataModelObjectAboutForm = ({
value={value ?? undefined}
onChange={(nextValue) => onChange(nextValue ?? null)}
disabled={disableEdition}
onBlur={onBlur}
/>
)}
/>

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { IconCircleOff, isDefined, useIcons } from 'twenty-ui';
import { IconCircleOff, useIcons } from 'twenty-ui';
import { z } from 'zod';
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
@ -19,11 +19,16 @@ export const settingsDataModelObjectIdentifiersFormSchema =
export type SettingsDataModelObjectIdentifiersFormValues = z.infer<
typeof settingsDataModelObjectIdentifiersFormSchema
>;
export type SettingsDataModelObjectIdentifiers =
keyof SettingsDataModelObjectIdentifiersFormValues;
type SettingsDataModelObjectIdentifiersFormProps = {
objectMetadataItem: ObjectMetadataItem;
defaultLabelIdentifierFieldMetadataId: string;
onBlur: () => void;
};
const LABEL_IDENTIFIER_FIELD_METADATA_ID: SettingsDataModelObjectIdentifiers =
'labelIdentifierFieldMetadataId';
const IMAGE_IDENTIFIER_FIELD_METADATA_ID: SettingsDataModelObjectIdentifiers =
'imageIdentifierFieldMetadataId';
const StyledContainer = styled.div`
display: flex;
@ -32,12 +37,11 @@ const StyledContainer = styled.div`
export const SettingsDataModelObjectIdentifiersForm = ({
objectMetadataItem,
defaultLabelIdentifierFieldMetadataId,
onBlur,
}: SettingsDataModelObjectIdentifiersFormProps) => {
const { control } =
useFormContext<SettingsDataModelObjectIdentifiersFormValues>();
const { getIcon } = useIcons();
const labelIdentifierFieldOptions = useMemo(
() =>
getActiveFieldMetadataItems(objectMetadataItem)
@ -65,41 +69,37 @@ export const SettingsDataModelObjectIdentifiersForm = ({
{[
{
label: 'Record label',
fieldName: 'labelIdentifierFieldMetadataId' as const,
fieldName: LABEL_IDENTIFIER_FIELD_METADATA_ID,
options: labelIdentifierFieldOptions,
defaultValue: objectMetadataItem.labelIdentifierFieldMetadataId,
},
{
label: 'Record image',
fieldName: 'imageIdentifierFieldMetadataId' as const,
fieldName: IMAGE_IDENTIFIER_FIELD_METADATA_ID,
options: imageIdentifierFieldOptions,
defaultValue: null,
},
].map(({ fieldName, label, options }) => (
].map(({ fieldName, label, options, defaultValue }) => (
<Controller
key={fieldName}
name={fieldName}
control={control}
defaultValue={
fieldName === 'labelIdentifierFieldMetadataId'
? isDefined(objectMetadataItem[fieldName])
? objectMetadataItem[fieldName]
: defaultLabelIdentifierFieldMetadataId
: objectMetadataItem[fieldName]
}
render={({ field: { onBlur, onChange, value } }) => {
return (
<Select
label={label}
disabled={!objectMetadataItem.isCustom || !options.length}
fullWidth
dropdownId={`${fieldName}-select`}
emptyOption={emptyOption}
options={options}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
}}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => (
<Select
label={label}
disabled={!objectMetadataItem.isCustom || !options.length}
fullWidth
dropdownId={`${fieldName}-select`}
emptyOption={emptyOption}
options={options}
value={value}
onChange={(value) => {
onChange(value);
onBlur();
}}
/>
)}
/>
))}
</StyledContainer>

View File

@ -1,21 +1,18 @@
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { SettingsDataModelCardTitle } from '@/settings/data-model/components/SettingsDataModelCardTitle';
import { SettingsDataModelFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary';
import {
SettingsDataModelObjectIdentifiersForm,
SettingsDataModelObjectIdentifiersFormValues,
} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
import { SettingsDataModelObjectIdentifiersForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
import { Trans } from '@lingui/react/macro';
import { Card, CardContent } from 'twenty-ui';
type SettingsDataModelObjectSettingsFormCardProps = {
objectMetadataItem: ObjectMetadataItem;
onBlur: () => void;
};
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
@ -38,22 +35,15 @@ const StyledObjectSummaryCardContent = styled(CardContent)`
export const SettingsDataModelObjectSettingsFormCard = ({
objectMetadataItem,
onBlur,
}: SettingsDataModelObjectSettingsFormCardProps) => {
const { watch: watchFormValue } =
useFormContext<SettingsDataModelObjectIdentifiersFormValues>();
const labelIdentifierFieldMetadataIdFormValue = watchFormValue(
'labelIdentifierFieldMetadataId',
);
const labelIdentifierFieldMetadataItem = useMemo(
() =>
getLabelIdentifierFieldMetadataItem({
fields: objectMetadataItem.fields,
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadataIdFormValue,
}),
[labelIdentifierFieldMetadataIdFormValue, objectMetadataItem],
);
const labelIdentifierFieldMetadataItem = useMemo(() => {
return getLabelIdentifierFieldMetadataItem({
fields: objectMetadataItem.fields,
labelIdentifierFieldMetadataId:
objectMetadataItem.labelIdentifierFieldMetadataId,
});
}, [objectMetadataItem]);
return (
<Card fullWidth>
@ -80,9 +70,7 @@ export const SettingsDataModelObjectSettingsFormCard = ({
<CardContent>
<SettingsDataModelObjectIdentifiersForm
objectMetadataItem={objectMetadataItem}
defaultLabelIdentifierFieldMetadataId={
labelIdentifierFieldMetadataItem?.id
}
onBlur={onBlur}
/>
</CardContent>
</Card>