Add optimistic rendering for tasks (#1140)

* Add optimistic rendering for tasks

* Refetch activities for lists updates
This commit is contained in:
Emilien Chauvet
2023-08-09 20:05:08 +02:00
committed by GitHub
parent 702b6e5154
commit db8a176342
5 changed files with 109 additions and 37 deletions

View File

@ -1,3 +1,5 @@
import { useApolloClient } from '@apollo/client';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
@ -11,6 +13,8 @@ import {
useUpdateActivityMutation, useUpdateActivityMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update';
export type OwnProps = { export type OwnProps = {
activity: Pick<Activity, 'id'> & { activity: Pick<Activity, 'id'> & {
accountOwner?: Pick<User, 'id' | 'displayName'> | null; accountOwner?: Pick<User, 'id' | 'displayName'> | null;
@ -41,6 +45,8 @@ export function ActivityAssigneePicker({
entityType: Entity.User, entityType: Entity.User,
id: user.id, id: user.id,
name: user.displayName, name: user.displayName,
firstName: user.firstName,
lastName: user.lastName,
avatarType: 'rounded', avatarType: 'rounded',
avatarUrl: user.avatarUrl ?? '', avatarUrl: user.avatarUrl ?? '',
}), }),
@ -48,17 +54,34 @@ export function ActivityAssigneePicker({
searchOnFields: ['firstName', 'lastName'], 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, selectedUser: UserForSelect | null | undefined,
) { ) {
if (selectedUser) { if (selectedUser) {
await updateActivity({ updateActivity({
variables: { variables: {
where: { id: activity.id }, where: { id: activity.id },
data: { data: {
assignee: { connect: { id: selectedUser.id } }, assignee: { connect: { id: selectedUser.id } },
}, },
}, },
optimisticResponse: {
__typename: 'Mutation',
updateOneActivity: {
...cachedActivity,
assignee: {
__typename: 'User',
...selectedUser,
displayName: selectedUser.name,
},
},
},
}); });
} }

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities'; import { useApolloClient } from '@apollo/client';
import { BlockNoteEditor } from '@blocknote/core'; import { BlockNoteEditor } from '@blocknote/core';
import { useBlockNote } from '@blocknote/react'; import { useBlockNote } from '@blocknote/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -8,7 +8,7 @@ import debounce from 'lodash.debounce';
import { BlockEditor } from '@/ui/editor/components/BlockEditor'; import { BlockEditor } from '@/ui/editor/components/BlockEditor';
import { Activity, useUpdateActivityMutation } from '~/generated/graphql'; 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` const BlockNoteStyledContainer = styled.div`
width: 100%; width: 100%;
@ -22,6 +22,12 @@ type OwnProps = {
export function ActivityBodyEditor({ activity, onChange }: OwnProps) { export function ActivityBodyEditor({ activity, onChange }: OwnProps) {
const [updateActivityMutation] = useUpdateActivityMutation(); const [updateActivityMutation] = useUpdateActivityMutation();
const client = useApolloClient();
const cachedActivity = client.readFragment({
id: `Activity:${activity.id}`,
fragment: ACTIVITY_UPDATE_FRAGMENT,
});
const [body, setBody] = useState<string | null>(null); const [body, setBody] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@ -42,12 +48,18 @@ export function ActivityBodyEditor({ activity, onChange }: OwnProps) {
body: activityBody, body: activityBody,
}, },
}, },
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], optimisticResponse: {
__typename: 'Mutation',
updateOneActivity: {
...cachedActivity,
body: activityBody,
},
},
}); });
} }
return debounce(onInternalChange, 200); return debounce(onInternalChange, 200);
}, [activity, updateActivityMutation, setBody]); }, [updateActivityMutation, activity.id, cachedActivity]);
const editor: BlockNoteEditor | null = useBlockNote({ const editor: BlockNoteEditor | null = useBlockNote({
initialContent: activity.body ? JSON.parse(activity.body) : undefined, initialContent: activity.body ? JSON.parse(activity.body) : undefined,

View File

@ -1,14 +1,12 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor'; import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { ActivityComments } from '@/activities/components/ActivityComments'; import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { import { GET_ACTIVITIES } from '@/activities/queries';
GET_ACTIVITIES,
GET_ACTIVITIES_BY_TARGETS,
} from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField'; import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField';
import { IconCalendar } from '@/ui/icon/index'; import { IconCalendar } from '@/ui/icon/index';
@ -24,6 +22,7 @@ import { debounce } from '~/utils/debounce';
import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField'; import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField';
import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField'; import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField';
import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update';
import { CommentForDrawer } from '../types/CommentForDrawer'; import { CommentForDrawer } from '../types/CommentForDrawer';
import { ActivityTitle } from './ActivityTitle'; import { ActivityTitle } from './ActivityTitle';
@ -96,6 +95,12 @@ export function ActivityEditor({
const [updateActivityMutation] = useUpdateActivityMutation(); const [updateActivityMutation] = useUpdateActivityMutation();
const client = useApolloClient();
const cachedActivity = client.readFragment({
id: `Activity:${activity.id}`,
fragment: ACTIVITY_UPDATE_FRAGMENT,
});
const updateTitle = useCallback( const updateTitle = useCallback(
(newTitle: string) => { (newTitle: string) => {
updateActivityMutation({ updateActivityMutation({
@ -107,10 +112,17 @@ export function ActivityEditor({
title: newTitle ?? '', 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( const handleActivityCompletionChange = useCallback(
@ -124,11 +136,19 @@ export function ActivityEditor({
completedAt: value ? new Date().toISOString() : null, 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); setCompletedAt(value ? new Date().toISOString() : null);
}, },
[activity, updateActivityMutation], [activity.id, cachedActivity, updateActivityMutation],
); );
const debouncedUpdateTitle = debounce(updateTitle, 200); const debouncedUpdateTitle = debounce(updateTitle, 200);

View File

@ -1,32 +1,43 @@
import { useCallback } from 'react'; 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, useUpdateActivityMutation } from '~/generated/graphql';
import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update';
type Task = Pick<Activity, 'id'>; type Task = Pick<Activity, 'id'>;
export function useCompleteTask(task: Task) { export function useCompleteTask(task: Task) {
const [updateActivityMutation] = useUpdateActivityMutation(); 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( const completeTask = useCallback(
(value: boolean) => { (value: boolean) => {
const completedAt = value ? new Date().toISOString() : null;
updateActivityMutation({ updateActivityMutation({
variables: { variables: {
where: { id: task.id }, where: { id: task.id },
data: { 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 { return {

View File

@ -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` export const UPDATE_ACTIVITY = gql`
mutation UpdateActivity( mutation UpdateActivity(
$where: ActivityWhereUniqueInput! $where: ActivityWhereUniqueInput!
$data: ActivityUpdateInput! $data: ActivityUpdateInput!
) { ) {
updateOneActivity(where: $where, data: $data) { updateOneActivity(where: $where, data: $data) {
id ...ActivityUpdateParts
body
title
type
completedAt
dueAt
assignee {
id
firstName
lastName
displayName
}
} }
} }
`; `;