feat: add Select field preview and form (#2655)

Closes #2432
This commit is contained in:
Thaïs
2023-11-28 23:44:21 +01:00
committed by GitHub
parent 0fa55b0634
commit bc787f72ba
18 changed files with 317 additions and 87 deletions

View File

@ -10,15 +10,16 @@ import { Field } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect'; import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect';
import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes';
import { useFieldPreview } from '../hooks/useFieldPreview'; import { useFieldPreview } from '../hooks/useFieldPreview';
import { useRelationFieldPreview } from '../hooks/useRelationFieldPreview';
import { SettingsObjectFieldSelectFormValues } from './SettingsObjectFieldSelectForm';
export type SettingsObjectFieldPreviewProps = { export type SettingsObjectFieldPreviewProps = {
className?: string; className?: string;
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string }; fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
objectMetadataId: string; objectMetadataId: string;
relationObjectMetadataId?: string; relationObjectMetadataId?: string;
selectOptions?: SettingsObjectFieldSelectFormValues;
shrink?: boolean; shrink?: boolean;
}; };
@ -73,6 +74,7 @@ export const SettingsObjectFieldPreview = ({
fieldMetadata, fieldMetadata,
objectMetadataId, objectMetadataId,
relationObjectMetadataId, relationObjectMetadataId,
selectOptions,
shrink, shrink,
}: SettingsObjectFieldPreviewProps) => { }: SettingsObjectFieldPreviewProps) => {
const theme = useTheme(); const theme = useTheme();
@ -81,27 +83,17 @@ export const SettingsObjectFieldPreview = ({
entityId, entityId,
FieldIcon, FieldIcon,
fieldName, fieldName,
hasValue,
ObjectIcon, ObjectIcon,
objectMetadataItem, objectMetadataItem,
relationObjectMetadataItem,
value, value,
} = useFieldPreview({ } = useFieldPreview({
fieldMetadata, fieldMetadata,
objectMetadataId, objectMetadataId,
relationObjectMetadataId,
selectOptions,
}); });
const { defaultValue: relationDefaultValue, relationObjectMetadataItem } =
useRelationFieldPreview({
relationObjectMetadataId,
skipDefaultValue:
fieldMetadata.type !== FieldMetadataType.Relation || hasValue,
});
const defaultValue =
fieldMetadata.type === FieldMetadataType.Relation
? relationDefaultValue
: settingsFieldMetadataTypes[fieldMetadata.type].defaultValue;
return ( return (
<StyledContainer className={className}> <StyledContainer className={className}>
<StyledObjectSummary> <StyledObjectSummary>
@ -123,7 +115,7 @@ export const SettingsObjectFieldPreview = ({
<SettingsObjectFieldPreviewValueEffect <SettingsObjectFieldPreviewValueEffect
entityId={entityId} entityId={entityId}
fieldName={fieldName} fieldName={fieldName}
value={value ?? defaultValue} value={value}
/> />
<StyledFieldPreview shrink={shrink}> <StyledFieldPreview shrink={shrink}>
<StyledFieldLabel> <StyledFieldLabel>

View File

@ -0,0 +1,58 @@
import styled from '@emotion/styled';
import { TextInput } from '@/ui/input/components/TextInput';
import { ThemeColor } from '@/ui/theme/constants/colors';
export type SettingsObjectFieldSelectFormValues = {
color: ThemeColor;
text: string;
}[];
type SettingsObjectFieldSelectFormProps = {
onChange: (values: SettingsObjectFieldSelectFormValues) => void;
values?: SettingsObjectFieldSelectFormValues;
};
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: ${({ theme }) => theme.spacing(3)};
text-transform: uppercase;
`;
const StyledInputsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
`;
const StyledOptionInput = styled(TextInput)`
& input {
height: ${({ theme }) => theme.spacing(2)};
}
`;
export const SettingsObjectFieldSelectForm = ({
onChange,
values = [],
}: SettingsObjectFieldSelectFormProps) => {
return (
<div>
<StyledLabel>Options</StyledLabel>
<StyledInputsContainer>
{values.map((value, index) => (
<StyledOptionInput
value={value.text}
onChange={(text) => {
const nextValues = [...values];
nextValues.splice(index, 1, { ...values[index], text });
onChange(nextValues);
}}
/>
))}
</StyledInputsContainer>
</div>
);
};

View File

@ -16,11 +16,16 @@ import {
SettingsObjectFieldRelationForm, SettingsObjectFieldRelationForm,
SettingsObjectFieldRelationFormValues, SettingsObjectFieldRelationFormValues,
} from './SettingsObjectFieldRelationForm'; } from './SettingsObjectFieldRelationForm';
import {
SettingsObjectFieldSelectForm,
SettingsObjectFieldSelectFormValues,
} from './SettingsObjectFieldSelectForm';
import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard'; import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard';
export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{ export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{
type: FieldMetadataType; type: FieldMetadataType;
relation: SettingsObjectFieldRelationFormValues; relation: SettingsObjectFieldRelationFormValues;
select: SettingsObjectFieldSelectFormValues;
}>; }>;
type SettingsObjectFieldTypeSelectSectionProps = { type SettingsObjectFieldTypeSelectSectionProps = {
@ -54,6 +59,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
values, values,
}: SettingsObjectFieldTypeSelectSectionProps) => { }: SettingsObjectFieldTypeSelectSectionProps) => {
const relationFormConfig = values?.relation; const relationFormConfig = values?.relation;
const selectFormConfig = values?.select;
const fieldTypeOptions = Object.entries(settingsFieldMetadataTypes) const fieldTypeOptions = Object.entries(settingsFieldMetadataTypes)
.filter(([key]) => !excludedFieldTypes?.includes(key as FieldMetadataType)) .filter(([key]) => !excludedFieldTypes?.includes(key as FieldMetadataType))
@ -80,6 +86,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
FieldMetadataType.Boolean, FieldMetadataType.Boolean,
FieldMetadataType.Currency, FieldMetadataType.Currency,
FieldMetadataType.DateTime, FieldMetadataType.DateTime,
FieldMetadataType.Enum,
FieldMetadataType.Link, FieldMetadataType.Link,
FieldMetadataType.Number, FieldMetadataType.Number,
FieldMetadataType.Relation, FieldMetadataType.Relation,
@ -98,6 +105,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
relationObjectMetadataId={ relationObjectMetadataId={
relationFormConfig?.objectMetadataId relationFormConfig?.objectMetadataId
} }
selectOptions={selectFormConfig}
/> />
{values.type === FieldMetadataType.Relation && {values.type === FieldMetadataType.Relation &&
!!relationFormConfig?.type && !!relationFormConfig?.type &&
@ -127,7 +135,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
</> </>
} }
form={ form={
values.type === FieldMetadataType.Relation && ( values.type === FieldMetadataType.Relation ? (
<SettingsObjectFieldRelationForm <SettingsObjectFieldRelationForm
disableFieldEdition={ disableFieldEdition={
relationFieldMetadata && !relationFieldMetadata.isCustom relationFieldMetadata && !relationFieldMetadata.isCustom
@ -140,7 +148,12 @@ export const SettingsObjectFieldTypeSelectSection = ({
}) })
} }
/> />
) ) : values.type === FieldMetadataType.Enum ? (
<SettingsObjectFieldSelectForm
values={selectFormConfig}
onChange={(nextValues) => onChange({ select: nextValues })}
/>
) : undefined
} }
/> />
)} )}

View File

@ -61,7 +61,6 @@ export const settingsFieldMetadataTypes: Record<
[FieldMetadataType.Enum]: { [FieldMetadataType.Enum]: {
label: 'Select', label: 'Select',
Icon: IconTag, Icon: IconTag,
defaultValue: { color: 'green', text: 'Option 1' },
}, },
[FieldMetadataType.Currency]: { [FieldMetadataType.Currency]: {
label: 'Currency', label: 'Currency',

View File

@ -2,6 +2,7 @@ import { useState } from 'react';
import { DeepPartial } from 'react-hook-form'; import { DeepPartial } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { mainColors, ThemeColor } from '@/ui/theme/constants/colors';
import { import {
FieldMetadataType, FieldMetadataType,
RelationMetadataType, RelationMetadataType,
@ -16,6 +17,7 @@ type FormValues = {
label: string; label: string;
type: FieldMetadataType; type: FieldMetadataType;
relation: SettingsObjectFieldTypeSelectSectionFormValues['relation']; relation: SettingsObjectFieldTypeSelectSectionFormValues['relation'];
select: SettingsObjectFieldTypeSelectSectionFormValues['select'];
}; };
const defaultValues: FormValues = { const defaultValues: FormValues = {
@ -25,6 +27,7 @@ const defaultValues: FormValues = {
relation: { relation: {
type: RelationMetadataType.OneToMany, type: RelationMetadataType.OneToMany,
}, },
select: [{ color: 'green', text: 'Option 1' }],
}; };
const fieldSchema = z.object({ const fieldSchema = z.object({
@ -48,7 +51,27 @@ const relationSchema = fieldSchema.merge(
}), }),
); );
const { Relation: _, ...otherFieldTypes } = FieldMetadataType; const selectSchema = fieldSchema.merge(
z.object({
type: z.literal(FieldMetadataType.Enum),
select: z
.array(
z.object({
color: z.enum(
Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]],
),
text: z.string().min(1),
}),
)
.nonempty(),
}),
);
const {
Enum: _Enum,
Relation: _Relation,
...otherFieldTypes
} = FieldMetadataType;
const otherFieldTypesSchema = fieldSchema.merge( const otherFieldTypesSchema = fieldSchema.merge(
z.object({ z.object({
@ -63,9 +86,13 @@ const otherFieldTypesSchema = fieldSchema.merge(
const schema = z.discriminatedUnion('type', [ const schema = z.discriminatedUnion('type', [
relationSchema, relationSchema,
selectSchema,
otherFieldTypesSchema, otherFieldTypesSchema,
]); ]);
type PartialFormValues = Partial<FormValues> &
DeepPartial<Pick<FormValues, 'relation'>>;
export const useFieldMetadataForm = () => { export const useFieldMetadataForm = () => {
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const [initialFormValues, setInitialFormValues] = const [initialFormValues, setInitialFormValues] =
@ -73,14 +100,15 @@ export const useFieldMetadataForm = () => {
const [formValues, setFormValues] = useState<FormValues>(defaultValues); const [formValues, setFormValues] = useState<FormValues>(defaultValues);
const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false); const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false);
const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false); const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false);
const [hasSelectFormChanged, setHasSelectFormChanged] = useState(false);
const [validationResult, setValidationResult] = useState( const [validationResult, setValidationResult] = useState(
schema.safeParse(formValues), schema.safeParse(formValues),
); );
const mergePartialValues = ( const mergePartialValues = (
previousValues: FormValues, previousValues: FormValues,
nextValues: DeepPartial<FormValues>, nextValues: PartialFormValues,
) => ({ ): FormValues => ({
...previousValues, ...previousValues,
...nextValues, ...nextValues,
relation: { relation: {
@ -93,7 +121,7 @@ export const useFieldMetadataForm = () => {
}, },
}); });
const initForm = (lazyInitialFormValues: DeepPartial<FormValues>) => { const initForm = (lazyInitialFormValues: PartialFormValues) => {
if (isInitialized) return; if (isInitialized) return;
const mergedFormValues = mergePartialValues( const mergedFormValues = mergePartialValues(
@ -107,16 +135,22 @@ export const useFieldMetadataForm = () => {
setIsInitialized(true); setIsInitialized(true);
}; };
const handleFormChange = (values: DeepPartial<FormValues>) => { const handleFormChange = (values: PartialFormValues) => {
const nextFormValues = mergePartialValues(formValues, values); const nextFormValues = mergePartialValues(formValues, values);
setFormValues(nextFormValues); setFormValues(nextFormValues);
setValidationResult(schema.safeParse(nextFormValues)); setValidationResult(schema.safeParse(nextFormValues));
const { relation: initialRelationFormValues, ...initialFieldFormValues } = const {
initialFormValues; relation: initialRelationFormValues,
const { relation: nextRelationFormValues, ...nextFieldFormValues } = select: initialSelectFormValues,
nextFormValues; ...initialFieldFormValues
} = initialFormValues;
const {
relation: nextRelationFormValues,
select: nextSelectFormValues,
...nextFieldFormValues
} = nextFormValues;
setHasFieldFormChanged( setHasFieldFormChanged(
!isDeeplyEqual(initialFieldFormValues, nextFieldFormValues), !isDeeplyEqual(initialFieldFormValues, nextFieldFormValues),
@ -125,13 +159,18 @@ export const useFieldMetadataForm = () => {
nextFieldFormValues.type === FieldMetadataType.Relation && nextFieldFormValues.type === FieldMetadataType.Relation &&
!isDeeplyEqual(initialRelationFormValues, nextRelationFormValues), !isDeeplyEqual(initialRelationFormValues, nextRelationFormValues),
); );
setHasSelectFormChanged(
nextFieldFormValues.type === FieldMetadataType.Enum &&
!isDeeplyEqual(initialSelectFormValues, nextSelectFormValues),
);
}; };
return { return {
formValues, formValues,
handleFormChange, handleFormChange,
hasFieldFormChanged, hasFieldFormChanged,
hasFormChanged: hasFieldFormChanged || hasRelationFormChanged, hasFormChanged:
hasFieldFormChanged || hasRelationFormChanged || hasSelectFormChanged,
hasRelationFormChanged, hasRelationFormChanged,
initForm, initForm,
isInitialized, isInitialized,

View File

@ -1,43 +1,73 @@
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon'; import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
import { Field } from '~/generated-metadata/graphql'; import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
import { assertNotNull } from '~/utils/assert';
import { SettingsObjectFieldSelectFormValues } from '../components/SettingsObjectFieldSelectForm';
import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes';
import { useFieldPreviewValue } from './useFieldPreviewValue';
import { useRelationFieldPreviewValue } from './useRelationFieldPreviewValue';
export const useFieldPreview = ({ export const useFieldPreview = ({
fieldMetadata, fieldMetadata,
objectMetadataId, objectMetadataId,
relationObjectMetadataId,
selectOptions,
}: { }: {
fieldMetadata: Partial<Pick<Field, 'icon' | 'id' | 'type'>>; fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
objectMetadataId: string; objectMetadataId: string;
relationObjectMetadataId?: string;
selectOptions?: SettingsObjectFieldSelectFormValues;
}) => { }) => {
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings(); const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
const objectMetadataItem = findObjectMetadataItemById(objectMetadataId); const objectMetadataItem = findObjectMetadataItemById(objectMetadataId);
const { objects } = useFindManyObjectRecords({
objectNamePlural: objectMetadataItem?.namePlural,
skip: !objectMetadataItem || !fieldMetadata.id,
});
const { Icon: ObjectIcon } = useLazyLoadIcon(objectMetadataItem?.icon ?? ''); const { Icon: ObjectIcon } = useLazyLoadIcon(objectMetadataItem?.icon ?? '');
const { Icon: FieldIcon } = useLazyLoadIcon(fieldMetadata.icon ?? ''); const { Icon: FieldIcon } = useLazyLoadIcon(fieldMetadata.icon ?? '');
const [firstRecord] = objects;
const fieldName = fieldMetadata.id const fieldName = fieldMetadata.id
? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name ? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name
: undefined; : undefined;
const value =
fieldMetadata.type !== 'RELATION' && fieldName const { value: firstRecordFieldValue } = useFieldPreviewValue({
? firstRecord?.[fieldName] fieldName: fieldName || '',
: undefined; objectNamePlural: objectMetadataItem?.namePlural || '',
skip:
!fieldName ||
!objectMetadataItem ||
fieldMetadata.type === FieldMetadataType.Relation,
});
const { relationObjectMetadataItem, value: relationValue } =
useRelationFieldPreviewValue({
relationObjectMetadataId,
skip: fieldMetadata.type !== FieldMetadataType.Relation,
});
const defaultValue =
fieldMetadata.type === FieldMetadataType.Enum
? selectOptions?.[0]
: settingsFieldMetadataTypes[fieldMetadata.type].defaultValue;
const isValidSelectValue =
fieldMetadata.type === FieldMetadataType.Enum &&
!!firstRecordFieldValue &&
selectOptions?.some(
(selectOption) => selectOption.text === firstRecordFieldValue,
);
return { return {
entityId: firstRecord?.id || `${objectMetadataId}-no-records`, entityId: `${objectMetadataId}-field-form`,
FieldIcon, FieldIcon,
fieldName: fieldName || `${fieldMetadata.type}-new-field`, fieldName: fieldName || `${fieldMetadata.type}-new-field`,
hasValue: assertNotNull(value),
ObjectIcon, ObjectIcon,
objectMetadataItem, objectMetadataItem,
value, relationObjectMetadataItem,
value:
(fieldMetadata.type === FieldMetadataType.Relation
? relationValue
: fieldMetadata.type !== FieldMetadataType.Enum || isValidSelectValue
? firstRecordFieldValue
: undefined) || defaultValue,
}; };
}; };

View File

@ -0,0 +1,25 @@
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { assertNotNull } from '~/utils/assert';
export const useFieldPreviewValue = ({
fieldName,
objectNamePlural,
skip,
}: {
fieldName: string;
objectNamePlural: string;
skip?: boolean;
}) => {
const { objects } = useFindManyObjectRecords({
objectNamePlural,
skip,
});
const firstRecordWithValue = objects.find(
(record) => assertNotNull(record[fieldName]) && record[fieldName] !== '',
);
return {
value: firstRecordWithValue?.[fieldName],
};
};

View File

@ -1,13 +1,12 @@
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { capitalize } from '~/utils/string/capitalize';
export const useRelationFieldPreview = ({ export const useRelationFieldPreviewValue = ({
relationObjectMetadataId, relationObjectMetadataId,
skipDefaultValue, skip,
}: { }: {
relationObjectMetadataId?: string; relationObjectMetadataId?: string;
skipDefaultValue: boolean; skip?: boolean;
}) => { }) => {
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings(); const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
@ -17,18 +16,16 @@ export const useRelationFieldPreview = ({
const { objects: relationObjects } = useFindManyObjectRecords({ const { objects: relationObjects } = useFindManyObjectRecords({
objectNamePlural: relationObjectMetadataItem?.namePlural, objectNamePlural: relationObjectMetadataItem?.namePlural,
skip: skipDefaultValue || !relationObjectMetadataItem, skip: skip || !relationObjectMetadataItem,
}); });
const mockValueName = capitalize( const label = relationObjectMetadataItem?.labelSingular ?? '';
relationObjectMetadataItem?.nameSingular ?? '',
);
return { return {
relationObjectMetadataItem, relationObjectMetadataItem,
defaultValue: relationObjects?.[0] ?? { value: relationObjects?.[0] ?? {
company: { name: mockValueName }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented company: { name: label }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented
name: mockValueName, name: label,
}, },
}; };
}; };

View File

@ -1,28 +1,29 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { ChipFieldDisplay } from '@/ui/object/field/meta-types/display/components/ChipFieldDisplay';
import { FullNameFieldDisplay } from '@/ui/object/field/meta-types/display/components/FullNameFieldDisplay';
import { LinkFieldDisplay } from '@/ui/object/field/meta-types/display/components/LinkFieldDisplay';
import { RelationFieldDisplay } from '@/ui/object/field/meta-types/display/components/RelationFieldDisplay';
import { UuidFieldDisplay } from '@/ui/object/field/meta-types/display/components/UuidFieldDisplay';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { isFieldLink } from '@/ui/object/field/types/guards/isFieldLink';
import { isFieldUuid } from '@/ui/object/field/types/guards/isFieldUuid';
import { FieldContext } from '../contexts/FieldContext'; import { FieldContext } from '../contexts/FieldContext';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay'; import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay'; import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay'; import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay'; import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay'; import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay';
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay'; import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
import { isFieldCurrency } from '../types/guards/isFieldCurrency'; import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDateTime } from '../types/guards/isFieldDateTime'; import { isFieldDateTime } from '../types/guards/isFieldDateTime';
import { isFieldEmail } from '../types/guards/isFieldEmail'; import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldFullName } from '../types/guards/isFieldFullName';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldNumber } from '../types/guards/isFieldNumber'; import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone'; import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRelation } from '../types/guards/isFieldRelation'; import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldSelect } from '../types/guards/isFieldSelect';
import { isFieldText } from '../types/guards/isFieldText'; import { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid';
export const FieldDisplay = () => { export const FieldDisplay = () => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext); const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
@ -55,6 +56,8 @@ export const FieldDisplay = () => {
<FullNameFieldDisplay /> <FullNameFieldDisplay />
) : isFieldPhone(fieldDefinition) ? ( ) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldDisplay /> <PhoneFieldDisplay />
) : isFieldSelect(fieldDefinition) ? (
<SelectFieldDisplay />
) : ( ) : (
<></> <></>
)} )}

View File

@ -0,0 +1,9 @@
import { Tag } from '@/ui/display/tag/components/Tag';
import { useSelectField } from '../../hooks/useSelectField';
export const SelectFieldDisplay = () => {
const { fieldValue } = useSelectField();
return <Tag color={fieldValue.color} text={fieldValue.text} />;
};

View File

@ -0,0 +1,47 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldSelectValue } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldSelect } from '../../types/guards/isFieldSelect';
import { isFieldSelectValue } from '../../types/guards/isFieldSelectValue';
export const useSelectField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('ENUM', isFieldSelect, fieldDefinition);
const { fieldName } = fieldDefinition.metadata;
const [fieldValue, setFieldValue] = useRecoilState<FieldSelectValue>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldSelectValue = isFieldSelectValue(fieldValue)
? fieldValue
: { color: 'green' as ThemeColor, text: '' };
const fieldInitialValue = useFieldInitialValue();
const initialValue = {
color: 'green' as ThemeColor,
text: fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldSelectValue?.text ?? '',
};
return {
fieldDefinition,
fieldValue: fieldSelectValue,
initialValue,
setFieldValue,
hotkeyScope,
};
};

View File

@ -1,4 +1,5 @@
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { ThemeColor } from '@/ui/theme/constants/colors';
export type FieldUuidMetadata = { export type FieldUuidMetadata = {
objectMetadataNameSingular?: string; objectMetadataNameSingular?: string;
@ -80,19 +81,24 @@ export type FieldRelationMetadata = {
relationObjectMetadataNamePlural: string; relationObjectMetadataNamePlural: string;
}; };
export type FieldSelectMetadata = {
fieldName: string;
};
export type FieldMetadata = export type FieldMetadata =
| FieldBooleanMetadata | FieldBooleanMetadata
| FieldNumberMetadata
| FieldDateTimeMetadata
| FieldTextMetadata
| FieldUuidMetadata
| FieldCurrencyMetadata | FieldCurrencyMetadata
| FieldLinkMetadata | FieldDateTimeMetadata
| FieldPhoneMetadata
| FieldEmailMetadata | FieldEmailMetadata
| FieldFullNameMetadata
| FieldLinkMetadata
| FieldNumberMetadata
| FieldPhoneMetadata
| FieldProbabilityMetadata | FieldProbabilityMetadata
| FieldRelationMetadata | FieldRelationMetadata
| FieldFullNameMetadata; | FieldSelectMetadata
| FieldTextMetadata
| FieldUuidMetadata;
export type FieldTextValue = string; export type FieldTextValue = string;
export type FieldUUidValue = string; export type FieldUUidValue = string;
@ -109,5 +115,6 @@ export type FieldCurrencyValue = {
}; };
export type FieldFullNameValue = { firstName: string; lastName: string }; export type FieldFullNameValue = { firstName: string; lastName: string };
export type FieldProbabilityValue = number; export type FieldProbabilityValue = number;
export type FieldSelectValue = { color: ThemeColor; text: string };
export type FieldRelationValue = EntityForSelect | null; export type FieldRelationValue = EntityForSelect | null;

View File

@ -11,6 +11,7 @@ import {
FieldPhoneMetadata, FieldPhoneMetadata,
FieldProbabilityMetadata, FieldProbabilityMetadata,
FieldRelationMetadata, FieldRelationMetadata,
FieldSelectMetadata,
FieldTextMetadata, FieldTextMetadata,
FieldUuidMetadata, FieldUuidMetadata,
} from '../FieldMetadata'; } from '../FieldMetadata';
@ -28,6 +29,8 @@ type AssertFieldMetadataFunction = <
? FieldDateTimeMetadata ? FieldDateTimeMetadata
: E extends 'EMAIL' : E extends 'EMAIL'
? FieldEmailMetadata ? FieldEmailMetadata
: E extends 'ENUM'
? FieldSelectMetadata
: E extends 'LINK' : E extends 'LINK'
? FieldLinkMetadata ? FieldLinkMetadata
: E extends 'NUMBER' : E extends 'NUMBER'

View File

@ -1,14 +0,0 @@
import { z } from 'zod';
import { mainColors, ThemeColor } from '@/ui/theme/constants/colors';
const enumColors = Object.keys(mainColors) as [ThemeColor, ...ThemeColor[]];
const enumValueSchema = z.object({
color: z.enum(enumColors),
text: z.string(),
});
export const isFieldEnumValue = (
fieldValue: unknown,
): fieldValue is z.infer<typeof enumValueSchema> =>
enumValueSchema.safeParse(fieldValue).success;

View File

@ -0,0 +1,6 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldSelectMetadata } from '../FieldMetadata';
export const isFieldSelect = (
field: FieldDefinition<FieldMetadata>,
): field is FieldDefinition<FieldSelectMetadata> => field.type === 'ENUM';

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
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(),
});
export const isFieldSelectValue = (
fieldValue: unknown,
): fieldValue is z.infer<typeof selectValueSchema> =>
selectValueSchema.safeParse(fieldValue).success;

View File

@ -172,6 +172,7 @@ export const SettingsObjectFieldEdit = () => {
values={{ values={{
type: formValues.type, type: formValues.type,
relation: formValues.relation, relation: formValues.relation,
select: formValues.select,
}} }}
/> />
<Section> <Section>

View File

@ -223,6 +223,7 @@ export const SettingsObjectNewFieldStep2 = () => {
values={{ values={{
type: formValues.type, type: formValues.type,
relation: formValues.relation, relation: formValues.relation,
select: formValues.select,
}} }}
/> />
</SettingsPageContainer> </SettingsPageContainer>