4778 multi select field front implement multi select type (#4887)
This commit is contained in:
@ -24,6 +24,7 @@ export type SettingsObjectFieldSelectFormValues =
|
||||
type SettingsObjectFieldSelectFormProps = {
|
||||
onChange: (values: SettingsObjectFieldSelectFormValues) => void;
|
||||
values: SettingsObjectFieldSelectFormValues;
|
||||
isMultiSelect?: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled(CardContent)`
|
||||
@ -60,6 +61,7 @@ const getNextColor = (currentColor: ThemeColor) => {
|
||||
export const SettingsObjectFieldSelectForm = ({
|
||||
onChange,
|
||||
values,
|
||||
isMultiSelect = false,
|
||||
}: SettingsObjectFieldSelectFormProps) => {
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
@ -72,6 +74,38 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
onChange(nextOptions);
|
||||
};
|
||||
|
||||
const handleDefaultValueChange = (
|
||||
index: number,
|
||||
option: SettingsObjectFieldSelectFormOption,
|
||||
nextOption: SettingsObjectFieldSelectFormOption,
|
||||
forceUniqueDefaultValue: boolean,
|
||||
) => {
|
||||
const computeUniqueDefaultValue =
|
||||
forceUniqueDefaultValue && option.isDefault !== nextOption.isDefault;
|
||||
|
||||
const nextOptions = computeUniqueDefaultValue
|
||||
? values.map((value) => ({
|
||||
...value,
|
||||
isDefault: false,
|
||||
}))
|
||||
: [...values];
|
||||
|
||||
nextOptions.splice(index, 1, nextOption);
|
||||
onChange(nextOptions);
|
||||
};
|
||||
|
||||
const findNewLabel = () => {
|
||||
let optionIndex = values.length + 1;
|
||||
while (optionIndex < 100) {
|
||||
const newLabel = `Option ${optionIndex}`;
|
||||
if (!values.map((value) => value.label).includes(newLabel)) {
|
||||
return newLabel;
|
||||
}
|
||||
optionIndex += 1;
|
||||
}
|
||||
return `Option 100`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer>
|
||||
@ -91,18 +125,12 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
key={option.value}
|
||||
isDefault={option.isDefault}
|
||||
onChange={(nextOption) => {
|
||||
const hasDefaultOptionChanged =
|
||||
!option.isDefault && nextOption.isDefault;
|
||||
const nextOptions = hasDefaultOptionChanged
|
||||
? values.map((value) => ({
|
||||
...value,
|
||||
isDefault: false,
|
||||
}))
|
||||
: [...values];
|
||||
|
||||
nextOptions.splice(index, 1, nextOption);
|
||||
|
||||
onChange(nextOptions);
|
||||
handleDefaultValueChange(
|
||||
index,
|
||||
option,
|
||||
nextOption,
|
||||
!isMultiSelect,
|
||||
);
|
||||
}}
|
||||
onRemove={
|
||||
values.length > 1
|
||||
@ -131,7 +159,7 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
...values,
|
||||
{
|
||||
color: getNextColor(values[values.length - 1].color),
|
||||
label: `Option ${values.length + 1}`,
|
||||
label: findNewLabel(),
|
||||
value: v4(),
|
||||
},
|
||||
])
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
IconPhone,
|
||||
IconRelationManyToMany,
|
||||
IconTag,
|
||||
IconTags,
|
||||
IconTextSize,
|
||||
IconUser,
|
||||
} from 'twenty-ui';
|
||||
@ -75,8 +76,8 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
|
||||
Icon: IconTag,
|
||||
},
|
||||
[FieldMetadataType.MultiSelect]: {
|
||||
label: 'MultiSelect',
|
||||
Icon: IconTag,
|
||||
label: 'Multi-select',
|
||||
Icon: IconTags,
|
||||
},
|
||||
[FieldMetadataType.Currency]: {
|
||||
label: 'Currency',
|
||||
|
||||
@ -27,6 +27,7 @@ export type SettingsDataModelFieldSettingsFormValues = {
|
||||
currency: SettingsObjectFieldCurrencyFormValues;
|
||||
relation: SettingsObjectFieldRelationFormValues;
|
||||
select: SettingsObjectFieldSelectFormValues;
|
||||
multiSelect: SettingsObjectFieldSelectFormValues;
|
||||
defaultValue: any;
|
||||
};
|
||||
|
||||
@ -63,6 +64,7 @@ const previewableTypes = [
|
||||
FieldMetadataType.Currency,
|
||||
FieldMetadataType.DateTime,
|
||||
FieldMetadataType.Select,
|
||||
FieldMetadataType.MultiSelect,
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Number,
|
||||
FieldMetadataType.Rating,
|
||||
@ -98,7 +100,11 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
shrink={fieldMetadataItem.type === FieldMetadataType.Relation}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
selectOptions={values.select}
|
||||
selectOptions={
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
||||
? values.multiSelect
|
||||
: values.select
|
||||
}
|
||||
/>
|
||||
{fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
!!relationObjectMetadataItem && (
|
||||
@ -155,6 +161,14 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
onChange({ select: nextSelectValues })
|
||||
}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
|
||||
<SettingsObjectFieldSelectForm
|
||||
values={values.multiSelect}
|
||||
onChange={(nextMultiSelectValues) =>
|
||||
onChange({ multiSelect: nextMultiSelectValues })
|
||||
}
|
||||
isMultiSelect={true}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Boolean ? (
|
||||
<SettingsDataModelDefaultValueForm
|
||||
value={values.defaultValue}
|
||||
|
||||
@ -26,6 +26,7 @@ const defaultValues = {
|
||||
currency: fieldMetadataFormDefaultValues.currency,
|
||||
relation: fieldMetadataFormDefaultValues.relation,
|
||||
select: fieldMetadataFormDefaultValues.select,
|
||||
multiSelect: fieldMetadataFormDefaultValues.multiSelect,
|
||||
defaultValue: fieldMetadataFormDefaultValues.defaultValue,
|
||||
};
|
||||
|
||||
|
||||
@ -27,6 +27,9 @@ describe('useFieldMetadataForm', () => {
|
||||
select: [
|
||||
{ color: 'green', label: 'Option 1', value: expect.any(String) },
|
||||
],
|
||||
multiSelect: [
|
||||
{ color: 'green', label: 'Option 1', value: expect.any(String) },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -34,13 +34,19 @@ export const fieldMetadataFormDefaultValues: FormValues = {
|
||||
},
|
||||
defaultValue: null,
|
||||
select: [{ color: 'green', label: 'Option 1', value: v4() }],
|
||||
multiSelect: [{ color: 'green', label: 'Option 1', value: v4() }],
|
||||
};
|
||||
|
||||
const fieldSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
icon: z.string().startsWith('Icon'),
|
||||
label: z.string().min(1),
|
||||
defaultValue: z.any(),
|
||||
type: z.enum(
|
||||
Object.values(FieldMetadataType) as [
|
||||
FieldMetadataType,
|
||||
...FieldMetadataType[],
|
||||
],
|
||||
),
|
||||
});
|
||||
|
||||
const currencySchema = fieldSchema.merge(
|
||||
@ -83,10 +89,27 @@ const selectSchema = fieldSchema.merge(
|
||||
}),
|
||||
);
|
||||
|
||||
const multiSelectSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.MultiSelect),
|
||||
multiSelect: z
|
||||
.array(
|
||||
z.object({
|
||||
color: themeColorSchema,
|
||||
id: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
label: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.nonempty(),
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
Currency: _Currency,
|
||||
Relation: _Relation,
|
||||
Select: _Select,
|
||||
MultiSelect: _MultiSelect,
|
||||
...otherFieldTypes
|
||||
} = FieldMetadataType;
|
||||
|
||||
@ -95,6 +118,7 @@ type OtherFieldType = Exclude<
|
||||
| FieldMetadataType.Currency
|
||||
| FieldMetadataType.Relation
|
||||
| FieldMetadataType.Select
|
||||
| FieldMetadataType.MultiSelect
|
||||
>;
|
||||
|
||||
const otherFieldTypesSchema = fieldSchema.merge(
|
||||
@ -109,6 +133,7 @@ const schema = z.discriminatedUnion('type', [
|
||||
currencySchema,
|
||||
relationSchema,
|
||||
selectSchema,
|
||||
multiSelectSchema,
|
||||
otherFieldTypesSchema,
|
||||
]);
|
||||
|
||||
@ -127,6 +152,8 @@ export const useFieldMetadataForm = () => {
|
||||
const [hasCurrencyFormChanged, setHasCurrencyFormChanged] = useState(false);
|
||||
const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false);
|
||||
const [hasSelectFormChanged, setHasSelectFormChanged] = useState(false);
|
||||
const [hasMultiSelectFormChanged, setHasMultiSelectFormChanged] =
|
||||
useState(false);
|
||||
const [hasDefaultValueChanged, setHasDefaultValueFormChanged] =
|
||||
useState(false);
|
||||
const [validationResult, setValidationResult] = useState(
|
||||
@ -174,13 +201,15 @@ export const useFieldMetadataForm = () => {
|
||||
currency: initialCurrencyFormValues,
|
||||
relation: initialRelationFormValues,
|
||||
select: initialSelectFormValues,
|
||||
defaultValue: initalDefaultValue,
|
||||
multiSelect: initialMultiSelectFormValues,
|
||||
defaultValue: initialDefaultValue,
|
||||
...initialFieldFormValues
|
||||
} = initialFormValues;
|
||||
const {
|
||||
currency: nextCurrencyFormValues,
|
||||
relation: nextRelationFormValues,
|
||||
select: nextSelectFormValues,
|
||||
multiSelect: nextMultiSelectFormValues,
|
||||
defaultValue: nextDefaultValue,
|
||||
...nextFieldFormValues
|
||||
} = nextFormValues;
|
||||
@ -200,9 +229,13 @@ export const useFieldMetadataForm = () => {
|
||||
nextFieldFormValues.type === FieldMetadataType.Select &&
|
||||
!isDeeplyEqual(initialSelectFormValues, nextSelectFormValues),
|
||||
);
|
||||
setHasMultiSelectFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.MultiSelect &&
|
||||
!isDeeplyEqual(initialMultiSelectFormValues, nextMultiSelectFormValues),
|
||||
);
|
||||
setHasDefaultValueFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Boolean &&
|
||||
!isDeeplyEqual(initalDefaultValue, nextDefaultValue),
|
||||
!isDeeplyEqual(initialDefaultValue, nextDefaultValue),
|
||||
);
|
||||
};
|
||||
|
||||
@ -215,9 +248,11 @@ export const useFieldMetadataForm = () => {
|
||||
hasCurrencyFormChanged ||
|
||||
hasRelationFormChanged ||
|
||||
hasSelectFormChanged ||
|
||||
hasMultiSelectFormChanged ||
|
||||
hasDefaultValueChanged,
|
||||
hasRelationFormChanged,
|
||||
hasSelectFormChanged,
|
||||
hasMultiSelectFormChanged,
|
||||
hasDefaultValueChanged,
|
||||
initForm,
|
||||
isInitialized,
|
||||
|
||||
@ -21,7 +21,6 @@ export const getFieldDefaultPreviewValue = ({
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
}) => {
|
||||
// Select field
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Select &&
|
||||
isDefined(selectOptions)
|
||||
@ -31,7 +30,13 @@ export const getFieldDefaultPreviewValue = ({
|
||||
return defaultSelectOption.value;
|
||||
}
|
||||
|
||||
// Relation field
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect &&
|
||||
isDefined(selectOptions)
|
||||
) {
|
||||
return selectOptions.map((selectOption) => selectOption.value);
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
isDefined(relationObjectMetadataItem)
|
||||
@ -60,7 +65,6 @@ export const getFieldDefaultPreviewValue = ({
|
||||
return defaultRelationRecord;
|
||||
}
|
||||
|
||||
// Other fields
|
||||
const isLabelIdentifier =
|
||||
!!fieldMetadataItem.id &&
|
||||
!!fieldMetadataItem.name &&
|
||||
|
||||
Reference in New Issue
Block a user