Task#state
-> Task#status
(enum)
This commit is contained in:
parent
828e9dea15
commit
d475e83602
10 changed files with 99 additions and 31 deletions
|
@ -0,0 +1,12 @@
|
||||||
|
class ChangeStateToStatus < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
rename_column :tasks, :state, :status
|
||||||
|
Twenty::Task.where(status: "open").update_all(status: '0')
|
||||||
|
Twenty::Task.where(status: "closed").update_all(status: '2')
|
||||||
|
change_column :tasks, :status, :integer
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrrversibleMigration
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
class ChangeStatusDefault < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
change_column :tasks, :status, :integer, default: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrrversibleMigration
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,24 +3,21 @@
|
||||||
class Twenty::Task < Twenty::Model
|
class Twenty::Task < Twenty::Model
|
||||||
self.table_name = "tasks"
|
self.table_name = "tasks"
|
||||||
|
|
||||||
|
STATUS = {ready: 0, in_progress: 1, complete: 2}
|
||||||
|
enum :status, STATUS, default: :ready
|
||||||
|
|
||||||
##
|
##
|
||||||
# Validations
|
# Validations
|
||||||
validates :title, presence: true
|
validates :title, presence: true
|
||||||
validates :content, presence: true
|
validates :content, presence: true
|
||||||
validates :state, inclusion: {in: %w[open closed]}
|
|
||||||
validates :project, presence: true
|
validates :project, presence: true
|
||||||
|
|
||||||
##
|
##
|
||||||
# Associations
|
# Associations
|
||||||
belongs_to :project, class_name: "Twenty::Project"
|
belongs_to :project, class_name: "Twenty::Project"
|
||||||
|
|
||||||
##
|
|
||||||
# Scopes
|
|
||||||
scope :open, -> { where(state: "open") }
|
|
||||||
scope :closed, -> { where(state: "closed") }
|
|
||||||
|
|
||||||
def to_json(options = {})
|
def to_json(options = {})
|
||||||
{id:, title:, content:, state:,
|
{id:, title:, content:, status:,
|
||||||
project_id:, created_at:, updated_at:}.to_json(options)
|
project_id:, created_at:, updated_at:}.to_json(options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,4 +10,11 @@ class Twenty::Servlet < WEBrick::HTTPServlet::AbstractServlet
|
||||||
require_relative "servlet/mixin/response_mixin"
|
require_relative "servlet/mixin/response_mixin"
|
||||||
extend ServerMixin
|
extend ServerMixin
|
||||||
include ResponseMixin
|
include ResponseMixin
|
||||||
|
|
||||||
|
def parse_body(req, only: [], except: [])
|
||||||
|
body = JSON.parse(req.body)
|
||||||
|
body = only.size > 0 ? body.slice(*only) : body
|
||||||
|
body = except.size > 0 ? body.except(*except) : body
|
||||||
|
body
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Twenty::Servlet::Tasks < Twenty::Servlet
|
||||||
def do_GET(req, res)
|
def do_GET(req, res)
|
||||||
case req.path_info
|
case req.path_info
|
||||||
when ""
|
when ""
|
||||||
tasks = Twenty::Task.open.order(updated_at: :desc)
|
tasks = Twenty::Task.ready.order(updated_at: :desc)
|
||||||
ok(res, tasks:)
|
ok(res, tasks:)
|
||||||
when %r{\A/([\d]+)/?\z}
|
when %r{\A/([\d]+)/?\z}
|
||||||
task = Twenty::Task.find_by(id: $1)
|
task = Twenty::Task.find_by(id: $1)
|
||||||
|
@ -22,7 +22,8 @@ class Twenty::Servlet::Tasks < Twenty::Servlet
|
||||||
def do_POST(req, res)
|
def do_POST(req, res)
|
||||||
case req.path_info
|
case req.path_info
|
||||||
when ""
|
when ""
|
||||||
task = Twenty::Task.new(JSON.parse(req.body))
|
body = parse_body(req.body, only: ["title", "content", "project_id"])
|
||||||
|
task = Twenty::Task.new(body)
|
||||||
if task.save
|
if task.save
|
||||||
ok(res, task:)
|
ok(res, task:)
|
||||||
else
|
else
|
||||||
|
@ -39,10 +40,10 @@ class Twenty::Servlet::Tasks < Twenty::Servlet
|
||||||
def do_PUT(req, res)
|
def do_PUT(req, res)
|
||||||
case req.path_info
|
case req.path_info
|
||||||
when ""
|
when ""
|
||||||
body = JSON.parse(req.body)
|
body = parse_body(req, except: ["id"])
|
||||||
id = body.delete("id")
|
id = parse_body(req, only: ["id"]).fetch("id", nil)
|
||||||
task = Twenty::Task.find_by(id:)
|
task = Twenty::Task.find_by(id:)
|
||||||
task.update(body) ? ok(res, task:) : not_found(res)
|
task?.update(body) ? ok(res, task:) : not_found(res)
|
||||||
else
|
else
|
||||||
not_found(res)
|
not_found(res)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,11 +4,13 @@ ul.items {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
||||||
li.item {
|
li.item {
|
||||||
margin: 0 0 15px 0;
|
&:hover {
|
||||||
|
background: $gray1;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
display: flex;
|
display: flex;
|
||||||
background: #f4f0ec;
|
border-bottom: 1px solid $gray1;
|
||||||
border: #cfcfc4 1px solid;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
.top, .bottom {
|
.top, .bottom {
|
||||||
|
@ -20,6 +22,16 @@ ul.items {
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.top {
|
||||||
|
a {
|
||||||
|
$blue: #008cff;
|
||||||
|
color: darken($blue, 10%);
|
||||||
|
&:active, &:visited, &:link, &:hover {
|
||||||
|
color: darken($blue, 10%);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.bottom {
|
.bottom {
|
||||||
margin: 10px 0 0 0;
|
margin: 10px 0 0 0;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export function TrashIcon({ onClick }: { onClick: () => unknown }) {
|
export function TrashIcon({
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
onClick: (e: React.MouseEvent) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
height="512"
|
height="512"
|
||||||
viewBox="0 0 128 128"
|
viewBox="0 0 128 128"
|
||||||
width="512"
|
width="512"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
onClick={onClick}
|
onClick={e => onClick(e)}
|
||||||
className="trash icon"
|
className="trash icon"
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
|
@ -20,13 +24,17 @@ export function TrashIcon({ onClick }: { onClick: () => unknown }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DoneIcon({ onClick }: { onClick: () => unknown }) {
|
export function DoneIcon({
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
onClick: (e: React.MouseEvent) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
enable-background="new 0 0 24 24"
|
enableBackground="new 0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
onClick={onClick}
|
onClick={e => onClick(e)}
|
||||||
className="done icon"
|
className="done icon"
|
||||||
>
|
>
|
||||||
<switch>
|
<switch>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useTasks } from "/hooks/useTasks";
|
||||||
import { useDestroyTask } from "/hooks/useDestroyTask";
|
import { useDestroyTask } from "/hooks/useDestroyTask";
|
||||||
import { TrashIcon, DoneIcon } from "/components/Icons";
|
import { TrashIcon, DoneIcon } from "/components/Icons";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { Task } from "/types/schema";
|
import { Task, TASK_COMPLETE } from "/types/schema";
|
||||||
import { useUpsertTask } from "/hooks/useUpsertTask";
|
import { useUpsertTask } from "/hooks/useUpsertTask";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ export function Tasks() {
|
||||||
};
|
};
|
||||||
const onComplete = (task: Task) => {
|
const onComplete = (task: Task) => {
|
||||||
const action = () =>
|
const action = () =>
|
||||||
upsertTask({ input: { id: task.id, state: "closed" } });
|
upsertTask({ input: { id: task.id, status: TASK_COMPLETE } });
|
||||||
perform(action, { on: task, tasks, setTask: setCompletedTask });
|
perform(action, { on: task, tasks, setTask: setCompletedTask });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,15 +58,30 @@ export function Tasks() {
|
||||||
const wasDestroyed = task === destroyedTask;
|
const wasDestroyed = task === destroyedTask;
|
||||||
const wasCompleted = task === completedTask;
|
const wasCompleted = task === completedTask;
|
||||||
const classes = { completed: wasCompleted, removed: wasDestroyed };
|
const classes = { completed: wasCompleted, removed: wasDestroyed };
|
||||||
|
const editHref = `/tasks/edit#id=${task.id}`;
|
||||||
return (
|
return (
|
||||||
<li className={classnames("item", classes)} key={key}>
|
<li
|
||||||
|
onClick={() => (location.href = editHref)}
|
||||||
|
className={classnames("item", classes)}
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
<div className="top">
|
<div className="top">
|
||||||
<a href={`/tasks/edit#id=${task.id}`}>
|
<a href={editHref}>
|
||||||
<span className="item title">{task.title}</span>
|
<span className="item title">{task.title}</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<DoneIcon onClick={() => onComplete(task)} />
|
<DoneIcon
|
||||||
<TrashIcon onClick={() => onDestroy(task)} />
|
onClick={(e: React.MouseEvent) => [
|
||||||
|
e.stopPropagation(),
|
||||||
|
onComplete(task),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<TrashIcon
|
||||||
|
onClick={(e: React.MouseEvent) => [
|
||||||
|
e.stopPropagation(),
|
||||||
|
onDestroy(task),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bottom">
|
<div className="bottom">
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { TASK_STATUS } from "/types/schema";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
id?: number;
|
id?: number;
|
||||||
state?: "open" | "closed";
|
status?: TASK_STATUS,
|
||||||
title?: string;
|
title?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
|
@ -8,8 +10,8 @@ type Params = {
|
||||||
|
|
||||||
export function useUpsertTask() {
|
export function useUpsertTask() {
|
||||||
const normalize = (input: Params) => {
|
const normalize = (input: Params) => {
|
||||||
const { id, title, content, state, projectId } = input;
|
const { id, title, content, status, projectId } = input;
|
||||||
return { id, title, content, state, project_id: projectId };
|
return { id, title, content, status, project_id: projectId };
|
||||||
};
|
};
|
||||||
return function ({ input }: { input: Params }) {
|
return function ({ input }: { input: Params }) {
|
||||||
return new Promise((accept, reject) => {
|
return new Promise((accept, reject) => {
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
export const TASK_READY = "ready";
|
||||||
|
export const TASK_IN_PROGRESS = "in_progress";
|
||||||
|
export const TASK_COMPLETE = "complete";
|
||||||
|
export type TASK_STATUS = "ready" | "in_progress" | "complete";
|
||||||
|
|
||||||
export type Project = {
|
export type Project = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -8,7 +13,7 @@ export type Task = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
state: "open" | "closed";
|
status: TASK_STATUS;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
project_id: number;
|
project_id: number;
|
||||||
|
|
Loading…
Reference in a new issue