feat: set Select field option as default option (#2725)

Closes #2591
This commit is contained in:
Thaïs
2023-11-29 12:19:56 +01:00
committed by GitHub
parent f00c05c342
commit 34a3197fee
14 changed files with 127 additions and 74 deletions

View File

@ -11,17 +11,17 @@ import { Field } from '~/generated-metadata/graphql';
import { relationTypes } from '../constants/relationTypes';
import { RelationType } from '../types/RelationType';
export type SettingsObjectFieldRelationFormValues = Partial<{
field: Partial<Pick<Field, 'icon' | 'label'>>;
export type SettingsObjectFieldRelationFormValues = {
field: Pick<Field, 'icon' | 'label'>;
objectMetadataId: string;
type: RelationType;
}>;
};
type SettingsObjectFieldRelationFormProps = {
disableFieldEdition?: boolean;
disableRelationEdition?: boolean;
onChange: (values: SettingsObjectFieldRelationFormValues) => void;
values?: SettingsObjectFieldRelationFormValues;
onChange: (values: Partial<SettingsObjectFieldRelationFormValues>) => void;
values: SettingsObjectFieldRelationFormValues;
};
const StyledContainer = styled.div`
@ -61,7 +61,7 @@ export const SettingsObjectFieldRelationForm = ({
useObjectMetadataItemForSettings();
const selectedObjectMetadataItem =
(values?.objectMetadataId
(values.objectMetadataId
? findObjectMetadataItemById(values.objectMetadataId)
: undefined) || objectMetadataItems[0];
@ -72,7 +72,7 @@ export const SettingsObjectFieldRelationForm = ({
label="Relation type"
dropdownScopeId="relation-type-select"
disabled={disableRelationEdition}
value={values?.type}
value={values.type}
options={Object.entries(relationTypes).map(
([value, { label, Icon }]) => ({
label,
@ -86,7 +86,7 @@ export const SettingsObjectFieldRelationForm = ({
label="Object destination"
dropdownScopeId="object-destination-select"
disabled={disableRelationEdition}
value={values?.objectMetadataId}
value={values.objectMetadataId}
options={objectMetadataItems.map((objectMetadataItem) => ({
label: objectMetadataItem.labelPlural,
value: objectMetadataItem.id,
@ -104,10 +104,10 @@ export const SettingsObjectFieldRelationForm = ({
<IconPicker
disabled={disableFieldEdition}
dropdownScopeId="field-destination-icon-picker"
selectedIconKey={values?.field?.icon || undefined}
selectedIconKey={values.field.icon || undefined}
onChange={(value) =>
onChange({
field: { ...values?.field, icon: value.iconKey },
field: { ...values.field, icon: value.iconKey },
})
}
variant="primary"
@ -115,11 +115,11 @@ export const SettingsObjectFieldRelationForm = ({
<TextInput
disabled={disableFieldEdition}
placeholder="Field name"
value={values?.field?.label || ''}
value={values.field.label}
onChange={(value) => {
if (!value || validateMetadataLabel(value)) {
onChange({
field: { ...values?.field, label: value },
field: { ...values.field, label: value },
});
}
}}

View File

@ -4,17 +4,16 @@ import { IconPlus } from '@/ui/display/icon';
import { Button } from '@/ui/input/button/components/Button';
import { mainColors, ThemeColor } from '@/ui/theme/constants/colors';
import {
SettingsObjectFieldSelectFormOption,
SettingsObjectFieldSelectFormOptionRow,
} from './SettingsObjectFieldSelectFormOptionRow';
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
import { SettingsObjectFieldSelectFormOptionRow } from './SettingsObjectFieldSelectFormOptionRow';
export type SettingsObjectFieldSelectFormValues =
SettingsObjectFieldSelectFormOption[];
type SettingsObjectFieldSelectFormProps = {
onChange: (values: SettingsObjectFieldSelectFormValues) => void;
values?: SettingsObjectFieldSelectFormValues;
values: SettingsObjectFieldSelectFormValues;
};
const StyledContainer = styled.div`
@ -54,31 +53,38 @@ const getNextColor = (currentColor: ThemeColor) => {
export const SettingsObjectFieldSelectForm = ({
onChange,
values = [],
values,
}: SettingsObjectFieldSelectFormProps) => {
return (
<>
<StyledContainer>
<StyledLabel>Options</StyledLabel>
<StyledRows>
{values.map((value, index) => (
{values.map((option, index) => (
<SettingsObjectFieldSelectFormOptionRow
key={index}
onChange={(optionValue) => {
const nextValues = [...values];
nextValues.splice(index, 1, optionValue);
onChange(nextValues);
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);
}}
onRemove={
values.length > 1
? () => {
const nextValues = [...values];
nextValues.splice(index, 1);
onChange(nextValues);
const nextOptions = [...values];
nextOptions.splice(index, 1);
onChange(nextOptions);
}
: undefined
}
value={value}
option={option}
/>
))}
</StyledRows>
@ -92,7 +98,7 @@ export const SettingsObjectFieldSelectForm = ({
...values,
{
color: getNextColor(values[values.length - 1].color),
text: `Option ${values.length + 1}`,
label: `Option ${values.length + 1}`,
},
])
}

View File

@ -2,7 +2,12 @@ import { useMemo } from 'react';
import styled from '@emotion/styled';
import { v4 } from 'uuid';
import { IconDotsVertical, IconTrash } from '@/ui/display/icon';
import {
IconCheck,
IconDotsVertical,
IconTrash,
IconX,
} from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -11,17 +16,14 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { ThemeColor } from '@/ui/theme/constants/colors';
export type SettingsObjectFieldSelectFormOption = {
color: ThemeColor;
text: string;
};
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
type SettingsObjectFieldSelectFormOptionRowProps = {
isDefault?: boolean;
onChange: (value: SettingsObjectFieldSelectFormOption) => void;
onRemove?: () => void;
value: SettingsObjectFieldSelectFormOption;
option: SettingsObjectFieldSelectFormOption;
};
const StyledRow = styled.div`
@ -41,9 +43,10 @@ const StyledOptionInput = styled(TextInput)`
`;
export const SettingsObjectFieldSelectFormOptionRow = ({
isDefault,
onChange,
onRemove,
value,
option,
}: SettingsObjectFieldSelectFormOptionRowProps) => {
const dropdownScopeId = useMemo(() => `select-field-option-row-${v4()}`, []);
@ -52,8 +55,9 @@ export const SettingsObjectFieldSelectFormOptionRow = ({
return (
<StyledRow>
<StyledOptionInput
value={value.text}
onChange={(text) => onChange({ ...value, text })}
value={option.label}
onChange={(label) => onChange({ ...option, label })}
RightIcon={isDefault ? IconCheck : undefined}
/>
<DropdownScope dropdownScopeId={dropdownScopeId}>
<Dropdown
@ -65,6 +69,25 @@ export const SettingsObjectFieldSelectFormOptionRow = ({
dropdownComponents={
<DropdownMenu>
<DropdownMenuItemsContainer>
{isDefault ? (
<MenuItem
LeftIcon={IconX}
text="Remove as default"
onClick={() => {
onChange({ ...option, isDefault: false });
closeDropdown();
}}
/>
) : (
<MenuItem
LeftIcon={IconCheck}
text="Set as default"
onClick={() => {
onChange({ ...option, isDefault: true });
closeDropdown();
}}
/>
)}
{!!onRemove && (
<MenuItem
accent="danger"

View File

@ -22,18 +22,20 @@ import {
} from './SettingsObjectFieldSelectForm';
import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard';
export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{
export type SettingsObjectFieldTypeSelectSectionFormValues = {
type: FieldMetadataType;
relation: SettingsObjectFieldRelationFormValues;
select: SettingsObjectFieldSelectFormValues;
}>;
};
type SettingsObjectFieldTypeSelectSectionProps = {
excludedFieldTypes?: FieldMetadataType[];
fieldMetadata: Pick<Field, 'icon' | 'label'> & { id?: string };
onChange: (values: SettingsObjectFieldTypeSelectSectionFormValues) => void;
onChange: (
values: Partial<SettingsObjectFieldTypeSelectSectionFormValues>,
) => void;
relationFieldMetadata?: Pick<Field, 'id' | 'isCustom'>;
values?: SettingsObjectFieldTypeSelectSectionFormValues;
values: SettingsObjectFieldTypeSelectSectionFormValues;
} & Pick<SettingsObjectFieldPreviewProps, 'objectMetadataId'>;
const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)`
@ -58,8 +60,8 @@ export const SettingsObjectFieldTypeSelectSection = ({
relationFieldMetadata,
values,
}: SettingsObjectFieldTypeSelectSectionProps) => {
const relationFormConfig = values?.relation;
const selectFormConfig = values?.select;
const relationFormConfig = values.relation;
const selectFormConfig = values.select;
const fieldTypeOptions = Object.entries(settingsFieldMetadataTypes)
.filter(([key]) => !excludedFieldTypes?.includes(key as FieldMetadataType))

View File

@ -26,8 +26,10 @@ const defaultValues: FormValues = {
type: FieldMetadataType.Text,
relation: {
type: RelationMetadataType.OneToMany,
objectMetadataId: '',
field: { label: '' },
},
select: [{ color: 'green', text: 'Option 1' }],
select: [{ color: 'green', label: 'Option 1' }],
};
const fieldSchema = z.object({
@ -60,7 +62,8 @@ const selectSchema = fieldSchema.merge(
color: z.enum(
Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]],
),
text: z.string().min(1),
isDefault: z.boolean().optional(),
label: z.string().min(1),
}),
)
.nonempty(),
@ -90,7 +93,7 @@ const schema = z.discriminatedUnion('type', [
otherFieldTypesSchema,
]);
type PartialFormValues = Partial<FormValues> &
type PartialFormValues = Partial<Omit<FormValues, 'relation'>> &
DeepPartial<Pick<FormValues, 'relation'>>;
export const useFieldMetadataForm = () => {

View File

@ -2,8 +2,8 @@ import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObj
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
import { SettingsObjectFieldSelectFormValues } from '../components/SettingsObjectFieldSelectForm';
import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes';
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
import { useFieldPreviewValue } from './useFieldPreviewValue';
import { useRelationFieldPreviewValue } from './useRelationFieldPreviewValue';
@ -17,7 +17,7 @@ export const useFieldPreview = ({
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
objectMetadataId: string;
relationObjectMetadataId?: string;
selectOptions?: SettingsObjectFieldSelectFormValues;
selectOptions?: SettingsObjectFieldSelectFormOption[];
}) => {
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
const objectMetadataItem = findObjectMetadataItemById(objectMetadataId);
@ -44,17 +44,16 @@ export const useFieldPreview = ({
skip: fieldMetadata.type !== FieldMetadataType.Relation,
});
const defaultValue =
fieldMetadata.type === FieldMetadataType.Enum
? selectOptions?.[0]
: settingsFieldMetadataTypes[fieldMetadata.type].defaultValue;
const { defaultValue } = settingsFieldMetadataTypes[fieldMetadata.type];
const isValidSelectValue =
const defaultSelectValue = selectOptions?.[0];
const selectValue =
fieldMetadata.type === FieldMetadataType.Enum &&
!!firstRecordFieldValue &&
selectOptions?.some(
(selectOption) => selectOption.text === firstRecordFieldValue,
);
typeof firstRecordFieldValue === 'string'
? selectOptions?.find(
(selectOption) => selectOption.value === firstRecordFieldValue,
)
: undefined;
return {
entityId: `${objectMetadataId}-field-form`,
@ -64,10 +63,10 @@ export const useFieldPreview = ({
objectMetadataItem,
relationObjectMetadataItem,
value:
(fieldMetadata.type === FieldMetadataType.Relation
fieldMetadata.type === FieldMetadataType.Relation
? relationValue
: fieldMetadata.type !== FieldMetadataType.Enum || isValidSelectValue
? firstRecordFieldValue
: undefined) || defaultValue,
: fieldMetadata.type === FieldMetadataType.Enum
? selectValue || defaultSelectValue
: firstRecordFieldValue || defaultValue,
};
};

View File

@ -0,0 +1,8 @@
import { ThemeColor } from '@/ui/theme/constants/colors';
export type SettingsObjectFieldSelectFormOption = {
color: ThemeColor;
isDefault?: boolean;
label: string;
value?: string;
};

View File

@ -13,6 +13,7 @@ import { Key } from 'ts-key-enum';
import { IconAlertCircle } from '@/ui/display/icon';
import { IconEye, IconEyeOff } from '@/ui/display/icon/index';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
@ -29,6 +30,7 @@ export type TextInputComponentProps = Omit<
fullWidth?: boolean;
disableHotkeys?: boolean;
error?: string;
RightIcon?: IconComponent;
};
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
@ -101,7 +103,7 @@ const StyledTrailingIconContainer = styled.div`
const StyledTrailingIcon = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
cursor: pointer;
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
display: flex;
justify-content: center;
`;
@ -125,6 +127,7 @@ const TextInputComponent = (
placeholder,
disabled,
tabIndex,
RightIcon,
}: TextInputComponentProps,
// eslint-disable-next-line twenty/component-props-naming
ref: ForwardedRef<HTMLInputElement>,
@ -201,6 +204,11 @@ const TextInputComponent = (
)}
</StyledTrailingIcon>
)}
{!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && (
<StyledTrailingIcon>
<RightIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
)}
</StyledTrailingIconContainer>
</StyledInputContainer>
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}

View File

@ -5,5 +5,5 @@ import { useSelectField } from '../../hooks/useSelectField';
export const SelectFieldDisplay = () => {
const { fieldValue } = useSelectField();
return <Tag color={fieldValue.color} text={fieldValue.text} />;
return <Tag color={fieldValue.color} text={fieldValue.label} />;
};

View File

@ -26,15 +26,15 @@ export const useSelectField = () => {
);
const fieldSelectValue = isFieldSelectValue(fieldValue)
? fieldValue
: { color: 'green' as ThemeColor, text: '' };
: { color: 'green' as ThemeColor, label: '' };
const fieldInitialValue = useFieldInitialValue();
const initialValue = {
color: 'green' as ThemeColor,
text: fieldInitialValue?.isEmpty
label: fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldSelectValue?.text ?? '',
: fieldInitialValue?.value ?? fieldSelectValue?.label ?? '',
};
return {

View File

@ -116,6 +116,6 @@ export type FieldCurrencyValue = {
};
export type FieldFullNameValue = { firstName: string; lastName: string };
export type FieldProbabilityValue = number;
export type FieldSelectValue = { color: ThemeColor; text: string };
export type FieldSelectValue = { color: ThemeColor; label: string };
export type FieldRelationValue = EntityForSelect | null;

View File

@ -5,7 +5,7 @@ import { mainColors, ThemeColor } from '@/ui/theme/constants/colors';
const selectColors = Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]];
const selectValueSchema = z.object({
color: z.enum(selectColors),
text: z.string(),
label: z.string(),
});
export const isFieldSelectValue = (

View File

@ -19,7 +19,10 @@ import { Button } from '@/ui/input/button/components/Button';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
export const SettingsObjectFieldEdit = () => {
const navigate = useNavigate();
@ -70,10 +73,10 @@ export const SettingsObjectFieldEdit = () => {
relation: {
field: {
icon: relationFieldMetadataItem?.icon,
label: relationFieldMetadataItem?.label,
label: relationFieldMetadataItem?.label || '',
},
objectMetadataId: relationObjectMetadataItem?.id,
type: relationType,
objectMetadataId: relationObjectMetadataItem?.id || '',
type: relationType || RelationMetadataType.OneToMany,
},
});
}, [

View File

@ -54,7 +54,8 @@ export const SettingsObjectNewFieldStep2 = () => {
initForm({
relation: {
field: { icon: activeObjectMetadataItem.icon },
objectMetadataId: findObjectMetadataItemByNamePlural('people')?.id,
objectMetadataId:
findObjectMetadataItemByNamePlural('people')?.id || '',
},
});
}, [