From db8a17634222893fbe7b2cbe9778bc174b52c2d2 Mon Sep 17 00:00:00 2001 From: Emilien Chauvet Date: Wed, 9 Aug 2023 20:05:08 +0200 Subject: [PATCH] Add optimistic rendering for tasks (#1140) * Add optimistic rendering for tasks * Refetch activities for lists updates --- .../components/ActivityAssigneePicker.tsx | 27 ++++++++++++-- .../components/ActivityBodyEditor.tsx | 20 ++++++++--- .../activities/components/ActivityEditor.tsx | 36 ++++++++++++++----- .../activities/hooks/useCompleteTask.ts | 33 +++++++++++------ .../src/modules/activities/queries/update.ts | 30 +++++++++------- 5 files changed, 109 insertions(+), 37 deletions(-) diff --git a/front/src/modules/activities/components/ActivityAssigneePicker.tsx b/front/src/modules/activities/components/ActivityAssigneePicker.tsx index 17772b26e..5580cb1dd 100644 --- a/front/src/modules/activities/components/ActivityAssigneePicker.tsx +++ b/front/src/modules/activities/components/ActivityAssigneePicker.tsx @@ -1,3 +1,5 @@ +import { useApolloClient } from '@apollo/client'; + import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; @@ -11,6 +13,8 @@ import { useUpdateActivityMutation, } from '~/generated/graphql'; +import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update'; + export type OwnProps = { activity: Pick & { accountOwner?: Pick | null; @@ -41,6 +45,8 @@ export function ActivityAssigneePicker({ entityType: Entity.User, id: user.id, name: user.displayName, + firstName: user.firstName, + lastName: user.lastName, avatarType: 'rounded', avatarUrl: user.avatarUrl ?? '', }), @@ -48,17 +54,34 @@ export function ActivityAssigneePicker({ searchOnFields: ['firstName', 'lastName'], }); - async function handleEntitySelected( + const client = useApolloClient(); + const cachedActivity = client.readFragment({ + id: `Activity:${activity.id}`, + fragment: ACTIVITY_UPDATE_FRAGMENT, + }); + + function handleEntitySelected( selectedUser: UserForSelect | null | undefined, ) { if (selectedUser) { - await updateActivity({ + updateActivity({ variables: { where: { id: activity.id }, data: { assignee: { connect: { id: selectedUser.id } }, }, }, + optimisticResponse: { + __typename: 'Mutation', + updateOneActivity: { + ...cachedActivity, + assignee: { + __typename: 'User', + ...selectedUser, + displayName: selectedUser.name, + }, + }, + }, }); } diff --git a/front/src/modules/activities/components/ActivityBodyEditor.tsx b/front/src/modules/activities/components/ActivityBodyEditor.tsx index 61f7ee0a0..ea9c0c483 100644 --- a/front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { getOperationName } from '@apollo/client/utilities'; +import { useApolloClient } from '@apollo/client'; import { BlockNoteEditor } from '@blocknote/core'; import { useBlockNote } from '@blocknote/react'; import styled from '@emotion/styled'; @@ -8,7 +8,7 @@ import debounce from 'lodash.debounce'; import { BlockEditor } from '@/ui/editor/components/BlockEditor'; import { Activity, useUpdateActivityMutation } from '~/generated/graphql'; -import { GET_ACTIVITIES_BY_TARGETS } from '../queries/select'; +import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update'; const BlockNoteStyledContainer = styled.div` width: 100%; @@ -22,6 +22,12 @@ type OwnProps = { export function ActivityBodyEditor({ activity, onChange }: OwnProps) { const [updateActivityMutation] = useUpdateActivityMutation(); + const client = useApolloClient(); + const cachedActivity = client.readFragment({ + id: `Activity:${activity.id}`, + fragment: ACTIVITY_UPDATE_FRAGMENT, + }); + const [body, setBody] = useState(null); useEffect(() => { @@ -42,12 +48,18 @@ export function ActivityBodyEditor({ activity, onChange }: OwnProps) { body: activityBody, }, }, - refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], + optimisticResponse: { + __typename: 'Mutation', + updateOneActivity: { + ...cachedActivity, + body: activityBody, + }, + }, }); } return debounce(onInternalChange, 200); - }, [activity, updateActivityMutation, setBody]); + }, [updateActivityMutation, activity.id, cachedActivity]); const editor: BlockNoteEditor | null = useBlockNote({ initialContent: activity.body ? JSON.parse(activity.body) : undefined, diff --git a/front/src/modules/activities/components/ActivityEditor.tsx b/front/src/modules/activities/components/ActivityEditor.tsx index fed3cfb7d..dcf575792 100644 --- a/front/src/modules/activities/components/ActivityEditor.tsx +++ b/front/src/modules/activities/components/ActivityEditor.tsx @@ -1,14 +1,12 @@ import React, { useCallback, useState } from 'react'; +import { useApolloClient } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import styled from '@emotion/styled'; import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor'; import { ActivityComments } from '@/activities/components/ActivityComments'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; -import { - GET_ACTIVITIES, - GET_ACTIVITIES_BY_TARGETS, -} from '@/activities/queries'; +import { GET_ACTIVITIES } from '@/activities/queries'; import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField'; import { IconCalendar } from '@/ui/icon/index'; @@ -24,6 +22,7 @@ import { debounce } from '~/utils/debounce'; import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField'; import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField'; +import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update'; import { CommentForDrawer } from '../types/CommentForDrawer'; import { ActivityTitle } from './ActivityTitle'; @@ -96,6 +95,12 @@ export function ActivityEditor({ const [updateActivityMutation] = useUpdateActivityMutation(); + const client = useApolloClient(); + const cachedActivity = client.readFragment({ + id: `Activity:${activity.id}`, + fragment: ACTIVITY_UPDATE_FRAGMENT, + }); + const updateTitle = useCallback( (newTitle: string) => { updateActivityMutation({ @@ -107,10 +112,17 @@ export function ActivityEditor({ title: newTitle ?? '', }, }, - refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], + optimisticResponse: { + __typename: 'Mutation', + updateOneActivity: { + __typename: 'Activity', + ...cachedActivity, + title: newTitle, + }, + }, }); }, - [activity, updateActivityMutation], + [activity.id, cachedActivity, updateActivityMutation], ); const handleActivityCompletionChange = useCallback( @@ -124,11 +136,19 @@ export function ActivityEditor({ completedAt: value ? new Date().toISOString() : null, }, }, - refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], + optimisticResponse: { + __typename: 'Mutation', + updateOneActivity: { + __typename: 'Activity', + ...cachedActivity, + completedAt: value ? new Date().toISOString() : null, + }, + }, + refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''], }); setCompletedAt(value ? new Date().toISOString() : null); }, - [activity, updateActivityMutation], + [activity.id, cachedActivity, updateActivityMutation], ); const debouncedUpdateTitle = debounce(updateTitle, 200); diff --git a/front/src/modules/activities/hooks/useCompleteTask.ts b/front/src/modules/activities/hooks/useCompleteTask.ts index 8ca77eae0..9b535c259 100644 --- a/front/src/modules/activities/hooks/useCompleteTask.ts +++ b/front/src/modules/activities/hooks/useCompleteTask.ts @@ -1,32 +1,43 @@ import { useCallback } from 'react'; -import { getOperationName } from '@apollo/client/utilities'; +import { useApolloClient } from '@apollo/client'; -import { - GET_ACTIVITIES, - GET_ACTIVITIES_BY_TARGETS, -} from '@/activities/queries'; import { Activity, useUpdateActivityMutation } from '~/generated/graphql'; +import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update'; + type Task = Pick; export function useCompleteTask(task: Task) { const [updateActivityMutation] = useUpdateActivityMutation(); + + const client = useApolloClient(); + const cachedTask = client.readFragment({ + id: `Activity:${task.id}`, + fragment: ACTIVITY_UPDATE_FRAGMENT, + }); + + console.log('cachedTask', cachedTask); + const completeTask = useCallback( (value: boolean) => { + const completedAt = value ? new Date().toISOString() : null; updateActivityMutation({ variables: { where: { id: task.id }, data: { - completedAt: value ? new Date().toISOString() : null, + completedAt, + }, + }, + optimisticResponse: { + __typename: 'Mutation', + updateOneActivity: { + ...cachedTask, + completedAt, }, }, - refetchQueries: [ - getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '', - getOperationName(GET_ACTIVITIES) ?? '', - ], }); }, - [task, updateActivityMutation], + [cachedTask, task.id, updateActivityMutation], ); return { diff --git a/front/src/modules/activities/queries/update.ts b/front/src/modules/activities/queries/update.ts index f136dd41b..be716efac 100644 --- a/front/src/modules/activities/queries/update.ts +++ b/front/src/modules/activities/queries/update.ts @@ -60,24 +60,30 @@ export const DELETE_ACTIVITY = gql` } `; +export const ACTIVITY_UPDATE_FRAGMENT = gql` + fragment ActivityUpdateParts on Activity { + id + body + title + type + completedAt + dueAt + assignee { + id + firstName + lastName + displayName + } + } +`; + export const UPDATE_ACTIVITY = gql` mutation UpdateActivity( $where: ActivityWhereUniqueInput! $data: ActivityUpdateInput! ) { updateOneActivity(where: $where, data: $data) { - id - body - title - type - completedAt - dueAt - assignee { - id - firstName - lastName - displayName - } + ...ActivityUpdateParts } } `;