diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx index b580b2a44..fc2408ba9 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { BlockNoteEditor } from '@blocknote/core'; import { useBlockNote } from '@blocknote/react'; import styled from '@emotion/styled'; @@ -9,6 +9,7 @@ import { useDebouncedCallback } from 'use-debounce'; import { v4 } from 'uuid'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState'; import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; import { canCreateActivityState } from '@/activities/states/canCreateActivityState'; import { Activity } from '@/activities/types/Activity'; @@ -37,17 +38,27 @@ const StyledBlockNoteStyledContainer = styled.div` `; type ActivityBodyEditorProps = { - activity: Activity; + activityId: string; fillTitleFromBody: boolean; }; export const ActivityBodyEditor = ({ - activity, + activityId, fillTitleFromBody, }: ActivityBodyEditorProps) => { + const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId)); + + const activity = activityInStore as Activity | null; + const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( activityTitleHasBeenSetFamilyState({ - activityId: activity.id, + activityId: activityId, + }), + ); + + const [activityBody, setActivityBody] = useRecoilState( + activityBodyFamilyState({ + activityId: activityId, }), ); @@ -67,27 +78,31 @@ export const ActivityBodyEditor = ({ const { upsertActivity } = useUpsertActivity(); const persistBodyDebounced = useDebouncedCallback((newBody: string) => { - upsertActivity({ - activity, - input: { - body: newBody, - }, - }); - }, 500); - - const persistTitleAndBodyDebounced = useDebouncedCallback( - (newTitle: string, newBody: string) => { + if (activity) { upsertActivity({ activity, input: { - title: newTitle, body: newBody, }, }); + } + }, 300); - setActivityTitleHasBeenSet(true); + const persistTitleAndBodyDebounced = useDebouncedCallback( + (newTitle: string, newBody: string) => { + if (activity) { + upsertActivity({ + activity, + input: { + title: newTitle, + body: newBody, + }, + }); + + setActivityTitleHasBeenSet(true); + } }, - 500, + 200, ); const updateTitleAndBody = useCallback( @@ -104,28 +119,6 @@ export const ActivityBodyEditor = ({ canCreateActivityState, ); - const handleBodyChange = useCallback( - (activityBody: string) => { - if (!canCreateActivity) { - setCanCreateActivity(true); - } - - if (!activityTitleHasBeenSet && fillTitleFromBody) { - updateTitleAndBody(activityBody); - } else { - persistBodyDebounced(activityBody); - } - }, - [ - fillTitleFromBody, - persistBodyDebounced, - activityTitleHasBeenSet, - updateTitleAndBody, - setCanCreateActivity, - canCreateActivity, - ], - ); - const slashMenuItems = getSlashMenu(); const [uploadFile] = useUploadFileMutation(); @@ -148,63 +141,105 @@ export const ActivityBodyEditor = ({ return imageUrl; }; - const editor: BlockNoteEditor | null = useBlockNote({ - initialContent: - isNonEmptyString(activity.body) && activity.body !== '{}' - ? JSON.parse(activity.body) - : undefined, - domAttributes: { editor: { class: 'editor' } }, - onEditorContentChange: useRecoilCallback( - ({ snapshot, set }) => - (editor: BlockNoteEditor) => { - const newStringifiedBody = - JSON.stringify(editor.topLevelBlocks) ?? ''; + const handlePersistBody = useCallback( + (activityBody: string) => { + if (!canCreateActivity) { + setCanCreateActivity(true); + } - set(recordStoreFamilyState(activity.id), (oldActivity) => { + if (!activityTitleHasBeenSet && fillTitleFromBody) { + updateTitleAndBody(activityBody); + } else { + persistBodyDebounced(activityBody); + } + }, + [ + fillTitleFromBody, + persistBodyDebounced, + activityTitleHasBeenSet, + updateTitleAndBody, + setCanCreateActivity, + canCreateActivity, + ], + ); + + const handleBodyChange = useRecoilCallback( + ({ snapshot, set }) => + (newStringifiedBody: string) => { + set(recordStoreFamilyState(activityId), (oldActivity) => { + return { + ...oldActivity, + id: activityId, + body: newStringifiedBody, + }; + }); + + modifyActivityFromCache(activityId, { + body: () => { + return newStringifiedBody; + }, + }); + + const activityTitleHasBeenSet = snapshot + .getLoadable( + activityTitleHasBeenSetFamilyState({ + activityId: activityId, + }), + ) + .getValue(); + + const blockBody = JSON.parse(newStringifiedBody); + const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string; + + if (!activityTitleHasBeenSet && fillTitleFromBody) { + set(recordStoreFamilyState(activityId), (oldActivity) => { return { ...oldActivity, - id: activity.id, - body: newStringifiedBody, + id: activityId, + title: newTitleFromBody, }; }); - modifyActivityFromCache(activity.id, { - body: () => { - return newStringifiedBody; + modifyActivityFromCache(activityId, { + title: () => { + return newTitleFromBody; }, }); + } - const activityTitleHasBeenSet = snapshot - .getLoadable( - activityTitleHasBeenSetFamilyState({ - activityId: activity.id, - }), - ) - .getValue(); + handlePersistBody(newStringifiedBody); + }, + [activityId, fillTitleFromBody, modifyActivityFromCache, handlePersistBody], + ); - const blockBody = JSON.parse(newStringifiedBody); - const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string; + const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500); - if (!activityTitleHasBeenSet && fillTitleFromBody) { - set(recordStoreFamilyState(activity.id), (oldActivity) => { - return { - ...oldActivity, - id: activity.id, - title: newTitleFromBody, - }; - }); + const handleEditorChange = (newEditor: BlockNoteEditor) => { + const newStringifiedBody = JSON.stringify(newEditor.topLevelBlocks) ?? ''; - modifyActivityFromCache(activity.id, { - title: () => { - return newTitleFromBody; - }, - }); - } + setActivityBody(newStringifiedBody); - handleBodyChange(newStringifiedBody); - }, - [activity, fillTitleFromBody, modifyActivityFromCache, handleBodyChange], - ), + handleBodyChangeDebounced(newStringifiedBody); + }; + + const initialBody = useMemo(() => { + if (isNonEmptyString(activityBody) && activityBody !== '{}') { + return JSON.parse(activityBody); + } else if ( + activity && + isNonEmptyString(activity.body) && + activity?.body !== '{}' + ) { + return JSON.parse(activity.body); + } else { + return undefined; + } + }, [activity, activityBody]); + + const editor: BlockNoteEditor | null = useBlockNote({ + initialContent: initialBody, + domAttributes: { editor: { class: 'editor' } }, + onEditorContentChange: handleEditorChange, slashMenuItems, blockSpecs: blockSpecs, uploadFile: handleUploadAttachment, diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEffect.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEffect.tsx new file mode 100644 index 000000000..7da880ed1 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEffect.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { useRecoilState } from 'recoil'; + +import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; + +export const ActivityBodyEffect = ({ activityId }: { activityId: string }) => { + const [activityFromStore] = useRecoilState( + recordStoreFamilyState(activityId), + ); + + const [activityBody, setActivityBody] = useRecoilState( + activityBodyFamilyState({ activityId }), + ); + + useEffect(() => { + if ( + activityBody === '' && + activityFromStore && + activityBody !== activityFromStore.body + ) { + setActivityBody(activityFromStore.body); + } + }, [activityFromStore, activityBody, setActivityBody]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx index 37a9197f2..16bdbed69 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx @@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { Comment } from '@/activities/comment/Comment'; -import { Activity } from '@/activities/types/Activity'; import { Comment as CommentType } from '@/activities/types/Comment'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -50,12 +49,12 @@ const StyledThreadCommentTitle = styled.div` `; type ActivityCommentsProps = { - activity: Pick; + activityId: string; scrollableContainerRef: React.RefObject; }; export const ActivityComments = ({ - activity, + activityId, scrollableContainerRef, }: ActivityCommentsProps) => { const { createOneRecord: createOneComment } = useCreateOneRecord({ @@ -66,10 +65,10 @@ export const ActivityComments = ({ const { records: comments } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Comment, - skip: !isNonEmptyString(activity?.id), + skip: !isNonEmptyString(activityId), filter: { activityId: { - eq: activity?.id ?? '', + eq: activityId, }, }, }); @@ -87,7 +86,7 @@ export const ActivityComments = ({ id: v4(), authorId: currentWorkspaceMember?.id ?? '', author: currentWorkspaceMember, - activityId: activity?.id ?? '', + activityId: activityId, body: commentText, createdAt: new Date().toISOString(), }); diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx index 50fcbb8cf..3c7a0134d 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx @@ -1,28 +1,12 @@ import { useRef } from 'react'; import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor'; +import { ActivityBodyEffect } from '@/activities/components/ActivityBodyEffect'; import { ActivityComments } from '@/activities/components/ActivityComments'; +import { ActivityEditorFields } from '@/activities/components/ActivityEditorFields'; +import { ActivityTitleEffect } from '@/activities/components/ActivityTitleEffect'; 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 { canCreateActivityState } from '@/activities/states/canCreateActivityState'; -import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; -import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; -import { Activity } from '@/activities/types/Activity'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFieldContext } from '@/object-record/hooks/useFieldContext'; -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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -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 { ActivityTitle } from './ActivityTitle'; @@ -60,152 +44,36 @@ const StyledTopContainer = styled.div` `; type ActivityEditorProps = { - activity: Activity; + activityId: string; showComment?: boolean; fillTitleFromBody?: boolean; }; export const ActivityEditor = ({ - activity, + activityId, showComment = true, fillTitleFromBody = false, }: ActivityEditorProps) => { const containerRef = useRef(null); - const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener( - RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID, - ); - - const { upsertActivity } = useUpsertActivity(); - const { deleteActivityFromCache } = useDeleteActivityFromCache(); - - const useUpsertOneActivityMutation: RecordUpdateHook = () => { - const upsertActivityMutation = async ({ - variables, - }: RecordUpdateHookParams) => { - await upsertActivity({ activity, input: variables.updateOneRecordInput }); - }; - - return [upsertActivityMutation, { loading: false }]; - }; - - const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({ - objectNameSingular: CoreObjectNameSingular.Activity, - objectRecordId: activity.id, - fieldMetadataName: 'dueAt', - fieldPosition: 0, - clearable: true, - customUseUpdateOneObjectHook: useUpsertOneActivityMutation, - }); - - const { FieldContextProvider: AssigneeFieldContextProvider } = - useFieldContext({ - objectNameSingular: CoreObjectNameSingular.Activity, - objectRecordId: activity.id, - fieldMetadataName: 'assignee', - fieldPosition: 1, - clearable: true, - customUseUpdateOneObjectHook: useUpsertOneActivityMutation, - }); - - const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState( - isActivityInCreateModeState, - ); - - const [isUpsertingActivityInDB] = useRecoilState( - isUpsertingActivityInDBState, - ); - - const [canCreateActivity] = useRecoilState(canCreateActivityState); - - const [activityFromStore] = useRecoilState( - recordStoreFamilyState(activity.id), - ); - - const { FieldContextProvider: ActivityTargetsContextProvider } = - useFieldContext({ - objectNameSingular: CoreObjectNameSingular.Activity, - objectRecordId: activity?.id ?? '', - fieldMetadataName: 'activityTargets', - fieldPosition: 2, - }); - - useRegisterClickOutsideListenerCallback({ - callbackId: 'activity-editor', - callbackFunction: () => { - if (isUpsertingActivityInDB || !activityFromStore) { - return; - } - - if (isActivityInCreateMode) { - if (canCreateActivity) { - upsertActivity({ - activity, - input: { - title: activityFromStore.title, - body: activityFromStore.body, - }, - }); - } else { - deleteActivityFromCache(activity); - } - - setIsActivityInCreateMode(false); - } else { - if ( - activityFromStore.title !== activity.title || - activityFromStore.body !== activity.body - ) { - upsertActivity({ - activity, - input: { - title: activityFromStore.title, - body: activityFromStore.body, - }, - }); - } - } - }, - }); - - if (!activity) { - return <>; - } - return ( - - - - {activity.type === 'Task' && - DueAtFieldContextProvider && - AssigneeFieldContextProvider && ( - <> - - - - - - - - )} - {ActivityTargetsContextProvider && ( - - - - )} - + + + + + {showComment && ( )} diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx new file mode 100644 index 000000000..cffc5e079 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx @@ -0,0 +1,98 @@ +import { useRecoilCallback } from 'recoil'; + +import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; +import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState'; +import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState'; +import { canCreateActivityState } from '@/activities/states/canCreateActivityState'; +import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; +import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; +import { Activity } from '@/activities/types/Activity'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; +import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; + +export const ActivityEditorEffect = ({ + activityId, +}: { + activityId: string; +}) => { + const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener( + RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID, + ); + + const { upsertActivity } = useUpsertActivity(); + const { deleteActivityFromCache } = useDeleteActivityFromCache(); + + const upsertActivityCallback = useRecoilCallback( + ({ snapshot, set }) => + () => { + const isUpsertingActivityInDB = snapshot + .getLoadable(isUpsertingActivityInDBState) + .getValue(); + + const canCreateActivity = snapshot + .getLoadable(canCreateActivityState) + .getValue(); + + const isActivityInCreateMode = snapshot + .getLoadable(isActivityInCreateModeState) + .getValue(); + + const activityFromStore = snapshot + .getLoadable(recordStoreFamilyState(activityId)) + .getValue(); + + const activity = activityFromStore as Activity | null; + + const activityTitle = snapshot + .getLoadable(activityTitleFamilyState({ activityId })) + .getValue(); + + const activityBody = snapshot + .getLoadable(activityBodyFamilyState({ activityId })) + .getValue(); + + if (isUpsertingActivityInDB || !activityFromStore) { + return; + } + + if (isActivityInCreateMode && activity) { + if (canCreateActivity) { + upsertActivity({ + activity, + input: { + title: activityFromStore.title, + body: activityFromStore.body, + }, + }); + } else { + deleteActivityFromCache(activity); + } + + set(isActivityInCreateModeState, false); + } else if (activity) { + if ( + activity.title !== activityTitle || + activity.body !== activityBody + ) { + upsertActivity({ + activity, + input: { + title: activityTitle, + body: activityBody, + }, + }); + } + } + }, + [activityId, deleteActivityFromCache, upsertActivity], + ); + + useRegisterClickOutsideListenerCallback({ + callbackId: 'activity-editor', + callbackFunction: upsertActivityCallback, + }); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx new file mode 100644 index 000000000..4db2a8eef --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditorFields.tsx @@ -0,0 +1,92 @@ +import { useRecoilState } from 'recoil'; + +import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; +import { Activity } from '@/activities/types/Activity'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFieldContext } from '@/object-record/hooks/useFieldContext'; +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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; + +export const ActivityEditorFields = ({ + activityId, +}: { + activityId: string; +}) => { + const { upsertActivity } = useUpsertActivity(); + + const [activityFromStore] = useRecoilState( + recordStoreFamilyState(activityId), + ); + + const activity = activityFromStore as Activity; + + const useUpsertOneActivityMutation: RecordUpdateHook = () => { + const upsertActivityMutation = async ({ + variables, + }: RecordUpdateHookParams) => { + if (activityFromStore) { + await upsertActivity({ + activity: activityFromStore as Activity, + input: variables.updateOneRecordInput, + }); + } + }; + + return [upsertActivityMutation, { loading: false }]; + }; + + const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({ + objectNameSingular: CoreObjectNameSingular.Activity, + objectRecordId: activityId, + fieldMetadataName: 'dueAt', + fieldPosition: 0, + clearable: true, + customUseUpdateOneObjectHook: useUpsertOneActivityMutation, + }); + + const { FieldContextProvider: AssigneeFieldContextProvider } = + useFieldContext({ + objectNameSingular: CoreObjectNameSingular.Activity, + objectRecordId: activityId, + fieldMetadataName: 'assignee', + fieldPosition: 1, + clearable: true, + customUseUpdateOneObjectHook: useUpsertOneActivityMutation, + }); + + const { FieldContextProvider: ActivityTargetsContextProvider } = + useFieldContext({ + objectNameSingular: CoreObjectNameSingular.Activity, + objectRecordId: activityId, + fieldMetadataName: 'activityTargets', + fieldPosition: 2, + }); + + return ( + + {activity.type === 'Task' && + DueAtFieldContextProvider && + AssigneeFieldContextProvider && ( + <> + + + + + + + + )} + {ActivityTargetsContextProvider && ( + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx index e40b85e10..05cc0bdd3 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx @@ -6,6 +6,7 @@ import { Key } from 'ts-key-enum'; import { useDebouncedCallback } from 'use-debounce'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState'; import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; import { canCreateActivityState } from '@/activities/states/canCreateActivityState'; import { Activity } from '@/activities/types/Activity'; @@ -55,14 +56,20 @@ const StyledContainer = styled.div` `; type ActivityTitleProps = { - activity: Activity; + activityId: string; }; -export const ActivityTitle = ({ activity }: ActivityTitleProps) => { +export const ActivityTitle = ({ activityId }: ActivityTitleProps) => { const [activityInStore, setActivityInStore] = useRecoilState( - recordStoreFamilyState(activity.id), + recordStoreFamilyState(activityId), ); + const [activityTitle, setActivityTitle] = useRecoilState( + activityTitleFamilyState({ activityId }), + ); + + const activity = activityInStore as Activity; + const [canCreateActivity, setCanCreateActivity] = useRecoilState( canCreateActivityState, ); @@ -96,7 +103,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => { const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( activityTitleHasBeenSetFamilyState({ - activityId: activity.id, + activityId: activityId, }), ); @@ -122,7 +129,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => { } }, 500); - const handleTitleChange = (newTitle: string) => { + const setTitleDebounced = useDebouncedCallback((newTitle: string) => { setActivityInStore((currentActivity) => { return { ...currentActivity, @@ -140,6 +147,12 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => { return newTitle; }, }); + }, 500); + + const handleTitleChange = (newTitle: string) => { + setActivityTitle(newTitle); + + setTitleDebounced(newTitle); persistTitleDebounced(newTitle); }; @@ -171,7 +184,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => { ref={titleInputRef} placeholder={`${activity.type} title`} onChange={(event) => handleTitleChange(event.target.value)} - value={activityInStore?.title ?? ''} + value={activityTitle} completed={completed} onBlur={handleBlur} onFocus={handleFocus} diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTitleEffect.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTitleEffect.tsx new file mode 100644 index 000000000..c5fcadaa7 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityTitleEffect.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import { useRecoilState } from 'recoil'; + +import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; + +export const ActivityTitleEffect = ({ activityId }: { activityId: string }) => { + const [activityFromStore] = useRecoilState( + recordStoreFamilyState(activityId), + ); + + const [activityTitle, setActivityTitle] = useRecoilState( + activityTitleFamilyState({ activityId }), + ); + + useEffect(() => { + if ( + activityTitle === '' && + activityFromStore && + activityTitle !== activityFromStore.title + ) { + setActivityTitle(activityFromStore.title); + } + }, [activityFromStore, activityTitle, setActivityTitle]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTypeDropdown.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTypeDropdown.tsx index 5924147d3..d1b927e35 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityTypeDropdown.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityTypeDropdown.tsx @@ -1,6 +1,7 @@ import { useTheme } from '@emotion/react'; +import { useRecoilState } from 'recoil'; -import { Activity } from '@/activities/types/Activity'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { Chip, ChipAccent, @@ -10,18 +11,21 @@ import { import { IconCheckbox, IconNotes } from '@/ui/display/icon'; type ActivityTypeDropdownProps = { - activity: Pick; + activityId: string; }; export const ActivityTypeDropdown = ({ - activity, + activityId, }: ActivityTypeDropdownProps) => { + const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId)); + const theme = useTheme(); + return ( ) : ( diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts index ecbc37f3f..b28f1f6f9 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts @@ -90,7 +90,7 @@ export const useActivities = ({ const loading = loadingActivities || loadingActivityTargets; // TODO: fix connection in relation => automatically change to an array - const activities = activitiesWithConnection + const activities: Activity[] = activitiesWithConnection ?.map(makeActivityWithoutConnection as any) .map(({ activity }: any) => activity); diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts index db4a06f71..2dbe174a6 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts @@ -10,7 +10,10 @@ export const useActivityTargetsForTargetableObjects = ({ targetableObjects, skip, }: { - targetableObjects: ActivityTargetableObject[]; + targetableObjects: Pick< + ActivityTargetableObject, + 'id' | 'targetObjectNameSingular' + >[]; skip?: boolean; }) => { const activityTargetsFilter = getActivityTargetsFilter({ diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts index cbead3e30..b9809e797 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts @@ -1,4 +1,4 @@ -import { useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections'; @@ -6,13 +6,15 @@ import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline- 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 { makeActivityTargetsToCreateFromTargetableObjects } 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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; +import { isDefined } from '~/utils/isDefined'; export const useCreateActivityInCache = () => { const { createManyRecordsInCache: createManyActivityTargetsInCache } = @@ -39,58 +41,86 @@ export const useCreateActivityInCache = () => { const { attachRelationInBothDirections } = useAttachRelationInBothDirections(); - const createActivityInCache = ({ - type, - targetableObjects, - customAssignee, - }: { - type: ActivityType; - targetableObjects: ActivityTargetableObject[]; - customAssignee?: WorkspaceMember; - }) => { - const activityId = v4(); - - const createdActivityInCache = createOneActivityInCache({ - id: activityId, - author: currentWorkspaceMemberRecord, - authorId: currentWorkspaceMemberRecord?.id, - assignee: customAssignee ?? currentWorkspaceMemberRecord, - assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id, - type, - }); - - const activityTargetsToCreate = - getActivityTargetsToCreateFromTargetableObjects({ - activityId, + const createActivityInCache = useRecoilCallback( + ({ snapshot, set }) => + ({ + type, targetableObjects, - }); + customAssignee, + }: { + type: ActivityType; + targetableObjects: ActivityTargetableObject[]; + customAssignee?: WorkspaceMember; + }) => { + const activityId = v4(); - const createdActivityTargetsInCache = createManyActivityTargetsInCache( - activityTargetsToCreate, - ); + const createdActivityInCache = createOneActivityInCache({ + id: activityId, + author: currentWorkspaceMemberRecord, + authorId: currentWorkspaceMemberRecord?.id, + assignee: customAssignee ?? currentWorkspaceMemberRecord, + assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id, + type, + }); - injectIntoActivityTargetInlineCellCache({ - activityId, - activityTargetsToInject: createdActivityTargetsInCache, - }); + const targetObjectRecords = targetableObjects + .map((targetableObject) => { + const targetObject = snapshot + .getLoadable(recordStoreFamilyState(targetableObject.id)) + .getValue(); - attachRelationInBothDirections({ - sourceRecord: createdActivityInCache, - fieldNameOnSourceRecord: 'activityTargets', - sourceObjectNameSingular: CoreObjectNameSingular.Activity, - fieldNameOnTargetRecord: 'activity', - targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget, - targetRecords: createdActivityTargetsInCache, - }); + return targetObject; + }) + .filter(isDefined); - return { - createdActivityInCache: { - ...createdActivityInCache, - activityTargets: createdActivityTargetsInCache, + const activityTargetsToCreate = + makeActivityTargetsToCreateFromTargetableObjects({ + activityId, + targetableObjects, + targetObjectRecords, + }); + + const createdActivityTargetsInCache = createManyActivityTargetsInCache( + activityTargetsToCreate, + ); + + injectIntoActivityTargetInlineCellCache({ + activityId, + activityTargetsToInject: createdActivityTargetsInCache, + }); + + attachRelationInBothDirections({ + sourceRecord: createdActivityInCache, + fieldNameOnSourceRecord: 'activityTargets', + sourceObjectNameSingular: CoreObjectNameSingular.Activity, + fieldNameOnTargetRecord: 'activity', + targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget, + targetRecords: createdActivityTargetsInCache, + }); + + // TODO: should refactor when refactoring make activity connection utils + set(recordStoreFamilyState(activityId), { + ...createdActivityInCache, + activityTargets: createdActivityTargetsInCache, + comments: [], + }); + + return { + createdActivityInCache: { + ...createdActivityInCache, + activityTargets: createdActivityTargetsInCache, + }, + createdActivityTargetsInCache, + }; }, - createdActivityTargetsInCache, - }; - }; + [ + attachRelationInBothDirections, + createManyActivityTargetsInCache, + createOneActivityInCache, + currentWorkspaceMemberRecord, + injectIntoActivityTargetInlineCellCache, + ], + ); return { createActivityInCache, diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts index 237201aba..9bcc1ec21 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts @@ -1,7 +1,6 @@ import { useRecoilState } from 'recoil'; -import { activityInDrawerState } from '@/activities/states/activityInDrawerState'; -import { Activity } from '@/activities/types/Activity'; +import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; 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'; @@ -15,21 +14,21 @@ export const useOpenActivityRightDrawer = () => { const [viewableActivityId, setViewableActivityId] = useRecoilState( viewableActivityIdState, ); - const [, setActivityInDrawer] = useRecoilState(activityInDrawerState); + const [, setActivityIdInDrawer] = useRecoilState(activityIdInDrawerState); const setHotkeyScope = useSetHotkeyScope(); - return (activity: Activity) => { + return (activityId: string) => { if ( isRightDrawerOpen && rightDrawerPage === RightDrawerPages.EditActivity && - viewableActivityId === activity.id + viewableActivityId === activityId ) { return; } setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - setViewableActivityId(activity.id); - setActivityInDrawer(activity); + setViewableActivityId(activityId); + setActivityIdInDrawer(activityId); openRightDrawer(RightDrawerPages.EditActivity); }; }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 24131b80f..6b7e83c44 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -1,7 +1,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache'; -import { activityInDrawerState } from '@/activities/states/activityInDrawerState'; +import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; @@ -34,7 +34,7 @@ export const useOpenCreateActivityDrawer = () => { temporaryActivityForEditorState, ); - const setActivityInDrawer = useSetRecoilState(activityInDrawerState); + const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState); const [, setIsUpsertingActivityInDB] = useRecoilState( isUpsertingActivityInDBState, @@ -55,7 +55,7 @@ export const useOpenCreateActivityDrawer = () => { customAssignee, }); - setActivityInDrawer(createdActivityInCache); + setActivityIdInDrawer(createdActivityInCache.id); setTemporaryActivityForEditor(createdActivityInCache); setIsCreatingActivity(true); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); diff --git a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts index 379013bd6..4570622b5 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts @@ -6,13 +6,13 @@ import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB' import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries'; import { useInjectIntoActivityTargetsQueries } from '@/activities/hooks/useInjectIntoActivityTargetsQueries'; import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; -import { activityInDrawerState } from '@/activities/states/activityInDrawerState'; +import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries'; -import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; import { Activity } from '@/activities/types/Activity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; @@ -34,7 +34,7 @@ export const useUpsertActivity = () => { isUpsertingActivityInDBState, ); - const setActivityInDrawer = useSetRecoilState(activityInDrawerState); + const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState); const objectShowPageTargetableObject = useRecoilValue( objectShowPageTargetableObjectState, @@ -169,7 +169,7 @@ export const useUpsertActivity = () => { await createActivityInDB(activityToCreate); - setActivityInDrawer(activityToCreate); + setActivityIdInDrawer(activityToCreate.id); setIsActivityInCreateMode(false); } else { 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 74a9633cf..323546127 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx @@ -94,7 +94,7 @@ export const NoteCard = ({ openActivityRightDrawer(note)} + onClick={() => openActivityRightDrawer(note.id)} > {note.title ?? 'Task Title'} {body} 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 386458955..cc25fab5d 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,14 +1,14 @@ import { useLocation } from 'react-router-dom'; import styled from '@emotion/styled'; import { isNonEmptyArray } from '@sniptt/guards'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries'; import { useRemoveFromActivityTargetsQueries } from '@/activities/hooks/useRemoveFromActivityTargetsQueries'; import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; -import { activityInDrawerState } from '@/activities/states/activityInDrawerState'; +import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; @@ -17,11 +17,13 @@ import { viewableActivityIdState } from '@/activities/states/viewableActivityIdS import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; -import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; +import { Activity } from '@/activities/types/Activity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { getChildRelationArray } from '@/object-record/utils/getChildRelationArray'; import { mapToRecordId } from '@/object-record/utils/mapToObjectId'; import { IconPlus, IconTrash } from '@/ui/display/icon'; import { IconButton } from '@/ui/input/button/components/IconButton'; @@ -35,7 +37,7 @@ const StyledButtonContainer = styled.div` export const ActivityActionBar = () => { const viewableActivityId = useRecoilValue(viewableActivityIdState); - const activityInDrawer = useRecoilValue(activityInDrawerState); + const activityIdInDrawer = useRecoilValue(activityIdInDrawerState); const activityTargetableEntityArray = useRecoilValue( activityTargetableEntityArrayState, @@ -60,9 +62,11 @@ export const ActivityActionBar = () => { const [isUpsertingActivityInDB] = useRecoilState( isUpsertingActivityInDBState, ); + const objectShowPageTargetableObject = useRecoilValue( objectShowPageTargetableObjectState, ); + const openCreateActivity = useOpenCreateActivityDrawer(); const currentCompletedTaskQueryVariables = useRecoilValue( @@ -85,90 +89,130 @@ export const ActivityActionBar = () => { const weAreOnObjectShowPage = pathname.startsWith('/object'); const weAreOnTaskPage = pathname.startsWith('/tasks'); - const deleteActivity = async () => { - setIsRightDrawerOpen(false); - - if (viewableActivityId) { - if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) { - deleteActivityFromCache(temporaryActivityForEditor); - setTemporaryActivityForEditor(null); - } else { - if (activityInDrawer) { - const activityTargetIdsToDelete = - activityInDrawer?.activityTargets.map(mapToRecordId) ?? []; - - if (weAreOnTaskPage) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [], - activitiesFilters: currentCompletedTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentCompletedTaskQueryVariables?.orderBy, - }); - - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [], - activitiesFilters: currentIncompleteTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentIncompleteTaskQueryVariables?.orderBy, - }); - } else if ( - weAreOnObjectShowPage && - isDefined(objectShowPageTargetableObject) - ) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: {}, - activitiesOrderByVariables: - FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, - }); - - if (isDefined(currentCompletedTaskQueryVariables)) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: currentCompletedTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentCompletedTaskQueryVariables?.orderBy, - }); - } - - if (isDefined(currentIncompleteTaskQueryVariables)) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: currentIncompleteTaskQueryVariables?.filter, - activitiesOrderByVariables: - currentIncompleteTaskQueryVariables?.orderBy, - }); - } - - if (isDefined(currentNotesQueryVariables)) { - removeFromActivitiesQueries({ - activityIdToRemove: viewableActivityId, - targetableObjects: [objectShowPageTargetableObject], - activitiesFilters: currentNotesQueryVariables?.filter, - activitiesOrderByVariables: currentNotesQueryVariables?.orderBy, - }); - } - - removeFromActivityTargetsQueries({ - activityTargetsToRemove: activityInDrawer?.activityTargets ?? [], - targetableObjects: [objectShowPageTargetableObject], - }); - } - - if (isNonEmptyArray(activityTargetIdsToDelete)) { - await deleteManyActivityTargets(activityTargetIdsToDelete); - } - - await deleteOneActivity?.(viewableActivityId); + const deleteActivity = useRecoilCallback( + ({ snapshot }) => + async () => { + if (!activityIdInDrawer) { + throw new Error( + 'activityIdInDrawer is not defined, this should not happen', + ); } - } - } - }; + + const activity = snapshot + .getLoadable(recordStoreFamilyState(activityIdInDrawer)) + .getValue() as Activity; + + const activityTargets = getChildRelationArray({ + childRelation: activity.activityTargets, + }); + + setIsRightDrawerOpen(false); + + if (viewableActivityId) { + if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) { + deleteActivityFromCache(temporaryActivityForEditor); + setTemporaryActivityForEditor(null); + } else { + if (activityIdInDrawer) { + const activityTargetIdsToDelete: string[] = + activityTargets.map(mapToRecordId) ?? []; + + if (weAreOnTaskPage) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [], + activitiesFilters: currentCompletedTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentCompletedTaskQueryVariables?.orderBy, + }); + + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [], + activitiesFilters: + currentIncompleteTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentIncompleteTaskQueryVariables?.orderBy, + }); + } else if ( + weAreOnObjectShowPage && + isDefined(objectShowPageTargetableObject) + ) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: {}, + activitiesOrderByVariables: + FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + }); + + if (isDefined(currentCompletedTaskQueryVariables)) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: + currentCompletedTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentCompletedTaskQueryVariables?.orderBy, + }); + } + + if (isDefined(currentIncompleteTaskQueryVariables)) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: + currentIncompleteTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentIncompleteTaskQueryVariables?.orderBy, + }); + } + + if (isDefined(currentNotesQueryVariables)) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: currentNotesQueryVariables?.filter, + activitiesOrderByVariables: + currentNotesQueryVariables?.orderBy, + }); + } + + removeFromActivityTargetsQueries({ + activityTargetsToRemove: activity?.activityTargets ?? [], + targetableObjects: [objectShowPageTargetableObject], + }); + } + + if (isNonEmptyArray(activityTargetIdsToDelete)) { + await deleteManyActivityTargets(activityTargetIdsToDelete); + } + + await deleteOneActivity?.(viewableActivityId); + } + } + } + }, + [ + activityIdInDrawer, + currentCompletedTaskQueryVariables, + currentIncompleteTaskQueryVariables, + currentNotesQueryVariables, + deleteActivityFromCache, + deleteManyActivityTargets, + deleteOneActivity, + isActivityInCreateMode, + objectShowPageTargetableObject, + removeFromActivitiesQueries, + removeFromActivityTargetsQueries, + setTemporaryActivityForEditor, + temporaryActivityForEditor, + viewableActivityId, + weAreOnObjectShowPage, + weAreOnTaskPage, + setIsRightDrawerOpen, + ], + ); const record = useRecoilValue( recordStoreFamilyState(viewableActivityId ?? ''), 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 bb179446a..eaa50a218 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,7 +1,7 @@ import styled from '@emotion/styled'; import { ActivityEditor } from '@/activities/components/ActivityEditor'; -import { useActivityById } from '@/activities/hooks/useActivityById'; +import { ActivityEditorEffect } from '@/activities/components/ActivityEditorEffect'; const StyledContainer = styled.div` box-sizing: border-box; @@ -24,18 +24,11 @@ export const RightDrawerActivity = ({ showComment = true, fillTitleFromBody = false, }: RightDrawerActivityProps) => { - const { activity, loading } = useActivityById({ - activityId, - }); - - if (!activity || loading) { - return <>; - } - return ( + diff --git a/packages/twenty-front/src/modules/activities/states/activityBodyFamilyState.ts b/packages/twenty-front/src/modules/activities/states/activityBodyFamilyState.ts new file mode 100644 index 000000000..452dc0220 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/states/activityBodyFamilyState.ts @@ -0,0 +1,9 @@ +import { atomFamily } from 'recoil'; + +export const activityBodyFamilyState = atomFamily< + string, + { activityId: string } +>({ + key: 'activityBodyFamilyState', + default: '', +}); diff --git a/packages/twenty-front/src/modules/activities/states/activityIdInDrawerState.ts b/packages/twenty-front/src/modules/activities/states/activityIdInDrawerState.ts new file mode 100644 index 000000000..7bc9362fc --- /dev/null +++ b/packages/twenty-front/src/modules/activities/states/activityIdInDrawerState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const activityIdInDrawerState = atom({ + key: 'activityIdInDrawerState', + default: null, +}); diff --git a/packages/twenty-front/src/modules/activities/states/activityInDrawerState.ts b/packages/twenty-front/src/modules/activities/states/activityInDrawerState.ts deleted file mode 100644 index 115dd9381..000000000 --- a/packages/twenty-front/src/modules/activities/states/activityInDrawerState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from 'recoil'; - -import { Activity } from '@/activities/types/Activity'; - -export const activityInDrawerState = atom({ - key: 'activityInDrawerState', - default: null, -}); diff --git a/packages/twenty-front/src/modules/activities/states/activityTitleFamilyState.ts b/packages/twenty-front/src/modules/activities/states/activityTitleFamilyState.ts new file mode 100644 index 000000000..6196bae01 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/states/activityTitleFamilyState.ts @@ -0,0 +1,9 @@ +import { atomFamily } from 'recoil'; + +export const activityTitleFamilyState = atomFamily< + string, + { activityId: string } +>({ + key: 'activityTitleFamilyState', + default: '', +}); diff --git a/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx b/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx new file mode 100644 index 000000000..f725249b4 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx @@ -0,0 +1,47 @@ +import { useEffect } from 'react'; +import { DateTime } from 'luxon'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { parseDate } from '~/utils/date-utils'; + +export const CurrentUserDueTaskCountEffect = () => { + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const [currentUserDueTaskCount, setCurrentUserDueTaskCount] = useRecoilState( + currentUserDueTaskCountState, + ); + + const { records: tasks } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.Activity, + filter: { + type: { eq: 'Task' }, + completedAt: { is: 'NULL' }, + assigneeId: { eq: currentWorkspaceMember?.id }, + }, + }); + + const computedCurrentUserDueTaskCount = tasks.filter((task) => { + if (!task.dueAt) { + return false; + } + const dueDate = parseDate(task.dueAt).toJSDate(); + const today = DateTime.now().endOf('day').toJSDate(); + return dueDate <= today; + }).length; + + useEffect(() => { + if (currentUserDueTaskCount !== computedCurrentUserDueTaskCount) { + setCurrentUserDueTaskCount(computedCurrentUserDueTaskCount); + } + }, [ + computedCurrentUserDueTaskCount, + currentUserDueTaskCount, + setCurrentUserDueTaskCount, + ]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index 9c00fb080..b5e1501a1 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -85,7 +85,7 @@ export const TaskRow = ({ task }: { task: Activity }) => { return ( { - openActivityRightDrawer(task); + openActivityRightDrawer(task.id); }} >
({ + default: 0, + key: 'currentUserDueTaskCountState', +}); diff --git a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx index ffb01a993..37cc77560 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx +++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx @@ -1,11 +1,8 @@ -import { useEffect } from 'react'; import styled from '@emotion/styled'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; -import { useActivities } from '@/activities/hooks/useActivities'; import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup'; -import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; -import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; +import { timelineActivitiesNetworkingState } from '@/activities/timeline/states/timelineActivitiesNetworkingState'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { @@ -15,7 +12,6 @@ import { AnimatedPlaceholderEmptyTitle, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { isDefined } from '~/utils/isDefined'; import { TimelineItemsContainer } from './TimelineItemsContainer'; @@ -36,21 +32,10 @@ export const Timeline = ({ }: { targetableObject: ActivityTargetableObject; }) => { - const { activities, initialized, noActivities } = useActivities({ - targetableObjects: [targetableObject], - activitiesFilters: {}, - activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, - skip: !isDefined(targetableObject), - }); - - const setTimelineTargetableObject = useSetRecoilState( - objectShowPageTargetableObjectState, + const { initialized, noActivities } = useRecoilValue( + timelineActivitiesNetworkingState, ); - useEffect(() => { - setTimelineTargetableObject(targetableObject); - }, [targetableObject, setTimelineTargetableObject]); - const showEmptyState = noActivities; const showLoadingState = !initialized; @@ -79,7 +64,7 @@ export const Timeline = ({ return ( - + ); }; diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx index fbff3cf8d..3574022c4 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx +++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineActivity.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; -import { Activity } from '@/activities/types/Activity'; +import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityFirstLevelFamilySelector'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { IconCheckbox, IconNotes } from '@/ui/display/icon'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -136,28 +136,42 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>` `; type TimelineActivityProps = { - activity: Activity; isLastActivity?: boolean; + activityId: string; }; export const TimelineActivity = ({ - activity, isLastActivity, + activityId, }: TimelineActivityProps) => { - const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt); - const exactCreatedAt = beautifyExactDateTime(activity.createdAt); + const activityForTimeline = useRecoilValue( + timelineActivityWithoutTargetsFamilyState(activityId), + ); + + const beautifiedCreatedAt = activityForTimeline + ? beautifyPastDateRelativeToNow(activityForTimeline.createdAt) + : ''; + const exactCreatedAt = activityForTimeline + ? beautifyExactDateTime(activityForTimeline.createdAt) + : ''; const openActivityRightDrawer = useOpenActivityRightDrawer(); const theme = useTheme(); - const activityFromStore = useRecoilValue(recordStoreFamilyState(activity.id)); + const activityFromStore = useRecoilValue( + recordStoreFamilyState(activityForTimeline?.id ?? ''), + ); + + if (!activityForTimeline) { + return <>; + } return ( <> @@ -166,23 +180,26 @@ export const TimelineActivity = ({ - {activity.author?.name.firstName}{' '} - {activity.author?.name.lastName} + {activityForTimeline.author?.name.firstName}{' '} + {activityForTimeline.author?.name.lastName} - created a {activity.type.toLowerCase()} + created a {activityForTimeline.type.toLowerCase()} - {activity.type === 'Note' && ( + {activityForTimeline.type === 'Note' && ( )} - {activity.type === 'Task' && ( + {activityForTimeline.type === 'Task' && ( )} - {(activity.type === 'Note' || activity.type === 'Task') && ( + {(activityForTimeline.type === 'Note' || + activityForTimeline.type === 'Task') && ( openActivityRightDrawer(activity)} + onClick={() => + openActivityRightDrawer(activityForTimeline.id) + } > “ - + {beautifiedCreatedAt} { + const timelineActivitiesForGroup = useRecoilValue( + timelineActivitiesForGroupState, + ); -export const TimelineItemsContainer = ({ - activities, -}: TimelineItemsContainerProps) => { - const groupedActivities = groupActivitiesByMonth(activities); + const groupedActivities = groupActivitiesByMonth(timelineActivitiesForGroup); return ( diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineQueryEffect.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineQueryEffect.tsx new file mode 100644 index 000000000..f6ec016c8 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineQueryEffect.tsx @@ -0,0 +1,131 @@ +import { useEffect } from 'react'; +import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; + +import { useActivities } from '@/activities/hooks/useActivities'; +import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; +import { timelineActivitiesFammilyState } from '@/activities/timeline/states/timelineActivitiesFamilyState'; +import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState'; +import { timelineActivitiesNetworkingState } from '@/activities/timeline/states/timelineActivitiesNetworkingState'; +import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityFirstLevelFamilySelector'; +import { Activity } from '@/activities/types/Activity'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { sortObjectRecordByDateField } from '@/object-record/utils/sortObjectRecordByDateField'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; + +export const TimelineQueryEffect = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + const setTimelineTargetableObject = useSetRecoilState( + objectShowPageTargetableObjectState, + ); + + useEffect(() => { + setTimelineTargetableObject(targetableObject); + }, [targetableObject, setTimelineTargetableObject]); + + const { activities, initialized, noActivities } = useActivities({ + targetableObjects: [targetableObject], + activitiesFilters: {}, + activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + skip: !isDefined(targetableObject), + }); + + const [timelineActivitiesNetworking, setTimelineActivitiesNetworking] = + useRecoilState(timelineActivitiesNetworkingState); + + const [timelineActivitiesForGroup, setTimelineActivitiesForGroup] = + useRecoilState(timelineActivitiesForGroupState); + + useEffect(() => { + if (!isDefined(targetableObject)) { + return; + } + + const activitiesForGroup = activities + .map((activity) => ({ + id: activity.id, + createdAt: activity.createdAt, + })) + .toSorted(sortObjectRecordByDateField('createdAt', 'DescNullsLast')); + + const timelineActivitiesForGroupSorted = + timelineActivitiesForGroup.toSorted( + sortObjectRecordByDateField('createdAt', 'DescNullsLast'), + ); + + if (!isDeeplyEqual(activitiesForGroup, timelineActivitiesForGroupSorted)) { + setTimelineActivitiesForGroup(activitiesForGroup); + } + + if ( + !isDeeplyEqual(timelineActivitiesNetworking.initialized, initialized) || + !isDeeplyEqual(timelineActivitiesNetworking.noActivities, noActivities) + ) { + setTimelineActivitiesNetworking({ + initialized, + noActivities, + }); + } + }, [ + activities, + initialized, + noActivities, + setTimelineActivitiesNetworking, + targetableObject, + timelineActivitiesNetworking, + timelineActivitiesForGroup, + setTimelineActivitiesForGroup, + ]); + + const updateTimelineActivities = useRecoilCallback( + ({ snapshot, set }) => + (newActivities: Activity[]) => { + for (const newActivity of newActivities) { + const currentActivity = snapshot + .getLoadable(timelineActivitiesFammilyState(newActivity.id)) + .getValue(); + + if (!isDeeplyEqual(newActivity, currentActivity)) { + set(timelineActivitiesFammilyState(newActivity.id), newActivity); + } + + const currentActivityWithoutTarget = snapshot + .getLoadable( + timelineActivityWithoutTargetsFamilyState(newActivity.id), + ) + .getValue(); + + const newActivityWithoutTarget = { + id: newActivity.id, + title: newActivity.title, + createdAt: newActivity.createdAt, + author: newActivity.author, + type: newActivity.type, + }; + + if ( + !isDeeplyEqual( + newActivityWithoutTarget, + currentActivityWithoutTarget, + ) + ) { + set( + timelineActivityWithoutTargetsFamilyState(newActivity.id), + newActivityWithoutTarget, + ); + } + } + }, + [], + ); + + useEffect(() => { + updateTimelineActivities(activities); + }, [activities, updateTimelineActivities]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx index f405ab2dc..98a0933f5 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelingeActivityGroup.tsx @@ -68,7 +68,7 @@ export const TimelineActivityGroup = ({ {group.items.map((activity, index) => ( ))} 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 a6b5a4cbb..1e6b97eea 100644 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts @@ -4,7 +4,7 @@ import { useRecoilCallback, useRecoilState } from 'recoil'; import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject'; -import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; import { Activity } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; diff --git a/packages/twenty-front/src/modules/activities/timeline/states/objectShowPageTargetableObjectState.ts b/packages/twenty-front/src/modules/activities/timeline/states/objectShowPageTargetableObjectIdState.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timeline/states/objectShowPageTargetableObjectState.ts rename to packages/twenty-front/src/modules/activities/timeline/states/objectShowPageTargetableObjectIdState.ts diff --git a/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesFamilyState.ts b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesFamilyState.ts new file mode 100644 index 000000000..de045ed03 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesFamilyState.ts @@ -0,0 +1,11 @@ +import { atomFamily } from 'recoil'; + +import { Activity } from '@/activities/types/Activity'; + +export const timelineActivitiesFammilyState = atomFamily< + Activity | null, + string +>({ + key: 'timelineActivitiesFammilyState', + default: null, +}); diff --git a/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesForGroupState.ts b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesForGroupState.ts new file mode 100644 index 000000000..ddbea64f2 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesForGroupState.ts @@ -0,0 +1,10 @@ +import { atom } from 'recoil'; + +import { ActivityForActivityGroup } from '@/activities/timeline/utils/groupActivitiesByMonth'; + +export const timelineActivitiesForGroupState = atom( + { + key: 'timelineActivitiesForGroupState', + default: [], + }, +); diff --git a/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesNetworkingState.ts b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesNetworkingState.ts new file mode 100644 index 000000000..e3ca7a837 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivitiesNetworkingState.ts @@ -0,0 +1,12 @@ +import { atom } from 'recoil'; + +export const timelineActivitiesNetworkingState = atom<{ + initialized: boolean; + noActivities: boolean; +}>({ + key: 'timelineActivitiesNetworkingState', + default: { + initialized: false, + noActivities: false, + }, +}); diff --git a/packages/twenty-front/src/modules/activities/timeline/states/timelineActivityFirstLevelFamilySelector.ts b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivityFirstLevelFamilySelector.ts new file mode 100644 index 000000000..767bb5e8f --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/states/timelineActivityFirstLevelFamilySelector.ts @@ -0,0 +1,11 @@ +import { atomFamily } from 'recoil'; + +import { Activity } from '@/activities/types/Activity'; + +export const timelineActivityWithoutTargetsFamilyState = atomFamily< + Pick | null, + string +>({ + key: 'timelineActivityFirstLevelFamilySelector', + default: null, +}); diff --git a/packages/twenty-front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts b/packages/twenty-front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts index 2dcc43bc7..c93550dc0 100644 --- a/packages/twenty-front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts +++ b/packages/twenty-front/src/modules/activities/timeline/utils/groupActivitiesByMonth.ts @@ -1,13 +1,18 @@ -import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer'; +import { Activity } from '@/activities/types/Activity'; -export interface ActivityGroup { +export type ActivityForActivityGroup = Pick; + +export type ActivityGroup = { month: number; year: number; - items: ActivityForDrawer[]; -} + items: ActivityForActivityGroup[]; +}; -export const groupActivitiesByMonth = (activities: ActivityForDrawer[]) => { +export const groupActivitiesByMonth = ( + activities: ActivityForActivityGroup[], +) => { const acitivityGroups: ActivityGroup[] = []; + for (const activity of activities) { const d = new Date(activity.createdAt); const month = d.getMonth(); diff --git a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts index c68fca98d..22d25858f 100644 --- a/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts +++ b/packages/twenty-front/src/modules/activities/types/ActivityTargetableEntity.ts @@ -1,8 +1,5 @@ -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; - export type ActivityTargetableObject = { id: string; targetObjectNameSingular: string; - targetObjectRecord: ObjectRecord; relatedTargetableObjects?: ActivityTargetableObject[]; }; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts index fab80703c..20b8d009c 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts @@ -4,13 +4,16 @@ import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -export const getActivityTargetsToCreateFromTargetableObjects = ({ +export const makeActivityTargetsToCreateFromTargetableObjects = ({ targetableObjects, activityId, + targetObjectRecords, }: { targetableObjects: ActivityTargetableObject[]; activityId: string; + targetObjectRecords: ObjectRecord[]; }): Partial[] => { const activityTargetableObjects = targetableObjects ? flattenTargetableObjectsAndTheirRelatedTargetableObjects( @@ -24,9 +27,12 @@ export const getActivityTargetsToCreateFromTargetableObjects = ({ nameSingular: targetableObject.targetObjectNameSingular, }); + const relatedObjectRecord = targetObjectRecords.find( + (record) => record.id === targetableObject.id, + ); + const activityTarget = { - [targetableObject.targetObjectNameSingular]: - targetableObject.targetObjectRecord, + [targetableObject.targetObjectNameSingular]: relatedObjectRecord, [targetableObjectFieldIdName]: targetableObject.id, activityId, id: v4(), diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 95a7e2e25..15bc43d08 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -196,7 +196,7 @@ export const CommandMenu = () => { id: activity.id, label: activity.title ?? '', to: '', - onCommandClick: () => openActivityRightDrawer(activity), + onCommandClick: () => openActivityRightDrawer(activity.id), })), [activities, openActivityRightDrawer], ); @@ -372,7 +372,7 @@ export const CommandMenu = () => { Icon={IconNotes} key={activity.id} label={activity.title ?? ''} - onClick={() => openActivityRightDrawer(activity)} + onClick={() => openActivityRightDrawer(activity.id)} /> ))} diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx index de94c04d4..f8a437ed7 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx @@ -1,7 +1,8 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount'; +import { CurrentUserDueTaskCountEffect } from '@/activities/tasks/components/CurrentUserDueTaskCountEffect'; +import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { Favorites } from '@/favorites/components/Favorites'; import { ObjectMetadataNavItems } from '@/object-metadata/components/ObjectMetadataNavItems'; @@ -23,7 +24,7 @@ export const MainNavigationDrawerItems = () => { const isMobile = useIsMobile(); const { toggleCommandMenu } = useCommandMenu(); const isTasksPage = useIsTasksPage(); - const { currentUserDueTaskCount } = useCurrentUserTaskCount(); + const currentUserDueTaskCount = useRecoilValue(currentUserDueTaskCountState); const navigate = useNavigate(); const location = useLocation(); const setNavigationMemorizedUrl = useSetRecoilState( @@ -54,6 +55,7 @@ export const MainNavigationDrawerItems = () => { }} Icon={IconSettings} /> + { - const { - objectMetadataItem, - labelIdentifierFieldMetadata, - mapToObjectRecordIdentifier, - } = useObjectMetadataItem({ - objectNameSingular, - }); + const { objectMetadataItem, labelIdentifierFieldMetadata } = + useObjectMetadataItem({ + objectNameSingular, + }); - const setEntityFields = useSetRecoilState( + const [recordLoading] = useRecoilState( + recordLoadingFamilyState(objectRecordId), + ); + + const [recordFromStore] = useRecoilState( recordStoreFamilyState(objectRecordId), ); - const { record, loading } = useFindOneRecord({ - objectRecordId, - objectNameSingular, - depth: 3, - }); - - useEffect(() => { - if (!record) return; - setEntityFields(record); - }, [record, setEntityFields]); + const recordIdentifier = useRecoilValue( + recordStoreIdentifierFamilySelector({ + objectNameSingular, + recordId: objectRecordId, + }), + ); const [uploadImage] = useUploadImageMutation(); const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular }); @@ -96,12 +94,12 @@ export const RecordShowContainer = ({ if (!updateOneRecord) { return; } - if (!record) { + if (!recordFromStore) { return; } await updateOneRecord({ - idToUpdate: record.id, + idToUpdate: objectRecordId, updateOneRecordInput: { avatarUrl, }, @@ -132,23 +130,19 @@ export const RecordShowContainer = ({ - {!loading && !!record && ( + {!recordLoading && isDefined(recordFromStore) && ( <> } - avatarType={ - mapToObjectRecordIdentifier(record).avatarType ?? 'rounded' - } + avatarType={recordIdentifier?.avatarType ?? 'rounded'} onUploadPicture={ objectNameSingular === 'person' ? onUploadPicture : undefined } @@ -179,11 +171,11 @@ export const RecordShowContainer = ({ {inlineFieldMetadataItems.map((fieldMetadataItem, index) => ( ( )} - {record ? ( + {recordFromStore ? ( { + const { record, loading } = useFindOneRecord({ + objectRecordId, + objectNameSingular, + depth: 3, + }); + + const setRecordStore = useSetRecoilState( + recordStoreFamilyState(objectRecordId), + ); + + const [recordLoading, setRecordLoading] = useRecoilState( + recordLoadingFamilyState(objectRecordId), + ); + + useEffect(() => { + if (loading !== recordLoading) { + setRecordLoading(loading); + } + }, [loading, recordLoading, setRecordLoading]); + + const { makeActivityWithoutConnection } = useActivityConnectionUtils(); + + useEffect(() => { + if (!loading && isDefined(record)) { + const { activity: activityWithoutConnection } = + makeActivityWithoutConnection(record as any); + + setRecordStore(activityWithoutConnection as Activity); + } + }, [loading, record, setRecordStore, makeActivityWithoutConnection]); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/recordLoadingFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-store/states/recordLoadingFamilyState.ts new file mode 100644 index 000000000..68a97545e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-store/states/recordLoadingFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const recordLoadingFamilyState = atomFamily({ + key: 'recordLoadingFamilyState', + default: false, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts new file mode 100644 index 000000000..24502a288 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierSelector.ts @@ -0,0 +1,35 @@ +import { selectorFamily } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; + +export const recordStoreIdentifierFamilySelector = selectorFamily({ + key: 'recordStoreIdentifierFamilySelector', + get: + ({ + recordId, + objectNameSingular, + }: { + recordId: string; + objectNameSingular: string; + }) => + ({ get }) => { + const recordFromStore = get(recordStoreFamilyState(recordId)); + + const objectMetadataItems = get(objectMetadataItemsState); + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === objectNameSingular, + ); + + if (!objectMetadataItem || !recordFromStore) { + return null; + } + + return getObjectRecordIdentifier({ + objectMetadataItem: objectMetadataItem, + record: recordFromStore, + }); + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/utils/getChildRelationArray.ts b/packages/twenty-front/src/modules/object-record/utils/getChildRelationArray.ts new file mode 100644 index 000000000..f7a93507c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/getChildRelationArray.ts @@ -0,0 +1,14 @@ +import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; +import { isDefined } from '~/utils/isDefined'; + +export const getChildRelationArray = ({ + childRelation, +}: { + childRelation: any; +}) => { + if (isDefined(childRelation.edges) && Array.isArray(childRelation.edges)) { + return childRelation.edges.map((edge: ObjectRecordEdge) => edge.node); + } else { + return childRelation; + } +}; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx index 9fb0362fa..0934aa7cd 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx @@ -6,6 +6,7 @@ import { Attachments } from '@/activities/files/components/Attachments'; import { Notes } from '@/activities/notes/components/Notes'; import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks'; import { Timeline } from '@/activities/timeline/components/Timeline'; +import { TimelineQueryEffect } from '@/activities/timeline/components/TimelineQueryEffect'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -42,7 +43,10 @@ const StyledTabListContainer = styled.div` export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list'; type ShowPageRightContainerProps = { - targetableObject: ActivityTargetableObject; + targetableObject: Pick< + ActivityTargetableObject, + 'targetObjectNameSingular' | 'id' + >; timeline?: boolean; tasks?: boolean; notes?: boolean; @@ -114,7 +118,10 @@ export const ShowPageRightContainer = ({ {activeTabId === 'timeline' && ( - + <> + + + )} {activeTabId === 'tasks' && ( diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index b4cc3d408..44dfa369e 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -97,7 +97,6 @@ export const RecordShowPage = () => { activityTargetObject={{ id: record.id, targetObjectNameSingular: objectMetadataItem?.nameSingular, - targetObjectRecord: record, }} />