# Introduction For motivations and context please have a look to https://github.com/twentyhq/twenty/pull/9394 whom this PR results from. In this pull-request we remove any `metadataField` and `objectMetadata` sluggification. We directly consume `objectMetadata.namePlural` and `metadataField.name`, ***it seems like that historically the consumed `metadataField.name`*** are we sure that we wanna change this behavior ? ## Notes Unless I'm mistaken by reverting the `kebabcase` url formatting we might be creating deadlinks that user could have save beforehand => Discussed with Charles said it's controlled risk. --------- Co-authored-by: Paul Rastoin <paulrastoin@Pauls-MacBook-Pro.local>
249 lines
9.4 KiB
TypeScript
249 lines
9.4 KiB
TypeScript
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
import { SettingsDataModelNewFieldBreadcrumbDropDown } from '@/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown';
|
|
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
|
|
import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm';
|
|
import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm';
|
|
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
|
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
|
|
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
|
|
import { AppPath } from '@/types/AppPath';
|
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
|
import { View } from '@/views/types/View';
|
|
import { ViewType } from '@/views/types/ViewType';
|
|
import { useApolloClient } from '@apollo/client';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import pick from 'lodash.pick';
|
|
import { useEffect, useState } from 'react';
|
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
import { H2Title, Section } from 'twenty-ui';
|
|
import { z } from 'zod';
|
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
import { DEFAULT_ICONS_BY_FIELD_TYPE } from '~/pages/settings/data-model/constants/DefaultIconsByFieldType';
|
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|
|
|
type SettingsDataModelNewFieldFormValues = z.infer<
|
|
ReturnType<typeof settingsFieldFormSchema>
|
|
> &
|
|
any;
|
|
|
|
const DEFAULT_ICON_FOR_NEW_FIELD = 'IconUsers';
|
|
|
|
export const SettingsObjectNewFieldConfigure = () => {
|
|
const navigate = useNavigate();
|
|
const { objectNamePlural = '' } = useParams();
|
|
const [searchParams] = useSearchParams();
|
|
const fieldType =
|
|
(searchParams.get('fieldType') as SettingsFieldType) ||
|
|
FieldMetadataType.Text;
|
|
const { enqueueSnackBar } = useSnackBar();
|
|
|
|
const { findActiveObjectMetadataItemByNamePlural } =
|
|
useFilteredObjectMetadataItems();
|
|
const activeObjectMetadataItem =
|
|
findActiveObjectMetadataItemByNamePlural(objectNamePlural);
|
|
const { createMetadataField } = useFieldMetadataItem();
|
|
const apolloClient = useApolloClient();
|
|
|
|
const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
|
|
mode: 'onTouched',
|
|
resolver: zodResolver(
|
|
settingsFieldFormSchema(
|
|
activeObjectMetadataItem?.fields.map((value) => value.name),
|
|
),
|
|
),
|
|
defaultValues: {
|
|
type: fieldType,
|
|
icon:
|
|
DEFAULT_ICONS_BY_FIELD_TYPE[fieldType] ?? DEFAULT_ICON_FOR_NEW_FIELD,
|
|
label: '',
|
|
description: '',
|
|
name: '',
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
formConfig.setValue(
|
|
'icon',
|
|
DEFAULT_ICONS_BY_FIELD_TYPE[fieldType] ?? DEFAULT_ICON_FOR_NEW_FIELD,
|
|
);
|
|
}, [fieldType, formConfig]);
|
|
|
|
const [, setObjectViews] = useState<View[]>([]);
|
|
const [, setRelationObjectViews] = useState<View[]>([]);
|
|
|
|
useFindManyRecords<View>({
|
|
objectNameSingular: CoreObjectNameSingular.View,
|
|
filter: {
|
|
type: { eq: ViewType.Table },
|
|
objectMetadataId: { eq: activeObjectMetadataItem?.id },
|
|
},
|
|
onCompleted: async (views) => {
|
|
if (isUndefinedOrNull(views)) return;
|
|
setObjectViews(views);
|
|
},
|
|
});
|
|
|
|
const relationObjectMetadataId = formConfig.watch(
|
|
'relation.objectMetadataId',
|
|
);
|
|
|
|
useFindManyRecords<View>({
|
|
objectNameSingular: CoreObjectNameSingular.View,
|
|
skip: !relationObjectMetadataId,
|
|
filter: {
|
|
type: { eq: ViewType.Table },
|
|
objectMetadataId: { eq: relationObjectMetadataId },
|
|
},
|
|
onCompleted: async (views) => {
|
|
if (isUndefinedOrNull(views)) return;
|
|
setRelationObjectViews(views);
|
|
},
|
|
});
|
|
const { createOneRelationMetadataItem: createOneRelationMetadata } =
|
|
useCreateOneRelationMetadataItem();
|
|
|
|
useEffect(() => {
|
|
if (!activeObjectMetadataItem) {
|
|
navigate(AppPath.NotFound);
|
|
}
|
|
}, [activeObjectMetadataItem, navigate]);
|
|
|
|
if (!activeObjectMetadataItem) return null;
|
|
|
|
const { isValid, isSubmitting } = formConfig.formState;
|
|
const canSave = isValid && !isSubmitting;
|
|
|
|
const handleSave = async (
|
|
formValues: SettingsDataModelNewFieldFormValues,
|
|
) => {
|
|
try {
|
|
if (
|
|
formValues.type === FieldMetadataType.Relation &&
|
|
'relation' in formValues
|
|
) {
|
|
const { relation: relationFormValues, ...fieldFormValues } = formValues;
|
|
|
|
await createOneRelationMetadata({
|
|
relationType: relationFormValues.type,
|
|
field: pick(fieldFormValues, [
|
|
'icon',
|
|
'label',
|
|
'description',
|
|
'name',
|
|
'isLabelSyncedWithName',
|
|
]),
|
|
objectMetadataId: activeObjectMetadataItem.id,
|
|
connect: {
|
|
field: {
|
|
icon: relationFormValues.field.icon,
|
|
label: relationFormValues.field.label,
|
|
name:
|
|
(relationFormValues.field.isLabelSyncedWithName ?? true)
|
|
? computeMetadataNameFromLabel(relationFormValues.field.label)
|
|
: relationFormValues.field.name,
|
|
},
|
|
objectMetadataId: relationFormValues.objectMetadataId,
|
|
},
|
|
});
|
|
} else {
|
|
await createMetadataField({
|
|
...formValues,
|
|
objectMetadataId: activeObjectMetadataItem.id,
|
|
});
|
|
}
|
|
|
|
navigate(`/settings/objects/${objectNamePlural}`);
|
|
|
|
// TODO: fix optimistic update logic
|
|
// Forcing a refetch for now but it's not ideal
|
|
await apolloClient.refetchQueries({
|
|
include: ['FindManyViews', 'CombinedFindManyRecords'],
|
|
});
|
|
} catch (error) {
|
|
enqueueSnackBar((error as Error).message, {
|
|
variant: SnackBarVariant.Error,
|
|
});
|
|
}
|
|
};
|
|
if (!activeObjectMetadataItem) return null;
|
|
|
|
return (
|
|
<RecordFieldValueSelectorContextProvider>
|
|
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
|
|
{...formConfig}
|
|
>
|
|
<SubMenuTopBarContainer
|
|
title="2. Configure field"
|
|
links={[
|
|
{ children: 'Workspace', href: '/settings/workspace' },
|
|
{ children: 'Objects', href: '/settings/objects' },
|
|
{
|
|
children: activeObjectMetadataItem.labelPlural,
|
|
href: `/settings/objects/${objectNamePlural}`,
|
|
},
|
|
|
|
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
|
|
]}
|
|
actionButton={
|
|
<SaveAndCancelButtons
|
|
isSaveDisabled={!canSave}
|
|
isCancelDisabled={isSubmitting}
|
|
onCancel={() =>
|
|
navigate(
|
|
`/settings/objects/${objectNamePlural}/new-field/select?fieldType=${fieldType}`,
|
|
)
|
|
}
|
|
onSave={formConfig.handleSubmit(handleSave)}
|
|
/>
|
|
}
|
|
>
|
|
<SettingsPageContainer>
|
|
<Section>
|
|
<H2Title
|
|
title="Icon and Name"
|
|
description="The name and icon of this field"
|
|
/>
|
|
<SettingsDataModelFieldIconLabelForm
|
|
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
|
canToggleSyncLabelWithName={
|
|
fieldType !== FieldMetadataType.Relation
|
|
}
|
|
/>
|
|
</Section>
|
|
<Section>
|
|
<H2Title title="Values" description="The values of this field" />
|
|
<SettingsDataModelFieldSettingsFormCard
|
|
fieldMetadataItem={{
|
|
icon: formConfig.watch('icon'),
|
|
label: formConfig.watch('label') || 'New Field',
|
|
settings: formConfig.watch('settings') || null,
|
|
type: fieldType as FieldMetadataType,
|
|
}}
|
|
objectMetadataItem={activeObjectMetadataItem}
|
|
/>
|
|
</Section>
|
|
<Section>
|
|
<H2Title
|
|
title="Description"
|
|
description="The description of this field"
|
|
/>
|
|
<SettingsDataModelFieldDescriptionForm />
|
|
</Section>
|
|
</SettingsPageContainer>
|
|
</SubMenuTopBarContainer>
|
|
</FormProvider>
|
|
</RecordFieldValueSelectorContextProvider>
|
|
);
|
|
};
|