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

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

View File

@ -1,43 +1,73 @@
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
import { Field } from '~/generated-metadata/graphql';
import { assertNotNull } from '~/utils/assert';
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
import { SettingsObjectFieldSelectFormValues } from '../components/SettingsObjectFieldSelectForm';
import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes';
import { useFieldPreviewValue } from './useFieldPreviewValue';
import { useRelationFieldPreviewValue } from './useRelationFieldPreviewValue';
export const useFieldPreview = ({
fieldMetadata,
objectMetadataId,
relationObjectMetadataId,
selectOptions,
}: {
fieldMetadata: Partial<Pick<Field, 'icon' | 'id' | 'type'>>;
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
objectMetadataId: string;
relationObjectMetadataId?: string;
selectOptions?: SettingsObjectFieldSelectFormValues;
}) => {
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
const objectMetadataItem = findObjectMetadataItemById(objectMetadataId);
const { objects } = useFindManyObjectRecords({
objectNamePlural: objectMetadataItem?.namePlural,
skip: !objectMetadataItem || !fieldMetadata.id,
});
const { Icon: ObjectIcon } = useLazyLoadIcon(objectMetadataItem?.icon ?? '');
const { Icon: FieldIcon } = useLazyLoadIcon(fieldMetadata.icon ?? '');
const [firstRecord] = objects;
const fieldName = fieldMetadata.id
? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name
: undefined;
const value =
fieldMetadata.type !== 'RELATION' && fieldName
? firstRecord?.[fieldName]
: undefined;
const { value: firstRecordFieldValue } = useFieldPreviewValue({
fieldName: fieldName || '',
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 {
entityId: firstRecord?.id || `${objectMetadataId}-no-records`,
entityId: `${objectMetadataId}-field-form`,
FieldIcon,
fieldName: fieldName || `${fieldMetadata.type}-new-field`,
hasValue: assertNotNull(value),
ObjectIcon,
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 { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { capitalize } from '~/utils/string/capitalize';
export const useRelationFieldPreview = ({
export const useRelationFieldPreviewValue = ({
relationObjectMetadataId,
skipDefaultValue,
skip,
}: {
relationObjectMetadataId?: string;
skipDefaultValue: boolean;
skip?: boolean;
}) => {
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
@ -17,18 +16,16 @@ export const useRelationFieldPreview = ({
const { objects: relationObjects } = useFindManyObjectRecords({
objectNamePlural: relationObjectMetadataItem?.namePlural,
skip: skipDefaultValue || !relationObjectMetadataItem,
skip: skip || !relationObjectMetadataItem,
});
const mockValueName = capitalize(
relationObjectMetadataItem?.nameSingular ?? '',
);
const label = relationObjectMetadataItem?.labelSingular ?? '';
return {
relationObjectMetadataItem,
defaultValue: relationObjects?.[0] ?? {
company: { name: mockValueName }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented
name: mockValueName,
value: relationObjects?.[0] ?? {
company: { name: label }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented
name: label,
},
};
};