Can now update title
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing

This commit is contained in:
Kasper Juul Hermansen 2021-11-15 22:31:52 +01:00
parent 6ed71eb8f8
commit 1865425d38
Signed by: kjuulh
GPG Key ID: 0F95C140730F2F23
10 changed files with 251 additions and 8 deletions

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Todo.Api.Hubs.Models;
public record UpdateTodoRequest
{
[JsonPropertyName("id")]
public string Id { get; init; }
[JsonPropertyName("title")]
public string Title { get; init; }
[JsonPropertyName("status")]
public bool Status { get; init; }
[JsonPropertyName("project")]
public string Project { get; set; }
}

View File

@ -51,7 +51,6 @@ namespace Todo.Api.Hubs
await Clients.Caller.SendAsync("todos", serializedTodos); await Clients.Caller.SendAsync("todos", serializedTodos);
} }
public async Task GetInboxTodos() public async Task GetInboxTodos()
{ {
var todos = await _todoRepository.GetNotDoneTodos(); var todos = await _todoRepository.GetNotDoneTodos();
@ -61,5 +60,44 @@ namespace Todo.Api.Hubs
await Clients.Caller.SendAsync("getInboxTodos", serializedTodos); await Clients.Caller.SendAsync("getInboxTodos", serializedTodos);
} }
public async Task GetTodo(string todoId)
{
var todo = await _todoRepository.GetTodoByIdAsync(todoId);
var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
{
Id = todo.Id,
Project = todo.Project,
Status = todo.Status,
Title = todo.Title,
});
await Clients.Caller.SendAsync("getTodo", serializedTodo);
}
public async Task ReplaceTodo(string updateTodoRequest)
{
var updateTodo = JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest);
if (updateTodo is null)
throw new InvalidOperationException("Could not parse invalid updateTodo");
var updatedTodo = await _todoRepository.UpdateTodoAsync(new Core.Entities.Todo()
{
Id = updateTodo.Id,
Project = updateTodo.Project,
Status = updateTodo.Status,
Title = updateTodo.Title
});
var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
{
Id = updatedTodo.Id,
Project = updatedTodo.Project,
Status = updatedTodo.Status,
Title = updatedTodo.Title,
});
await Clients.Caller.SendAsync("getTodo", serializedTodo);
}
} }
} }

View File

@ -1,3 +1,4 @@
namespace Todo.Core.Interfaces.Persistence; namespace Todo.Core.Interfaces.Persistence;
public interface ITodoRepository public interface ITodoRepository
@ -6,4 +7,6 @@ public interface ITodoRepository
Task<IEnumerable<Entities.Todo>> GetTodosAsync(); Task<IEnumerable<Entities.Todo>> GetTodosAsync();
Task UpdateTodoStatus(string todoId, bool todoStatus); Task UpdateTodoStatus(string todoId, bool todoStatus);
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos(); Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
Task<Entities.Todo> GetTodoByIdAsync(string todoId);
Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo);
} }

View File

@ -44,4 +44,37 @@ public class TodoRepository : ITodoRepository
var todos = await GetTodosAsync(); var todos = await GetTodosAsync();
return todos.Where(t => t.Status == false); return todos.Where(t => t.Status == false);
} }
public async Task<Core.Entities.Todo> GetTodoByIdAsync(string todoId)
{
var todoCursor = await _todosCollection.FindAsync(f => f.Id == todoId);
var todo = await todoCursor.FirstOrDefaultAsync();
return new Core.Entities.Todo()
{
Id = todo.Id,
Project = todo.ProjectName,
Status = todo.Status,
Title = todo.Title
};
}
public async Task<Core.Entities.Todo> UpdateTodoAsync(Core.Entities.Todo todo)
{
var updatedTodo = await _todosCollection.FindOneAndReplaceAsync<MongoTodo>(f => f.Id == todo.Id, new MongoTodo()
{
Id = todo.Id,
Status = todo.Status,
Title = todo.Title,
ProjectName = todo.Project
});
return new Core.Entities.Todo()
{
Id = updatedTodo.Id,
Project = updatedTodo.ProjectName,
Status = updatedTodo.Status,
Title = updatedTodo.Title
};
}
} }

View File

@ -13,7 +13,7 @@ export const TodoCheckmark: FC<TodoCheckmarkProps> = (props) => (
} }
className={`todo-checkmark h-5 w-5 rounded-full border dark:border-gray-500 ${ className={`todo-checkmark h-5 w-5 rounded-full border dark:border-gray-500 ${
props.todo.status === StatusState.done props.todo.status === StatusState.done
? "dark:bg-gray-700" ? "dark:bg-gray-500"
: "hover:dark:bg-gray-600" : "hover:dark:bg-gray-600"
}`} }`}
/> />

View File

@ -2,6 +2,7 @@ import { Todo } from "@src/core/entities/todo";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { TodoCheckmark } from "@src/components/todos/todoCheckmark"; import { TodoCheckmark } from "@src/components/todos/todoCheckmark";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import Link from "next/link";
interface TodoItemProps { interface TodoItemProps {
todo: Todo; todo: Todo;
@ -23,9 +24,11 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<TodoCheckmark {...props} /> <TodoCheckmark {...props} />
<div className="flex flex-col md:flex-row flex-grow gap-0.5 md:gap-2 pr-6"> <div className="flex flex-col md:flex-row flex-grow gap-0.5 md:gap-2 pr-6">
<div className="flex-grow w-full break-all text-sm"> <Link href={`/todos/${props.todo.id}`} passHref>
{props.todo.title} <a className="flex-grow w-full break-all text-sm">
</div> {props.todo.title}
</a>
</Link>
<div> <div>
{props.displayProject && props.todo.project && ( {props.displayProject && props.todo.project && (
<div className="text-gray-500 text-xs text-right whitespace-nowrap place-self-end"> <div className="text-gray-500 text-xs text-right whitespace-nowrap place-self-end">
@ -51,7 +54,9 @@ export const TodoItem: FC<TodoItemProps> = (props) => {
tabIndex={0} tabIndex={0}
> >
<button className="hover:bg-accent-500">Delete</button> <button className="hover:bg-accent-500">Delete</button>
<button>Edit</button> <Link href={`/todos/${props.todo.id}`} passHref>
<a>Edit</a>
</Link>
</div> </div>
} }
> >

View File

@ -22,7 +22,7 @@ export const asTodo = (item: Todo): Todo => {
throw new Error("Validation failed: title is null"); throw new Error("Validation failed: title is null");
} }
if (!!item.status) { if (typeof item.status === "undefined") {
throw new Error("Validation failed: status is null"); throw new Error("Validation failed: status is null");
} }

View File

@ -0,0 +1,105 @@
import { useRouter } from "next/router";
import {
useSelectTodo,
useUpdateTodo,
} from "@src/presentation/hooks/socketHooks";
import { Todo } from "@src/core/entities/todo";
import { FC, useEffect, useState } from "react";
import { PageHeading } from "@src/components/common/headings/pageHeading";
import { TodoCheckmark } from "@src/components/todos/todoCheckmark";
interface EditTodoProps {
todo: Todo;
onCancel: () => void;
onSave: (todo: Todo) => void;
}
const EditTodo: FC<EditTodoProps> = ({ todo, onCancel, onSave }) => {
const [todoTitle, setTodoTitle] = useState(todo.title);
return (
<div className="space-y-4 flex-grow">
<div className="bg-gray-900 rounded-lg">
<input
className="py-2 px-4"
value={todoTitle}
onChange={(e) => setTodoTitle(e.target.value)}
type="text"
/>
</div>
<div className="space-x-4">
<button
className="base-button dark:bg-accent-500 disabled:bg-accent-800 active:bg-accent-400"
onClick={() => {
onSave({ ...todo, title: todoTitle });
}}
>
Save
</button>
<button className="base-button" onClick={() => onCancel()}>
Cancel
</button>
</div>
</div>
);
};
interface TodoDetailsProps {
todo: Todo;
}
const TodoDetails: FC<TodoDetailsProps> = ({ todo }) => {
const [updatedTodo, setUpdatedTodo] = useState(todo);
const { updateTodo } = useUpdateTodo();
const [editMode, setEditMode] = useState(false);
useEffect(() => {
updateTodo(updatedTodo);
}, [updatedTodo]);
return (
<div className="bg-gray-800 rounded-xl p-8 shadow-lg space-y-4">
<PageHeading title={updatedTodo.project || "Inbox"} />
<div className="flex flex-row items-center gap-4">
{editMode ? (
<EditTodo
todo={todo}
onCancel={() => setEditMode(false)}
onSave={(todo) => {
setUpdatedTodo(todo);
setEditMode(false);
}}
/>
) : (
<>
<TodoCheckmark
updateTodo={(t) => {
setUpdatedTodo(t);
}}
todo={updatedTodo}
/>
<h4 className="flex-grow" onClick={() => setEditMode(true)}>
{updatedTodo.title}
</h4>
</>
)}
</div>
</div>
);
};
const TodoDetailsPage = () => {
const router = useRouter();
const { todoId } = router.query as { todoId: string };
const { todo } = useSelectTodo(todoId);
return (
<main className="py-8 px-14 space-y-6">
<section className="space-y-8 mx-auto max-w-4xl">
{todo ? <TodoDetails todo={todo} /> : <div>Todo was not found</div>}
</section>
</main>
);
};
export default TodoDetailsPage;

View File

@ -14,6 +14,8 @@ interface SocketContextProps {
getInboxTodos: () => void; getInboxTodos: () => void;
createTodo: (todoName: string, project: string) => void; createTodo: (todoName: string, project: string) => void;
updateTodo: (todoId: string, todoStatus: StatusState) => void; updateTodo: (todoId: string, todoStatus: StatusState) => void;
getTodoById(todoId: string): void;
replaceTodo(todo: Todo): void;
} }
export const SocketContext = createContext<SocketContextProps>({ export const SocketContext = createContext<SocketContextProps>({
@ -24,6 +26,8 @@ export const SocketContext = createContext<SocketContextProps>({
getInboxTodos: () => {}, getInboxTodos: () => {},
createTodo: (todoName, project) => {}, createTodo: (todoName, project) => {},
updateTodo: (todoId, todoStatus) => {}, updateTodo: (todoId, todoStatus) => {},
getTodoById(todoId: string) {},
replaceTodo(todo: Todo) {},
}); });
export const SocketProvider: FC = (props) => { export const SocketProvider: FC = (props) => {
@ -55,6 +59,15 @@ export const SocketProvider: FC = (props) => {
setInboxTodos(validatedTodos); setInboxTodos(validatedTodos);
}); });
connection.on("getTodo", (todo) => {
const newTodo = JSON.parse(todo) as Todo;
const validatedTodo = asTodo(newTodo);
setTodos([
...todos.filter((t) => t.id != validatedTodo.id),
validatedTodo,
]);
});
connection.start().then(() => { connection.start().then(() => {
setConn(connection); setConn(connection);
}); });
@ -78,6 +91,12 @@ export const SocketProvider: FC = (props) => {
updateTodo: (todoId, todoStatus) => { updateTodo: (todoId, todoStatus) => {
conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error); conn.invoke("UpdateTodo", todoId, todoStatus).catch(console.error);
}, },
getTodoById(todoId: string) {
conn.invoke("GetTodo", todoId).catch(console.error);
},
replaceTodo: (todo) => {
conn.invoke("ReplaceTodo", JSON.stringify(todo)).catch(console.error);
},
}} }}
> >
{conn ? props.children : "loading"} {conn ? props.children : "loading"}

View File

@ -1,6 +1,6 @@
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { SocketContext } from "@src/presentation/contexts/SocketContext"; import { SocketContext } from "@src/presentation/contexts/SocketContext";
import { StatusState } from "@src/core/entities/todo"; import { StatusState, Todo } from "@src/core/entities/todo";
export const useSelectInboxTodos = () => { export const useSelectInboxTodos = () => {
const socketContext = useContext(SocketContext); const socketContext = useContext(SocketContext);
@ -34,3 +34,25 @@ export const useUpdateTodoState = () => {
}, },
}; };
}; };
export const useSelectTodo = (todoId: string): { todo: Todo | undefined } => {
const socketContext = useContext(SocketContext);
useEffect(() => {
socketContext.getTodoById(todoId);
}, []);
return {
todo: socketContext.todos.find((t) => t.id === todoId),
};
};
export const useUpdateTodo = () => {
const socketContext = useContext(SocketContext);
return {
updateTodo: (todo: Todo) => {
socketContext.replaceTodo(todo);
},
};
};