Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ITG-24] Create, view and delete subtasks. #25

Merged
merged 16 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions bode/bode/resources/task_relations/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from psycopg2 import DataError
from sqlalchemy.exc import IntegrityError

from bode.app import db
from bode.models.task import Task
from bode.models.task_relation import TaskRelation
from bode.resources.task_relations.schemas import (
SimpleTaskRelationSchema,
TaskRelationInputSchema,
TasksRelatedSchema,
)

blueprint = Blueprint("task-relations", "task-relations", url_prefix="/task-relations")
Expand All @@ -21,9 +25,30 @@ def post(self, relation_data):
except IntegrityError:
abort(422, message="Relation already exists")

@blueprint.response(200, SimpleTaskRelationSchema(many=True))
def get(self):
return TaskRelation.query.all()


@blueprint.route("/<relation_id>")
class TasksRelationsById(MethodView):
@blueprint.response(200, SimpleTaskRelationSchema)
def delete(self, relation_id):
return TaskRelation.delete(relation_id)


@blueprint.route("/<task_id>/<relation_type>")
class TasksRelatedByIdAndRelationType(MethodView):
@blueprint.response(200, TasksRelatedSchema(many=True))
def get(self, task_id, relation_type):
try:
result = (
db.session.query(TaskRelation.id, Task)
.join(Task, Task.id == TaskRelation.second_task_id)
.filter(TaskRelation.first_task_id == task_id)
.filter(TaskRelation.type == relation_type)
.all()
gregori0o marked this conversation as resolved.
Show resolved Hide resolved
)
return [{"id": rel_id, "task": task} for rel_id, task in result]
except DataError:
abort(404)
6 changes: 6 additions & 0 deletions bode/bode/resources/task_relations/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from bode.models.task_relation import RelationType
from bode.resources.base_schema import BaseSchema
from bode.resources.tasks.schemas import TaskSchema


class TaskRelationInputSchema(BaseSchema):
Expand All @@ -20,3 +21,8 @@ class SimpleTaskRelationSchema(BaseSchema):
first_task_id = fields.UUID()
second_task_id = fields.UUID()
type = fields.String()


class TasksRelatedSchema(BaseSchema):
id = fields.UUID(dump_only=True)
task = fields.Nested(TaskSchema)
15 changes: 15 additions & 0 deletions cabra/src/api/taskRelations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ITaskRelation, ITasksRelated } from "../types/taskRelation";

import axios from "axios";

export const getSubtasks = {
cacheKey: (id: string) => ["task-relations", id, "SUBTASK"],
run: (id: string) =>
axios.get<ITasksRelated[]>(`/task-relations/${id}/SUBTASK`),
};

export const createRelation = (data: Omit<ITaskRelation, "id">) =>
axios.post<ITaskRelation>("/task-relations", data);

export const deleteRelation = (id: string) =>
axios.delete<ITaskRelation>(`/task-relations/${id}`);
2 changes: 2 additions & 0 deletions cabra/src/pages/TaskDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import "twin.macro";

import { useNavigate, useParams } from "react-router-dom";

import { ArrowLeftIcon } from "@heroicons/react/solid";
import Layout from "./components/Layout";
import NavigationButton from "./components/NavigationButton";
Expand Down
24 changes: 23 additions & 1 deletion cabra/src/pages/TaskEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import "twin.macro";

import TaskForm, { TaskFormInputs } from "./components/TaskForm";
import { getTask, getTasks, updateTask } from "../api/tasks";
import tw, { styled } from "twin.macro";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate, useParams } from "react-router-dom";

import AddDependenceButton from "./components/AddDependenceButton";
import { ArrowLeftIcon } from "@heroicons/react/solid";
import Layout from "./components/Layout";
import NavigationButton from "./components/NavigationButton";
import SubtasksListEdit from "./components/SubtasksListEdit";
import { routeHelpers } from "../routes";

const Label = styled.label(tw`text-gray-50 font-bold`);

export default function TaskEditPage() {
const navigate = useNavigate();
const { id } = useParams() as { id: string };
Expand Down Expand Up @@ -45,8 +50,25 @@ export default function TaskEditPage() {
Go Back
</NavigationButton>
</div>

<TaskForm task={data.data} onSubmit={editTask.mutateAsync} />
<div tw="grid gap-4 grid-cols-4">
<div tw="w-full">
<Label>subtasks:</Label>
<SubtasksListEdit parentId={id} />
</div>
<div tw="w-full">
<Label>depends on:</Label>
<AddDependenceButton onClick={() => NaN}>+add</AddDependenceButton>
</div>
<div tw="w-full">
<Label>is dependant on:</Label>
<AddDependenceButton onClick={() => NaN}>+add</AddDependenceButton>
</div>
<div tw="w-full">
<Label>interchangable:</Label>
<AddDependenceButton onClick={() => NaN}>+add</AddDependenceButton>
</div>
gregori0o marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</Layout>
);
Expand Down
7 changes: 7 additions & 0 deletions cabra/src/pages/components/AddDependenceButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import tw, { styled } from "twin.macro";

const AddDependenceButton = styled.button(
tw`rounded-lg flex flex-row items-start h-5 py-1 px-4 static w-16 bg-tertiary font-bold text-lg color[#787DAB]`
gregori0o marked this conversation as resolved.
Show resolved Hide resolved
);

export default AddDependenceButton;
32 changes: 32 additions & 0 deletions cabra/src/pages/components/MiniTaskDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import "twin.macro";

import NavigationButton from "./NavigationButton";
import { TrashIcon } from "@heroicons/react/solid";

interface Props {
title: string;
relationId: string;
taskId: string;
onClickDelete: CallableFunction;
gregori0o marked this conversation as resolved.
Show resolved Hide resolved
}

export default function MiniTaskDelete({
title,
onClickDelete,
relationId,
taskId,
}: Props) {
return (
<div tw="rounded-xl w-full bg-white shadow-2xl text-blue-800 p-1.5">
<p tw="flex items-center font-size[small]">
gregori0o marked this conversation as resolved.
Show resolved Hide resolved
{title}
<NavigationButton
tw="text-stone-50 bg-red-500 flex ml-auto"
onClick={() => onClickDelete(relationId, taskId)}
>
<TrashIcon height={10} width={10} />
</NavigationButton>
</p>
</div>
);
}
85 changes: 85 additions & 0 deletions cabra/src/pages/components/SubtasksList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { getTask, getTasks, updateTask } from "../../api/tasks";
import tw, { styled } from "twin.macro";
import { useMutation, useQuery, useQueryClient } from "react-query";

import Checkbox from "./CheckBox";
import { ITask } from "../../types/task";
import { getSubtasks } from "../../api/taskRelations";
import { useState } from "react";

interface Props {
subtask: ITask;
parentId: string;
}

interface PropsList {
parentId: string;
}

const Container = styled.div(tw`space-x-2 space-y-3 grid grid-cols-4`);

const Subtask = ({ subtask, parentId }: Props) => {
const [errorMessage, setErrorMessage] = useState("");
const client = useQueryClient();
const editTask = useMutation((task: ITask) => updateTask(task.id, task), {
onSuccess: () => {
client.invalidateQueries(getTasks.cacheKey);
client.invalidateQueries(getTask.cacheKey(parentId));
client.invalidateQueries(getSubtasks.cacheKey(parentId));
},
});
const handleIsDoneChange = async () => {
try {
const updatedTask = {
...subtask,
isDone: !subtask.isDone,
};
await editTask.mutateAsync(updatedTask);
} catch (error) {
setErrorMessage(
"Something went wrong :C, It's not possible to uncheck the task."
);
}
};

return (
<div tw="rounded-xl bg-tertiary text-secondary p-1.5 grid">
<p tw="font-medium text-xs">{subtask.title}</p>
<p tw="place-self-end">
<Checkbox checked={subtask.isDone} onChange={handleIsDoneChange} />
</p>
{errorMessage && (
<p tw="flex items-center text-orange-500 pt-1">&nbsp;{errorMessage}</p>
)}
</div>
);
};

export default function SubtasksList({ parentId }: PropsList) {
const { data, isLoading, error } = useQuery(
getSubtasks.cacheKey(parentId),
() => getSubtasks.run(parentId)
);

if (isLoading) return <Container>Loading</Container>;
if (error) return <Container>Oops</Container>;
if (!data?.data) return <Container />;

const subtasks = data.data.slice().reverse();

if (subtasks.length == 0) {
return <Container>{"<No tasks>"}</Container>;
}

return (
<Container>
{subtasks.map((relation) => (
<Subtask
key={relation.id}
subtask={relation.task}
parentId={parentId}
/>
))}
</Container>
);
}
123 changes: 123 additions & 0 deletions cabra/src/pages/components/SubtasksListEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { FormEvent, useState } from "react";
import {
createRelation,
deleteRelation,
getSubtasks,
} from "../../api/taskRelations";
import { createTask, deleteTask, getTask, getTasks } from "../../api/tasks";
import tw, { styled } from "twin.macro";
import { useMutation, useQuery, useQueryClient } from "react-query";

import AddDependenceButton from "./AddDependenceButton";
import { ITask } from "../../types/task";
import { ITaskRelation } from "../../types/taskRelation";
import MiniTaskDelete from "./MiniTaskDelete";

const Container = styled.div(tw`text-gray-50 w-full space-y-4`);
const fieldStyles = tw`w-full px-4 py-2 rounded-xl text-blue-800 placeholder:text-blue-800/60 font-size[small]`;

const emptyTask: Omit<ITask, "id"> = {
description: "",
dueDate: null,
title: "",
isDone: false,
tags: [],
};

interface Props {
parentId: string;
}

export default function SubtasksListEdit({ parentId }: Props) {
const [val, setVal] = useState("");
const client = useQueryClient();

const addRelation = useMutation(createRelation, {
onSuccess: () => {
client.invalidateQueries(getTasks.cacheKey);
client.invalidateQueries(getTask.cacheKey(parentId));
client.invalidateQueries(getSubtasks.cacheKey(parentId));
},
});
const addSubtask = useMutation(createTask, {
onSuccess: (data) => {
const relation: Omit<ITaskRelation, "id"> = {
firstTaskId: parentId,
secondTaskId: data.data.id,
type: "SUBTASK",
};
addRelation.mutateAsync(relation);
},
});

const handleSubmitSubtask = (event: FormEvent) => {
event.preventDefault();
setVal("");
const inputs = {
...emptyTask,
title: val,
};
addSubtask.mutateAsync(inputs);
};

const removeRelation = useMutation((relationId: string) =>
deleteRelation(relationId)
);
gregori0o marked this conversation as resolved.
Show resolved Hide resolved
const removeTask = useMutation((subtaskId: string) => deleteTask(subtaskId), {
onSuccess: () => {
client.invalidateQueries(getTasks.cacheKey);
client.invalidateQueries(getTask.cacheKey(parentId));
client.invalidateQueries(getSubtasks.cacheKey(parentId));
},
});

const removeSubtask = (relationId: string, subtaskId: string) => {
removeRelation.mutateAsync(relationId);
removeTask.mutateAsync(subtaskId);
};
gregori0o marked this conversation as resolved.
Show resolved Hide resolved

const { data, isLoading, error } = useQuery(
getSubtasks.cacheKey(parentId),
() => getSubtasks.run(parentId)
);

if (isLoading) return <Container>Loading</Container>;
if (error) return <Container>Oops</Container>;
if (!data?.data) return <Container />;

const subtasks = data.data.slice().reverse();

return (
<div>
<Container>
{subtasks.map((relation) => (
<MiniTaskDelete
key={relation.id}
title={relation.task.title}
onClickDelete={removeSubtask}
taskId={relation.task.id}
relationId={relation.id}
/>
))}
</Container>
<form onSubmit={handleSubmitSubtask}>
<div tw="rounded-xl w-full text-blue-800 p-1.5">
<p tw="flex items-center">
<input
css={[tw`form-input`, fieldStyles]}
id="subtask"
type="text"
value={val}
maxLength={80}
required
onChange={(event) => setVal(event.target.value)}
/>
</p>
<p tw="flex items-center">
<AddDependenceButton type="submit">+add</AddDependenceButton>
</p>
</div>
</form>
</div>
);
}
Loading