diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts index 6a41f42f7..0522cea6c 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts @@ -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, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/checkObjectMetadataItemHasFieldCreatedBy.ts b/packages/twenty-front/src/modules/object-metadata/utils/checkObjectMetadataItemHasFieldCreatedBy.ts new file mode 100644 index 000000000..3d68f355b --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/checkObjectMetadataItemHasFieldCreatedBy.ts @@ -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', + ); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index efc393d1c..3428a712c 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -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; + 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; - relationrecordFields?: Record; - 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, })} diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index 9e705d428..faec027f0 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -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; + recordGqlFields?: RecordGqlFields; + computeReferences?: boolean; + isRootLevel?: boolean; +}; export const mapObjectMetadataToGraphQLQuery = ({ objectMetadataItems, objectMetadataItem, recordGqlFields, computeReferences = false, isRootLevel = true, -}: { - objectMetadataItems: ObjectMetadataItem[]; - objectMetadataItem: Pick; - recordGqlFields?: Record; - 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, }); }) diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts index c213cd7a0..aad93e842 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts @@ -24,6 +24,10 @@ export const useCreateOneRecordInCache = ({ 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 = ({ objectMetadataItems, objectMetadataItem, computeReferences: true, - recordGqlFields: generateDepthOneRecordGqlFields({ - objectMetadataItem, - }), + recordGqlFields, })} `; diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFields.ts index 1148458a2..5c2b33b16 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFields.ts @@ -1 +1,3 @@ -export type RecordGqlFields = Record; +export type RecordGqlFields = { + [k: string]: boolean | RecordGqlFields | undefined; +}; diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsDeprecated.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsDeprecated.ts new file mode 100644 index 000000000..e8672467d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsDeprecated.ts @@ -0,0 +1 @@ +export type RecordGqlFieldsDeprecated = Record; diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationGqlRecordFields.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationGqlRecordFields.ts index b2ba0834b..34f948bd0 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationGqlRecordFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationGqlRecordFields.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/__tests__/generateDepthOneRecordGqlFields.test.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/__tests__/generateDepthOneRecordGqlFields.test.ts new file mode 100644 index 000000000..dcde53c7d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/__tests__/generateDepthOneRecordGqlFields.test.ts @@ -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, +} +`); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/isRecordGraphlFieldsNode.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/isRecordGraphlFieldsNode.ts new file mode 100644 index 000000000..c13a690f9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/isRecordGraphlFieldsNode.ts @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts index de86ff086..8a06e895d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -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 & { @@ -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, }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts index c8379ec6d..f4555f7e6 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -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( { 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, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index bc767622d..d202c4271 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -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; }; - +type UpdateOneRecordArgs = { + idToUpdate: string; + updateOneRecordInput: Partial>; + optimisticRecord?: Partial; +}; 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>; - optimisticRecord?: Partial; - }) => { + }: UpdateOneRecordArgs) => { const optimisticRecordInput = optimisticRecord ?? computeOptimisticRecordFromInput({ objectMetadataItem, + currentWorkspaceMember: currentWorkspaceMember, recordInput: updateOneRecordInput, cache: apolloClient.cache, objectMetadataItems, diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx index 5a03a45a0..28a1632df 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecords.test.tsx @@ -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: {}, }, ], diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts index e6257fe8d..a1cbfabe9 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/__tests__/useCombinedFindManyRecordsQueryVariables.test.ts @@ -13,7 +13,7 @@ describe('useCombinedFindManyRecordsQueryVariables', () => { firstName: true, lastName: true, }, - } as RecordGqlFields, + } satisfies RecordGqlFields, variables: { filter: { id: { eq: '123' } }, orderBy: [{ createdAt: 'AscNullsLast' }], diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts index 5dad666c7..5af88f00d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts @@ -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 }), diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index e42bb1a63..84296ec13 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -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 = { diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell.tsx index e349c4d52..5eb130f15 100644 --- a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell.tsx @@ -26,7 +26,9 @@ export const RightDrawerTitleRecordInlineCell = () => { const draftValue = useRecoilValue(getDraftValueSelector()); useListenRightDrawerClose(() => { - persistField(draftValue); + if (draftValue !== undefined) { + persistField(draftValue); + } closeInlineCell(); }); diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts index f4a7c9e7b..672923433 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts @@ -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', diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts index ce138d7dd..20d26d97a 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts @@ -56,16 +56,17 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( onSubmit: async (data) => { const createInputs = data.validStructuredRows.map((record) => { const fieldMapping: Record = - 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, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts index 95d701632..5c55cfd2d 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts @@ -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, - fields: FieldMetadataItem[], -) => { +type BuildRecordFromImportedStructuredRowArgs = { + importedStructuredRow: ImportedStructuredRow; + fields: FieldMetadataItem[]; +}; +export const buildRecordFromImportedStructuredRow = ({ + fields, + importedStructuredRow, +}: BuildRecordFromImportedStructuredRowArgs) => { const recordToBuild: Record = {}; 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: { diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts index 8e2b6f27a..70c18219e 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeOptimisticRecordFromInput.test.ts @@ -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: { diff --git a/packages/twenty-front/src/modules/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember.ts b/packages/twenty-front/src/modules/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember.ts new file mode 100644 index 000000000..8398756fe --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts index eb9ded875..111a473b6 100644 --- a/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts @@ -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; + currentWorkspaceMember: CurrentWorkspaceMember | null; } & Pick; 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 = {}; 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; diff --git a/packages/twenty-front/src/modules/object-record/utils/generateDefaultFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateDefaultFieldValue.ts index 415c1b604..27ac3583c 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateDefaultFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateDefaultFieldValue.ts @@ -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, -) => { +type GenerateEmptyFieldValueArgs = { + fieldMetadataItem: Pick; +}; +export const generateDefaultFieldValue = ({ + fieldMetadataItem, +}: GenerateEmptyFieldValueArgs) => { const defaultValue = isFieldValueEmpty({ fieldValue: fieldMetadataItem.defaultValue, fieldDefinition: fieldMetadataItem, }) - ? generateEmptyFieldValue(fieldMetadataItem) + ? generateEmptyFieldValue({ + fieldMetadataItem, + }) : stripSimpleQuotesFromString(fieldMetadataItem.defaultValue); switch (defaultValue) { diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts index 57b01306f..fbd19e39b 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -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, -) => { +export type GenerateEmptyFieldValueArgs = { + fieldMetadataItem: Pick; +}; +// 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 { diff --git a/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts b/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts index a71b4ae11..52786434b 100644 --- a/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts +++ b/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts @@ -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; +}; export const prefillRecord = ({ objectMetadataItem, input, -}: { - objectMetadataItem: ObjectMetadataItem; - input: Record; -}) => { +}: PrefillRecordArgs) => { return Object.fromEntries( objectMetadataItem.fields .map((fieldMetadataItem) => { @@ -26,12 +27,10 @@ export const prefillRecord = ({ 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; diff --git a/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts index b951dc417..8ffd11478 100644 --- a/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts +++ b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts @@ -23,11 +23,12 @@ export const usePrefetchedData = ( objectNameSingular, }); + const recordGqlFields = + operationSignatureFactory({ objectMetadataItem }).fields ?? filter; const { records } = useFindManyRecords({ skip: !isDataPrefetched, objectNameSingular: objectNameSingular, - recordGqlFields: - operationSignatureFactory({ objectMetadataItem }).fields ?? filter, + recordGqlFields, }); return { diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts index c88b3447b..06d284868 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -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 }, diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts index a2f1d9358..6c645dd0b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts @@ -53,6 +53,8 @@ export const useFieldPreviewValue = ({ case FieldMetadataType.PHONES: return getPhonesFieldPreviewValue({ fieldMetadataItem }); default: - return getFieldPreviewValue({ fieldMetadataItem }); + return getFieldPreviewValue({ + fieldMetadataItem, + }); } }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts index edb267685..5bec90e8e 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts @@ -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(); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts index 1255bcd95..e69252c1b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getFieldPreviewValue.ts @@ -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; +}; export const getFieldPreviewValue = ({ fieldMetadataItem, -}: { - fieldMetadataItem: Pick; -}) => { +}: 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); diff --git a/packages/twenty-front/src/testing/jest/generateEmptyJestRecordNode.ts b/packages/twenty-front/src/testing/jest/generateEmptyJestRecordNode.ts index f27e4f3a3..ba28eaa8b 100644 --- a/packages/twenty-front/src/testing/jest/generateEmptyJestRecordNode.ts +++ b/packages/twenty-front/src/testing/jest/generateEmptyJestRecordNode.ts @@ -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; + withDepthOneRelation?: boolean; +}; export const generateEmptyJestRecordNode = ({ objectNameSingular, input, withDepthOneRelation = false, -}: { - objectNameSingular: string; - input: Record; - 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, diff --git a/packages/twenty-front/src/testing/mock-data/people.ts b/packages/twenty-front/src/testing/mock-data/people.ts index dd81c3603..bb02de9a2 100644 --- a/packages/twenty-front/src/testing/mock-data/people.ts +++ b/packages/twenty-front/src/testing/mock-data/people.ts @@ -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, index = 0, diff --git a/packages/twenty-front/src/testing/mock-data/workspace-members.ts b/packages/twenty-front/src/testing/mock-data/workspace-members.ts index 34b96364c..5e77a57ec 100644 --- a/packages/twenty-front/src/testing/mock-data/workspace-members.ts +++ b/packages/twenty-front/src/testing/mock-data/workspace-members.ts @@ -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, + }), + );