diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx
new file mode 100644
index 000000000..0521676c7
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx
@@ -0,0 +1,103 @@
+import { Button } from '@/ui/input/button/components/Button';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+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 { 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};
+ cursor: pointer;
+ display: flex;
+ font-size: ${({ theme }) => theme.font.size.md};
+`;
+const StyledButtonContainer = styled.div`
+ position: relative;
+ width: 100%;
+`;
+
+const StyledDownChevron = styled(IconChevronDown)`
+ color: ${({ theme }) => theme.font.color.primary};
+ position: absolute;
+ right: ${({ theme }) => theme.spacing(1.5)};
+ top: 50%;
+ transform: translateY(-50%);
+`;
+
+const StyledMenuItem = styled(MenuItem)<{ selected?: boolean }>`
+ background: ${({ theme, selected }) =>
+ selected ? theme.background.quaternary : 'transparent'};
+ cursor: pointer;
+`;
+
+const StyledSpan = styled.span`
+ margin-left: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledButton = styled(Button)`
+ color: ${({ theme }) => theme.font.color.primary};
+ padding-right: ${({ theme }) => theme.spacing(6)};
+`;
+
+export const SettingsDataModelNewFieldBreadcrumbDropDown = ({
+ isConfigureStep,
+ onBreadcrumbClick,
+}: SettingsDataModelNewFieldBreadcrumbDropDownProps) => {
+ const dropdownId = `settings-object-new-field-breadcrumb-dropdown`;
+
+ const { closeDropdown } = useDropdown(dropdownId);
+
+ const handleClick = (step: boolean) => {
+ onBreadcrumbClick(step);
+ closeDropdown();
+ };
+ const theme = useTheme();
+
+ return (
+
+ New Field -
+
+
+ {isConfigureStep ? (
+
+ ) : (
+
+ )}
+
+ }
+ dropdownComponents={
+
+
+ handleClick(false)}
+ selected={!isConfigureStep}
+ />
+ handleClick(true)}
+ selected={isConfigureStep}
+ />
+
+
+ }
+ dropdownHotkeyScope={{
+ scope: dropdownId,
+ }}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategories.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategories.ts
new file mode 100644
index 000000000..07f8aa28c
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategories.ts
@@ -0,0 +1,7 @@
+import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
+
+export const SETTINGS_FIELD_TYPE_CATEGORIES: SettingsFieldTypeCategoryType[] = [
+ 'Basic',
+ 'Relation',
+ 'Advanced',
+];
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions.ts
new file mode 100644
index 000000000..aac9c8163
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions.ts
@@ -0,0 +1,10 @@
+import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
+
+export const SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS: Record<
+ SettingsFieldTypeCategoryType,
+ string
+> = {
+ Basic: 'All the basic field types you need to start',
+ Advanced: 'More advanced fields for advanced projects',
+ Relation: 'Create a relation with another object',
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts
index 3272ad990..0e6cf0cbf 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts
@@ -23,6 +23,7 @@ import {
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue';
+import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -32,6 +33,7 @@ export type SettingsFieldTypeConfig = {
label: string;
Icon: IconComponent;
exampleValue?: unknown;
+ category: SettingsFieldTypeCategoryType;
};
export const SETTINGS_FIELD_TYPE_CONFIGS = {
@@ -39,85 +41,106 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = {
label: 'Unique ID',
Icon: IconKey,
exampleValue: '00000000-0000-0000-0000-000000000000',
+ category: 'Advanced',
},
[FieldMetadataType.Text]: {
label: 'Text',
Icon: IconTextSize,
exampleValue:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.',
+ category: 'Basic',
},
[FieldMetadataType.Numeric]: {
label: 'Numeric',
Icon: IconNumbers,
exampleValue: 2000,
+ category: 'Basic',
},
[FieldMetadataType.Number]: {
label: 'Number',
Icon: IconNumbers,
exampleValue: 2000,
+ category: 'Basic',
},
[FieldMetadataType.Link]: {
label: 'Link',
Icon: IconLink,
exampleValue: { url: 'www.twenty.com', label: '' },
+ category: 'Basic',
},
[FieldMetadataType.Links]: {
label: 'Links',
Icon: IconLink,
exampleValue: { primaryLinkUrl: 'twenty.com', primaryLinkLabel: '' },
+ category: 'Basic',
},
[FieldMetadataType.Boolean]: {
label: 'True/False',
Icon: IconCheck,
exampleValue: true,
+ category: 'Basic',
},
[FieldMetadataType.DateTime]: {
label: 'Date and Time',
Icon: IconCalendarTime,
exampleValue: DEFAULT_DATE_VALUE.toISOString(),
+ category: 'Basic',
},
[FieldMetadataType.Date]: {
label: 'Date',
Icon: IconCalendarEvent,
exampleValue: DEFAULT_DATE_VALUE.toISOString(),
+ category: 'Basic',
},
[FieldMetadataType.Select]: {
label: 'Select',
Icon: IconTag,
+ category: 'Basic',
},
[FieldMetadataType.MultiSelect]: {
label: 'Multi-select',
Icon: IconTags,
+ category: 'Basic',
},
[FieldMetadataType.Currency]: {
label: 'Currency',
Icon: IconCoins,
exampleValue: { amountMicros: 2000000000, currencyCode: CurrencyCode.USD },
+ category: 'Basic',
},
[FieldMetadataType.Relation]: {
label: 'Relation',
Icon: IconRelationManyToMany,
+ category: 'Relation',
+ },
+ [FieldMetadataType.Email]: {
+ label: 'Email',
+ Icon: IconMail,
+ category: 'Basic',
},
- [FieldMetadataType.Email]: { label: 'Email', Icon: IconMail },
[FieldMetadataType.Emails]: {
label: 'Emails',
Icon: IconMail,
exampleValue: { primaryEmail: 'john@twenty.com' },
+ category: 'Basic',
},
[FieldMetadataType.Phone]: {
label: 'Phone',
Icon: IconPhone,
exampleValue: '+1234-567-890',
+ category: 'Basic',
},
[FieldMetadataType.Rating]: {
label: 'Rating',
Icon: IconTwentyStar,
exampleValue: '3',
+ category: 'Basic',
},
[FieldMetadataType.FullName]: {
label: 'Full Name',
Icon: IconUser,
exampleValue: { firstName: 'John', lastName: 'Doe' },
+ category: 'Advanced',
},
[FieldMetadataType.Address]: {
label: 'Address',
@@ -132,20 +155,25 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = {
addressLat: 34.0522,
addressLng: -118.2437,
},
+ category: 'Basic',
},
[FieldMetadataType.RawJson]: {
label: 'JSON',
Icon: IconJson,
exampleValue: { key: 'value' },
+
+ category: 'Basic',
},
[FieldMetadataType.RichText]: {
label: 'Rich Text',
Icon: IconFilePencil,
exampleValue: { key: 'value' },
+ category: 'Basic',
},
[FieldMetadataType.Actor]: {
label: 'Actor',
Icon: IconCreativeCommonsSa,
+ category: 'Basic',
},
} as const satisfies Record<
SettingsSupportedFieldType,
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm.tsx
deleted file mode 100644
index ee825e776..000000000
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import styled from '@emotion/styled';
-import { Controller, useFormContext } from 'react-hook-form';
-import { z } from 'zod';
-
-import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
-import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
-import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages';
-import { IconPicker } from '@/ui/input/components/IconPicker';
-import { TextArea } from '@/ui/input/components/TextArea';
-import { TextInput } from '@/ui/input/components/TextInput';
-
-export const settingsDataModelFieldAboutFormSchema = (
- existingLabels?: string[],
-) => {
- return fieldMetadataItemSchema(existingLabels || []).pick({
- description: true,
- icon: true,
- label: true,
- });
-};
-
-// Correctly infer the type from the returned schema
-type SettingsDataModelFieldAboutFormValues = z.infer<
- ReturnType
->;
-
-type SettingsDataModelFieldAboutFormProps = {
- disabled?: boolean;
- fieldMetadataItem?: FieldMetadataItem;
- maxLength?: number;
-};
-
-const StyledInputsContainer = styled.div`
- display: flex;
- gap: ${({ theme }) => theme.spacing(2)};
- margin-bottom: ${({ theme }) => theme.spacing(2)};
- width: 100%;
-`;
-
-const LABEL = 'label';
-
-export const SettingsDataModelFieldAboutForm = ({
- disabled,
- fieldMetadataItem,
- maxLength,
-}: SettingsDataModelFieldAboutFormProps) => {
- const {
- control,
- trigger,
- formState: { errors },
- } = useFormContext();
- return (
- <>
-
- (
- onChange(iconKey)}
- variant="primary"
- />
- )}
- />
- (
- {
- onChange(e);
- trigger(LABEL);
- }}
- error={getErrorMessageFromError(errors.label?.message)}
- disabled={disabled}
- maxLength={maxLength}
- fullWidth
- />
- )}
- />
-
- (
-
- )}
- />
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm.tsx
new file mode 100644
index 000000000..40341b6b5
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm.tsx
@@ -0,0 +1,47 @@
+import { Controller, useFormContext } from 'react-hook-form';
+import { z } from 'zod';
+
+import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
+
+import { TextArea } from '@/ui/input/components/TextArea';
+
+export const settingsDataModelFieldDescriptionFormSchema = () => {
+ return fieldMetadataItemSchema([]).pick({
+ description: true,
+ });
+};
+
+type SettingsDataModelFieldDescriptionFormValues = z.infer<
+ ReturnType
+>;
+
+type SettingsDataModelFieldDescriptionFormProps = {
+ disabled?: boolean;
+ fieldMetadataItem?: FieldMetadataItem;
+};
+
+export const SettingsDataModelFieldDescriptionForm = ({
+ disabled,
+ fieldMetadataItem,
+}: SettingsDataModelFieldDescriptionFormProps) => {
+ const { control } =
+ useFormContext();
+
+ return (
+ (
+
+ )}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx
new file mode 100644
index 000000000..114993114
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx
@@ -0,0 +1,84 @@
+import styled from '@emotion/styled';
+import { Controller, useFormContext } from 'react-hook-form';
+import { z } from 'zod';
+
+import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
+import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages';
+import { IconPicker } from '@/ui/input/components/IconPicker';
+import { TextInput } from '@/ui/input/components/TextInput';
+
+export const settingsDataModelFieldIconLabelFormSchema = (
+ existingLabels?: string[],
+) => {
+ return fieldMetadataItemSchema(existingLabels || []).pick({
+ icon: true,
+ label: true,
+ });
+};
+
+type SettingsDataModelFieldIconLabelFormValues = z.infer<
+ ReturnType
+>;
+
+const StyledInputsContainer = styled.div`
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(2)};
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
+ width: 100%;
+`;
+
+type SettingsDataModelFieldIconLabelFormProps = {
+ disabled?: boolean;
+ fieldMetadataItem?: FieldMetadataItem;
+ maxLength?: number;
+};
+
+export const SettingsDataModelFieldIconLabelForm = ({
+ disabled,
+ fieldMetadataItem,
+ maxLength,
+}: SettingsDataModelFieldIconLabelFormProps) => {
+ const {
+ control,
+ trigger,
+ formState: { errors },
+ } = useFormContext();
+
+ return (
+
+ (
+ onChange(iconKey)}
+ variant="primary"
+ />
+ )}
+ />
+ (
+ {
+ onChange(e);
+ trigger('label');
+ }}
+ error={getErrorMessageFromError(errors.label?.message)}
+ disabled={disabled}
+ maxLength={maxLength}
+ fullWidth
+ />
+ )}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect.tsx
index 5d517c61a..304843fe4 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect.tsx
@@ -1,8 +1,10 @@
-import omit from 'lodash.omit';
+import styled from '@emotion/styled';
import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { SETTINGS_FIELD_TYPE_CATEGORIES } from '@/settings/data-model/constants/SettingsFieldTypeCategories';
+import { SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS } from '@/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions';
import {
SETTINGS_FIELD_TYPE_CONFIGS,
SettingsFieldTypeConfig,
@@ -11,7 +13,12 @@ import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/field
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
-import { Select, SelectOption } from '@/ui/input/components/Select';
+import { Button } from '@/ui/input/button/components/Button';
+import { TextInput } from '@/ui/input/components/TextInput';
+import { useTheme } from '@emotion/react';
+import { Section } from '@react-email/components';
+import { useState } from 'react';
+import { H2Title, IconChevronRight, IconSearch } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const settingsDataModelFieldTypeFormSchema = z.object({
@@ -23,39 +30,76 @@ export const settingsDataModelFieldTypeFormSchema = z.object({
),
});
-type SettingsDataModelFieldTypeFormValues = z.infer<
+export type SettingsDataModelFieldTypeFormValues = z.infer<
typeof settingsDataModelFieldTypeFormSchema
>;
type SettingsDataModelFieldTypeSelectProps = {
className?: string;
- disabled?: boolean;
excludedFieldTypes?: SettingsSupportedFieldType[];
fieldMetadataItem?: Pick<
FieldMetadataItem,
'defaultValue' | 'options' | 'type'
>;
+ onFieldTypeSelect: () => void;
};
+const StyledTypeSelectContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: inherit;
+ width: 100%;
+`;
+
+const StyledButton = styled(Button)<{ isActive: boolean }>`
+ background: ${({ theme, isActive }) =>
+ isActive ? theme.background.quaternary : theme.background.secondary};
+ height: 40px;
+ width: 100%;
+ border-radius: ${({ theme }) => theme.border.radius.md};
+`;
+const StyledContainer = styled.div`
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(2)};
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ width: 100%;
+`;
+
+const StyledButtonContainer = styled.div`
+ display: flex;
+
+ position: relative;
+ width: calc(50% - ${({ theme }) => theme.spacing(1)});
+`;
+
+const StyledRightChevron = styled(IconChevronRight)`
+ color: ${({ theme }) => theme.font.color.secondary};
+ position: absolute;
+ right: ${({ theme }) => theme.spacing(2)};
+ top: 50%;
+ transform: translateY(-50%);
+`;
+const StyledSearchInput = styled(TextInput)`
+ width: 100%;
+`;
+
export const SettingsDataModelFieldTypeSelect = ({
className,
- disabled,
excludedFieldTypes = [],
fieldMetadataItem,
+ onFieldTypeSelect,
}: SettingsDataModelFieldTypeSelectProps) => {
const { control } = useFormContext();
-
- const fieldTypeConfigs: Partial<
- Record
- > = omit(SETTINGS_FIELD_TYPE_CONFIGS, excludedFieldTypes);
-
- const fieldTypeOptions = Object.entries(
- fieldTypeConfigs,
- ).map>(([key, dataTypeConfig]) => ({
- Icon: dataTypeConfig.Icon,
- label: dataTypeConfig.label,
- value: key as SettingsSupportedFieldType,
- }));
+ const [searchQuery, setSearchQuery] = useState('');
+ const theme = useTheme();
+ const fieldTypeConfigs = Object.entries(
+ SETTINGS_FIELD_TYPE_CONFIGS,
+ ).filter(
+ ([key, config]) =>
+ !excludedFieldTypes.includes(key as SettingsSupportedFieldType) &&
+ config.label.toLowerCase().includes(searchQuery.toLowerCase()),
+ );
const { resetDefaultValueField: resetBooleanDefaultValueField } =
useBooleanSettingsFormInitialValues({ fieldMetadataItem });
@@ -66,8 +110,6 @@ export const SettingsDataModelFieldTypeSelect = ({
const { resetDefaultValueField: resetSelectDefaultValueField } =
useSelectSettingsFormInitialValues({ fieldMetadataItem });
- // Reset defaultValue on type change with a valid value for the selected type
- // so the form does not become invalid.
const resetDefaultValueField = (nextValue: SettingsSupportedFieldType) => {
switch (nextValue) {
case FieldMetadataType.Boolean:
@@ -95,18 +137,49 @@ export const SettingsDataModelFieldTypeSelect = ({
: FieldMetadataType.Text
}
render={({ field: { onChange, value } }) => (
-