From cca72da708a54370009a9868044089a553d23576 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 9 Feb 2024 14:51:30 +0100 Subject: [PATCH] Activity cache injection (#3791) * WIP * Minor fixes * Added TODO * Fix post merge * Fix * Fixed warnings * Fixed comments * Fixed comments * Fixed naming * Removed comment * WIP * WIP 2 * Finished working version * Fixes * Fixed typing * Fixes * Fixes * Fixes * Naming fixes * WIP * Fix import * WIP * Working version on title * Fixed create record id overwrite * Removed unecessary callback * Masterpiece * Fixed delete on click outside drawer or delete * Cleaned * Cleaned * Cleaned * Minor fixes * Fixes * Fixed naming * WIP * Fix * Fixed create from target inline cell * Removed console.log * Fixed delete activity optimistic effect * Fixed no title * Fixed debounce and title body creation --------- Co-authored-by: Charles Bochet --- package.json | 2 + .../components/ActivityBodyEditor.tsx | 115 ++++++++--- .../components/ActivityComments.tsx | 1 + .../activities/components/ActivityEditor.tsx | 120 +++++------ .../activities/components/ActivityTitle.tsx | 118 ++++++++--- .../activities/hooks/useActivityById.ts | 34 ++++ .../hooks/useActivityTargetObjectRecords.ts | 2 + ... useActivityTargetsForTargetableObject.ts} | 8 +- .../useAttachRelationInBothDirections.ts | 91 +++++++++ .../hooks/useCreateActivityInCache.ts | 115 +++++++++++ .../activities/hooks/useCreateActivityInDB.ts | 59 ++++++ .../hooks/useDeleteActivityFromCache.ts | 39 ++++ .../hooks/useOpenCreateActivityDrawerV2.ts | 174 ++++------------ .../activities/hooks/useUpsertActivity.ts | 45 +++++ .../hooks/useWriteActivityTargetsInCache.ts | 84 -------- .../ActivityTargetInlineCellEditMode.tsx | 114 +++++++++-- .../components/ActivityTargetsInlineCell.tsx | 15 +- ...InjectIntoActivityTargetInlineCellCache.ts | 51 ++--- .../activities/notes/components/NoteCard.tsx | 5 +- .../activities/notes/hooks/useNotes.ts | 6 +- .../components/ActivityActionBar.tsx | 25 ++- .../components/RightDrawerActivity.tsx | 24 +-- .../create/RightDrawerCreateActivity.tsx | 2 +- .../activityTitleHasBeenSetFamilyState.ts | 9 + .../states/isCreatingActivityState.ts | 6 + .../states/temporaryActivityForEditorState.ts | 8 + .../components/TimelineActivityCardFooter.tsx | 71 ------- .../components/TimelineCreateButtonGroup.tsx | 6 +- .../useInjectIntoTimelineActivitiesQuery.ts | 89 --------- ...TimelineActivitiesQueryAfterDrawerMount.ts | 124 ++++++++++++ .../timeline/hooks/useTimelineActivities.ts | 14 +- .../makeTimelineActivitiesQueryVariables.ts | 2 +- .../activities/types/ActivityForEditor.ts | 20 ++ .../activities/types/ActivityTarget.ts | 4 +- ...ityTargetsToCreateFromTargetableObjects.ts | 8 +- .../utils/useActivityConnectionUtils.ts | 102 ++++++++++ .../apollo/hooks/useCachedRootQuery.ts | 2 +- .../utils/getRelationDefinition.ts | 62 ++++++ .../triggerAttachRelationOptimisticEffect.ts | 76 ++++--- .../triggerCreateRecordsOptimisticEffect.ts | 126 ++++++------ .../triggerDeleteRecordsOptimisticEffect.ts | 58 +++--- .../triggerDetachRelationOptimisticEffect.ts | 68 ++++--- .../triggerUpdateRecordOptimisticEffect.ts | 187 ++++++++++++------ .../triggerUpdateRelationsOptimisticEffect.ts | 164 +++++++++------ ...coreObjectNamesToDeleteOnRelationDetach.ts | 2 +- .../useMapFieldMetadataToGraphQLQuery.ts | 76 +++++-- .../hooks/useObjectMetadataItems.ts | 11 ++ .../getObjectMetadataItemBySingularName.ts | 22 +++ .../utils/getObjectRecordIdentifier.ts | 13 +- .../MaxQueryDepthForCacheInjection.ts | 1 + .../cache/hooks/useAddRecordInCache.ts | 68 ++++--- .../useAppendToFindManyRecordsQueryInCache.ts | 50 +++++ .../cache/hooks/useGetRecordFromCache.ts | 2 +- .../useInjectIntoFindOneRecordQueryCache.ts | 44 +++++ .../useReadFindManyRecordsQueryInCache.ts | 53 +++++ .../useUpsertFindManyRecordsQueryInCache.ts | 51 +++++ .../hooks/useCreateManyRecords.ts | 57 ++++-- .../hooks/useCreateManyRecordsInCache.ts | 2 +- .../object-record/hooks/useCreateOneRecord.ts | 47 +++-- .../hooks/useCreateOneRecordInCache.ts | 2 +- .../hooks/useDeleteManyRecords.ts | 8 +- .../object-record/hooks/useDeleteOneRecord.ts | 10 +- .../object-record/hooks/useFieldContext.tsx | 5 +- .../object-record/hooks/useFindOneRecord.ts | 9 +- .../useGenerateCreateManyRecordMutation.ts | 6 +- .../useGenerateCreateOneRecordMutation.ts | 6 +- ...teExecuteQuickActionOnOneRecordMutation.ts | 6 +- ...anyRecordsForMultipleMetadataItemsQuery.ts | 7 +- .../hooks/useGenerateFindManyRecordsQuery.ts | 7 +- .../hooks/useGenerateFindOneRecordQuery.ts | 14 +- .../useGenerateUpdateOneRecordMutation.ts | 2 +- .../object-record/hooks/useUpdateOneRecord.ts | 10 +- ...cordBoardDeprecatedColumnEditTitleMenu.tsx | 2 +- .../utils/generateEmptyFieldValue.ts | 2 +- .../utils/mapEdgeToObjectRecord.ts | 8 + .../right-drawer/components/RightDrawer.tsx | 4 +- .../components/ShowPageAddButton.tsx | 18 +- .../hooks/useClickOustideListenerStates.ts | 5 + .../hooks/useClickOutsideListener.ts | 113 ++++++++++- ...ckOutsideListenerCallbacksStateScopeMap.ts | 9 + .../types/ClickOutsideListenerCallback.ts | 6 + .../ClickOutsideListenerCallbackFunction.ts | 3 + .../pages/object-record/RecordShowPage.tsx | 2 +- .../twenty-front/src/types/StringKeyOf.ts | 1 + packages/twenty-front/src/utils/debounce.ts | 12 -- .../src/utils/parseApolloStoreFieldName.ts | 4 +- yarn.lock | 18 ++ 87 files changed, 2195 insertions(+), 1058 deletions(-) create mode 100644 packages/twenty-front/src/modules/activities/hooks/useActivityById.ts rename packages/twenty-front/src/modules/activities/hooks/{useActivityTargets.ts => useActivityTargetsForTargetableObject.ts} (83%) create mode 100644 packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts create mode 100644 packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts delete mode 100644 packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts create mode 100644 packages/twenty-front/src/modules/activities/states/activityTitleHasBeenSetFamilyState.ts create mode 100644 packages/twenty-front/src/modules/activities/states/isCreatingActivityState.ts create mode 100644 packages/twenty-front/src/modules/activities/states/temporaryActivityForEditorState.ts delete mode 100644 packages/twenty-front/src/modules/activities/timeline/components/TimelineActivityCardFooter.tsx delete mode 100644 packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts create mode 100644 packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount.ts create mode 100644 packages/twenty-front/src/modules/activities/types/ActivityForEditor.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts create mode 100644 packages/twenty-front/src/modules/apollo/optimistic-effect/utils/getRelationDefinition.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemBySingularName.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/constants/MaxQueryDepthForCacheInjection.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/mapEdgeToObjectRecord.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksStateScopeMap.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallback.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallbackFunction.ts create mode 100644 packages/twenty-front/src/types/StringKeyOf.ts delete mode 100644 packages/twenty-front/src/utils/debounce.ts diff --git a/package.json b/package.json index 626d8c44c0..66d5e176aa 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "danger-plugin-todos": "^1.3.1", "dataloader": "^2.2.2", "date-fns": "^2.30.0", + "debounce": "^2.0.0", "deep-equal": "^2.2.2", "docusaurus-node-polyfills": "^1.0.0", "dotenv-cli": "^7.2.1", @@ -149,6 +150,7 @@ "tsup": "^8.0.1", "type-fest": "^4.1.0", "typeorm": "^0.3.17", + "use-debounce": "^10.0.0", "uuid": "^9.0.0", "vite-tsconfig-paths": "^4.2.1", "xlsx-ugnis": "^0.19.3", diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx index 79137c2748..3ba4c2737c 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -1,13 +1,17 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { BlockNoteEditor } from '@blocknote/core'; import { useBlockNote } from '@blocknote/react'; import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; -import debounce from 'lodash.debounce'; +import { useRecoilState } from 'recoil'; +import { useDebouncedCallback } from 'use-debounce'; +import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; import { Activity } from '@/activities/types/Activity'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; @@ -23,38 +27,99 @@ const StyledBlockNoteStyledContainer = styled.div` `; type ActivityBodyEditorProps = { - activity: Pick; - onChange?: (activityBody: string) => void; + activity: Activity; + fillTitleFromBody: boolean; }; export const ActivityBodyEditor = ({ activity, - onChange, + fillTitleFromBody, }: ActivityBodyEditorProps) => { - const [body, setBody] = useState(null); - const { updateOneRecord } = useUpdateOneRecord({ - objectNameSingular: CoreObjectNameSingular.Activity, + const [stringifiedBodyFromEditor, setStringifiedBodyFromEditor] = useState< + string | null + >(activity.body); + + const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( + activityTitleHasBeenSetFamilyState({ + activityId: activity.id, + }), + ); + + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const modifyActivityFromCache = useModifyRecordFromCache({ + objectMetadataItem: objectMetadataItemActivity, }); - useEffect(() => { - if (body) { - onChange?.(body); - } - }, [body, onChange]); + const { upsertActivity } = useUpsertActivity(); - const debounceOnChange = useMemo(() => { - const onInternalChange = (activityBody: string) => { - setBody(activityBody); - updateOneRecord?.({ - idToUpdate: activity.id, - updateOneRecordInput: { - body: activityBody, + const persistBodyDebounced = useDebouncedCallback((newBody: string) => { + upsertActivity({ + activity, + input: { + body: newBody, + }, + }); + }, 500); + + const persistTitleAndBodyDebounced = useDebouncedCallback( + (newTitle: string, newBody: string) => { + upsertActivity({ + activity, + input: { + title: newTitle, + body: newBody, }, }); - }; - return debounce(onInternalChange, 200); - }, [updateOneRecord, activity.id]); + setActivityTitleHasBeenSet(true); + }, + 500, + ); + + const updateTitleAndBody = useCallback( + (newStringifiedBody: string) => { + const blockBody = JSON.parse(newStringifiedBody); + const newTitleFromBody = blockBody[0]?.content?.[0]?.text; + + modifyActivityFromCache(activity.id, { + title: () => { + return newTitleFromBody; + }, + }); + + persistTitleAndBodyDebounced(newTitleFromBody, newStringifiedBody); + }, + [activity.id, modifyActivityFromCache, persistTitleAndBodyDebounced], + ); + + const handleBodyChange = useCallback( + (activityBody: string) => { + if (!activityTitleHasBeenSet && fillTitleFromBody) { + updateTitleAndBody(activityBody); + } else { + persistBodyDebounced(activityBody); + } + }, + [ + fillTitleFromBody, + persistBodyDebounced, + activityTitleHasBeenSet, + updateTitleAndBody, + ], + ); + + useEffect(() => { + if ( + isNonEmptyString(stringifiedBodyFromEditor) && + activity.body !== stringifiedBodyFromEditor + ) { + handleBodyChange(stringifiedBodyFromEditor); + } + }, [stringifiedBodyFromEditor, handleBodyChange, activity]); const slashMenuItems = getSlashMenu(); @@ -85,7 +150,7 @@ export const ActivityBodyEditor = ({ : undefined, domAttributes: { editor: { class: 'editor' } }, onEditorContentChange: (editor: BlockNoteEditor) => { - debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? ''); + setStringifiedBodyFromEditor(JSON.stringify(editor.topLevelBlocks) ?? ''); }, slashMenuItems, blockSpecs: blockSpecs, diff --git a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx index 9d88bcc272..2c4e2070d2 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx @@ -67,6 +67,7 @@ export const ActivityComments = ({ const { records: comments } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Comment, + skip: !isNonEmptyString(activity?.id), filter: { activityId: { eq: activity?.id ?? '', diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx index eda4710171..d157773aa2 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx @@ -1,22 +1,26 @@ -import React, { useCallback, useRef, useState } from 'react'; +import { useRef } from 'react'; import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor'; import { ActivityComments } from '@/activities/components/ActivityComments'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; +import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; +import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; +import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; import { Activity } from '@/activities/types/Activity'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { Comment } from '@/activities/types/Comment'; -import { GraphQLActivity } from '@/activities/types/GraphQLActivity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFieldContext } from '@/object-record/hooks/useFieldContext'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { + RecordUpdateHook, + RecordUpdateHookParams, +} from '@/object-record/record-field/contexts/FieldContext'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; +import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; +import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; -import { debounce } from '~/utils/debounce'; import { ActivityTitle } from './ActivityTitle'; @@ -54,36 +58,32 @@ const StyledTopContainer = styled.div` `; type ActivityEditorProps = { - activity: Pick< - Activity, - 'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt' - > & { - comments?: Array | null; - } & { - assignee?: Pick | null; - } & { - activityTargets?: Array< - Pick - > | null; - }; + activity: Activity; showComment?: boolean; - autoFillTitle?: boolean; + fillTitleFromBody?: boolean; }; export const ActivityEditor = ({ activity, showComment = true, - autoFillTitle = false, + fillTitleFromBody = false, }: ActivityEditorProps) => { - const [hasUserManuallySetTitle, setHasUserManuallySetTitle] = - useState(false); - - const [title, setTitle] = useState(activity.title ?? ''); - const containerRef = useRef(null); - const { updateOneRecord: updateOneActivity } = useUpdateOneRecord({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); + + const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener( + RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID, + ); + + const { upsertActivity } = useUpsertActivity(); + const { deleteActivityFromCache } = useDeleteActivityFromCache(); + + const useUpsertOneActivityMutation: RecordUpdateHook = () => { + const upsertActivityMutation = ({ variables }: RecordUpdateHookParams) => { + upsertActivity({ activity, input: variables.updateOneRecordInput }); + }; + + return [upsertActivityMutation, { loading: false }]; + }; const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({ objectNameSingular: CoreObjectNameSingular.Activity, @@ -91,6 +91,7 @@ export const ActivityEditor = ({ fieldMetadataName: 'dueAt', fieldPosition: 0, clearable: true, + customUseUpdateOneObjectHook: useUpsertOneActivityMutation, }); const { FieldContextProvider: AssigneeFieldContextProvider } = @@ -100,41 +101,24 @@ export const ActivityEditor = ({ fieldMetadataName: 'assignee', fieldPosition: 1, clearable: true, + customUseUpdateOneObjectHook: useUpsertOneActivityMutation, }); - const updateTitle = useCallback( - (newTitle: string) => { - updateOneActivity?.({ - idToUpdate: activity.id, - updateOneRecordInput: { - title: newTitle ?? '', - }, - }); - }, - [activity.id, updateOneActivity], - ); - const handleActivityCompletionChange = useCallback( - (value: boolean) => { - updateOneActivity?.({ - idToUpdate: activity.id, - updateOneRecordInput: { - completedAt: value ? new Date().toISOString() : null, - }, - }); - }, - [activity.id, updateOneActivity], + const [isCreatingActivity, setIsCreatingActivity] = useRecoilState( + isCreatingActivityState, ); - const debouncedUpdateTitle = debounce(updateTitle, 200); + // TODO: remove - const updateTitleFromBody = (body: string) => { - const blockBody = JSON.parse(body); - const parsedTitle = blockBody[0]?.content?.[0]?.text; - if (!hasUserManuallySetTitle && autoFillTitle) { - setTitle(parsedTitle); - debouncedUpdateTitle(parsedTitle); - } - }; + useRegisterClickOutsideListenerCallback({ + callbackId: 'activity-editor', + callbackFunction: () => { + if (isCreatingActivity) { + setIsCreatingActivity(false); + deleteActivityFromCache(activity); + } + }, + }); if (!activity) { return <>; @@ -145,17 +129,7 @@ export const ActivityEditor = ({ - { - setTitle(newTitle); - setHasUserManuallySetTitle(true); - debouncedUpdateTitle(newTitle); - }} - onCompletionChange={handleActivityCompletionChange} - /> + {activity.type === 'Task' && DueAtFieldContextProvider && @@ -169,14 +143,12 @@ export const ActivityEditor = ({ )} - + {showComment && ( diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx index af581a710a..06e9df00e4 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx @@ -1,11 +1,20 @@ +import { useState } from 'react'; import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; +import { useDebouncedCallback } from 'use-debounce'; -import { ActivityType } from '@/activities/types/Activity'; +import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; +import { Activity } from '@/activities/types/Activity'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; import { Checkbox, CheckboxShape, CheckboxSize, } from '@/ui/input/components/Checkbox'; +import { isDefined } from '~/utils/isDefined'; const StyledEditableTitleInput = styled.input<{ completed: boolean; @@ -39,36 +48,83 @@ const StyledContainer = styled.div` `; type ActivityTitleProps = { - title: string; - type: ActivityType; - completed: boolean; - onTitleChange: (title: string) => void; - onCompletionChange: (value: boolean) => void; + activity: Activity; }; -export const ActivityTitle = ({ - title, - completed, - type, - onTitleChange, - onCompletionChange, -}: ActivityTitleProps) => ( - - {type === 'Task' && ( - onCompletionChange(value)} +export const ActivityTitle = ({ activity }: ActivityTitleProps) => { + const [internalTitle, setInternalTitle] = useState(activity.title); + + const { upsertActivity } = useUpsertActivity(); + + const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( + activityTitleHasBeenSetFamilyState({ + activityId: activity.id, + }), + ); + + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const modifyActivityFromCache = useModifyRecordFromCache({ + objectMetadataItem: objectMetadataItemActivity, + }); + + const persistTitleDebounced = useDebouncedCallback((newTitle: string) => { + upsertActivity({ + activity, + input: { + title: newTitle, + }, + }); + + if (!activityTitleHasBeenSet) { + setActivityTitleHasBeenSet(true); + } + }, 500); + + const handleTitleChange = (newTitle: string) => { + setInternalTitle(newTitle); + + modifyActivityFromCache(activity.id, { + title: () => { + return newTitle; + }, + }); + + persistTitleDebounced(newTitle); + }; + + const handleActivityCompletionChange = (value: boolean) => { + upsertActivity({ + activity, + input: { + completedAt: value ? new Date().toISOString() : null, + }, + }); + }; + + const completed = isDefined(activity.completedAt); + + return ( + + {activity.type === 'Task' && ( + handleActivityCompletionChange(value)} + /> + )} + handleTitleChange(event.target.value)} + value={internalTitle} + completed={completed} /> - )} - onTitleChange(event.target.value)} - value={title} - completed={completed} - /> - -); + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts new file mode 100644 index 0000000000..4e8ba80631 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts @@ -0,0 +1,34 @@ +import { useSetRecoilState } from 'recoil'; + +import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; + +const QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS = 3; + +export const useActivityById = ({ activityId }: { activityId: string }) => { + const setEntityFields = useSetRecoilState(recordStoreFamilyState(activityId)); + + const { makeActivityWithoutConnection } = useActivityConnectionUtils(); + + const { record: activityWithConnections } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.Activity, + objectRecordId: activityId, + skip: !activityId, + onCompleted: (activityWithConnections: any) => { + const { activity } = makeActivityWithoutConnection( + activityWithConnections, + ); + + setEntityFields(activity); + }, + depth: QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS, + }); + + const { activity } = makeActivityWithoutConnection(activityWithConnections); + + return { + activity, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts index 4b7477a93e..12b050b02c 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts @@ -1,3 +1,4 @@ +import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject'; @@ -17,6 +18,7 @@ export const useActivityTargetObjectRecords = ({ const { records: activityTargets, loading: loadingActivityTargets } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.ActivityTarget, + skip: !isNonEmptyString(activityId), filter: { activityId: { eq: activityId, diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObject.ts similarity index 83% rename from packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts rename to packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObject.ts index 68e88f4ecc..7cdf18e207 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargets.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObject.ts @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { isNonEmptyString } from '@sniptt/guards'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; @@ -6,7 +7,7 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTarget import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -export const useActivityTargets = ({ +export const useActivityTargetsForTargetableObject = ({ targetableObject, }: { targetableObject: ActivityTargetableObject; @@ -17,9 +18,14 @@ export const useActivityTargets = ({ const [initialized, setInitialized] = useState(false); + const targetableObjectId = targetableObject.id; + + const skipRequest = !isNonEmptyString(targetableObjectId); + const { records: activityTargets, loading: loadingActivityTargets } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.ActivityTarget, + skip: skipRequest, filter: { [targetObjectFieldName]: { eq: targetableObject.id, diff --git a/packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts b/packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts new file mode 100644 index 0000000000..2ef89de545 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useAttachRelationInBothDirections.ts @@ -0,0 +1,91 @@ +import { useApolloClient } from '@apollo/client'; +import { StringKeyOf } from 'type-fest'; + +import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition'; +import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { isDefined } from '~/utils/isDefined'; + +export const useAttachRelationInBothDirections = () => { + const { objectMetadataItems } = useObjectMetadataItems(); + + const apolloClient = useApolloClient(); + + const attachRelationInBothDirections = < + Source extends ObjectRecord = ObjectRecord, + Target extends ObjectRecord = ObjectRecord, + >({ + sourceRecord, + targetRecords, + sourceObjectNameSingular, + targetObjectNameSingular, + fieldNameOnSourceRecord, + fieldNameOnTargetRecord, + }: { + sourceRecord: Source; + targetRecords: Target[]; + sourceObjectNameSingular: string; + targetObjectNameSingular: string; + fieldNameOnSourceRecord: StringKeyOf; + fieldNameOnTargetRecord: StringKeyOf; + }) => { + const sourceObjectMetadataItem = getObjectMetadataItemByNameSingular({ + objectMetadataItems, + objectNameSingular: sourceObjectNameSingular, + }); + + const targetObjectMetadataItem = getObjectMetadataItemByNameSingular({ + objectMetadataItems, + objectNameSingular: targetObjectNameSingular, + }); + + const fieldMetadataItemOnSourceRecord = + sourceObjectMetadataItem.fields.find( + (field) => field.name === fieldNameOnSourceRecord, + ); + + if (!isDefined(fieldMetadataItemOnSourceRecord)) { + throw new Error( + `Field ${fieldNameOnSourceRecord} not found on object ${sourceObjectNameSingular}`, + ); + } + + const relationDefinition = getRelationDefinition({ + fieldMetadataItemOnSourceRecord: fieldMetadataItemOnSourceRecord, + objectMetadataItems, + }); + + if (!isDefined(relationDefinition)) { + throw new Error( + `Relation metadata not found for field ${fieldNameOnSourceRecord} on object ${sourceObjectNameSingular}`, + ); + } + + // TODO: could we use triggerUpdateRelationsOptimisticEffect here? + targetRecords.forEach((relationTargetRecord) => { + triggerAttachRelationOptimisticEffect({ + cache: apolloClient.cache, + sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular, + sourceRecordId: sourceRecord.id, + fieldNameOnTargetRecord: fieldNameOnTargetRecord, + targetObjectNameSingular: targetObjectMetadataItem.nameSingular, + targetRecordId: relationTargetRecord.id, + }); + + triggerAttachRelationOptimisticEffect({ + cache: apolloClient.cache, + sourceObjectNameSingular: targetObjectMetadataItem.nameSingular, + sourceRecordId: relationTargetRecord.id, + fieldNameOnTargetRecord: fieldNameOnSourceRecord, + targetObjectNameSingular: sourceObjectMetadataItem.nameSingular, + targetRecordId: sourceRecord.id, + }); + }); + }; + + return { + attachRelationInBothDirections, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts new file mode 100644 index 0000000000..f03babb849 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts @@ -0,0 +1,115 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { useRecoilValue } from 'recoil'; +import { v4 } from 'uuid'; + +import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections'; +import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; +import { useInjectIntoTimelineActivitiesQueryAfterDrawerMount } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount'; +import { Activity, ActivityType } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { getActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache'; +import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; + +export const useCreateActivityInCache = () => { + const { createManyRecordsInCache: createManyActivityTargetsInCache } = + useCreateManyRecordsInCache({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { createOneRecordInCache: createOneActivityInCache } = + useCreateOneRecordInCache({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const { record: workspaceMemberRecord } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + objectRecordId: currentWorkspaceMember?.id, + depth: 3, + }); + + const { injectIntoTimelineActivitiesQueryAfterDrawerMount } = + useInjectIntoTimelineActivitiesQueryAfterDrawerMount(); + + const { injectIntoActivityTargetInlineCellCache } = + useInjectIntoActivityTargetInlineCellCache(); + + const { + attachRelationInBothDirections: + attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache, + } = useAttachRelationInBothDirections(); + + const createActivityInCache = ({ + type, + targetableObjects, + timelineTargetableObject, + assigneeId, + }: { + type: ActivityType; + targetableObjects: ActivityTargetableObject[]; + timelineTargetableObject: ActivityTargetableObject; + assigneeId?: string; + }) => { + const activityId = v4(); + + const createdActivityInCache = createOneActivityInCache({ + id: activityId, + author: workspaceMemberRecord, + authorId: workspaceMemberRecord?.id, + assignee: !assigneeId ? workspaceMemberRecord : undefined, + assigneeId: + assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id) + ? workspaceMemberRecord?.id + : undefined, + type: type, + }); + + const activityTargetsToCreate = + getActivityTargetsToCreateFromTargetableObjects({ + activityId, + targetableObjects, + }); + + const createdActivityTargetsInCache = createManyActivityTargetsInCache( + activityTargetsToCreate, + ); + + injectIntoTimelineActivitiesQueryAfterDrawerMount({ + activityToInject: createdActivityInCache, + activityTargetsToInject: createdActivityTargetsInCache, + timelineTargetableObject, + }); + + injectIntoActivityTargetInlineCellCache({ + activityId, + activityTargetsToInject: createdActivityTargetsInCache, + }); + + attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache({ + sourceRecord: createdActivityInCache, + fieldNameOnSourceRecord: 'activityTargets', + sourceObjectNameSingular: CoreObjectNameSingular.Activity, + fieldNameOnTargetRecord: 'activity', + targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget, + targetRecords: createdActivityTargetsInCache, + }); + + return { + createdActivityInCache: { + ...createdActivityInCache, + activityTargets: createdActivityTargetsInCache, + }, + createdActivityTargetsInCache, + }; + }; + + return { + createActivityInCache, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts new file mode 100644 index 0000000000..ed6e7a5565 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts @@ -0,0 +1,59 @@ +import { isNonEmptyArray } from '@sniptt/guards'; + +import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache'; +import { ActivityForEditor } from '@/activities/types/ActivityForEditor'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; + +export const useCreateActivityInDB = () => { + const { createOneRecord: createOneActivity } = useCreateOneRecord({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const { createManyRecords: createManyActivityTargets } = + useCreateManyRecords({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { makeActivityWithConnection } = useActivityConnectionUtils(); + + const { modifyActivityTargetsOnActivityCache } = + useModifyActivityTargetsOnActivityCache(); + + const createActivityInDB = async (activityToCreate: ActivityForEditor) => { + const { activityWithConnection } = makeActivityWithConnection( + activityToCreate as any, // TODO: fix type + ); + + await createOneActivity?.( + { + ...activityWithConnection, + updatedAt: new Date().toISOString(), + }, + { + skipOptimisticEffect: true, + }, + ); + + const activityTargetsToCreate = activityToCreate.activityTargets ?? []; + + if (isNonEmptyArray(activityTargetsToCreate)) { + await createManyActivityTargets(activityTargetsToCreate, { + skipOptimisticEffect: true, + }); + } + + // TODO: replace by trigger optimistic effect + modifyActivityTargetsOnActivityCache({ + activityId: activityToCreate.id, + activityTargets: activityTargetsToCreate, + }); + }; + + return { + createActivityInDB, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts b/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts new file mode 100644 index 0000000000..26db54c7cc --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts @@ -0,0 +1,39 @@ +import { useApolloClient } from '@apollo/client'; + +import { ActivityForEditor } from '@/activities/types/ActivityForEditor'; +import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; +import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; + +// TODO: this should be useDeleteRecordFromCache +export const useDeleteActivityFromCache = () => { + const { makeActivityWithConnection } = useActivityConnectionUtils(); + + const apolloClient = useApolloClient(); + + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const deleteActivityFromCache = (activityToDelete: ActivityForEditor) => { + const { activityWithConnection } = makeActivityWithConnection( + activityToDelete as any, // TODO: fix type + ); + + triggerDeleteRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem: objectMetadataItemActivity, + objectMetadataItems, + recordsToDelete: [activityWithConnection], + }); + }; + + return { + deleteActivityFromCache, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts index cb8ebfda75..1b1dff71a7 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerV2.ts @@ -1,163 +1,61 @@ -import { useCallback } from 'react'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { v4 } from 'uuid'; +import { useRecoilState, useSetRecoilState } from 'recoil'; -import { useActivityTargets } from '@/activities/hooks/useActivityTargets'; -import { useModifyActivityOnActivityTargetsCache } from '@/activities/hooks/useModifyActivityOnActivityTargetCache'; -import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache'; -import { useWriteActivityTargetsInCache } from '@/activities/hooks/useWriteActivityTargetsInCache'; -import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; -import { useInjectIntoTimelineActivitiesQuery } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery'; -import { Activity, ActivityType } from '@/activities/types/Activity'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { getActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache'; -import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { mapToRecordId } from '@/object-record/utils/mapToObjectId'; +import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache'; +import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; +import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; +import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState'; +import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; +import { ActivityType } from '@/activities/types/Activity'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState'; -import { viewableActivityIdState } from '../states/viewableActivityIdState'; import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; -export const useOpenCreateActivityDrawerV2 = ({ - targetableObject, -}: { - targetableObject: ActivityTargetableObject; -}) => { +export const useOpenCreateActivityDrawerV2 = () => { const { openRightDrawer } = useRightDrawer(); - const { createManyRecordsInCache: createManyActivityTargetsInCache } = - useCreateManyRecordsInCache({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const { createOneRecordInCache: createOneActivityInCache } = - useCreateOneRecordInCache({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); - - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - - const { record: workspaceMemberRecord } = useFindOneRecord({ - objectNameSingular: CoreObjectNameSingular.WorkspaceMember, - objectRecordId: currentWorkspaceMember?.id, - }); - const setHotkeyScope = useSetHotkeyScope(); + const { createActivityInCache } = useCreateActivityInCache(); + const [, setActivityTargetableEntityArray] = useRecoilState( activityTargetableEntityArrayState, ); const [, setViewableActivityId] = useRecoilState(viewableActivityIdState); - const { activityTargets } = useActivityTargets({ - targetableObject, - }); + const setIsCreatingActivity = useSetRecoilState(isCreatingActivityState); - const { injectIntoTimelineActivitiesNextQuery } = - useInjectIntoTimelineActivitiesQuery(); + const setTemporaryActivityForEditor = useSetRecoilState( + temporaryActivityForEditorState, + ); - const { injectIntoActivityTargetInlineCellCache } = - useInjectIntoActivityTargetInlineCellCache(); - - const { injectIntoUseActivityTargets } = useWriteActivityTargetsInCache(); - - const { modifyActivityTargetsOnActivityCache } = - useModifyActivityTargetsOnActivityCache(); - - const { modifyActivityOnActivityTargetsCache } = - useModifyActivityOnActivityTargetsCache(); - - return useCallback( - async ({ + const openCreateActivityDrawer = async ({ + type, + targetableObjects, + timelineTargetableObject, + assigneeId, + }: { + type: ActivityType; + targetableObjects: ActivityTargetableObject[]; + timelineTargetableObject: ActivityTargetableObject; + assigneeId?: string; + }) => { + const { createdActivityInCache } = createActivityInCache({ type, targetableObjects, + timelineTargetableObject, assigneeId, - }: { - type: ActivityType; - targetableObjects: ActivityTargetableObject[]; - assigneeId?: string; - }) => { - const activityId = v4(); + }); - const createdActivityInCache = await createOneActivityInCache({ - id: activityId, - author: workspaceMemberRecord, - authorId: workspaceMemberRecord?.id, - assignee: !assigneeId ? workspaceMemberRecord : undefined, - assigneeId: - assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id) - ? workspaceMemberRecord?.id - : undefined, - type: type, - }); + setTemporaryActivityForEditor(createdActivityInCache); + setIsCreatingActivity(true); + setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); + setViewableActivityId(createdActivityInCache.id); + setActivityTargetableEntityArray(targetableObjects ?? []); + openRightDrawer(RightDrawerPages.CreateActivity); + }; - if (!createdActivityInCache) { - return; - } - - const activityTargetsToCreate = - getActivityTargetsToCreateFromTargetableObjects({ - activityId, - targetableObjects, - }); - - const createdActivityTargetsInCache = - await createManyActivityTargetsInCache(activityTargetsToCreate); - - injectIntoUseActivityTargets({ - targetableObject, - activityTargetsToInject: createdActivityTargetsInCache, - }); - - injectIntoTimelineActivitiesNextQuery({ - activityTargets, - activityToInject: createdActivityInCache, - }); - - injectIntoActivityTargetInlineCellCache({ - activityId, - activityTargetsToInject: createdActivityTargetsInCache, - }); - - modifyActivityTargetsOnActivityCache({ - activityId, - activityTargets: createdActivityTargetsInCache, - }); - - modifyActivityOnActivityTargetsCache({ - activityTargetIds: createdActivityTargetsInCache.map(mapToRecordId), - activity: createdActivityInCache, - }); - - setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - setViewableActivityId(activityId); - setActivityTargetableEntityArray(targetableObjects ?? []); - openRightDrawer(RightDrawerPages.CreateActivity); - }, - [ - openRightDrawer, - setActivityTargetableEntityArray, - createManyActivityTargetsInCache, - setHotkeyScope, - setViewableActivityId, - createOneActivityInCache, - workspaceMemberRecord, - activityTargets, - targetableObject, - injectIntoTimelineActivitiesNextQuery, - injectIntoActivityTargetInlineCellCache, - injectIntoUseActivityTargets, - modifyActivityTargetsOnActivityCache, - modifyActivityOnActivityTargetsCache, - ], - ); + return openCreateActivityDrawer; }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts new file mode 100644 index 0000000000..2310ba7fb2 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts @@ -0,0 +1,45 @@ +import { useRecoilState } from 'recoil'; + +import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'; +import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; +import { Activity } from '@/activities/types/Activity'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; + +export const useUpsertActivity = () => { + const [isCreatingActivity, setIsCreatingActivity] = useRecoilState( + isCreatingActivityState, + ); + + const { updateOneRecord: updateOneActivity } = useUpdateOneRecord({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const { createActivityInDB } = useCreateActivityInDB(); + + const upsertActivity = ({ + activity, + input, + }: { + activity: Activity; + input: Partial; + }) => { + if (isCreatingActivity) { + createActivityInDB({ + ...activity, + ...input, + }); + + setIsCreatingActivity(false); + } else { + updateOneActivity?.({ + idToUpdate: activity.id, + updateOneRecordInput: input, + }); + } + }; + + return { + upsertActivity, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts b/packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts deleted file mode 100644 index 7e4830a93e..0000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useWriteActivityTargetsInCache.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useApolloClient } from '@apollo/client'; - -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; - -export const useWriteActivityTargetsInCache = () => { - const apolloClient = useApolloClient(); - - const { - objectMetadataItem: objectMetadataItemActivityTarget, - findManyRecordsQuery: findManyActivityTargetsQuery, - } = useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const injectIntoUseActivityTargets = ({ - targetableObject, - activityTargetsToInject, - }: { - targetableObject: Pick< - ActivityTargetableObject, - 'id' | 'targetObjectNameSingular' - >; - activityTargetsToInject: ActivityTarget[]; - }) => { - const targetObjectFieldName = getActivityTargetObjectFieldIdName({ - nameSingular: targetableObject.targetObjectNameSingular, - }); - - const existingActivityTargetsForTargetableObjectQueryResult = - apolloClient.readQuery({ - query: findManyActivityTargetsQuery, - variables: { - filter: { - [targetObjectFieldName]: { - eq: targetableObject.id, - }, - }, - }, - }); - - const existingActivityTargetsForTargetableObject = - getRecordsFromRecordConnection({ - recordConnection: existingActivityTargetsForTargetableObjectQueryResult[ - objectMetadataItemActivityTarget.namePlural - ] as ObjectRecordConnection, - }); - - const newActivityTargetsForTargetableObject = [ - ...existingActivityTargetsForTargetableObject, - ...activityTargetsToInject, - ]; - - const newActivityTargetsConnection = getRecordConnectionFromRecords({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - records: newActivityTargetsForTargetableObject, - }); - - apolloClient.writeQuery({ - query: findManyActivityTargetsQuery, - variables: { - filter: { - [targetObjectFieldName]: { - eq: targetableObject.id, - }, - }, - }, - data: { - [objectMetadataItemActivityTarget.namePlural]: - newActivityTargetsConnection, - }, - }); - }; - - return { - injectIntoUseActivityTargets, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx index 7a22cd4dd3..e799b5cd8a 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx @@ -1,10 +1,18 @@ import styled from '@emotion/styled'; +import { isNonEmptyArray } from '@sniptt/guards'; +import { useRecoilState } from 'recoil'; import { v4 } from 'uuid'; +import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; +import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; +import { Activity } from '@/activities/types/Activity'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; @@ -18,14 +26,16 @@ const StyledSelectContainer = styled.div` `; type ActivityTargetInlineCellEditModeProps = { - activityId: string; + activity: Activity; activityTargetObjectRecords: ActivityTargetObjectRecord[]; }; export const ActivityTargetInlineCellEditMode = ({ - activityId, + activity, activityTargetObjectRecords, }: ActivityTargetInlineCellEditModeProps) => { + const [isCreatingActivity] = useRecoilState(isCreatingActivityState); + const selectedObjectRecordIds = activityTargetObjectRecords.map( (activityTarget) => ({ objectNameSingular: activityTarget.targetObjectNameSingular, @@ -46,6 +56,21 @@ export const ActivityTargetInlineCellEditMode = ({ const { closeInlineCell: closeEditableField } = useInlineCell(); + const { upsertActivity } = useUpsertActivity(); + + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { injectIntoActivityTargetInlineCellCache } = + useInjectIntoActivityTargetInlineCellCache(); + + const { generateObjectRecordOptimisticResponse } = + useGenerateObjectRecordOptimisticResponse({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => { closeEditableField(); @@ -67,25 +92,74 @@ export const ActivityTargetInlineCellEditMode = ({ ), ); - if (activityTargetRecordsToCreate.length > 0) { - await createManyActivityTargets( - activityTargetRecordsToCreate.map((selectedRecord) => ({ - id: v4(), - activityId, - [getActivityTargetObjectFieldIdName({ - nameSingular: selectedRecord.objectMetadataItem.nameSingular, - })]: selectedRecord.recordIdentifier.id, - })), - ); - } + if (isCreatingActivity) { + let activityTargetsForCreation = activity.activityTargets; - if (activityTargetRecordsToDelete.length > 0) { - await deleteManyActivityTargets( - activityTargetRecordsToDelete.map( - (activityTargetObjectRecord) => - activityTargetObjectRecord.activityTargetRecord.id, - ), - ); + if (isNonEmptyArray(activityTargetsForCreation)) { + const generatedActivityTargets = activityTargetRecordsToCreate.map( + (selectedRecord) => { + const emptyActivityTarget = + generateObjectRecordOptimisticResponse({ + id: v4(), + activityId: activity.id, + activity, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + [getActivityTargetObjectFieldIdName({ + nameSingular: selectedRecord.objectMetadataItem.nameSingular, + })]: selectedRecord.recordIdentifier.id, + }); + + return emptyActivityTarget; + }, + ); + + activityTargetsForCreation.push(...generatedActivityTargets); + } + + if (isNonEmptyArray(activityTargetRecordsToDelete)) { + activityTargetsForCreation = activityTargetsForCreation.filter( + (activityTarget) => + !activityTargetRecordsToDelete.some( + (activityTargetObjectRecord) => + activityTargetObjectRecord.targetObjectRecord.id === + activityTarget.id, + ), + ); + } + + injectIntoActivityTargetInlineCellCache({ + activityId: activity.id, + activityTargetsToInject: activityTargetsForCreation, + }); + + upsertActivity({ + activity, + input: { + activityTargets: activityTargetsForCreation, + }, + }); + } else { + if (activityTargetRecordsToCreate.length > 0) { + await createManyActivityTargets( + activityTargetRecordsToCreate.map((selectedRecord) => ({ + id: v4(), + activityId: activity.id, + [getActivityTargetObjectFieldIdName({ + nameSingular: selectedRecord.objectMetadataItem.nameSingular, + })]: selectedRecord.recordIdentifier.id, + })), + ); + } + + if (activityTargetRecordsToDelete.length > 0) { + await deleteManyActivityTargets( + activityTargetRecordsToDelete.map( + (activityTargetObjectRecord) => + activityTargetObjectRecord.activityTargetRecord.id, + ), + ); + } } }; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx index a419c959e2..78c0716c14 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx @@ -1,8 +1,7 @@ import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { GraphQLActivity } from '@/activities/types/GraphQLActivity'; +import { Activity } from '@/activities/types/Activity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFieldContext } from '@/object-record/hooks/useFieldContext'; import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope'; @@ -11,13 +10,7 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types import { IconArrowUpRight, IconPencil } from '@/ui/display/icon'; type ActivityTargetsInlineCellProps = { - activity?: Pick & { - activityTargets?: { - edges: Array<{ - node: Pick; - }> | null; - }; - }; + activity: Activity; }; export const ActivityTargetsInlineCell = ({ @@ -47,8 +40,8 @@ export const ActivityTargetsInlineCell = ({ IconLabel={IconArrowUpRight} editModeContent={ } label="Relations" diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts index ec0bdeb139..d3c52f1bf3 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache.ts @@ -1,19 +1,19 @@ -import { useApolloClient } from '@apollo/client'; - import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { getRecordConnectionFromEdges } from '@/object-record/cache/utils/getRecordConnectionFromEdges'; -import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; export const useInjectIntoActivityTargetInlineCellCache = () => { - const apolloClient = useApolloClient(); + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); const { + upsertFindManyRecordsQueryInCache: + overwriteFindManyActivityTargetsQueryInCache, + } = useUpsertFindManyRecordsQueryInCache({ objectMetadataItem: objectMetadataItemActivityTarget, - findManyRecordsQuery: findManyActivityTargetsQuery, - } = useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, }); const injectIntoActivityTargetInlineCellCache = ({ @@ -23,32 +23,17 @@ export const useInjectIntoActivityTargetInlineCellCache = () => { activityId: string; activityTargetsToInject: ActivityTarget[]; }) => { - const newActivityTargetEdgesForCache = activityTargetsToInject.map( - (activityTargetToInject) => - getRecordEdgeFromRecord({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - record: activityTargetToInject, - }), - ); - - const newActivityTargetConnectionForCache = getRecordConnectionFromEdges({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - edges: newActivityTargetEdgesForCache, - }); - - apolloClient.writeQuery({ - query: findManyActivityTargetsQuery, - variables: { - filter: { - activityId: { - eq: activityId, - }, + const activityTargetInlineCellQueryVariables = { + filter: { + activityId: { + eq: activityId, }, }, - data: { - [objectMetadataItemActivityTarget.namePlural]: - newActivityTargetConnectionForCache, - }, + }; + + overwriteFindManyActivityTargetsQueryInCache({ + queryVariables: activityTargetInlineCellQueryVariables, + objectRecordsToOverwrite: activityTargetsToInject, }); }; diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx index 7fb2abdd60..323546127a 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx @@ -4,7 +4,6 @@ import styled from '@emotion/styled'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; -import { GraphQLActivity } from '@/activities/types/GraphQLActivity'; import { Note } from '@/activities/types/Note'; import { getActivityPreview } from '@/activities/utils/getActivityPreview'; import { @@ -101,9 +100,7 @@ export const NoteCard = ({ {body} - + {note.comments && note.comments.length > 0 && ( diff --git a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts index de0f8ef378..546f97c81c 100644 --- a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts +++ b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts @@ -1,4 +1,4 @@ -import { useActivityTargets } from '@/activities/hooks/useActivityTargets'; +import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject'; import { Note } from '@/activities/types/Note'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { OrderByField } from '@/object-metadata/types/OrderByField'; @@ -7,7 +7,9 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity'; export const useNotes = (targetableObject: ActivityTargetableObject) => { - const { activityTargets } = useActivityTargets({ targetableObject }); + const { activityTargets } = useActivityTargetsForTargetableObject({ + targetableObject, + }); const filter = { id: { diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx index dad940eff2..7042e3050a 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx @@ -1,11 +1,16 @@ +import { useApolloClient } from '@apollo/client'; import { useRecoilState, useRecoilValue } from 'recoil'; +import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; +import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; +import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState'; import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { IconTrash } from '@/ui/display/icon'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; +import { isDefined } from '~/utils/isDefined'; export const ActivityActionBar = () => { const viewableActivityId = useRecoilValue(viewableActivityIdState); @@ -15,9 +20,27 @@ export const ActivityActionBar = () => { refetchFindManyQuery: true, }); + const [temporaryActivityForEditor, setTemporaryActivityForEditor] = + useRecoilState(temporaryActivityForEditorState); + + const { deleteActivityFromCache } = useDeleteActivityFromCache(); + + const [isCreatingActivity] = useRecoilState(isCreatingActivityState); + + const apolloClient = useApolloClient(); + const deleteActivity = () => { if (viewableActivityId) { - deleteOneActivity?.(viewableActivityId); + if (isCreatingActivity && isDefined(temporaryActivityForEditor)) { + deleteActivityFromCache(temporaryActivityForEditor); + setTemporaryActivityForEditor(null); + } else { + deleteOneActivity?.(viewableActivityId); + // TODO: find a better way to do this with custom optimistic rendering for activities + apolloClient.refetchQueries({ + include: ['FindManyActivities'], + }); + } } setIsRightDrawerOpen(false); diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx index 093323ef69..812222b42f 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx @@ -1,12 +1,7 @@ -import React from 'react'; import styled from '@emotion/styled'; -import { useSetRecoilState } from 'recoil'; import { ActivityEditor } from '@/activities/components/ActivityEditor'; -import { Activity } from '@/activities/types/Activity'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useActivityById } from '@/activities/hooks/useActivityById'; const StyledContainer = styled.div` box-sizing: border-box; @@ -21,23 +16,16 @@ const StyledContainer = styled.div` type RightDrawerActivityProps = { activityId: string; showComment?: boolean; - autoFillTitle?: boolean; + fillTitleFromBody?: boolean; }; export const RightDrawerActivity = ({ activityId, showComment = true, - autoFillTitle = false, + fillTitleFromBody = false, }: RightDrawerActivityProps) => { - const setEntityFields = useSetRecoilState(recordStoreFamilyState(activityId)); - - const { record: activity } = useFindOneRecord({ - objectNameSingular: CoreObjectNameSingular.Activity, - objectRecordId: activityId, - skip: !activityId, - onCompleted: (activity: Activity) => { - setEntityFields(activity ?? {}); - }, + const { activity } = useActivityById({ + activityId, }); if (!activity) { @@ -49,7 +37,7 @@ export const RightDrawerActivity = ({ ); diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx index b78d8093cf..fe6ac8ecaf 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx @@ -13,7 +13,7 @@ export const RightDrawerCreateActivity = () => { )} diff --git a/packages/twenty-front/src/modules/activities/states/activityTitleHasBeenSetFamilyState.ts b/packages/twenty-front/src/modules/activities/states/activityTitleHasBeenSetFamilyState.ts new file mode 100644 index 0000000000..096df500dd --- /dev/null +++ b/packages/twenty-front/src/modules/activities/states/activityTitleHasBeenSetFamilyState.ts @@ -0,0 +1,9 @@ +import { atomFamily } from 'recoil'; + +export const activityTitleHasBeenSetFamilyState = atomFamily< + boolean, + { activityId: string } +>({ + key: 'activityTitleHasBeenSetFamilyState', + default: false, +}); diff --git a/packages/twenty-front/src/modules/activities/states/isCreatingActivityState.ts b/packages/twenty-front/src/modules/activities/states/isCreatingActivityState.ts new file mode 100644 index 0000000000..91b0393734 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/states/isCreatingActivityState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isCreatingActivityState = atom({ + key: 'isCreatingActivityState', + default: false, +}); diff --git a/packages/twenty-front/src/modules/activities/states/temporaryActivityForEditorState.ts b/packages/twenty-front/src/modules/activities/states/temporaryActivityForEditorState.ts new file mode 100644 index 0000000000..5b657a3698 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/states/temporaryActivityForEditorState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +import { ActivityForEditor } from '@/activities/types/ActivityForEditor'; + +export const temporaryActivityForEditorState = atom({ + key: 'temporaryActivityForEditorState', + default: null, +}); diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivityCardFooter.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivityCardFooter.tsx deleted file mode 100644 index ab26b4cab9..0000000000 --- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivityCardFooter.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { isNonEmptyArray } from '@apollo/client/utilities'; -import styled from '@emotion/styled'; - -import CommentCounter from '@/activities/comment/CommentCounter'; -import { Activity } from '@/activities/types/Activity'; -import { UserChip } from '@/users/components/UserChip'; -import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; -import { beautifyExactDate } from '~/utils/date-utils'; - -type TimelineActivityCardFooterProps = { - activity: Pick & { - assignee?: Pick | null; - }; -}; - -const StyledContainer = styled.div` - align-items: center; - border-top: 1px solid ${({ theme }) => theme.border.color.medium}; - color: ${({ theme }) => theme.font.color.primary}; - display: flex; - flex-direction: row; - gap: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(2)}; - width: calc(100% - ${({ theme }) => theme.spacing(4)}); -`; - -const StyledVerticalSeparator = styled.div` - border-left: 1px solid ${({ theme }) => theme.border.color.medium}; - height: 24px; -`; - -const StyledComment = styled.div` - margin-left: auto; -`; -export const TimelineActivityCardFooter = ({ - activity, -}: TimelineActivityCardFooterProps) => { - const hasComments = isNonEmptyArray(activity.comments || []); - - return ( - <> - {(activity.assignee || activity.dueAt || hasComments) && ( - - {activity.assignee && ( - - )} - - {activity.dueAt && ( - <> - {activity.assignee && } - {beautifyExactDate(activity.dueAt)} - - )} - - {hasComments && ( - - )} - - - )} - - ); -}; diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx index b8542cd19f..e92623a285 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx @@ -1,7 +1,7 @@ import { useSetRecoilState } from 'recoil'; import { Button, ButtonGroup } from 'tsup.ui.index'; -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; +import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { IconCheckbox, @@ -19,7 +19,7 @@ export const TimelineCreateButtonGroup = ({ const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID); const setActiveTabId = useSetRecoilState(getActiveTabIdState()); - const openCreateActivity = useOpenCreateActivityDrawer(); + const openCreateActivity = useOpenCreateActivityDrawerV2(); return ( @@ -30,6 +30,7 @@ export const TimelineCreateButtonGroup = ({ openCreateActivity({ type: 'Note', targetableObjects: [targetableObject], + timelineTargetableObject: targetableObject, }) } /> @@ -40,6 +41,7 @@ export const TimelineCreateButtonGroup = ({ openCreateActivity({ type: 'Task', targetableObjects: [targetableObject], + timelineTargetableObject: targetableObject, }) } /> diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts deleted file mode 100644 index 3f15c968c3..0000000000 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useApolloClient } from '@apollo/client'; -import { isNonEmptyString } from '@sniptt/guards'; - -import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; -import { Activity } from '@/activities/types/Activity'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; -import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; - -export const useInjectIntoTimelineActivitiesQuery = () => { - const apolloClient = useApolloClient(); - - const { - objectMetadataItem: objectMetadataItemActivity, - findManyRecordsQuery: findManyActivitiesQuery, - } = useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); - - const injectIntoTimelineActivitiesQuery = ({ - activityTargets, - activityToInject, - }: { - activityTargets: ActivityTarget[]; - activityToInject: Activity; - }) => { - const activityIds = activityTargets - ?.map((activityTarget) => activityTarget.activityId) - .filter(isNonEmptyString); - - const timelineActivitiesQueryVariables = - makeTimelineActivitiesQueryVariables({ - activityIds, - }); - - const exitistingActivitiesQueryResult = apolloClient.readQuery({ - query: findManyActivitiesQuery, - variables: timelineActivitiesQueryVariables, - }); - - const extistingActivities = exitistingActivitiesQueryResult - ? getRecordsFromRecordConnection({ - recordConnection: exitistingActivitiesQueryResult[ - objectMetadataItemActivity.namePlural - ] as ObjectRecordConnection, - }) - : []; - - const newActivity = { - ...activityToInject, - __typename: 'Activity', - }; - - const newActivitiesSortedAsActivitiesQuery = [ - newActivity, - ...extistingActivities, - ]; - - const newActivityIdsSortedAsActivityTargetsQuery = [ - ...extistingActivities, - newActivity, - ].map((activity) => activity.id); - - const newTimelineActivitiesQueryVariables = - makeTimelineActivitiesQueryVariables({ - activityIds: newActivityIdsSortedAsActivityTargetsQuery, - }); - - const newActivityConnectionForCache = getRecordConnectionFromRecords({ - objectNameSingular: CoreObjectNameSingular.Activity, - records: newActivitiesSortedAsActivitiesQuery, - }); - - apolloClient.writeQuery({ - query: findManyActivitiesQuery, - variables: newTimelineActivitiesQueryVariables, - data: { - [objectMetadataItemActivity.namePlural]: newActivityConnectionForCache, - }, - }); - }; - - return { - injectIntoTimelineActivitiesNextQuery: injectIntoTimelineActivitiesQuery, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount.ts new file mode 100644 index 0000000000..2aeac4d5bb --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount.ts @@ -0,0 +1,124 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; +import { Activity } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; + +export const useInjectIntoTimelineActivitiesQueryAfterDrawerMount = () => { + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const { + upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache, + } = useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivity, + }); + + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { + readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, + } = useReadFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const { + readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache, + } = useReadFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivity, + }); + + const { + upsertFindManyRecordsQueryInCache: + overwriteFindManyActivityTargetsQueryInCache, + } = useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const injectIntoTimelineActivitiesQueryAfterDrawerMount = ({ + activityToInject, + activityTargetsToInject, + timelineTargetableObject, + }: { + activityToInject: Activity; + activityTargetsToInject: ActivityTarget[]; + timelineTargetableObject: ActivityTargetableObject; + }) => { + const newActivity = { + ...activityToInject, + __typename: 'Activity', + }; + + const targetObjectFieldName = getActivityTargetObjectFieldIdName({ + nameSingular: timelineTargetableObject.targetObjectNameSingular, + }); + + const activitiyTargetsForTargetableObjectQueryVariables = { + filter: { + [targetObjectFieldName]: { + eq: timelineTargetableObject.id, + }, + }, + }; + + const existingActivityTargetsForTargetableObject = + readFindManyActivityTargetsQueryInCache({ + queryVariables: activitiyTargetsForTargetableObjectQueryVariables, + }); + + const newActivityTargetsForTargetableObject = [ + ...existingActivityTargetsForTargetableObject, + ...activityTargetsToInject, + ]; + + const existingActivityIds = existingActivityTargetsForTargetableObject + ?.map((activityTarget) => activityTarget.activityId) + .filter(isNonEmptyString); + + const timelineActivitiesQueryVariablesBeforeDrawerMount = + makeTimelineActivitiesQueryVariables({ + activityIds: existingActivityIds, + }); + + const existingActivities = readFindManyActivitiesQueryInCache({ + queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount, + }); + + const activityIdsAfterDrawerMount = [ + ...existingActivityIds, + newActivity.id, + ]; + + const timelineActivitiesQueryVariablesAfterDrawerMount = + makeTimelineActivitiesQueryVariables({ + activityIds: activityIdsAfterDrawerMount, + }); + + overwriteFindManyActivityTargetsQueryInCache({ + objectRecordsToOverwrite: newActivityTargetsForTargetableObject, + queryVariables: activitiyTargetsForTargetableObjectQueryVariables, + }); + + const newActivities = [newActivity, ...existingActivities]; + + overwriteFindManyActivitiesInCache({ + objectRecordsToOverwrite: newActivities, + queryVariables: timelineActivitiesQueryVariablesAfterDrawerMount, + }); + }; + + return { + injectIntoTimelineActivitiesQueryAfterDrawerMount, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts index 675c1edcc6..ee0df89cb2 100644 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; -import { useActivityTargets } from '@/activities/hooks/useActivityTargets'; +import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject'; import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; import { Activity } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; @@ -17,14 +17,12 @@ export const useTimelineActivities = ({ activityTargets, loadingActivityTargets, initialized: initializedActivityTargets, - } = useActivityTargets({ + } = useActivityTargetsForTargetableObject({ targetableObject, }); const [initialized, setInitialized] = useState(false); - const [activities, setActivities] = useState([]); - const activityIds = activityTargets ?.map((activityTarget) => activityTarget.activityId) .filter(isNonEmptyString); @@ -35,7 +33,7 @@ export const useTimelineActivities = ({ }, ); - const { records: activitiesFromRequest, loading: loadingActivities } = + const { records: activities, loading: loadingActivities } = useFindManyRecords({ skip: loadingActivityTargets || !isNonEmptyArray(activityTargets), objectNameSingular: CoreObjectNameSingular.Activity, @@ -48,12 +46,6 @@ export const useTimelineActivities = ({ }, }); - useEffect(() => { - if (!loadingActivities) { - setActivities(activitiesFromRequest); - } - }, [activitiesFromRequest, loadingActivities]); - const noActivityTargets = initializedActivityTargets && !isNonEmptyArray(activityTargets); diff --git a/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts index 4cd8092d46..ae39da0bd9 100644 --- a/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts +++ b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts @@ -12,7 +12,7 @@ export const makeTimelineActivitiesQueryVariables = ({ }, }, orderBy: { - createdAt: 'AscNullsFirst', + createdAt: 'DescNullsFirst', }, }; }; diff --git a/packages/twenty-front/src/modules/activities/types/ActivityForEditor.ts b/packages/twenty-front/src/modules/activities/types/ActivityForEditor.ts new file mode 100644 index 0000000000..611f3794f2 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/types/ActivityForEditor.ts @@ -0,0 +1,20 @@ +import { Activity } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { Comment } from '@/activities/types/Comment'; +import { WorkspaceMember } from '~/generated-metadata/graphql'; + +export type ActivityForEditor = Pick< + Activity, + 'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt' | 'updatedAt' +> & { + comments?: Comment[]; +} & { + assignee?: Pick | null; +} & { + activityTargets?: Array< + Pick< + ActivityTarget, + 'id' | 'companyId' | 'personId' | 'createdAt' | 'updatedAt' | 'activity' + > + >; +}; diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTarget.ts b/packages/twenty-front/src/modules/activities/types/ActivityTarget.ts index 63240e01b5..b06cb6b39a 100644 --- a/packages/twenty-front/src/modules/activities/types/ActivityTarget.ts +++ b/packages/twenty-front/src/modules/activities/types/ActivityTarget.ts @@ -6,8 +6,8 @@ export type ActivityTarget = { id: string; createdAt: string; updatedAt: string; - companyId: string | null; - personId: string | null; + companyId?: string | null; + personId?: string | null; activity: Pick; person?: Pick | null; company?: Pick | null; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts index 34d87be327..fab80703c3 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts @@ -24,13 +24,17 @@ export const getActivityTargetsToCreateFromTargetableObjects = ({ nameSingular: targetableObject.targetObjectNameSingular, }); - return { + const activityTarget = { [targetableObject.targetObjectNameSingular]: targetableObject.targetObjectRecord, [targetableObjectFieldIdName]: targetableObject.id, activityId, id: v4(), - }; + updatedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + } as Partial; + + return activityTarget; }, ); diff --git a/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts b/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts new file mode 100644 index 0000000000..b0c6bacec1 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts @@ -0,0 +1,102 @@ +import { isNonEmptyArray } from '@apollo/client/utilities'; + +import { Activity } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { Comment } from '@/activities/types/Comment'; +import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo'; +import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; +import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; +import { isDefined } from '~/utils/isDefined'; + +export const useActivityConnectionUtils = () => { + const mapConnectionToRecords = useMapConnectionToRecords(); + + const makeActivityWithoutConnection = (activityWithConnections: any) => { + if (!isDefined(activityWithConnections)) { + return { activity: null }; + } + + const hasActivityTargetsConnection = isObjectRecordConnection( + CoreObjectNameSingular.ActivityTarget, + activityWithConnections?.activityTargets, + ); + + const activityTargets: ActivityTarget[] = []; + + if (hasActivityTargetsConnection) { + const newActivityTargets = mapConnectionToRecords({ + objectRecordConnection: activityWithConnections?.activityTargets, + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + depth: 5, + }) as ActivityTarget[]; + + activityTargets.push(...newActivityTargets); + } + + const hasCommentsConnection = isObjectRecordConnection( + CoreObjectNameSingular.Comment, + activityWithConnections?.comments, + ); + + const comments: Comment[] = []; + + if (hasCommentsConnection) { + const newComments = mapConnectionToRecords({ + objectRecordConnection: activityWithConnections?.comments, + objectNameSingular: CoreObjectNameSingular.Comment, + depth: 5, + }) as Comment[]; + + comments.push(...newComments); + } + + const activity: Activity = { + ...activityWithConnections, + activityTargets, + comments, + }; + + return { activity }; + }; + + const makeActivityWithConnection = (activity: Activity) => { + const activityTargetEdges = isNonEmptyArray(activity?.activityTargets) + ? activity.activityTargets.map((activityTarget) => ({ + node: activityTarget, + cursor: '', + })) + : []; + + const commentEdges = isNonEmptyArray(activity?.comments) + ? activity.comments.map((comment) => ({ + node: comment, + cursor: '', + })) + : []; + + const activityTargets = { + edges: activityTargetEdges, + pageInfo: getEmptyPageInfo(), + } as ObjectRecordConnection; + + const comments = { + edges: commentEdges, + pageInfo: getEmptyPageInfo(), + } as ObjectRecordConnection; + + const activityWithConnection = { + ...activity, + activityTargets, + comments, + }; + + return { activityWithConnection }; + }; + + return { + makeActivityWithoutConnection, + makeActivityWithConnection, + }; +}; diff --git a/packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts b/packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts index f3d7b981a5..b16bb88947 100644 --- a/packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts +++ b/packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts @@ -22,7 +22,7 @@ export const useCachedRootQuery = ({ const buildRecordFieldsFragment = () => { return objectMetadataItem.fields .filter((field) => field.type !== 'RELATION') - .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .map((field) => mapFieldMetadataToGraphQLQuery({ field })) .join(' \n'); }; diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/getRelationDefinition.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/getRelationDefinition.ts new file mode 100644 index 0000000000..a9f44909f6 --- /dev/null +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/getRelationDefinition.ts @@ -0,0 +1,62 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RelationType } from '@/settings/data-model/types/RelationType'; +import { + FieldMetadataType, + RelationMetadataType, +} from '~/generated-metadata/graphql'; + +export const getRelationDefinition = ({ + objectMetadataItems, + fieldMetadataItemOnSourceRecord, +}: { + objectMetadataItems: ObjectMetadataItem[]; + fieldMetadataItemOnSourceRecord: FieldMetadataItem; +}) => { + if (fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation) { + return null; + } + + const relationMetadataItem = + fieldMetadataItemOnSourceRecord.fromRelationMetadata || + fieldMetadataItemOnSourceRecord.toRelationMetadata; + + if (!relationMetadataItem) return null; + + const relationSourceFieldMetadataItemId = + 'toFieldMetadataId' in relationMetadataItem + ? relationMetadataItem.toFieldMetadataId + : relationMetadataItem.fromFieldMetadataId; + + if (!relationSourceFieldMetadataItemId) return null; + + // TODO: precise naming, is it relationTypeFromTargetPointOfView or relationTypeFromSourcePointOfView ? + const relationType = + relationMetadataItem.relationType === RelationMetadataType.OneToMany && + fieldMetadataItemOnSourceRecord.toRelationMetadata + ? ('MANY_TO_ONE' satisfies RelationType) + : (relationMetadataItem.relationType as RelationType); + + const targetObjectMetadataNameSingular = + 'toObjectMetadata' in relationMetadataItem + ? relationMetadataItem.toObjectMetadata.nameSingular + : relationMetadataItem.fromObjectMetadata.nameSingular; + + const targetObjectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === targetObjectMetadataNameSingular, + ); + + if (!targetObjectMetadataItem) return null; + + const fieldMetadataItemOnTargetRecord = targetObjectMetadataItem.fields.find( + (field) => field.id === relationSourceFieldMetadataItemId, + ); + + if (!fieldMetadataItemOnTargetRecord) return null; + + return { + fieldMetadataItemOnTargetRecord, + targetObjectMetadataItem, + relationType, + }; +}; diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts index 68ac70a345..ba99ce464e 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts @@ -2,57 +2,69 @@ import { ApolloCache, StoreObject } from '@apollo/client'; import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; +import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; export const triggerAttachRelationOptimisticEffect = ({ cache, - objectNameSingular, - recordId, - relationObjectMetadataNameSingular, - relationFieldName, - relationRecordId, + sourceObjectNameSingular, + sourceRecordId, + targetObjectNameSingular, + fieldNameOnTargetRecord, + targetRecordId, }: { cache: ApolloCache; - objectNameSingular: string; - recordId: string; - relationObjectMetadataNameSingular: string; - relationFieldName: string; - relationRecordId: string; + sourceObjectNameSingular: string; + sourceRecordId: string; + targetObjectNameSingular: string; + fieldNameOnTargetRecord: string; + targetRecordId: string; }) => { - const recordTypeName = capitalize(objectNameSingular); - const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular); + const sourceRecordTypeName = capitalize(sourceObjectNameSingular); + const targetRecordTypeName = capitalize(targetObjectNameSingular); + + const targetRecordCacheId = cache.identify({ + id: targetRecordId, + __typename: targetRecordTypeName, + }); cache.modify({ - id: cache.identify({ - id: relationRecordId, - __typename: relationRecordTypeName, - }), + id: targetRecordCacheId, fields: { - [relationFieldName]: (cachedFieldValue, { toReference }) => { - const nodeReference = toReference({ - id: recordId, - __typename: recordTypeName, + [fieldNameOnTargetRecord]: (targetRecordFieldValue, { toReference }) => { + const fieldValueIsCachedObjectRecordConnection = + isCachedObjectRecordConnection( + sourceObjectNameSingular, + targetRecordFieldValue, + ); + + const sourceRecordReference = toReference({ + id: sourceRecordId, + __typename: sourceRecordTypeName, }); - if (!nodeReference) return cachedFieldValue; + if (!isDefined(sourceRecordReference)) { + return targetRecordFieldValue; + } - if ( - isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue) - ) { - // To many objects => add record to next relation field list + if (fieldValueIsCachedObjectRecordConnection) { const nextEdges: CachedObjectRecordEdge[] = [ - ...cachedFieldValue.edges, + ...targetRecordFieldValue.edges, { - __typename: `${recordTypeName}Edge`, - node: nodeReference, + __typename: `${sourceRecordTypeName}Edge`, + node: sourceRecordReference, cursor: '', }, ]; - return { ...cachedFieldValue, edges: nextEdges }; - } - // To one object => attach next relation record - return nodeReference; + return { + ...targetRecordFieldValue, + edges: nextEdges, + }; + } else { + // To one object => attach next relation record + return sourceRecordReference; + } }, }, }); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts index 7fb85b0b19..5ed242589f 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect.ts @@ -4,9 +4,8 @@ import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { capitalize } from '~/utils/string/capitalize'; +import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; /* TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are. @@ -16,32 +15,32 @@ import { capitalize } from '~/utils/string/capitalize'; export const triggerCreateRecordsOptimisticEffect = ({ cache, objectMetadataItem, - records, - getRelationMetadata, + recordsToCreate, + objectMetadataItems, }: { cache: ApolloCache; objectMetadataItem: ObjectMetadataItem; - records: CachedObjectRecord[]; - getRelationMetadata: ReturnType; + recordsToCreate: CachedObjectRecord[]; + objectMetadataItems: ObjectMetadataItem[]; }) => { - const objectEdgeTypeName = `${capitalize( - objectMetadataItem.nameSingular, - )}Edge`; + const objectEdgeTypeName = getEdgeTypename({ + objectNameSingular: objectMetadataItem.nameSingular, + }); - records.forEach((record) => + recordsToCreate.forEach((record) => triggerUpdateRelationsOptimisticEffect({ cache, - objectMetadataItem, - previousRecord: null, - nextRecord: record, - getRelationMetadata, + sourceObjectMetadataItem: objectMetadataItem, + currentSourceRecord: null, + updatedSourceRecord: record, + objectMetadataItems, }), ); cache.modify({ fields: { [objectMetadataItem.namePlural]: ( - cachedConnection, + rootQueryCachedResponse, { DELETE: _DELETE, readField, @@ -49,42 +48,49 @@ export const triggerCreateRecordsOptimisticEffect = ({ toReference, }, ) => { - if ( - !isCachedObjectRecordConnection( - objectMetadataItem.nameSingular, - cachedConnection, - ) - ) - return cachedConnection; - - /* const { variables } = - parseApolloStoreFieldName( - storeFieldName, - ); */ - - const cachedEdges = readField( - 'edges', - cachedConnection, + const shouldSkip = !isCachedObjectRecordConnection( + objectMetadataItem.nameSingular, + rootQueryCachedResponse, ); - const nextCachedEdges = cachedEdges ? [...cachedEdges] : []; - const hasAddedRecords = records - .map((record) => { - /* const matchesFilter = - !variables?.filter || - isRecordMatchingFilter({ - record, - filter: variables.filter, - objectMetadataItem, - }); */ + if (shouldSkip) { + return rootQueryCachedResponse; + } - if (/* matchesFilter && */ record.id) { - const nodeReference = toReference(record); + const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse; - if (nodeReference) { - nextCachedEdges.unshift({ + const rootQueryCachedRecordEdges = readField( + 'edges', + rootQueryCachedObjectRecordConnection, + ); + const nextRootQueryCachedRecordEdges = rootQueryCachedRecordEdges + ? [...rootQueryCachedRecordEdges] + : []; + + const hasAddedRecords = recordsToCreate + .map((recordToCreate) => { + if (recordToCreate.id) { + const recordToCreateReference = toReference(recordToCreate); + + if (!recordToCreateReference) { + throw new Error( + `Failed to create reference for record with id: ${recordToCreate.id}`, + ); + } + + const recordAlreadyInCache = rootQueryCachedRecordEdges?.some( + (cachedEdge) => { + return ( + cache.identify(recordToCreateReference) === + cache.identify(cachedEdge.node) + ); + }, + ); + + if (recordToCreateReference && !recordAlreadyInCache) { + nextRootQueryCachedRecordEdges.unshift({ __typename: objectEdgeTypeName, - node: nodeReference, + node: recordToCreateReference, cursor: '', }); @@ -96,30 +102,14 @@ export const triggerCreateRecordsOptimisticEffect = ({ }) .some((hasAddedRecord) => hasAddedRecord); - if (!hasAddedRecords) return cachedConnection; - - /* if (variables?.orderBy) { - nextCachedEdges = sortCachedObjectEdges({ - edges: nextCachedEdges, - orderBy: variables.orderBy, - readCacheField: readField, - }); + if (!hasAddedRecords) { + return rootQueryCachedObjectRecordConnection; } - if (isDefined(variables?.first)) { - if ( - cachedEdges?.length === variables.first && - nextCachedEdges.length < variables.first - ) { - return DELETE; - } - - if (nextCachedEdges.length > variables.first) { - nextCachedEdges.splice(variables.first); - } - } */ - - return { ...cachedConnection, edges: nextCachedEdges }; + return { + ...rootQueryCachedObjectRecordConnection, + edges: nextRootQueryCachedRecordEdges, + }; }, }, }); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts index 0ee5020463..264f2c100d 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts @@ -5,7 +5,6 @@ import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effe import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isDefined } from '~/utils/isDefined'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; @@ -13,70 +12,79 @@ import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; export const triggerDeleteRecordsOptimisticEffect = ({ cache, objectMetadataItem, - records, - getRelationMetadata, + recordsToDelete, + objectMetadataItems, }: { cache: ApolloCache; objectMetadataItem: ObjectMetadataItem; - records: CachedObjectRecord[]; - getRelationMetadata: ReturnType; + recordsToDelete: CachedObjectRecord[]; + objectMetadataItems: ObjectMetadataItem[]; }) => { cache.modify({ fields: { [objectMetadataItem.namePlural]: ( - cachedConnection, + rootQueryCachedResponse, { DELETE, readField, storeFieldName }, ) => { - if ( + const rootQueryCachedResponseIsNotACachedObjectRecordConnection = !isCachedObjectRecordConnection( objectMetadataItem.nameSingular, - cachedConnection, - ) - ) { - return cachedConnection; + rootQueryCachedResponse, + ); + + if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) { + return rootQueryCachedResponse; } - const { variables } = + const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse; + + const { fieldArguments: rootQueryVariables } = parseApolloStoreFieldName( storeFieldName, ); - const recordIds = records.map(({ id }) => id); + const recordIdsToDelete = recordsToDelete.map(({ id }) => id); const cachedEdges = readField( 'edges', - cachedConnection, + rootQueryCachedObjectRecordConnection, ); + const nextCachedEdges = cachedEdges?.filter((cachedEdge) => { const nodeId = readField('id', cachedEdge.node); - return nodeId && !recordIds.includes(nodeId); + + return nodeId && !recordIdsToDelete.includes(nodeId); }) || []; if (nextCachedEdges.length === cachedEdges?.length) - return cachedConnection; + return rootQueryCachedObjectRecordConnection; + // TODO: same as in update, should we trigger DELETE ? if ( - isDefined(variables?.first) && - cachedEdges?.length === variables.first + isDefined(rootQueryVariables?.first) && + cachedEdges?.length === rootQueryVariables.first ) { return DELETE; } - return { ...cachedConnection, edges: nextCachedEdges }; + return { + ...rootQueryCachedObjectRecordConnection, + edges: nextCachedEdges, + }; }, }, }); - records.forEach((record) => { + recordsToDelete.forEach((recordToDelete) => { triggerUpdateRelationsOptimisticEffect({ cache, - objectMetadataItem, - previousRecord: record, - nextRecord: null, - getRelationMetadata, + sourceObjectMetadataItem: objectMetadataItem, + currentSourceRecord: recordToDelete, + updatedSourceRecord: null, + objectMetadataItems, }); - cache.evict({ id: cache.identify(record) }); + cache.evict({ id: cache.identify(recordToDelete) }); }); }; diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts index f68265920f..7a1196cf4e 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect.ts @@ -5,44 +5,60 @@ import { capitalize } from '~/utils/string/capitalize'; export const triggerDetachRelationOptimisticEffect = ({ cache, - objectNameSingular, - recordId, - relationObjectMetadataNameSingular, - relationFieldName, - relationRecordId, + sourceObjectNameSingular, + sourceRecordId, + targetObjectNameSingular, + fieldNameOnTargetRecord, + targetRecordId, }: { cache: ApolloCache; - objectNameSingular: string; - recordId: string; - relationObjectMetadataNameSingular: string; - relationFieldName: string; - relationRecordId: string; + sourceObjectNameSingular: string; + sourceRecordId: string; + targetObjectNameSingular: string; + fieldNameOnTargetRecord: string; + targetRecordId: string; }) => { - const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular); + const targetRecordTypeName = capitalize(targetObjectNameSingular); + + const targetRecordCacheId = cache.identify({ + id: targetRecordId, + __typename: targetRecordTypeName, + }); cache.modify({ - id: cache.identify({ - id: relationRecordId, - __typename: relationRecordTypeName, - }), + id: targetRecordCacheId, fields: { - [relationFieldName]: (cachedFieldValue, { isReference, readField }) => { - // To many objects => remove record from previous relation field list - if ( - isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue) - ) { - const nextEdges = cachedFieldValue.edges.filter( - ({ node }) => readField('id', node) !== recordId, + [fieldNameOnTargetRecord]: ( + targetRecordFieldValue, + { isReference, readField }, + ) => { + const isRelationTargetFieldAnObjectRecordConnection = + isCachedObjectRecordConnection( + sourceObjectNameSingular, + targetRecordFieldValue, ); - return { ...cachedFieldValue, edges: nextEdges }; + + if (isRelationTargetFieldAnObjectRecordConnection) { + const relationTargetFieldEdgesWithoutRelationSourceRecordToDetach = + targetRecordFieldValue.edges.filter( + ({ node }) => readField('id', node) !== sourceRecordId, + ); + + return { + ...targetRecordFieldValue, + edges: relationTargetFieldEdgesWithoutRelationSourceRecordToDetach, + }; } - // To one object => detach previous relation record - if (isReference(cachedFieldValue)) { + const isRelationTargetFieldASingleObjectRecord = isReference( + targetRecordFieldValue, + ); + + if (isRelationTargetFieldASingleObjectRecord) { return null; } - return cachedFieldValue; + return targetRecordFieldValue; }, }, }); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts index b83b4edbd5..11b69677b5 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts @@ -6,124 +6,185 @@ import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effe import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge'; import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; import { isDefined } from '~/utils/isDefined'; import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName'; -import { capitalize } from '~/utils/string/capitalize'; +// TODO: add extensive unit tests for this function +// That will also serve as documentation export const triggerUpdateRecordOptimisticEffect = ({ cache, objectMetadataItem, - previousRecord, - nextRecord, - getRelationMetadata, + currentRecord, + updatedRecord, + objectMetadataItems, }: { cache: ApolloCache; objectMetadataItem: ObjectMetadataItem; - previousRecord: CachedObjectRecord; - nextRecord: CachedObjectRecord; - getRelationMetadata: ReturnType; + currentRecord: CachedObjectRecord; + updatedRecord: CachedObjectRecord; + objectMetadataItems: ObjectMetadataItem[]; }) => { - const objectEdgeTypeName = `${capitalize( - objectMetadataItem.nameSingular, - )}Edge`; + const objectEdgeTypeName = getEdgeTypename({ + objectNameSingular: objectMetadataItem.nameSingular, + }); triggerUpdateRelationsOptimisticEffect({ cache, - objectMetadataItem, - previousRecord, - nextRecord, - getRelationMetadata, + sourceObjectMetadataItem: objectMetadataItem, + currentSourceRecord: currentRecord, + updatedSourceRecord: updatedRecord, + objectMetadataItems, }); - // Optimistically update record lists cache.modify({ fields: { [objectMetadataItem.namePlural]: ( - cachedConnection, + rootQueryCachedResponse, { DELETE, readField, storeFieldName, toReference }, ) => { - if ( + const rootQueryCachedResponseIsNotACachedObjectRecordConnection = !isCachedObjectRecordConnection( objectMetadataItem.nameSingular, - cachedConnection, - ) - ) - return cachedConnection; + rootQueryCachedResponse, + ); - const { variables } = + if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) { + return rootQueryCachedResponse; + } + + const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse; + + const { fieldArguments: rootQueryVariables } = parseApolloStoreFieldName( storeFieldName, ); - const cachedEdges = readField( - 'edges', - cachedConnection, - ); - let nextCachedEdges = cachedEdges ? [...cachedEdges] : []; + const rootQueryCurrentCachedRecordEdges = + readField( + 'edges', + rootQueryCachedObjectRecordConnection, + ) ?? []; - // Test if the record matches this list's filters - if (variables?.filter) { - const matchesFilter = isRecordMatchingFilter({ - record: nextRecord, - filter: variables.filter, - objectMetadataItem, - }); - const recordIndex = nextCachedEdges.findIndex( - (cachedEdge) => readField('id', cachedEdge.node) === nextRecord.id, - ); + let rootQueryNextCachedRecordEdges = [ + ...rootQueryCurrentCachedRecordEdges, + ]; - // If after update, the record matches this list's filters, then add it to the list - if (matchesFilter && recordIndex === -1) { - const nodeReference = toReference(nextRecord); - nodeReference && - nextCachedEdges.push({ + const rootQueryFilter = rootQueryVariables?.filter; + const rootQueryOrderBy = rootQueryVariables?.orderBy; + const rootQueryLimit = rootQueryVariables?.first; + + const shouldTestThatUpdatedRecordMatchesThisRootQueryFilter = + isDefined(rootQueryFilter); + + if (shouldTestThatUpdatedRecordMatchesThisRootQueryFilter) { + const updatedRecordMatchesThisRootQueryFilter = + isRecordMatchingFilter({ + record: updatedRecord, + filter: rootQueryFilter, + objectMetadataItem, + }); + + const updatedRecordIndexInRootQueryEdges = + rootQueryCurrentCachedRecordEdges.findIndex( + (cachedEdge) => + readField('id', cachedEdge.node) === updatedRecord.id, + ); + + const updatedRecordShouldBeAddedToRootQueryEdges = + updatedRecordMatchesThisRootQueryFilter && + updatedRecordIndexInRootQueryEdges === -1; + + const updatedRecordShouldBeRemovedFromRootQueryEdges = + updatedRecordMatchesThisRootQueryFilter && + updatedRecordIndexInRootQueryEdges === -1; + + if (updatedRecordShouldBeAddedToRootQueryEdges) { + const updatedRecordNodeReference = toReference(updatedRecord); + + if (isDefined(updatedRecordNodeReference)) { + rootQueryNextCachedRecordEdges.push({ __typename: objectEdgeTypeName, - node: nodeReference, + node: updatedRecordNodeReference, cursor: '', }); + } } - // If after update, the record does not match this list's filters anymore, then remove it from the list - if (!matchesFilter && recordIndex > -1) { - nextCachedEdges.splice(recordIndex, 1); + if (updatedRecordShouldBeRemovedFromRootQueryEdges) { + rootQueryNextCachedRecordEdges.splice( + updatedRecordIndexInRootQueryEdges, + 1, + ); } } - // Sort updated list - if (variables?.orderBy) { - nextCachedEdges = sortCachedObjectEdges({ - edges: nextCachedEdges, - orderBy: variables.orderBy, + const nextRootQueryEdgesShouldBeSorted = isDefined(rootQueryOrderBy); + + if (nextRootQueryEdgesShouldBeSorted) { + rootQueryNextCachedRecordEdges = sortCachedObjectEdges({ + edges: rootQueryNextCachedRecordEdges, + orderBy: rootQueryOrderBy, readCacheField: readField, }); } - // Limit the updated list to the required size - if (isDefined(variables?.first)) { + const shouldLimitNextRootQueryEdges = isDefined(rootQueryLimit); + + // TODO: not sure that we should trigger a DELETE here, as it will trigger a network request + // Is it the responsibility of this optimistic effect function to delete a root query that will trigger a network request ? + // Shouldn't we let the response from the network overwrite the cache and keep this util purely about cache updates ? + // + // Shoud we even apply the limit at all since with pagination we cannot really do optimistic rendering and should + // wait for the network response to update the cache + // + // Maybe we could apply a merging function instead and exclude limit from the caching field arguments ? + // Also we have a problem that is not yet present with this but may arise if we start + // to use limit arguments, as for now we rely on the hard coded limit of 60 in pg_graphql. + // This is as if we had a { limit: 60 } argument in every query but we don't. + // so Apollo correctly merges the return of fetchMore for now, because of this, + // but wouldn't do it well like Thomas had the problem with mail threads + // because he applied a limit of 2 and Apollo created one root query in the cache for each. + // In Thomas' case we should implement this because he use a hack to overwrite the first request with the return of the other. + // See: https://www.apollographql.com/docs/react/pagination/cursor-based/#relay-style-cursor-pagination + // See: https://www.apollographql.com/docs/react/pagination/core-api/#merging-queries + if (shouldLimitNextRootQueryEdges) { // If previous edges length was exactly at the required limit, // but after update next edges length is under the limit, // we cannot for sure know if re-fetching the query // would return more edges, so we cannot optimistically deduce // the query's result. // In this case, invalidate the cache entry so it can be re-fetched. - if ( - cachedEdges?.length === variables.first && - nextCachedEdges.length < variables.first - ) { + const rootQueryCurrentCachedRecordEdgesLengthIsAtLimit = + rootQueryCurrentCachedRecordEdges.length === rootQueryLimit; + + // If next edges length is under limit, then we can wait for the network response and merge the result + // then in the merge function we could implement this mechanism to limit the number of edges in the cache + const rootQueryNextCachedRecordEdgesLengthIsUnderLimit = + rootQueryNextCachedRecordEdges.length < rootQueryLimit; + + const shouldDeleteRootQuerySoItCanBeRefetched = + rootQueryCurrentCachedRecordEdgesLengthIsAtLimit && + rootQueryNextCachedRecordEdgesLengthIsUnderLimit; + + if (shouldDeleteRootQuerySoItCanBeRefetched) { return DELETE; } - // If next edges length exceeds the required limit, - // trim the next edges array to the correct length. - if (nextCachedEdges.length > variables.first) { - nextCachedEdges.splice(variables.first); + const rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit = + rootQueryNextCachedRecordEdges.length > rootQueryLimit; + + if (rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit) { + rootQueryNextCachedRecordEdges.splice(rootQueryLimit); } } - return { ...cachedConnection, edges: nextCachedEdges }; + return { + ...rootQueryCachedObjectRecordConnection, + edges: rootQueryNextCachedRecordEdges, + }; }, }, }); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts index f21652a653..6610ba3023 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts @@ -1,111 +1,147 @@ import { ApolloCache } from '@apollo/client'; +import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition'; import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection'; import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; -import { coreObjectNamesToDeleteOnRelationDetach } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; +import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH as CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDefined } from '~/utils/isDefined'; export const triggerUpdateRelationsOptimisticEffect = ({ cache, - objectMetadataItem, - previousRecord, - nextRecord, - getRelationMetadata, + sourceObjectMetadataItem, + currentSourceRecord, + updatedSourceRecord, + objectMetadataItems, }: { cache: ApolloCache; - objectMetadataItem: ObjectMetadataItem; - previousRecord: CachedObjectRecord | null; - nextRecord: CachedObjectRecord | null; - getRelationMetadata: ReturnType; + sourceObjectMetadataItem: ObjectMetadataItem; + currentSourceRecord: CachedObjectRecord | null; + updatedSourceRecord: CachedObjectRecord | null; + objectMetadataItems: ObjectMetadataItem[]; }) => - // Optimistically update relation records - objectMetadataItem.fields.forEach((fieldMetadataItem) => { - if (nextRecord && !(fieldMetadataItem.name in nextRecord)) return; + sourceObjectMetadataItem.fields.forEach((fieldMetadataItemOnSourceRecord) => { + const notARelationField = + fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation; - const relationMetadata = getRelationMetadata({ - fieldMetadataItem, + if (notARelationField) { + return; + } + + const fieldDoesNotExist = + isDefined(updatedSourceRecord) && + !(fieldMetadataItemOnSourceRecord.name in updatedSourceRecord); + + if (fieldDoesNotExist) { + return; + } + + const relationDefinition = getRelationDefinition({ + fieldMetadataItemOnSourceRecord, + objectMetadataItems, }); - if (!relationMetadata) return; + if (!relationDefinition) { + return; + } - const { - // Object metadata for the related record - relationObjectMetadataItem, - // Field on the related record - relationFieldMetadataItem, - } = relationMetadata; + const { targetObjectMetadataItem, fieldMetadataItemOnTargetRecord } = + relationDefinition; - const previousFieldValue: + const currentFieldValueOnSourceRecord: | ObjectRecordConnection | CachedObjectRecord - | null = previousRecord?.[fieldMetadataItem.name]; - const nextFieldValue: ObjectRecordConnection | CachedObjectRecord | null = - nextRecord?.[fieldMetadataItem.name]; + | null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name]; - if (isDeeplyEqual(previousFieldValue, nextFieldValue)) return; + const updatedFieldValueOnSourceRecord: + | ObjectRecordConnection + | CachedObjectRecord + | null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name]; - const isPreviousFieldValueRecordConnection = isObjectRecordConnection( - relationObjectMetadataItem.nameSingular, - previousFieldValue, - ); - const relationRecordsToDetach = isPreviousFieldValueRecordConnection - ? previousFieldValue.edges.map(({ node }) => node as CachedObjectRecord) - : [previousFieldValue].filter(isDefined); + if ( + isDeeplyEqual( + currentFieldValueOnSourceRecord, + updatedFieldValueOnSourceRecord, + ) + ) { + return; + } - const isNextFieldValueRecordConnection = isObjectRecordConnection( - relationObjectMetadataItem.nameSingular, - nextFieldValue, - ); - const relationRecordsToAttach = isNextFieldValueRecordConnection - ? nextFieldValue.edges.map(({ node }) => node as CachedObjectRecord) - : [nextFieldValue].filter(isDefined); + const currentFieldValueOnSourceRecordIsARecordConnection = + isObjectRecordConnection( + targetObjectMetadataItem.nameSingular, + currentFieldValueOnSourceRecord, + ); - if (previousRecord && relationRecordsToDetach.length) { - const shouldDeleteRelationRecord = - coreObjectNamesToDeleteOnRelationDetach.includes( - relationObjectMetadataItem.nameSingular as CoreObjectNameSingular, + const targetRecordsToDetachFrom = + currentFieldValueOnSourceRecordIsARecordConnection + ? currentFieldValueOnSourceRecord.edges.map( + ({ node }) => node as CachedObjectRecord, + ) + : [currentFieldValueOnSourceRecord].filter(isDefined); + + const updatedFieldValueOnSourceRecordIsARecordConnection = + isObjectRecordConnection( + targetObjectMetadataItem.nameSingular, + updatedFieldValueOnSourceRecord, + ); + + const targetRecordsToAttachTo = + updatedFieldValueOnSourceRecordIsARecordConnection + ? updatedFieldValueOnSourceRecord.edges.map( + ({ node }) => node as CachedObjectRecord, + ) + : [updatedFieldValueOnSourceRecord].filter(isDefined); + + const shouldDetachSourceFromAllTargets = + isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0; + + if (shouldDetachSourceFromAllTargets) { + const shouldStartByDeletingRelationTargetRecordsFromCache = + CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH.includes( + targetObjectMetadataItem.nameSingular as CoreObjectNameSingular, ); - if (shouldDeleteRelationRecord) { + if (shouldStartByDeletingRelationTargetRecordsFromCache) { triggerDeleteRecordsOptimisticEffect({ cache, - objectMetadataItem: relationObjectMetadataItem, - records: relationRecordsToDetach, - getRelationMetadata, + objectMetadataItem: targetObjectMetadataItem, + recordsToDelete: targetRecordsToDetachFrom, + objectMetadataItems, }); } else { - relationRecordsToDetach.forEach((relationRecordToDetach) => { + targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => { triggerDetachRelationOptimisticEffect({ cache, - objectNameSingular: objectMetadataItem.nameSingular, - recordId: previousRecord.id, - relationFieldName: relationFieldMetadataItem.name, - relationObjectMetadataNameSingular: - relationObjectMetadataItem.nameSingular, - relationRecordId: relationRecordToDetach.id, + sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular, + sourceRecordId: currentSourceRecord.id, + fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name, + targetObjectNameSingular: targetObjectMetadataItem.nameSingular, + targetRecordId: targetRecordToDetachFrom.id, }); }); } } - if (nextRecord && relationRecordsToAttach.length) { - relationRecordsToAttach.forEach((relationRecordToAttach) => + const shouldAttachSourceToAllTargets = + updatedSourceRecord && targetRecordsToAttachTo.length; + + if (shouldAttachSourceToAllTargets) { + targetRecordsToAttachTo.forEach((targetRecordToAttachTo) => triggerAttachRelationOptimisticEffect({ cache, - objectNameSingular: objectMetadataItem.nameSingular, - recordId: nextRecord.id, - relationFieldName: relationFieldMetadataItem.name, - relationObjectMetadataNameSingular: - relationObjectMetadataItem.nameSingular, - relationRecordId: relationRecordToAttach.id, + sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular, + sourceRecordId: updatedSourceRecord.id, + fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name, + targetObjectNameSingular: targetObjectMetadataItem.nameSingular, + targetRecordId: targetRecordToAttachTo.id, }), ); } diff --git a/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts b/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts index 5e40afc6ba..f28caf779c 100644 --- a/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts +++ b/packages/twenty-front/src/modules/apollo/types/coreObjectNamesToDeleteOnRelationDetach.ts @@ -1,5 +1,5 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -export const coreObjectNamesToDeleteOnRelationDetach = [ +export const CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH = [ CoreObjectNameSingular.Favorite, ]; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts index faa838d263..7c775d4ade 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts @@ -8,10 +8,15 @@ import { FieldMetadataItem } from '../types/FieldMetadataItem'; export const useMapFieldMetadataToGraphQLQuery = () => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const mapFieldMetadataToGraphQLQuery = ( - field: FieldMetadataItem, - maxDepthForRelations: number = 2, - ): any => { + const mapFieldMetadataToGraphQLQuery = ({ + field, + maxDepthForRelations = 2, + onlyTypenameAndIdOnDeepestRelationFields = false, + }: { + field: FieldMetadataItem; + maxDepthForRelations?: number; + onlyTypenameAndIdOnDeepestRelationFields?: boolean; + }): any => { if (maxDepthForRelations <= 0) { return ''; } @@ -45,14 +50,25 @@ export const useMapFieldMetadataToGraphQLQuery = () => { (field.toRelationMetadata as any)?.fromObjectMetadata?.id, ); + let subfieldQuery = ''; + + if (maxDepthForRelations > 0) { + subfieldQuery = `${(relationMetadataItem?.fields ?? []) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + maxDepthForRelations: maxDepthForRelations - 1, + onlyTypenameAndIdOnDeepestRelationFields, + }), + ) + .join('\n')}`; + } + return `${field.name} { + __typename id - ${(relationMetadataItem?.fields ?? []) - .map((field) => - mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1), - ) - .join('\n')} + ${subfieldQuery} }`; } else if ( fieldType === 'RELATION' && @@ -64,14 +80,25 @@ export const useMapFieldMetadataToGraphQLQuery = () => { (field.toRelationMetadata as any)?.fromObjectMetadata?.id, ); + let subfieldQuery = ''; + + if (maxDepthForRelations > 0) { + subfieldQuery = `${(relationMetadataItem?.fields ?? []) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + maxDepthForRelations: maxDepthForRelations - 1, + onlyTypenameAndIdOnDeepestRelationFields, + }), + ) + .join('\n')}`; + } + return `${field.name} { + __typename id - ${(relationMetadataItem?.fields ?? []) - .map((field) => - mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1), - ) - .join('\n')} + ${subfieldQuery} }`; } else if ( fieldType === 'RELATION' && @@ -83,16 +110,27 @@ export const useMapFieldMetadataToGraphQLQuery = () => { (field.fromRelationMetadata as any)?.toObjectMetadata?.id, ); + let subfieldQuery = ''; + + if (maxDepthForRelations > 0) { + subfieldQuery = `${(relationMetadataItem?.fields ?? []) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + maxDepthForRelations: maxDepthForRelations - 1, + onlyTypenameAndIdOnDeepestRelationFields, + }), + ) + .join('\n')}`; + } + return `${field.name} { edges { node { + __typename id - ${(relationMetadataItem?.fields ?? []) - .map((field) => - mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1), - ) - .join('\n')} + ${subfieldQuery} } } }`; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts new file mode 100644 index 0000000000..215d61200c --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts @@ -0,0 +1,11 @@ +import { useRecoilValue } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; + +export const useObjectMetadataItems = () => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + return { + objectMetadataItems, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemBySingularName.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemBySingularName.ts new file mode 100644 index 0000000000..fcc77b9e05 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemBySingularName.ts @@ -0,0 +1,22 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const getObjectMetadataItemByNameSingular = ({ + objectMetadataItems, + objectNameSingular, +}: { + objectMetadataItems: ObjectMetadataItem[]; + objectNameSingular: string; +}) => { + const foundObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === objectNameSingular, + ); + + if (!foundObjectMetadataItem) { + throw new Error( + `Could not find object metadata item with singular name ${objectNameSingular}`, + ); + } + + return foundObjectMetadataItem; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts index f0217bc3ce..36c8b7be7e 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts @@ -17,12 +17,19 @@ export const getObjectRecordIdentifier = ({ }): ObjectRecordIdentifier => { switch (objectMetadataItem.nameSingular) { case CoreObjectNameSingular.WorkspaceMember: { - const workspaceMember = record as WorkspaceMember; + const workspaceMember = record as Partial & { + id: string; + }; + + const name = workspaceMember.name + ? `${workspaceMember.name?.firstName ?? ''} ${ + workspaceMember.name?.lastName ?? '' + }` + : ''; return { id: workspaceMember.id, - name: - workspaceMember.name.firstName + ' ' + workspaceMember.name.lastName, + name, avatarUrl: workspaceMember.avatarUrl ?? undefined, avatarType: 'rounded', }; diff --git a/packages/twenty-front/src/modules/object-record/cache/constants/MaxQueryDepthForCacheInjection.ts b/packages/twenty-front/src/modules/object-record/cache/constants/MaxQueryDepthForCacheInjection.ts new file mode 100644 index 0000000000..e474d8a2ef --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/constants/MaxQueryDepthForCacheInjection.ts @@ -0,0 +1 @@ +export const MAX_QUERY_DEPTH_FOR_CACHE_INJECTION = 1; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts index 7e1293a159..afc2af6e5b 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts @@ -4,7 +4,8 @@ import { useRecoilCallback } from 'recoil'; import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery'; +import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection'; +import { useInjectIntoFindOneRecordQueryCache } from '@/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { capitalize } from '~/utils/string/capitalize'; @@ -17,47 +18,44 @@ export const useAddRecordInCache = ({ const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery(); const apolloClient = useApolloClient(); - const generateFindOneRecordQuery = useGenerateFindOneRecordQuery(); - - const findOneRecordQuery = generateFindOneRecordQuery({ - objectMetadataItem, - }); + const { injectIntoFindOneRecordQueryCache } = + useInjectIntoFindOneRecordQueryCache({ + objectMetadataItem, + }); return useRecoilCallback( ({ set }) => (record: ObjectRecord) => { + const fragment = gql` + fragment Create${capitalize( + objectMetadataItem.nameSingular, + )}InCache on ${capitalize(objectMetadataItem.nameSingular)} { + __typename + id + ${objectMetadataItem.fields + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + maxDepthForRelations: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, + }), + ) + .join('\n')} + } + `; + + const cachedObjectRecord = { + __typename: `${capitalize(objectMetadataItem.nameSingular)}`, + ...record, + }; + apolloClient.writeFragment({ id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`, - fragment: gql` - fragment Create${capitalize( - objectMetadataItem.nameSingular, - )}InCache on ${capitalize(objectMetadataItem.nameSingular)} { - __typename - id - ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field)) - .join('\n')} - } - `, - data: { - __typename: `${capitalize(objectMetadataItem.nameSingular)}`, - ...record, - }, + fragment, + data: cachedObjectRecord, }); - // TODO: Turn into injectIntoFindOneRecordQueryCache - apolloClient.writeQuery({ - query: findOneRecordQuery, - variables: { - objectRecordId: record.id, - }, - data: { - [objectMetadataItem.nameSingular]: { - __typename: `${capitalize(objectMetadataItem.nameSingular)}`, - ...record, - }, - }, - }); + // TODO: should we keep this here ? Or should the caller of createOneRecordInCache/createManyRecordsInCache be responsible for this ? + injectIntoFindOneRecordQueryCache(cachedObjectRecord); // TODO: remove this once we get rid of entityFieldsFamilyState set(recordStoreFamilyState(record.id), record); @@ -66,7 +64,7 @@ export const useAddRecordInCache = ({ objectMetadataItem, apolloClient, mapFieldMetadataToGraphQLQuery, - findOneRecordQuery, + injectIntoFindOneRecordQueryCache, ], ); }; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts new file mode 100644 index 0000000000..a7fe89f4da --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useAppendToFindManyRecordsQueryInCache.ts @@ -0,0 +1,50 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export const useAppendToFindManyRecordsQueryInCache = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { readFindManyRecordsQueryInCache } = + useReadFindManyRecordsQueryInCache({ + objectMetadataItem, + }); + + const { + upsertFindManyRecordsQueryInCache: overwriteFindManyRecordsQueryInCache, + } = useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem, + }); + + const appendToFindManyRecordsQueryInCache = < + T extends ObjectRecord = ObjectRecord, + >({ + queryVariables, + objectRecordsToAppend, + }: { + queryVariables: ObjectRecordQueryVariables; + objectRecordsToAppend: T[]; + }) => { + const existingObjectRecords = readFindManyRecordsQueryInCache({ + queryVariables, + }); + + const newObjectRecordList = [ + ...existingObjectRecords, + ...objectRecordsToAppend, + ]; + + overwriteFindManyRecordsQueryInCache({ + objectRecordsToOverwrite: newObjectRecordList, + queryVariables, + }); + }; + + return { + appendToFindManyRecordsQueryInCache, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts index a81109367b..7253e25f46 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts @@ -27,7 +27,7 @@ export const useGetRecordFromCache = ({ fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} { id ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .map((field) => mapFieldMetadataToGraphQLQuery({ field })) .join('\n')} } `; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts new file mode 100644 index 0000000000..9e153f9913 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache.ts @@ -0,0 +1,44 @@ +import { useApolloClient } from '@apollo/client'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { capitalize } from '~/utils/string/capitalize'; + +export const useInjectIntoFindOneRecordQueryCache = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const apolloClient = useApolloClient(); + + const generateFindOneRecordQuery = useGenerateFindOneRecordQuery(); + + const injectIntoFindOneRecordQueryCache = < + T extends ObjectRecord = ObjectRecord, + >( + record: T, + ) => { + const findOneRecordQueryForCacheInjection = generateFindOneRecordQuery({ + objectMetadataItem, + depth: 1, + }); + + apolloClient.writeQuery({ + query: findOneRecordQueryForCacheInjection, + variables: { + objectRecordId: record.id, + }, + data: { + [objectMetadataItem.nameSingular]: { + __typename: `${capitalize(objectMetadataItem.nameSingular)}`, + ...record, + }, + }, + }); + }; + + return { + injectIntoFindOneRecordQueryCache, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts new file mode 100644 index 0000000000..4b0bc978eb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useReadFindManyRecordsQueryInCache.ts @@ -0,0 +1,53 @@ +import { useApolloClient } from '@apollo/client'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordQueryResult } from '@/object-record/types/ObjectRecordQueryResult'; +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; +import { isDefined } from '~/utils/isDefined'; + +export const useReadFindManyRecordsQueryInCache = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const apolloClient = useApolloClient(); + + const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery(); + + const readFindManyRecordsQueryInCache = < + T extends ObjectRecord = ObjectRecord, + >({ + queryVariables, + }: { + queryVariables: ObjectRecordQueryVariables; + }) => { + const findManyRecordsQueryForCacheRead = generateFindManyRecordsQuery({ + objectMetadataItem, + }); + + const existingRecordsQueryResult = apolloClient.readQuery< + ObjectRecordQueryResult + >({ + query: findManyRecordsQueryForCacheRead, + variables: queryVariables, + }); + + const existingRecordConnection = + existingRecordsQueryResult?.[objectMetadataItem.namePlural]; + + const existingObjectRecords = isDefined(existingRecordConnection) + ? getRecordsFromRecordConnection({ + recordConnection: existingRecordConnection, + }) + : []; + + return existingObjectRecords; + }; + + return { + readFindManyRecordsQueryInCache, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts new file mode 100644 index 0000000000..d519428f4b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts @@ -0,0 +1,51 @@ +import { useApolloClient } from '@apollo/client'; + +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection'; +import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; +import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export const useUpsertFindManyRecordsQueryInCache = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const apolloClient = useApolloClient(); + + const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery(); + + const upsertFindManyRecordsQueryInCache = < + T extends ObjectRecord = ObjectRecord, + >({ + queryVariables, + objectRecordsToOverwrite, + }: { + queryVariables: ObjectRecordQueryVariables; + objectRecordsToOverwrite: T[]; + }) => { + const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({ + objectMetadataItem, + depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, + }); + + const newObjectRecordConnection = getRecordConnectionFromRecords({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + records: objectRecordsToOverwrite, + }); + + apolloClient.writeQuery({ + query: findManyRecordsQueryForCacheOverwrite, + variables: queryVariables, + data: { + [objectMetadataItem.namePlural]: newObjectRecordConnection, + }, + }); + }; + + return { + upsertFindManyRecordsQueryInCache, + }; +}; 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 5aa32ceab6..5e133b8688 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -2,14 +2,18 @@ import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; +type CreateManyRecordsOptions = { + skipOptimisticEffect?: boolean; +}; + export const useCreateManyRecords = < CreatedObjectRecord extends ObjectRecord = ObjectRecord, >({ @@ -27,15 +31,22 @@ export const useCreateManyRecords = < objectMetadataItem, }); - const getRelationMetadata = useGetRelationMetadata(); + const { objectMetadataItems } = useObjectMetadataItems(); - const createManyRecords = async (data: Partial[]) => { - const sanitizedCreateManyRecordsInput = data.map((input) => - sanitizeRecordInput({ + const createManyRecords = async ( + data: Partial[], + options?: CreateManyRecordsOptions, + ) => { + const sanitizedCreateManyRecordsInput = data.map((input) => { + const idForCreation = input.id ?? v4(); + + const sanitizedRecordInput = sanitizeRecordInput({ objectMetadataItem, - recordInput: { ...input, id: v4() }, - }), - ); + recordInput: { ...input, id: idForCreation }, + }); + + return sanitizedRecordInput; + }); const optimisticallyCreatedRecords = sanitizedCreateManyRecordsInput.map( (record) => @@ -51,21 +62,25 @@ export const useCreateManyRecords = < variables: { data: sanitizedCreateManyRecordsInput, }, - optimisticResponse: { - [mutationResponseField]: optimisticallyCreatedRecords, - }, - update: (cache, { data }) => { - const records = data?.[mutationResponseField]; + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: optimisticallyCreatedRecords, + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; - if (!records?.length) return; + if (!records?.length) return; - triggerCreateRecordsOptimisticEffect({ - cache, - objectMetadataItem, - records, - getRelationMetadata, - }); - }, + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: records, + objectMetadataItems, + }); + }, }); return createdObjects.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts index f1f131c444..426e4176f9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecordsInCache.ts @@ -22,7 +22,7 @@ export const useCreateManyRecordsInCache = ({ objectMetadataItem, }); - const createManyRecordsInCache = async (data: Partial[]) => { + const createManyRecordsInCache = (data: Partial[]) => { const recordsWithId = data.map((record) => ({ ...record, id: (record.id as string) ?? v4(), 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 33f5d4a208..938f848a89 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -2,8 +2,8 @@ import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; @@ -13,6 +13,10 @@ type useCreateOneRecordProps = { objectNameSingular: string; }; +type CreateOneRecordOptions = { + skipOptimisticEffect?: boolean; +}; + export const useCreateOneRecord = < CreatedObjectRecord extends ObjectRecord = ObjectRecord, >({ @@ -29,12 +33,17 @@ export const useCreateOneRecord = < objectMetadataItem, }); - const getRelationMetadata = useGetRelationMetadata(); + const { objectMetadataItems } = useObjectMetadataItems(); + + const createOneRecord = async ( + input: Partial, + options?: CreateOneRecordOptions, + ) => { + const idForCreation = input.id ?? v4(); - const createOneRecord = async (input: Partial) => { const sanitizedCreateOneRecordInput = sanitizeRecordInput({ objectMetadataItem, - recordInput: { ...input, id: v4() }, + recordInput: { ...input, id: idForCreation }, }); const optimisticallyCreatedRecord = @@ -51,21 +60,25 @@ export const useCreateOneRecord = < variables: { input: sanitizedCreateOneRecordInput, }, - optimisticResponse: { - [mutationResponseField]: optimisticallyCreatedRecord, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: optimisticallyCreatedRecord, + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const record = data?.[mutationResponseField]; - if (!record) return; + if (!record) return; - triggerCreateRecordsOptimisticEffect({ - cache, - objectMetadataItem, - records: [record], - getRelationMetadata, - }); - }, + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: [record], + objectMetadataItems, + }); + }, }); return createdObject.data?.[mutationResponseField] ?? null; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts index 358084a28a..ebd22838c4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecordInCache.ts @@ -23,7 +23,7 @@ export const useCreateOneRecordInCache = ({ objectMetadataItem, }); - const createOneRecordInCache = async (input: ObjectRecord) => { + const createOneRecordInCache = (input: ObjectRecord) => { const generatedCachedObjectRecord = generateObjectRecordOptimisticResponse(input); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index fc0e8161a8..b055e0dca4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -1,8 +1,8 @@ import { useApolloClient } from '@apollo/client'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; @@ -20,7 +20,7 @@ export const useDeleteManyRecords = ({ const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } = useObjectMetadataItem({ objectNameSingular }); - const getRelationMetadata = useGetRelationMetadata(); + const { objectMetadataItems } = useObjectMetadataItems(); const mutationResponseField = getDeleteManyRecordsMutationResponseField( objectMetadataItem.namePlural, @@ -50,8 +50,8 @@ export const useDeleteManyRecords = ({ triggerDeleteRecordsOptimisticEffect({ cache, objectMetadataItem, - records: cachedRecords, - getRelationMetadata, + recordsToDelete: cachedRecords, + objectMetadataItems, }); }, }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index 14ce35a421..e43a30e85a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -2,8 +2,8 @@ import { useCallback } from 'react'; import { useApolloClient } from '@apollo/client'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation'; import { capitalize } from '~/utils/string/capitalize'; @@ -20,7 +20,7 @@ export const useDeleteOneRecord = ({ const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } = useObjectMetadataItem({ objectNameSingular }); - const getRelationMetadata = useGetRelationMetadata(); + const { objectMetadataItems } = useObjectMetadataItems(); const mutationResponseField = getDeleteOneRecordMutationResponseField(objectNameSingular); @@ -48,8 +48,8 @@ export const useDeleteOneRecord = ({ triggerDeleteRecordsOptimisticEffect({ cache, objectMetadataItem, - records: [cachedRecord], - getRelationMetadata, + recordsToDelete: [cachedRecord], + objectMetadataItems, }); }, }); @@ -60,10 +60,10 @@ export const useDeleteOneRecord = ({ apolloClient, deleteOneRecordMutation, getRecordFromCache, - getRelationMetadata, mutationResponseField, objectMetadataItem, objectNameSingular, + objectMetadataItems, ], ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFieldContext.tsx b/packages/twenty-front/src/modules/object-record/hooks/useFieldContext.tsx index c3f6b68b10..3429c83406 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFieldContext.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/useFieldContext.tsx @@ -17,6 +17,7 @@ export const useFieldContext = ({ isLabelIdentifier = false, objectNameSingular, objectRecordId, + customUseUpdateOneObjectHook, }: { clearable?: boolean; fieldMetadataName: string; @@ -24,6 +25,7 @@ export const useFieldContext = ({ isLabelIdentifier?: boolean; objectNameSingular: string; objectRecordId: string; + customUseUpdateOneObjectHook?: RecordUpdateHook; }) => { const { basePathToShowPage, objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -65,7 +67,8 @@ export const useFieldContext = ({ position: fieldPosition, objectMetadataItem, }), - useUpdateRecord: useUpdateOneObjectMutation, + useUpdateRecord: + customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation, hotkeyScope: InlineCellHotkeyScope.InlineCell, clearable, }} diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts index 9382df8bae..7b8e81d115 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindOneRecord.ts @@ -2,10 +2,9 @@ import { useQuery } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -export const useFindOneRecord = < - ObjectType extends { id: string } & Record, ->({ +export const useFindOneRecord = ({ objectNameSingular, objectRecordId = '', onCompleted, @@ -13,7 +12,7 @@ export const useFindOneRecord = < skip, }: ObjectMetadataItemIdentifier & { objectRecordId: string | undefined; - onCompleted?: (data: ObjectType) => void; + onCompleted?: (data: T) => void; skip?: boolean; depth?: number; }) => { @@ -23,7 +22,7 @@ export const useFindOneRecord = < ); const { data, loading, error } = useQuery< - { [nameSingular: string]: ObjectType }, + { [nameSingular: string]: T }, { objectRecordId: string } >(findOneRecordQuery, { skip: !objectMetadataItem || !objectRecordId || skip, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts index 4a11ac7873..1f9a92b497 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts @@ -31,7 +31,11 @@ export const useGenerateCreateManyRecordMutation = ({ ${mutationResponseField}(data: $data) { id ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + }), + ) .join('\n')} } }`; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts index 6f3a1a0112..cd607c83fa 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts @@ -31,7 +31,11 @@ export const useGenerateCreateOneRecordMutation = ({ ${mutationResponseField}(data: $input) { id ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + }), + ) .join('\n')} } } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts index 3b8ca0e101..743eee38f6 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts @@ -36,7 +36,11 @@ export const useGenerateExecuteQuickActionOnOneRecordMutation = ({ ${graphQLFieldForExecuteQuickActionOnOneRecordMutation}(id: $idToExecuteQuickActionOn) { id ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + }), + ) .join('\n')} } } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts index fa95b5e5c7..3ae1bf241e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts @@ -71,7 +71,12 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({ node { id ${fields - .map((field) => mapFieldMetadataToGraphQLQuery(field, depth)) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + maxDepthForRelations: depth, + }), + ) .join('\n')} } cursor diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts index 9c910df821..5fb8d84718 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts @@ -28,7 +28,12 @@ export const useGenerateFindManyRecordsQuery = () => { node { id ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field, depth)) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + maxDepthForRelations: depth, + }), + ) .join('\n')} } cursor diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts index 46259719df..aee59872ab 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts @@ -12,8 +12,8 @@ export const useGenerateFindOneRecordQuery = () => { }: { objectMetadataItem: Pick; depth?: number; - }) => - gql` + }) => { + return gql` query FindOne${objectMetadataItem.nameSingular}($objectRecordId: UUID!) { ${objectMetadataItem.nameSingular}(filter: { id: { @@ -22,9 +22,15 @@ export const useGenerateFindOneRecordQuery = () => { }){ id ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field, depth)) + .map((field) => + mapFieldMetadataToGraphQLQuery({ + field, + maxDepthForRelations: depth, + }), + ) .join('\n')} } } - `; + `; + }; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts index ecb38188b6..8a00f1beef 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts @@ -31,7 +31,7 @@ export const useGenerateUpdateOneRecordMutation = ({ ${mutationResponseField}(id: $idToUpdate, data: $input) { id ${objectMetadataItem.fields - .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .map((field) => mapFieldMetadataToGraphQLQuery({ field })) .join('\n')} } } 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 031ed2c4c0..c76431c24c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -1,8 +1,8 @@ import { useApolloClient } from '@apollo/client'; import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; @@ -27,7 +27,7 @@ export const useUpdateOneRecord = < objectMetadataItem, }); - const getRelationMetadata = useGetRelationMetadata(); + const { objectMetadataItems } = useObjectMetadataItems(); const updateOneRecord = async ({ idToUpdate, @@ -69,9 +69,9 @@ export const useUpdateOneRecord = < triggerUpdateRecordOptimisticEffect({ cache, objectMetadataItem, - previousRecord: cachedRecord, - nextRecord: record, - getRelationMetadata, + currentRecord: cachedRecord, + updatedRecord: record, + objectMetadataItems, }); }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board-deprecated/components/RecordBoardDeprecatedColumnEditTitleMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board-deprecated/components/RecordBoardDeprecatedColumnEditTitleMenu.tsx index d753d727b3..31e0abe4f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-board-deprecated/components/RecordBoardDeprecatedColumnEditTitleMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board-deprecated/components/RecordBoardDeprecatedColumnEditTitleMenu.tsx @@ -1,5 +1,6 @@ import { ChangeEvent, useCallback, useContext, useState } from 'react'; import styled from '@emotion/styled'; +import debounce from 'lodash.debounce'; import { IconTrash } from '@/ui/display/icon'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -8,7 +9,6 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor'; import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors'; import { textInputStyle } from '@/ui/theme/constants/effects'; -import { debounce } from '~/utils/debounce'; import { BoardColumnContext } from '../contexts/BoardColumnContext'; import { useRecordBoardDeprecated } from '../hooks/useRecordBoardDeprecated'; 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 40ff109cb8..1275379afd 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -36,7 +36,7 @@ export const generateEmptyFieldValue = ( return null; } case FieldMetadataType.Uuid: { - return ''; + return null; } case FieldMetadataType.Boolean: { return true; diff --git a/packages/twenty-front/src/modules/object-record/utils/mapEdgeToObjectRecord.ts b/packages/twenty-front/src/modules/object-record/utils/mapEdgeToObjectRecord.ts new file mode 100644 index 0000000000..0bb1d52e74 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/mapEdgeToObjectRecord.ts @@ -0,0 +1,8 @@ +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; + +export const mapEdgeToObjectRecord = ( + objectRecordEdge: ObjectRecordEdge, +) => { + return objectRecordEdge.node as T; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index 8d5419819b..445039aa75 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -58,7 +58,9 @@ export const RightDrawer = () => { useListenClickOutside({ refs: [rightDrawerRef], - callback: () => closeRightDrawer(), + callback: () => { + closeRightDrawer(); + }, mode: ClickOutsideMode.comparePixels, }); diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx index b0b3daa7ef..08812ccbb8 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; +import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2'; import { ActivityType } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; @@ -19,17 +19,19 @@ const StyledContainer = styled.div` `; export const ShowPageAddButton = ({ - entity, + activityTargetObject, }: { - entity: ActivityTargetableObject; + activityTargetObject: ActivityTargetableObject; }) => { - const { closeDropdown, toggleDropdown } = useDropdown( - SHOW_PAGE_ADD_BUTTON_DROPDOWN_ID, - ); - const openCreateActivity = useOpenCreateActivityDrawer(); + const { closeDropdown, toggleDropdown } = useDropdown('add-show-page'); + const openCreateActivity = useOpenCreateActivityDrawerV2(); const handleSelect = (type: ActivityType) => { - openCreateActivity({ type, targetableObjects: [entity] }); + openCreateActivity({ + type, + targetableObjects: [activityTargetObject], + timelineTargetableObject: activityTargetObject, + }); closeDropdown(); }; diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts index 50c598ea0b..9ca424aef7 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts @@ -1,3 +1,4 @@ +import { clickOutsideListenerCallbacksStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksStateScopeMap'; import { clickOutsideListenerIsActivatedStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedStateScopeMap'; import { clickOutsideListenerIsMouseDownInsideStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsMouseDownInsideStateScopeMap'; import { lockedListenerIdState } from '@/ui/utilities/pointer-event/states/lockedListenerIdState'; @@ -9,6 +10,10 @@ export const useClickOustideListenerStates = (componentId: string) => { return { scopeId, + getClickOutsideListenerCallbacksState: getState( + clickOutsideListenerCallbacksStateScopeMap, + scopeId, + ), getClickOutsideListenerIsMouseDownInsideState: getState( clickOutsideListenerIsMouseDownInsideStateScopeMap, scopeId, diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts index 9abfee1536..e1db02e483 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useRecoilCallback } from 'recoil'; import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates'; @@ -5,14 +6,18 @@ import { ClickOutsideListenerProps, useListenClickOutsideV2, } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; +import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; +import { isDefined } from '~/utils/isDefined'; export const useClickOutsideListener = (componentId: string) => { // TODO: improve typing const scopeId = getScopeIdFromComponentId(componentId) ?? ''; - const { getClickOutsideListenerIsActivatedState } = - useClickOustideListenerStates(componentId); + const { + getClickOutsideListenerIsActivatedState, + getClickOutsideListenerCallbacksState, + } = useClickOustideListenerStates(componentId); const useListenClickOutside = ({ callback, @@ -23,7 +28,21 @@ export const useClickOutsideListener = (componentId: string) => { return useListenClickOutsideV2({ listenerId: componentId, refs, - callback, + callback: useRecoilCallback( + ({ snapshot }) => + (event) => { + callback(event); + + const additionalCallbacks = snapshot + .getLoadable(getClickOutsideListenerCallbacksState()) + .getValue(); + + additionalCallbacks.forEach((additionalCallback) => { + additionalCallback.callbackFunction(event); + }); + }, + [callback], + ), enabled, mode, }); @@ -37,9 +56,97 @@ export const useClickOutsideListener = (componentId: string) => { [getClickOutsideListenerIsActivatedState], ); + const registerOnClickOutsideCallback = useRecoilCallback( + ({ set, snapshot }) => + ({ callbackFunction, callbackId }: ClickOutsideListenerCallback) => { + const existingCallbacks = snapshot + .getLoadable(getClickOutsideListenerCallbacksState()) + .getValue(); + + const existingCallbackWithSameId = existingCallbacks.find( + (callback) => callback.callbackId === callbackId, + ); + + if (!isDefined(existingCallbackWithSameId)) { + const existingCallbacksWithNewCallback = existingCallbacks.concat({ + callbackId, + callbackFunction, + }); + + set( + getClickOutsideListenerCallbacksState(), + existingCallbacksWithNewCallback, + ); + } else { + const existingCallbacksWithOverwrittenCallback = [ + ...existingCallbacks, + ]; + + const indexOfExistingCallbackWithSameId = + existingCallbacksWithOverwrittenCallback.findIndex( + (callback) => callback.callbackId === callbackId, + ); + + existingCallbacksWithOverwrittenCallback[ + indexOfExistingCallbackWithSameId + ] = { + callbackId, + callbackFunction, + }; + + set( + getClickOutsideListenerCallbacksState(), + existingCallbacksWithOverwrittenCallback, + ); + } + }, + [getClickOutsideListenerCallbacksState], + ); + + const unregisterOnClickOutsideCallback = useRecoilCallback( + ({ set, snapshot }) => + ({ callbackId }: { callbackId: string }) => { + const existingCallbacks = snapshot + .getLoadable(getClickOutsideListenerCallbacksState()) + .getValue(); + + const indexOfCallbackToUnsubscribe = existingCallbacks.findIndex( + (callback) => callback.callbackId === callbackId, + ); + + const callbackToUnsubscribeIsFound = indexOfCallbackToUnsubscribe > -1; + + if (callbackToUnsubscribeIsFound) { + const newCallbacksWithoutCallbackToUnsubscribe = + existingCallbacks.toSpliced(indexOfCallbackToUnsubscribe, 1); + + set( + getClickOutsideListenerCallbacksState(), + newCallbacksWithoutCallbackToUnsubscribe, + ); + } + }, + [getClickOutsideListenerCallbacksState], + ); + + const useRegisterClickOutsideListenerCallback = ( + callback: ClickOutsideListenerCallback, + ) => { + useEffect(() => { + registerOnClickOutsideCallback(callback); + + return () => { + unregisterOnClickOutsideCallback({ + callbackId: callback.callbackId, + }); + }; + }, [callback]); + }; + return { scopeId, useListenClickOutside, toggleClickOutsideListener, + useRegisterClickOutsideListenerCallback, }; }; diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksStateScopeMap.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksStateScopeMap.ts new file mode 100644 index 0000000000..425c3ca81d --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksStateScopeMap.ts @@ -0,0 +1,9 @@ +import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback'; +import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap'; + +export const clickOutsideListenerCallbacksStateScopeMap = createStateScopeMap< + ClickOutsideListenerCallback[] +>({ + key: 'clickOutsideListenerCallbacksStateScopeMap', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallback.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallback.ts new file mode 100644 index 0000000000..5bc8490838 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallback.ts @@ -0,0 +1,6 @@ +import { ClickOutsideListenerCallbackFunction } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallbackFunction'; + +export type ClickOutsideListenerCallback = { + callbackId: string; + callbackFunction: ClickOutsideListenerCallbackFunction; +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallbackFunction.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallbackFunction.ts new file mode 100644 index 0000000000..b3ad996c8d --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/types/ClickOutsideListenerCallbackFunction.ts @@ -0,0 +1,3 @@ +export type ClickOutsideListenerCallbackFunction = ( + event: MouseEvent | TouchEvent, +) => void; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 4c19183a04..b4cc3d408f 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -94,7 +94,7 @@ export const RecordShowPage = () => { /> = Extract; diff --git a/packages/twenty-front/src/utils/debounce.ts b/packages/twenty-front/src/utils/debounce.ts deleted file mode 100644 index 0ccef83239..0000000000 --- a/packages/twenty-front/src/utils/debounce.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const debounce = ( - func: (...args: FuncArgs) => void, - delay: number, -) => { - let timeoutId: ReturnType; - return (...args: FuncArgs) => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - func(...args); - }, delay); - }; -}; diff --git a/packages/twenty-front/src/utils/parseApolloStoreFieldName.ts b/packages/twenty-front/src/utils/parseApolloStoreFieldName.ts index 16bd36b047..cb25568e80 100644 --- a/packages/twenty-front/src/utils/parseApolloStoreFieldName.ts +++ b/packages/twenty-front/src/utils/parseApolloStoreFieldName.ts @@ -16,11 +16,11 @@ export const parseApolloStoreFieldName = < const fieldName = matches[1] as string; try { - const variables = stringifiedVariables + const fieldArguments = stringifiedVariables ? (JSON.parse(stringifiedVariables) as Variables) : undefined; - return { fieldName, variables }; + return { fieldName, fieldArguments }; } catch { return { fieldName }; } diff --git a/yarn.lock b/yarn.lock index a22dbf1649..b9de1da499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22307,6 +22307,13 @@ __metadata: languageName: node linkType: hard +"debounce@npm:^2.0.0": + version: 2.0.0 + resolution: "debounce@npm:2.0.0" + checksum: 35a492f7f4f346f0539d486d7e68a68bd2eaa0c7fb159df8e59f89a20afd9b0ed024d0dc5a6f283b6963f5f032dacd66d805a325e9d8338511e77b505b76f26e + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.8, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -43447,6 +43454,7 @@ __metadata: danger-plugin-todos: "npm:^1.3.1" dataloader: "npm:^2.2.2" date-fns: "npm:^2.30.0" + debounce: "npm:^2.0.0" deep-equal: "npm:^2.2.2" docusaurus-node-polyfills: "npm:^1.0.0" dotenv-cli: "npm:^7.2.1" @@ -43560,6 +43568,7 @@ __metadata: type-fest: "npm:^4.1.0" typeorm: "npm:^0.3.17" typescript: "npm:^5.3.3" + use-debounce: "npm:^10.0.0" uuid: "npm:^9.0.0" vite: "npm:^5.0.0" vite-plugin-checker: "npm:^0.6.2" @@ -44676,6 +44685,15 @@ __metadata: languageName: node linkType: hard +"use-debounce@npm:^10.0.0": + version: 10.0.0 + resolution: "use-debounce@npm:10.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: c1166cba52dedeab17e3e29275af89c57a3e8981b75f6e38ae2896ac36ecd4ed7d8fff5f882ba4b2f91eac7510d5ae0dd89fa4f7d081622ed436c3c89eda5cd1 + languageName: node + linkType: hard + "use-isomorphic-layout-effect@npm:^1.1.1": version: 1.1.2 resolution: "use-isomorphic-layout-effect@npm:1.1.2"