79aba75649
Issue #6496 Hi team, Is this the right approach for handling type selection with states and conditional rendering, or should these be managed on separate pages altogether? Please let me know Ill make changes accordingly :) I’m also working on styling the buttons according to the Figma design and will be moving constants like categoryDescriptions and categories to the constants folder. Thanks for your guidance! https://github.com/user-attachments/assets/452bea9f-4d0a-4472-9941-421b54cda47f --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
279 lines
10 KiB
TypeScript
279 lines
10 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 { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
|
|
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
|
|
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
|
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/SubMenuTopBarContainer';
|
|
import { Section } from '@/ui/layout/section/components/Section';
|
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
|
import { View } from '@/views/types/View';
|
|
import { ViewType } from '@/views/types/ViewType';
|
|
import { useApolloClient } from '@apollo/client';
|
|
import styled from '@emotion/styled';
|
|
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 { H1Title, H1TitleFontColor, H2Title, IconHierarchy2 } from 'twenty-ui';
|
|
import { z } from 'zod';
|
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
import { isDefined } from '~/utils/isDefined';
|
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|
|
|
type SettingsDataModelNewFieldFormValues = z.infer<
|
|
ReturnType<typeof settingsFieldFormSchema>
|
|
>;
|
|
|
|
const StyledH1Title = styled(H1Title)`
|
|
margin-bottom: 0;
|
|
`;
|
|
export const SettingsObjectNewFieldStep2 = () => {
|
|
const navigate = useNavigate();
|
|
const { objectSlug = '' } = useParams();
|
|
const [searchParams] = useSearchParams();
|
|
const fieldType = searchParams.get('fieldType') as SettingsSupportedFieldType;
|
|
const { enqueueSnackBar } = useSnackBar();
|
|
|
|
const [isConfigureStep, setIsConfigureStep] = useState(false);
|
|
const { findActiveObjectMetadataItemBySlug } =
|
|
useFilteredObjectMetadataItems();
|
|
|
|
const activeObjectMetadataItem =
|
|
findActiveObjectMetadataItemBySlug(objectSlug);
|
|
const { createMetadataField } = useFieldMetadataItem();
|
|
|
|
const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
|
|
mode: 'onTouched',
|
|
resolver: zodResolver(
|
|
settingsFieldFormSchema(
|
|
activeObjectMetadataItem?.fields.map((value) => value.name),
|
|
),
|
|
),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!activeObjectMetadataItem) {
|
|
navigate(AppPath.NotFound);
|
|
}
|
|
}, [activeObjectMetadataItem, navigate]);
|
|
|
|
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();
|
|
|
|
const apolloClient = useApolloClient();
|
|
|
|
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']),
|
|
objectMetadataId: activeObjectMetadataItem.id,
|
|
connect: {
|
|
field: {
|
|
icon: relationFormValues.field.icon,
|
|
label: relationFormValues.field.label,
|
|
},
|
|
objectMetadataId: relationFormValues.objectMetadataId,
|
|
},
|
|
});
|
|
} else {
|
|
await createMetadataField({
|
|
...formValues,
|
|
objectMetadataId: activeObjectMetadataItem.id,
|
|
});
|
|
}
|
|
|
|
navigate(`/settings/objects/${objectSlug}`);
|
|
|
|
// 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,
|
|
});
|
|
}
|
|
};
|
|
|
|
const excludedFieldTypes: SettingsSupportedFieldType[] = (
|
|
[
|
|
FieldMetadataType.Link,
|
|
FieldMetadataType.Numeric,
|
|
FieldMetadataType.RichText,
|
|
FieldMetadataType.Actor,
|
|
FieldMetadataType.Email,
|
|
] as const
|
|
).filter(isDefined);
|
|
|
|
return (
|
|
<RecordFieldValueSelectorContextProvider>
|
|
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
|
|
{...formConfig}
|
|
>
|
|
<SubMenuTopBarContainer
|
|
Icon={IconHierarchy2}
|
|
title={
|
|
<Breadcrumb
|
|
links={[
|
|
{
|
|
children: 'Objects',
|
|
href: '/settings/objects',
|
|
styles: { minWidth: 'max-content' },
|
|
},
|
|
{
|
|
children: activeObjectMetadataItem.labelPlural,
|
|
href: `/settings/objects/${objectSlug}`,
|
|
styles: { maxWidth: '50%' },
|
|
},
|
|
{
|
|
children: (
|
|
<SettingsDataModelNewFieldBreadcrumbDropDown
|
|
isConfigureStep={isConfigureStep}
|
|
onBreadcrumbClick={setIsConfigureStep}
|
|
/>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
}
|
|
actionButton={
|
|
!activeObjectMetadataItem.isRemote && (
|
|
<SaveAndCancelButtons
|
|
isSaveDisabled={!canSave}
|
|
isCancelDisabled={isSubmitting}
|
|
onCancel={() => {
|
|
if (!isConfigureStep) {
|
|
navigate(`/settings/objects/${objectSlug}`);
|
|
} else {
|
|
setIsConfigureStep(false);
|
|
}
|
|
}}
|
|
onSave={formConfig.handleSubmit(handleSave)}
|
|
/>
|
|
)
|
|
}
|
|
>
|
|
<SettingsPageContainer>
|
|
<StyledH1Title
|
|
title={
|
|
!isConfigureStep
|
|
? '1. Select a field type'
|
|
: '2. Configure field'
|
|
}
|
|
fontColor={H1TitleFontColor.Primary}
|
|
/>
|
|
|
|
{!isConfigureStep ? (
|
|
<SettingsDataModelFieldTypeSelect
|
|
excludedFieldTypes={excludedFieldTypes}
|
|
fieldMetadataItem={{
|
|
type: fieldType,
|
|
}}
|
|
onFieldTypeSelect={() => setIsConfigureStep(true)}
|
|
/>
|
|
) : (
|
|
<>
|
|
<Section>
|
|
<H2Title
|
|
title="Icon and Name"
|
|
description="The name and icon of this field"
|
|
/>
|
|
<SettingsDataModelFieldIconLabelForm
|
|
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
|
/>
|
|
</Section>
|
|
<Section>
|
|
<H2Title
|
|
title="Values"
|
|
description="The values of this field"
|
|
/>
|
|
|
|
<SettingsDataModelFieldSettingsFormCard
|
|
fieldMetadataItem={{
|
|
icon: formConfig.watch('icon'),
|
|
label: formConfig.watch('label') || 'Employees',
|
|
type: formConfig.watch('type'),
|
|
}}
|
|
objectMetadataItem={activeObjectMetadataItem}
|
|
/>
|
|
</Section>
|
|
<Section>
|
|
<H2Title
|
|
title="Description"
|
|
description="The description of this field"
|
|
/>
|
|
<SettingsDataModelFieldDescriptionForm />
|
|
</Section>
|
|
</>
|
|
)}
|
|
</SettingsPageContainer>
|
|
</SubMenuTopBarContainer>
|
|
</FormProvider>
|
|
</RecordFieldValueSelectorContextProvider>
|
|
);
|
|
};
|