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:
Guillim
2025-08-19 16:37:26 +02:00
committed by GitHub
parent 9eaede6e1e
commit 90c237a185
31 changed files with 1209 additions and 83 deletions
@@ -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
@@ -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>
);
};
@@ -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}
/>
}
/>
);
};
@@ -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: '',
};
};
@@ -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];
};
@@ -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;
};
@@ -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;
};
@@ -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;
};
@@ -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,
@@ -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>
);
};
@@ -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} />
);
};
@@ -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
@@ -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
@@ -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>
);
};