Remove step 1 of new object field (#7397)
fixes #7356 fixes #6967 fixes #7102 fixes #7121 fixes #7505
This commit is contained in:
@ -11,7 +11,6 @@ import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/Field
|
||||
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 { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
@ -22,42 +21,36 @@ import { Section } from '@/ui/layout/section/components/Section';
|
||||
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 { H2Title } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
// TODO: fix this type
|
||||
type SettingsDataModelNewFieldFormValues = z.infer<
|
||||
ReturnType<typeof settingsFieldFormSchema>
|
||||
> &
|
||||
any;
|
||||
|
||||
const StyledH1Title = styled(H1Title)`
|
||||
margin-bottom: 0;
|
||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
export const SettingsObjectNewFieldStep2 = () => {
|
||||
export const SettingsObjectNewFieldConfigure = () => {
|
||||
const navigate = useNavigate();
|
||||
const { objectSlug = '' } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const fieldType = searchParams.get('fieldType') as SettingsFieldType;
|
||||
const fieldType =
|
||||
(searchParams.get('fieldType') as SettingsFieldType) ||
|
||||
FieldMetadataType.Text;
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [isConfigureStep, setIsConfigureStep] = useState(false);
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
const { createMetadataField } = useFieldMetadataItem();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
|
||||
mode: 'onTouched',
|
||||
@ -66,14 +59,14 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
activeObjectMetadataItem?.fields.map((value) => value.name),
|
||||
),
|
||||
),
|
||||
defaultValues: {
|
||||
type: fieldType,
|
||||
icon: 'IconUsers',
|
||||
label: '',
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem) {
|
||||
navigate(AppPath.NotFound);
|
||||
}
|
||||
}, [activeObjectMetadataItem, navigate]);
|
||||
|
||||
const [, setObjectViews] = useState<View[]>([]);
|
||||
const [, setRelationObjectViews] = useState<View[]>([]);
|
||||
|
||||
@ -85,7 +78,6 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
},
|
||||
onCompleted: async (views) => {
|
||||
if (isUndefinedOrNull(views)) return;
|
||||
|
||||
setObjectViews(views);
|
||||
},
|
||||
});
|
||||
@ -103,15 +95,17 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
},
|
||||
onCompleted: async (views) => {
|
||||
if (isUndefinedOrNull(views)) return;
|
||||
|
||||
setRelationObjectViews(views);
|
||||
},
|
||||
});
|
||||
|
||||
const { createOneRelationMetadataItem: createOneRelationMetadata } =
|
||||
useCreateOneRelationMetadataItem();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem) {
|
||||
navigate(AppPath.NotFound);
|
||||
}
|
||||
}, [activeObjectMetadataItem, navigate]);
|
||||
|
||||
if (!activeObjectMetadataItem) return null;
|
||||
|
||||
@ -160,17 +154,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const excludedFieldTypes: SettingsFieldType[] = (
|
||||
[
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Numeric,
|
||||
FieldMetadataType.RichText,
|
||||
FieldMetadataType.Actor,
|
||||
FieldMetadataType.Email,
|
||||
FieldMetadataType.Phone,
|
||||
] as const
|
||||
).filter(isDefined);
|
||||
if (!activeObjectMetadataItem) return null;
|
||||
|
||||
return (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
@ -178,96 +162,57 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
{...formConfig}
|
||||
>
|
||||
<SubMenuTopBarContainer
|
||||
Icon={IconHierarchy2}
|
||||
title="2. Configure field"
|
||||
links={[
|
||||
{
|
||||
children: 'Objects',
|
||||
href: '/settings/objects',
|
||||
},
|
||||
{ children: 'Workspace', href: '/settings/workspace' },
|
||||
{ children: 'Objects', href: '/settings/objects' },
|
||||
{
|
||||
children: activeObjectMetadataItem.labelPlural,
|
||||
href: `/settings/objects/${objectSlug}`,
|
||||
},
|
||||
{
|
||||
children: (
|
||||
<SettingsDataModelNewFieldBreadcrumbDropDown
|
||||
isConfigureStep={isConfigureStep}
|
||||
onBreadcrumbClick={setIsConfigureStep}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
|
||||
]}
|
||||
actionButton={
|
||||
!activeObjectMetadataItem.isRemote && (
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
isCancelDisabled={isSubmitting}
|
||||
onCancel={() => {
|
||||
if (!isConfigureStep) {
|
||||
navigate(`/settings/objects/${objectSlug}`);
|
||||
} else {
|
||||
setIsConfigureStep(false);
|
||||
}
|
||||
}}
|
||||
onSave={formConfig.handleSubmit(handleSave)}
|
||||
/>
|
||||
)
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
isCancelDisabled={isSubmitting}
|
||||
onCancel={() =>
|
||||
navigate(`/settings/objects/${objectSlug}/new-field/select`)
|
||||
}
|
||||
onSave={formConfig.handleSubmit(handleSave)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<StyledH1Title
|
||||
title={
|
||||
!isConfigureStep
|
||||
? '1. Select a field type'
|
||||
: '2. Configure field'
|
||||
}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
|
||||
{!isConfigureStep ? (
|
||||
<SettingsDataModelFieldTypeSelect
|
||||
excludedFieldTypes={excludedFieldTypes}
|
||||
<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
|
||||
isCreatingField
|
||||
fieldMetadataItem={{
|
||||
icon: formConfig.watch('icon'),
|
||||
label: formConfig.watch('label') || 'New Field',
|
||||
type: fieldType as FieldMetadataType,
|
||||
}}
|
||||
onFieldTypeSelect={() => setIsConfigureStep(true)}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<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
|
||||
isCreatingField
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Description"
|
||||
description="The description of this field"
|
||||
/>
|
||||
<SettingsDataModelFieldDescriptionForm />
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</FormProvider>
|
||||
@ -0,0 +1,90 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsDataModelNewFieldBreadcrumbDropDown } from '@/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown';
|
||||
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
||||
import { SettingsObjectNewFieldSelector } from '@/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector';
|
||||
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const settingsDataModelFieldTypeFormSchema = z.object({
|
||||
type: z.enum(
|
||||
Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [
|
||||
SettingsFieldType,
|
||||
...SettingsFieldType[],
|
||||
],
|
||||
),
|
||||
});
|
||||
|
||||
export type SettingsDataModelFieldTypeFormValues = z.infer<
|
||||
typeof settingsDataModelFieldTypeFormSchema
|
||||
>;
|
||||
|
||||
export const SettingsObjectNewFieldSelect = () => {
|
||||
const navigate = useNavigate();
|
||||
const { objectSlug = '' } = useParams();
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useFilteredObjectMetadataItems();
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
const formMethods = useForm({
|
||||
resolver: zodResolver(settingsDataModelFieldTypeFormSchema),
|
||||
defaultValues: {
|
||||
type: FieldMetadataType.Text,
|
||||
},
|
||||
});
|
||||
const excludedFieldTypes: SettingsFieldType[] = (
|
||||
[
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Numeric,
|
||||
FieldMetadataType.RichText,
|
||||
FieldMetadataType.Actor,
|
||||
FieldMetadataType.Email,
|
||||
FieldMetadataType.Phone,
|
||||
] as const
|
||||
).filter(isDefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem) {
|
||||
navigate(AppPath.NotFound);
|
||||
}
|
||||
}, [activeObjectMetadataItem, navigate]);
|
||||
|
||||
if (!activeObjectMetadataItem) return null;
|
||||
|
||||
return (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...formMethods}
|
||||
>
|
||||
<SubMenuTopBarContainer
|
||||
title="1. Select a field type"
|
||||
links={[
|
||||
{ children: 'Workspace', href: '/settings/workspace' },
|
||||
{ children: 'Objects', href: '/settings/objects' },
|
||||
{
|
||||
children: activeObjectMetadataItem.labelPlural,
|
||||
href: `/settings/objects/${objectSlug}`,
|
||||
},
|
||||
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsObjectNewFieldSelector
|
||||
objectSlug={objectSlug}
|
||||
excludedFieldTypes={excludedFieldTypes}
|
||||
/>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</FormProvider>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
);
|
||||
};
|
||||
@ -1,133 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { H2Title, IconHierarchy2, IconPlus } from 'twenty-ui';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
||||
|
||||
const StyledSection = styled(Section)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledAddCustomFieldButton = styled(Button)`
|
||||
align-self: flex-end;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsObjectNewFieldStep1 = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { objectSlug = '' } = useParams();
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
|
||||
const [settingsObjectFields] = useRecoilState(
|
||||
settingsObjectFieldsFamilyState({
|
||||
objectMetadataItemId: activeObjectMetadataItem?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const { activateMetadataField, deactivateMetadataField } =
|
||||
useFieldMetadataItem();
|
||||
|
||||
const canSave = settingsObjectFields?.some(
|
||||
(field, index) =>
|
||||
field.isActive !== activeObjectMetadataItem?.fields[index].isActive,
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!activeObjectMetadataItem || !settingsObjectFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
settingsObjectFields.map((fieldMetadataItem, index) => {
|
||||
if (
|
||||
fieldMetadataItem.isActive ===
|
||||
activeObjectMetadataItem.fields[index].isActive
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fieldMetadataItem.isActive
|
||||
? activateMetadataField(fieldMetadataItem)
|
||||
: deactivateMetadataField(fieldMetadataItem);
|
||||
}),
|
||||
);
|
||||
|
||||
navigate(`/settings/objects/${objectSlug}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem) {
|
||||
navigate(AppPath.NotFound);
|
||||
return;
|
||||
}
|
||||
}, [activeObjectMetadataItem, navigate]);
|
||||
|
||||
if (!activeObjectMetadataItem) return null;
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
Icon={IconHierarchy2}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'Objects', href: '/settings/objects' },
|
||||
{
|
||||
children: activeObjectMetadataItem.labelPlural,
|
||||
href: `/settings/objects/${objectSlug}`,
|
||||
},
|
||||
{ children: 'New Field' },
|
||||
]}
|
||||
actionButton={
|
||||
!activeObjectMetadataItem.isRemote && (
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<StyledSection>
|
||||
<H2Title
|
||||
title="Check deactivated fields"
|
||||
description="Before creating a custom field, check if it already exists in the deactivated section."
|
||||
/>
|
||||
<SettingsObjectFieldTable
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
mode="new-field"
|
||||
/>
|
||||
<StyledAddCustomFieldButton
|
||||
Icon={IconPlus}
|
||||
title="Add Custom Field"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
to={`/settings/objects/${objectSlug}/new-field/step-2`}
|
||||
/>
|
||||
</StyledSection>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user