Update with more optimistic updates

This commit is contained in:
Kasper Juul Hermansen 2021-11-21 16:52:56 +01:00
parent bb99f99a22
commit 3c5fe488cd
Signed by: kjuulh
GPG Key ID: 0F95C140730F2F23
11 changed files with 80 additions and 2874 deletions

View File

@ -37,11 +37,15 @@ public class TodosController : ApiController
[HttpPost] [HttpPost]
public async Task<ActionResult<string>> CreateTodo( public async Task<ActionResult<string>> CreateTodo(
[FromBody] CreateTodoRequest request) [FromBody] CreateTodoRequest request)
=> Ok(await Mediator.Send(request.To())); {
return Ok(await Mediator.Send(request.To()));
}
[HttpGet("{todoId}")] [HttpGet("{todoId}")]
public async Task<ActionResult<TodoViewModel>> GetTodoById([FromRoute] string todoId) public async Task<ActionResult<TodoViewModel>> GetTodoById([FromRoute] string todoId)
=> await Mediator.Send(new GetTodoByIdQuery(todoId)); {
return await Mediator.Send(new GetTodoByIdQuery(todoId));
}
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<TodoViewModel>>> GetTodos([FromQuery] bool onlyActive = false) public async Task<ActionResult<IEnumerable<TodoViewModel>>> GetTodos([FromQuery] bool onlyActive = false)

View File

@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.Threading; using System.Threading;
using MediatR; using MediatR;
using Todo.Core.Application.Notifications.Todo; using Todo.Core.Application.Notifications.Todo;
using Todo.Core.Application.Queries.Todos;
using Todo.Core.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
using Todo.Core.Interfaces.User; using Todo.Core.Interfaces.User;
@ -10,9 +11,9 @@ namespace Todo.Core.Application.Commands.Todo;
public record CreateTodoCommand( public record CreateTodoCommand(
[Required] string TodoTitle, [Required] string TodoTitle,
string? TodoProject, string? TodoProject,
string? TodoDescription) : IRequest<string> string? TodoDescription) : IRequest<TodoViewModel>
{ {
internal class Handler : IRequestHandler<CreateTodoCommand, string> internal class Handler : IRequestHandler<CreateTodoCommand, TodoViewModel>
{ {
private readonly ICurrentUserService _currentUserService; private readonly ICurrentUserService _currentUserService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
@ -28,7 +29,7 @@ public record CreateTodoCommand(
_mediator = mediator; _mediator = mediator;
} }
public async Task<string> Handle( public async Task<TodoViewModel> Handle(
CreateTodoCommand request, CreateTodoCommand request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -47,7 +48,7 @@ public record CreateTodoCommand(
new TodoCreated(todo.Id), new TodoCreated(todo.Id),
cancellationToken); cancellationToken);
return todo.Id; return TodoViewModel.From(todo);
} }
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"sw.js","sources":["../../../../../../../../tmp/2966604b4704df68e7ceebb70ae0c809/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/home/kjuulh/git/git.front.kjuulh.io/kjuulh/todo/src/client/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/home/kjuulh/git/git.front.kjuulh.io/kjuulh/todo/src/client/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/home/kjuulh/git/git.front.kjuulh.io/kjuulh/todo/src/client/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/home/kjuulh/git/git.front.kjuulh.io/kjuulh/todo/src/client/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({request, response, event, state}) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, {status: 200, statusText: 'OK', headers: response.headers}); } return response; } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGiK;EACjK;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAGAA,aAAa;EAUbC,IAAI,CAACC,WAAL;AAEAC,sBAAyB;AAIzBC,uBAA6B,CAAC,GAAD,EAAM,IAAIC,oBAAJ,CAAoC;EAAE,eAAY,WAAd;EAA2BC,EAAAA,OAAO,EAAE,CAAC;EAAEC,IAAAA,eAAe,EAAE,OAAO;EAACC,MAAAA,OAAD;EAAUC,MAAAA,QAAV;EAAoBC,MAAAA,KAApB;EAA2BC,MAAAA;EAA3B,KAAP,KAA6C;EAAE,UAAIF,QAAQ,IAAIA,QAAQ,CAACG,IAAT,KAAkB,gBAAlC,EAAoD;EAAE,eAAO,IAAIC,QAAJ,CAAaJ,QAAQ,CAACK,IAAtB,EAA4B;EAACC,UAAAA,MAAM,EAAE,GAAT;EAAcC,UAAAA,UAAU,EAAE,IAA1B;EAAgCC,UAAAA,OAAO,EAAER,QAAQ,CAACQ;EAAlD,SAA5B,CAAP;EAAiG;;EAAC,aAAOR,QAAP;EAAkB;EAA5O,GAAD;EAApC,CAApC,CAAN,EAAmU,KAAnU,CAA7B;AACAL,uBAA6B,CAAC,KAAD,EAAQ,IAAIc,mBAAJ,CAAmC;EAAE,eAAY,KAAd;EAAqBZ,EAAAA,OAAO,EAAE;EAA9B,CAAnC,CAAR,EAAgF,KAAhF,CAA7B;;"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -21,7 +21,9 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
<div <div
className="py-3 border-b border-gray-300 dark:border-gray-600 relative" className="py-3 border-b border-gray-300 dark:border-gray-600 relative"
onMouseEnter={() => { onMouseEnter={() => {
prefetchTodo(props.todo.id); if (!props.todo.id.startsWith("temp")) {
prefetchTodo(props.todo.id);
}
setIsHovering(true); setIsHovering(true);
}} }}
onMouseLeave={() => setIsHovering(false)} onMouseLeave={() => setIsHovering(false)}

View File

@ -1,6 +1,7 @@
import { createApi } from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQueryWithReauth } from "@src/infrastructure/apis/utilities"; import { baseQueryWithReauth } from "@src/infrastructure/apis/utilities";
import { Todo } from "@src/core/entities/todo"; import { Todo } from "@src/core/entities/todo";
import { nanoid } from "@reduxjs/toolkit";
export const todosApi = createApi({ export const todosApi = createApi({
reducerPath: "api", reducerPath: "api",
@ -12,7 +13,7 @@ export const todosApi = createApi({
providesTags: (result, error, id) => [{ type: "todo", id }], providesTags: (result, error, id) => [{ type: "todo", id }],
}), }),
getAllTodos: build.query<Todo[], void>({ getAllTodos: build.query<Todo[], string>({
query: () => ({ url: `api/todos` }), query: () => ({ url: `api/todos` }),
providesTags: (result) => providesTags: (result) =>
result result
@ -39,7 +40,7 @@ export const todosApi = createApi({
: [], : [],
}), }),
createTodo: build.mutation<string, Partial<Todo>>({ createTodo: build.mutation<Todo, Todo>({
query(body) { query(body) {
return { return {
url: "/api/todos", url: "/api/todos",
@ -47,7 +48,44 @@ export const todosApi = createApi({
body, body,
}; };
}, },
invalidatesTags: (result, error, id) => [{ type: "todo", id: "LIST" }], invalidatesTags: (result, error, id) => [{ type: "todo", id: result.id }],
async onQueryStarted({ id, ...body }, { dispatch, queryFulfilled }) {
const tempId = nanoid();
const replaceGetAllResult = dispatch(
todosApi.util.updateQueryData(
"getActiveTodos",
undefined,
(draft) => {
const todo: Todo = { id: "temp" + tempId, ...body };
if (todo) {
draft.push(todo);
}
}
)
);
try {
const data = await queryFulfilled;
const finishUpdated = dispatch(
todosApi.util.updateQueryData(
"getActiveTodos",
undefined,
(draft) => {
const todo = draft.find((t) => t.id == "temp" + tempId);
if (todo) {
Object.assign(todo, data.data);
}
}
)
);
} catch (e) {
console.error(e);
replaceGetAllResult.undo();
dispatch(
todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }])
);
}
},
}), }),
replaceTodo: build.mutation<string, Partial<Todo>>({ replaceTodo: build.mutation<string, Partial<Todo>>({
@ -67,11 +105,28 @@ export const todosApi = createApi({
}) })
); );
const replaceGetAllResult = dispatch(
todosApi.util.updateQueryData(
"getActiveTodos",
undefined,
(draft) => {
const todo = draft.find((t) => t.id === id);
if (todo) {
Object.assign(todo, body);
}
}
)
);
try { try {
await queryFulfilled; await queryFulfilled;
} catch { } catch {
replaceGetAllResult.undo();
replaceResult.undo(); replaceResult.undo();
dispatch(todosApi.util.invalidateTags([{ type: "todo", id }])); dispatch(todosApi.util.invalidateTags([{ type: "todo", id }]));
dispatch(
todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }])
);
} }
}, },
}), }),

View File

@ -25,7 +25,7 @@ export const SocketProvider: FC = (props) => {
const { todoId } = JSON.parse(todoCreated) as { todoId: string }; const { todoId } = JSON.parse(todoCreated) as { todoId: string };
if (todoId) { if (todoId) {
dispatch(todosApi.endpoints.getTodoById.initiate(todoId)); dispatch(todosApi.endpoints.getTodoById.initiate(todoId));
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }])); // dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }]));
} }
}); });
@ -33,7 +33,7 @@ export const SocketProvider: FC = (props) => {
const { todoId } = JSON.parse(todoUpdated) as { todoId: string }; const { todoId } = JSON.parse(todoUpdated) as { todoId: string };
if (todoId) { if (todoId) {
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: todoId }])); dispatch(todosApi.util.invalidateTags([{ type: "todo", id: todoId }]));
dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }])); // dispatch(todosApi.util.invalidateTags([{ type: "todo", id: "LIST" }]));
} }
}); });

View File

@ -3,6 +3,7 @@ import { useAppSelector } from "@src/infrastructure/store";
import { todosSelectors } from "@src/infrastructure/state/todo"; import { todosSelectors } from "@src/infrastructure/state/todo";
import { todosApi } from "@src/infrastructure/apis/todosApi"; import { todosApi } from "@src/infrastructure/apis/todosApi";
import { Todo } from "@src/core/entities/todo"; import { Todo } from "@src/core/entities/todo";
import { nanoid } from "@reduxjs/toolkit";
export const useSelectInboxTodos = () => { export const useSelectInboxTodos = () => {
const todos = useAppSelector(todosSelectors.selectAll); const todos = useAppSelector(todosSelectors.selectAll);
@ -19,6 +20,9 @@ export const useCreateTodo = () => {
return { return {
createTodo: (todoName: string, project: string, description: string) => { createTodo: (todoName: string, project: string, description: string) => {
createTodo({ createTodo({
id: nanoid(),
created: 0,
status: false,
title: todoName, title: todoName,
project, project,
description, description,