Refactor....
This commit is contained in:
parent
6479c6c1c9
commit
25ef272feb
20 changed files with 359 additions and 364 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -8,6 +8,9 @@ body .wrapper {
|
|||
color: $accent-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
|
|
80
twenty-frontend/src/css/_groups.scss
Normal file
80
twenty-frontend/src/css/_groups.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
72
twenty-frontend/src/js/components/Group.tsx
Normal file
72
twenty-frontend/src/js/components/Group.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
28
twenty-frontend/src/js/components/TaskStatusSelect.tsx
Normal file
28
twenty-frontend/src/js/components/TaskStatusSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue