Morph-front-settings (#13872)
# Implement Morph Frontend Settings Possiblity to create a morh relation from the Settings ! ## Overview Morph object system with new frontend settings UI for configuring morph relations. ## What Changed - **New Components**: `SettingsDataModelFieldMorphRelationForm`, `SettingsMorphRelationMultiSelect` - **Form System**: React Hook Form + Zod validation for morph relation configuration - **Preview System**: Live preview of field configurations with validation - **State Management**: Enhanced component state handling for complex form interactions ## Technical Details - **Architecture**: Restructured field input/display components for better maintainability - **Types**: Added comprehensive TypeScript types for morph relations (`isFieldMorphRelation`, etc.) - **Validation**: Dynamic validation rules based on field types and relation patterns - **Performance**: Optimized rendering with proper state management --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+19
@@ -16,6 +16,9 @@ import { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/
|
||||
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
|
||||
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
import { SettingsDataModelFieldDateSettingsFormCard } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard';
|
||||
import { settingsDataModelFieldMorphRelationFormSchema } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm';
|
||||
|
||||
import { SettingsDataModelFieldMorphRelationFormCard } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationFormCard';
|
||||
import { settingsDataModelFieldNumberFormSchema } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm';
|
||||
import { SettingsDataModelFieldNumberSettingsFormCard } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard';
|
||||
import { settingsDataModelFieldPhonesFormSchema } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm';
|
||||
@@ -59,6 +62,10 @@ const relationFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.RELATION) })
|
||||
.merge(settingsDataModelFieldRelationFormSchema);
|
||||
|
||||
const morphRelationFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.MORPH_RELATION) })
|
||||
.merge(settingsDataModelFieldMorphRelationFormSchema);
|
||||
|
||||
const selectFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.SELECT) })
|
||||
.merge(settingsDataModelFieldSelectFormSchema);
|
||||
@@ -94,6 +101,7 @@ const otherFieldsFormSchema = z
|
||||
FieldMetadataType.BOOLEAN,
|
||||
FieldMetadataType.CURRENCY,
|
||||
FieldMetadataType.RELATION,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.MULTI_SELECT,
|
||||
FieldMetadataType.DATE,
|
||||
@@ -116,6 +124,7 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
||||
dateFieldFormSchema,
|
||||
dateTimeFieldFormSchema,
|
||||
relationFieldFormSchema,
|
||||
morphRelationFieldFormSchema,
|
||||
selectFieldFormSchema,
|
||||
multiSelectFieldFormSchema,
|
||||
numberFieldFormSchema,
|
||||
@@ -154,6 +163,7 @@ const previewableTypes = [
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.RELATION,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.UUID,
|
||||
@@ -206,6 +216,15 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) {
|
||||
return (
|
||||
<SettingsDataModelFieldMorphRelationFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.NUMBER) {
|
||||
return (
|
||||
<SettingsDataModelFieldNumberSettingsFormCard
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { StyledContainer } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenuStyles';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsMorphRelationMultiSelect } from '@/settings/components/SettingsMorphRelationMultiSelect';
|
||||
import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength';
|
||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||
import { useMorphRelationSettingsFormDefaultValuesOnDestination } from '@/settings/data-model/fields/forms/morph-relation/hooks/useMorphRelationSettingsFormDefaultValuesOnDestination';
|
||||
import { useMorphRelationSettingsFormInitialTargetMetadatas } from '@/settings/data-model/fields/forms/morph-relation/hooks/useMorphRelationSettingsFormInitialTargetMetadatas';
|
||||
import { fieldMetadataItemDisableFieldEdition } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemDisableFieldEdition';
|
||||
import { fieldMetadataItemHasMorphRelations } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemHasMorphRelations';
|
||||
|
||||
import { fieldMetadataItemInitialRelationType } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemInitialRelationType';
|
||||
import {
|
||||
RELATION_TYPE_OPTIONS,
|
||||
StyledInputsContainer,
|
||||
StyledInputsLabel,
|
||||
StyledSelectsContainer,
|
||||
} from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const settingsDataModelFieldMorphRelationFormSchema = z.object({
|
||||
morphRelationObjectMetadataIds: z.array(z.string().uuid()).min(2),
|
||||
relationType: z.enum(
|
||||
Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]],
|
||||
),
|
||||
targetFieldLabel: z.string().min(1),
|
||||
iconOnDestination: z.string().min(1),
|
||||
});
|
||||
|
||||
export type SettingsDataModelFieldMorphRelationFormValues = z.infer<
|
||||
typeof settingsDataModelFieldMorphRelationFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldMorphRelationFormProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'morphRelations'>;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldMorphRelationForm = ({
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldMorphRelationFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldMorphRelationFormValues>();
|
||||
|
||||
const morphRelationPreviouslyCreated =
|
||||
fieldMetadataItemHasMorphRelations(fieldMetadataItem);
|
||||
|
||||
const disableRelationEdition = !!morphRelationPreviouslyCreated;
|
||||
const disableFieldEdition =
|
||||
fieldMetadataItemDisableFieldEdition(fieldMetadataItem);
|
||||
const initialRelationObjectMetadataItems =
|
||||
useMorphRelationSettingsFormInitialTargetMetadatas({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const initialRelationType =
|
||||
fieldMetadataItemInitialRelationType(fieldMetadataItem);
|
||||
|
||||
const { label: defaultLabelOnDestination, icon: defaultIconOnDestination } =
|
||||
useMorphRelationSettingsFormDefaultValuesOnDestination({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem: initialRelationObjectMetadataItems[0],
|
||||
relationType: initialRelationType,
|
||||
});
|
||||
|
||||
const initialMorphRelationsObjectMetadataIds =
|
||||
initialRelationObjectMetadataItems.map(
|
||||
(relationObjectMetadataItem) => relationObjectMetadataItem.id,
|
||||
);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSelectsContainer isMobile={isMobile}>
|
||||
<Controller
|
||||
name="relationType"
|
||||
control={control}
|
||||
defaultValue={initialRelationType}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
label={t`Relation type`}
|
||||
dropdownId="relation-type-select"
|
||||
fullWidth
|
||||
disabled={disableRelationEdition}
|
||||
value={value}
|
||||
options={RELATION_TYPE_OPTIONS}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="morphRelationObjectMetadataIds"
|
||||
control={control}
|
||||
defaultValue={initialMorphRelationsObjectMetadataIds}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SettingsMorphRelationMultiSelect
|
||||
label={t`Object destination`}
|
||||
dropdownId="object-destination-select"
|
||||
fullWidth
|
||||
disabled={disableRelationEdition}
|
||||
selectedObjectMetadataIds={value}
|
||||
withSearchInput={true}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledSelectsContainer>
|
||||
<StyledInputsLabel>{t`Field on destination`}</StyledInputsLabel>
|
||||
<StyledInputsContainer>
|
||||
<Controller
|
||||
name="iconOnDestination"
|
||||
control={control}
|
||||
defaultValue={defaultIconOnDestination}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<IconPicker
|
||||
disabled={disableFieldEdition}
|
||||
dropdownId="field-destination-icon-picker"
|
||||
selectedIconKey={value ?? undefined}
|
||||
onChange={({ iconKey }) => onChange(iconKey)}
|
||||
variant="primary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="targetFieldLabel"
|
||||
control={control}
|
||||
defaultValue={defaultLabelOnDestination}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SettingsTextInput
|
||||
instanceId="relation-field-label"
|
||||
disabled={disableFieldEdition}
|
||||
placeholder={t`Field name`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
maxLength={FIELD_NAME_MAXIMUM_LENGTH}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||
import {
|
||||
SettingsDataModelFieldMorphRelationForm,
|
||||
type SettingsDataModelFieldMorphRelationFormValues,
|
||||
} from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldMorphRelationForm';
|
||||
import { useMorphRelationSettingsFormInitialTargetMetadatas } from '@/settings/data-model/fields/forms/morph-relation/hooks/useMorphRelationSettingsFormInitialTargetMetadatas';
|
||||
import { fieldMetadataItemInitialRelationType } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemInitialRelationType';
|
||||
import { SettingsDataModelFieldRelationPreviewContent } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewContent';
|
||||
import { SettingsDataModelRelationPreviewImage } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewImageCard';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelMorphRelationFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelMorphRelationFieldPreviewCard';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationType,
|
||||
type Relation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsDataModelFieldMorphRelationFormCardProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
export const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
const StyledMorphRelationFieldPreviewCard = styled(
|
||||
SettingsDataModelMorphRelationFieldPreviewCard,
|
||||
)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldMorphRelationFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelFieldMorphRelationFormCardProps) => {
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldMorphRelationFormValues>();
|
||||
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
||||
const isMobile = useIsMobile();
|
||||
const initialRelationType =
|
||||
fieldMetadataItemInitialRelationType(fieldMetadataItem);
|
||||
const initialRelationObjectMetadataItems =
|
||||
useMorphRelationSettingsFormInitialTargetMetadatas({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const initialRelationFieldMetadataItem = {
|
||||
icon: initialRelationObjectMetadataItems[0].icon,
|
||||
label: [RelationType.MANY_TO_ONE].includes(initialRelationType)
|
||||
? initialRelationObjectMetadataItems[0].namePlural
|
||||
: initialRelationObjectMetadataItems[0].nameSingular,
|
||||
};
|
||||
const initialRelationObjectMetadataItem =
|
||||
initialRelationObjectMetadataItems[0];
|
||||
|
||||
const relationObjectMetadataId = watchFormValue(
|
||||
'morphRelationObjectMetadataIds.0',
|
||||
initialRelationObjectMetadataItem?.id,
|
||||
);
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
relationObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!relationObjectMetadataItem) return null;
|
||||
|
||||
const relationType: RelationType = watchFormValue(
|
||||
'relationType',
|
||||
initialRelationType,
|
||||
);
|
||||
|
||||
const relationTypeConfig = RELATION_TYPES[relationType];
|
||||
|
||||
if (!isDefined(relationTypeConfig)) return null;
|
||||
|
||||
const oppositeRelationType =
|
||||
relationType === RelationType.MANY_TO_ONE
|
||||
? RelationType.ONE_TO_MANY
|
||||
: RelationType.MANY_TO_ONE;
|
||||
|
||||
const morphRelationsForStyledFieldPreviewCard = watchFormValue(
|
||||
'morphRelationObjectMetadataIds',
|
||||
)?.map((morphRelationObjectMetadataId) => {
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
morphRelationObjectMetadataId,
|
||||
);
|
||||
if (!relationObjectMetadataItem)
|
||||
throw new Error('Relation object metadata item not found');
|
||||
return {
|
||||
targetObjectMetadata: {
|
||||
id: relationObjectMetadataItem.id,
|
||||
nameSingular: relationObjectMetadataItem.labelSingular,
|
||||
labelSingular: relationObjectMetadataItem.labelSingular,
|
||||
labelPlural: relationObjectMetadataItem.labelPlural,
|
||||
icon: relationObjectMetadataItem.icon,
|
||||
isCustom: relationObjectMetadataItem.isCustom,
|
||||
isRemote: relationObjectMetadataItem.isRemote,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<SettingsDataModelFieldRelationPreviewContent isMobile={isMobile}>
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
relation: {
|
||||
type: relationType,
|
||||
} as Relation,
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
pluralizeLabel={
|
||||
watchFormValue('relationType') === RelationType.MANY_TO_ONE
|
||||
}
|
||||
/>
|
||||
<SettingsDataModelRelationPreviewImage
|
||||
src={relationTypeConfig.imageSrc}
|
||||
flip={relationTypeConfig.isImageFlipped}
|
||||
alt={relationTypeConfig.label}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
{morphRelationsForStyledFieldPreviewCard && (
|
||||
<StyledMorphRelationFieldPreviewCard
|
||||
fieldMetadataItem={{
|
||||
...initialRelationFieldMetadataItem,
|
||||
icon: watchFormValue(
|
||||
'iconOnDestination',
|
||||
initialRelationFieldMetadataItem.icon ?? undefined,
|
||||
),
|
||||
label:
|
||||
watchFormValue(
|
||||
'targetFieldLabel',
|
||||
initialRelationFieldMetadataItem.label,
|
||||
) || 'Field name',
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: {
|
||||
type: oppositeRelationType,
|
||||
} as Relation,
|
||||
morphRelations: morphRelationsForStyledFieldPreviewCard,
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={relationObjectMetadataItem}
|
||||
relationObjectMetadataItem={objectMetadataItem}
|
||||
pluralizeLabel={
|
||||
watchFormValue('relationType') !== RelationType.MANY_TO_ONE
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SettingsDataModelFieldRelationPreviewContent>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldMorphRelationForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { fieldMetadataItemHasMorphRelations } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemHasMorphRelations';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMorphRelationSettingsFormDefaultValuesOnDestination = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationType,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'type' | 'morphRelations'>;
|
||||
objectMetadataItem?: Pick<
|
||||
ObjectMetadataItem,
|
||||
'id' | 'namePlural' | 'nameSingular' | 'icon'
|
||||
>;
|
||||
relationType: RelationType;
|
||||
}): {
|
||||
icon: string;
|
||||
label: string;
|
||||
} => {
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
if (
|
||||
isDefined(fieldMetadataItem) &&
|
||||
fieldMetadataItemHasMorphRelations(fieldMetadataItem)
|
||||
) {
|
||||
const firstMorphRelation = fieldMetadataItem.morphRelations?.[0];
|
||||
|
||||
if (isDefined(firstMorphRelation)) {
|
||||
const targetObjectMetadata = activeObjectMetadataItems.find(
|
||||
(item) => item.id === firstMorphRelation.targetObjectMetadata.id,
|
||||
);
|
||||
const targetFieldMetadata = targetObjectMetadata?.fields.find(
|
||||
(field) => field.id === firstMorphRelation.targetFieldMetadata.id,
|
||||
);
|
||||
|
||||
return {
|
||||
icon: targetFieldMetadata?.icon ?? 'IconUsers',
|
||||
label: targetFieldMetadata?.label ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
const label = [RelationType.MANY_TO_ONE].includes(relationType)
|
||||
? objectMetadataItem.namePlural
|
||||
: objectMetadataItem.nameSingular;
|
||||
|
||||
return {
|
||||
icon: objectMetadataItem.icon ?? 'IconUsers',
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
icon: 'IconUsers',
|
||||
label: '',
|
||||
};
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||
import { fieldMetadataItemHasMorphRelations } from '@/settings/data-model/fields/forms/morph-relation/utils/fieldMetadataItemHasMorphRelations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMorphRelationSettingsFormInitialTargetMetadatas = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'type' | 'morphRelations'>;
|
||||
}) => {
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
|
||||
if (
|
||||
isDefined(fieldMetadataItem) &&
|
||||
fieldMetadataItemHasMorphRelations(fieldMetadataItem)
|
||||
) {
|
||||
return (
|
||||
fieldMetadataItem.morphRelations?.map((morphRelation) => {
|
||||
return {
|
||||
...morphRelation.targetObjectMetadata,
|
||||
icon: activeObjectMetadataItems.find(
|
||||
(item) => item.id === morphRelation.targetObjectMetadata.id,
|
||||
)?.icon,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
const availableItems = activeObjectMetadataItems
|
||||
.filter(isObjectMetadataAvailableForRelation)
|
||||
.sort((a, b) => a.labelSingular.localeCompare(b.labelSingular));
|
||||
const firstInitialObjectCandidate = availableItems[0];
|
||||
if (!isDefined(firstInitialObjectCandidate)) {
|
||||
throw new Error(
|
||||
'Relation Form initialization error: invariant violated – no valid object available for relation (this should never happen).',
|
||||
);
|
||||
}
|
||||
const secondInitialObjectCandidate = availableItems[1];
|
||||
if (!isDefined(secondInitialObjectCandidate)) {
|
||||
throw new Error(
|
||||
'Relation Form initialization error: invariant violated – no valid object available for relation (this should never happen).',
|
||||
);
|
||||
}
|
||||
|
||||
return [firstInitialObjectCandidate, secondInitialObjectCandidate];
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const fieldMetadataItemDisableFieldEdition = (
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'morphRelations'>,
|
||||
) => {
|
||||
const morphRelations = fieldMetadataItem?.morphRelations;
|
||||
|
||||
// TODO: @guillim to be updated with isCustom when available
|
||||
return isDefined(morphRelations) && morphRelations.length > 0
|
||||
? morphRelations.every(
|
||||
(morphRelation) => !morphRelation.targetFieldMetadata.isCustom,
|
||||
)
|
||||
: false;
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const fieldMetadataItemHasMorphRelations = (
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'morphRelations'>,
|
||||
) => {
|
||||
const morphRelations = fieldMetadataItem?.morphRelations;
|
||||
return isDefined(morphRelations) && morphRelations.length > 0;
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const fieldMetadataItemInitialRelationType = (
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'morphRelations'>,
|
||||
) => {
|
||||
const morphRelations = fieldMetadataItem?.morphRelations;
|
||||
return isDefined(morphRelations) && morphRelations.length > 0
|
||||
? morphRelations[0].type
|
||||
: RelationType.ONE_TO_MANY;
|
||||
};
|
||||
+5
-5
@@ -50,17 +50,17 @@ type SettingsDataModelFieldRelationFormProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
export const StyledContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledSelectsContainer = styled.div<{ isMobile: boolean }>`
|
||||
export const StyledSelectsContainer = styled.div<{ isMobile: boolean }>`
|
||||
display: grid;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
grid-template-columns: ${({ isMobile }) => (isMobile ? '1fr' : '1fr 1fr')};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
const StyledInputsLabel = styled.span`
|
||||
export const StyledInputsLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: block;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
@@ -68,13 +68,13 @@ const StyledInputsLabel = styled.span`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
export const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES).map(
|
||||
export const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES).map(
|
||||
([value, { label, Icon }]) => ({
|
||||
label,
|
||||
value: value as RelationType,
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
const StyledPreviewContent = styled.div<{ isMobile: boolean }>`
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-direction: ${({ isMobile }) => (isMobile ? 'column' : 'row')};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldRelationPreviewContent = ({
|
||||
isMobile,
|
||||
children,
|
||||
}: {
|
||||
isMobile: boolean;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<StyledPreviewContent isMobile={isMobile}>{children}</StyledPreviewContent>
|
||||
);
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledRelationImage = styled.img<{
|
||||
flip?: boolean;
|
||||
isMobile: boolean;
|
||||
}>`
|
||||
transform: ${({ flip, isMobile }) => {
|
||||
let transform = '';
|
||||
if (isMobile) {
|
||||
transform += 'rotate(90deg) ';
|
||||
}
|
||||
if (flip === true) {
|
||||
transform += 'scaleX(-1)';
|
||||
}
|
||||
return transform.trim();
|
||||
}};
|
||||
margin: auto;
|
||||
width: 54px;
|
||||
`;
|
||||
|
||||
export const SettingsDataModelRelationPreviewImage = ({
|
||||
isMobile,
|
||||
flip,
|
||||
src,
|
||||
alt,
|
||||
}: {
|
||||
isMobile: boolean;
|
||||
flip?: boolean;
|
||||
src: string;
|
||||
alt: string;
|
||||
}) => {
|
||||
return (
|
||||
<StyledRelationImage isMobile={isMobile} flip={flip} src={src} alt={alt} />
|
||||
);
|
||||
};
|
||||
+5
-24
@@ -9,6 +9,8 @@ import {
|
||||
SettingsDataModelFieldRelationForm,
|
||||
type SettingsDataModelFieldRelationFormValues,
|
||||
} from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm';
|
||||
import { SettingsDataModelFieldRelationPreviewContent } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewContent';
|
||||
import { SettingsDataModelRelationPreviewImage } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationPreviewImageCard';
|
||||
import { useRelationSettingsFormInitialValues } from '@/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
@@ -30,27 +32,6 @@ const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
const StyledPreviewContent = styled.div<{ isMobile: boolean }>`
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-direction: ${({ isMobile }) => (isMobile ? 'column' : 'row')};
|
||||
`;
|
||||
|
||||
const StyledRelationImage = styled.img<{ flip?: boolean; isMobile: boolean }>`
|
||||
transform: ${({ flip, isMobile }) => {
|
||||
let transform = '';
|
||||
if (isMobile) {
|
||||
transform += 'rotate(90deg) ';
|
||||
}
|
||||
if (flip === true) {
|
||||
transform += 'scaleX(-1)';
|
||||
}
|
||||
return transform.trim();
|
||||
}};
|
||||
margin: auto;
|
||||
width: 54px;
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldRelationSettingsFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
@@ -92,7 +73,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledPreviewContent isMobile={isMobile}>
|
||||
<SettingsDataModelFieldRelationPreviewContent isMobile={isMobile}>
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
@@ -107,7 +88,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
|
||||
watchFormValue('relation.type') === RelationType.MANY_TO_ONE
|
||||
}
|
||||
/>
|
||||
<StyledRelationImage
|
||||
<SettingsDataModelRelationPreviewImage
|
||||
src={relationTypeConfig.imageSrc}
|
||||
flip={relationTypeConfig.isImageFlipped}
|
||||
alt={relationTypeConfig.label}
|
||||
@@ -137,7 +118,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
|
||||
watchFormValue('relation.type') !== RelationType.MANY_TO_ONE
|
||||
}
|
||||
/>
|
||||
</StyledPreviewContent>
|
||||
</SettingsDataModelFieldRelationPreviewContent>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldRelationForm
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import {
|
||||
SettingsDataModelFieldPreview,
|
||||
type SettingsDataModelFieldPreviewProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview';
|
||||
import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary';
|
||||
import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary';
|
||||
import { Card, CardContent } from 'twenty-ui/layout';
|
||||
|
||||
export type SettingsDataModelFieldPreviewCardProps =
|
||||
@@ -34,8 +34,8 @@ export const SettingsDataModelFieldPreviewCard = ({
|
||||
return (
|
||||
<StyledCard className={className} fullWidth>
|
||||
<StyledCardContent>
|
||||
<SettingsDataModelObjectSummary
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
<SettingsDataModelObjectPreview
|
||||
objectMetadataItems={[objectMetadataItem]}
|
||||
pluralizeLabel={pluralizeLabel}
|
||||
/>
|
||||
<SettingsDataModelFieldPreview
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsDataModelFieldPreview } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview';
|
||||
import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary';
|
||||
import { Card, CardContent } from 'twenty-ui/layout';
|
||||
|
||||
export type SettingsDataModelMorphRelationFieldPreviewCardProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
| 'icon'
|
||||
| 'label'
|
||||
| 'type'
|
||||
| 'defaultValue'
|
||||
| 'options'
|
||||
| 'settings'
|
||||
| 'relation'
|
||||
> & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
morphRelations: {
|
||||
targetObjectMetadata: Pick<
|
||||
ObjectMetadataItem,
|
||||
| 'id'
|
||||
| 'nameSingular'
|
||||
| 'labelSingular'
|
||||
| 'labelPlural'
|
||||
| 'icon'
|
||||
| 'isCustom'
|
||||
| 'isRemote'
|
||||
>;
|
||||
}[];
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
shrink?: boolean;
|
||||
withFieldLabel?: boolean;
|
||||
className?: string;
|
||||
pluralizeLabel?: boolean;
|
||||
};
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled(CardContent)`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelMorphRelationFieldPreviewCard = ({
|
||||
className,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
pluralizeLabel = false,
|
||||
}: SettingsDataModelMorphRelationFieldPreviewCardProps) => {
|
||||
const objectMetadataItems = fieldMetadataItem.morphRelations?.map(
|
||||
(morphRelation) => morphRelation.targetObjectMetadata,
|
||||
);
|
||||
|
||||
if (!objectMetadataItems || objectMetadataItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCard className={className} fullWidth>
|
||||
<StyledCardContent>
|
||||
<SettingsDataModelObjectPreview
|
||||
objectMetadataItems={objectMetadataItems}
|
||||
pluralizeLabel={pluralizeLabel}
|
||||
/>
|
||||
<SettingsDataModelFieldPreview
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
shrink={shrink}
|
||||
withFieldLabel={withFieldLabel}
|
||||
/>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user