Skip to content

Commit

Permalink
feat: adds undo logic for tasks deletion in golang backend
Browse files Browse the repository at this point in the history
  • Loading branch information
tonitienda committed Apr 24, 2024
1 parent 4b9ef60 commit 718b4f3
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 4 deletions.
1 change: 1 addition & 0 deletions backend-golang-rest/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func setupRouter() *gin.Engine {
tasks.GET("", ErrorHandler(tasksHandler.GetTasks))
tasks.POST("", ErrorHandler(tasksHandler.AddTask))
tasks.DELETE("/:taskID", ErrorHandler(tasksHandler.DeleteTask))
tasks.POST("/:taskID/undo-delete", ErrorHandler(tasksHandler.UndoDeletion))
}

r.GET("/healthz", func(c *gin.Context) {
Expand Down
67 changes: 67 additions & 0 deletions backend-golang-rest/pkg/tasks/handlers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tasks

import (
"fmt"
"net/http"
"time"

Expand Down Expand Up @@ -95,6 +96,19 @@ func canDeleteTask(userId string, task Task) error {
return nil
}

func canUndoTaskDeletion(userId string, task Task) error {
if task.DeletedBy != userId {
return common.NewForbiddenError("You can only recover the tasks you deleted")
}

fmt.Println("task.DeletedAt:" + task.DeletedAt.Local().String())

if task.DeletedAt.IsZero() {
return common.NewStatusIncorrectError("The task is not deleted")
}
return nil
}

func (h *TasksHandler) DeleteTask(c *gin.Context) error {
userId := c.GetString("userId")

Expand Down Expand Up @@ -132,6 +146,59 @@ func (h *TasksHandler) DeleteTask(c *gin.Context) error {
return err
}

c.JSON(http.StatusAccepted, TaskDeletionResponse{
Url: fmt.Sprintf("/v0/tasks/%s/undo-delete", task.ID),
Method: "POST",
})
return nil
}

func (h *TasksHandler) UndoDeletion(c *gin.Context) error {
fmt.Println("Undoing deletion")
userId := c.GetString("userId")

if !common.IsValidUUID(userId) {
return common.NewValidationError("Invalid userId")
}
taskId, taskIdParamFound := c.Params.Get("taskID")

if !taskIdParamFound {
return common.NewValidationError("taskID not found")
}

if !common.IsValidUUID(taskId) {
return common.NewValidationError("Invalid taskID")
}
fmt.Println("Undeleting " + taskId)

task, taskFound := h.datasource.GetTask(taskId)

if !taskFound {
return common.NewNotFoundError("Task not found")
}

err := canUndoTaskDeletion(userId, task)

if err != nil {
return err
}

// TODO - See how to reset the "deletedAt"
task2 := Task{
ID: task.ID,
OwnerID: task.OwnerID,
Title: task.Title,
Description: task.Description,
Status: task.Status,
}
fmt.Println("Updating task ")

err = h.datasource.UpdateTask(task2)

if err != nil {
return err
}

c.JSON(http.StatusAccepted, nil)
return nil
}
5 changes: 5 additions & 0 deletions backend-golang-rest/pkg/tasks/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ type TaskResponse struct {
func NewTaskResponse(task Task) TaskResponse {
return TaskResponse(task)
}

type TaskDeletionResponse struct {
Method string `json:"method"`
Url string `json:"url"`
}
25 changes: 25 additions & 0 deletions e2e-cypress/nextjs/tasks.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ const deleteAllTasks = (cy) => {
});
};

const deleteFirstTask = (cy) => {
cy.get("ul#task-list").then((taskList) => {
if (taskList.find('li[id^="task-"]').length === 0) {
return;
}

taskList.find('li[id^="task-"]').find("button").click();
});
};

const undo = (cy) => {
cy.get("button").contains("Undo").click();
};

const assertNumTasks = (cy, numTasks) => {
cy.get("ul#task-list")
.find('li[id^="task-"]')
.should("have.length", numTasks);
};

describe("Managing tasks", () => {
it("should get task list", () => {
cy.log("should log in");
Expand Down Expand Up @@ -66,5 +86,10 @@ describe("Managing tasks", () => {

deleteAllTasks(cy);
addTasks(cy, 3);
assertNumTasks(cy, 3);
deleteFirstTask(cy);
assertNumTasks(cy, 2);
undo(cy);
assertNumTasks(cy, 3);
});
});
28 changes: 27 additions & 1 deletion webapp-nextjs/src/api/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
export type UndoAction = {
url: string;
method: string;
body?: any;
};

export async function undoRequest(undo: UndoAction) {
console.log("undoRequest:", undo);
const res = await fetch("/api/actions/undo", {
method: "POST",
body: JSON.stringify(undo),
});
// The return value is *not* serialized
// You can return Date, Map, Set, etc.

if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error("Failed to undo");
}
}

export async function getTasks() {
const res = await fetch("/api/tasks");

Expand Down Expand Up @@ -42,5 +63,10 @@ export async function deleteTask(taskID: string) {
throw new Error("Failed to delete task");
}

return;
// TODO - Validate response
const undo = (await res.json()) as UndoAction;

console.log("deleteTask:", undo);

return undo;
}
22 changes: 22 additions & 0 deletions webapp-nextjs/src/app/api/actions/undo/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { buildUrl, getAuthHeader } from "../../tools";

export const POST = async function undoAction(req: Request) {
const body = await req.json();

console.log(body);

const res = await fetch(buildUrl(body.url), {
method: body.method,
headers: {
Authorization: await getAuthHeader(),
},
});

if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error("Failed to undo action");
}

const resData = res.json();
return Response.json(resData);
};
5 changes: 4 additions & 1 deletion webapp-nextjs/src/app/api/tasks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const DELETE = async function deleteTask(
throw new Error("Failed to delete task");
}

const resData = res.json();
const resData = await res.json();

console.log("deleteTask:", resData);

return Response.json(resData);
};
29 changes: 27 additions & 2 deletions webapp-nextjs/src/components/TaskList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import Divider from "@mui/material/Divider";
import Input from "@mui/material/Input";
import ListItemText from "@mui/material/ListItemText";
import ListItemAvatar from "@mui/material/ListItemAvatar";
import { getTasks, addTask, deleteTask } from "@/api/tasks";
import {
getTasks,
addTask,
deleteTask,
undoRequest,
UndoAction,
} from "@/api/tasks";
import PendingIcon from "@mui/icons-material/Pending";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import DeleteIcon from "@mui/icons-material/Delete";
Expand Down Expand Up @@ -53,6 +59,7 @@ export default function TasksList() {
title: "",
description: "",
});
const [undo, setUndo] = React.useState<UndoAction | null>(null);

const refreshTasks = () => getTasks().then(setTasks);

Expand All @@ -65,6 +72,21 @@ export default function TasksList() {
<Typography variant="h4" component="h3" gutterBottom>
Tasks
</Typography>
{
// TODO - Show Undo as a banner
}
{undo && (
<Button
id="undo"
onClick={() => {
undoRequest(undo)
.then(refreshTasks)
.then(() => setUndo(null));
}}
>
Undo
</Button>
)}
<List id="task-list">
{tasks.map((task: Task) => (
<>
Expand All @@ -78,7 +100,10 @@ export default function TasksList() {
aria-label="delete-task"
onClick={async () => {
// FIXME: handle potential error
await deleteTask(task.id).then(refreshTasks);
await deleteTask(task.id).then((undo: UndoAction) => {
setUndo(undo);
refreshTasks();
});
}}
>
<DeleteIcon />
Expand Down

0 comments on commit 718b4f3

Please sign in to comment.