Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -0,0 +1,191 @@
|
||||
import { useState } from 'react';
|
||||
import { DeepPartial } from 'react-hook-form';
|
||||
import { v4 } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { SettingsObjectFieldTypeSelectSectionFormValues } from '../components/SettingsObjectFieldTypeSelectSection';
|
||||
|
||||
type FormValues = {
|
||||
description?: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
type: FieldMetadataType;
|
||||
relation: SettingsObjectFieldTypeSelectSectionFormValues['relation'];
|
||||
select: SettingsObjectFieldTypeSelectSectionFormValues['select'];
|
||||
};
|
||||
|
||||
export const fieldMetadataFormDefaultValues: FormValues = {
|
||||
icon: 'IconUsers',
|
||||
label: '',
|
||||
type: FieldMetadataType.Text,
|
||||
relation: {
|
||||
type: RelationMetadataType.OneToMany,
|
||||
objectMetadataId: '',
|
||||
field: { label: '' },
|
||||
},
|
||||
select: [{ 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),
|
||||
});
|
||||
|
||||
const relationSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.Relation),
|
||||
relation: z.object({
|
||||
field: fieldSchema,
|
||||
objectMetadataId: z.string().uuid(),
|
||||
type: z.enum([
|
||||
RelationMetadataType.OneToMany,
|
||||
RelationMetadataType.OneToOne,
|
||||
'MANY_TO_ONE',
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const selectSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.Select),
|
||||
select: z
|
||||
.array(
|
||||
z.object({
|
||||
color: themeColorSchema,
|
||||
id: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
label: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.nonempty(),
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
Select: _Select,
|
||||
Relation: _Relation,
|
||||
...otherFieldTypes
|
||||
} = FieldMetadataType;
|
||||
|
||||
type OtherFieldType = Exclude<
|
||||
FieldMetadataType,
|
||||
FieldMetadataType.Relation | FieldMetadataType.Select
|
||||
>;
|
||||
|
||||
const otherFieldTypesSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.enum(
|
||||
Object.values(otherFieldTypes) as [OtherFieldType, ...OtherFieldType[]],
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = z.discriminatedUnion('type', [
|
||||
relationSchema,
|
||||
selectSchema,
|
||||
otherFieldTypesSchema,
|
||||
]);
|
||||
|
||||
type PartialFormValues = Partial<Omit<FormValues, 'relation'>> &
|
||||
DeepPartial<Pick<FormValues, 'relation'>>;
|
||||
|
||||
export const useFieldMetadataForm = () => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [initialFormValues, setInitialFormValues] = useState<FormValues>(
|
||||
fieldMetadataFormDefaultValues,
|
||||
);
|
||||
const [formValues, setFormValues] = useState<FormValues>(
|
||||
fieldMetadataFormDefaultValues,
|
||||
);
|
||||
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: PartialFormValues,
|
||||
): FormValues => ({
|
||||
...previousValues,
|
||||
...nextValues,
|
||||
relation: {
|
||||
...previousValues.relation,
|
||||
...nextValues.relation,
|
||||
field: {
|
||||
...previousValues.relation?.field,
|
||||
...nextValues.relation?.field,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initForm = (lazyInitialFormValues: PartialFormValues) => {
|
||||
if (isInitialized) return;
|
||||
|
||||
const mergedFormValues = mergePartialValues(
|
||||
initialFormValues,
|
||||
lazyInitialFormValues,
|
||||
);
|
||||
|
||||
setInitialFormValues(mergedFormValues);
|
||||
setFormValues(mergedFormValues);
|
||||
setValidationResult(schema.safeParse(mergedFormValues));
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
const handleFormChange = (values: PartialFormValues) => {
|
||||
const nextFormValues = mergePartialValues(formValues, values);
|
||||
|
||||
setFormValues(nextFormValues);
|
||||
setValidationResult(schema.safeParse(nextFormValues));
|
||||
|
||||
const {
|
||||
relation: initialRelationFormValues,
|
||||
select: initialSelectFormValues,
|
||||
...initialFieldFormValues
|
||||
} = initialFormValues;
|
||||
const {
|
||||
relation: nextRelationFormValues,
|
||||
select: nextSelectFormValues,
|
||||
...nextFieldFormValues
|
||||
} = nextFormValues;
|
||||
|
||||
setHasFieldFormChanged(
|
||||
!isDeeplyEqual(initialFieldFormValues, nextFieldFormValues),
|
||||
);
|
||||
setHasRelationFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Relation &&
|
||||
!isDeeplyEqual(initialRelationFormValues, nextRelationFormValues),
|
||||
);
|
||||
setHasSelectFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Select &&
|
||||
!isDeeplyEqual(initialSelectFormValues, nextSelectFormValues),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
formValues,
|
||||
handleFormChange,
|
||||
hasFieldFormChanged,
|
||||
hasFormChanged:
|
||||
hasFieldFormChanged || hasRelationFormChanged || hasSelectFormChanged,
|
||||
hasRelationFormChanged,
|
||||
hasSelectFormChanged,
|
||||
initForm,
|
||||
isInitialized,
|
||||
isValid: validationResult.success,
|
||||
validatedFormValues: validationResult.success
|
||||
? validationResult.data
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
|
||||
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes';
|
||||
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
|
||||
|
||||
import { useFieldPreviewValue } from './useFieldPreviewValue';
|
||||
import { useRelationFieldPreviewValue } from './useRelationFieldPreviewValue';
|
||||
|
||||
export const useFieldPreview = ({
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
relationObjectMetadataId,
|
||||
selectOptions,
|
||||
}: {
|
||||
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
|
||||
objectMetadataId: string;
|
||||
relationObjectMetadataId?: string;
|
||||
selectOptions?: SettingsObjectFieldSelectFormOption[];
|
||||
}) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
const objectMetadataItem = findObjectMetadataItemById(objectMetadataId);
|
||||
|
||||
const { Icon: ObjectIcon } = useLazyLoadIcon(objectMetadataItem?.icon ?? '');
|
||||
const { Icon: FieldIcon } = useLazyLoadIcon(fieldMetadata.icon ?? '');
|
||||
|
||||
const fieldName = fieldMetadata.id
|
||||
? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name
|
||||
: 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 settingsFieldMetadataType =
|
||||
settingsFieldMetadataTypes[fieldMetadata.type];
|
||||
|
||||
const defaultSelectValue = selectOptions?.[0];
|
||||
const selectValue =
|
||||
fieldMetadata.type === FieldMetadataType.Select &&
|
||||
typeof firstRecordFieldValue === 'string'
|
||||
? selectOptions?.find(
|
||||
(selectOption) => selectOption.value === firstRecordFieldValue,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
entityId: `${objectMetadataId}-field-form`,
|
||||
FieldIcon,
|
||||
fieldName: fieldName || `${fieldMetadata.type}-new-field`,
|
||||
ObjectIcon,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
value:
|
||||
fieldMetadata.type === FieldMetadataType.Relation
|
||||
? relationValue
|
||||
: fieldMetadata.type === FieldMetadataType.Select
|
||||
? selectValue || defaultSelectValue
|
||||
: firstRecordFieldValue || settingsFieldMetadataType?.defaultValue,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
export const useFieldPreviewValue = ({
|
||||
fieldName,
|
||||
objectNamePlural,
|
||||
skip,
|
||||
}: {
|
||||
fieldName: string;
|
||||
objectNamePlural: string;
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
objectNamePlural,
|
||||
});
|
||||
|
||||
const { records } = useFindManyRecords({
|
||||
objectNameSingular,
|
||||
skip,
|
||||
});
|
||||
|
||||
const firstRecordWithValue = records.find(
|
||||
(record) => assertNotNull(record[fieldName]) && record[fieldName] !== '',
|
||||
);
|
||||
|
||||
return {
|
||||
value: firstRecordWithValue?.[fieldName],
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
export const useRelationFieldPreviewValue = ({
|
||||
relationObjectMetadataId,
|
||||
skip,
|
||||
}: {
|
||||
relationObjectMetadataId?: string;
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
|
||||
// TODO: make this impossible to be undefined
|
||||
const relationObjectMetadataItem = relationObjectMetadataId
|
||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
const { records: relationObjects } = useFindManyRecords({
|
||||
objectNameSingular: relationObjectMetadataItem?.nameSingular ?? 'company', // TODO fix this hack
|
||||
skip: skip || !relationObjectMetadataItem,
|
||||
});
|
||||
|
||||
const label = relationObjectMetadataItem?.labelSingular ?? '';
|
||||
|
||||
return {
|
||||
relationObjectMetadataItem,
|
||||
value: relationObjects?.[0] ?? {
|
||||
company: { name: label }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented
|
||||
name: label,
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user