Added new view to select types for objects (#6700)
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>
This commit is contained in:
@ -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 (
|
||||
<StyledContainer>
|
||||
New Field <StyledSpan>-</StyledSpan>
|
||||
<Dropdown
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<StyledButtonContainer>
|
||||
<StyledDownChevron size={theme.icon.size.md} />
|
||||
{isConfigureStep ? (
|
||||
<StyledButton variant="tertiary" title="2. Configure" />
|
||||
) : (
|
||||
<StyledButton variant="tertiary" title="1. Type" />
|
||||
)}
|
||||
</StyledButtonContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<StyledMenuItem
|
||||
text="1. Type"
|
||||
onClick={() => handleClick(false)}
|
||||
selected={!isConfigureStep}
|
||||
/>
|
||||
<StyledMenuItem
|
||||
text="2. Configure"
|
||||
onClick={() => handleClick(true)}
|
||||
selected={isConfigureStep}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType';
|
||||
|
||||
export const SETTINGS_FIELD_TYPE_CATEGORIES: SettingsFieldTypeCategoryType[] = [
|
||||
'Basic',
|
||||
'Relation',
|
||||
'Advanced',
|
||||
];
|
||||
@ -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',
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -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<typeof settingsDataModelFieldAboutFormSchema>
|
||||
>;
|
||||
|
||||
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<SettingsDataModelFieldAboutFormValues>();
|
||||
return (
|
||||
<>
|
||||
<StyledInputsContainer>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<IconPicker
|
||||
disabled={disabled}
|
||||
selectedIconKey={value ?? ''}
|
||||
onChange={({ iconKey }) => onChange(iconKey)}
|
||||
variant="primary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name={LABEL}
|
||||
control={control}
|
||||
defaultValue={fieldMetadataItem?.label}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
placeholder="Employees"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e);
|
||||
trigger(LABEL);
|
||||
}}
|
||||
error={getErrorMessageFromError(errors.label?.message)}
|
||||
disabled={disabled}
|
||||
maxLength={maxLength}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
defaultValue={fieldMetadataItem?.description}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextArea
|
||||
placeholder="Write a description"
|
||||
minRows={4}
|
||||
value={value ?? undefined}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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<typeof settingsDataModelFieldDescriptionFormSchema>
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldDescriptionFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem?: FieldMetadataItem;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldDescriptionForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldDescriptionFormProps) => {
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldDescriptionFormValues>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
defaultValue={fieldMetadataItem?.description}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextArea
|
||||
placeholder="Write a description"
|
||||
minRows={4}
|
||||
value={value ?? undefined}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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<typeof settingsDataModelFieldIconLabelFormSchema>
|
||||
>;
|
||||
|
||||
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<SettingsDataModelFieldIconLabelFormValues>();
|
||||
|
||||
return (
|
||||
<StyledInputsContainer>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<IconPicker
|
||||
disabled={disabled}
|
||||
selectedIconKey={value ?? ''}
|
||||
onChange={({ iconKey }) => onChange(iconKey)}
|
||||
variant="primary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="label"
|
||||
control={control}
|
||||
defaultValue={fieldMetadataItem?.label}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextInput
|
||||
placeholder="Employees"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e);
|
||||
trigger('label');
|
||||
}}
|
||||
error={getErrorMessageFromError(errors.label?.message)}
|
||||
disabled={disabled}
|
||||
maxLength={maxLength}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
);
|
||||
};
|
||||
@ -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<SettingsDataModelFieldTypeFormValues>();
|
||||
|
||||
const fieldTypeConfigs: Partial<
|
||||
Record<SettingsSupportedFieldType, SettingsFieldTypeConfig>
|
||||
> = omit(SETTINGS_FIELD_TYPE_CONFIGS, excludedFieldTypes);
|
||||
|
||||
const fieldTypeOptions = Object.entries<SettingsFieldTypeConfig>(
|
||||
fieldTypeConfigs,
|
||||
).map<SelectOption<SettingsSupportedFieldType>>(([key, dataTypeConfig]) => ({
|
||||
Icon: dataTypeConfig.Icon,
|
||||
label: dataTypeConfig.label,
|
||||
value: key as SettingsSupportedFieldType,
|
||||
}));
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const theme = useTheme();
|
||||
const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig>(
|
||||
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 } }) => (
|
||||
<Select
|
||||
className={className}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-type-select"
|
||||
value={value}
|
||||
onChange={(nextValue) => {
|
||||
onChange(nextValue);
|
||||
resetDefaultValueField(nextValue);
|
||||
}}
|
||||
options={fieldTypeOptions}
|
||||
/>
|
||||
<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]) => (
|
||||
<StyledButtonContainer>
|
||||
<StyledButton
|
||||
key={key}
|
||||
onClick={() => {
|
||||
onChange(key as SettingsSupportedFieldType);
|
||||
resetDefaultValueField(
|
||||
key as SettingsSupportedFieldType,
|
||||
);
|
||||
onFieldTypeSelect();
|
||||
}}
|
||||
title={config.label}
|
||||
Icon={config.Icon}
|
||||
size="small"
|
||||
isActive={value === key}
|
||||
/>
|
||||
<StyledRightChevron size={theme.icon.size.md} />
|
||||
</StyledButtonContainer>
|
||||
))}
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
))}
|
||||
</StyledTypeSelectContainer>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
|
||||
|
||||
import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||
import { SettingsDataModelFieldDescriptionForm } from '../SettingsDataModelFieldDescriptionForm';
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldDescriptionForm> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsDataModelFieldDescriptionForm',
|
||||
component: SettingsDataModelFieldDescriptionForm,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ flex: 1 }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
FormProviderDecorator,
|
||||
ComponentDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldDescriptionForm>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithFieldMetadataItem: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||
({ description }) => description === 'description',
|
||||
)!,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
@ -4,17 +4,17 @@ import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
|
||||
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
|
||||
import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||
|
||||
import { SettingsDataModelFieldAboutForm } from '../SettingsDataModelFieldAboutForm';
|
||||
import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||
import { SettingsDataModelFieldIconLabelForm } from '../SettingsDataModelFieldIconLabelForm';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldAboutForm> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsDataModelFieldAboutForm',
|
||||
component: SettingsDataModelFieldAboutForm,
|
||||
const meta: Meta<typeof SettingsDataModelFieldIconLabelForm> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsDataModelFieldIconLabelForm',
|
||||
component: SettingsDataModelFieldIconLabelForm,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StyledContainer>
|
||||
@ -28,11 +28,11 @@ const meta: Meta<typeof SettingsDataModelFieldAboutForm> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldAboutForm>;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldIconLabelForm>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithDefaultValues: Story = {
|
||||
export const WithFieldMetadataItem: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'name',
|
||||
@ -45,3 +45,9 @@ export const Disabled: Story = {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithMaxLength: Story = {
|
||||
args: {
|
||||
maxLength: 50,
|
||||
},
|
||||
};
|
||||
@ -24,12 +24,6 @@ type Story = StoryObj<typeof SettingsDataModelFieldTypeSelect>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOpenSelect: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { settingsDataModelFieldAboutFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||
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';
|
||||
|
||||
export const settingsFieldFormSchema = (existingLabels?: string[]) => {
|
||||
return z
|
||||
.object({})
|
||||
.merge(settingsDataModelFieldAboutFormSchema(existingLabels))
|
||||
.merge(settingsDataModelFieldIconLabelFormSchema(existingLabels))
|
||||
.merge(settingsDataModelFieldDescriptionFormSchema())
|
||||
.merge(settingsDataModelFieldTypeFormSchema)
|
||||
.and(settingsDataModelFieldSettingsFormSchema);
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export type SettingsFieldTypeCategoryType = 'Basic' | 'Advanced' | 'Relation';
|
||||
@ -1,10 +1,14 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { CSSProperties, Fragment } from 'react';
|
||||
import { CSSProperties, Fragment, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type BreadcrumbProps = {
|
||||
className?: string;
|
||||
links: { children: string; href?: string; styles?: CSSProperties }[];
|
||||
links: {
|
||||
href?: string;
|
||||
styles?: CSSProperties;
|
||||
children?: string | ReactNode;
|
||||
}[];
|
||||
};
|
||||
|
||||
const StyledWrapper = styled.nav`
|
||||
@ -15,6 +19,7 @@ const StyledWrapper = styled.nav`
|
||||
// font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
`;
|
||||
@ -34,21 +39,28 @@ const StyledText = styled.span`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const Breadcrumb = ({ className, links }: BreadcrumbProps) => (
|
||||
<StyledWrapper className={className}>
|
||||
{links.map((link, index) => (
|
||||
<Fragment key={index}>
|
||||
{link.href ? (
|
||||
<StyledLink style={link.styles} title={link.children} to={link.href}>
|
||||
{link.children}
|
||||
</StyledLink>
|
||||
) : (
|
||||
<StyledText style={link.styles} title={link.children}>
|
||||
{link.children}
|
||||
</StyledText>
|
||||
)}
|
||||
{index < links.length - 1 && '/'}
|
||||
</Fragment>
|
||||
))}
|
||||
</StyledWrapper>
|
||||
);
|
||||
// TODO: not sure that passing styles to the link is a good idea
|
||||
export const Breadcrumb = ({ className, links }: BreadcrumbProps) => {
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
{links.map((link, index) => {
|
||||
const text = typeof link.children === 'string' ? link.children : '';
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{link.href ? (
|
||||
<StyledLink style={link.styles} title={text} to={link.href}>
|
||||
{link.children}
|
||||
</StyledLink>
|
||||
) : (
|
||||
<StyledText style={link.styles} title={text}>
|
||||
{link.children}
|
||||
</StyledText>
|
||||
)}
|
||||
{index < links.length - 1 && '/'}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import omit from 'lodash.omit';
|
||||
import pick from 'lodash.pick';
|
||||
@ -22,9 +21,9 @@ import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
|
||||
import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||
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 { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
@ -40,12 +39,6 @@ type SettingsDataModelFieldEditFormValues = z.infer<
|
||||
ReturnType<typeof settingsFieldFormSchema>
|
||||
>;
|
||||
|
||||
const StyledSettingsObjectFieldTypeSelect = styled(
|
||||
SettingsDataModelFieldTypeSelect,
|
||||
)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const canPersistFieldMetadataItemUpdate = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
@ -204,34 +197,33 @@ export const SettingsObjectFieldEdit = () => {
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Name and description"
|
||||
description="The name and description of this field"
|
||||
title="Icon and Name"
|
||||
description="The name and icon of this field"
|
||||
/>
|
||||
<SettingsDataModelFieldAboutForm
|
||||
<SettingsDataModelFieldIconLabelForm
|
||||
disabled={!activeMetadataField.isCustom}
|
||||
fieldMetadataItem={activeMetadataField}
|
||||
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Type and values"
|
||||
description="The field's type and values."
|
||||
/>
|
||||
<StyledSettingsObjectFieldTypeSelect
|
||||
disabled
|
||||
fieldMetadataItem={activeMetadataField}
|
||||
excludedFieldTypes={[
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Email,
|
||||
]}
|
||||
/>
|
||||
<H2Title title="Values" description="The values of this field" />
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
disableCurrencyForm
|
||||
fieldMetadataItem={activeMetadataField}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Description"
|
||||
description="The description of this field"
|
||||
/>
|
||||
<SettingsDataModelFieldDescriptionForm
|
||||
disabled={!activeMetadataField.isCustom}
|
||||
fieldMetadataItem={activeMetadataField}
|
||||
/>
|
||||
</Section>
|
||||
{!isLabelIdentifier && (
|
||||
<Section>
|
||||
<H2Title
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { H2Title, IconPlus, IconSettings } from 'twenty-ui';
|
||||
import { H2Title, IconHierarchy2, IconPlus } from 'twenty-ui';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
@ -85,7 +85,7 @@ export const SettingsObjectNewFieldStep1 = () => {
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
Icon={IconSettings}
|
||||
Icon={IconHierarchy2}
|
||||
title={
|
||||
<Breadcrumb
|
||||
links={[
|
||||
|
||||
@ -1,13 +1,3 @@
|
||||
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 { H2Title, IconSettings } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
@ -15,10 +5,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
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 { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||
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';
|
||||
@ -31,6 +22,15 @@ 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';
|
||||
@ -39,12 +39,9 @@ type SettingsDataModelNewFieldFormValues = z.infer<
|
||||
ReturnType<typeof settingsFieldFormSchema>
|
||||
>;
|
||||
|
||||
const StyledSettingsObjectFieldTypeSelect = styled(
|
||||
SettingsDataModelFieldTypeSelect,
|
||||
)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
const StyledH1Title = styled(H1Title)`
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
export const SettingsObjectNewFieldStep2 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { objectSlug = '' } = useParams();
|
||||
@ -52,6 +49,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
const fieldType = searchParams.get('fieldType') as SettingsSupportedFieldType;
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [isConfigureStep, setIsConfigureStep] = useState(false);
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
@ -176,62 +174,102 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
<FormProvider // eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...formConfig}
|
||||
>
|
||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||
<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>
|
||||
<SettingsHeaderContainer>
|
||||
<Breadcrumb
|
||||
links={[
|
||||
{
|
||||
children: 'Objects',
|
||||
href: '/settings/objects',
|
||||
styles: { minWidth: 'max-content' },
|
||||
},
|
||||
{
|
||||
children: activeObjectMetadataItem.labelPlural,
|
||||
href: `/settings/objects/${objectSlug}`,
|
||||
styles: { maxWidth: '50%' },
|
||||
},
|
||||
{ children: 'New Field' },
|
||||
]}
|
||||
/>
|
||||
{!activeObjectMetadataItem.isRemote && (
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
isCancelDisabled={isSubmitting}
|
||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||
onSave={formConfig.handleSubmit(handleSave)}
|
||||
/>
|
||||
)}
|
||||
</SettingsHeaderContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Name and description"
|
||||
description="The name and description of this field"
|
||||
/>
|
||||
<SettingsDataModelFieldAboutForm
|
||||
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Type and values"
|
||||
description="The field's type and values."
|
||||
/>
|
||||
<StyledSettingsObjectFieldTypeSelect
|
||||
<StyledH1Title
|
||||
title={
|
||||
!isConfigureStep
|
||||
? '1. Select a field type'
|
||||
: '2. Configure field'
|
||||
}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
|
||||
{!isConfigureStep ? (
|
||||
<SettingsDataModelFieldTypeSelect
|
||||
excludedFieldTypes={excludedFieldTypes}
|
||||
fieldMetadataItem={{
|
||||
type: fieldType,
|
||||
}}
|
||||
onFieldTypeSelect={() => setIsConfigureStep(true)}
|
||||
/>
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
fieldMetadataItem={{
|
||||
icon: formConfig.watch('icon'),
|
||||
label: formConfig.watch('label') || 'Employees',
|
||||
type: formConfig.watch('type'),
|
||||
}}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
/>
|
||||
</Section>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user