Remove step 1 of new object field (#7397)

fixes #7356 
fixes #6967 
fixes #7102
fixes #7121 
fixes #7505
This commit is contained in:
nitin
2024-10-09 14:54:49 +05:30
committed by GitHub
parent 9b9e03fbf6
commit 58cbcbfe70
46 changed files with 374 additions and 574 deletions

View File

@ -1,6 +1,7 @@
import { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
@ -202,22 +203,21 @@ const SettingsIntegrationShowDatabaseConnection = lazy(() =>
})),
);
const SettingsObjectNewFieldStep1 = lazy(() =>
const SettingsObjectNewFieldSelect = lazy(() =>
import(
'~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1'
'~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect'
).then((module) => ({
default: module.SettingsObjectNewFieldStep1,
default: module.SettingsObjectNewFieldSelect,
})),
);
const SettingsObjectNewFieldStep2 = lazy(() =>
const SettingsObjectNewFieldConfigure = lazy(() =>
import(
'~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2'
'~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure'
).then((module) => ({
default: module.SettingsObjectNewFieldStep2,
default: module.SettingsObjectNewFieldConfigure,
})),
);
const SettingsObjectFieldEdit = lazy(() =>
import('~/pages/settings/data-model/SettingsObjectFieldEdit').then(
(module) => ({
@ -245,7 +245,7 @@ export const SettingsRoutes = ({
isCRMMigrationEnabled,
isServerlessFunctionSettingsEnabled,
}: SettingsRoutesProps) => (
<Suspense fallback={null}>
<Suspense fallback={<SettingsSkeletonLoader />}>
<Routes>
<Route path={SettingsPath.ProfilePage} element={<SettingsProfile />} />
<Route path={SettingsPath.Appearance} element={<SettingsAppearance />} />
@ -345,12 +345,12 @@ export const SettingsRoutes = ({
element={<SettingsIntegrationShowDatabaseConnection />}
/>
<Route
path={SettingsPath.ObjectNewFieldStep1}
element={<SettingsObjectNewFieldStep1 />}
path={SettingsPath.ObjectNewFieldSelect}
element={<SettingsObjectNewFieldSelect />}
/>
<Route
path={SettingsPath.ObjectNewFieldStep2}
element={<SettingsObjectNewFieldStep2 />}
path={SettingsPath.ObjectNewFieldConfigure}
element={<SettingsObjectNewFieldConfigure />}
/>
<Route
path={SettingsPath.ObjectFieldEdit}

View File

@ -1,9 +0,0 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getDisabledFieldMetadataItems = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
) =>
objectMetadataItem.fields.filter(
(fieldMetadataItem) =>
!fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
);

View File

@ -0,0 +1,52 @@
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageHeader } from '@/ui/layout/page/PageHeader';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
const StyledTitleLoaderContainer = styled.div`
margin: ${({ theme }) => theme.spacing(8, 8, 2)};
`;
export const SettingsSkeletonLoader = () => {
const theme = useTheme();
return (
<StyledContainer>
<PageHeader
title={
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
width={120}
/>{' '}
</SkeletonTheme>
}
/>
<PageBody>
<StyledTitleLoaderContainer>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
width={200}
/>
</SkeletonTheme>
</StyledTitleLoaderContainer>
</PageBody>
</StyledContainer>
);
};

View File

@ -6,20 +6,17 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { IconChevronDown } from 'twenty-ui';
type SettingsDataModelNewFieldBreadcrumbDropDownProps = {
isConfigureStep: boolean;
onBreadcrumbClick: (isConfigureStep: boolean) => void;
};
const StyledContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.secondary};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
`;
const StyledButtonContainer = styled.div`
position: relative;
width: 100%;
@ -48,19 +45,24 @@ const StyledButton = styled(Button)`
padding-right: ${({ theme }) => theme.spacing(6)};
`;
export const SettingsDataModelNewFieldBreadcrumbDropDown = ({
isConfigureStep,
onBreadcrumbClick,
}: SettingsDataModelNewFieldBreadcrumbDropDownProps) => {
export const SettingsDataModelNewFieldBreadcrumbDropDown = () => {
const dropdownId = `settings-object-new-field-breadcrumb-dropdown`;
const { closeDropdown } = useDropdown(dropdownId);
const navigate = useNavigate();
const location = useLocation();
const { objectSlug = '' } = useParams();
const theme = useTheme();
const handleClick = (step: boolean) => {
onBreadcrumbClick(step);
const isConfigureStep = location.pathname.includes('/configure');
const handleClick = (isConfigureStep: boolean) => {
if (isConfigureStep) {
navigate(`/settings/objects/${objectSlug}/new-field/configure`);
} else {
navigate(`/settings/objects/${objectSlug}/new-field/select`);
}
closeDropdown();
};
const theme = useTheme();
return (
<StyledContainer>

View File

@ -10,36 +10,25 @@ import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fiel
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { TextInput } from '@/ui/input/components/TextInput';
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Section } from '@react-email/components';
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { H2Title, IconSearch } from 'twenty-ui';
import { z } from 'zod';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { SettingsDataModelFieldTypeFormValues } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect';
export const settingsDataModelFieldTypeFormSchema = z.object({
type: z.enum(
Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [
SettingsFieldType,
...SettingsFieldType[],
],
),
});
export type SettingsDataModelFieldTypeFormValues = z.infer<
typeof settingsDataModelFieldTypeFormSchema
>;
type SettingsDataModelFieldTypeSelectProps = {
type SettingsObjectNewFieldSelectorProps = {
className?: string;
excludedFieldTypes?: SettingsFieldType[];
fieldMetadataItem?: Pick<
FieldMetadataItem,
'defaultValue' | 'options' | 'type'
>;
onFieldTypeSelect: () => void;
objectSlug: string;
};
const StyledTypeSelectContainer = styled.div`
@ -68,12 +57,11 @@ const StyledSearchInput = styled(TextInput)`
width: 100%;
`;
export const SettingsDataModelFieldTypeSelect = ({
className,
export const SettingsObjectNewFieldSelector = ({
excludedFieldTypes = [],
fieldMetadataItem,
onFieldTypeSelect,
}: SettingsDataModelFieldTypeSelectProps) => {
objectSlug,
}: SettingsObjectNewFieldSelectorProps) => {
const theme = useTheme();
const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>();
const [searchQuery, setSearchQuery] = useState('');
@ -112,59 +100,60 @@ export const SettingsDataModelFieldTypeSelect = ({
};
return (
<Controller
name="type"
control={control}
defaultValue={
fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs
? (fieldMetadataItem.type as SettingsFieldType)
: FieldMetadataType.Text
}
render={({ field: { onChange } }) => (
<StyledTypeSelectContainer className={className}>
<Section>
<StyledSearchInput
LeftIcon={IconSearch}
placeholder="Search a type"
value={searchQuery}
onChange={setSearchQuery}
/>
</Section>
{SETTINGS_FIELD_TYPE_CATEGORIES.map((category) => (
<Section key={category}>
<H2Title
title={category}
description={
SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS[category]
}
/>
<StyledContainer>
{fieldTypeConfigs
.filter(([, config]) => config.category === category)
.map(([key, config]) => (
<StyledCardContainer>
<SettingsCard
key={key}
onClick={() => {
onChange(key as SettingsFieldType);
resetDefaultValueField(key as SettingsFieldType);
onFieldTypeSelect();
}}
Icon={
<config.Icon
size={theme.icon.size.xl}
stroke={theme.icon.stroke.sm}
<>
{' '}
<Section>
<StyledSearchInput
LeftIcon={IconSearch}
placeholder="Search a type"
value={searchQuery}
onChange={setSearchQuery}
/>
</Section>
<Controller
name="type"
control={control}
render={() => (
<StyledTypeSelectContainer>
{SETTINGS_FIELD_TYPE_CATEGORIES.map((category) => (
<Section key={category}>
<H2Title
title={category}
description={
SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS[category]
}
/>
<StyledContainer>
{fieldTypeConfigs
.filter(([, config]) => config.category === category)
.map(([key, config]) => (
<StyledCardContainer key={key}>
<UndecoratedLink
to={`/settings/objects/${objectSlug}/new-field/configure?fieldType=${key}`}
fullWidth
onClick={() =>
resetDefaultValueField(key as SettingsFieldType)
}
>
<SettingsCard
key={key}
Icon={
<config.Icon
size={theme.icon.size.xl}
stroke={theme.icon.stroke.sm}
/>
}
title={config.label}
/>
}
title={config.label}
/>
</StyledCardContainer>
))}
</StyledContainer>
</Section>
))}
</StyledTypeSelectContainer>
)}
/>
</UndecoratedLink>
</StyledCardContainer>
))}
</StyledContainer>
</Section>
))}
</StyledTypeSelectContainer>
)}
/>
</>
);
};

View File

@ -1,58 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SettingsDataModelFieldTypeSelect } from '../SettingsDataModelFieldTypeSelect';
const meta: Meta<typeof SettingsDataModelFieldTypeSelect> = {
title:
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect',
component: SettingsDataModelFieldTypeSelect,
decorators: [FormProviderDecorator, ComponentDecorator],
parameters: {
container: { width: 512 },
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof SettingsDataModelFieldTypeSelect>;
export const Default: Story = {};
export const WithOpenSelect: Story = {
play: async () => {
const canvas = within(document.body);
const inputField = await canvas.findByText('Text');
await userEvent.click(inputField);
const input = await canvas.findByText('Unique ID');
await userEvent.click(input);
await userEvent.click(inputField);
},
};
export const WithExcludedFieldTypes: Story = {
args: {
excludedFieldTypes: [FieldMetadataType.Uuid, FieldMetadataType.Numeric],
},
play: async () => {
const canvas = within(document.body);
const inputField = await canvas.findByText('Text');
await userEvent.click(inputField);
await canvas.findByText('Number');
expect(canvas.queryByText('Unique ID')).toBeNull();
expect(canvas.queryByText('Numeric')).toBeNull();
},
};

View File

@ -1,9 +1,8 @@
import { z } from 'zod';
import { settingsDataModelFieldDescriptionFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm';
import { settingsDataModelFieldIconLabelFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm';
import { settingsDataModelFieldSettingsFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { settingsDataModelFieldTypeFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
import { z } from 'zod';
import { settingsDataModelFieldTypeFormSchema } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect';
export const settingsFieldFormSchema = (existingOtherLabels?: string[]) => {
return z

View File

@ -12,8 +12,8 @@ export enum SettingsPath {
ObjectOverview = 'objects/overview',
ObjectDetail = 'objects/:objectSlug',
ObjectEdit = 'objects/:objectSlug/edit',
ObjectNewFieldStep1 = 'objects/:objectSlug/new-field/step-1',
ObjectNewFieldStep2 = 'objects/:objectSlug/new-field/step-2',
ObjectNewFieldSelect = 'objects/:objectSlug/new-field/select',
ObjectNewFieldConfigure = 'objects/:objectSlug/new-field/configure',
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug',
NewObject = 'objects/new',
NewServerlessFunction = 'functions/new',

View File

@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import { JSX, ReactNode } from 'react';
import { IconComponent } from 'twenty-ui';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import {
@ -14,7 +13,6 @@ type SubMenuTopBarContainerProps = {
children: JSX.Element | JSX.Element[];
title?: string;
actionButton?: ReactNode;
Icon: IconComponent;
className?: string;
links: BreadcrumbProps['links'];
};

View File

@ -38,7 +38,7 @@ export const useGetAvailableFieldsForKanban = () => {
navigate(
`/settings/objects/${getObjectSlug(
objectMetadataItem,
)}/new-field/step-2?fieldType=${FieldMetadataType.Select}`,
)}/new-field/configure?fieldType=${FieldMetadataType.Select}`,
);
} else {
navigate(`/settings/objects`);