Merge branch 'main' into develop
This commit is contained in:
commit
04af5bad1e
25 changed files with 486 additions and 407 deletions
|
@ -34,7 +34,7 @@ module Twenty
|
|||
ActiveRecord::Base.establish_connection(
|
||||
adapter: "sqlite3",
|
||||
database: path,
|
||||
pool: 3
|
||||
pool: 16
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,5 +7,9 @@ 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?
|
||||
field :is_backlogged, Boolean, null: false, method: :backlog?
|
||||
field :is_complete, Boolean, null: false, method: :complete?
|
||||
field :in_progress, Boolean, null: false, method: :in_progress?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module Twenty::GraphQL::Type
|
||||
class TaskStatus < GraphQL::Schema::Enum
|
||||
value :backlog
|
||||
value :ready
|
||||
value :in_progress
|
||||
value :complete
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
class Twenty::Task < Twenty::Model
|
||||
self.table_name = "tasks"
|
||||
|
||||
STATUS = {ready: 0, in_progress: 1, complete: 2}
|
||||
enum :status, STATUS, default: :ready
|
||||
STATUS = {backlog: 0, ready: 1, in_progress: 2, complete: 3}
|
||||
enum :status, STATUS, default: :backlog
|
||||
|
||||
##
|
||||
# Validations
|
||||
|
|
|
@ -50,6 +50,10 @@ type Query {
|
|||
type Task {
|
||||
content: String!
|
||||
id: Int!
|
||||
inProgress: Boolean!
|
||||
isBacklogged: Boolean!
|
||||
isComplete: Boolean!
|
||||
isReady: Boolean!
|
||||
project: Project!
|
||||
status: TaskStatus!
|
||||
title: String!
|
||||
|
@ -57,12 +61,14 @@ type Task {
|
|||
}
|
||||
|
||||
input TaskInput {
|
||||
content: String!
|
||||
projectId: Int!
|
||||
title: String!
|
||||
content: String
|
||||
projectId: Int
|
||||
status: TaskStatus
|
||||
title: String
|
||||
}
|
||||
|
||||
enum TaskStatus {
|
||||
backlog
|
||||
complete
|
||||
in_progress
|
||||
ready
|
||||
|
|
|
@ -8,6 +8,9 @@ body .wrapper {
|
|||
color: $accent-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
|
|
116
twenty-frontend/src/css/_groups.scss
Normal file
116
twenty-frontend/src/css/_groups.scss
Normal file
|
@ -0,0 +1,116 @@
|
|||
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;
|
||||
border-radius: 5px;
|
||||
|
||||
ul.tabs {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
grid-gap: 15px;
|
||||
font-size: medium;
|
||||
|
||||
li.tab {
|
||||
height: 24px;
|
||||
width: 100px;
|
||||
a {
|
||||
display: block;
|
||||
background: $primary-color;
|
||||
color: $accent-color;
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
li.tab.active {
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-items {
|
||||
min-height: 75px;
|
||||
|
||||
p.empty-group {
|
||||
padding-top: 2.5%;
|
||||
}
|
||||
|
||||
ul.items {
|
||||
li.item {
|
||||
display: flex;
|
||||
padding: 7.5px;
|
||||
border: 1px solid $primary-color;
|
||||
|
||||
select {
|
||||
height: 35px;
|
||||
padding: 10px;
|
||||
background: $primary-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $secondary-color;
|
||||
color: $primary-color;
|
||||
border-radius: 10px;
|
||||
border: 1px solid $secondary-color;
|
||||
|
||||
a {
|
||||
color: $primary-color;
|
||||
text-decoration: none;
|
||||
span {
|
||||
color: $primary-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 50px;
|
||||
justify-content: space-between;
|
||||
padding: 10px 10px 5px 0px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $secondary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
.tag {
|
||||
color: $primary-color;
|
||||
border: 1px solid $secondary-color;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
ul ,
|
||||
ul li,
|
||||
ul li.task-list-item input[type='checkbox']
|
||||
{
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
h1, h2, h3, h4, p {
|
||||
}
|
||||
}
|
|
@ -1,28 +1,75 @@
|
|||
.two-columns {
|
||||
display: flex;
|
||||
flex-direction: space-between;
|
||||
|
||||
.column-1 {
|
||||
width: 20%;
|
||||
}
|
||||
.column-2 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.m-0-auto { margin: 0 auto; }
|
||||
.maxw-1024 { max-width: 1024px; }
|
||||
|
||||
.max-width {
|
||||
width: 75%;
|
||||
max-width: 1024px;
|
||||
}
|
||||
/* flex */
|
||||
.flex { display: flex; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-wrap-reverse { flex-wrap: wrap-reverse; }
|
||||
.flex-nowrap { flex-wrap: nowrap; }
|
||||
.flex-row { flex-direction: row; }
|
||||
.flex-column { flex-direction: column; }
|
||||
.justify-content-start { justify-content: flex-start; }
|
||||
.justify-content-center { justify-content: center; }
|
||||
.justify-content-end { justify-content: flex-end; }
|
||||
|
||||
.align-center {
|
||||
margin: 0 auto;
|
||||
}
|
||||
/* colors */
|
||||
.bg-primary { background: $primary-color; }
|
||||
.bg-secondary { background: $secondary-color; }
|
||||
.bg-accent { background: $accent-color; }
|
||||
.text-primary { color: $primary-color; }
|
||||
.text-secondary { color: $secondary-color; }
|
||||
.text-accent { color: $accent-color; }
|
||||
|
||||
/* font sizes */
|
||||
.text-xxsmall { font-size: xx-small; }
|
||||
.text-xsmall { font-size: x-small; }
|
||||
.text-small { font-size: small; }
|
||||
.text-smaller { font-size: smaller; }
|
||||
.text-medium { font-size: medium; }
|
||||
.text-large { font-size: large; }
|
||||
.text-larger { font-size: larger; }
|
||||
|
||||
/* width */
|
||||
.w-100 { width: 100%; }
|
||||
.w-95 { width: 95%; }
|
||||
.w-90 { width: 90%; }
|
||||
.w-85 { width: 85%; }
|
||||
.w-80 { width: 80%; }
|
||||
.w-75 { width: 75%; }
|
||||
.w-70 { width: 70%; }
|
||||
.w-65 { width: 65%; }
|
||||
.w-60 { width: 60%; }
|
||||
.w-55 { width: 55%; }
|
||||
.w-50 { width: 50%; }
|
||||
.w-45 { width: 45%; }
|
||||
.w-40 { width: 40%; }
|
||||
.w-35 { width: 35%; }
|
||||
.w-30 { width: 30%; }
|
||||
.w-25 { width: 25%; }
|
||||
.w-20 { width: 20%; }
|
||||
.w-15 { width: 15%; }
|
||||
.w-10 { width: 10%; }
|
||||
.w-5 { width: 5%; }
|
||||
|
||||
/* height */
|
||||
.h-100 { height: 100%; }
|
||||
.h-95 { height: 95%; }
|
||||
.h-90 { height: 90%; }
|
||||
.h-85 { height: 85%; }
|
||||
.h-80 { height: 80%; }
|
||||
.h-50 { height: 50%; }
|
||||
.h-75 { height: 75%; }
|
||||
.h-70 { height: 70%; }
|
||||
.h-65 { height: 65%; }
|
||||
.h-60 { height: 60%; }
|
||||
.h-55 { height: 55%; }
|
||||
.h-50 { height: 50%; }
|
||||
.h-45 { height: 45%; }
|
||||
.h-40 { height: 40%; }
|
||||
.h-35 { height: 35%; }
|
||||
.h-30 { height: 30%; }
|
||||
.h-25 { height: 25%; }
|
||||
.h-20 { height: 20%; }
|
||||
.h-15 { height: 15%; }
|
||||
.h-10 { height: 10%; }
|
||||
.h-5 { height: 5%; }
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
-----------------
|
||||
**/
|
||||
|
||||
ul.collection {
|
||||
h1 {
|
||||
ul {
|
||||
h1, h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
|
@ -14,85 +14,15 @@ ul.collection {
|
|||
color: $secondary-color;
|
||||
height: 25px;
|
||||
width: 100%;
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -100,12 +30,11 @@ ul.items.nav {
|
|||
border-radius: 5px;
|
||||
background: $secondary-color;
|
||||
color: $primary-color;
|
||||
width: 85%;
|
||||
width: 90%;
|
||||
padding: 10px;
|
||||
|
||||
h1 {
|
||||
height: unset;
|
||||
font-size: smaller;
|
||||
align-items: center;
|
||||
background: $secondary-color;
|
||||
color: $primary-color;
|
||||
|
@ -122,7 +51,6 @@ ul.items.nav {
|
|||
color: $secondary-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: smaller;
|
||||
height: 25px;
|
||||
a {
|
||||
display: inline;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,6 @@
|
|||
border-left: none;
|
||||
}
|
||||
li {
|
||||
font-size: small;
|
||||
height: 100%;
|
||||
margin-right: 5px;
|
||||
border: 1px solid $gray3;
|
||||
|
@ -68,7 +67,6 @@
|
|||
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;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
@import "colors";
|
||||
@import "global";
|
||||
@import "layout";
|
||||
@import "panels";
|
||||
@import "groups";
|
||||
@import "lists";
|
||||
|
||||
@import "vendor/forms";
|
||||
|
@ -14,6 +14,7 @@
|
|||
|
||||
html, html body, .wrapper {
|
||||
height: 100%;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -33,15 +34,7 @@ body {
|
|||
.react-mount {
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.trash.icon, .done.icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.trash.icon {
|
||||
g {
|
||||
fill: #d9534f;
|
||||
}
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class='wrapper align-center max-width'>
|
||||
<div class='wrapper m-0-auto maxw-1024'>
|
||||
<div class="react-mount projects"></div>
|
||||
<script src="/js/main/projects.js"></script>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="wrapper align-center max-width h-100">
|
||||
<div class="wrapper m-0-auto maxw-1024 h-100">
|
||||
<div class="react-mount edit-task h-100"></div>
|
||||
<script src="/js/main/task/edit.js"></script>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="wrapper align-center max-width">
|
||||
<div class="wrapper m-0-auto maxw-1024">
|
||||
<div class="react-mount tasks"></div>
|
||||
<script src="/js/main/tasks.js"></script>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="wrapper align-center max-width h-100">
|
||||
<div class="wrapper m-0-auto maxw-1024 h-100">
|
||||
<div class="react-mount new-task h-100"></div>
|
||||
<script src="/js/main/task/new.js"></script>
|
||||
</div>
|
||||
|
|
74
twenty-frontend/src/js/components/Group.tsx
Normal file
74
twenty-frontend/src/js/components/Group.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
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 w-100">{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="flex flex-wrap w-75">
|
||||
<div className="w-100">
|
||||
<a href={editHref}>
|
||||
<span>{task.title}</span>
|
||||
<span className="text-smaller text-secondary">
|
||||
{datetime.toFormat("dd LLL, yyyy")} at{" "}
|
||||
{datetime.toFormat("HH:mm")}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-100">
|
||||
<span className="tags">
|
||||
<span
|
||||
style={{ backgroundColor: task.project.color }}
|
||||
className="tag"
|
||||
>
|
||||
{task.project.name}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-content-end w-25">
|
||||
<TaskStatusSelect task={task} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="empty-group">
|
||||
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 });
|
||||
}
|
||||
|
|
|
@ -66,82 +66,78 @@ export function Task({ taskId }: { taskId?: number }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="two-columns h-100">
|
||||
<div className="column-1">
|
||||
<div className="flex w-100 h-100">
|
||||
<div className="w-25">
|
||||
<NavBar />
|
||||
</div>
|
||||
<div className="column-2 h-100">
|
||||
<form className="task h-100" onSubmit={handleSubmit(onSave)}>
|
||||
<div className="panel 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)}
|
||||
>
|
||||
<div className="w-75 h-100">
|
||||
<h1>{task ? "Edit task" : "New task"}</h1>
|
||||
<form className="group h-100" onSubmit={handleSubmit(onSave)}>
|
||||
<div className="group-name">
|
||||
<ul className="tabs">
|
||||
<li className={classnames("tab", { active: isEditable })}>
|
||||
<a href="#" onClick={() => setIsEditable(true)}>
|
||||
Editor
|
||||
</a>
|
||||
</li>
|
||||
<li className={classnames("tab", { active: !isEditable })}>
|
||||
<a href="#" onClick={() => setIsEditable(false)}>
|
||||
Preview
|
||||
</li>
|
||||
</ul>
|
||||
</a>
|
||||
</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="markdown h-50"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rendermd(content || task?.content),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
29
twenty-frontend/src/js/components/TaskStatusSelect.tsx
Normal file
29
twenty-frontend/src/js/components/TaskStatusSelect.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
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.Backlog}>Backlog</option>
|
||||
<option value={TaskStatus.Ready}>Ready</option>
|
||||
<option value={TaskStatus.InProgress}>Working on it</option>
|
||||
<option value={TaskStatus.Complete}>Complete</option>
|
||||
</select>
|
||||
);
|
||||
}
|
|
@ -1,103 +1,33 @@
|
|||
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, TaskStatus } 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({variables: {status: TaskStatus.Ready}});
|
||||
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">
|
||||
<div className="column-1">
|
||||
<div className="flex">
|
||||
<div className="w-25">
|
||||
<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>
|
||||
<div className="w-75">
|
||||
<h1>Tasks</h1>
|
||||
<Group
|
||||
groupName="Working on it"
|
||||
getItems={getTasks(TaskStatus.InProgress)}
|
||||
/>
|
||||
<Group groupName="Ready" getItems={getTasks(TaskStatus.Ready)} />
|
||||
<Group groupName="Backlog" getItems={getTasks(TaskStatus.Backlog)} />
|
||||
</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,12 +1,13 @@
|
|||
import { useQuery, gql } from "@apollo/client";
|
||||
import { Task, TaskStatus } from "/types/schema";
|
||||
import { Task } from "/types/schema";
|
||||
|
||||
const GQL = gql`
|
||||
query Query($status: TaskStatus!) {
|
||||
export const GET_TASKS = gql`
|
||||
query GetTasks($status: TaskStatus!) {
|
||||
tasks(status: $status) {
|
||||
id
|
||||
title
|
||||
status
|
||||
isReady
|
||||
updatedAt
|
||||
project {
|
||||
name
|
||||
|
@ -16,6 +17,6 @@ const GQL = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export function useTasks({...rest}) {
|
||||
return useQuery<{tasks: Task[]}>(GQL, rest);
|
||||
export function useTasks({ ...rest }) {
|
||||
return useQuery<{ tasks: Task[] }>(GET_TASKS, rest);
|
||||
}
|
||||
|
|
|
@ -87,14 +87,22 @@ export type QueryFindTaskArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryTasksArgs = {
|
||||
status: TaskStatus;
|
||||
};
|
||||
|
||||
export type QueryTasksArgs = {
|
||||
status: TaskStatus;
|
||||
};
|
||||
|
||||
export type Task = {
|
||||
__typename?: 'Task';
|
||||
content: Scalars['String']['output'];
|
||||
id: Scalars['Int']['output'];
|
||||
__typename?: "Task";
|
||||
content: Scalars["String"]["output"];
|
||||
id: Scalars["Int"]["output"];
|
||||
inProgress: Scalars["Boolean"]["output"];
|
||||
isBacklogged: Scalars["Boolean"]["output"];
|
||||
isComplete: Scalars["Boolean"]["output"];
|
||||
isReady: Scalars["Boolean"]["output"];
|
||||
project: Project;
|
||||
status: TaskStatus;
|
||||
title: Scalars['String']['output'];
|
||||
|
@ -102,15 +110,17 @@ 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 {
|
||||
Complete = 'complete',
|
||||
InProgress = 'in_progress',
|
||||
Ready = 'ready'
|
||||
Backlog = "backlog",
|
||||
Complete = "complete",
|
||||
InProgress = "in_progress",
|
||||
Ready = "ready",
|
||||
}
|
||||
|
||||
/** Autogenerated return type of UpdateTask. */
|
||||
|
|
Loading…
Reference in a new issue