fix: reset default value on field type switch in Settings/Data Model … (#5436)

…field form

Closes #5412
This commit is contained in:
Thaïs
2024-05-22 09:53:15 +02:00
committed by GitHub
parent 48003887ce
commit 944b2b0254
9 changed files with 68 additions and 38 deletions

View File

@ -1,82 +0,0 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { IconCheck, IconX } from 'twenty-ui';
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { Select } from '@/ui/input/components/Select';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { isDefined } from '~/utils/isDefined';
// TODO: rename to SettingsDataModelFieldBooleanForm and move to settings/data-model/fields/forms/components
export const settingsDataModelFieldBooleanFormSchema = z.object({
defaultValue: z.boolean(),
});
type SettingsDataModelFieldBooleanFormValues = z.infer<
typeof settingsDataModelFieldBooleanFormSchema
>;
type SettingsDataModelFieldBooleanFormProps = {
className?: string;
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue'>;
};
const StyledContainer = styled(CardContent)`
padding-bottom: ${({ theme }) => theme.spacing(3.5)};
`;
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.light};
display: block;
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: 6px;
margin-top: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsDataModelFieldBooleanForm = ({
className,
fieldMetadataItem,
}: SettingsDataModelFieldBooleanFormProps) => {
const { control } = useFormContext<SettingsDataModelFieldBooleanFormValues>();
const isEditMode = isDefined(fieldMetadataItem?.defaultValue);
const initialValue = fieldMetadataItem?.defaultValue ?? true;
return (
<StyledContainer>
<StyledLabel>Default Value</StyledLabel>
<Controller
name="defaultValue"
control={control}
defaultValue={initialValue}
render={({ field: { onChange, value } }) => (
<Select
className={className}
fullWidth
// TODO: temporary fix - disabling edition because after editing the defaultValue,
// newly created records are not taking into account the updated defaultValue properly.
disabled={isEditMode}
dropdownId="object-field-default-value-select"
value={value}
onChange={onChange}
options={[
{
value: true,
label: 'True',
Icon: IconCheck,
},
{
value: false,
label: 'False',
Icon: IconX,
},
]}
/>
)}
/>
</StyledContainer>
);
};

View File

@ -1,175 +0,0 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { useIcons } from 'twenty-ui';
import { z } from 'zod';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
import { useRelationSettingsFormInitialValues } from '@/settings/data-model/fields/forms/hooks/useRelationSettingsFormInitialValues';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
import { RELATION_TYPES } from '../constants/RelationTypes';
import { RelationType } from '../types/RelationType';
// TODO: rename to SettingsDataModelFieldRelationForm and move to settings/data-model/fields/forms/components
export const settingsDataModelFieldRelationFormSchema = z.object({
relation: z.object({
field: fieldMetadataItemSchema.pick({
icon: true,
label: true,
}),
objectMetadataId: z.string().uuid(),
type: z.enum(
Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]],
),
}),
});
export type SettingsDataModelFieldRelationFormValues = z.infer<
typeof settingsDataModelFieldRelationFormSchema
>;
type SettingsDataModelFieldRelationFormProps = {
fieldMetadataItem?: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
};
const StyledContainer = styled.div`
padding: ${({ theme }) => theme.spacing(4)};
`;
const StyledSelectsContainer = styled.div`
display: grid;
gap: ${({ theme }) => theme.spacing(4)};
grid-template-columns: 1fr 1fr;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledInputsLabel = styled.span`
color: ${({ theme }) => theme.font.color.light};
display: block;
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
`;
const StyledInputsContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES)
.filter(([value]) => 'ONE_TO_ONE' !== value)
.map(([value, { label, Icon }]) => ({
label,
value: value as RelationType,
Icon,
}));
export const SettingsDataModelFieldRelationForm = ({
fieldMetadataItem,
}: SettingsDataModelFieldRelationFormProps) => {
const { control, watch: watchFormValue } =
useFormContext<SettingsDataModelFieldRelationFormValues>();
const { getIcon } = useIcons();
const { objectMetadataItems, findObjectMetadataItemById } =
useFilteredObjectMetadataItems();
const {
disableFieldEdition,
disableRelationEdition,
initialRelationFieldMetadataItem,
initialRelationObjectMetadataItem,
initialRelationType,
} = useRelationSettingsFormInitialValues({ fieldMetadataItem });
const selectedObjectMetadataItem = findObjectMetadataItemById(
watchFormValue('relation.objectMetadataId'),
);
return (
<StyledContainer>
<StyledSelectsContainer>
<Controller
name="relation.type"
control={control}
defaultValue={initialRelationType}
render={({ field: { onChange, value } }) => (
<Select
label="Relation type"
dropdownId="relation-type-select"
fullWidth
disabled={disableRelationEdition}
value={value}
options={RELATION_TYPE_OPTIONS}
onChange={onChange}
/>
)}
/>
<Controller
name="relation.objectMetadataId"
control={control}
defaultValue={initialRelationObjectMetadataItem.id}
render={({ field: { onChange, value } }) => (
<Select
label="Object destination"
dropdownId="object-destination-select"
fullWidth
disabled={disableRelationEdition}
value={value}
options={objectMetadataItems
.filter(isObjectMetadataAvailableForRelation)
.map((objectMetadataItem) => ({
label: objectMetadataItem.labelPlural,
value: objectMetadataItem.id,
Icon: getIcon(objectMetadataItem.icon),
}))}
onChange={onChange}
/>
)}
/>
</StyledSelectsContainer>
<StyledInputsLabel>
Field on {selectedObjectMetadataItem?.labelPlural}
</StyledInputsLabel>
<StyledInputsContainer>
<Controller
name="relation.field.icon"
control={control}
defaultValue={initialRelationFieldMetadataItem.icon}
render={({ field: { onChange, value } }) => (
<IconPicker
disabled={disableFieldEdition}
dropdownId="field-destination-icon-picker"
selectedIconKey={value ?? undefined}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
/>
)}
/>
<Controller
name="relation.field.label"
control={control}
defaultValue={initialRelationFieldMetadataItem.label}
render={({ field: { onChange, value } }) => (
<TextInput
disabled={disableFieldEdition}
placeholder="Field name"
value={value}
onChange={onChange}
fullWidth
/>
)}
/>
</StyledInputsContainer>
</StyledContainer>
);
};

View File

@ -1,259 +0,0 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { DropResult } from '@hello-pangea/dnd';
import { IconPlus } from 'twenty-ui';
import { z } from 'zod';
import {
FieldMetadataItem,
FieldMetadataItemOption,
} from '@/object-metadata/types/FieldMetadataItem';
import { selectOptionsSchema } from '@/object-metadata/validation-schemas/selectOptionsSchema';
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/hooks/useSelectSettingsFormInitialValues';
import { generateNewSelectOption } from '@/settings/data-model/fields/forms/utils/generateNewSelectOption';
import { isSelectOptionDefaultValue } from '@/settings/data-model/utils/isSelectOptionDefaultValue';
import { LightButton } from '@/ui/input/button/components/LightButton';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { CardFooter } from '@/ui/layout/card/components/CardFooter';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { toSpliced } from '~/utils/array/toSpliced';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';
import { SettingsObjectFieldSelectFormOptionRow } from './SettingsObjectFieldSelectFormOptionRow';
// TODO: rename to SettingsDataModelFieldSelectForm and move to settings/data-model/fields/forms/components
export const settingsDataModelFieldSelectFormSchema = z.object({
defaultValue: simpleQuotesStringSchema.nullable(),
options: selectOptionsSchema,
});
export const settingsDataModelFieldMultiSelectFormSchema = z.object({
defaultValue: z.array(simpleQuotesStringSchema).nullable(),
options: selectOptionsSchema,
});
const selectOrMultiSelectFormSchema = z.union([
settingsDataModelFieldSelectFormSchema,
settingsDataModelFieldMultiSelectFormSchema,
]);
export type SettingsDataModelFieldSelectFormValues = z.infer<
typeof selectOrMultiSelectFormSchema
>;
type SettingsDataModelFieldSelectFormProps = {
fieldMetadataItem: Pick<
FieldMetadataItem,
'defaultValue' | 'options' | 'type'
>;
};
const StyledContainer = styled(CardContent)`
padding-bottom: ${({ theme }) => theme.spacing(3.5)};
`;
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.light};
display: block;
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: 6px;
margin-top: ${({ theme }) => theme.spacing(1)};
`;
const StyledFooter = styled(CardFooter)`
background-color: ${({ theme }) => theme.background.secondary};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledButton = styled(LightButton)`
justify-content: center;
width: 100%;
`;
export const SettingsDataModelFieldSelectForm = ({
fieldMetadataItem,
}: SettingsDataModelFieldSelectFormProps) => {
const { initialDefaultValue, initialOptions } =
useSelectSettingsFormInitialValues({ fieldMetadataItem });
const {
control,
setValue: setFormValue,
watch: watchFormValue,
getValues,
} = useFormContext<SettingsDataModelFieldSelectFormValues>();
const handleDragEnd = (
values: FieldMetadataItemOption[],
result: DropResult,
onChange: (options: FieldMetadataItemOption[]) => void,
) => {
if (!result.destination) return;
const nextOptions = moveArrayItem(values, {
fromIndex: result.source.index,
toIndex: result.destination.index,
}).map((option, index) => ({ ...option, position: index }));
onChange(nextOptions);
};
const isOptionDefaultValue = (
optionValue: FieldMetadataItemOption['value'],
) =>
isSelectOptionDefaultValue(optionValue, {
type: fieldMetadataItem.type,
defaultValue: watchFormValue('defaultValue'),
});
const handleSetOptionAsDefault = (
optionValue: FieldMetadataItemOption['value'],
) => {
if (isOptionDefaultValue(optionValue)) return;
if (fieldMetadataItem.type === FieldMetadataType.Select) {
setFormValue('defaultValue', applySimpleQuotesToString(optionValue), {
shouldDirty: true,
});
return;
}
const previousDefaultValue = getValues('defaultValue');
if (
fieldMetadataItem.type === FieldMetadataType.MultiSelect &&
(Array.isArray(previousDefaultValue) || previousDefaultValue === null)
) {
setFormValue(
'defaultValue',
[
...(previousDefaultValue ?? []),
applySimpleQuotesToString(optionValue),
],
{ shouldDirty: true },
);
}
};
const handleRemoveOptionAsDefault = (
optionValue: FieldMetadataItemOption['value'],
) => {
if (!isOptionDefaultValue(optionValue)) return;
if (fieldMetadataItem.type === FieldMetadataType.Select) {
setFormValue('defaultValue', null, { shouldDirty: true });
return;
}
const previousDefaultValue = getValues('defaultValue');
if (
fieldMetadataItem.type === FieldMetadataType.MultiSelect &&
(Array.isArray(previousDefaultValue) || previousDefaultValue === null)
) {
const nextDefaultValue = previousDefaultValue?.filter(
(value) => value !== applySimpleQuotesToString(optionValue),
);
setFormValue(
'defaultValue',
nextDefaultValue?.length ? nextDefaultValue : null,
{ shouldDirty: true },
);
}
};
return (
<>
<Controller
name="defaultValue"
control={control}
defaultValue={initialDefaultValue}
render={() => <></>}
/>
<Controller
name="options"
control={control}
defaultValue={initialOptions}
render={({ field: { onChange, value: options } }) => (
<>
<StyledContainer>
<StyledLabel>Options</StyledLabel>
<DraggableList
onDragEnd={(result) => handleDragEnd(options, result, onChange)}
draggableItems={
<>
{options.map((option, index) => (
<DraggableItem
key={option.id}
draggableId={option.id}
index={index}
isDragDisabled={options.length === 1}
itemComponent={
<SettingsObjectFieldSelectFormOptionRow
key={option.id}
option={option}
onChange={(nextOption) => {
const nextOptions = toSpliced(
options,
index,
1,
nextOption,
);
onChange(nextOptions);
// Update option value in defaultValue if value has changed
if (
nextOption.value !== option.value &&
isOptionDefaultValue(option.value)
) {
handleRemoveOptionAsDefault(option.value);
handleSetOptionAsDefault(nextOption.value);
}
}}
onRemove={() => {
const nextOptions = toSpliced(
options,
index,
1,
).map((option, nextOptionIndex) => ({
...option,
position: nextOptionIndex,
}));
onChange(nextOptions);
}}
isDefault={isOptionDefaultValue(option.value)}
onSetAsDefault={() =>
handleSetOptionAsDefault(option.value)
}
onRemoveAsDefault={() =>
handleRemoveOptionAsDefault(option.value)
}
/>
}
/>
))}
</>
}
/>
</StyledContainer>
<StyledFooter>
<StyledButton
title="Add option"
Icon={IconPlus}
onClick={() =>
onChange([...options, generateNewSelectOption(options)])
}
/>
</StyledFooter>
</>
)}
/>
</>
);
};

View File

@ -1,164 +0,0 @@
import { useMemo } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
IconCheck,
IconDotsVertical,
IconGripVertical,
IconTrash,
IconX,
} from 'twenty-ui';
import { v4 } from 'uuid';
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/utils/getOptionValueFromLabel';
import { ColorSample } from '@/ui/display/color/components/ColorSample';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { TextInput } from '@/ui/input/components/TextInput';
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 { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor';
import { MAIN_COLOR_NAMES } from '@/ui/theme/constants/MainColorNames';
type SettingsObjectFieldSelectFormOptionRowProps = {
className?: string;
isDefault?: boolean;
onChange: (value: FieldMetadataItemOption) => void;
onRemove?: () => void;
onSetAsDefault?: () => void;
onRemoveAsDefault?: () => void;
option: FieldMetadataItemOption;
};
const StyledRow = styled.div`
align-items: center;
display: flex;
height: ${({ theme }) => theme.spacing(6)};
padding: ${({ theme }) => theme.spacing(1.5)} 0;
`;
const StyledColorSample = styled(ColorSample)`
cursor: pointer;
margin-left: 9px;
margin-right: 14px;
`;
const StyledOptionInput = styled(TextInput)`
flex: 1 0 auto;
margin-right: ${({ theme }) => theme.spacing(2)};
& input {
height: ${({ theme }) => theme.spacing(6)};
}
`;
export const SettingsObjectFieldSelectFormOptionRow = ({
className,
isDefault,
onChange,
onRemove,
onSetAsDefault,
onRemoveAsDefault,
option,
}: SettingsObjectFieldSelectFormOptionRowProps) => {
const theme = useTheme();
const dropdownIds = useMemo(() => {
const baseScopeId = `select-field-option-row-${v4()}`;
return { color: `${baseScopeId}-color`, actions: `${baseScopeId}-actions` };
}, []);
const { closeDropdown: closeColorDropdown } = useDropdown(dropdownIds.color);
const { closeDropdown: closeActionsDropdown } = useDropdown(
dropdownIds.actions,
);
return (
<StyledRow className={className}>
<IconGripVertical
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.extraLight}
/>
<Dropdown
dropdownId={dropdownIds.color}
dropdownPlacement="bottom-start"
dropdownHotkeyScope={{
scope: dropdownIds.color,
}}
clickableComponent={<StyledColorSample colorName={option.color} />}
dropdownComponents={
<DropdownMenu>
<DropdownMenuItemsContainer>
{MAIN_COLOR_NAMES.map((colorName) => (
<MenuItemSelectColor
key={colorName}
onClick={() => {
onChange({ ...option, color: colorName });
closeColorDropdown();
}}
color={colorName}
selected={colorName === option.color}
/>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
}
/>
<StyledOptionInput
value={option.label}
onChange={(label) =>
onChange({ ...option, label, value: getOptionValueFromLabel(label) })
}
RightIcon={isDefault ? IconCheck : undefined}
/>
<Dropdown
dropdownId={dropdownIds.actions}
dropdownPlacement="right-start"
dropdownHotkeyScope={{
scope: dropdownIds.actions,
}}
clickableComponent={<LightIconButton Icon={IconDotsVertical} />}
dropdownComponents={
<DropdownMenu>
<DropdownMenuItemsContainer>
{isDefault ? (
<MenuItem
LeftIcon={IconX}
text="Remove as default"
onClick={() => {
onRemoveAsDefault?.();
closeActionsDropdown();
}}
/>
) : (
<MenuItem
LeftIcon={IconCheck}
text="Set as default"
onClick={() => {
onSetAsDefault?.();
closeActionsDropdown();
}}
/>
)}
{!!onRemove && !isDefault && (
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text="Remove option"
onClick={() => {
onRemove();
closeActionsDropdown();
}}
/>
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
}
/>
</StyledRow>
);
};