Added checklists to database, rendering and toggling items
This commit is contained in:
parent
5c27f9ed1c
commit
f98a6ab863
24
main.go
24
main.go
@ -104,6 +104,7 @@ 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_item/state", true, true, nodeChecklistItemState)
|
||||||
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)
|
||||||
@ -477,6 +478,29 @@ func nodeSearch(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{
|
|||||||
"Nodes": nodes,
|
"Nodes": nodes,
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
logger.Info("webserver", "request", "/node/checklist_item/state")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
ChecklistItemID int
|
||||||
|
State bool
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ChecklistItemState(sess.UserID, req.ChecklistItemID, req.State)
|
||||||
|
if err != nil {
|
||||||
|
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")
|
||||||
|
111
node.go
111
node.go
@ -8,6 +8,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ChecklistItem struct {
|
||||||
|
ID int
|
||||||
|
GroupID int `db:"checklist_group_id"`
|
||||||
|
Order int
|
||||||
|
Label string
|
||||||
|
Checked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChecklistGroup struct {
|
||||||
|
ID int
|
||||||
|
NodeID int `db:"node_id"`
|
||||||
|
Order int
|
||||||
|
Label string
|
||||||
|
Items []ChecklistItem
|
||||||
|
}
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
ID int
|
ID int
|
||||||
UserID int `db:"user_id"`
|
UserID int `db:"user_id"`
|
||||||
@ -22,6 +38,8 @@ type Node struct {
|
|||||||
Complete bool
|
Complete bool
|
||||||
Level int
|
Level int
|
||||||
|
|
||||||
|
ChecklistGroups []ChecklistGroup
|
||||||
|
|
||||||
ContentEncrypted string `db:"content_encrypted" json:"-"`
|
ContentEncrypted string `db:"content_encrypted" json:"-"`
|
||||||
Markdown bool
|
Markdown bool
|
||||||
}
|
}
|
||||||
@ -211,6 +229,8 @@ func RetrieveNode(userID, nodeID int) (node Node, err error) { // {{{
|
|||||||
} else {
|
} else {
|
||||||
node.Content = row.Content
|
node.Content = row.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node.retrieveChecklist()
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.Level == 1 {
|
if row.Level == 1 {
|
||||||
@ -421,5 +441,96 @@ func SearchNodes(userID int, search string) (nodes []Node, err error) { // {{{
|
|||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func ChecklistItemState(userID, checklistItemID int, state bool) (err error) {// {{{
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`
|
||||||
|
UPDATE checklist_item i
|
||||||
|
SET checked = $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,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
func (node *Node) retrieveChecklist() (err error) { // {{{
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
rows, err = service.Db.Conn.Queryx(`
|
||||||
|
SELECT
|
||||||
|
g.id AS group_id,
|
||||||
|
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
|
||||||
|
|
||||||
|
FROM public.checklist_group g
|
||||||
|
LEFT JOIN public.checklist_item i ON i.checklist_group_id = g.id
|
||||||
|
WHERE
|
||||||
|
g.node_id = $1
|
||||||
|
ORDER BY
|
||||||
|
g.order DESC,
|
||||||
|
i.order DESC
|
||||||
|
`, node.ID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
groups := make(map[int]*ChecklistGroup)
|
||||||
|
var found bool
|
||||||
|
var group *ChecklistGroup
|
||||||
|
var item ChecklistItem
|
||||||
|
for rows.Next() {
|
||||||
|
row := struct {
|
||||||
|
GroupID int `db:"group_id"`
|
||||||
|
GroupOrder int `db:"group_order"`
|
||||||
|
GroupLabel string `db:"group_label"`
|
||||||
|
|
||||||
|
ItemID int `db:"item_id"`
|
||||||
|
ItemOrder int `db:"item_order"`
|
||||||
|
ItemLabel string `db:"item_label"`
|
||||||
|
Checked bool
|
||||||
|
}{}
|
||||||
|
err = rows.StructScan(&row)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if group, found = groups[row.GroupID]; !found {
|
||||||
|
group = new(ChecklistGroup)
|
||||||
|
group.ID = row.GroupID
|
||||||
|
group.NodeID = node.ID
|
||||||
|
group.Order = row.GroupOrder
|
||||||
|
group.Label = row.GroupLabel
|
||||||
|
group.Items = []ChecklistItem{}
|
||||||
|
groups[group.ID] = group
|
||||||
|
}
|
||||||
|
|
||||||
|
item = ChecklistItem{}
|
||||||
|
item.ID = row.ItemID
|
||||||
|
item.GroupID = row.GroupID
|
||||||
|
item.Order = row.ItemOrder
|
||||||
|
item.Label = row.ItemLabel
|
||||||
|
item.Checked = row.Checked
|
||||||
|
group.Items = append(group.Items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.ChecklistGroups = []ChecklistGroup{}
|
||||||
|
for _, group := range groups {
|
||||||
|
node.ChecklistGroups = append(node.ChecklistGroups, *group)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
18
sql/00014.sql
Normal file
18
sql/00014.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE checklist_group (
|
||||||
|
id serial NOT NULL,
|
||||||
|
node_id int4 NOT NULL,
|
||||||
|
"order" int NOT NULL DEFAULT 0,
|
||||||
|
label varchar NOT NULL,
|
||||||
|
CONSTRAINT checklist_group_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT checklist_group_node_fk FOREIGN KEY (node_id) REFERENCES public."node"(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE checklist_item (
|
||||||
|
id serial NOT NULL,
|
||||||
|
checklist_group_id int4 NOT NULL,
|
||||||
|
"order" int NOT NULL DEFAULT 0,
|
||||||
|
label varchar NOT NULL,
|
||||||
|
checked bool NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT checklist_item_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT checklist_group_item_fk FOREIGN KEY (checklist_group_id) REFERENCES public."checklist_group"(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
)
|
@ -26,12 +26,12 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0px;
|
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
color: #518048;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 32px;
|
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
|
color: #518048;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@ -307,10 +307,15 @@ header .menu {
|
|||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
#markdown {
|
#markdown {
|
||||||
padding: 16px;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
|
justify-self: center;
|
||||||
|
width: calc(100% - 32px);
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
#markdown table {
|
#markdown table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@ -330,6 +335,68 @@ header .menu {
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
#checklist {
|
||||||
|
grid-area: checklist;
|
||||||
|
color: #333;
|
||||||
|
justify-self: center;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
#checklist .checklist-group {
|
||||||
|
margin-top: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item.checked {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
#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;
|
||||||
|
font: inherit;
|
||||||
|
color: currentColor;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
border: 0.15em solid currentColor;
|
||||||
|
border-radius: 0.15em;
|
||||||
|
transform: translateY(-0.075em);
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item input[type="checkbox"].ok {
|
||||||
|
border: 0.15em solid #54b356;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item input[type="checkbox"].ok::before {
|
||||||
|
box-shadow: inset 1em 1em #54b356;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item input[type="checkbox"]::before {
|
||||||
|
content: "";
|
||||||
|
width: 0.7em;
|
||||||
|
height: 0.7em;
|
||||||
|
transform: scale(0);
|
||||||
|
transition: 120ms transform ease-in-out;
|
||||||
|
box-shadow: inset 1em 1em #666;
|
||||||
|
}
|
||||||
|
#checklist .checklist-item input[type="checkbox"]:checked::before {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
#checklist .checklist-item label {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
/* ============================================================= *
|
/* ============================================================= *
|
||||||
* Textarea replicates the height of an element expanding height *
|
* Textarea replicates the height of an element expanding height *
|
||||||
* ============================================================= */
|
* ============================================================= */
|
||||||
@ -476,9 +543,9 @@ header .menu {
|
|||||||
}
|
}
|
||||||
.layout-tree {
|
.layout-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree checklist" "tree files" "tree blank";
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: min-content 1fr;
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr;
|
||||||
/* blank */
|
/* blank */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
@ -511,14 +578,17 @@ header .menu {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.layout-crumbs {
|
.layout-crumbs {
|
||||||
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "files" "blank";
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr;
|
||||||
/* blank */
|
/* blank */
|
||||||
}
|
}
|
||||||
.layout-crumbs #tree {
|
.layout-crumbs #tree {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.layout-crumbs #checklist {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
.layout-keys {
|
.layout-keys {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header" "keys";
|
grid-template-areas: "header" "keys";
|
||||||
@ -557,22 +627,25 @@ header .menu {
|
|||||||
}
|
}
|
||||||
#app.node {
|
#app.node {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank";
|
grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree checklist" "tree files" "tree blank";
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: min-content 1fr;
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr;
|
||||||
/* blank */
|
/* blank */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
#app.node.toggle-tree {
|
#app.node.toggle-tree {
|
||||||
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "files" "blank";
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr;
|
||||||
/* blank */
|
/* blank */
|
||||||
}
|
}
|
||||||
#app.node.toggle-tree #tree {
|
#app.node.toggle-tree #tree {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#app.node.toggle-tree #checklist {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
#profile-settings {
|
#profile-settings {
|
||||||
color: #333;
|
color: #333;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@ -588,14 +661,17 @@ header .menu {
|
|||||||
}
|
}
|
||||||
@media only screen and (max-width: 932px) {
|
@media only screen and (max-width: 932px) {
|
||||||
#app.node {
|
#app.node {
|
||||||
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank";
|
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "files" "blank";
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr;
|
grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr;
|
||||||
/* blank */
|
/* blank */
|
||||||
}
|
}
|
||||||
#app.node #tree {
|
#app.node #tree {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#app.node #checklist {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
#app.node.toggle-tree {
|
#app.node.toggle-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "header" "tree";
|
grid-template-areas: "header" "tree";
|
||||||
@ -628,7 +704,9 @@ header .menu {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
#file-section {
|
#file-section,
|
||||||
|
#checklist,
|
||||||
|
#markdown {
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"node": "/js/{{ .VERSION }}/node.mjs",
|
"node": "/js/{{ .VERSION }}/node.mjs",
|
||||||
"key": "/js/{{ .VERSION }}/key.mjs",
|
"key": "/js/{{ .VERSION }}/key.mjs",
|
||||||
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
||||||
|
"checklist": "/js/{{ .VERSION }}/checklist.mjs",
|
||||||
"ws": "/_js/{{ .VERSION }}/websocket.mjs"
|
"ws": "/_js/{{ .VERSION }}/websocket.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
94
static/js/checklist.mjs
Normal file
94
static/js/checklist.mjs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { h, Component, createRef } from 'preact'
|
||||||
|
import htm from 'htm'
|
||||||
|
import { signal } from 'preact/signals'
|
||||||
|
const html = htm.bind(h)
|
||||||
|
|
||||||
|
export class ChecklistGroup {
|
||||||
|
static sort(a, b) {//{{{
|
||||||
|
if (a.Order < b.Order) return -1
|
||||||
|
if (a.Order > b.Order) return 1
|
||||||
|
return 0
|
||||||
|
}//}}}
|
||||||
|
constructor(data) {//{{{
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
if (key == 'Items')
|
||||||
|
this.items = data[key].map(itemData =>
|
||||||
|
new ChecklistItem(itemData)
|
||||||
|
).sort(ChecklistItem.sort)
|
||||||
|
else
|
||||||
|
this[key] = data[key]
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChecklistItem {
|
||||||
|
static sort(a, b) {//{{{
|
||||||
|
if (a.Order < b.Order) return -1
|
||||||
|
if (a.Order > b.Order) return 1
|
||||||
|
return 0
|
||||||
|
}//}}}
|
||||||
|
constructor(data) {//{{{
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
this[key] = data[key]
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Checklist extends Component {
|
||||||
|
render({ groups }) {//{{{
|
||||||
|
if (groups.length == 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
groups.sort(ChecklistGroup.sort)
|
||||||
|
|
||||||
|
let groupElements = groups.map(group => html`<${ChecklistGroupElement} group=${group} />`)
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div id="checklist">
|
||||||
|
<h1>Checklist</h1>
|
||||||
|
${groupElements}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChecklistGroupElement extends Component {
|
||||||
|
render({ group }) {//{{{
|
||||||
|
let items = group.items.map(item => html`<${ChecklistItemElement} item=${item} />`)
|
||||||
|
return html`
|
||||||
|
<div class="checklist-group">${group.Label}</div>
|
||||||
|
${items}
|
||||||
|
`
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChecklistItemElement extends Component {
|
||||||
|
constructor(props) {//{{{
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
checked: props.item.Checked,
|
||||||
|
}
|
||||||
|
this.checkbox = createRef()
|
||||||
|
}//}}}
|
||||||
|
render({ item }, { checked }) {//{{{
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}//}}}
|
||||||
|
update(checked) {//{{{
|
||||||
|
this.setState({ checked })
|
||||||
|
window._app.current.request('/node/checklist_item/state', {
|
||||||
|
ChecklistItemID: this.props.item.ID,
|
||||||
|
State: checked,
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.checkbox.current.classList.add('ok')
|
||||||
|
setTimeout(()=>this.checkbox.current.classList.remove('ok'), 500)
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(window._app.current.responseError)
|
||||||
|
}//}}}
|
||||||
|
}
|
@ -3,7 +3,7 @@ import htm from 'htm'
|
|||||||
import { signal } from 'preact/signals'
|
import { signal } from 'preact/signals'
|
||||||
import { Keys, Key } from 'key'
|
import { Keys, Key } from 'key'
|
||||||
import Crypto from 'crypto'
|
import Crypto from 'crypto'
|
||||||
//import { marked } from 'marked'
|
import { Checklist, ChecklistGroup } from 'checklist'
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
|
|
||||||
export class NodeUI extends Component {
|
export class NodeUI extends Component {
|
||||||
@ -65,12 +65,14 @@ export class NodeUI extends Component {
|
|||||||
let padlock = ''
|
let padlock = ''
|
||||||
if (node.CryptoKeyID > 0)
|
if (node.CryptoKeyID > 0)
|
||||||
padlock = html`<img src="/images/${window._VERSION}/padlock-black.svg" style="height: 24px;" />`
|
padlock = html`<img src="/images/${window._VERSION}/padlock-black.svg" style="height: 24px;" />`
|
||||||
|
|
||||||
page = html`
|
page = html`
|
||||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||||
<div class="node-name">
|
<div class="node-name">
|
||||||
${node.Name} ${padlock}
|
${node.Name} ${padlock}
|
||||||
</div>
|
</div>
|
||||||
<${NodeContent} key=${node.ID} node=${node} ref=${this.nodeContent} />
|
<${NodeContent} key=${node.ID} node=${node} ref=${this.nodeContent} />
|
||||||
|
<${Checklist} groups=${node.ChecklistGroups} />
|
||||||
<${NodeFiles} node=${this.node.value} />
|
<${NodeFiles} node=${this.node.value} />
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@ -342,16 +344,6 @@ class NodeContent extends Component {
|
|||||||
let textarea = document.getElementById('node-content')
|
let textarea = document.getElementById('node-content')
|
||||||
if (textarea)
|
if (textarea)
|
||||||
textarea.parentNode.dataset.replicatedValue = textarea.value
|
textarea.parentNode.dataset.replicatedValue = textarea.value
|
||||||
|
|
||||||
let crumbsEl = document.getElementById('crumbs')
|
|
||||||
let markdown = document.getElementById('markdown')
|
|
||||||
if (markdown) {
|
|
||||||
let margins = (crumbsEl.clientWidth - 900) / 2.0
|
|
||||||
if (margins < 0)
|
|
||||||
margins = 0
|
|
||||||
markdown.style.marginLeft = `${margins}px`
|
|
||||||
markdown.style.marginRight = `${margins}px`
|
|
||||||
}
|
|
||||||
}//}}}
|
}//}}}
|
||||||
unlock() {//{{{
|
unlock() {//{{{
|
||||||
let pass = prompt(`Password for "${this.props.model.description}"`)
|
let pass = prompt(`Password for "${this.props.model.description}"`)
|
||||||
@ -368,9 +360,9 @@ class NodeContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MarkdownContent extends Component {
|
class MarkdownContent extends Component {
|
||||||
render({ content }) {
|
render({ content }) {//{{{
|
||||||
return html`<div id="markdown"></div>`
|
return html`<div id="markdown"></div>`
|
||||||
}
|
}//}}}
|
||||||
componentDidMount() {//{{{
|
componentDidMount() {//{{{
|
||||||
const markdown = document.getElementById('markdown')
|
const markdown = document.getElementById('markdown')
|
||||||
if (markdown)
|
if (markdown)
|
||||||
@ -430,6 +422,7 @@ export class Node {
|
|||||||
this.Files = []
|
this.Files = []
|
||||||
this._decrypted = false
|
this._decrypted = false
|
||||||
this._expanded = false // start value for the TreeNode component,
|
this._expanded = false // start value for the TreeNode component,
|
||||||
|
this.ChecklistGroups = {}
|
||||||
// it doesn't control it afterwards.
|
// it doesn't control it afterwards.
|
||||||
// Used to expand the crumbs upon site loading.
|
// Used to expand the crumbs upon site loading.
|
||||||
}//}}}
|
}//}}}
|
||||||
@ -446,6 +439,7 @@ export class Node {
|
|||||||
this.Files = res.Node.Files
|
this.Files = res.Node.Files
|
||||||
this.Markdown = res.Node.Markdown
|
this.Markdown = res.Node.Markdown
|
||||||
this.RenderMarkdown.value = this.Markdown
|
this.RenderMarkdown.value = this.Markdown
|
||||||
|
this.initChecklist(res.Node.ChecklistGroups)
|
||||||
callback(this)
|
callback(this)
|
||||||
})
|
})
|
||||||
.catch(this.app.responseError)
|
.catch(this.app.responseError)
|
||||||
@ -605,6 +599,13 @@ export class Node {
|
|||||||
this._decrypted = false
|
this._decrypted = false
|
||||||
return this._content
|
return this._content
|
||||||
}//}}}
|
}//}}}
|
||||||
|
initChecklist(checklistData) {//{{{
|
||||||
|
if (checklistData === undefined || checklistData === null)
|
||||||
|
return
|
||||||
|
this.ChecklistGroups = checklistData.map(groupData=>{
|
||||||
|
return new ChecklistGroup(groupData)
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Menu extends Component {
|
class Menu extends Component {
|
||||||
|
@ -23,13 +23,13 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0px;
|
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
color: @header_1;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-top: 32px;
|
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
|
color: @header_1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -352,10 +352,17 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#markdown {
|
#markdown {
|
||||||
padding: 16px;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
|
|
||||||
|
justify-self: center;
|
||||||
|
width: calc(100% - 32px);
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@ -379,6 +386,83 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#checklist {
|
||||||
|
grid-area: checklist;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
justify-self: center;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
|
||||||
|
.checklist-group {
|
||||||
|
margin-top: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.50em;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0 8px 0 0;
|
||||||
|
font: inherit;
|
||||||
|
color: currentColor;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
border: 0.15em solid currentColor;
|
||||||
|
border-radius: 0.15em;
|
||||||
|
transform: translateY(-0.075em);
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"].ok {
|
||||||
|
border: 0.15em solid #54b356;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"].ok::before {
|
||||||
|
box-shadow: inset 1em 1em #54b356;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input[type="checkbox"]::before {
|
||||||
|
content: "";
|
||||||
|
width: 0.70em;
|
||||||
|
height: 0.70em;
|
||||||
|
transform: scale(0);
|
||||||
|
transition: 120ms transform ease-in-out;
|
||||||
|
box-shadow: inset 1em 1em @checkbox_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"]:checked::before {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================= *
|
/* ============================================================= *
|
||||||
* Textarea replicates the height of an element expanding height *
|
* Textarea replicates the height of an element expanding height *
|
||||||
* ============================================================= */
|
* ============================================================= */
|
||||||
@ -555,6 +639,7 @@ header {
|
|||||||
"tree child-nodes"
|
"tree child-nodes"
|
||||||
"tree name"
|
"tree name"
|
||||||
"tree content"
|
"tree content"
|
||||||
|
"tree checklist"
|
||||||
"tree files"
|
"tree files"
|
||||||
"tree blank"
|
"tree blank"
|
||||||
;
|
;
|
||||||
@ -565,6 +650,7 @@ header {
|
|||||||
min-content /* child-nodes */
|
min-content /* child-nodes */
|
||||||
min-content /* name */
|
min-content /* name */
|
||||||
min-content /* content */
|
min-content /* content */
|
||||||
|
min-content /* checklist */
|
||||||
min-content /* files */
|
min-content /* files */
|
||||||
1fr; /* blank */
|
1fr; /* blank */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -597,6 +683,7 @@ header {
|
|||||||
"child-nodes"
|
"child-nodes"
|
||||||
"name"
|
"name"
|
||||||
"content"
|
"content"
|
||||||
|
"checklist"
|
||||||
"files"
|
"files"
|
||||||
"blank"
|
"blank"
|
||||||
;
|
;
|
||||||
@ -607,9 +694,14 @@ header {
|
|||||||
min-content /* child-nodes */
|
min-content /* child-nodes */
|
||||||
min-content /* name */
|
min-content /* name */
|
||||||
min-content /* content */
|
min-content /* content */
|
||||||
|
min-content /* checklist */
|
||||||
min-content /* files */
|
min-content /* files */
|
||||||
1fr; /* blank */
|
1fr; /* blank */
|
||||||
#tree { display: none }
|
#tree { display: none }
|
||||||
|
|
||||||
|
#checklist {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
}// }}}
|
}// }}}
|
||||||
.layout-keys {
|
.layout-keys {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -681,7 +773,7 @@ header {
|
|||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-section {
|
#file-section, #checklist, #markdown {
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
|
@ -4,6 +4,11 @@
|
|||||||
@accent_2: #ecbf00;
|
@accent_2: #ecbf00;
|
||||||
@accent_3: #c84a37;
|
@accent_3: #c84a37;
|
||||||
|
|
||||||
|
@header_1: #518048;
|
||||||
|
@header_2: #518048;
|
||||||
|
|
||||||
|
@checkbox_1: #666;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@theme_gradient: linear-gradient(to right, #009fff, #ec2f4b);
|
@theme_gradient: linear-gradient(to right, #009fff, #ec2f4b);
|
||||||
@theme_gradient: linear-gradient(to right, #f5af19, #f12711);
|
@theme_gradient: linear-gradient(to right, #f5af19, #f12711);
|
||||||
|
Loading…
Reference in New Issue
Block a user