Checklist management without reordering
This commit is contained in:
parent
dc2b6dac8b
commit
d0f410323e
146
main.go
146
main.go
@ -104,7 +104,13 @@ func main() { // {{{
|
|||||||
service.Register("/node/delete", true, true, nodeDelete)
|
service.Register("/node/delete", true, true, nodeDelete)
|
||||||
service.Register("/node/download", true, true, nodeDownload)
|
service.Register("/node/download", true, true, nodeDownload)
|
||||||
service.Register("/node/search", true, true, nodeSearch)
|
service.Register("/node/search", true, true, nodeSearch)
|
||||||
|
service.Register("/node/checklist_group/add", true, true, nodeChecklistGroupAdd)
|
||||||
|
service.Register("/node/checklist_group/item_add", true, true, nodeChecklistGroupItemAdd)
|
||||||
|
service.Register("/node/checklist_group/label", true, true, nodeChecklistGroupLabel)
|
||||||
|
service.Register("/node/checklist_group/delete", true, true, nodeChecklistGroupDelete)
|
||||||
service.Register("/node/checklist_item/state", true, true, nodeChecklistItemState)
|
service.Register("/node/checklist_item/state", true, true, nodeChecklistItemState)
|
||||||
|
service.Register("/node/checklist_item/label", true, true, nodeChecklistItemLabel)
|
||||||
|
service.Register("/node/checklist_item/delete", true, true, nodeChecklistItemDelete)
|
||||||
service.Register("/key/retrieve", true, true, keyRetrieve)
|
service.Register("/key/retrieve", true, true, keyRetrieve)
|
||||||
service.Register("/key/create", true, true, keyCreate)
|
service.Register("/key/create", true, true, keyCreate)
|
||||||
service.Register("/key/counter", true, true, keyCounter)
|
service.Register("/key/counter", true, true, keyCounter)
|
||||||
@ -478,8 +484,102 @@ func nodeSearch(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{
|
|||||||
"Nodes": nodes,
|
"Nodes": nodes,
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func nodeChecklistGroupAdd(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var err error
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
NodeID int
|
||||||
|
Label string
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var group ChecklistGroup
|
||||||
|
group, err = ChecklistGroupAdd(sess.UserID, req.NodeID, req.Label)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
group.Items = []ChecklistItem{}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
"Group": group,
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
|
func nodeChecklistGroupItemAdd(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var err error
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
ChecklistGroupID int
|
||||||
|
Label string
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var item ChecklistItem
|
||||||
|
item, err = ChecklistGroupItemAdd(sess.UserID, req.ChecklistGroupID, req.Label)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
"Item": item,
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
|
func nodeChecklistGroupLabel(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var err error
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
ChecklistGroupID int
|
||||||
|
Label string
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var item ChecklistItem
|
||||||
|
item, err = ChecklistGroupLabel(sess.UserID, req.ChecklistGroupID, req.Label)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
"Item": item,
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
|
func nodeChecklistGroupDelete(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var err error
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
ChecklistGroupID int
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ChecklistGroupDelete(sess.UserID, req.ChecklistGroupID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("checklist", "error", err)
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
logger.Info("webserver", "request", "/node/checklist_item/state")
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
req := struct {
|
req := struct {
|
||||||
@ -501,6 +601,50 @@ func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *sessio
|
|||||||
"OK": true,
|
"OK": true,
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func nodeChecklistItemLabel(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var err error
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
ChecklistItemID int
|
||||||
|
Label string
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ChecklistItemLabel(sess.UserID, req.ChecklistItemID, req.Label)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
|
func nodeChecklistItemDelete(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var err error
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
ChecklistItemID int
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ChecklistItemDelete(sess.UserID, req.ChecklistItemID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("checklist", "error", err)
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
|
|
||||||
func keyRetrieve(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
func keyRetrieve(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
logger.Info("webserver", "request", "/key/retrieve")
|
logger.Info("webserver", "request", "/key/retrieve")
|
||||||
|
152
node.go
152
node.go
@ -441,6 +441,110 @@ func SearchNodes(userID int, search string) (nodes []Node, err error) { // {{{
|
|||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
|
func ChecklistGroupAdd(userID, nodeID int, label string) (item ChecklistGroup, err error) { // {{{
|
||||||
|
var row *sqlx.Row
|
||||||
|
row = service.Db.Conn.QueryRowx(
|
||||||
|
`
|
||||||
|
INSERT INTO checklist_group(node_id, "order", "label")
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
MAX("order")+1 AS "order",
|
||||||
|
$2 AS "label"
|
||||||
|
FROM checklist_group g
|
||||||
|
INNER JOIN node n ON g.node_id = n.id
|
||||||
|
WHERE
|
||||||
|
user_id = $3 AND
|
||||||
|
node_id = $1
|
||||||
|
GROUP BY
|
||||||
|
node_id
|
||||||
|
) UNION (
|
||||||
|
SELECT
|
||||||
|
node.id AS node_id,
|
||||||
|
0 AS "order",
|
||||||
|
$2 AS "label"
|
||||||
|
FROM node
|
||||||
|
WHERE
|
||||||
|
user_id = $3 AND
|
||||||
|
node.id = $1
|
||||||
|
)
|
||||||
|
ORDER BY "order" DESC
|
||||||
|
LIMIT 1
|
||||||
|
RETURNING
|
||||||
|
*
|
||||||
|
`,
|
||||||
|
nodeID,
|
||||||
|
label,
|
||||||
|
userID,
|
||||||
|
)
|
||||||
|
err = row.StructScan(&item)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func ChecklistGroupLabel(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`
|
||||||
|
UPDATE checklist_group g
|
||||||
|
SET label = $3
|
||||||
|
FROM node n
|
||||||
|
WHERE
|
||||||
|
g.node_id = n.id AND
|
||||||
|
n.user_id = $1 AND
|
||||||
|
g.id = $2;
|
||||||
|
`,
|
||||||
|
userID,
|
||||||
|
checklistGroupID,
|
||||||
|
label,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func ChecklistGroupItemAdd(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{
|
||||||
|
var row *sqlx.Row
|
||||||
|
row = service.Db.Conn.QueryRowx(
|
||||||
|
`
|
||||||
|
INSERT INTO checklist_item(checklist_group_id, "order", "label")
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
checklist_group_id,
|
||||||
|
MAX("order")+1 AS "order",
|
||||||
|
$1 AS "label"
|
||||||
|
FROM checklist_item
|
||||||
|
WHERE
|
||||||
|
checklist_group_id = $2
|
||||||
|
GROUP BY
|
||||||
|
checklist_group_id
|
||||||
|
) UNION (
|
||||||
|
SELECT $2 AS checklist_group_id, 0 AS "order", $1 AS "label"
|
||||||
|
)
|
||||||
|
ORDER BY "order" DESC
|
||||||
|
LIMIT 1
|
||||||
|
RETURNING
|
||||||
|
*
|
||||||
|
`,
|
||||||
|
label,
|
||||||
|
checklistGroupID,
|
||||||
|
)
|
||||||
|
err = row.StructScan(&item)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func ChecklistGroupDelete(userID, checklistGroupID int) (err error) { // {{{
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`
|
||||||
|
DELETE
|
||||||
|
FROM checklist_group g
|
||||||
|
USING
|
||||||
|
node n
|
||||||
|
WHERE
|
||||||
|
g.id = $2 AND
|
||||||
|
g.node_id = n.id AND
|
||||||
|
n.user_id = $1
|
||||||
|
`,
|
||||||
|
userID,
|
||||||
|
checklistGroupID,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
func ChecklistItemState(userID, checklistItemID int, state bool) (err error) { // {{{
|
func ChecklistItemState(userID, checklistItemID int, state bool) (err error) { // {{{
|
||||||
_, err = service.Db.Conn.Exec(
|
_, err = service.Db.Conn.Exec(
|
||||||
`
|
`
|
||||||
@ -459,6 +563,43 @@ func ChecklistItemState(userID, checklistItemID int, state bool) (err error) {//
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func ChecklistItemLabel(userID, checklistItemID int, label string) (err error) { // {{{
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`
|
||||||
|
UPDATE checklist_item i
|
||||||
|
SET label = $3
|
||||||
|
FROM checklist_group g, node n
|
||||||
|
WHERE
|
||||||
|
i.checklist_group_id = g.id AND
|
||||||
|
g.node_id = n.id AND
|
||||||
|
n.user_id = $1 AND
|
||||||
|
i.id = $2;
|
||||||
|
`,
|
||||||
|
userID,
|
||||||
|
checklistItemID,
|
||||||
|
label,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func ChecklistItemDelete(userID, checklistItemID int) (err error) { // {{{
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`
|
||||||
|
DELETE
|
||||||
|
FROM checklist_item i
|
||||||
|
USING
|
||||||
|
checklist_group g,
|
||||||
|
node n
|
||||||
|
WHERE
|
||||||
|
i.id = $2 AND
|
||||||
|
i.checklist_group_id = g.id AND
|
||||||
|
g.node_id = n.id AND
|
||||||
|
n.user_id = $1
|
||||||
|
`,
|
||||||
|
userID,
|
||||||
|
checklistItemID,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
func (node *Node) retrieveChecklist() (err error) { // {{{
|
func (node *Node) retrieveChecklist() (err error) { // {{{
|
||||||
var rows *sqlx.Rows
|
var rows *sqlx.Rows
|
||||||
@ -468,10 +609,10 @@ func (node *Node) retrieveChecklist() (err error) { // {{{
|
|||||||
g.order AS group_order,
|
g.order AS group_order,
|
||||||
g.label AS group_label,
|
g.label AS group_label,
|
||||||
|
|
||||||
i.id AS item_id,
|
COALESCE(i.id, 0) AS item_id,
|
||||||
i.order AS item_order,
|
COALESCE(i.order, 0) AS item_order,
|
||||||
i.label AS item_label,
|
COALESCE(i.label, '') AS item_label,
|
||||||
i.checked
|
COALESCE(i.checked, false) AS checked
|
||||||
|
|
||||||
FROM public.checklist_group g
|
FROM public.checklist_group g
|
||||||
LEFT JOIN public.checklist_item i ON i.checklist_group_id = g.id
|
LEFT JOIN public.checklist_item i ON i.checklist_group_id = g.id
|
||||||
@ -522,8 +663,11 @@ func (node *Node) retrieveChecklist() (err error) { // {{{
|
|||||||
item.Order = row.ItemOrder
|
item.Order = row.ItemOrder
|
||||||
item.Label = row.ItemLabel
|
item.Label = row.ItemLabel
|
||||||
item.Checked = row.Checked
|
item.Checked = row.Checked
|
||||||
|
|
||||||
|
if item.ID > 0 {
|
||||||
group.Items = append(group.Items, item)
|
group.Items = append(group.Items, item)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node.ChecklistGroups = []ChecklistGroup{}
|
node.ChecklistGroups = []ChecklistGroup{}
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
|
@ -330,6 +330,11 @@ header .menu {
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
#markdown pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
#markdown pre > code {
|
#markdown pre > code {
|
||||||
background: unset;
|
background: unset;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -346,27 +351,72 @@ header .menu {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
#checklist .header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: 0 16px;
|
||||||
|
}
|
||||||
|
#checklist .header img {
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#checklist .header + .checklist-group.edit {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
#checklist .checklist-group {
|
#checklist .checklist-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, min-content);
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: 0 8px;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
margin-bottom: 8px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
#checklist .checklist-group .label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#checklist .checklist-group .label.ok {
|
||||||
|
color: #54b356;
|
||||||
|
}
|
||||||
|
#checklist .checklist-group .label.error {
|
||||||
|
color: #d13636;
|
||||||
|
}
|
||||||
|
#checklist .checklist-group.edit {
|
||||||
|
margin-top: 32px;
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
#checklist .checklist-group:not(.edit) .reorder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#checklist .checklist-group:not(.edit) img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#checklist .checklist-group img {
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
#checklist .checklist-item {
|
#checklist .checklist-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
grid-gap: 0 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 0.5em;
|
padding: 4px 0;
|
||||||
|
border-bottom: 2px solid #fff;
|
||||||
}
|
}
|
||||||
#checklist .checklist-item.checked {
|
#checklist .checklist-item.checked {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
color: #888;
|
color: #aaa;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item.drag-target {
|
||||||
|
border-bottom: 2px solid #71c837;
|
||||||
}
|
}
|
||||||
#checklist .checklist-item input[type="checkbox"] {
|
#checklist .checklist-item input[type="checkbox"] {
|
||||||
margin-left: 0px;
|
|
||||||
margin-right: 8px;
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin: 0 8px 0 0;
|
margin: 0 2px 0 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
@ -377,6 +427,12 @@ header .menu {
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
}
|
}
|
||||||
|
#checklist .checklist-item label.ok {
|
||||||
|
color: #54b356;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item label.error {
|
||||||
|
color: #d13636;
|
||||||
|
}
|
||||||
#checklist .checklist-item input[type="checkbox"].ok {
|
#checklist .checklist-item input[type="checkbox"].ok {
|
||||||
border: 0.15em solid #54b356;
|
border: 0.15em solid #54b356;
|
||||||
}
|
}
|
||||||
@ -400,8 +456,22 @@ header .menu {
|
|||||||
#checklist .checklist-item input[type="checkbox"]:checked::before {
|
#checklist .checklist-item input[type="checkbox"]:checked::before {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
#checklist .checklist-item.edit input[type="checkbox"] {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item:not(.edit) .reorder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item:not(.edit) img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item img {
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
#checklist .checklist-item label {
|
#checklist .checklist-item label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
/* ============================================================= *
|
/* ============================================================= *
|
||||||
* Textarea replicates the height of an element expanding height *
|
* Textarea replicates the height of an element expanding height *
|
||||||
|
76
static/images/add-gray.svg
Normal file
76
static/images/add-gray.svg
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="127.99999"
|
||||||
|
height="127.99999"
|
||||||
|
viewBox="0 0 33.866664 33.866666"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
|
||||||
|
sodipodi:docname="add-gray.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1"
|
||||||
|
inkscape:cx="28.5"
|
||||||
|
inkscape:cy="32"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1044"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="1096"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d6d6d6"
|
||||||
|
showborder="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-57.706364,-79.853668)">
|
||||||
|
<rect
|
||||||
|
style="color:#000000;overflow:visible;fill:#8dd35f;fill-rule:evenodd;stroke-width:3.175;paint-order:markers stroke fill;stop-color:#000000;fill-opacity:1"
|
||||||
|
id="rect232"
|
||||||
|
width="33.866669"
|
||||||
|
height="7.4083333"
|
||||||
|
x="57.706364"
|
||||||
|
y="93.082832" />
|
||||||
|
<rect
|
||||||
|
style="color:#000000;overflow:visible;fill:#8dd35f;fill-rule:evenodd;stroke-width:3.175;paint-order:markers stroke fill;stop-color:#000000;fill-opacity:1"
|
||||||
|
id="rect396"
|
||||||
|
width="33.866665"
|
||||||
|
height="7.4083333"
|
||||||
|
x="79.853668"
|
||||||
|
y="-78.343864"
|
||||||
|
transform="rotate(90)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
66
static/images/edit-list-gray.svg
Normal file
66
static/images/edit-list-gray.svg
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="19.463146mm"
|
||||||
|
height="14.45555mm"
|
||||||
|
viewBox="0 0 19.463145 14.455551"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
|
||||||
|
sodipodi:docname="edit.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="4"
|
||||||
|
inkscape:cx="34.875"
|
||||||
|
inkscape:cy="63"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1044"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="1096"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="false"
|
||||||
|
inkscape:deskcolor="#dddddd"
|
||||||
|
showborder="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-95.481611,-106.78967)">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m 95.481611,106.78967 v 2.06503 h 11.357839 v -2.06503 H 95.481611 m 0,4.13011 v 2.06507 h 11.357839 v -2.06507 H 95.481611 m 17.553019,0.10341 c -0.10342,0 -0.30986,0.10342 -0.41304,0.20644 l -1.03251,1.03256 2.16829,2.16829 1.03255,-1.03252 c 0.20645,-0.20644 0.20645,-0.61951 0,-0.82603 l -1.34229,-1.3423 c -0.10342,-0.10302 -0.20644,-0.20644 -0.413,-0.20644 m -1.96181,1.85855 -6.29844,6.19518 v 2.1683 h 2.16829 l 6.29844,-6.29844 -2.16829,-2.06504 m -15.591209,2.1683 v 2.06507 h 7.227699 v -2.06507 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:1.03253;fill:#b1b1b1;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
66
static/images/edit-list.svg
Normal file
66
static/images/edit-list.svg
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="19.463146mm"
|
||||||
|
height="14.45555mm"
|
||||||
|
viewBox="0 0 19.463145 14.455551"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
|
||||||
|
sodipodi:docname="edit.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1"
|
||||||
|
inkscape:cx="-9.5"
|
||||||
|
inkscape:cy="166"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1044"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="1096"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="false"
|
||||||
|
inkscape:deskcolor="#dddddd"
|
||||||
|
showborder="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-95.481611,-106.78967)">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m 95.481611,106.78967 v 2.06503 h 11.357839 v -2.06503 H 95.481611 m 0,4.13011 v 2.06507 h 11.357839 v -2.06507 H 95.481611 m 17.553019,0.10341 c -0.10342,0 -0.30986,0.10342 -0.41304,0.20644 l -1.03251,1.03256 2.16829,2.16829 1.03255,-1.03252 c 0.20645,-0.20644 0.20645,-0.61951 0,-0.82603 l -1.34229,-1.3423 c -0.10342,-0.10302 -0.20644,-0.20644 -0.413,-0.20644 m -1.96181,1.85855 -6.29844,6.19518 v 2.1683 h 2.16829 l 6.29844,-6.29844 -2.16829,-2.06504 m -15.591209,2.1683 v 2.06507 h 7.227699 v -2.06507 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:1.03253" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
66
static/images/edit.svg
Normal file
66
static/images/edit.svg
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="26.156595mm"
|
||||||
|
height="26.156595mm"
|
||||||
|
viewBox="0 0 26.156594 26.156597"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
|
||||||
|
sodipodi:docname="edit.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1"
|
||||||
|
inkscape:cx="6.5"
|
||||||
|
inkscape:cy="41"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1044"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="1096"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="false"
|
||||||
|
inkscape:deskcolor="#dddddd"
|
||||||
|
showborder="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-103.00032,-112.64716)">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m 128.73192,118.52071 c 0.56666,-0.56666 0.56666,-1.51108 0,-2.04869 l -3.39986,-3.39986 c -0.53761,-0.56666 -1.48203,-0.56666 -2.04869,0 l -2.67339,2.6589 5.44854,5.44849 m -23.0582,12.17566 v 5.44855 h 5.44849 l 16.06958,-16.08408 -5.44854,-5.44855 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:1.45294;fill:#ff9955;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
66
static/images/trashcan.svg
Normal file
66
static/images/trashcan.svg
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="23.835697mm"
|
||||||
|
height="26.815161mm"
|
||||||
|
viewBox="0 0 23.835696 26.815162"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
|
||||||
|
sodipodi:docname="trashcan.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="1"
|
||||||
|
inkscape:cx="45.5"
|
||||||
|
inkscape:cy="51"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1044"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="1096"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:showpageshadow="true"
|
||||||
|
inkscape:pagecheckerboard="false"
|
||||||
|
inkscape:deskcolor="#dddddd"
|
||||||
|
showborder="true" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-81.215483,-137.6695)">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="m 88.66414,137.6695 v 1.48977 h -7.448657 v 2.97942 h 1.489734 v 19.36654 a 2.9794621,2.9794621 0 0 0 2.979459,2.97943 h 14.897314 a 2.9794621,2.9794621 0 0 0 2.97946,-2.97943 v -19.36654 h 1.48973 v -2.97942 H 97.602526 V 137.6695 H 88.66414 m -2.979464,4.46919 h 14.897314 v 19.36654 H 85.684676 v -19.36654 m 2.979464,2.97948 v 13.40758 h 2.979464 V 145.11817 H 88.66414 m 5.958922,0 v 13.40758 h 2.979464 v -13.40758 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:1.48973;fill:#d35f5f" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
@ -1,10 +1,12 @@
|
|||||||
|
import 'preact/debug'
|
||||||
import 'preact/devtools'
|
import 'preact/devtools'
|
||||||
import { signal } from 'preact/signals'
|
|
||||||
import { h, Component, render, createRef } from 'preact'
|
import { h, Component, render, createRef } from 'preact'
|
||||||
import htm from 'htm'
|
import htm from 'htm'
|
||||||
import { Session } from 'session'
|
import { Session } from 'session'
|
||||||
import { Node, NodeUI } from 'node'
|
import { Node, NodeUI } from 'node'
|
||||||
import { Websocket } from 'ws'
|
import { Websocket } from 'ws'
|
||||||
|
import { signal } from 'preact/signals'
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
@ -12,13 +12,47 @@ export class ChecklistGroup {
|
|||||||
constructor(data) {//{{{
|
constructor(data) {//{{{
|
||||||
Object.keys(data).forEach(key => {
|
Object.keys(data).forEach(key => {
|
||||||
if (key == 'Items')
|
if (key == 'Items')
|
||||||
this.items = data[key].map(itemData =>
|
this.items = data[key].map(itemData => {
|
||||||
new ChecklistItem(itemData)
|
let item = new ChecklistItem(itemData)
|
||||||
|
item.checklistGroup = this
|
||||||
|
return item
|
||||||
|
}
|
||||||
).sort(ChecklistItem.sort)
|
).sort(ChecklistItem.sort)
|
||||||
else
|
else
|
||||||
this[key] = data[key]
|
this[key] = data[key]
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
addItem(label, okCallback) {//{{{
|
||||||
|
window._app.current.request('/node/checklist_group/item_add', {
|
||||||
|
ChecklistGroupID: this.ID,
|
||||||
|
Label: label,
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
let item = new ChecklistItem(json.Item)
|
||||||
|
item.checklistGroup = this
|
||||||
|
this.items.push(item)
|
||||||
|
okCallback()
|
||||||
|
})
|
||||||
|
.catch(window._app.current.responseError)
|
||||||
|
return
|
||||||
|
}//}}}
|
||||||
|
updateLabel(newLabel, okCallback, errCallback) {//{{{
|
||||||
|
window._app.current.request('/node/checklist_group/label', {
|
||||||
|
ChecklistGroupID: this.ID,
|
||||||
|
Label: newLabel,
|
||||||
|
})
|
||||||
|
.then(okCallback)
|
||||||
|
.catch(errCallback)
|
||||||
|
}//}}}
|
||||||
|
delete(okCallback, errCallback) {//{{{
|
||||||
|
window._app.current.request('/node/checklist_group/delete', {
|
||||||
|
ChecklistGroupID: this.ID,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
okCallback()
|
||||||
|
})
|
||||||
|
.catch(errCallback)
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChecklistItem {
|
export class ChecklistItem {
|
||||||
@ -32,34 +66,193 @@ export class ChecklistItem {
|
|||||||
this[key] = data[key]
|
this[key] = data[key]
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
updateState(newState, okCallback, errCallback) {//{{{
|
||||||
|
window._app.current.request('/node/checklist_item/state', {
|
||||||
|
ChecklistItemID: this.ID,
|
||||||
|
State: newState,
|
||||||
|
})
|
||||||
|
.then(okCallback)
|
||||||
|
.catch(errCallback)
|
||||||
|
}//}}}
|
||||||
|
updateLabel(newLabel, okCallback, errCallback) {//{{{
|
||||||
|
window._app.current.request('/node/checklist_item/label', {
|
||||||
|
ChecklistItemID: this.ID,
|
||||||
|
Label: newLabel,
|
||||||
|
})
|
||||||
|
.then(okCallback)
|
||||||
|
.catch(errCallback)
|
||||||
|
}//}}}
|
||||||
|
delete(okCallback, errCallback) {//{{{
|
||||||
|
window._app.current.request('/node/checklist_item/delete', {
|
||||||
|
ChecklistItemID: this.ID,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.checklistGroup.items = this.checklistGroup.items.filter(item => item.ID != this.ID)
|
||||||
|
okCallback()
|
||||||
|
})
|
||||||
|
.catch(errCallback)
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Checklist extends Component {
|
export class Checklist extends Component {
|
||||||
render({ groups }) {//{{{
|
constructor() {//{{{
|
||||||
|
super()
|
||||||
|
this.edit = signal(true)
|
||||||
|
this.dragItemSource = null
|
||||||
|
this.dragItemTarget = null
|
||||||
|
this.state = {
|
||||||
|
confirmDeletion: true,
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
render({ groups }, { confirmDeletion }) {//{{{
|
||||||
if (groups.length == 0)
|
if (groups.length == 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
groups.sort(ChecklistGroup.sort)
|
if (typeof groups.sort != 'function')
|
||||||
|
groups = []
|
||||||
|
|
||||||
let groupElements = groups.map(group => html`<${ChecklistGroupElement} group=${group} />`)
|
groups.sort(ChecklistGroup.sort)
|
||||||
|
let groupElements = groups.map(group => html`<${ChecklistGroupElement} key="group-${group.ID}" ui=${this} group=${group} />`)
|
||||||
|
|
||||||
|
let edit = 'edit-list-gray.svg'
|
||||||
|
let confirmDeletionEl = ''
|
||||||
|
if (this.edit.value) {
|
||||||
|
edit = 'edit-list.svg'
|
||||||
|
confirmDeletionEl = html`
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="confirm-checklist-delete" checked=${confirmDeletion} onchange=${() => this.setState({ confirmDeletion: !confirmDeletion })} />
|
||||||
|
<label for="confirm-checklist-delete">Confirm checklist deletion</label>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
let addGroup = ()=>{
|
||||||
|
if (this.edit.value)
|
||||||
|
return html`<img src="/images/${_VERSION}/add-gray.svg" onclick=${()=>this.addGroup()} />`
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div id="checklist">
|
<div id="checklist">
|
||||||
|
<div class="header">
|
||||||
<h1>Checklist</h1>
|
<h1>Checklist</h1>
|
||||||
|
<img src="/images/${_VERSION}/${edit}" onclick=${() => this.toggleEdit()} />
|
||||||
|
<${addGroup} />
|
||||||
|
</div>
|
||||||
|
${confirmDeletionEl}
|
||||||
${groupElements}
|
${groupElements}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
|
toggleEdit() {//{{{
|
||||||
|
this.edit.value = !this.edit.value
|
||||||
|
}//}}}
|
||||||
|
addGroup() {//{{{
|
||||||
|
let label = prompt("Create a new group")
|
||||||
|
if (label === null)
|
||||||
|
return
|
||||||
|
label = label.trim()
|
||||||
|
if (label == '')
|
||||||
|
return
|
||||||
|
|
||||||
|
window._app.current.request('/node/checklist_group/add', {
|
||||||
|
NodeID: window._app.current.nodeUI.current.node.value.ID,
|
||||||
|
Label: label,
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
let group = new ChecklistGroup(json.Group)
|
||||||
|
this.props.groups.push(group)
|
||||||
|
this.forceUpdate()
|
||||||
|
})
|
||||||
|
.catch(window._app.current.responseError)
|
||||||
|
return
|
||||||
|
}//}}}
|
||||||
|
dragTarget(target) {//{{{
|
||||||
|
if (this.dragItemTarget)
|
||||||
|
this.dragItemTarget.setDragTarget(false)
|
||||||
|
this.dragItemTarget = target
|
||||||
|
target.setDragTarget(true)
|
||||||
|
}//}}}
|
||||||
|
dragReset() {//{{{
|
||||||
|
if (this.dragItemTarget) {
|
||||||
|
this.dragItemTarget.setDragTarget(false)
|
||||||
|
this.dragItemTarget = null
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChecklistGroupElement extends Component {
|
class ChecklistGroupElement extends Component {
|
||||||
render({ group }) {//{{{
|
constructor() {//{{{
|
||||||
let items = group.items.map(item => html`<${ChecklistItemElement} item=${item} />`)
|
super()
|
||||||
|
this.label = createRef()
|
||||||
|
}//}}}
|
||||||
|
render({ ui, group }) {//{{{
|
||||||
|
let items = ({ ui, group }) =>
|
||||||
|
group.items.map(item => html`<${ChecklistItemElement} key="item-${item.ID}" ui=${ui} group=${this} item=${item} />`)
|
||||||
|
|
||||||
|
let label = ()=>html`<div class="label" style="cursor: pointer" ref=${this.label} onclick=${()=>this.editLabel()}>${group.Label}</div>`
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="checklist-group">${group.Label}</div>
|
<div class="checklist-group-container">
|
||||||
${items}
|
<div class="checklist-group ${ui.edit.value ? 'edit' : ''}">
|
||||||
|
<div class="reorder" style="cursor: grab">☰</div>
|
||||||
|
<img src="/images/${_VERSION}/trashcan.svg" onclick=${() => this.delete()} />
|
||||||
|
<${label} />
|
||||||
|
<img src="/images/${_VERSION}/add-gray.svg" onclick=${() => this.addItem()} />
|
||||||
|
</div>
|
||||||
|
<${items} ui=${ui} group=${group} />
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
|
addItem() {//{{{
|
||||||
|
let label = prompt("Create a new item")
|
||||||
|
if (label === null)
|
||||||
|
return
|
||||||
|
label = label.trim()
|
||||||
|
if (label == '')
|
||||||
|
return
|
||||||
|
|
||||||
|
this.props.group.addItem(label, () => {
|
||||||
|
this.forceUpdate()
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
|
editLabel() {//{{{
|
||||||
|
let label = prompt('Edit label', this.props.group.Label)
|
||||||
|
if (label === null)
|
||||||
|
return
|
||||||
|
|
||||||
|
label = label.trim()
|
||||||
|
if (label == '') {
|
||||||
|
alert(`A label can't be empty.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.label.current.classList.remove('error')
|
||||||
|
this.props.group.updateLabel(label, () => {
|
||||||
|
this.props.group.Label = label
|
||||||
|
this.label.current.innerHTML = label
|
||||||
|
this.label.current.classList.add('ok')
|
||||||
|
this.forceUpdate()
|
||||||
|
setTimeout(() => this.label.current.classList.remove('ok'), 500)
|
||||||
|
}, () => {
|
||||||
|
this.label.current.classList.add('error')
|
||||||
|
})
|
||||||
|
|
||||||
|
}//}}}
|
||||||
|
delete() {//{{{
|
||||||
|
if (this.props.ui.state.confirmDeletion) {
|
||||||
|
if (!confirm(`Delete '${this.props.group.Label}'?`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.group.delete(() => {
|
||||||
|
this.props.ui.props.groups = this.props.ui.props.groups.filter(g => g.ID != this.props.group.ID)
|
||||||
|
this.props.ui.forceUpdate()
|
||||||
|
}, err => {
|
||||||
|
console.log(err)
|
||||||
|
console.log('error')
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChecklistItemElement extends Component {
|
class ChecklistItemElement extends Component {
|
||||||
@ -67,31 +260,100 @@ class ChecklistItemElement extends Component {
|
|||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
checked: props.item.Checked,
|
checked: props.item.Checked,
|
||||||
|
dragTarget: false,
|
||||||
}
|
}
|
||||||
this.checkbox = createRef()
|
this.checkbox = createRef()
|
||||||
|
this.label = createRef()
|
||||||
}//}}}
|
}//}}}
|
||||||
render({ item }, { checked }) {//{{{
|
render({ ui, item }, { checked, dragTarget }) {//{{{
|
||||||
|
let checkbox = ()=>{
|
||||||
|
if (ui.edit.value)
|
||||||
|
return html`<label ref=${this.label} onclick=${() => this.editLabel()} style="cursor: pointer">${item.Label}</label>`
|
||||||
|
else
|
||||||
return html`
|
return html`
|
||||||
<div class="checklist-item ${checked ? 'checked' : ''}">
|
|
||||||
<input type="checkbox" ref=${this.checkbox} key="checkbox-${item.ID}" id="checkbox-${item.ID}" checked=${checked} onchange=${evt => this.update(evt.target.checked)} />
|
<input type="checkbox" ref=${this.checkbox} key="checkbox-${item.ID}" id="checkbox-${item.ID}" checked=${checked} onchange=${evt => this.update(evt.target.checked)} />
|
||||||
<label for="checkbox-${item.ID}">${item.Label}</label>
|
<label ref=${this.label} for="checkbox-${item.ID}">${item.Label}</label>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="checklist-item ${checked ? 'checked' : ''} ${ui.edit.value ? 'edit' : ''} ${dragTarget ? 'drag-target' : ''}" draggable=true>
|
||||||
|
<div class="reorder" style="user-select: none;">☰</div>
|
||||||
|
<img src="/images/${_VERSION}/trashcan.svg" onclick=${() => this.delete()} />
|
||||||
|
<${checkbox} />
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
|
componentDidMount() {//{{{
|
||||||
|
this.base.addEventListener('dragstart', ()=>this.dragStart())
|
||||||
|
this.base.addEventListener('dragend', ()=>this.dragEnd())
|
||||||
|
this.base.addEventListener('dragenter', evt=>this.dragEnter(evt))
|
||||||
|
}//}}}
|
||||||
|
|
||||||
update(checked) {//{{{
|
update(checked) {//{{{
|
||||||
this.setState({ checked })
|
this.setState({ checked })
|
||||||
this.checkbox.current.classList.remove('error')
|
this.checkbox.current.classList.remove('error')
|
||||||
window._app.current.request('/node/checklist_item/state', {
|
this.props.item.updateState(checked, () => {
|
||||||
ChecklistItemID: this.props.item.ID,
|
|
||||||
State: checked,
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
this.checkbox.current.classList.add('ok')
|
this.checkbox.current.classList.add('ok')
|
||||||
setTimeout(() => this.checkbox.current.classList.remove('ok'), 500)
|
setTimeout(() => this.checkbox.current.classList.remove('ok'), 500)
|
||||||
|
}, () => {
|
||||||
})
|
|
||||||
.catch(()=>{
|
|
||||||
this.checkbox.current.classList.add('error')
|
this.checkbox.current.classList.add('error')
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
editLabel() {//{{{
|
||||||
|
let label = prompt('Edit label', this.props.item.Label)
|
||||||
|
if (label === null)
|
||||||
|
return
|
||||||
|
|
||||||
|
label = label.trim()
|
||||||
|
if (label == '') {
|
||||||
|
alert(`A label can't be empty.`)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.label.current.classList.remove('error')
|
||||||
|
this.props.item.updateLabel(label, () => {
|
||||||
|
this.props.item.Label = label
|
||||||
|
this.label.current.innerHTML = label
|
||||||
|
this.label.current.classList.add('ok')
|
||||||
|
setTimeout(() => this.label.current.classList.remove('ok'), 500)
|
||||||
|
}, () => {
|
||||||
|
this.label.current.classList.add('error')
|
||||||
|
})
|
||||||
|
|
||||||
|
}//}}}
|
||||||
|
delete() {//{{{
|
||||||
|
if (this.props.ui.state.confirmDeletion) {
|
||||||
|
if (!confirm(`Delete '${this.props.item.Label}'?`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.item.delete(() => {
|
||||||
|
this.props.group.forceUpdate()
|
||||||
|
}, err => {
|
||||||
|
console.log(err)
|
||||||
|
console.log('error')
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
|
|
||||||
|
setDragTarget(state) {//{{{
|
||||||
|
this.setState({ dragTarget: state })
|
||||||
|
}//}}}
|
||||||
|
dragStart() {//{{{
|
||||||
|
// Shouldn't be needed, but in case the previous drag was bungled up, we reset.
|
||||||
|
this.props.ui.dragReset()
|
||||||
|
this.props.ui.dragItemSource = this
|
||||||
|
}//}}}
|
||||||
|
dragEnter(evt) {//{{{
|
||||||
|
evt.preventDefault()
|
||||||
|
this.props.ui.dragTarget(this)
|
||||||
|
}//}}}
|
||||||
|
dragEnd() {//{{{
|
||||||
|
console.log(
|
||||||
|
this.props.ui.dragItemSource.props.item.Label,
|
||||||
|
this.props.ui.dragItemTarget.props.item.Label,
|
||||||
|
)
|
||||||
|
this.props.ui.dragReset()
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vim: foldmethod=marker
|
||||||
|
@ -146,6 +146,10 @@ export class NodeUI extends Component {
|
|||||||
return
|
return
|
||||||
|
|
||||||
switch (evt.key.toUpperCase()) {
|
switch (evt.key.toUpperCase()) {
|
||||||
|
case 'C':
|
||||||
|
this.showPage('node')
|
||||||
|
break
|
||||||
|
|
||||||
case 'E':
|
case 'E':
|
||||||
this.showPage('keys')
|
this.showPage('keys')
|
||||||
break
|
break
|
||||||
|
@ -379,6 +379,12 @@ header {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
pre > code {
|
pre > code {
|
||||||
background: unset;
|
background: unset;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -398,31 +404,83 @@ header {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: 0 16px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header + .checklist-group.edit {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.checklist-group {
|
.checklist-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, min-content);
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: 0 8px;
|
||||||
|
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
margin-bottom: 8px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.ok {
|
||||||
|
color: #54b356;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label.error {
|
||||||
|
color: #d13636;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.edit {
|
||||||
|
margin-top: 32px;
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.edit) {
|
||||||
|
.reorder { display: none; }
|
||||||
|
img { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-item {
|
.checklist-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
grid-gap: 0 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 0.50em;
|
padding: 4px 0;
|
||||||
|
border-bottom: 2px solid #fff;
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
color: #888;
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.drag-target {
|
||||||
|
border-bottom: 2px solid #71c837;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
margin-left: 0px;
|
|
||||||
margin-right: 8px;
|
|
||||||
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin: 0 8px 0 0;
|
margin: 0 2px 0 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
@ -435,6 +493,14 @@ header {
|
|||||||
place-content: center;
|
place-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.ok {
|
||||||
|
color: #54b356;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.error {
|
||||||
|
color: #d13636;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="checkbox"].ok {
|
input[type="checkbox"].ok {
|
||||||
border: 0.15em solid #54b356;
|
border: 0.15em solid #54b356;
|
||||||
}
|
}
|
||||||
@ -464,8 +530,30 @@ header {
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.edit {
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.edit) {
|
||||||
|
.reorder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user