[REFACTOR][BUG] Dynamically compute field to write in cache CREATE (#10130)
# Introduction While importing records encountering missing expected fields when writting a fragment from apollo cache ## Updates ### 1/ `createdBy` Default value When inserting in cache in create single or many we will now make optimistic behavior on the createdBy value ### 2/ `createRecordInCache` dynamically create `recordGrqlFields` When creating an entry in cache, we will now dynamically generate fields to be written in the fragment instead of expecting all of them. As by nature record could be partial ### 3/ Strictly typed `RecordGqlFields` # Conclusion closes #9927
This commit is contained in:
@ -95,7 +95,7 @@ idealCustomerProfile
|
||||
it('should return only return relation subFields that are in recordGqlFields', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
relationrecordFields: {
|
||||
relationRecordGqlFields: {
|
||||
accountOwner: { id: true, name: true },
|
||||
people: true,
|
||||
xLink: true,
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
|
||||
export const checkObjectMetadataItemHasFieldCreatedBy = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
) =>
|
||||
objectMetadataItem.fields.some(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.ACTOR && field.name === 'createdBy',
|
||||
);
|
||||
@ -1,26 +1,27 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
type MapFieldMetadataToGraphQLQueryArgs = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
field: Pick<FieldMetadataItem, 'name' | 'type' | 'relationDefinition'>;
|
||||
relationRecordGqlFields?: RecordGqlFields;
|
||||
computeReferences?: boolean;
|
||||
};
|
||||
// TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field
|
||||
export const mapFieldMetadataToGraphQLQuery = ({
|
||||
objectMetadataItems,
|
||||
field,
|
||||
relationrecordFields,
|
||||
relationRecordGqlFields,
|
||||
computeReferences = false,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
field: Pick<FieldMetadataItem, 'name' | 'type' | 'relationDefinition'>;
|
||||
relationrecordFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
}): any => {
|
||||
}: MapFieldMetadataToGraphQLQueryArgs): string => {
|
||||
const fieldType = field.type;
|
||||
|
||||
const fieldIsSimpleValue = [
|
||||
@ -61,7 +62,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
recordGqlFields: relationrecordFields,
|
||||
recordGqlFields: relationRecordGqlFields,
|
||||
computeReferences: computeReferences,
|
||||
isRootLevel: false,
|
||||
})}`;
|
||||
@ -87,7 +88,7 @@ ${mapObjectMetadataToGraphQLQuery({
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
recordGqlFields: relationrecordFields,
|
||||
recordGqlFields: relationRecordGqlFields,
|
||||
computeReferences,
|
||||
isRootLevel: false,
|
||||
})}
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
|
||||
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
|
||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
|
||||
|
||||
type MapObjectMetadataToGraphQLQueryArgs = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>;
|
||||
recordGqlFields?: RecordGqlFields;
|
||||
computeReferences?: boolean;
|
||||
isRootLevel?: boolean;
|
||||
};
|
||||
export const mapObjectMetadataToGraphQLQuery = ({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
recordGqlFields,
|
||||
computeReferences = false,
|
||||
isRootLevel = true,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>;
|
||||
recordGqlFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
isRootLevel?: boolean;
|
||||
}): any => {
|
||||
}: MapObjectMetadataToGraphQLQueryArgs): string => {
|
||||
const fieldsThatShouldBeQueried =
|
||||
objectMetadataItem?.fields
|
||||
.filter((field) => field.isActive)
|
||||
@ -36,13 +39,16 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
__typename
|
||||
${fieldsThatShouldBeQueried
|
||||
.map((field) => {
|
||||
const currentRecordGqlFields = recordGqlFields?.[field.name];
|
||||
const relationRecordGqlFields = isRecordGqlFieldsNode(
|
||||
currentRecordGqlFields,
|
||||
)
|
||||
? currentRecordGqlFields
|
||||
: undefined;
|
||||
return mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
field,
|
||||
relationrecordFields:
|
||||
typeof recordGqlFields?.[field.name] === 'boolean'
|
||||
? undefined
|
||||
: recordGqlFields?.[field.name],
|
||||
relationRecordGqlFields,
|
||||
computeReferences,
|
||||
});
|
||||
})
|
||||
|
||||
@ -24,6 +24,10 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
return (record: ObjectRecord) => {
|
||||
const recordGqlFields = generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
});
|
||||
const fragment = gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
@ -33,9 +37,7 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
computeReferences: true,
|
||||
recordGqlFields: generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
}),
|
||||
recordGqlFields,
|
||||
})}
|
||||
`;
|
||||
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export type RecordGqlFields = Record<string, any>;
|
||||
export type RecordGqlFields = {
|
||||
[k: string]: boolean | RecordGqlFields | undefined;
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export type RecordGqlFieldsDeprecated = Record<string, any>;
|
||||
@ -1,3 +1,3 @@
|
||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { RecordGqlFieldsDeprecated } from '@/object-record/graphql/types/RecordGqlFieldsDeprecated';
|
||||
|
||||
export type RecordGqlOperationGqlRecordFields = RecordGqlFields;
|
||||
export type RecordGqlOperationGqlRecordFields = RecordGqlFieldsDeprecated;
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import {
|
||||
getPersonObjectMetadataItem,
|
||||
getPersonRecord,
|
||||
} from '~/testing/mock-data/people';
|
||||
|
||||
describe('generateDepthOneRecordGqlFields', () => {
|
||||
const objectMetadataItem = getPersonObjectMetadataItem();
|
||||
it('Should handle basic call with both objectMetadataItem and record', () => {
|
||||
const personRecord = getPersonRecord();
|
||||
const result = generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
record: personRecord,
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"attachments": false,
|
||||
"avatarUrl": false,
|
||||
"calendarEventParticipants": false,
|
||||
"city": true,
|
||||
"company": true,
|
||||
"companyId": false,
|
||||
"createdAt": true,
|
||||
"createdBy": true,
|
||||
"deletedAt": true,
|
||||
"emails": false,
|
||||
"favorites": false,
|
||||
"id": true,
|
||||
"intro": false,
|
||||
"jobTitle": true,
|
||||
"linkedinLink": true,
|
||||
"messageParticipants": false,
|
||||
"name": true,
|
||||
"noteTargets": true,
|
||||
"performanceRating": false,
|
||||
"phones": true,
|
||||
"pointOfContactForOpportunities": false,
|
||||
"position": true,
|
||||
"searchVector": false,
|
||||
"taskTargets": true,
|
||||
"timelineActivities": false,
|
||||
"updatedAt": false,
|
||||
"whatsapp": false,
|
||||
"workPreference": false,
|
||||
"xLink": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('Should handle basic call with standalone objectMetadataItem', () => {
|
||||
const result = generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"attachments": true,
|
||||
"avatarUrl": true,
|
||||
"calendarEventParticipants": true,
|
||||
"city": true,
|
||||
"company": true,
|
||||
"companyId": true,
|
||||
"createdAt": true,
|
||||
"createdBy": true,
|
||||
"deletedAt": true,
|
||||
"emails": true,
|
||||
"favorites": true,
|
||||
"id": true,
|
||||
"intro": true,
|
||||
"jobTitle": true,
|
||||
"linkedinLink": true,
|
||||
"messageParticipants": true,
|
||||
"name": true,
|
||||
"noteTargets": true,
|
||||
"performanceRating": true,
|
||||
"phones": true,
|
||||
"pointOfContactForOpportunities": true,
|
||||
"position": true,
|
||||
"searchVector": true,
|
||||
"taskTargets": true,
|
||||
"timelineActivities": true,
|
||||
"updatedAt": true,
|
||||
"whatsapp": true,
|
||||
"workPreference": true,
|
||||
"xLink": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const isRecordGqlFieldsNode = (
|
||||
recordGql: RecordGqlFields | boolean | undefined,
|
||||
): recordGql is RecordGqlFields =>
|
||||
isDefined(recordGql) &&
|
||||
typeof recordGql === 'object' &&
|
||||
recordGql !== null &&
|
||||
!Array.isArray(recordGql);
|
||||
@ -3,8 +3,10 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { checkObjectMetadataItemHasFieldCreatedBy } from '@/object-metadata/utils/checkObjectMetadataItemHasFieldCreatedBy';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
@ -13,10 +15,12 @@ import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
type PartialObjectRecordWithId = Partial<ObjectRecord> & {
|
||||
@ -44,6 +48,9 @@ export const useCreateManyRecords = <
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectMetadataHasCreatedByField =
|
||||
checkObjectMetadataItemHasFieldCreatedBy(objectMetadataItem);
|
||||
|
||||
const computedRecordGqlFields =
|
||||
recordGqlFields ?? generateDepthOneRecordGqlFields({ objectMetadataItem });
|
||||
|
||||
@ -56,6 +63,8 @@ export const useCreateManyRecords = <
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { refetchAggregateQueries } = useRefetchAggregateQueries({
|
||||
@ -77,12 +86,26 @@ export const useCreateManyRecords = <
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
const baseOptimisticRecordInputCreatedBy:
|
||||
| { createdBy: FieldActorForInputValue }
|
||||
| undefined = objectMetadataHasCreatedByField
|
||||
? {
|
||||
createdBy: {
|
||||
source: 'MANUAL',
|
||||
context: {},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const optimisticRecordInput = {
|
||||
...computeOptimisticRecordFromInput({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordInput: recordToCreate,
|
||||
currentWorkspaceMember: currentWorkspaceMember,
|
||||
recordInput: {
|
||||
...baseOptimisticRecordInputCreatedBy,
|
||||
...recordToCreate,
|
||||
},
|
||||
}),
|
||||
id: idForCreation,
|
||||
};
|
||||
|
||||
@ -4,8 +4,10 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { checkObjectMetadataItemHasFieldCreatedBy } from '@/object-metadata/utils/checkObjectMetadataItemHasFieldCreatedBy';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
@ -14,10 +16,12 @@ import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
type useCreateOneRecordProps = {
|
||||
@ -42,6 +46,9 @@ export const useCreateOneRecord = <
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectMetadataHasCreatedByField =
|
||||
checkObjectMetadataItemHasFieldCreatedBy(objectMetadataItem);
|
||||
|
||||
const computedRecordGqlFields =
|
||||
recordGqlFields ?? generateDepthOneRecordGqlFields({ objectMetadataItem });
|
||||
|
||||
@ -50,6 +57,8 @@ export const useCreateOneRecord = <
|
||||
recordGqlFields: computedRecordGqlFields,
|
||||
});
|
||||
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const createOneRecordInCache = useCreateOneRecordInCache<CreatedObjectRecord>(
|
||||
{
|
||||
objectMetadataItem,
|
||||
@ -75,11 +84,26 @@ export const useCreateOneRecord = <
|
||||
id: idForCreation,
|
||||
};
|
||||
|
||||
const baseOptimisticRecordInputCreatedBy:
|
||||
| { createdBy: FieldActorForInputValue }
|
||||
| undefined = objectMetadataHasCreatedByField
|
||||
? {
|
||||
createdBy: {
|
||||
source: 'MANUAL',
|
||||
context: {},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const optimisticRecordInput = computeOptimisticRecordFromInput({
|
||||
cache: apolloClient.cache,
|
||||
currentWorkspaceMember: currentWorkspaceMember,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordInput: { ...recordInput, id: idForCreation },
|
||||
recordInput: {
|
||||
...baseOptimisticRecordInputCreatedBy,
|
||||
...recordInput,
|
||||
id: idForCreation,
|
||||
},
|
||||
});
|
||||
const recordCreatedInCache = createOneRecordInCache({
|
||||
...optimisticRecordInput,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
@ -15,6 +16,7 @@ import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeO
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
|
||||
|
||||
@ -22,7 +24,11 @@ type useUpdateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
recordGqlFields?: Record<string, any>;
|
||||
};
|
||||
|
||||
type UpdateOneRecordArgs<UpdatedObjectRecord> = {
|
||||
idToUpdate: string;
|
||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
optimisticRecord?: Partial<ObjectRecord>;
|
||||
};
|
||||
export const useUpdateOneRecord = <
|
||||
UpdatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
@ -47,6 +53,8 @@ export const useUpdateOneRecord = <
|
||||
recordGqlFields: computedRecordGqlFields,
|
||||
});
|
||||
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { refetchAggregateQueries } = useRefetchAggregateQueries({
|
||||
@ -57,15 +65,12 @@ export const useUpdateOneRecord = <
|
||||
idToUpdate,
|
||||
updateOneRecordInput,
|
||||
optimisticRecord,
|
||||
}: {
|
||||
idToUpdate: string;
|
||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
optimisticRecord?: Partial<ObjectRecord>;
|
||||
}) => {
|
||||
}: UpdateOneRecordArgs<UpdatedObjectRecord>) => {
|
||||
const optimisticRecordInput =
|
||||
optimisticRecord ??
|
||||
computeOptimisticRecordFromInput({
|
||||
objectMetadataItem,
|
||||
currentWorkspaceMember: currentWorkspaceMember,
|
||||
recordInput: updateOneRecordInput,
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItems,
|
||||
|
||||
@ -212,7 +212,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {},
|
||||
},
|
||||
{
|
||||
@ -220,7 +220,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
fields: {
|
||||
id: true,
|
||||
name: true,
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {},
|
||||
},
|
||||
],
|
||||
@ -283,7 +283,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
cursorFilter: {
|
||||
@ -349,7 +349,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
cursorFilter: {
|
||||
@ -415,7 +415,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
},
|
||||
@ -495,7 +495,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
cursorFilter: {
|
||||
@ -509,7 +509,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
fields: {
|
||||
id: true,
|
||||
name: true,
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
},
|
||||
@ -558,7 +558,7 @@ describe('useCombinedFindManyRecords', () => {
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {},
|
||||
},
|
||||
],
|
||||
|
||||
@ -13,7 +13,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
} satisfies RecordGqlFields,
|
||||
variables: {
|
||||
filter: { id: { eq: '123' } },
|
||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
||||
|
||||
@ -42,7 +42,9 @@ export const useClearField = () => {
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const emptyFieldValue = generateEmptyFieldValue(foundFieldMetadataItem);
|
||||
const emptyFieldValue = generateEmptyFieldValue({
|
||||
fieldMetadataItem: foundFieldMetadataItem,
|
||||
});
|
||||
|
||||
set(
|
||||
recordStoreFamilySelector({ recordId, fieldName }),
|
||||
|
||||
@ -279,15 +279,29 @@ export type FieldRichTextV2Value = {
|
||||
|
||||
export type FieldRichTextValue = null | string;
|
||||
|
||||
type FieldActorSource =
|
||||
| 'API'
|
||||
| 'IMPORT'
|
||||
| 'EMAIL'
|
||||
| 'CALENDAR'
|
||||
| 'MANUAL'
|
||||
| 'SYSTEM'
|
||||
| 'WORKFLOW';
|
||||
|
||||
export type FieldActorValue = {
|
||||
source: string;
|
||||
workspaceMemberId?: string;
|
||||
source: FieldActorSource;
|
||||
workspaceMemberId: string | null;
|
||||
name: string;
|
||||
context?: {
|
||||
context: {
|
||||
provider?: ConnectedAccountProvider;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type FieldActorForInputValue = Pick<
|
||||
FieldActorValue,
|
||||
'context' | 'source'
|
||||
>;
|
||||
|
||||
export type FieldArrayValue = string[];
|
||||
|
||||
export type PhoneRecord = {
|
||||
|
||||
@ -26,7 +26,9 @@ export const RightDrawerTitleRecordInlineCell = () => {
|
||||
const draftValue = useRecoilValue(getDraftValueSelector());
|
||||
|
||||
useListenRightDrawerClose(() => {
|
||||
persistField(draftValue);
|
||||
if (draftValue !== undefined) {
|
||||
persistField(draftValue);
|
||||
}
|
||||
closeInlineCell();
|
||||
});
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spread
|
||||
|
||||
import { useOpenObjectRecordsSpreadsheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog';
|
||||
|
||||
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
|
||||
const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a';
|
||||
@ -294,7 +295,10 @@ const companyMocks = [
|
||||
variables: {
|
||||
data: [
|
||||
{
|
||||
createdBy: { source: 'IMPORT' },
|
||||
createdBy: {
|
||||
source: 'IMPORT',
|
||||
context: {},
|
||||
} satisfies FieldActorForInputValue,
|
||||
employees: 0,
|
||||
idealCustomerProfile: true,
|
||||
name: 'Example Company',
|
||||
|
||||
@ -56,16 +56,17 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||
onSubmit: async (data) => {
|
||||
const createInputs = data.validStructuredRows.map((record) => {
|
||||
const fieldMapping: Record<string, any> =
|
||||
buildRecordFromImportedStructuredRow(
|
||||
record,
|
||||
availableFieldMetadataItems,
|
||||
);
|
||||
buildRecordFromImportedStructuredRow({
|
||||
importedStructuredRow: record,
|
||||
fields: availableFieldMetadataItems,
|
||||
});
|
||||
|
||||
return fieldMapping;
|
||||
});
|
||||
|
||||
try {
|
||||
await createManyRecords(createInputs, true);
|
||||
const upsert = true;
|
||||
await createManyRecords(createInputs, upsert);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message || 'Something went wrong', {
|
||||
variant: SnackBarVariant.Error,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import {
|
||||
FieldActorForInputValue,
|
||||
FieldAddressValue,
|
||||
FieldEmailsValue,
|
||||
FieldLinksValue,
|
||||
@ -15,10 +16,14 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { castToString } from '~/utils/castToString';
|
||||
import { convertCurrencyAmountToCurrencyMicros } from '~/utils/convertCurrencyToCurrencyMicros';
|
||||
|
||||
export const buildRecordFromImportedStructuredRow = (
|
||||
importedStructuredRow: ImportedStructuredRow<any>,
|
||||
fields: FieldMetadataItem[],
|
||||
) => {
|
||||
type BuildRecordFromImportedStructuredRowArgs = {
|
||||
importedStructuredRow: ImportedStructuredRow<any>;
|
||||
fields: FieldMetadataItem[];
|
||||
};
|
||||
export const buildRecordFromImportedStructuredRow = ({
|
||||
fields,
|
||||
importedStructuredRow,
|
||||
}: BuildRecordFromImportedStructuredRowArgs) => {
|
||||
const recordToBuild: Record<string, any> = {};
|
||||
|
||||
const {
|
||||
@ -219,7 +224,8 @@ export const buildRecordFromImportedStructuredRow = (
|
||||
case FieldMetadataType.ACTOR:
|
||||
recordToBuild[field.name] = {
|
||||
source: 'IMPORT',
|
||||
};
|
||||
context: {},
|
||||
} satisfies FieldActorForInputValue;
|
||||
break;
|
||||
case FieldMetadataType.ARRAY:
|
||||
case FieldMetadataType.MULTI_SELECT: {
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { FieldActorForInputValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
import { getCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { getPersonObjectMetadataItem } from '~/testing/mock-data/people';
|
||||
import { mockCurrentWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
describe('computeOptimisticRecordFromInput', () => {
|
||||
const currentWorkspaceMember = mockCurrentWorkspaceMembers[0];
|
||||
const currentWorkspaceMemberFullname = `${currentWorkspaceMember.name.firstName} ${currentWorkspaceMember.name.lastName}`;
|
||||
it('should generate correct optimistic record if no relation field is present', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -26,11 +31,69 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate correct optimistic record with actor field', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
const actorFieldValueForInput: FieldActorForInputValue = {
|
||||
context: {},
|
||||
source: 'API',
|
||||
};
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
city: 'Paris',
|
||||
createdBy: actorFieldValueForInput,
|
||||
},
|
||||
cache,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
city: 'Paris',
|
||||
createdBy: {
|
||||
context: {},
|
||||
name: currentWorkspaceMemberFullname,
|
||||
source: 'API',
|
||||
workspaceMemberId: currentWorkspaceMember.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate correct optimistic record createdBy when recordInput contains id', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
id: '20202020-058c-4591-a7d7-50a75af6d1e6',
|
||||
createdBy: {
|
||||
source: 'SYSTEM',
|
||||
context: {},
|
||||
} satisfies FieldActorForInputValue,
|
||||
},
|
||||
cache,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '20202020-058c-4591-a7d7-50a75af6d1e6',
|
||||
createdBy: {
|
||||
context: {},
|
||||
name: currentWorkspaceMemberFullname,
|
||||
source: 'SYSTEM',
|
||||
workspaceMemberId: currentWorkspaceMember.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate correct optimistic record if relation field is present but cache is empty', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -73,6 +136,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
});
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -117,6 +181,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
});
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -136,6 +201,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
const personObjectMetadataItem = getPersonObjectMetadataItem();
|
||||
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -156,6 +222,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
|
||||
expect(() =>
|
||||
computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -167,7 +234,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
cache,
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadaItem person"`,
|
||||
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadataItem person"`,
|
||||
);
|
||||
});
|
||||
|
||||
@ -177,6 +244,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
|
||||
expect(() =>
|
||||
computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
@ -196,6 +264,7 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
|
||||
expect(() =>
|
||||
computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const buildOptimisticActorFieldValueFromCurrentWorkspaceMember = (
|
||||
currentWorkspaceMember: CurrentWorkspaceMember | null,
|
||||
): FieldActorValue => {
|
||||
const defaultActorFieldValue: FieldActorValue = {
|
||||
context: {},
|
||||
name: '',
|
||||
source: 'MANUAL',
|
||||
workspaceMemberId: null,
|
||||
};
|
||||
|
||||
if (!isDefined(currentWorkspaceMember)) {
|
||||
return defaultActorFieldValue;
|
||||
}
|
||||
|
||||
const {
|
||||
id: workspaceMemberId,
|
||||
name: { firstName, lastName },
|
||||
} = currentWorkspaceMember;
|
||||
const name = `${firstName} ${lastName}`;
|
||||
return {
|
||||
...defaultActorFieldValue,
|
||||
name: name,
|
||||
workspaceMemberId,
|
||||
};
|
||||
};
|
||||
@ -1,14 +1,18 @@
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
getRecordFromCache,
|
||||
GetRecordFromCacheArgs,
|
||||
} from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { GRAPHQL_TYPENAME_KEY } from '@/object-record/constants/GraphqlTypenameKey';
|
||||
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
@ -17,12 +21,14 @@ import { FieldMetadataType } from '~/generated/graphql';
|
||||
type ComputeOptimisticCacheRecordInputArgs = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordInput: Partial<ObjectRecord>;
|
||||
currentWorkspaceMember: CurrentWorkspaceMember | null;
|
||||
} & Pick<GetRecordFromCacheArgs, 'cache' | 'objectMetadataItems'>;
|
||||
export const computeOptimisticRecordFromInput = ({
|
||||
objectMetadataItem,
|
||||
recordInput,
|
||||
cache,
|
||||
objectMetadataItems,
|
||||
currentWorkspaceMember,
|
||||
}: ComputeOptimisticCacheRecordInputArgs) => {
|
||||
const unknownRecordInputFields = Object.keys(recordInput).filter(
|
||||
(recordKey) => {
|
||||
@ -35,12 +41,14 @@ export const computeOptimisticRecordFromInput = ({
|
||||
);
|
||||
if (unknownRecordInputFields.length > 0) {
|
||||
throw new Error(
|
||||
`Should never occur, encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadaItem ${objectMetadataItem.nameSingular}`,
|
||||
`Should never occur, encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadataItem ${objectMetadataItem.nameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const optimisticRecord: Partial<ObjectRecord> = {};
|
||||
for (const fieldMetadataItem of objectMetadataItem.fields) {
|
||||
const recordInputFieldValue: unknown = recordInput[fieldMetadataItem.name];
|
||||
|
||||
if (isFieldUuid(fieldMetadataItem)) {
|
||||
const isRelationFieldId = objectMetadataItem.fields.some(
|
||||
({ type, relationDefinition }) => {
|
||||
@ -65,10 +73,19 @@ export const computeOptimisticRecordFromInput = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (isFieldActor(fieldMetadataItem) && isDefined(recordInputFieldValue)) {
|
||||
const defaultActorFieldValue =
|
||||
buildOptimisticActorFieldValueFromCurrentWorkspaceMember(
|
||||
currentWorkspaceMember,
|
||||
);
|
||||
optimisticRecord[fieldMetadataItem.name] = {
|
||||
...defaultActorFieldValue,
|
||||
...(recordInputFieldValue as FieldActorValue),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const isRelationField = isFieldRelation(fieldMetadataItem);
|
||||
|
||||
const recordInputFieldValue: unknown = recordInput[fieldMetadataItem.name];
|
||||
|
||||
if (!isRelationField) {
|
||||
if (!isDefined(recordInputFieldValue)) {
|
||||
continue;
|
||||
|
||||
@ -4,14 +4,19 @@ import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFiel
|
||||
import { v4 } from 'uuid';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
export const generateDefaultFieldValue = (
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue' | 'type'>,
|
||||
) => {
|
||||
type GenerateEmptyFieldValueArgs = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue' | 'type'>;
|
||||
};
|
||||
export const generateDefaultFieldValue = ({
|
||||
fieldMetadataItem,
|
||||
}: GenerateEmptyFieldValueArgs) => {
|
||||
const defaultValue = isFieldValueEmpty({
|
||||
fieldValue: fieldMetadataItem.defaultValue,
|
||||
fieldDefinition: fieldMetadataItem,
|
||||
})
|
||||
? generateEmptyFieldValue(fieldMetadataItem)
|
||||
? generateEmptyFieldValue({
|
||||
fieldMetadataItem,
|
||||
})
|
||||
: stripSimpleQuotesFromString(fieldMetadataItem.defaultValue);
|
||||
|
||||
switch (defaultValue) {
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const generateEmptyFieldValue = (
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>,
|
||||
) => {
|
||||
export type GenerateEmptyFieldValueArgs = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>;
|
||||
};
|
||||
// TODO strictly type each fieldValue following their FieldMetadataType
|
||||
export const generateEmptyFieldValue = ({
|
||||
fieldMetadataItem,
|
||||
}: GenerateEmptyFieldValueArgs) => {
|
||||
switch (fieldMetadataItem.type) {
|
||||
case FieldMetadataType.TEXT: {
|
||||
return '';
|
||||
@ -94,10 +99,10 @@ export const generateEmptyFieldValue = (
|
||||
case FieldMetadataType.ACTOR: {
|
||||
return {
|
||||
source: 'MANUAL',
|
||||
workspaceMemberId: null,
|
||||
name: '',
|
||||
context: {},
|
||||
};
|
||||
name: '',
|
||||
workspaceMemberId: null,
|
||||
} satisfies FieldActorValue;
|
||||
}
|
||||
case FieldMetadataType.PHONES: {
|
||||
return {
|
||||
|
||||
@ -7,13 +7,14 @@ import { generateDefaultFieldValue } from '@/object-record/utils/generateDefault
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { FieldMetadataType, RelationDefinitionType } from '~/generated/graphql';
|
||||
|
||||
type PrefillRecordArgs = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
input: Record<string, unknown>;
|
||||
};
|
||||
export const prefillRecord = <T extends ObjectRecord>({
|
||||
objectMetadataItem,
|
||||
input,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
input: Record<string, unknown>;
|
||||
}) => {
|
||||
}: PrefillRecordArgs) => {
|
||||
return Object.fromEntries(
|
||||
objectMetadataItem.fields
|
||||
.map((fieldMetadataItem) => {
|
||||
@ -26,12 +27,10 @@ export const prefillRecord = <T extends ObjectRecord>({
|
||||
throwIfInputRelationDataIsInconsistent(input, fieldMetadataItem);
|
||||
}
|
||||
|
||||
return [
|
||||
fieldMetadataItem.name,
|
||||
isUndefined(inputValue)
|
||||
? generateDefaultFieldValue(fieldMetadataItem)
|
||||
: inputValue,
|
||||
];
|
||||
const fieldValue = isUndefined(inputValue)
|
||||
? generateDefaultFieldValue({ fieldMetadataItem })
|
||||
: inputValue;
|
||||
return [fieldMetadataItem.name, fieldValue];
|
||||
})
|
||||
.filter(isDefined),
|
||||
) as T;
|
||||
|
||||
@ -23,11 +23,12 @@ export const usePrefetchedData = <T extends ObjectRecord>(
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const recordGqlFields =
|
||||
operationSignatureFactory({ objectMetadataItem }).fields ?? filter;
|
||||
const { records } = useFindManyRecords<T>({
|
||||
skip: !isDataPrefetched,
|
||||
objectNameSingular: objectNameSingular,
|
||||
recordGqlFields:
|
||||
operationSignatureFactory({ objectMetadataItem }).fields ?? filter,
|
||||
recordGqlFields,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -181,7 +181,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
||||
context: 'Context',
|
||||
},
|
||||
exampleValue: {
|
||||
source: 'source',
|
||||
source: 'IMPORT',
|
||||
name: 'name',
|
||||
workspaceMemberId: 'id',
|
||||
context: { provider: ConnectedAccountProvider.GOOGLE },
|
||||
|
||||
@ -53,6 +53,8 @@ export const useFieldPreviewValue = ({
|
||||
case FieldMetadataType.PHONES:
|
||||
return getPhonesFieldPreviewValue({ fieldMetadataItem });
|
||||
default:
|
||||
return getFieldPreviewValue({ fieldMetadataItem });
|
||||
return getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -24,7 +24,9 @@ describe('getFieldPreviewValue', () => {
|
||||
}
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValue({ fieldMetadataItem });
|
||||
const result = getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
@ -42,7 +44,9 @@ describe('getFieldPreviewValue', () => {
|
||||
}
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValue({ fieldMetadataItem });
|
||||
const result = getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(2000);
|
||||
@ -63,7 +67,9 @@ describe('getFieldPreviewValue', () => {
|
||||
}
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValue({ fieldMetadataItem });
|
||||
const result = getFieldPreviewValue({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBeNull();
|
||||
|
||||
@ -5,11 +5,12 @@ import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSetti
|
||||
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
type getFieldPreviewValueArgs = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'defaultValue'>;
|
||||
};
|
||||
export const getFieldPreviewValue = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'defaultValue'>;
|
||||
}) => {
|
||||
}: getFieldPreviewValueArgs) => {
|
||||
if (!isFieldTypeSupportedInSettings(fieldMetadataItem.type)) return null;
|
||||
|
||||
if (
|
||||
@ -18,7 +19,9 @@ export const getFieldPreviewValue = ({
|
||||
fieldValue: fieldMetadataItem.defaultValue,
|
||||
})
|
||||
) {
|
||||
return generateDefaultFieldValue(fieldMetadataItem);
|
||||
return generateDefaultFieldValue({
|
||||
fieldMetadataItem,
|
||||
});
|
||||
}
|
||||
|
||||
const fieldTypeConfig = getSettingsFieldTypeConfig(fieldMetadataItem.type);
|
||||
|
||||
@ -3,15 +3,16 @@ import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/g
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
type GenerateEmptyJestRecordNodeArgs = {
|
||||
objectNameSingular: string;
|
||||
input: Record<string, unknown>;
|
||||
withDepthOneRelation?: boolean;
|
||||
};
|
||||
export const generateEmptyJestRecordNode = ({
|
||||
objectNameSingular,
|
||||
input,
|
||||
withDepthOneRelation = false,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
input: Record<string, unknown>;
|
||||
withDepthOneRelation?: boolean;
|
||||
}) => {
|
||||
}: GenerateEmptyJestRecordNodeArgs) => {
|
||||
const objectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
@ -22,7 +23,10 @@ export const generateEmptyJestRecordNode = ({
|
||||
);
|
||||
}
|
||||
|
||||
const prefilledRecord = prefillRecord({ objectMetadataItem, input });
|
||||
const prefilledRecord = prefillRecord({
|
||||
objectMetadataItem,
|
||||
input,
|
||||
});
|
||||
|
||||
return getRecordNodeFromRecord({
|
||||
record: prefilledRecord,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
export const getPeopleMock = (): ObjectRecord[] => {
|
||||
@ -20,6 +21,22 @@ export const getPersonObjectMetadataItem = () => {
|
||||
return personObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const getPersonFieldMetadataItem = (
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
objectMetadataItem = getPersonObjectMetadataItem(),
|
||||
) => {
|
||||
const result = objectMetadataItem.fields.find(
|
||||
(field) => field.type === fieldMetadataType,
|
||||
);
|
||||
if (!result) {
|
||||
throw new Error(
|
||||
`Person fieldmetadata item type ${fieldMetadataType} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getPersonRecord = (
|
||||
overrides?: Partial<ObjectRecord>,
|
||||
index = 0,
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
export const mockWorkspaceMembers = [
|
||||
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export const mockWorkspaceMembers: WorkspaceMember[] = [
|
||||
{
|
||||
id: '20202020-1553-45c6-a028-5a9064cce07f',
|
||||
name: {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
__typename: 'WorkspaceMember',
|
||||
userEmail: 'jane.doe@twenty.com',
|
||||
locale: 'en',
|
||||
avatarUrl: '',
|
||||
createdAt: '2023-12-18T09:51:19.645Z',
|
||||
@ -18,6 +23,8 @@ export const mockWorkspaceMembers = [
|
||||
firstName: 'John',
|
||||
lastName: 'Wick',
|
||||
},
|
||||
userEmail: 'john.wick@twenty.com',
|
||||
__typename: 'WorkspaceMember',
|
||||
locale: 'en',
|
||||
avatarUrl: '',
|
||||
createdAt: '2023-12-18T09:51:19.645Z',
|
||||
@ -26,3 +33,26 @@ export const mockWorkspaceMembers = [
|
||||
colorScheme: 'Dark' as const,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCurrentWorkspaceMembers: CurrentWorkspaceMember[] =
|
||||
mockWorkspaceMembers.map(
|
||||
({
|
||||
id,
|
||||
locale,
|
||||
name,
|
||||
avatarUrl,
|
||||
colorScheme,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
timeZone,
|
||||
}) => ({
|
||||
id,
|
||||
locale,
|
||||
name,
|
||||
avatarUrl,
|
||||
colorScheme,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
timeZone,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user