Remove step 1 of new object field (#7397)
fixes #7356 fixes #6967 fixes #7102 fixes #7121 fixes #7505
This commit is contained in:
@ -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}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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'];
|
||||
};
|
||||
|
||||
@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user