diff --git a/datagraph b/datagraph index 8ac4768..659ce64 100755 Binary files a/datagraph and b/datagraph differ diff --git a/node.go b/node.go index 159fcb6..143a03e 100644 --- a/node.go +++ b/node.go @@ -4,9 +4,11 @@ import ( // External werr "git.gibonuddevalla.se/go/wrappederror" "github.com/jmoiron/sqlx" + "github.com/lib/pq" // Standard "encoding/json" + "errors" "time" ) @@ -30,10 +32,11 @@ type Node struct { Children []*Node } -func GetNode(nodeID int) (node Node, err error) { +func GetNode(nodeID int) (node Node, err error) { // {{{ row := db.QueryRowx(` SELECT n.id, + COALESCE(n.parent_id, -1) AS parent_id, n.name, n.updated, n.data AS data_raw, @@ -66,9 +69,9 @@ func GetNode(nodeID int) (node Node, err error) { return } return -} +} // }}} -func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) {// {{{ +func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) { // {{{ nodes := make(map[int]*Node) var rows *sqlx.Rows rows, err = GetNodeRows(startNodeID, maxDepth) @@ -96,47 +99,42 @@ func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) {// {{{ } return -}// }}} -func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) {// {{{ +} // }}} +func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) { // {{{ rows, err = db.Queryx(` WITH RECURSIVE nodes AS ( SELECT - COALESCE( - (SELECT parent FROM connection WHERE child = $1), - 0 - ) AS parent_id, $1::int AS id, 0 AS depth - UNION SELECT - c.parent, - c.child, + n.id, ns.depth+1 AS depth - FROM connection c - INNER JOIN nodes ns ON ns.depth < $2 AND c.parent = ns.id + FROM node n + INNER JOIN nodes ns ON ns.depth < $2 AND n.parent_id = ns.id ) SEARCH DEPTH FIRST BY id SET ordercol SELECT - ns.parent_id, + COALESCE(n.parent_id, -1) AS parent_id, n.id, - n.name, + CONCAT(REPEAT(' ', ns.depth), n.name) AS name, n.type_id, t.name AS type_name, COALESCE(t.schema->>'icon', '') AS type_icon, n.updated, n.data AS data_raw, - COUNT(c.child) AS num_children + COUNT(node_children.id) AS num_children FROM nodes ns INNER JOIN public.node n ON ns.id = n.id INNER JOIN public.type t ON n.type_id = t.id - LEFT JOIN public.connection c ON c.parent = n.id + LEFT JOIN node node_children ON node_children.parent_id = n.id + GROUP BY ns.depth, - ns.parent_id, + n.parent_id, n.id, t.name, t.schema, @@ -152,8 +150,8 @@ func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) {// {{{ } return -}// }}} -func ComposeTree(nodes map[int]*Node, node *Node) {// {{{ +} // }}} +func ComposeTree(nodes map[int]*Node, node *Node) { // {{{ if node.Children == nil { node.Children = []*Node{} } @@ -168,18 +166,17 @@ func ComposeTree(nodes map[int]*Node, node *Node) {// {{{ } nodes[node.ID] = node -}// }}} +} // }}} -func UpdateNode(nodeID int, data []byte) (err error) {// {{{ +func UpdateNode(nodeID int, data []byte) (err error) { // {{{ _, err = db.Exec(`UPDATE public.node SET data=$2 WHERE id=$1`, nodeID, data) return -}// }}} -func RenameNode(nodeID int, name string) (err error) {// {{{ +} // }}} +func RenameNode(nodeID int, name string) (err error) { // {{{ _, err = db.Exec(`UPDATE node SET name=$2 WHERE id=$1`, nodeID, name) return -}// }}} - -func CreateNode(parentNodeID, typeID int, name string) (err error) {// {{{ +} // }}} +func CreateNode(parentNodeID, typeID int, name string) (nodeID int, err error) { // {{{ j, _ := json.Marshal( struct { New bool `json:"x-new"` @@ -188,22 +185,28 @@ func CreateNode(parentNodeID, typeID int, name string) (err error) {// {{{ }) row := db.QueryRow(` - INSERT INTO node(type_id, name, data) - VALUES($1, $2, $3::jsonb) + INSERT INTO node(parent_id, type_id, name, data) + VALUES($1, $2, $3, $4::jsonb) RETURNING id `, - typeID, name, j) - - var id int - err = row.Scan(&id) + parentNodeID, typeID, name, j) + err = row.Scan(&nodeID) if err != nil { err = werr.Wrap(err) - return } - _, err = db.Exec(`INSERT INTO connection("parent", "child") VALUES($1, $2)`, parentNodeID, id) - return -}// }}} +} // }}} +func DeleteNode(nodeID int) (err error) { // {{{ + _, err = db.Exec(`DELETE FROM node WHERE id=$1`, nodeID) + if err != nil { + pqErr, ok := err.(*pq.Error) + if ok && pqErr.Code == "23503" { + err = errors.New("Can't delete a node with children.") + return + } + } + return +} // }}} // vim: foldmethod=marker diff --git a/sql/0003.sql b/sql/0003.sql new file mode 100644 index 0000000..19809a9 --- /dev/null +++ b/sql/0003.sql @@ -0,0 +1,6 @@ +ALTER TABLE public.node ADD parent_node_id int4 DEFAULT 0 NULL; + +UPDATE node +SET parent_node_id = ( + SELECT parent FROM connection WHERE child = id +) WHERE id > 0; diff --git a/sql/0004.sql b/sql/0004.sql new file mode 100644 index 0000000..ba107ab --- /dev/null +++ b/sql/0004.sql @@ -0,0 +1 @@ +ALTER TABLE public.node RENAME COLUMN parent_node_id TO parent_id; diff --git a/static/css/main.css b/static/css/main.css index 696e03a..9bf8c42 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -73,7 +73,7 @@ body { } #editor-node > div.ops { display: grid; - grid-template-columns: min-content 1fr min-content; + grid-template-columns: min-content 1fr min-content min-content; align-items: center; grid-gap: 8px; } @@ -115,15 +115,13 @@ body { display: grid; grid-template-columns: min-content min-content 100%; white-space: nowrap; + user-select: none; } .node img { height: 24px; } .node.selected > .name { - color: #a02c2c; -} -.node.selected > .type-icon { - filter: invert(0.7) sepia(0.5) hue-rotate(0deg) saturate(750%) brightness(0.85) !important; + font-weight: bold; } .node.expanded > .children { display: block; diff --git a/static/js/app.mjs b/static/js/app.mjs index 63d5dad..2fe5c01 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -13,12 +13,14 @@ export class App { this.tree = new Tree(document.getElementById('nodes')) const events = [ - 'MENU_ITEM_SELECTED', - 'NODE_SELECTED', 'EDITOR_NODE_SAVE', - 'TYPES_LIST_FETCHED', + 'MENU_ITEM_SELECTED', 'NODE_CREATE_DIALOG', + 'NODE_DELETE', 'NODE_EDIT_NAME', + 'NODE_SELECTED', + 'TREE_RELOAD_NODE', + 'TYPES_LIST_FETCHED', ] for (const eventName of events) mbus.subscribe(eventName, event => this.eventHandler(event)) @@ -45,6 +47,12 @@ export class App { this.edit(event.detail) break + case 'NODE_DELETE': + if (!confirm('Are you sure you want to delete this node?')) + return + this.nodeDelete(this.currentNode.ID) + break + case 'EDITOR_NODE_SAVE': this.nodeUpdate() break @@ -65,7 +73,18 @@ export class App { const newName = prompt('Rename node', this.currentNode.Name) if (newName === null) return + this.nodeRename(this.currentNode.ID, newName) + .then(() => mbus.dispatch('TREE_RELOAD_NODE', { parentNodeID: this.currentNode.ParentID })) + break + + case 'TREE_RELOAD_NODE': + this.tree.updateNode(event.detail.parentNodeID) + .then(() => { + if (event.detail.callback) + event.detail.callback() + .catch(err => showError(err)) + }) break default: @@ -77,6 +96,10 @@ export class App { let handled = true switch (event.key.toUpperCase()) { + case 'D': + mbus.dispatch('NODE_DELETE') + break + case 'N': if (!event.shiftKey || !event.altKey) return @@ -145,30 +168,38 @@ export class App { // Name is separate from the JSON node. const name = document.getElementById('editor-node-name') name.innerText = json.Node.Name + + // The editor-node div is hidden from the start as a lot of the elements + // doesn't make any sense before a node is selected. + document.getElementById('editor-node').style.display = 'grid' }) }// }}} - nodeRename(nodeID, name) {// {{{ - name = name.trim() - if (name.length === 0) { - alert('A name must be provided.') - return - } + async nodeRename(nodeID, name) {// {{{ + return new Promise((resolve, reject) => { + name = name.trim() + if (name.length === 0) { + alert('A name must be provided.') + return + } - fetch(`/nodes/rename/${nodeID}`, { - method: 'POST', - body: JSON.stringify({ - Name: name, - }), - }) - .then(data => data.json()) - .then(json => { - if (!json.OK) { - showError(json.Error) - return - } - - this.edit(nodeID) + fetch(`/nodes/rename/${nodeID}`, { + method: 'POST', + body: JSON.stringify({ + Name: name, + }), }) + .then(data => data.json()) + .then(json => { + if (!json.OK) { + showError(json.Error) + return + } + + this.edit(nodeID) + resolve() + }) + .catch(err => reject(err)) + }) }// }}} nodeUpdate() {// {{{ if (this.editor === null) @@ -198,6 +229,17 @@ export class App { btn.disabled = false }) }// }}} + nodeDelete(nodeID) {// {{{ + fetch(`/nodes/delete/${nodeID}`) + .then(data => data.json()) + .then(json => { + if (!json.OK) { + showError(json.Error) + return + } + }) + .catch(err => showError(err)) + }// }}} } class NodeCreateDialog { @@ -226,10 +268,11 @@ class NodeCreateDialog {