Task#state -> Task#status (enum)

This commit is contained in:
0x1eef 2023-12-24 17:32:13 -03:00
parent 828e9dea15
commit d475e83602
10 changed files with 99 additions and 31 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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>

View file

@ -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">

View file

@ -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) => {

View file

@ -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;