Refactor....

This commit is contained in:
0x1eef 2024-01-11 12:59:48 -03:00
parent 6479c6c1c9
commit 25ef272feb
20 changed files with 359 additions and 364 deletions

View file

@ -1,7 +1,9 @@
module Twenty::GraphQL::Input
class TaskInput < GraphQL::Schema::InputObject
argument :title, String
argument :content, String
argument :project_id, Int
require_relative "../type/task_status"
argument :title, String, required: false
argument :content, String, required: false
argument :project_id, Int, required: false
argument :status, Twenty::GraphQL::Type::TaskStatus, required: false
end
end

View file

@ -3,16 +3,18 @@ module Twenty::GraphQL::Type
field :find_task, Task, null: true do
argument :task_id, Int
end
field :tasks, [Task], null: false
field :tasks, [Task], null: false do
argument :status, TaskStatus
end
field :projects, [Project], null: false
def find_task(task_id:)
Twenty::Task.find_by(id: task_id)
end
def tasks
def tasks(status:)
Twenty::Task
.ready
.where(status:)
.order(updated_at: :desc)
end

View file

@ -7,5 +7,6 @@ module Twenty::GraphQL::Type
field :content, String, null: false
field :project, Project, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
field :is_ready, Boolean, null: false, method: :ready?
end
end

View file

@ -44,12 +44,13 @@ type Project {
type Query {
findTask(taskId: Int!): Task
projects: [Project!]!
tasks: [Task!]!
tasks(status: TaskStatus!): [Task!]!
}
type Task {
content: String!
id: Int!
isReady: Boolean!
project: Project!
status: TaskStatus!
title: String!
@ -57,9 +58,10 @@ type Task {
}
input TaskInput {
content: String!
projectId: Int!
title: String!
content: String
projectId: Int
status: TaskStatus
title: String
}
enum TaskStatus {

View file

@ -3,6 +3,6 @@ source "https://rubygems.org"
gemspec
gem "nanoc", "~> 4.12"
gem "nanoc-live", "~> 1.0"
gem "nanoc-webpack.rb", "~> 0.4"
gem "nanoc-webpack.rb", "~> 0.5"
gem "sass", "~> 3.7"
gem "rainpress", "~> 1.0"

View file

@ -75,7 +75,7 @@ GEM
listen (~> 3.0)
nanoc-cli (~> 4.11, >= 4.11.14)
nanoc-core (~> 4.11, >= 4.11.14)
nanoc-webpack.rb (0.4.5)
nanoc-webpack.rb (0.5.5)
ryo.rb (~> 0.4)
parallel (1.24.0)
pastel (0.8.0)
@ -123,7 +123,7 @@ PLATFORMS
DEPENDENCIES
nanoc (~> 4.12)
nanoc-live (~> 1.0)
nanoc-webpack.rb (~> 0.4)
nanoc-webpack.rb (~> 0.5)
rainpress (~> 1.0)
rake (~> 13.0)
sass (~> 3.7)

View file

@ -8,6 +8,9 @@ body .wrapper {
color: $accent-color;
text-decoration: none;
}
&:hover {
text-decoration: underline;
}
}
ul {
margin: 0;

View file

@ -0,0 +1,80 @@
body .wrapper .group {
@import "colors";
width: 100%;
margin-bottom: 10px;
.group-name {
margin: 10px 0 10px 0;
padding: 10px;
background: $secondary-color;
color: $primary-color;
font-size: small;
border-radius: 5px;
}
.group-items {
min-height: 75px;
p {
padding-top: 2.5%;
font-size: small;
}
ul.items {
li.item {
display: flex;
padding: 7.5px;
border: 1px solid $primary-color;
select {
height: 35px;
padding: 10px;
background: $primary-color;
font-size: smaller;
}
&:hover {
background: $secondary-color;
color: $primary-color;
border-radius: 10px;
border: 1px solid $secondary-color;
a {
color: $primary-color;
.subtitle {
color: $primary-color;
}
}
}
a {
display: flex;
flex-direction: column;
height: 50px;
justify-content: space-between;
padding: 10px 10px 5px 0px;
.title {
display: flex;
padding-bottom: 5px;
font-size: smaller;
}
.subtitle {
font-size: small;
color: $secondary-color;
}
}
.tags {
.tag {
color: $primary-color;
border: 1px solid $secondary-color;
border-radius: 5px;
font-size: small;
font-weight: bold;
}
}
}
}
}
}

View file

@ -6,6 +6,7 @@
width: 20%;
}
.column-2 {
h1, h2, h3 { font-size: medium; }
width: 100%;
}
}
@ -22,6 +23,7 @@
.w-100 { width: 100%; }
.w-95 { width: 95%; }
.w-85 { width: 85%; }
.w-75 { width: 75%; }
.h-100 { height: 100%; }
.h-80 { height: 80%; }
.h-50 { height: 50%; }

View file

@ -4,8 +4,8 @@
-----------------
**/
ul.collection {
h1 {
ul {
h1, h2 {
display: flex;
align-items: center;
margin: 0;
@ -16,83 +16,15 @@ ul.collection {
width: 100%;
font-size: medium;
}
h2 { font-size: small; }
}
ul.collection li.item {
display: flex;
flex-wrap: wrap;
height: 85px;
padding: 0px 2.5px 0 2.5px;
border-left: 1px solid $primary-color;
border-right: 1px solid $primary-color;
&:hover {
border-left: 1px solid $secondary-color;
border-right: 1px solid $secondary-color;
}
a {
display: flex;
flex-direction: column;
height: 50px;
justify-content: space-between;
padding: 10px 10px 5px 0px;
.title {
display: flex;
padding-bottom: 5px;
font-size: smaller;
}
.subtitle {
font-size: small;
color: $secondary-color;
}
}
.tags {
.tag {
color: $primary-color;
border: 1px solid $secondary-color;
border-radius: 5px;
font-size: small;
font-weight: bold;
}
}
.break {
display: flex;
width: 100%;
}
ul.actions {
list-style-type: none;
width: 5%;
display: flex;
place-content: flex-end;
li {
display: flex;
align-items: flex-start;
}
}
}
ul.collection li.item.removed {
animation: bounceOutDown;
animation-duration: 0.5s;
}
ul.collection li.item.completed {
animation: bounceOutUp;
animation-duration: 0.5s;
}
ul.items {
@import "colors";
margin: 0;
padding: 0;
list-style-type: none;
}
ul.items.nav {
@ -147,3 +79,15 @@ ul.items.tasks {
}
}
}
ul.action-group {
display: flex;
list-style-type: none;
width: 5%;
place-content: flex-end;
li {
display: flex;
align-items: flex-start;
}
}

View file

@ -1,76 +0,0 @@
.panel h1 {
@import "colors";
display: flex;
align-items: center;
margin: 0;
padding: 0;
background: $primary-color;
color: $secondary-color;
height: 25px;
width: 100%;
font-size: medium;
}
.panel .panel-header.panel-tabs {
@import "colors";
padding: 5px 5px 0 0px;
ul.tabs {
list-style-type: none;
display: flex;
height: 100%;
margin: 0;
padding: 0;
li:first-child {
border-left: none;
}
li {
font-size: small;
height: 100%;
margin-right: 5px;
border: 1px solid $accent-color;
border-bottom: none;
padding: 10px 5px 5px 10px;
border-radius: 5px;
min-width: 120px;
text-align: center;
background: $primary-color;
color: $secondary-color;
cursor: pointer;
opacity: 0.5;
}
li.active, li:hover {
font-weight: bold;
color: $secondary-color;
opacity: 1;
}
}
}
.panel .panel-body {
@import "colors";
width: 100%;
.task.content {
padding: 10px;
ul {
padding: revert;
}
ul li {
line-height: 1.7em;
}
h3,h4,h5 {
&:first-child {
margin: 0 0 5px 0;
}
margin: 15px 0 5px 0;
}
code {
padding: 0px 5px 0px 5px;
font-family: "Noto Sans Mono Regular";
font-size: smaller;
font-weight: bold;
color: lighten(#FF0000, 25%);
border-radius: 5px;
border: 1px solid $accent-color;
}
}
}

View file

@ -2,7 +2,7 @@
@import "colors";
@import "global";
@import "layout";
@import "panels";
@import "groups";
@import "lists";
@import "vendor/forms";
@ -34,12 +34,12 @@ body {
padding-top: 25px;
}
.trash.icon, .done.icon {
.destroy-task.icon, .complete-task.icon, .start-task.icon {
height: 20px;
width: 20px;
cursor: pointer;
}
.trash.icon {
.destroy-task.icon {
g {
fill: #d9534f;
}

View file

@ -0,0 +1,72 @@
import React from "react";
import type { Task } from "/types/schema";
import classnames from "classnames";
import { DateTime } from "luxon";
import { QueryResult } from "@apollo/client";
import { TaskStatusSelect } from "/components/TaskStatusSelect";
type Props = {
groupName: string;
getItems: () => QueryResult;
};
export function Group({ groupName, getItems }: Props) {
const { data, loading } = getItems();
const items = data?.tasks;
if (loading) {
return null;
}
return (
<div className="group">
<h1 className="group-name">{groupName}</h1>
<div className="group-items">
{items.length ? (
<ul className="items">
{items.map((task: Task, key: number) => {
const { updatedAt } = task;
const datetime = DateTime.fromISO(updatedAt);
const classes = {};
const editHref = `/tasks/edit#id=${task.id}`;
return (
<li className={classnames("item", classes)} key={key}>
<div className="w-95">
<a className="w-100" href={editHref}>
<span className="title">{task.title}</span>
<span className="subtitle">
<span className="datetime">
{datetime.toFormat("dd LLL, yyyy")} at{" "}
{datetime.toFormat("HH:mm")}
</span>
</span>
</a>
<span className="break" />
<span className="tags">
<span
style={{ backgroundColor: task.project.color }}
className="tag"
>
{task.project.name}
</span>
</span>
</div>
<TaskStatusSelect task={task} />
</li>
);
})}
</ul>
) : (
<p>
There are no {groupName.toLowerCase()} tasks.
<br />
<a className="w-100" href="/tasks/new">
Add a task
</a>
.
</p>
)}
</div>
</div>
);
}

View file

@ -1,43 +1,32 @@
import React from "react";
type IconProps = {
onClick: (e: React.MouseEvent) => void;
title?: string;
};
type Props = { name: IconName; onClick: (e: React.MouseEvent) => void };
type IconName = "start-task" | "complete-task" | "destroy-task";
type IconSet = Record<IconName, (props: Props) => JSX.Element>;
export function TrashIcon({ onClick, title }: IconProps) {
return (
const icons: IconSet = {
"start-task": ({ name, onClick }: Props) => (
<svg
height="512"
viewBox="0 0 128 128"
width="512"
xmlns="http://www.w3.org/2000/svg"
onClick={e => onClick(e)}
className="trash icon"
aria-labelledby="trash-icon-title"
viewBox="0 0 64 64"
onClick={onClick}
className={`${name} icon`}
>
<title id="trash-icon-title">{title}</title>
<g>
<path d="m95.331 27.471h-14.248v-4.516a5.712 5.712 0 0 0 -5.7-5.7h-22.762a5.712 5.712 0 0 0 -5.705 5.7v4.516h-14.248a5.425 5.425 0 0 0 -5.419 5.419v5.857a5.426 5.426 0 0 0 5.419 5.419h.071v59.041a7.551 7.551 0 0 0 7.542 7.543h47.437a7.551 7.551 0 0 0 7.542-7.543v-59.041h.071a5.425 5.425 0 0 0 5.418-5.419v-5.857a5.424 5.424 0 0 0 -5.418-5.419zm-44.915-4.516a2.207 2.207 0 0 1 2.205-2.2h22.757a2.207 2.207 0 0 1 2.2 2.2v4.516h-27.162zm41.344 80.252a4.047 4.047 0 0 1 -4.042 4.043h-47.437a4.047 4.047 0 0 1 -4.042-4.043v-59.041h55.521zm5.489-64.46a1.92 1.92 0 0 1 -1.918 1.919h-62.663a1.921 1.921 0 0 1 -1.919-1.919v-5.857a1.921 1.921 0 0 1 1.919-1.919h62.663a1.92 1.92 0 0 1 1.918 1.919z" />
<path d="m64 95.541a1.749 1.749 0 0 0 1.75-1.75v-36.166a1.75 1.75 0 1 0 -3.5 0v36.166a1.75 1.75 0 0 0 1.75 1.75z" />
<path d="m79.333 95.541a1.75 1.75 0 0 0 1.75-1.75v-36.166a1.75 1.75 0 0 0 -3.5 0v36.166a1.75 1.75 0 0 0 1.75 1.75z" />
<path d="m48.666 95.541a1.75 1.75 0 0 0 1.75-1.75v-36.166a1.75 1.75 0 0 0 -3.5 0v36.166a1.75 1.75 0 0 0 1.75 1.75z" />
<g id="_x31_78">
<path d="m13.2 7.6c-.8 4.3-.6 5.5-.1 8.2.4 2.3.9 4.9-.1 6.8-.6 1.2-1.5 2.3-1.9 3.8-.6 2.5.7 5.4 0 7.5-.2.8-1.1 1.7-.8 3.1 0 .7.3 1.3.8 1.8 1.6 1.7 8.9 4.5 16.6 3.5 4-.5 7.9-1.9 11.9-2.6 3.8-.7 8-.6 11.3 1.4-1.4 7.7-3.3 16.2-5.1 22.2-.3 1 .1 2.1 1 2.6.2.2.5.4.8.5.9.3 1.9 0 2.5-.7.5-.6.7-1.3.9-2.1 4.1-15.9 7-32.1 8.6-48.4.2-2.5.1-5.1.6-7.4.1-.5.1-1 0-1.5.2-2-1.4-3.4-3.1-3.5-1.3-.1-2.5.7-3.1 1.8-.4.7-.5 1.7-.4 2.5-9.6-3.2-15.6 1-21.2 2.6-6 1.7-12.8.8-18.2-2.5-.3-.4-.9-.1-1 .4zm1.9 11.5c3.9.6 7.9 1.2 11.9 1.4-.3 3.3-.8 6.6-1.6 9.8-4.8.2-9.3-.7-12.8-2.5 0-3.4 2.8-3.7 2.5-8.7zm10.3 21.1c-.5 0-1 0-1.5 0 1.1-2.7 1.9-5.5 2.7-8.4.5 0 .9-.1 1.4-.1 4-.5 7.3-1.3 11.7-1.8-.7 2.7-1.5 5.3-2.5 7.9-3.9.8-7.9 2.1-11.8 2.4zm-3.1-.1c-4.2-.3-6.4-1.5-8.9-2.3-.5-.1-1.5-.6-1.5-1.4 0-1.1 1.4-1.5 1-5.4-.1-.4-.1-.5-.2-1.4 3.2 1.5 7.2 2.4 11.4 2.4h1c-1.3 4.5-2.4 7.1-2.8 8.1zm29.2-2c-2.2-1.1-4-1.2-6.6-1.2-2.1 0-4.2.2-6 .6.9-2.6 1.7-5.2 2.4-7.8 3.9-.3 7.7-.1 11.5.8-.5 3.6-.6 3.8-1.3 7.6zm1.5-9.1c-3.7-.8-7.5-1.1-11.3-.8.7-3.1 1.3-6.2 1.7-9.3 3.6-.6 7.2-.9 10.7-.1-.4 3.4-.7 6.8-1.1 10.2zm-12.9-.7c-4.3.5-8.1 1.4-12.2 1.9-.3 0-.6 0-.8.1.7-3.2 1.2-6.4 1.5-9.7 5.4.1 8.1-.5 13.3-1.4-.5 3-1.1 6.1-1.8 9.1zm7.2 35.3c6.2-20.8 9-48.7 8.8-54.8.6.1 1.2.1 2-.1-.7 15.9-4.5 38.9-9.2 55.4-.1.2-.2.4-.4.5-.1.1-.3.1-.6 0l-.1-.1c-.4-.1-.6-.5-.5-.9zm8.2-58.5c.3-.6 1-1 1.7-1 1.4.1 2.2 1.8 1.2 2.9h-.1c-.9.5-2.1.4-3-.1 0 0 0 0-.1 0 0-.1 0-.2 0-.4 0-.4.1-1 .3-1.4zm-.9 3.8c0 .1-.1 3.9-.5 8.4-3.5-.8-7.1-.5-10.6.1.3-2.9.7-8.4.6-9.8 2.4-.4 8.3.1 10.5 1.3zm-12-1.1c0 3.3-.2 6.6-.6 9.8-5.3.9-7.9 1.6-13.3 1.5.2-2.5.2-4.9.1-7.4 5.9-.5 8.7-2.9 13.8-3.9zm-28.1 1.1c3.9 2.1 8.3 3.1 12.7 2.9.1 2.4 0 4.8-.1 7.2-4.1-.2-8.2-.8-12.1-1.5-.5-3.5-1.2-4.3-.5-8.6z" />
<path d="m7.7 6.8c-.2.4 0 .8.4 1l2.8 1.3c.4.2.8 0 1-.4s0-.8-.4-1l-2.8-1.3c-.4-.1-.8 0-1 .4z" />
<path d="m11.6 2.9c-.2-.4-.7-.5-1-.3-.4.2-.5.7-.3 1l1.6 2.8c.1.2.4.4.7.4.6 0 .9-.6.7-1.1z" />
<path d="m15.1 5.9c.4 0 .7-.3.7-.6l.5-2.8c.1-.4-.2-.8-.6-.9s-.8.2-.9.6l-.5 2.8c0 .5.3.9.8.9z" />
</g>
</svg>
);
}
export function DoneIcon({ onClick, title }: IconProps) {
return (
),
"complete-task": ({ name, onClick }: Props) => (
<svg
enableBackground="new 0 0 24 24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
onClick={e => onClick(e)}
className="done icon"
aria-labelledby="complete-icon-title"
viewBox="0 0 24 24"
onClick={onClick}
className={`${name} icon`}
>
<title id="complete-icon-title">{title}</title>
<switch>
<g>
<path
@ -47,5 +36,25 @@ export function DoneIcon({ onClick, title }: IconProps) {
</g>
</switch>
</svg>
);
),
"destroy-task": ({ name, onClick }: Props) => (
<svg
viewBox="0 0 128 128"
xmlns="http://www.w3.org/2000/svg"
onClick={onClick}
className={`${name} icon`}
>
<g>
<path d="m95.331 27.471h-14.248v-4.516a5.712 5.712 0 0 0 -5.7-5.7h-22.762a5.712 5.712 0 0 0 -5.705 5.7v4.516h-14.248a5.425 5.425 0 0 0 -5.419 5.419v5.857a5.426 5.426 0 0 0 5.419 5.419h.071v59.041a7.551 7.551 0 0 0 7.542 7.543h47.437a7.551 7.551 0 0 0 7.542-7.543v-59.041h.071a5.425 5.425 0 0 0 5.418-5.419v-5.857a5.424 5.424 0 0 0 -5.418-5.419zm-44.915-4.516a2.207 2.207 0 0 1 2.205-2.2h22.757a2.207 2.207 0 0 1 2.2 2.2v4.516h-27.162zm41.344 80.252a4.047 4.047 0 0 1 -4.042 4.043h-47.437a4.047 4.047 0 0 1 -4.042-4.043v-59.041h55.521zm5.489-64.46a1.92 1.92 0 0 1 -1.918 1.919h-62.663a1.921 1.921 0 0 1 -1.919-1.919v-5.857a1.921 1.921 0 0 1 1.919-1.919h62.663a1.92 1.92 0 0 1 1.918 1.919z" />
<path d="m64 95.541a1.749 1.749 0 0 0 1.75-1.75v-36.166a1.75 1.75 0 1 0 -3.5 0v36.166a1.75 1.75 0 0 0 1.75 1.75z" />
<path d="m79.333 95.541a1.75 1.75 0 0 0 1.75-1.75v-36.166a1.75 1.75 0 0 0 -3.5 0v36.166a1.75 1.75 0 0 0 1.75 1.75z" />
<path d="m48.666 95.541a1.75 1.75 0 0 0 1.75-1.75v-36.166a1.75 1.75 0 0 0 -3.5 0v36.166a1.75 1.75 0 0 0 1.75 1.75z" />
</g>
</svg>
),
};
export function Icon({ name, onClick }: Props) {
const getIcon = icons[name];
return getIcon({ name, onClick });
}

View file

@ -71,77 +71,62 @@ export function Task({ taskId }: { taskId?: number }) {
<NavBar />
</div>
<div className="column-2 h-100">
<form className="task h-100" onSubmit={handleSubmit(onSave)}>
<div className="panel h-100">
<form className="group h-100" onSubmit={handleSubmit(onSave)}>
<div className="group-name' h-100">
<h1>{task ? "Edit task" : "New task"}</h1>
<div className="panel-header panel-tabs">
<ul className="tabs">
<li
className={classnames({ active: isEditable })}
onClick={() => setIsEditable(true)}
>
Write
</li>
<li
className={classnames({ active: !isEditable })}
onClick={() => setIsEditable(false)}
>
Preview
</li>
</ul>
</div>
<div className="group-items h-80">
<div>
<select
{...register("projectId")}
className="form"
value={projectId}
onChange={event => {
const v: string = event.target.value;
set("projectId", Number(v));
}}
>
{projects.map((project: Project, key: number) => {
return (
<option key={key} value={project.id}>
{project.name}
</option>
);
})}
</select>
</div>
<div className="panel-body h-80">
<div>
<select
{...register("projectId")}
className="form"
value={projectId}
onChange={event => {
const v: string = event.target.value;
set("projectId", Number(v));
}}
>
{projects.map((project: Project, key: number) => {
return (
<option key={key} value={project.id}>
{project.name}
</option>
);
})}
</select>
</div>
<div>
<input
className="form"
type="text"
placeholder="Title"
defaultValue={task?.title}
{...register("title", { required: true })}
/>
</div>
{isEditable ? (
<>
<div className="row textarea h-70">
<textarea
className="form h-100"
placeholder="Add your description heren"
defaultValue={task?.content || DEFAULT_TASK_CONTENT}
{...register("content", { required: true })}
/>
</div>
<div className="row">
<input className="form" type="submit" value="Save" />
</div>
</>
) : (
<div
className="task content h-50"
dangerouslySetInnerHTML={{
__html: rendermd(content || task?.content),
}}
/>
)}
<div>
<input
className="form"
type="text"
placeholder="Title"
defaultValue={task?.title}
{...register("title", { required: true })}
/>
</div>
{isEditable ? (
<>
<div className="row textarea h-70">
<textarea
className="form h-100"
placeholder="Add your description heren"
defaultValue={task?.content || DEFAULT_TASK_CONTENT}
{...register("content", { required: true })}
/>
</div>
<div className="row">
<input className="form" type="submit" value="Save" />
</div>
</>
) : (
<div
className="task content h-50"
dangerouslySetInnerHTML={{
__html: rendermd(content || task?.content),
}}
/>
)}
</div>
</form>
</div>

View file

@ -0,0 +1,28 @@
import React from "react";
import { Task, TaskStatus } from "/types/schema";
import { useUpdateTask } from "hooks/mutations/useUpdateTask";
import { GET_TASKS } from "/hooks/queries/useTasks";
type Props = {
task: Task;
};
export function TaskStatusSelect({ task }: Props) {
const updateTask = useUpdateTask();
const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const status = event.target.value as TaskStatus;
updateTask({
awaitRefetchQueries: true,
refetchQueries: [GET_TASKS, "GetTasks"],
variables: { taskId: task.id, input: { status } },
});
};
return (
<select value={task.status} onChange={onChange}>
<option value={TaskStatus.InProgress}>Active</option>
<option value={TaskStatus.Ready}>Ready</option>
<option value={TaskStatus.Complete}>Complete</option>
</select>
);
}

View file

@ -1,28 +1,18 @@
import React, { useState, useEffect } from "react";
import { useTasks } from "/hooks/queries/useTasks";
import { useDestroyTask } from "/hooks/mutations/useDestroyTask";
import { TrashIcon, DoneIcon } from "/components/Icons";
import React, { useEffect } from "react";
import { NavBar } from "/components/NavBar";
import { DateTime } from "luxon";
import { Task } from "/types/schema";
import classnames from "classnames";
import { useCompleteTask } from "/hooks/mutations/useCompleteTask";
import { Group } from "/components/Group";
import { TaskStatus } from "/types/schema";
import { useTasks } from "/hooks/queries/useTasks";
export function Tasks() {
const { refetch, loading, data } = useTasks();
const tasks = data?.tasks;
const [destroyTask] = useDestroyTask();
const [completeTask] = useCompleteTask();
const [destroyedTask, setDestroyedTask] = useState<Task>(null);
const [completedTask, setCompletedTask] = useState<Task>(null);
useEffect(() => {
document.title = "Tasks";
}, []);
if (loading) {
return null;
}
const getTasks = (status: TaskStatus) => {
return () => {
return useTasks({ variables: { status } });
};
};
return (
<div className="two-columns">
@ -30,74 +20,9 @@ export function Tasks() {
<NavBar />
</div>
<div className="column-2">
<div className="panel">
<h1>Tasks</h1>
<div className="panel-body">
<ul className="collection">
{tasks.map((task: Task, key: number) => {
const { updatedAt } = task;
const datetime = DateTime.fromISO(updatedAt);
const wasDestroyed = task === destroyedTask;
const wasCompleted = task === completedTask;
const classes = {
completed: wasCompleted,
removed: wasDestroyed,
};
const editHref = `/tasks/edit#id=${task.id}`;
return (
<li className={classnames("item", classes)} key={key}>
<div className="w-95">
<a className="w-100" href={editHref}>
<span className="title">{task.title}</span>
<span className="subtitle">
<span className="datetime">
{datetime.toFormat("dd LLL, yyyy")} at{" "}
{datetime.toFormat("HH:mm")}
</span>
</span>
</a>
<span className="break" />
<span className="tags">
<span
style={{ backgroundColor: task.project.color }}
className="tag"
>
{task.project.name}
</span>
</span>
</div>
<ul className="actions">
<li>
<DoneIcon
title="Complete task"
onClick={async (_e: React.MouseEvent) => {
await completeTask({
variables: { taskId: task.id },
});
setCompletedTask(task);
setTimeout(refetch, 500);
}}
/>
</li>
<li>
<TrashIcon
title="Delete task"
onClick={async (_e: React.MouseEvent) => {
await destroyTask({
variables: { taskId: task.id },
});
setDestroyedTask(task);
setTimeout(refetch, 500);
}}
/>
</li>
</ul>
</li>
);
})}
</ul>
</div>
</div>
<h1>Tasks</h1>
<Group groupName="Active" getItems={getTasks(TaskStatus.InProgress)} />
<Group groupName="Ready" getItems={getTasks(TaskStatus.Ready)} />
</div>
</div>
);

View file

@ -3,7 +3,7 @@ import {
MutationUpdateTaskArgs,
TaskInput,
} from "/types/schema";
import { gql, useMutation } from "@apollo/client";
import { gql, useMutation, DocumentNode } from "@apollo/client";
const GQL = gql`
mutation UpdateTask($taskId: Int!, $input: TaskInput!) {
@ -15,16 +15,24 @@ const GQL = gql`
type TArgs = {
variables: { taskId: number; input: TaskInput };
refetchQueries?: Array<string | DocumentNode>;
awaitRefetchQueries?: boolean;
};
export function useUpdateTask() {
const [update] = useMutation<UpdateTaskPayload, MutationUpdateTaskArgs>(GQL);
return ({ variables: { taskId, input }, ...rest }: TArgs) => {
const projectId = Number(input.projectId);
const variables = {
taskId,
input: { ...input, ...{ projectId } },
};
return update({ variables, ...rest });
return ({
awaitRefetchQueries,
refetchQueries,
variables: { taskId, input },
...rest
}: TArgs) => {
const projectId = input.projectId ? Number(input.projectId) : null;
const variables = { taskId, input: {} };
Object.assign(
variables,
projectId ? { input: { ...input, projectId } } : { input },
);
return update({ variables, awaitRefetchQueries, refetchQueries, ...rest });
};
}

View file

@ -1,11 +1,13 @@
import { useQuery, gql } from "@apollo/client";
import { Task } from "/types/schema";
const GQL = gql`
query Query {
tasks {
export const GET_TASKS = gql`
query GetTasks($status: TaskStatus!) {
tasks(status: $status) {
id
title
status
isReady
updatedAt
project {
name
@ -15,6 +17,6 @@ const GQL = gql`
}
`;
export function useTasks() {
return useQuery(GQL);
export function useTasks({ ...rest }) {
return useQuery<{ tasks: Task[] }>(GET_TASKS, rest);
}

View file

@ -94,10 +94,15 @@ export type QueryFindTaskArgs = {
taskId: Scalars["Int"]["input"];
};
export type QueryTasksArgs = {
status: TaskStatus;
};
export type Task = {
__typename?: "Task";
content: Scalars["String"]["output"];
id: Scalars["Int"]["output"];
isReady: Scalars["Boolean"]["output"];
project: Project;
status: TaskStatus;
title: Scalars["String"]["output"];
@ -105,9 +110,10 @@ export type Task = {
};
export type TaskInput = {
content: Scalars["String"]["input"];
projectId: Scalars["Int"]["input"];
title: Scalars["String"]["input"];
content?: InputMaybe<Scalars["String"]["input"]>;
projectId?: InputMaybe<Scalars["Int"]["input"]>;
status?: InputMaybe<TaskStatus>;
title?: InputMaybe<Scalars["String"]["input"]>;
};
export enum TaskStatus {