@ -10,15 +10,16 @@ import { Field } from '~/generated/graphql';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect';
|
||||
import { settingsFieldMetadataTypes } from '../constants/settingsFieldMetadataTypes';
|
||||
import { useFieldPreview } from '../hooks/useFieldPreview';
|
||||
import { useRelationFieldPreview } from '../hooks/useRelationFieldPreview';
|
||||
|
||||
import { SettingsObjectFieldSelectFormValues } from './SettingsObjectFieldSelectForm';
|
||||
|
||||
export type SettingsObjectFieldPreviewProps = {
|
||||
className?: string;
|
||||
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
|
||||
objectMetadataId: string;
|
||||
relationObjectMetadataId?: string;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
shrink?: boolean;
|
||||
};
|
||||
|
||||
@ -73,6 +74,7 @@ export const SettingsObjectFieldPreview = ({
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
relationObjectMetadataId,
|
||||
selectOptions,
|
||||
shrink,
|
||||
}: SettingsObjectFieldPreviewProps) => {
|
||||
const theme = useTheme();
|
||||
@ -81,27 +83,17 @@ export const SettingsObjectFieldPreview = ({
|
||||
entityId,
|
||||
FieldIcon,
|
||||
fieldName,
|
||||
hasValue,
|
||||
ObjectIcon,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
value,
|
||||
} = useFieldPreview({
|
||||
fieldMetadata,
|
||||
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 (
|
||||
<StyledContainer className={className}>
|
||||
<StyledObjectSummary>
|
||||
@ -123,7 +115,7 @@ export const SettingsObjectFieldPreview = ({
|
||||
<SettingsObjectFieldPreviewValueEffect
|
||||
entityId={entityId}
|
||||
fieldName={fieldName}
|
||||
value={value ?? defaultValue}
|
||||
value={value}
|
||||
/>
|
||||
<StyledFieldPreview shrink={shrink}>
|
||||
<StyledFieldLabel>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -16,11 +16,16 @@ import {
|
||||
SettingsObjectFieldRelationForm,
|
||||
SettingsObjectFieldRelationFormValues,
|
||||
} from './SettingsObjectFieldRelationForm';
|
||||
import {
|
||||
SettingsObjectFieldSelectForm,
|
||||
SettingsObjectFieldSelectFormValues,
|
||||
} from './SettingsObjectFieldSelectForm';
|
||||
import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard';
|
||||
|
||||
export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{
|
||||
type: FieldMetadataType;
|
||||
relation: SettingsObjectFieldRelationFormValues;
|
||||
select: SettingsObjectFieldSelectFormValues;
|
||||
}>;
|
||||
|
||||
type SettingsObjectFieldTypeSelectSectionProps = {
|
||||
@ -54,6 +59,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
|
||||
values,
|
||||
}: SettingsObjectFieldTypeSelectSectionProps) => {
|
||||
const relationFormConfig = values?.relation;
|
||||
const selectFormConfig = values?.select;
|
||||
|
||||
const fieldTypeOptions = Object.entries(settingsFieldMetadataTypes)
|
||||
.filter(([key]) => !excludedFieldTypes?.includes(key as FieldMetadataType))
|
||||
@ -80,6 +86,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
|
||||
FieldMetadataType.Boolean,
|
||||
FieldMetadataType.Currency,
|
||||
FieldMetadataType.DateTime,
|
||||
FieldMetadataType.Enum,
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Number,
|
||||
FieldMetadataType.Relation,
|
||||
@ -98,6 +105,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
|
||||
relationObjectMetadataId={
|
||||
relationFormConfig?.objectMetadataId
|
||||
}
|
||||
selectOptions={selectFormConfig}
|
||||
/>
|
||||
{values.type === FieldMetadataType.Relation &&
|
||||
!!relationFormConfig?.type &&
|
||||
@ -127,7 +135,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
|
||||
</>
|
||||
}
|
||||
form={
|
||||
values.type === FieldMetadataType.Relation && (
|
||||
values.type === FieldMetadataType.Relation ? (
|
||||
<SettingsObjectFieldRelationForm
|
||||
disableFieldEdition={
|
||||
relationFieldMetadata && !relationFieldMetadata.isCustom
|
||||
@ -140,7 +148,12 @@ export const SettingsObjectFieldTypeSelectSection = ({
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : values.type === FieldMetadataType.Enum ? (
|
||||
<SettingsObjectFieldSelectForm
|
||||
values={selectFormConfig}
|
||||
onChange={(nextValues) => onChange({ select: nextValues })}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -61,7 +61,6 @@ export const settingsFieldMetadataTypes: Record<
|
||||
[FieldMetadataType.Enum]: {
|
||||
label: 'Select',
|
||||
Icon: IconTag,
|
||||
defaultValue: { color: 'green', text: 'Option 1' },
|
||||
},
|
||||
[FieldMetadataType.Currency]: {
|
||||
label: 'Currency',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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],
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user