Morph-front-display-table (#13979)
## Add Morph Relation Field Display Support for Tables (one to many & many to one) **Features:** - Display morph relation fields (one-to-many, many-to-one) in table cells - Multi-field record retrieval for improved performance - Enhanced table cell rendering with morph relation support **Components Added:** - `MorphRelationManyToOneFieldDisplay` - Shows many-to-one morph relations - `MorphRelationOneToManyFieldDisplay` - Shows one-to-many morph relations TODO : handle notes and tasks case if we choose these kind of objects as targets/source for a morph relation --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+2
@@ -229,3 +229,5 @@ idealCustomerProfile
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// todo @guillim add a test for the morph relation
|
||||
|
||||
+92
-5
@@ -1,5 +1,4 @@
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
@@ -7,7 +6,11 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
computeMorphRelationFieldJoinColumnName,
|
||||
computeMorphRelationFieldName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
type MapFieldMetadataToGraphQLQueryArgs = {
|
||||
@@ -15,7 +18,7 @@ type MapFieldMetadataToGraphQLQueryArgs = {
|
||||
gqlField: string;
|
||||
fieldMetadata: Pick<
|
||||
FieldMetadataItem,
|
||||
'name' | 'type' | 'relation' | 'settings'
|
||||
'name' | 'type' | 'relation' | 'morphRelations' | 'settings'
|
||||
>;
|
||||
relationRecordGqlFields?: RecordGqlFields;
|
||||
computeReferences?: boolean;
|
||||
@@ -41,6 +44,90 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
return gqlField;
|
||||
}
|
||||
|
||||
if (
|
||||
fieldType === FieldMetadataType.MORPH_RELATION &&
|
||||
(fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY ||
|
||||
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE)
|
||||
) {
|
||||
let gqlMorphField = '';
|
||||
for (const morphRelation of fieldMetadata.morphRelations ?? []) {
|
||||
const relationFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: fieldMetadata.settings?.relationType,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
const relationMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === morphRelation.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
if (!isDefined(relationMetadataItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(objectPermissionsByObjectMetadataId) &&
|
||||
isDefined(relationMetadataItem.id)
|
||||
) {
|
||||
if (!isDefined(morphRelation.targetObjectMetadata.id)) {
|
||||
throw new Error(
|
||||
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const objectPermission = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
morphRelation.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
if (!objectPermission.canReadObjectRecords) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) {
|
||||
gqlMorphField += `${relationFieldName}
|
||||
{
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
recordGqlFields: relationRecordGqlFields,
|
||||
computeReferences,
|
||||
isRootLevel: false,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
})}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) {
|
||||
const joinColumnName = computeMorphRelationFieldJoinColumnName({
|
||||
name: fieldMetadata.name,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
});
|
||||
if (gqlField === joinColumnName) {
|
||||
gqlMorphField += `${gqlField}
|
||||
`;
|
||||
continue;
|
||||
}
|
||||
|
||||
gqlMorphField += `${relationFieldName}
|
||||
${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
recordGqlFields: relationRecordGqlFields,
|
||||
computeReferences,
|
||||
isRootLevel: false,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
})}`;
|
||||
}
|
||||
}
|
||||
return `${gqlMorphField}`;
|
||||
}
|
||||
|
||||
if (
|
||||
fieldType === FieldMetadataType.RELATION &&
|
||||
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE
|
||||
@@ -51,7 +138,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
fieldMetadata.relation?.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
if (isUndefined(relationMetadataItem)) {
|
||||
if (!isDefined(relationMetadataItem)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -100,7 +187,7 @@ ${mapObjectMetadataToGraphQLQuery({
|
||||
fieldMetadata.relation?.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
if (isUndefined(relationMetadataItem)) {
|
||||
if (!isDefined(relationMetadataItem)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
+64
-11
@@ -5,7 +5,7 @@ import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueri
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
|
||||
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type MapObjectMetadataToGraphQLQueryArgs = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
@@ -46,22 +46,75 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
|
||||
const manyToOneRelationFields = objectMetadataItem?.readableFields
|
||||
.filter((field) => field.isActive)
|
||||
.filter((field) => field.type === FieldMetadataType.RELATION)
|
||||
.filter(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.RELATION ||
|
||||
field.type === FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
.filter((field) => isDefined(field.settings?.joinColumnName));
|
||||
|
||||
const manyToOneRelationGqlFieldWithFieldMetadata =
|
||||
manyToOneRelationFields.map((field) => ({
|
||||
gqlField: field.settings?.joinColumnName,
|
||||
fieldMetadata: field,
|
||||
manyToOneRelationFields.flatMap((field) => {
|
||||
const isMorphRelation = field.type === FieldMetadataType.MORPH_RELATION;
|
||||
if (!isMorphRelation) {
|
||||
return {
|
||||
gqlField: field.settings?.joinColumnName,
|
||||
fieldMetadata: field,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDefined(field.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${field.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
}
|
||||
|
||||
return field.morphRelations.map((morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
fieldMetadata: field,
|
||||
}));
|
||||
});
|
||||
|
||||
const readableFields = objectMetadataItem.readableFields.filter(
|
||||
(fieldMetadata) => fieldMetadata.isActive,
|
||||
);
|
||||
|
||||
const activeReadableFields = readableFields.flatMap((fieldMetadata) => {
|
||||
const isMorphRelation =
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION;
|
||||
if (!isMorphRelation) {
|
||||
return [
|
||||
{
|
||||
gqlField: fieldMetadata.name,
|
||||
fieldMetadata,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadata.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadata.morphRelations.map((morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
fieldMetadata,
|
||||
}));
|
||||
});
|
||||
|
||||
const gqlFieldWithFieldMetadataThatCouldBeQueried = [
|
||||
...objectMetadataItem.readableFields
|
||||
.filter((fieldMetadata) => fieldMetadata.isActive)
|
||||
.map((fieldMetadata) => ({
|
||||
gqlField: fieldMetadata.name,
|
||||
fieldMetadata,
|
||||
})),
|
||||
...activeReadableFields,
|
||||
...manyToOneRelationGqlFieldWithFieldMetadata,
|
||||
].sort((gqlFieldWithFieldMetadataA, gqlFieldWithFieldMetadataB) =>
|
||||
gqlFieldWithFieldMetadataA.gqlField.localeCompare(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
@@ -18,16 +18,21 @@ export const shouldFieldBeQueried = ({
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
}): any => {
|
||||
const isJoinColumn: boolean =
|
||||
isFieldRelation(fieldMetadata) &&
|
||||
(isFieldRelation(fieldMetadata) || isFieldMorphRelation(fieldMetadata)) &&
|
||||
fieldMetadata.settings.joinColumnName === gqlField;
|
||||
|
||||
if (
|
||||
isUndefinedOrNull(recordGqlFields) &&
|
||||
(fieldMetadata.type !== FieldMetadataType.RELATION || isJoinColumn)
|
||||
!isFieldRelation(fieldMetadata) &&
|
||||
!isFieldMorphRelation(fieldMetadata)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isUndefinedOrNull(recordGqlFields) && isJoinColumn) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(recordGqlFields) &&
|
||||
isDefined(recordGqlFields[gqlField]) &&
|
||||
|
||||
+5
-1
@@ -9,7 +9,11 @@ export const generateDepthOneWithoutRelationsRecordGqlFields = ({
|
||||
objectMetadataItem,
|
||||
}: GenerateDepthOneWithoutRelationsRecordGqlFields) => {
|
||||
return objectMetadataItem.fields
|
||||
.filter((field) => field.type !== FieldMetadataType.RELATION)
|
||||
.filter(
|
||||
(field) =>
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
.reduce<Record<string, true>>((acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
|
||||
+9
@@ -23,6 +23,11 @@ import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
|
||||
import { MorphRelationManyToOneFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/MorphRelationManyToOneFieldDisplay';
|
||||
import { MorphRelationOneToManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay';
|
||||
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
|
||||
import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||
@@ -73,6 +78,10 @@ export const FieldDisplay = () => {
|
||||
<RelationToOneFieldDisplay />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldDisplay />
|
||||
) : isFieldMorphRelationManyToOne(fieldDefinition) ? (
|
||||
<MorphRelationManyToOneFieldDisplay />
|
||||
) : isFieldMorphRelationOneToMany(fieldDefinition) ? (
|
||||
<MorphRelationOneToManyFieldDisplay />
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldDisplay />
|
||||
) : isFieldUuid(fieldDefinition) ? (
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useMorphRelationToOneFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useMorphRelationToOneFieldDisplay';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const MorphRelationManyToOneFieldDisplay = () => {
|
||||
const { morphFieldValuesWithObjectName } =
|
||||
useMorphRelationToOneFieldDisplay();
|
||||
|
||||
const { disableChipClick, triggerEvent } = useContext(FieldContext);
|
||||
|
||||
if (!isDefined(morphFieldValuesWithObjectName?.value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isWorkspaceMemberFieldMetadataRelation =
|
||||
morphFieldValuesWithObjectName.objectNameSingular ===
|
||||
CoreObjectNameSingular.WorkspaceMember;
|
||||
|
||||
return (
|
||||
<RecordChip
|
||||
key={morphFieldValuesWithObjectName.value.id}
|
||||
objectNameSingular={morphFieldValuesWithObjectName.objectNameSingular}
|
||||
record={morphFieldValuesWithObjectName.value}
|
||||
forceDisableClick={
|
||||
isWorkspaceMemberFieldMetadataRelation || disableChipClick
|
||||
}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus';
|
||||
import { useMorphRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useMorphRelationFromManyFieldDisplay';
|
||||
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { Fragment, useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const MorphRelationOneToManyFieldDisplay = () => {
|
||||
const { morphValuesWithObjectNameSingular } =
|
||||
useMorphRelationFromManyFieldDisplay();
|
||||
const { isFocused } = useFieldFocus();
|
||||
const { disableChipClick, triggerEvent } = useContext(FieldContext);
|
||||
|
||||
if (!morphValuesWithObjectNameSingular) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{morphValuesWithObjectNameSingular
|
||||
.filter(isDefined)
|
||||
.map((morphValueWithObjectNameSingular) => {
|
||||
return (
|
||||
<Fragment key={morphValueWithObjectNameSingular.objectNameSingular}>
|
||||
{morphValueWithObjectNameSingular.value.map((record) => {
|
||||
return (
|
||||
<RecordChip
|
||||
key={record.id}
|
||||
objectNameSingular={
|
||||
morphValueWithObjectNameSingular.objectNameSingular
|
||||
}
|
||||
record={record}
|
||||
forceDisableClick={disableChipClick}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</ExpandableList>
|
||||
);
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
||||
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
|
||||
import { recordStoreMorphOneToManyValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
|
||||
export const useMorphRelationFromManyFieldDisplay = () => {
|
||||
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
|
||||
const { chipGeneratorPerObjectPerField } = useContext(
|
||||
PreComputedChipGeneratorsContext,
|
||||
);
|
||||
|
||||
if (!isDefined(chipGeneratorPerObjectPerField)) {
|
||||
throw new Error('Chip generator per object per field is not defined');
|
||||
}
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
isFieldMorphRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const button = fieldDefinition.editButtonIcon;
|
||||
|
||||
const morphValuesWithObjectNameSingular = useRecoilValue(
|
||||
recordStoreMorphOneToManyValueWithObjectNameFamilySelector({
|
||||
recordId,
|
||||
morphRelations: fieldDefinition.metadata.morphRelations,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const maxWidthForField =
|
||||
isDefined(button) && isDefined(maxWidth)
|
||||
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
||||
: maxWidth;
|
||||
|
||||
if (
|
||||
!isDefined(fieldDefinition.metadata.objectMetadataNameSingular) ||
|
||||
!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)
|
||||
) {
|
||||
throw new Error('Object metadata name singular is not a non-empty string');
|
||||
}
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
morphValuesWithObjectNameSingular,
|
||||
maxWidth: maxWidthForField,
|
||||
recordId,
|
||||
};
|
||||
};
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
||||
|
||||
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
|
||||
import { recordStoreMorphManyToOneValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMorphRelationToOneFieldDisplay = () => {
|
||||
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
|
||||
const { chipGeneratorPerObjectPerField } = useContext(
|
||||
PreComputedChipGeneratorsContext,
|
||||
);
|
||||
|
||||
if (!isDefined(chipGeneratorPerObjectPerField)) {
|
||||
throw new Error('Chip generator per object per field is not defined');
|
||||
}
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
isFieldMorphRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const button = fieldDefinition.editButtonIcon;
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const morphFieldValueWithObjectName = useRecoilValue(
|
||||
recordStoreMorphManyToOneValueWithObjectNameFamilySelector({
|
||||
recordId,
|
||||
morphRelations: fieldDefinition.metadata.morphRelations,
|
||||
fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isDefined(morphFieldValueWithObjectName)) {
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValues: [],
|
||||
maxWidth: maxWidth,
|
||||
recordId,
|
||||
generateRecordChipData: () => ({
|
||||
recordId: '',
|
||||
objectNameSingular: '',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const maxWidthForField =
|
||||
isDefined(button) && isDefined(maxWidth)
|
||||
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
||||
: maxWidth;
|
||||
|
||||
if (
|
||||
!isDefined(fieldDefinition.metadata.objectMetadataNameSingular) ||
|
||||
!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)
|
||||
) {
|
||||
throw new Error('Object metadata name singular is not a non-empty string');
|
||||
}
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
morphFieldValuesWithObjectName: morphFieldValueWithObjectName,
|
||||
maxWidth: maxWidthForField,
|
||||
recordId,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -48,7 +48,7 @@ export const useRelationToOneFieldDisplay = () => {
|
||||
!isDefined(fieldDefinition.metadata.objectMetadataNameSingular) ||
|
||||
!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)
|
||||
) {
|
||||
throw new Error('Object metadata name singular is not a non-empty string');
|
||||
throw new Error('Object metadata name singular is empty');
|
||||
}
|
||||
|
||||
const fieldChipGenerator =
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ export const isFieldMorphRelationManyToOne = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||
): field is FieldDefinition<FieldMorphRelationMetadata> =>
|
||||
isFieldMorphRelation(field) &&
|
||||
field.metadata.morphRelations?.[0].type === RelationType.MANY_TO_ONE;
|
||||
field.metadata.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
+35
-3
@@ -5,6 +5,8 @@ import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/g
|
||||
import { generateDepthOneWithoutRelationsRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneWithoutRelationsRecordGqlFields';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRecordTableRecordGqlFields = ({
|
||||
objectMetadataItem,
|
||||
@@ -30,11 +32,41 @@ export const useRecordTableRecordGqlFields = ({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const gqlFieldsList = Object.fromEntries(
|
||||
visibleTableColumns.flatMap((column) => {
|
||||
const isMorphRelation = column.type === FieldMetadataType.MORPH_RELATION;
|
||||
|
||||
if (!isMorphRelation) {
|
||||
return [[column.metadata.fieldName, true]];
|
||||
}
|
||||
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.id === column.fieldMetadataId,
|
||||
);
|
||||
if (
|
||||
!isDefined(fieldMetadataItem) ||
|
||||
!isDefined(fieldMetadataItem.morphRelations)
|
||||
) {
|
||||
throw new Error(
|
||||
`Field ${column.metadata.fieldName} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadataItem.morphRelations.map((morphRelation) => [
|
||||
computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
true,
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
const recordGqlFields: Record<string, any> = {
|
||||
...allDepthOneWithoutRelationsRecordGqlFields,
|
||||
...Object.fromEntries(
|
||||
visibleTableColumns.map((column) => [column.metadata.fieldName, true]),
|
||||
),
|
||||
...gqlFieldsList,
|
||||
noteTargets: generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem: noteTargetObjectMetadataItem,
|
||||
}),
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector =
|
||||
selectorFamily({
|
||||
key: 'recordStoreMorphManyToOneValueWithObjectNameFamilySelector',
|
||||
get:
|
||||
({
|
||||
fieldName,
|
||||
recordId,
|
||||
morphRelations,
|
||||
}: {
|
||||
fieldName: string;
|
||||
recordId: string;
|
||||
morphRelations: FieldMetadataItemRelation[];
|
||||
}) =>
|
||||
({ get }) => {
|
||||
const morphValuesWithObjectName = morphRelations.map(
|
||||
(morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName,
|
||||
relationDirection: RelationType.MANY_TO_ONE,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return {
|
||||
objectNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
value: get(recordStoreFamilyState(recordId))?.[
|
||||
computedFieldName
|
||||
] as ObjectRecord,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const morphValueFiltered = morphValuesWithObjectName.filter(
|
||||
(morphValue) => isDefined(morphValue.value),
|
||||
);
|
||||
return morphValueFiltered.length > 0 ? morphValueFiltered[0] : null;
|
||||
},
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
|
||||
export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector =
|
||||
selectorFamily({
|
||||
key: 'recordStoreMorphOneToManyValueWithObjectNameFamilySelector',
|
||||
get:
|
||||
({
|
||||
fieldName,
|
||||
recordId,
|
||||
morphRelations,
|
||||
}: {
|
||||
fieldName: string;
|
||||
recordId: string;
|
||||
morphRelations: FieldMetadataItemRelation[];
|
||||
}) =>
|
||||
({ get }) => {
|
||||
const morphValuesWithObjectName = morphRelations.map(
|
||||
(morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName,
|
||||
relationDirection: RelationType.ONE_TO_MANY,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return {
|
||||
objectNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
value: (get(recordStoreFamilyState(recordId))?.[
|
||||
computedFieldName
|
||||
] || []) as ObjectRecord[],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return morphValuesWithObjectName;
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -29,7 +29,7 @@ export const RecordTableCellFieldContextGeneric = ({
|
||||
const updateRecord = useContext(RecordUpdateContext);
|
||||
|
||||
let hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
|
||||
|
||||
// todo @guillim : adjust this to handle morph relations permissions display
|
||||
if (
|
||||
isFieldRelationToOneObject(columnDefinition) ||
|
||||
isFieldRelationFromManyObjects(columnDefinition)
|
||||
|
||||
@@ -4,7 +4,10 @@ import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type GenerateEmptyFieldValueArgs = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relation'>;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'type' | 'settings' | 'defaultValue'
|
||||
>;
|
||||
shouldComputeFunctionDefaultValue?: boolean;
|
||||
};
|
||||
// TODO strictly type each fieldValue following their FieldMetadataType
|
||||
@@ -62,11 +65,13 @@ export const generateEmptyFieldValue = ({
|
||||
return null;
|
||||
}
|
||||
case FieldMetadataType.BOOLEAN: {
|
||||
return true;
|
||||
return fieldMetadataItem?.defaultValue ?? true;
|
||||
}
|
||||
case FieldMetadataType.RELATION:
|
||||
case FieldMetadataType.MORPH_RELATION: {
|
||||
if (fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE) {
|
||||
if (
|
||||
fieldMetadataItem.settings?.relationType === RelationType.MANY_TO_ONE
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+8
-22
@@ -1,7 +1,6 @@
|
||||
import { Separator } from '@/settings/components/Separator';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import {
|
||||
addressSchema as addressFieldDefaultValueSchema,
|
||||
addressSettingsSchema,
|
||||
@@ -30,10 +29,7 @@ import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFro
|
||||
type SettingsDataModelFieldAddressFormProps = {
|
||||
disabled?: boolean;
|
||||
defaultCountry?: string;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const settingsDataModelFieldAddressFormSchema = z.object({
|
||||
@@ -47,7 +43,7 @@ export type SettingsDataModelFieldTextFormValues = z.infer<
|
||||
|
||||
export const SettingsDataModelFieldAddressForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldAddressFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
@@ -66,33 +62,23 @@ export const SettingsDataModelFieldAddressForm = ({
|
||||
Flag({ width: props.size, height: props.size }),
|
||||
})),
|
||||
];
|
||||
const { initialDisplaySubFields, resetDefaultValueField } =
|
||||
useAddressSettingsFormInitialValues({ fieldMetadataItem });
|
||||
const {
|
||||
initialDisplaySubFields,
|
||||
initialDefaultValue,
|
||||
resetDefaultValueField,
|
||||
} = useAddressSettingsFormInitialValues({ existingFieldMetadataId });
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const reset = () => {
|
||||
resetDefaultValueField();
|
||||
closeDropdown('addressSubFieldsId');
|
||||
};
|
||||
const defaultDefaultValue = {
|
||||
addressStreet1: "''",
|
||||
addressStreet2: null,
|
||||
addressCity: null,
|
||||
addressState: null,
|
||||
addressPostcode: null,
|
||||
addressCountry: null,
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
name="defaultValue"
|
||||
defaultValue={{
|
||||
...defaultDefaultValue,
|
||||
...fieldMetadataItem?.defaultValue,
|
||||
}}
|
||||
defaultValue={initialDefaultValue}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const defaultCountry = value?.addressCountry || '';
|
||||
|
||||
+18
-34
@@ -1,62 +1,46 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
SettingsDataModelFieldAddressForm,
|
||||
type SettingsDataModelFieldTextFormValues,
|
||||
} from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm';
|
||||
import { useAddressSettingsFormInitialValues } from '@/settings/data-model/fields/forms/address/hooks/useAddressSettingsFormInitialValues';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
type SettingsDataModelFieldAddressSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldAddressSettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldAddressSettingsFormCardProps) => {
|
||||
const { initialDisplaySubFields } = useAddressSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldTextFormValues & SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
icon: watch('icon'),
|
||||
label: watch('label'),
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
settings: {
|
||||
...fieldMetadataItem.settings,
|
||||
subFields: watchFormValue(
|
||||
'settings.subFields',
|
||||
initialDisplaySubFields,
|
||||
),
|
||||
subFields: watch('settings.subFields'),
|
||||
},
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldAddressForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+117
-39
@@ -1,6 +1,9 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import { useAddressSettingsFormInitialValues } from '../useAddressSettingsFormInitialValues';
|
||||
|
||||
jest.mock('react-hook-form', () => ({
|
||||
@@ -12,6 +15,11 @@ const mockUseFormContext = useFormContext as jest.MockedFunction<
|
||||
typeof useFormContext
|
||||
>;
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
});
|
||||
|
||||
describe('useAddressSettingsFormInitialValues', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -21,8 +29,14 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
});
|
||||
|
||||
it('should return all address subfields when no fieldMetadataItem is provided', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAddressSettingsFormInitialValues({}),
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.initialDisplaySubFields).toEqual([
|
||||
@@ -36,12 +50,14 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
});
|
||||
|
||||
it('should return all address subfields when fieldMetadataItem has no settings', () => {
|
||||
const fieldMetadataItem: Pick<FieldMetadataItem, 'settings'> = {
|
||||
settings: undefined,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddressSettingsFormInitialValues({ fieldMetadataItem }),
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.initialDisplaySubFields).toEqual([
|
||||
@@ -55,14 +71,20 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
});
|
||||
|
||||
it('should return all address subfields when settings.subFields is null', () => {
|
||||
const fieldMetadataItem: Pick<FieldMetadataItem, 'settings'> = {
|
||||
settings: {
|
||||
subFields: null,
|
||||
},
|
||||
};
|
||||
// const fieldMetadataItem: Pick<FieldMetadataItem, 'settings'> = {
|
||||
// settings: {
|
||||
// subFields: null,
|
||||
// },
|
||||
// };
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddressSettingsFormInitialValues({ fieldMetadataItem }),
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.initialDisplaySubFields).toEqual([
|
||||
@@ -76,14 +98,20 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
});
|
||||
|
||||
it('should return all address subfields when settings.subFields is empty array', () => {
|
||||
const fieldMetadataItem: Pick<FieldMetadataItem, 'settings'> = {
|
||||
settings: {
|
||||
subFields: [],
|
||||
},
|
||||
};
|
||||
// const fieldMetadataItem: Pick<FieldMetadataItem, 'settings'> = {
|
||||
// settings: {
|
||||
// subFields: [],
|
||||
// },
|
||||
// };
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddressSettingsFormInitialValues({ fieldMetadataItem }),
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.initialDisplaySubFields).toEqual([
|
||||
@@ -97,14 +125,25 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
});
|
||||
|
||||
it('should return configured subFields when they exist', () => {
|
||||
const fieldMetadataItem: Pick<FieldMetadataItem, 'settings'> = {
|
||||
settings: {
|
||||
subFields: ['addressStreet1', 'addressCity', 'addressCountry'],
|
||||
},
|
||||
};
|
||||
const newGeneratedMockObjectMetadataItems = addNewAddressToMetadataItems(
|
||||
generatedMockObjectMetadataItems,
|
||||
'new-field',
|
||||
['addressStreet1', 'addressCity', 'addressCountry'],
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddressSettingsFormInitialValues({ fieldMetadataItem }),
|
||||
const WrapperSpecific = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
objectMetadataItems: newGeneratedMockObjectMetadataItems,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: WrapperSpecific,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.initialDisplaySubFields).toEqual([
|
||||
@@ -115,8 +154,14 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
});
|
||||
|
||||
it('should call resetField with all address subFields when resetDefaultValueField is called', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAddressSettingsFormInitialValues({}),
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
result.current.resetDefaultValueField();
|
||||
@@ -134,14 +179,22 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
});
|
||||
|
||||
it('should handle partial subFields configuration', () => {
|
||||
const fieldMetadataItem: Pick<FieldMetadataItem, 'settings'> = {
|
||||
settings: {
|
||||
subFields: ['addressStreet1', 'addressCity'],
|
||||
const WrapperSpecific = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
objectMetadataItems: addNewAddressToMetadataItems(
|
||||
generatedMockObjectMetadataItems,
|
||||
'new-field',
|
||||
['addressStreet1', 'addressCity'],
|
||||
),
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: WrapperSpecific,
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddressSettingsFormInitialValues({ fieldMetadataItem }),
|
||||
);
|
||||
|
||||
expect(result.current.initialDisplaySubFields).toEqual([
|
||||
@@ -150,3 +203,28 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const addNewAddressToMetadataItems = (
|
||||
generatedMockObjectMetadataItems: ObjectMetadataItem[],
|
||||
fieldNameId: string,
|
||||
subFields: string[],
|
||||
) => {
|
||||
return generatedMockObjectMetadataItems
|
||||
.filter((item) => item.nameSingular === 'company')
|
||||
.map((item) => {
|
||||
const fields = item.fields;
|
||||
const addressField = fields.find(
|
||||
(field) => field.type === FieldMetadataType.ADDRESS,
|
||||
);
|
||||
if (!addressField) {
|
||||
throw new Error('Address field not found');
|
||||
}
|
||||
const newField = {
|
||||
...addressField,
|
||||
id: fieldNameId,
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
settings: { subFields },
|
||||
};
|
||||
return { ...item, fields: [...fields, newField] };
|
||||
});
|
||||
};
|
||||
|
||||
+25
-5
@@ -1,13 +1,19 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { type SettingsDataModelFieldTextFormValues } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm';
|
||||
import { DEFAULT_SELECTION_ADDRESS_WITH_MESSAGES } from '@/settings/data-model/fields/forms/address/constants/DefaultSelectionAddressWithMessages';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
type UseAddressSettingsFormInitialValuesProps = {
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const useAddressSettingsFormInitialValues = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'settings'>;
|
||||
}) => {
|
||||
existingFieldMetadataId,
|
||||
}: UseAddressSettingsFormInitialValuesProps) => {
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
const allAddressSubFields = DEFAULT_SELECTION_ADDRESS_WITH_MESSAGES.map(
|
||||
(selectionAddres) => selectionAddres.value,
|
||||
);
|
||||
@@ -17,6 +23,19 @@ export const useAddressSettingsFormInitialValues = ({
|
||||
? fieldMetadataItem.settings.subFields
|
||||
: allAddressSubFields;
|
||||
|
||||
const defaultDefaultValue = {
|
||||
addressStreet1: "''",
|
||||
addressStreet2: null,
|
||||
addressCity: null,
|
||||
addressState: null,
|
||||
addressPostcode: null,
|
||||
addressCountry: null,
|
||||
addressLat: null,
|
||||
addressLng: null,
|
||||
};
|
||||
const initialDefaultValue =
|
||||
fieldMetadataItem?.defaultValue ?? defaultDefaultValue;
|
||||
|
||||
const { resetField } = useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
|
||||
const resetDefaultValueField = () => {
|
||||
@@ -26,6 +45,7 @@ export const useAddressSettingsFormInitialValues = ({
|
||||
};
|
||||
|
||||
return {
|
||||
initialDefaultValue,
|
||||
initialDisplaySubFields,
|
||||
resetDefaultValueField,
|
||||
};
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { BOOLEAN_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/boolean/constants/BooleanDataModelSelectOptions';
|
||||
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
|
||||
@@ -18,17 +17,17 @@ export type SettingsDataModelFieldBooleanFormValues = z.infer<
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldBooleanFormProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue'>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldBooleanForm = ({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldBooleanFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } = useFormContext<SettingsDataModelFieldBooleanFormValues>();
|
||||
|
||||
const { initialDefaultValue } = useBooleanSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
+19
-30
@@ -1,55 +1,44 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
SettingsDataModelFieldBooleanForm,
|
||||
type SettingsDataModelFieldBooleanFormValues,
|
||||
} from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm';
|
||||
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
type SettingsDataModelFieldBooleanSettingsFormCardProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue'
|
||||
>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldBooleanSettingsFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldBooleanSettingsFormCardProps) => {
|
||||
const { initialDefaultValue } = useBooleanSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldBooleanFormValues>();
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldBooleanFormValues &
|
||||
SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
defaultValue: watchFormValue('defaultValue', initialDefaultValue),
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: watch('label'),
|
||||
icon: watch('icon'),
|
||||
defaultValue: watch('defaultValue'),
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldBooleanForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+11
-5
@@ -1,13 +1,19 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { type SettingsDataModelFieldBooleanFormValues } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm';
|
||||
|
||||
type UseBooleanSettingsFormInitialValuesProps = {
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const useBooleanSettingsFormInitialValues = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue'>;
|
||||
}) => {
|
||||
existingFieldMetadataId,
|
||||
}: UseBooleanSettingsFormInitialValuesProps) => {
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
const initialDefaultValue =
|
||||
(fieldMetadataItem?.defaultValue as SettingsDataModelFieldBooleanFormValues['defaultValue']) ??
|
||||
true;
|
||||
|
||||
+24
-13
@@ -1,12 +1,12 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { Separator } from '@/settings/components/Separator';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { canBeUnique } from '@/settings/data-model/fields/forms/utils/canBeUnique.util';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { IconKey } from 'twenty-ui/display';
|
||||
import { Toggle } from 'twenty-ui/input';
|
||||
|
||||
@@ -15,31 +15,42 @@ type SettingsDataModelFieldIsUniqueFormValues = {
|
||||
};
|
||||
|
||||
type SettingsDataModelFieldIsUniqueFormProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'isCustom' | 'settings' | 'isUnique'
|
||||
> &
|
||||
Partial<{ id: string }>;
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'indexMetadatas'>;
|
||||
objectNameSingular: string;
|
||||
fieldType: FieldMetadataType;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldIsUniqueForm = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
fieldType,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldIsUniqueFormProps) => {
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldIsUniqueFormValues>();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
const hasStandardUniqueIndex = objectMetadataItem.indexMetadatas.some(
|
||||
(index) =>
|
||||
index.isUnique &&
|
||||
!index.isCustom &&
|
||||
index.indexFieldMetadatas?.some(
|
||||
(field) => field.fieldMetadataId === fieldMetadataItem.id,
|
||||
(field) => field.fieldMetadataId === existingFieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
if (!canBeUnique(fieldMetadataItem)) {
|
||||
if (
|
||||
!canBeUnique({
|
||||
type: fieldType,
|
||||
isCustom: fieldMetadataItem?.isCustom ?? true,
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+59
-56
@@ -1,8 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import omit from 'lodash.omit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
||||
import { settingsDataModelFieldAddressFormSchema } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm';
|
||||
@@ -30,11 +28,10 @@ import {
|
||||
settingsDataModelFieldSelectFormSchema,
|
||||
} from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
|
||||
import { SettingsDataModelFieldSelectSettingsFormCard } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectSettingsFormCard';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
const isUniqueFieldFormSchema = z.object({
|
||||
isUnique: z.boolean().nullable().default(false),
|
||||
@@ -136,16 +133,10 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
||||
);
|
||||
|
||||
type SettingsDataModelFieldSettingsFormCardProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'isCustom' | 'settings'
|
||||
> &
|
||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
fieldType: FieldMetadataType;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
const previewableTypes = [
|
||||
FieldMetadataType.ARRAY,
|
||||
@@ -170,105 +161,110 @@ const previewableTypes = [
|
||||
];
|
||||
|
||||
export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
fieldType,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
||||
if (!previewableTypes.includes(fieldMetadataItem.type)) {
|
||||
const { watch } = useFormContext<SettingsDataModelFieldEditFormValues>();
|
||||
|
||||
if (!previewableTypes.includes(fieldType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.BOOLEAN) {
|
||||
if (fieldType === FieldMetadataType.BOOLEAN) {
|
||||
return (
|
||||
<SettingsDataModelFieldBooleanSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.CURRENCY) {
|
||||
if (fieldType === FieldMetadataType.CURRENCY) {
|
||||
return (
|
||||
<SettingsDataModelFieldCurrencySettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.DATE ||
|
||||
fieldMetadataItem.type === FieldMetadataType.DATE_TIME
|
||||
fieldType === FieldMetadataType.DATE ||
|
||||
fieldType === FieldMetadataType.DATE_TIME
|
||||
) {
|
||||
return (
|
||||
<SettingsDataModelFieldDateSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
fieldType={fieldType}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.RELATION) {
|
||||
if (fieldType === FieldMetadataType.RELATION) {
|
||||
return (
|
||||
<SettingsDataModelFieldRelationSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) {
|
||||
if (fieldType === FieldMetadataType.MORPH_RELATION) {
|
||||
return (
|
||||
<SettingsDataModelFieldMorphRelationFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.NUMBER) {
|
||||
if (fieldType === FieldMetadataType.NUMBER) {
|
||||
return (
|
||||
<SettingsDataModelFieldNumberSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.TEXT) {
|
||||
if (fieldType === FieldMetadataType.TEXT) {
|
||||
return (
|
||||
<SettingsDataModelFieldTextSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.ADDRESS) {
|
||||
if (fieldType === FieldMetadataType.ADDRESS) {
|
||||
return (
|
||||
<SettingsDataModelFieldAddressSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.PHONES) {
|
||||
if (fieldType === FieldMetadataType.PHONES) {
|
||||
return (
|
||||
<SettingsDataModelFieldPhonesSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.SELECT ||
|
||||
fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT
|
||||
fieldType === FieldMetadataType.SELECT ||
|
||||
fieldType === FieldMetadataType.MULTI_SELECT
|
||||
) {
|
||||
return (
|
||||
<SettingsDataModelFieldSelectSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
fieldType={fieldType}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -276,15 +272,22 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
type: fieldType,
|
||||
label: watch('label'),
|
||||
icon: watch('icon'),
|
||||
defaultValue: watch('defaultValue'),
|
||||
settings: watch('settings'),
|
||||
}}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldIsUniqueForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldType={fieldType}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+5
-4
@@ -64,7 +64,6 @@ const StyledSearchInput = styled(SettingsTextInput)`
|
||||
|
||||
export const SettingsObjectNewFieldSelector = ({
|
||||
excludedFieldTypes = [],
|
||||
fieldMetadataItem,
|
||||
objectNamePlural,
|
||||
}: SettingsObjectNewFieldSelectorProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -80,13 +79,15 @@ export const SettingsObjectNewFieldSelector = ({
|
||||
);
|
||||
|
||||
const { resetDefaultValueField: resetBooleanDefaultValueField } =
|
||||
useBooleanSettingsFormInitialValues({ fieldMetadataItem });
|
||||
useBooleanSettingsFormInitialValues({ existingFieldMetadataId: 'new' });
|
||||
|
||||
const { resetDefaultValueField: resetCurrencyDefaultValueField } =
|
||||
useCurrencySettingsFormInitialValues({ fieldMetadataItem });
|
||||
useCurrencySettingsFormInitialValues({ existingFieldMetadataId: 'new' });
|
||||
|
||||
const { resetDefaultValueField: resetSelectDefaultValueField } =
|
||||
useSelectSettingsFormInitialValues({ fieldMetadataItem });
|
||||
useSelectSettingsFormInitialValues({
|
||||
fieldMetadataId: 'new',
|
||||
});
|
||||
|
||||
const resetDefaultValueField = (nextValue: SettingsFieldType) => {
|
||||
switch (nextValue) {
|
||||
|
||||
+9
-10
@@ -37,8 +37,9 @@ const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = {
|
||||
I18nFrontDecorator,
|
||||
],
|
||||
args: {
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
existingFieldMetadataId: fieldMetadataItem.id,
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
@@ -53,18 +54,16 @@ export const Default: Story = {};
|
||||
|
||||
export const WithRelationForm: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'people',
|
||||
),
|
||||
existingFieldMetadataId: 'new-field',
|
||||
fieldType: FieldMetadataType.RELATION,
|
||||
objectNameSingular: 'company',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSelectForm: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: {
|
||||
label: 'Industry',
|
||||
icon: 'IconBuildingFactory2',
|
||||
type: FieldMetadataType.SELECT,
|
||||
},
|
||||
existingFieldMetadataId: 'new-field',
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
objectNameSingular: 'company',
|
||||
},
|
||||
};
|
||||
|
||||
+8
-13
@@ -1,6 +1,6 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { TEXT_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/components/text/constants/TextDataModelSelectOptions';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -10,16 +10,7 @@ import { z } from 'zod';
|
||||
|
||||
type SettingsDataModelFieldTextFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
| 'icon'
|
||||
| 'label'
|
||||
| 'type'
|
||||
| 'defaultValue'
|
||||
| 'settings'
|
||||
| 'isUnique'
|
||||
| 'isCustom'
|
||||
>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
const textFieldDefaultValueSchema = z.object({
|
||||
@@ -36,16 +27,20 @@ export type SettingsDataModelFieldTextFormValues = z.infer<
|
||||
|
||||
export const SettingsDataModelFieldTextForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldTextFormProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { fieldMetadataItem: existingFieldMetadataItem } =
|
||||
useFieldMetadataItemById(existingFieldMetadataId);
|
||||
|
||||
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
return (
|
||||
<Controller
|
||||
name="settings"
|
||||
defaultValue={{
|
||||
displayedMaxRows: fieldMetadataItem?.settings?.displayedMaxRows || 0,
|
||||
displayedMaxRows:
|
||||
existingFieldMetadataItem?.settings?.displayedMaxRows || 0,
|
||||
}}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
|
||||
+16
-25
@@ -1,56 +1,47 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
|
||||
import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm';
|
||||
import { SettingsDataModelFieldTextForm } from '@/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
type SettingsDataModelFieldTextSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'isCustom' | 'settings'
|
||||
> &
|
||||
Partial<{ id: string }>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldTextSettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldTextSettingsFormCardProps) => {
|
||||
const { watch } = useFormContext();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
label: watch('label'),
|
||||
icon: watch('icon'),
|
||||
type: FieldMetadataType.TEXT,
|
||||
settings: watch('settings'),
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<>
|
||||
<SettingsDataModelFieldTextForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
<SettingsDataModelFieldIsUniqueForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldType={FieldMetadataType.TEXT}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+3
-7
@@ -1,7 +1,6 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldCurrencyFormat } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/currencyFieldDefaultValueSchema';
|
||||
import { currencyFieldSettingsSchema } from '@/object-record/record-field/ui/validation-schemas/currencyFieldSettingsSchema';
|
||||
@@ -24,15 +23,12 @@ export type SettingsDataModelFieldCurrencyFormValues = z.infer<
|
||||
|
||||
type SettingsDataModelFieldCurrencyFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldCurrencyForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldCurrencyFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const {
|
||||
@@ -40,7 +36,7 @@ export const SettingsDataModelFieldCurrencyForm = ({
|
||||
initialCurrencyCodeValue,
|
||||
initialSettingsValue,
|
||||
} = useCurrencySettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
});
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldCurrencyFormValues>();
|
||||
|
||||
+20
-32
@@ -1,60 +1,48 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
SettingsDataModelFieldCurrencyForm,
|
||||
type SettingsDataModelFieldCurrencyFormValues,
|
||||
} from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
|
||||
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
type SettingsDataModelFieldCurrencySettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldCurrencySettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldCurrencySettingsFormCardProps) => {
|
||||
const { initialDefaultValue, initialSettingsValue } =
|
||||
useCurrencySettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldCurrencyFormValues>();
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldCurrencyFormValues &
|
||||
SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
defaultValue: watchFormValue('defaultValue', initialDefaultValue),
|
||||
settings: watchFormValue('settings', initialSettingsValue),
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
label: watch('label'),
|
||||
icon: watch('icon'),
|
||||
defaultValue: watch('defaultValue'),
|
||||
settings: watch('settings'),
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldCurrencyForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+15
-6
@@ -1,21 +1,30 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { CurrencyCode } from '@/object-record/record-field/ui/types/CurrencyCode';
|
||||
import { type SettingsDataModelFieldCurrencyFormValues } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
type UseCurrencySettingsFormInitialValuesArgs = {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue' | 'settings'>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
export const useCurrencySettingsFormInitialValues = ({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: UseCurrencySettingsFormInitialValuesArgs) => {
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
const initialAmountMicrosValue =
|
||||
(fieldMetadataItem?.defaultValue?.amountMicros as number | null) ?? null;
|
||||
const initialCurrencyCodeValue =
|
||||
fieldMetadataItem?.defaultValue?.currencyCode ??
|
||||
applySimpleQuotesToString(CurrencyCode.USD);
|
||||
const initialCurrencyCodeValue = isNonEmptyString(
|
||||
stripSimpleQuotesFromString(fieldMetadataItem?.defaultValue?.currencyCode),
|
||||
)
|
||||
? fieldMetadataItem?.defaultValue?.currencyCode
|
||||
: applySimpleQuotesToString(CurrencyCode.USD);
|
||||
|
||||
const initialFormValues: SettingsDataModelFieldCurrencyFormValues = {
|
||||
settings: {
|
||||
format: fieldMetadataItem?.settings?.format ?? 'short',
|
||||
|
||||
+3
-4
@@ -2,7 +2,6 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { validateCustomDateFormat } from '@/localization/utils/validateCustomDateFormat';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isDateFieldCustomDisplayFormat } from '@/object-record/record-field/ui/types/guards/isDateFIeldCustomDisplayFormat';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
@@ -45,12 +44,12 @@ export type SettingsDataModelFieldDateFormValues = z.infer<
|
||||
|
||||
type SettingsDataModelFieldDateFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'settings'>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldDateForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldDateFormProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -59,7 +58,7 @@ export const SettingsDataModelFieldDateForm = ({
|
||||
|
||||
const { initialDisplayFormat, initialCustomUnicodeDateFormat } =
|
||||
useDateSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
fieldMetadataId: existingFieldMetadataId,
|
||||
});
|
||||
|
||||
const displayFormatFromForm = watch('settings.displayFormat');
|
||||
|
||||
+24
-39
@@ -1,75 +1,60 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm';
|
||||
import {
|
||||
SettingsDataModelFieldDateForm,
|
||||
type SettingsDataModelFieldDateFormValues,
|
||||
} from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
import { useDateSettingsFormInitialValues } from '@/settings/data-model/fields/forms/date/hooks/useDateSettingsFormInitialValues';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
type SettingsDataModelFieldDateSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'isCustom' | 'settings' | 'isUnique'
|
||||
> &
|
||||
Partial<{ id: string }>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
fieldType: FieldMetadataType.DATE_TIME | FieldMetadataType.DATE;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldDateSettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
fieldType,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldDateSettingsFormCardProps) => {
|
||||
const { initialDisplayFormat, initialCustomUnicodeDateFormat } =
|
||||
useDateSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldDateFormValues>();
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldDateFormValues & SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
type: fieldType,
|
||||
label: watch('label'),
|
||||
icon: watch('icon'),
|
||||
settings: {
|
||||
displayFormat: watchFormValue(
|
||||
'settings.displayFormat',
|
||||
initialDisplayFormat,
|
||||
),
|
||||
customUnicodeDateFormat: watchFormValue(
|
||||
displayFormat: watch('settings.displayFormat'),
|
||||
customUnicodeDateFormat: watch(
|
||||
'settings.customUnicodeDateFormat',
|
||||
initialCustomUnicodeDateFormat,
|
||||
),
|
||||
},
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<>
|
||||
<SettingsDataModelFieldDateForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
<SettingsDataModelFieldIsUniqueForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldType={fieldType}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+9
-5
@@ -1,14 +1,18 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { FieldDateDisplayFormat } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { type SettingsDataModelFieldDateFormValues } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
|
||||
type UseDateSettingsFormInitialValuesProps = {
|
||||
fieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const useDateSettingsFormInitialValues = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'settings'>;
|
||||
}) => {
|
||||
fieldMetadataId,
|
||||
}: UseDateSettingsFormInitialValuesProps) => {
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(fieldMetadataId);
|
||||
|
||||
const initialDisplayFormat =
|
||||
(fieldMetadataItem?.settings?.displayFormat as FieldDateDisplayFormat) ??
|
||||
FieldDateDisplayFormat.USER_SETTINGS;
|
||||
|
||||
+16
-14
@@ -2,16 +2,14 @@ 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 { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import {
|
||||
RELATION_TYPE_OPTIONS,
|
||||
StyledInputsContainer,
|
||||
@@ -23,7 +21,8 @@ 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';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const settingsDataModelFieldMorphRelationFormSchema = z.object({
|
||||
morphRelationObjectMetadataIds: z.array(z.string().uuid()).min(2),
|
||||
@@ -39,33 +38,36 @@ export type SettingsDataModelFieldMorphRelationFormValues = z.infer<
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldMorphRelationFormProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'morphRelations'>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldMorphRelationForm = ({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldMorphRelationFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldMorphRelationFormValues>();
|
||||
|
||||
const morphRelationPreviouslyCreated =
|
||||
fieldMetadataItemHasMorphRelations(fieldMetadataItem);
|
||||
const { fieldMetadataItem: existingFieldMetadataItem } =
|
||||
useFieldMetadataItemById(existingFieldMetadataId);
|
||||
|
||||
const disableRelationEdition = isDefined(existingFieldMetadataItem);
|
||||
const disableFieldEdition = isDefined(existingFieldMetadataItem)
|
||||
? fieldMetadataItemDisableFieldEdition(existingFieldMetadataItem)
|
||||
: false;
|
||||
|
||||
const disableRelationEdition = !!morphRelationPreviouslyCreated;
|
||||
const disableFieldEdition =
|
||||
fieldMetadataItemDisableFieldEdition(fieldMetadataItem);
|
||||
const initialRelationObjectMetadataItems =
|
||||
useMorphRelationSettingsFormInitialTargetMetadatas({
|
||||
fieldMetadataItem,
|
||||
fieldMetadataItem: existingFieldMetadataItem,
|
||||
});
|
||||
|
||||
const initialRelationType =
|
||||
fieldMetadataItemInitialRelationType(fieldMetadataItem);
|
||||
existingFieldMetadataItem?.settings?.relationType ??
|
||||
RelationType.ONE_TO_MANY;
|
||||
|
||||
const { label: defaultLabelOnDestination, icon: defaultIconOnDestination } =
|
||||
useMorphRelationSettingsFormDefaultValuesOnDestination({
|
||||
fieldMetadataItem,
|
||||
fieldMetadataItem: existingFieldMetadataItem,
|
||||
objectMetadataItem: initialRelationObjectMetadataItems[0],
|
||||
relationType: initialRelationType,
|
||||
});
|
||||
|
||||
+57
-116
@@ -1,132 +1,82 @@
|
||||
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 { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
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 { SettingsDataModelRelationFieldPreviewSubWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreviewSubWidget';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationType,
|
||||
type Relation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
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%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldMorphRelationFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldMorphRelationFormCardProps) => {
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldMorphRelationFormValues>();
|
||||
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldMorphRelationFormValues &
|
||||
SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
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 { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const relationObjectMetadataId = watchFormValue(
|
||||
'morphRelationObjectMetadataIds.0',
|
||||
initialRelationObjectMetadataItem?.id,
|
||||
);
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
relationObjectMetadataId,
|
||||
const relationObjectMetadataIds: string[] = watch(
|
||||
'morphRelationObjectMetadataIds',
|
||||
[],
|
||||
);
|
||||
|
||||
if (!relationObjectMetadataItem) return null;
|
||||
const relationObjectMetadataItems = relationObjectMetadataIds
|
||||
.map((relationObjectMetadataId) =>
|
||||
objectMetadataItems.find((item) => item.id === relationObjectMetadataId),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const relationType: RelationType = watchFormValue(
|
||||
const fallbackRelationObjectMetadataItem = objectMetadataItems[0];
|
||||
|
||||
const relationType: RelationType = watch(
|
||||
'relationType',
|
||||
initialRelationType,
|
||||
RelationType.ONE_TO_MANY,
|
||||
);
|
||||
|
||||
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
|
||||
<SettingsDataModelRelationFieldPreviewSubWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
relation: {
|
||||
type: relationType,
|
||||
} as Relation,
|
||||
icon: watch('icon'),
|
||||
label: watch('label'),
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
settings: {
|
||||
relationType,
|
||||
},
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
pluralizeLabel={
|
||||
watchFormValue('relationType') === RelationType.MANY_TO_ONE
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
fieldPreviewTargetObjectNameSingular={
|
||||
relationObjectMetadataItems[0]?.nameSingular ??
|
||||
fallbackRelationObjectMetadataItem.nameSingular
|
||||
}
|
||||
pluralizeLabel={watch('relationType') === RelationType.MANY_TO_ONE}
|
||||
/>
|
||||
<SettingsDataModelRelationPreviewImage
|
||||
src={relationTypeConfig.imageSrc}
|
||||
@@ -134,38 +84,29 @@ export const SettingsDataModelFieldMorphRelationFormCard = ({
|
||||
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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<SettingsDataModelRelationFieldPreviewSubWidget
|
||||
fieldMetadataItem={{
|
||||
icon: watch('iconOnDestination'),
|
||||
label: watch('targetFieldLabel'),
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: {
|
||||
relationType: oppositeRelationType,
|
||||
},
|
||||
}}
|
||||
shrink
|
||||
objectNameSingulars={
|
||||
relationObjectMetadataItems.length > 0
|
||||
? relationObjectMetadataItems.map((item) => item.nameSingular)
|
||||
: [fallbackRelationObjectMetadataItem.nameSingular]
|
||||
}
|
||||
fieldPreviewTargetObjectNameSingular={objectNameSingular}
|
||||
pluralizeLabel={watch('relationType') !== RelationType.MANY_TO_ONE}
|
||||
/>
|
||||
</SettingsDataModelFieldRelationPreviewContent>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldMorphRelationForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
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;
|
||||
};
|
||||
+7
-6
@@ -1,7 +1,7 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { numberFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/numberFieldDefaultValueSchema';
|
||||
import { Separator } from '@/settings/components/Separator';
|
||||
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
|
||||
@@ -22,19 +22,20 @@ export type SettingsDataModelFieldNumberFormValues = z.infer<
|
||||
|
||||
type SettingsDataModelFieldNumberFormProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldNumberForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldNumberFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } = useFormContext<SettingsDataModelFieldNumberFormValues>();
|
||||
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="settings"
|
||||
|
||||
+14
-26
@@ -1,59 +1,47 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm';
|
||||
import { SettingsDataModelFieldNumberForm } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
type SettingsDataModelFieldNumberSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'isCustom' | 'settings' | 'isUnique'
|
||||
> &
|
||||
Partial<{ id: string }>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldNumberSettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldNumberSettingsFormCardProps) => {
|
||||
const { watch } = useFormContext();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
icon: watch('icon'),
|
||||
label: watch('label') || 'New Field',
|
||||
settings: watch('settings') || null,
|
||||
type: fieldMetadataItem.type,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<>
|
||||
<SettingsDataModelFieldNumberForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
|
||||
<SettingsDataModelFieldIsUniqueForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldType={FieldMetadataType.NUMBER}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+9
-8
@@ -1,6 +1,6 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue';
|
||||
@@ -20,17 +20,14 @@ import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFro
|
||||
type SettingsDataModelFieldPhonesFormProps = {
|
||||
disabled?: boolean;
|
||||
defaultCountryCode?: string;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const settingsDataModelFieldPhonesFormSchema = z.object({
|
||||
defaultValue: phonesFieldDefaultValueSchema,
|
||||
});
|
||||
|
||||
export type SettingsDataModelFieldTextFormValues = z.infer<
|
||||
export type SettingsDataModelFieldPhonesFormValues = z.infer<
|
||||
typeof settingsDataModelFieldPhonesFormSchema
|
||||
>;
|
||||
|
||||
@@ -38,10 +35,14 @@ export type CountryCodeOrEmpty = CountryCode | '';
|
||||
|
||||
export const SettingsDataModelFieldPhonesForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldPhonesFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
const { control } = useFormContext<SettingsDataModelFieldPhonesFormValues>();
|
||||
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
const countries = [
|
||||
{ label: t`No country`, value: '', Icon: IconCircleOff },
|
||||
|
||||
+28
-26
@@ -1,51 +1,53 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
|
||||
import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm';
|
||||
import { SettingsDataModelFieldPhonesForm } from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
SettingsDataModelFieldPhonesForm,
|
||||
type SettingsDataModelFieldPhonesFormValues,
|
||||
} from '@/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
type SettingsDataModelFieldPhonesSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'isCustom' | 'settings'
|
||||
> &
|
||||
Partial<{ id: string }>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldPhonesSettingsFormCard = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldPhonesSettingsFormCardProps) => {
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldPhonesFormValues &
|
||||
SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
type: FieldMetadataType.PHONES,
|
||||
label: watch('label'),
|
||||
icon: watch('icon'),
|
||||
}}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<>
|
||||
<SettingsDataModelFieldPhonesForm
|
||||
disabled={disabled}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
<SettingsDataModelFieldIsUniqueForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldType={FieldMetadataType.PHONES}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
+4
-5
@@ -3,7 +3,6 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||
@@ -46,8 +45,8 @@ export type SettingsDataModelFieldRelationFormValues = z.infer<
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldRelationFormProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type'>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
existingFieldMetadataId: string;
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const StyledContainer = styled.div`
|
||||
@@ -83,7 +82,7 @@ export const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES).map(
|
||||
);
|
||||
|
||||
export const SettingsDataModelFieldRelationForm = ({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelFieldRelationFormProps) => {
|
||||
const { t } = useLingui();
|
||||
@@ -100,7 +99,7 @@ export const SettingsDataModelFieldRelationForm = ({
|
||||
initialRelationObjectMetadataItem,
|
||||
initialRelationType,
|
||||
} = useRelationSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
|
||||
+48
-79
@@ -1,8 +1,6 @@
|
||||
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 { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||
import {
|
||||
@@ -11,58 +9,35 @@ import {
|
||||
} 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,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelRelationFieldPreviewSubWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreviewSubWidget';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
type Relation,
|
||||
RelationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
type SettingsDataModelFieldRelationSettingsFormCardProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||
relationFieldMetadataItem?: FieldMetadataItem;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
type SettingsDataModelFieldRelationSettingsFormCardProps = {
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldRelationSettingsFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldRelationSettingsFormCardProps) => {
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldRelationFormValues>();
|
||||
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldRelationFormValues &
|
||||
SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
initialRelationObjectMetadataItem,
|
||||
initialRelationType,
|
||||
initialRelationFieldMetadataItem,
|
||||
} = useRelationSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const relationObjectMetadataId = watchFormValue(
|
||||
'relation.objectMetadataId',
|
||||
initialRelationObjectMetadataItem?.id,
|
||||
);
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
relationObjectMetadataId,
|
||||
);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
if (!relationObjectMetadataItem) return null;
|
||||
|
||||
const relationType: RelationType = watchFormValue(
|
||||
'relation.type',
|
||||
initialRelationType,
|
||||
const relationObjectMetadataId = watch('relation.objectMetadataId');
|
||||
const relationObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === relationObjectMetadataId,
|
||||
);
|
||||
const relationTargetField = watch('relation.field');
|
||||
|
||||
const relationType: RelationType = watch('relation.type');
|
||||
const relationTypeConfig = RELATION_TYPES[relationType];
|
||||
|
||||
const oppositeRelationType =
|
||||
@@ -74,56 +49,50 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<SettingsDataModelFieldRelationPreviewContent isMobile={isMobile}>
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelRelationFieldPreviewSubWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
relation: {
|
||||
type: relationType,
|
||||
} as Relation,
|
||||
icon: watch('icon'),
|
||||
label: watch('label'),
|
||||
type: FieldMetadataType.RELATION,
|
||||
settings: {
|
||||
relationType: relationType,
|
||||
},
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
pluralizeLabel={
|
||||
watchFormValue('relation.type') === RelationType.MANY_TO_ONE
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
fieldPreviewTargetObjectNameSingular={
|
||||
relationObjectMetadataItem?.nameSingular ?? 'company'
|
||||
}
|
||||
pluralizeLabel={relationType === RelationType.ONE_TO_MANY}
|
||||
/>
|
||||
<SettingsDataModelRelationPreviewImage
|
||||
src={relationTypeConfig.imageSrc}
|
||||
flip={relationTypeConfig.isImageFlipped}
|
||||
alt={relationTypeConfig.label}
|
||||
src={relationTypeConfig?.imageSrc}
|
||||
flip={relationTypeConfig?.isImageFlipped}
|
||||
alt={relationTypeConfig?.label}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelRelationFieldPreviewSubWidget
|
||||
fieldMetadataItem={{
|
||||
...initialRelationFieldMetadataItem,
|
||||
icon: watchFormValue(
|
||||
'relation.field.icon',
|
||||
initialRelationFieldMetadataItem.icon,
|
||||
),
|
||||
label:
|
||||
watchFormValue(
|
||||
'relation.field.label',
|
||||
initialRelationFieldMetadataItem.label,
|
||||
) || 'Field name',
|
||||
icon: relationTargetField?.icon,
|
||||
label: relationTargetField?.label,
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: {
|
||||
type: oppositeRelationType,
|
||||
} as Relation,
|
||||
settings: {
|
||||
relationType: oppositeRelationType,
|
||||
},
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={relationObjectMetadataItem}
|
||||
relationObjectMetadataItem={objectMetadataItem}
|
||||
pluralizeLabel={
|
||||
watchFormValue('relation.type') !== RelationType.MANY_TO_ONE
|
||||
}
|
||||
objectNameSingulars={[
|
||||
relationObjectMetadataItem?.nameSingular ?? 'company',
|
||||
]}
|
||||
fieldPreviewTargetObjectNameSingular={objectNameSingular}
|
||||
pluralizeLabel={oppositeRelationType === RelationType.ONE_TO_MANY}
|
||||
/>
|
||||
</SettingsDataModelFieldRelationPreviewContent>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldRelationForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectMetadataItem={relationObjectMetadataItem}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
+17
-14
@@ -1,22 +1,29 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { type SettingsDataModelFieldPreviewCardProps } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type UseRelationSettingsFormInitialValuesProps = {
|
||||
existingFieldMetadataId: string;
|
||||
objectMetadataItem?: Pick<
|
||||
ObjectMetadataItem,
|
||||
'id' | 'icon' | 'labelSingular' | 'labelPlural'
|
||||
>;
|
||||
};
|
||||
|
||||
export const useRelationSettingsFormInitialValues = ({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'type' | 'relation'>;
|
||||
objectMetadataItem?: SettingsDataModelFieldPreviewCardProps['objectMetadataItem'];
|
||||
}) => {
|
||||
}: UseRelationSettingsFormInitialValuesProps) => {
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
const {
|
||||
@@ -52,14 +59,10 @@ export const useRelationSettingsFormInitialValues = ({
|
||||
const initialRelationType =
|
||||
relationTypeFromFieldMetadata ?? RelationType.ONE_TO_MANY;
|
||||
|
||||
const targetIsMorphRelation =
|
||||
isDefined(relationFieldMetadataItem) &&
|
||||
isFieldMorphRelation(relationFieldMetadataItem);
|
||||
|
||||
return {
|
||||
disableFieldEdition:
|
||||
relationFieldMetadataItem &&
|
||||
(!relationFieldMetadataItem.isCustom || targetIsMorphRelation),
|
||||
isDefined(relationFieldMetadataItem) &&
|
||||
relationFieldMetadataItem?.isCustom === true,
|
||||
disableRelationEdition: !!relationFieldMetadataItem,
|
||||
initialRelationFieldMetadataItem: relationFieldMetadataItem ?? {
|
||||
icon: initialRelationObjectMetadataItem.icon ?? 'IconUsers',
|
||||
|
||||
+13
-15
@@ -3,10 +3,7 @@ import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type FieldMetadataItem,
|
||||
type FieldMetadataItemOption,
|
||||
} from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { selectOptionsSchema } from '@/object-metadata/validation-schemas/selectOptionsSchema';
|
||||
import { multiSelectFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/multiSelectFieldDefaultValueSchema';
|
||||
import { selectFieldDefaultValueSchema } from '@/object-record/record-field/ui/validation-schemas/selectFieldDefaultValueSchema';
|
||||
@@ -46,10 +43,8 @@ export type SettingsDataModelFieldSelectFormValues = z.infer<
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldSelectFormProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'defaultValue' | 'options' | 'type'
|
||||
>;
|
||||
fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled(CardContent)`
|
||||
@@ -112,10 +107,13 @@ const StyledButton = styled(LightButton)`
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldSelectForm = ({
|
||||
fieldMetadataItem,
|
||||
existingFieldMetadataId,
|
||||
fieldType,
|
||||
}: SettingsDataModelFieldSelectFormProps) => {
|
||||
const { initialDefaultValue, initialOptions } =
|
||||
useSelectSettingsFormInitialValues({ fieldMetadataItem });
|
||||
useSelectSettingsFormInitialValues({
|
||||
fieldMetadataId: existingFieldMetadataId,
|
||||
});
|
||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||
|
||||
const {
|
||||
@@ -144,7 +142,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
||||
optionValue: FieldMetadataItemOption['value'],
|
||||
) =>
|
||||
isSelectOptionDefaultValue(optionValue, {
|
||||
type: fieldMetadataItem.type,
|
||||
type: fieldType,
|
||||
defaultValue: watchFormValue('defaultValue'),
|
||||
});
|
||||
|
||||
@@ -153,7 +151,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
||||
) => {
|
||||
if (isOptionDefaultValue(optionValue)) return;
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.SELECT) {
|
||||
if (fieldType === FieldMetadataType.SELECT) {
|
||||
setFormValue('defaultValue', applySimpleQuotesToString(optionValue), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
@@ -163,7 +161,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
||||
const previousDefaultValue = getValues('defaultValue');
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT &&
|
||||
fieldType === FieldMetadataType.MULTI_SELECT &&
|
||||
(Array.isArray(previousDefaultValue) || previousDefaultValue === null)
|
||||
) {
|
||||
setFormValue(
|
||||
@@ -182,7 +180,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
||||
) => {
|
||||
if (!isOptionDefaultValue(optionValue)) return;
|
||||
|
||||
if (fieldMetadataItem.type === FieldMetadataType.SELECT) {
|
||||
if (fieldType === FieldMetadataType.SELECT) {
|
||||
setFormValue('defaultValue', null, { shouldDirty: true });
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +188,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
||||
const previousDefaultValue = getValues('defaultValue');
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.MULTI_SELECT &&
|
||||
fieldType === FieldMetadataType.MULTI_SELECT &&
|
||||
(Array.isArray(previousDefaultValue) || previousDefaultValue === null)
|
||||
) {
|
||||
const nextDefaultValue = previousDefaultValue?.filter(
|
||||
|
||||
+25
-42
@@ -1,19 +1,15 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { type z } from 'zod';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
type settingsDataModelFieldMultiSelectFormSchema,
|
||||
SettingsDataModelFieldSelectForm,
|
||||
type settingsDataModelFieldSelectFormSchema,
|
||||
} from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
|
||||
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
type SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
type SettingsDataModelFieldSelectOrMultiSelectFormValues = z.infer<
|
||||
| typeof settingsDataModelFieldSelectFormSchema
|
||||
@@ -21,53 +17,40 @@ type SettingsDataModelFieldSelectOrMultiSelectFormValues = z.infer<
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldSelectSettingsFormCardProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
| 'icon'
|
||||
| 'label'
|
||||
| 'type'
|
||||
| 'defaultValue'
|
||||
| 'options'
|
||||
| 'isUnique'
|
||||
| 'isCustom'
|
||||
>;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
objectNameSingular: string;
|
||||
fieldType: FieldMetadataType.SELECT | FieldMetadataType.MULTI_SELECT;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldSelectSettingsFormCard = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
fieldType,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldSelectSettingsFormCardProps) => {
|
||||
const { initialOptions, initialDefaultValue } =
|
||||
useSelectSettingsFormInitialValues({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldSelectOrMultiSelectFormValues>();
|
||||
const { watch: watchFormValue } = useFormContext<
|
||||
SettingsDataModelFieldSelectOrMultiSelectFormValues &
|
||||
SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledFieldPreviewCard
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
...fieldMetadataItem,
|
||||
defaultValue: watchFormValue('defaultValue', initialDefaultValue),
|
||||
options: watchFormValue('options', initialOptions),
|
||||
type: fieldType,
|
||||
label: watchFormValue('label'),
|
||||
icon: watchFormValue('icon'),
|
||||
defaultValue: watchFormValue('defaultValue'),
|
||||
options: watchFormValue('options'),
|
||||
}}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<>
|
||||
<SettingsDataModelFieldSelectForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
</>
|
||||
<SettingsDataModelFieldSelectForm
|
||||
fieldType={fieldType}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
+10
-8
@@ -2,10 +2,8 @@ import { useMemo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
type FieldMetadataItem,
|
||||
type FieldMetadataItemOption,
|
||||
} from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
|
||||
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
||||
|
||||
@@ -17,11 +15,15 @@ const DEFAULT_OPTION: FieldMetadataItemOption = {
|
||||
value: computeOptionValueFromLabel('Option 1'),
|
||||
};
|
||||
|
||||
type UseSelectSettingsFormInitialValuesProps = {
|
||||
fieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const useSelectSettingsFormInitialValues = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem?: Pick<FieldMetadataItem, 'defaultValue' | 'options'>;
|
||||
}) => {
|
||||
fieldMetadataId,
|
||||
}: UseSelectSettingsFormInitialValuesProps) => {
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(fieldMetadataId);
|
||||
|
||||
const initialDefaultValue =
|
||||
(fieldMetadataItem?.defaultValue as SettingsDataModelFieldSelectFormValues['defaultValue']) ??
|
||||
null;
|
||||
|
||||
+20
-57
@@ -1,38 +1,24 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { BooleanFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/BooleanFieldInput';
|
||||
import { RatingFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RatingFieldInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
|
||||
import { SettingsDataModelSetPreviewRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
|
||||
import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue';
|
||||
import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type SettingsDataModelFieldPreviewProps = {
|
||||
type SettingsDataModelFieldPreviewProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
| 'icon'
|
||||
| 'label'
|
||||
| 'type'
|
||||
| 'defaultValue'
|
||||
| 'options'
|
||||
| 'settings'
|
||||
| 'relation'
|
||||
> & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
'name' | 'icon' | 'label' | 'type' | 'defaultValue' | 'options' | 'settings'
|
||||
>;
|
||||
objectNameSingular: string;
|
||||
shrink?: boolean;
|
||||
withFieldLabel?: boolean;
|
||||
};
|
||||
@@ -64,54 +50,35 @@ const StyledFieldLabel = styled.div`
|
||||
|
||||
export const SettingsDataModelFieldPreview = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
objectNameSingular,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
}: SettingsDataModelFieldPreviewProps) => {
|
||||
const theme = useTheme();
|
||||
const { labelIdentifierFieldMetadataItem } =
|
||||
useLabelIdentifierFieldMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const FieldIcon = getIcon(fieldMetadataItem.icon);
|
||||
|
||||
// id and name are undefined in create mode (field does not exist yet)
|
||||
// and defined in edit mode.
|
||||
const isLabelIdentifier =
|
||||
!!fieldMetadataItem.id &&
|
||||
!!fieldMetadataItem.name &&
|
||||
isLabelIdentifierField({
|
||||
fieldMetadataItem: {
|
||||
id: fieldMetadataItem.id,
|
||||
name: fieldMetadataItem.name,
|
||||
},
|
||||
objectMetadataItem,
|
||||
});
|
||||
labelIdentifierFieldMetadataItem?.name === fieldMetadataItem.name;
|
||||
|
||||
const previewRecord = usePreviewRecord({
|
||||
objectMetadataItem,
|
||||
skip: !isLabelIdentifier,
|
||||
});
|
||||
const fieldName = fieldMetadataItem.name;
|
||||
const recordId = `${objectNameSingular}-${fieldName}-preview`;
|
||||
|
||||
const fieldPreviewValue = useFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
skip: isLabelIdentifier,
|
||||
});
|
||||
|
||||
const fieldName =
|
||||
fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`;
|
||||
const recordId =
|
||||
previewRecord?.id ??
|
||||
`${objectMetadataItem.nameSingular}-${fieldName}-${fieldMetadataItem.relation?.type}-${relationObjectMetadataItem?.nameSingular}-preview`;
|
||||
|
||||
const metadata = {
|
||||
fieldName,
|
||||
objectMetadataNameSingular: objectMetadataItem.nameSingular,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular || '',
|
||||
objectMetadataNameSingular: objectNameSingular,
|
||||
options: fieldMetadataItem.options ?? [],
|
||||
settings: fieldMetadataItem.settings,
|
||||
relationType: fieldMetadataItem.relation?.type,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -121,15 +88,11 @@ export const SettingsDataModelFieldPreview = ({
|
||||
instanceId: 'record-field-component-instance-id',
|
||||
}}
|
||||
>
|
||||
{isDefined(previewRecord) ? (
|
||||
<SettingsDataModelSetPreviewRecordEffect record={previewRecord} />
|
||||
) : (
|
||||
<SettingsDataModelSetFieldValueEffect
|
||||
recordId={recordId}
|
||||
fieldName={fieldName}
|
||||
value={fieldPreviewValue}
|
||||
/>
|
||||
)}
|
||||
<SettingsDataModelSetFieldValueEffect
|
||||
recordId={recordId}
|
||||
gqlFieldName={fieldMetadataItem.name ?? ''}
|
||||
value={fieldPreviewValue}
|
||||
/>
|
||||
<StyledFieldPreview shrink={shrink}>
|
||||
{!!withFieldLabel && (
|
||||
<StyledFieldLabel>
|
||||
@@ -147,7 +110,7 @@ export const SettingsDataModelFieldPreview = ({
|
||||
fieldDefinition: {
|
||||
type: fieldMetadataItem.type,
|
||||
iconName: 'FieldIcon',
|
||||
fieldMetadataId: fieldMetadataItem.id || '',
|
||||
fieldMetadataId: '',
|
||||
label: fieldMetadataItem.label,
|
||||
metadata,
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
SettingsDataModelFieldPreview,
|
||||
type SettingsDataModelFieldPreviewProps,
|
||||
} 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 SettingsDataModelFieldPreviewCardProps =
|
||||
SettingsDataModelFieldPreviewProps & {
|
||||
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 SettingsDataModelFieldPreviewCard = ({
|
||||
className,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
pluralizeLabel = false,
|
||||
}: SettingsDataModelFieldPreviewCardProps) => {
|
||||
return (
|
||||
<StyledCard className={className} fullWidth>
|
||||
<StyledCardContent>
|
||||
<SettingsDataModelObjectPreview
|
||||
objectMetadataItems={[objectMetadataItem]}
|
||||
pluralizeLabel={pluralizeLabel}
|
||||
/>
|
||||
<SettingsDataModelFieldPreview
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
shrink={shrink}
|
||||
withFieldLabel={withFieldLabel}
|
||||
/>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
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';
|
||||
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||
|
||||
type SettingsDataModelFieldPreviewWidgetProps = {
|
||||
className?: string;
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'defaultValue' | 'options' | 'settings'
|
||||
>;
|
||||
objectNameSingular: string;
|
||||
shrink?: boolean;
|
||||
withFieldLabel?: boolean;
|
||||
pluralizeLabel?: boolean;
|
||||
fullWidth?: 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 SettingsDataModelFieldPreviewWidget = ({
|
||||
className,
|
||||
fieldMetadataItem,
|
||||
objectNameSingular,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
pluralizeLabel = false,
|
||||
}: SettingsDataModelFieldPreviewWidgetProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledCard className={className} fullWidth>
|
||||
<StyledCardContent>
|
||||
<SettingsDataModelObjectPreview
|
||||
objectMetadataItems={[objectMetadataItem]}
|
||||
pluralizeLabel={pluralizeLabel}
|
||||
/>
|
||||
<SettingsDataModelFieldPreview
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
fieldMetadataItem={{
|
||||
label: fieldMetadataItem.label,
|
||||
icon: fieldMetadataItem.icon,
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
options: fieldMetadataItem.options,
|
||||
settings: fieldMetadataItem.settings,
|
||||
type: fieldMetadataItem.type,
|
||||
name: computeMetadataNameFromLabel(fieldMetadataItem.label),
|
||||
}}
|
||||
shrink={shrink}
|
||||
withFieldLabel={withFieldLabel}
|
||||
/>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
|
||||
import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsDataModelRelationFieldPreviewProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'settings'
|
||||
>;
|
||||
relationTargetObjectNameSingular: string;
|
||||
shrink?: boolean;
|
||||
withFieldLabel?: boolean;
|
||||
};
|
||||
|
||||
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: fit-content;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
padding: 0
|
||||
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
|
||||
white-space: nowrap;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledFieldLabel = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelRelationFieldPreview = ({
|
||||
fieldMetadataItem,
|
||||
relationTargetObjectNameSingular,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
}: SettingsDataModelRelationFieldPreviewProps) => {
|
||||
const { objectMetadataItem: relationTargetObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: relationTargetObjectNameSingular,
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const FieldIcon = getIcon(fieldMetadataItem.icon);
|
||||
|
||||
const fieldPreviewValue = useFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
relationObjectNameSingular: relationTargetObjectNameSingular,
|
||||
});
|
||||
|
||||
const fieldName = v4();
|
||||
|
||||
const recordId = `${relationTargetObjectNameSingular}-${fieldName}-preview`;
|
||||
|
||||
const metadata = {
|
||||
fieldName,
|
||||
objectMetadataNameSingular: 'company',
|
||||
relationObjectMetadataNameSingular: relationTargetObjectNameSingular,
|
||||
options: [],
|
||||
settings: fieldMetadataItem.settings,
|
||||
relationType: fieldMetadataItem.settings?.relationType,
|
||||
morphRelations:
|
||||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION
|
||||
? [
|
||||
{
|
||||
type: fieldMetadataItem.settings?.relationType,
|
||||
sourceFieldMetadata: {
|
||||
id: v4(),
|
||||
name: fieldName,
|
||||
},
|
||||
targetFieldMetadata: {
|
||||
id: v4(),
|
||||
name: 'does-not-matter',
|
||||
},
|
||||
sourceObjectMetadata: {
|
||||
id: v4(),
|
||||
namePlural: 'does-not-matter',
|
||||
nameSingular: 'does-not-matter',
|
||||
},
|
||||
targetObjectMetadata: {
|
||||
id: v4(),
|
||||
namePlural: relationTargetObjectMetadataItem.namePlural,
|
||||
nameSingular: relationTargetObjectMetadataItem.nameSingular,
|
||||
},
|
||||
} satisfies FieldMetadataItemRelation,
|
||||
]
|
||||
: [],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: 'record-field-component-instance-id',
|
||||
}}
|
||||
>
|
||||
<SettingsDataModelSetFieldValueEffect
|
||||
recordId={recordId}
|
||||
gqlFieldName={
|
||||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION
|
||||
? computeMorphRelationFieldName({
|
||||
fieldName: fieldName ?? '',
|
||||
relationDirection: fieldMetadataItem.settings?.relationType,
|
||||
nameSingular: relationTargetObjectNameSingular,
|
||||
namePlural: relationTargetObjectMetadataItem.namePlural,
|
||||
})
|
||||
: fieldName
|
||||
}
|
||||
value={fieldPreviewValue}
|
||||
/>
|
||||
<StyledFieldPreview shrink={shrink}>
|
||||
{!!withFieldLabel && (
|
||||
<StyledFieldLabel>
|
||||
<FieldIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
{fieldMetadataItem.label}:
|
||||
</StyledFieldLabel>
|
||||
)}
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
type: fieldMetadataItem.type,
|
||||
iconName: 'FieldIcon',
|
||||
fieldMetadataId: '',
|
||||
label: fieldMetadataItem.label,
|
||||
metadata,
|
||||
defaultValue: null,
|
||||
},
|
||||
isRecordFieldReadOnly: true,
|
||||
disableChipClick: true,
|
||||
}}
|
||||
>
|
||||
<FieldDisplay />
|
||||
</FieldContext.Provider>
|
||||
</StyledFieldPreview>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelRelationFieldPreview } from '@/settings/data-model/fields/preview/components/SettingsDataModelRelationFieldPreview';
|
||||
import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Card, CardContent } from 'twenty-ui/layout';
|
||||
|
||||
export type SettingsDataModelRelationFieldPreviewSubWidgetProps = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'icon' | 'label' | 'type' | 'settings'
|
||||
>;
|
||||
objectNameSingulars: string[];
|
||||
fieldPreviewTargetObjectNameSingular: string;
|
||||
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 SettingsDataModelRelationFieldPreviewSubWidget = ({
|
||||
className,
|
||||
fieldMetadataItem,
|
||||
objectNameSingulars,
|
||||
fieldPreviewTargetObjectNameSingular,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
pluralizeLabel = false,
|
||||
}: SettingsDataModelRelationFieldPreviewSubWidgetProps) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const targetObjectMetadataItems = objectNameSingulars
|
||||
.map((nameSingular) =>
|
||||
objectMetadataItems.find((item) => item.nameSingular === nameSingular),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
return (
|
||||
<StyledCard className={className} fullWidth>
|
||||
<StyledCardContent>
|
||||
<SettingsDataModelObjectPreview
|
||||
objectMetadataItems={targetObjectMetadataItems}
|
||||
pluralizeLabel={pluralizeLabel}
|
||||
/>
|
||||
<SettingsDataModelRelationFieldPreview
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
relationTargetObjectNameSingular={
|
||||
fieldPreviewTargetObjectNameSingular
|
||||
}
|
||||
shrink={shrink}
|
||||
withFieldLabel={withFieldLabel}
|
||||
/>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
+6
-22
@@ -1,44 +1,28 @@
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { settingsPreviewRecordIdState } from '@/settings/data-model/fields/preview/states/settingsPreviewRecordIdState';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
type SettingsDataModelSetFieldValueEffectProps = {
|
||||
recordId: string;
|
||||
fieldName: string;
|
||||
gqlFieldName: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
export const SettingsDataModelSetFieldValueEffect = ({
|
||||
recordId,
|
||||
fieldName,
|
||||
gqlFieldName,
|
||||
value,
|
||||
}: SettingsDataModelSetFieldValueEffectProps) => {
|
||||
const settingsPreviewRecordId = useRecoilValue(settingsPreviewRecordIdState);
|
||||
|
||||
const upsertedPreviewRecord = useRecoilValue(
|
||||
recordStoreFamilyState(settingsPreviewRecordId ?? ''),
|
||||
);
|
||||
|
||||
const setFieldValue = useSetRecoilState(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName,
|
||||
fieldName: gqlFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDefined(upsertedPreviewRecord) &&
|
||||
!!upsertedPreviewRecord[fieldName]
|
||||
) {
|
||||
setFieldValue(upsertedPreviewRecord[fieldName]);
|
||||
} else {
|
||||
setFieldValue(value);
|
||||
}
|
||||
}, [value, setFieldValue, recordId, fieldName, upsertedPreviewRecord]);
|
||||
setFieldValue(value);
|
||||
}, [value, setFieldValue, recordId, gqlFieldName]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { settingsPreviewRecordIdState } from '@/settings/data-model/fields/preview/states/settingsPreviewRecordIdState';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
type SettingsDataModelSetPreviewRecordEffectProps = {
|
||||
record: ObjectRecord;
|
||||
};
|
||||
|
||||
export const SettingsDataModelSetPreviewRecordEffect = ({
|
||||
record,
|
||||
}: SettingsDataModelSetPreviewRecordEffectProps) => {
|
||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const setSettingsPreviewRecordId = useSetRecoilState(
|
||||
settingsPreviewRecordIdState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
upsertRecordsInStore([record]);
|
||||
setSettingsPreviewRecordId(record.id);
|
||||
}, [record, upsertRecordsInStore, setSettingsPreviewRecordId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+14
-14
@@ -9,7 +9,7 @@ import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadat
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
|
||||
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
|
||||
import { SettingsDataModelFieldPreviewCard } from '../SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '../SettingsDataModelFieldPreviewWidget';
|
||||
|
||||
const mockedCompanyObjectMetadataItem =
|
||||
getMockObjectMetadataItemOrThrow('company');
|
||||
@@ -20,10 +20,10 @@ const mockedOpportunityObjectMetadataItem =
|
||||
const mockedPersonObjectMetadataItem =
|
||||
getMockObjectMetadataItemOrThrow('person');
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldPreviewCard> = {
|
||||
const meta: Meta<typeof SettingsDataModelFieldPreviewWidget> = {
|
||||
title:
|
||||
'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewCard',
|
||||
component: SettingsDataModelFieldPreviewCard,
|
||||
'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewWidget',
|
||||
component: SettingsDataModelFieldPreviewWidget,
|
||||
decorators: [
|
||||
MemoryRouterDecorator,
|
||||
ComponentDecorator,
|
||||
@@ -31,7 +31,7 @@ const meta: Meta<typeof SettingsDataModelFieldPreviewCard> = {
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
objectMetadataItem: mockedPersonObjectMetadataItem,
|
||||
objectNameSingular: mockedPersonObjectMetadataItem.nameSingular,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 480 },
|
||||
@@ -40,7 +40,7 @@ const meta: Meta<typeof SettingsDataModelFieldPreviewCard> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldPreviewCard>;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldPreviewWidget>;
|
||||
|
||||
export const LabelIdentifier: Story = {
|
||||
args: {
|
||||
@@ -65,7 +65,7 @@ export const Boolean: Story = {
|
||||
({ name, type }) =>
|
||||
name === 'idealCustomerProfile' && type === FieldMetadataType.BOOLEAN,
|
||||
),
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ export const Currency: Story = {
|
||||
name === 'annualRecurringRevenue' &&
|
||||
type === FieldMetadataType.CURRENCY,
|
||||
),
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ export const Date: Story = {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.DATE_TIME,
|
||||
),
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export const Links: Story = {
|
||||
({ name, type }) =>
|
||||
name === 'linkedinLink' && type === FieldMetadataType.LINKS,
|
||||
),
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export const Number: Story = {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem?.fields.find(
|
||||
({ type }) => type === FieldMetadataType.NUMBER,
|
||||
),
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
objectNameSingular: mockedCompanyObjectMetadataItem.nameSingular,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ export const Relation: Story = {
|
||||
fieldMetadataItem: mockedPersonObjectMetadataItem?.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
),
|
||||
relationObjectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
objectNameSingular: mockedPersonObjectMetadataItem.nameSingular,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -135,13 +135,13 @@ const selectFieldMetadataItem = getMockFieldMetadataItemOrThrow({
|
||||
export const Select: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: selectFieldMetadataItem,
|
||||
objectMetadataItem: mockedOpportunityObjectMetadataItem,
|
||||
objectNameSingular: mockedOpportunityObjectMetadataItem.nameSingular,
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiSelect: Story = {
|
||||
args: {
|
||||
objectMetadataItem: mockedOpportunityObjectMetadataItem,
|
||||
objectNameSingular: mockedOpportunityObjectMetadataItem.nameSingular,
|
||||
fieldMetadataItem: {
|
||||
...structuredClone(selectFieldMetadataItem),
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
+1
-1
@@ -86,7 +86,7 @@ describe('useFieldPreviewValue', () => {
|
||||
() =>
|
||||
useFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationObjectNameSingular: relationObjectMetadataItem?.nameSingular,
|
||||
}),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
+15
-14
@@ -1,6 +1,5 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRelationFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useRelationFieldPreviewValue';
|
||||
import { getAddressFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getAddressFieldPreviewValue';
|
||||
import { getCurrencyFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue';
|
||||
@@ -8,33 +7,31 @@ import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils
|
||||
import { getMultiSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue';
|
||||
import { getPhonesFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue';
|
||||
import { getSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type UseFieldPreviewParams = {
|
||||
fieldMetadataItem: Pick<
|
||||
FieldMetadataItem,
|
||||
'type' | 'options' | 'defaultValue' | 'relation'
|
||||
'type' | 'options' | 'defaultValue' | 'settings'
|
||||
>;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
relationObjectNameSingular?: string;
|
||||
skip?: boolean;
|
||||
};
|
||||
|
||||
export const useFieldPreviewValue = ({
|
||||
fieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationObjectNameSingular,
|
||||
skip,
|
||||
}: UseFieldPreviewParams) => {
|
||||
const relationFieldPreviewValue = useRelationFieldPreviewValue({
|
||||
relationObjectMetadataItem: relationObjectMetadataItem ?? {
|
||||
fields: [],
|
||||
labelSingular: '',
|
||||
labelIdentifierFieldMetadataId: '20202020-1000-4629-87e5-9a1fae1cc2fd',
|
||||
nameSingular: CoreObjectNameSingular.Company,
|
||||
},
|
||||
relationObjectNameSingular:
|
||||
relationObjectNameSingular ?? CoreObjectNameSingular.Company,
|
||||
skip:
|
||||
skip ||
|
||||
fieldMetadataItem.type !== FieldMetadataType.RELATION ||
|
||||
!relationObjectMetadataItem,
|
||||
(fieldMetadataItem.type !== FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.type !== FieldMetadataType.MORPH_RELATION) ||
|
||||
!isDefined(relationObjectNameSingular),
|
||||
});
|
||||
|
||||
if (skip === true) return null;
|
||||
@@ -42,8 +39,10 @@ export const useFieldPreviewValue = ({
|
||||
switch (fieldMetadataItem.type) {
|
||||
case FieldMetadataType.CURRENCY:
|
||||
return getCurrencyFieldPreviewValue({ fieldMetadataItem });
|
||||
case FieldMetadataType.MORPH_RELATION:
|
||||
case FieldMetadataType.RELATION:
|
||||
return fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE
|
||||
return fieldMetadataItem.settings?.relationType ===
|
||||
RelationType.MANY_TO_ONE
|
||||
? relationFieldPreviewValue
|
||||
: [relationFieldPreviewValue];
|
||||
case FieldMetadataType.SELECT:
|
||||
@@ -56,7 +55,9 @@ export const useFieldPreviewValue = ({
|
||||
return getPhonesFieldPreviewValue({ fieldMetadataItem });
|
||||
default:
|
||||
return getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
fieldSettings: fieldMetadataItem.settings,
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+17
-23
@@ -1,41 +1,36 @@
|
||||
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { pascalCase } from '~/utils/string/pascalCase';
|
||||
|
||||
type UsePreviewRecordParams = {
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
| 'fields'
|
||||
| 'labelIdentifierFieldMetadataId'
|
||||
| 'labelSingular'
|
||||
| 'nameSingular'
|
||||
>;
|
||||
objectNameSingular: string;
|
||||
skip?: boolean;
|
||||
};
|
||||
|
||||
export const usePreviewRecord = ({
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
skip: skipFromProps,
|
||||
}: UsePreviewRecordParams): ObjectRecord | null => {
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
const { labelIdentifierFieldMetadataItem } =
|
||||
useLabelIdentifierFieldMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const skip = skipFromProps || !labelIdentifierFieldMetadataItem;
|
||||
|
||||
let recordGqlFields: Record<string, boolean> | undefined = undefined;
|
||||
if (objectMetadataItem.nameSingular === CoreObjectNameSingular.NoteTarget)
|
||||
if (objectNameSingular === CoreObjectNameSingular.NoteTarget)
|
||||
recordGqlFields = { id: true, note: true };
|
||||
if (objectMetadataItem.nameSingular === CoreObjectNameSingular.TaskTarget)
|
||||
if (objectNameSingular === CoreObjectNameSingular.TaskTarget)
|
||||
recordGqlFields = { id: true, task: true };
|
||||
|
||||
const { records } = useFindManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectNameSingular,
|
||||
recordGqlFields,
|
||||
limit: 1,
|
||||
skip,
|
||||
@@ -55,15 +50,14 @@ export const usePreviewRecord = ({
|
||||
return firstRecord;
|
||||
}
|
||||
|
||||
const fieldPreviewValue =
|
||||
labelIdentifierFieldMetadataItem.type === FieldMetadataType.TEXT
|
||||
? objectMetadataItem.labelSingular
|
||||
: getFieldPreviewValue({
|
||||
fieldMetadataItem: labelIdentifierFieldMetadataItem,
|
||||
});
|
||||
const fieldPreviewValue = getFieldPreviewValue({
|
||||
fieldType: labelIdentifierFieldMetadataItem.type,
|
||||
fieldSettings: labelIdentifierFieldMetadataItem.settings,
|
||||
defaultValue: labelIdentifierFieldMetadataItem.defaultValue,
|
||||
});
|
||||
|
||||
const placeholderRecord = {
|
||||
__typename: pascalCase(objectMetadataItem.nameSingular),
|
||||
__typename: pascalCase(objectNameSingular),
|
||||
id: '',
|
||||
[labelIdentifierFieldMetadataItem.name]: fieldPreviewValue,
|
||||
};
|
||||
|
||||
+3
-10
@@ -1,22 +1,15 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord';
|
||||
|
||||
type UseRelationFieldPreviewParams = {
|
||||
relationObjectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
| 'fields'
|
||||
| 'labelIdentifierFieldMetadataId'
|
||||
| 'labelSingular'
|
||||
| 'nameSingular'
|
||||
>;
|
||||
relationObjectNameSingular: string;
|
||||
skip?: boolean;
|
||||
};
|
||||
|
||||
export const useRelationFieldPreviewValue = ({
|
||||
relationObjectMetadataItem,
|
||||
relationObjectNameSingular,
|
||||
skip,
|
||||
}: UseRelationFieldPreviewParams) =>
|
||||
usePreviewRecord({
|
||||
objectMetadataItem: relationObjectMetadataItem,
|
||||
objectNameSingular: relationObjectNameSingular,
|
||||
skip,
|
||||
});
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
export const settingsPreviewRecordIdState = createState<string | null>({
|
||||
key: 'settingsPreviewRecordIdState',
|
||||
defaultValue: null,
|
||||
});
|
||||
+10
-4
@@ -25,11 +25,13 @@ describe('getFieldPreviewValue', () => {
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
fieldSettings: fieldMetadataItem.settings,
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(true);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns a placeholder defaultValue if the field metadata does not have a defaultValue', () => {
|
||||
@@ -45,7 +47,9 @@ describe('getFieldPreviewValue', () => {
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
fieldSettings: fieldMetadataItem.settings,
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -68,7 +72,9 @@ describe('getFieldPreviewValue', () => {
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
fieldSettings: fieldMetadataItem.settings,
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
});
|
||||
|
||||
// Then
|
||||
|
||||
+17
-10
@@ -1,34 +1,41 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty';
|
||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
|
||||
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { stripSimpleQuotesFromStringRecursive } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
type getFieldPreviewValueArgs = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'defaultValue'>;
|
||||
fieldType: FieldMetadataType;
|
||||
fieldSettings: FieldMetadata['settings'];
|
||||
defaultValue: unknown;
|
||||
};
|
||||
export const getFieldPreviewValue = ({
|
||||
fieldMetadataItem,
|
||||
fieldType,
|
||||
fieldSettings,
|
||||
defaultValue,
|
||||
}: getFieldPreviewValueArgs) => {
|
||||
if (!isFieldTypeSupportedInSettings(fieldMetadataItem.type)) return null;
|
||||
if (!isFieldTypeSupportedInSettings(fieldType)) return null;
|
||||
|
||||
if (
|
||||
!isFieldValueEmpty({
|
||||
fieldDefinition: { type: fieldMetadataItem.type },
|
||||
fieldValue: stripSimpleQuotesFromStringRecursive(
|
||||
fieldMetadataItem.defaultValue,
|
||||
),
|
||||
fieldDefinition: { type: fieldType },
|
||||
fieldValue: stripSimpleQuotesFromStringRecursive(defaultValue),
|
||||
})
|
||||
) {
|
||||
return generateEmptyFieldValue({
|
||||
fieldMetadataItem,
|
||||
fieldMetadataItem: {
|
||||
type: fieldType,
|
||||
settings: fieldSettings,
|
||||
defaultValue,
|
||||
},
|
||||
shouldComputeFunctionDefaultValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
const fieldTypeConfig = getSettingsFieldTypeConfig(fieldMetadataItem.type);
|
||||
const fieldTypeConfig = getSettingsFieldTypeConfig(fieldType);
|
||||
|
||||
if (
|
||||
isDefined(fieldTypeConfig) &&
|
||||
|
||||
+1
@@ -138,6 +138,7 @@ export const SettingsDataModelObjectPreview = ({
|
||||
selected++;
|
||||
return selected <= 3 ? (
|
||||
<SettingsDataModelObjectPreviewItem
|
||||
key={`${objectMetadataItem.labelSingular}-${index}`}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
pluralizeLabel={pluralizeLabel}
|
||||
index={index}
|
||||
|
||||
+3
-7
@@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { SettingsDataModelCardTitle } from '@/settings/data-model/components/SettingsDataModelCardTitle';
|
||||
import { SettingsDataModelFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { SettingsDataModelObjectPreview } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary';
|
||||
import { SettingsDataModelObjectIdentifiersForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
@@ -14,10 +14,6 @@ type SettingsDataModelObjectSettingsFormCardProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTopCardContent = styled(CardContent)`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
`;
|
||||
@@ -50,8 +46,8 @@ export const SettingsDataModelObjectSettingsFormCard = ({
|
||||
<Trans>Preview</Trans>
|
||||
</SettingsDataModelCardTitle>
|
||||
{labelIdentifierFieldMetadataItem ? (
|
||||
<StyledFieldPreviewCard
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
fieldMetadataItem={labelIdentifierFieldMetadataItem}
|
||||
withFieldLabel={false}
|
||||
/>
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export const getFieldPreviewValueFromRecord = ({
|
||||
}) => {
|
||||
const recordFieldValue = record[fieldMetadataItem.name];
|
||||
|
||||
// todo @guillim : adjust this to handle morph relations
|
||||
// Relation fields (to many)
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
|
||||
@@ -37,7 +37,7 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
//TODO: fix this type
|
||||
type SettingsDataModelFieldEditFormValues = z.infer<
|
||||
export type SettingsDataModelFieldEditFormValues = z.infer<
|
||||
ReturnType<typeof settingsFieldFormSchema>
|
||||
> &
|
||||
any;
|
||||
@@ -91,6 +91,7 @@ export const SettingsObjectFieldEdit = () => {
|
||||
}, [navigateApp, objectMetadataItem, fieldMetadataItem]);
|
||||
|
||||
const { isDirty, isValid, isSubmitting } = formConfig.formState;
|
||||
|
||||
const canSave = isDirty && isValid && !isSubmitting;
|
||||
|
||||
if (!isDefined(objectMetadataItem) || !isDefined(fieldMetadataItem)) {
|
||||
@@ -236,8 +237,9 @@ export const SettingsObjectFieldEdit = () => {
|
||||
/>
|
||||
)}
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldType={fieldMetadataItem.type}
|
||||
existingFieldMetadataId={fieldMetadataItem.id}
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
|
||||
+4
-9
@@ -10,7 +10,6 @@ import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fie
|
||||
import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm';
|
||||
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
|
||||
import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@@ -48,7 +47,7 @@ export const SettingsObjectNewFieldConfigure = () => {
|
||||
const { objectNamePlural = '' } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const fieldType =
|
||||
(searchParams.get('fieldType') as SettingsFieldType) ||
|
||||
(searchParams.get('fieldType') as FieldMetadataType) ||
|
||||
FieldMetadataType.TEXT;
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
@@ -240,13 +239,9 @@ export const SettingsObjectNewFieldConfigure = () => {
|
||||
description={t`Customize field settings`}
|
||||
/>
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
fieldMetadataItem={{
|
||||
icon: formConfig.watch('icon'),
|
||||
label: formConfig.watch('label') || 'New Field',
|
||||
settings: formConfig.watch('settings') || null,
|
||||
type: fieldType as FieldMetadataType,
|
||||
}}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
fieldType={fieldType}
|
||||
existingFieldMetadataId={''}
|
||||
objectNameSingular={activeObjectMetadataItem.nameSingular}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
|
||||
@@ -2,19 +2,25 @@ import { type ReactNode, useEffect, useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
export const JestObjectMetadataItemSetter = ({
|
||||
children,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
objectMetadataItems?: ObjectMetadataItem[];
|
||||
}) => {
|
||||
const setObjectMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setObjectMetadataItems(generatedMockObjectMetadataItems);
|
||||
setObjectMetadataItems(
|
||||
objectMetadataItems ?? generatedMockObjectMetadataItems,
|
||||
);
|
||||
setIsLoaded(true);
|
||||
}, [setObjectMetadataItems]);
|
||||
}, [objectMetadataItems, setObjectMetadataItems]);
|
||||
|
||||
return isLoaded ? <>{children}</> : null;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RecoilRoot, type MutableSnapshot } from 'recoil';
|
||||
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
@@ -15,12 +16,14 @@ export const getJestMetadataAndApolloMocksWrapper = ({
|
||||
apolloMocks,
|
||||
cache,
|
||||
onInitializeRecoilSnapshot,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
cache?: InMemoryCache;
|
||||
apolloMocks?:
|
||||
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
|
||||
| undefined;
|
||||
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
|
||||
objectMetadataItems?: ObjectMetadataItem[];
|
||||
}) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot initializeState={onInitializeRecoilSnapshot}>
|
||||
@@ -34,7 +37,9 @@ export const getJestMetadataAndApolloMocksWrapper = ({
|
||||
<ViewComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'instanceId' }}
|
||||
>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<JestObjectMetadataItemSetter
|
||||
objectMetadataItems={objectMetadataItems}
|
||||
>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'instanceId' }}
|
||||
>
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { capitalize } from '@/utils/strings';
|
||||
|
||||
type ComputeMorphRelationFieldJoinColumnNameArgs = {
|
||||
name: string;
|
||||
targetObjectMetadataNameSingular: string;
|
||||
};
|
||||
|
||||
export const computeMorphRelationFieldJoinColumnName = ({
|
||||
name,
|
||||
targetObjectMetadataNameSingular,
|
||||
}: ComputeMorphRelationFieldJoinColumnNameArgs) => {
|
||||
return `${name}${capitalize(targetObjectMetadataNameSingular)}Id`;
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
|
||||
import { capitalize } from '@/utils/strings';
|
||||
|
||||
enum RelationType {
|
||||
MANY_TO_ONE = 'MANY_TO_ONE',
|
||||
ONE_TO_MANY = 'ONE_TO_MANY'
|
||||
}
|
||||
|
||||
|
||||
type ComputeMorphRelationFieldNameArgs = {
|
||||
fieldName: string;
|
||||
relationDirection: RelationType;
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
};
|
||||
|
||||
export const computeMorphRelationFieldName = ({
|
||||
fieldName,
|
||||
relationDirection,
|
||||
nameSingular,
|
||||
namePlural,
|
||||
}: ComputeMorphRelationFieldNameArgs): string => {
|
||||
if (relationDirection === RelationType.MANY_TO_ONE) {
|
||||
return `${fieldName}${capitalize(nameSingular)}`;
|
||||
}
|
||||
|
||||
if (relationDirection === RelationType.ONE_TO_MANY) {
|
||||
return `${fieldName}${capitalize(namePlural)}`;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invalid relation direction: ${relationDirection} for field ${fieldName}`,
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,8 @@ export { findByProperty } from './array/findByProperty';
|
||||
export { assertUnreachable } from './assertUnreachable';
|
||||
export { deepMerge } from './deepMerge';
|
||||
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
|
||||
export { computeMorphRelationFieldJoinColumnName } from './fieldMetadata/compute-morph-relation-field-join-column-name.util';
|
||||
export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name.util';
|
||||
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
||||
export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util';
|
||||
export { fromArrayToValuesByKeyRecord } from './fromArrayToValuesByKeyRecord.util';
|
||||
|
||||
Reference in New Issue
Block a user