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/download", true, true, nodeDownload)
|
||||
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/label", true, true, nodeChecklistItemLabel)
|
||||
service.Register("/node/checklist_item/delete", true, true, nodeChecklistItemDelete)
|
||||
service.Register("/key/retrieve", true, true, keyRetrieve)
|
||||
service.Register("/key/create", true, true, keyCreate)
|
||||
service.Register("/key/counter", true, true, keyCounter)
|
||||
@ -478,8 +484,102 @@ func nodeSearch(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{
|
||||
"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) { // {{{
|
||||
logger.Info("webserver", "request", "/node/checklist_item/state")
|
||||
var err error
|
||||
|
||||
req := struct {
|
||||
@ -501,6 +601,50 @@ func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *sessio
|
||||
"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) { // {{{
|
||||
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
|
||||
} // }}}
|
||||
|
||||
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) { // {{{
|
||||
_, err = service.Db.Conn.Exec(
|
||||
`
|
||||
@ -459,6 +563,43 @@ func ChecklistItemState(userID, checklistItemID int, state bool) (err error) {//
|
||||
)
|
||||
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) { // {{{
|
||||
var rows *sqlx.Rows
|
||||
@ -468,10 +609,10 @@ func (node *Node) retrieveChecklist() (err error) { // {{{
|
||||
g.order AS group_order,
|
||||
g.label AS group_label,
|
||||
|
||||
i.id AS item_id,
|
||||
i.order AS item_order,
|
||||
i.label AS item_label,
|
||||
i.checked
|
||||
COALESCE(i.id, 0) AS item_id,
|
||||
COALESCE(i.order, 0) AS item_order,
|
||||
COALESCE(i.label, '') AS item_label,
|
||||
COALESCE(i.checked, false) AS checked
|
||||
|
||||
FROM public.checklist_group g
|
||||
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.Label = row.ItemLabel
|
||||
item.Checked = row.Checked
|
||||
|
||||
if item.ID > 0 {
|
||||
group.Items = append(group.Items, item)
|
||||
}
|
||||
}
|
||||
|
||||
node.ChecklistGroups = []ChecklistGroup{}
|
||||
for _, group := range groups {
|
||||
|
@ -330,6 +330,11 @@ header .menu {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#markdown pre {
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
#markdown pre > code {
|
||||
background: unset;
|
||||
padding: 0px;
|
||||
@ -346,27 +351,72 @@ header .menu {
|
||||
margin-top: 8px;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, min-content);
|
||||
align-items: center;
|
||||
grid-gap: 0 8px;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 8px;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-columns: repeat(3, min-content);
|
||||
grid-gap: 0 8px;
|
||||
align-items: center;
|
||||
margin-top: 0.5em;
|
||||
padding: 4px 0;
|
||||
border-bottom: 2px solid #fff;
|
||||
}
|
||||
#checklist .checklist-item.checked {
|
||||
text-decoration: line-through;
|
||||
color: #888;
|
||||
color: #aaa;
|
||||
}
|
||||
#checklist .checklist-item.drag-target {
|
||||
border-bottom: 2px solid #71c837;
|
||||
}
|
||||
#checklist .checklist-item input[type="checkbox"] {
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
margin: 0 8px 0 0;
|
||||
margin: 0 2px 0 0;
|
||||
font: inherit;
|
||||
color: currentColor;
|
||||
width: 1.25em;
|
||||
@ -377,6 +427,12 @@ header .menu {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
#checklist .checklist-item label.ok {
|
||||
color: #54b356;
|
||||
}
|
||||
#checklist .checklist-item label.error {
|
||||
color: #d13636;
|
||||
}
|
||||
#checklist .checklist-item input[type="checkbox"].ok {
|
||||
border: 0.15em solid #54b356;
|
||||
}
|
||||
@ -400,8 +456,22 @@ header .menu {
|
||||
#checklist .checklist-item input[type="checkbox"]:checked::before {
|
||||
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 {
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* ============================================================= *
|
||||
* 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 { signal } from 'preact/signals'
|
||||
|
||||
import { h, Component, render, createRef } from 'preact'
|
||||
import htm from 'htm'
|
||||
import { Session } from 'session'
|
||||
import { Node, NodeUI } from 'node'
|
||||
import { Websocket } from 'ws'
|
||||
import { signal } from 'preact/signals'
|
||||
const html = htm.bind(h)
|
||||
|
||||
class App extends Component {
|
||||
|
@ -12,13 +12,47 @@ export class ChecklistGroup {
|
||||
constructor(data) {//{{{
|
||||
Object.keys(data).forEach(key => {
|
||||
if (key == 'Items')
|
||||
this.items = data[key].map(itemData =>
|
||||
new ChecklistItem(itemData)
|
||||
this.items = data[key].map(itemData => {
|
||||
let item = new ChecklistItem(itemData)
|
||||
item.checklistGroup = this
|
||||
return item
|
||||
}
|
||||
).sort(ChecklistItem.sort)
|
||||
else
|
||||
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 {
|
||||
@ -32,34 +66,193 @@ export class ChecklistItem {
|
||||
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 {
|
||||
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)
|
||||
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`
|
||||
<div id="checklist">
|
||||
<div class="header">
|
||||
<h1>Checklist</h1>
|
||||
<img src="/images/${_VERSION}/${edit}" onclick=${() => this.toggleEdit()} />
|
||||
<${addGroup} />
|
||||
</div>
|
||||
${confirmDeletionEl}
|
||||
${groupElements}
|
||||
</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 {
|
||||
render({ group }) {//{{{
|
||||
let items = group.items.map(item => html`<${ChecklistItemElement} item=${item} />`)
|
||||
constructor() {//{{{
|
||||
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`
|
||||
<div class="checklist-group">${group.Label}</div>
|
||||
${items}
|
||||
<div class="checklist-group-container">
|
||||
<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 {
|
||||
@ -67,31 +260,100 @@ class ChecklistItemElement extends Component {
|
||||
super(props)
|
||||
this.state = {
|
||||
checked: props.item.Checked,
|
||||
dragTarget: false,
|
||||
}
|
||||
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`
|
||||
<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)} />
|
||||
<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>
|
||||
`
|
||||
}//}}}
|
||||
componentDidMount() {//{{{
|
||||
this.base.addEventListener('dragstart', ()=>this.dragStart())
|
||||
this.base.addEventListener('dragend', ()=>this.dragEnd())
|
||||
this.base.addEventListener('dragenter', evt=>this.dragEnter(evt))
|
||||
}//}}}
|
||||
|
||||
update(checked) {//{{{
|
||||
this.setState({ checked })
|
||||
this.checkbox.current.classList.remove('error')
|
||||
window._app.current.request('/node/checklist_item/state', {
|
||||
ChecklistItemID: this.props.item.ID,
|
||||
State: checked,
|
||||
})
|
||||
.then(res => {
|
||||
this.props.item.updateState(checked, () => {
|
||||
this.checkbox.current.classList.add('ok')
|
||||
setTimeout(() => this.checkbox.current.classList.remove('ok'), 500)
|
||||
|
||||
})
|
||||
.catch(()=>{
|
||||
}, () => {
|
||||
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
|
||||
|
||||
switch (evt.key.toUpperCase()) {
|
||||
case 'C':
|
||||
this.showPage('node')
|
||||
break
|
||||
|
||||
case 'E':
|
||||
this.showPage('keys')
|
||||
break
|
||||
|
@ -379,6 +379,12 @@ header {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
background: unset;
|
||||
padding: 0px;
|
||||
@ -398,31 +404,83 @@ header {
|
||||
margin-top: 8px;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, min-content);
|
||||
align-items: center;
|
||||
grid-gap: 0 8px;
|
||||
|
||||
margin-top: 1em;
|
||||
margin-bottom: 8px;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-template-columns: repeat(3, min-content);
|
||||
grid-gap: 0 8px;
|
||||
align-items: center;
|
||||
margin-top: 0.50em;
|
||||
padding: 4px 0;
|
||||
border-bottom: 2px solid #fff;
|
||||
|
||||
&.checked {
|
||||
text-decoration: line-through;
|
||||
color: #888;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
&.drag-target {
|
||||
border-bottom: 2px solid #71c837;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
margin: 0 8px 0 0;
|
||||
margin: 0 2px 0 0;
|
||||
font: inherit;
|
||||
color: currentColor;
|
||||
width: 1.25em;
|
||||
@ -435,6 +493,14 @@ header {
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
label.ok {
|
||||
color: #54b356;
|
||||
}
|
||||
|
||||
label.error {
|
||||
color: #d13636;
|
||||
}
|
||||
|
||||
input[type="checkbox"].ok {
|
||||
border: 0.15em solid #54b356;
|
||||
}
|
||||
@ -464,8 +530,30 @@ header {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&.edit {
|
||||
input[type="checkbox"] {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.edit) {
|
||||
.reorder {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
label {
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user