diff --git a/node.go b/node.go index 199bb4d..d44b82b 100644 --- a/node.go +++ b/node.go @@ -211,7 +211,7 @@ func DeleteNode(nodeID int) (err error) { // {{{ } return } // }}} -func MoveNodes(newParentID int, nodeIDs []int) (err error) { +func MoveNodes(newParentID int, nodeIDs []int) (err error) { // {{{ // TODO - implement a method to verify that a node isn't moved underneath itself. // Preferably using a stored procedure? @@ -230,6 +230,50 @@ func MoveNodes(newParentID int, nodeIDs []int) (err error) { _, err = db.Exec(sql, newParentID) return -} +} // }}} +func GetNodeConnections(nodeID int) (connections []Node, err error) { // {{{ + connections = []Node{} + + var rows *sqlx.Rows + rows, err = db.Queryx(` + SELECT + n.*, + t.id AS type_id, + t.name AS type_name, + t.schema AS type_schema_raw, + t.schema->>'icon' AS type_icon + FROM public.connection c + INNER JOIN public.node n ON c.to = n.id + INNER JOIN public.type t ON n.type_id = t.id + WHERE + c.from = $1 + `, + nodeID, + ) + if err != nil { + err = werr.Wrap(err) + return + } + defer rows.Close() + + for rows.Next() { + var node Node + err = rows.StructScan(&node) + if err != nil { + err = werr.Wrap(err) + return + } + + err = json.Unmarshal(node.TypeSchemaRaw, &node.TypeSchema) + if err != nil { + err = werr.Wrap(err) + return + } + + connections = append(connections, node) + } + + return +} // }}} // vim: foldmethod=marker diff --git a/sql/0005.sql b/sql/0005.sql new file mode 100644 index 0000000..53ab86c --- /dev/null +++ b/sql/0005.sql @@ -0,0 +1,11 @@ +DROP TABLE public."connection"; + +CREATE TABLE public."connection" ( + id serial NOT NULL, + "from" int4 NOT NULL, + "to" int4 NOT NULL, + updated timestamptz DEFAULT NOW() NOT NULL, + CONSTRAINT newtable_pk PRIMARY KEY (id), + CONSTRAINT connection_node_fk FOREIGN KEY ("from") REFERENCES public.node(id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT connection_node_fk_1 FOREIGN KEY ("to") REFERENCES public.node(id) ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/static/css/main.css b/static/css/main.css index 81c2862..b531d85 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,5 +1,6 @@ :root { --textsize: 12pt; + --section-color: #73a44d; --je-color: #73a44d; --border-radius: 5px; } @@ -197,3 +198,29 @@ select:focus { outline: 2px solid #888; outline-offset: -2px; } +#connected-nodes > .label { + margin-bottom: 16px; + color: var(--section-color); + font-weight: bold; + font-size: 1.25em; +} +#connected-nodes .connected-nodes { + display: flex; + align-items: start; + flex-flow: row wrap; + gap: 32px; +} +#connected-nodes .connected-nodes .type-group { + display: grid; + grid-template-columns: 24px 1fr; + grid-gap: 8px; + align-items: center; +} +#connected-nodes .connected-nodes .type-group .type-name { + font-weight: bold; + grid-column: 1 / -1; +} +#connected-nodes .type-icon img { + display: block; + height: 24px; +} diff --git a/static/js/app.mjs b/static/js/app.mjs index d082f74..3657f7a 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -192,6 +192,22 @@ export class App { // doesn't make any sense before a node is selected. document.getElementById('editor-node').style.display = 'grid' }) + .catch(err => showError(err)) + + fetch(`/nodes/connections/${nodeID}`) + .then(data => data.json()) + .then(json => { + if (!json.OK) { + showError(err) + return + } + + const connectedNodes = new ConnectedNodes(json.Nodes) + document.getElementById('connected-nodes').replaceChildren(connectedNodes.render()) + + }) + .catch(err => showError(err)) + }// }}} async nodeRename(nodeID, name) {// {{{ return new Promise((resolve, reject) => { @@ -380,7 +396,6 @@ class NodeCreateDialog { }// }}} } - class SelectType { constructor(types) {// {{{ this.types = types @@ -549,7 +564,7 @@ export class TreeNode { if (this.expanded) this.tree.fetchNodes(this.node.ID) - .then(()=>this.render()) + .then(() => this.render()) }// }}} render() {// {{{ if (this.element === null) { @@ -727,4 +742,55 @@ function typeSort(a, b) {// {{{ return 0 }// }}} +class ConnectedNodes { + constructor(nodes) { + this.nodes = nodes + } + render() { + const div = document.createElement('template') + div.innerHTML = ` +
Connected nodes
+
+ ` + + const types = new Map() + for (const n of this.nodes) { + let typeGroup = types.get(n.TypeSchema.title) + if (typeGroup === undefined) { + typeGroup = document.createElement('div') + typeGroup.classList.add('type-group') + typeGroup.innerHTML = `
${n.TypeSchema.title}
` + types.set(n.TypeSchema.title, typeGroup) + } + + typeGroup.appendChild( + new ConnectedNode(n).render() + ) + } + + const connectedNodes = div.content.querySelector('.connected-nodes') + for (const t of Array.from(types.keys()).sort()) { + connectedNodes.append(types.get(t)) + } + + return div.content + } +} + + + +class ConnectedNode { + constructor(node) { + this.node = node + } + render() { + const tmpl = document.createElement('template') + tmpl.innerHTML = ` +
+
${this.node.Name}
+ ` + return tmpl.content + } +} + // vim: foldmethod=marker diff --git a/static/less/main.less b/static/less/main.less index 8f0b7e8..c594876 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -1,5 +1,6 @@ :root { --textsize: 12pt; + --section-color: #73a44d; --je-color: #73a44d; --border-radius: 5px; } @@ -266,3 +267,38 @@ select:focus { outline: 2px solid #888; outline-offset: -2px; } + +#connected-nodes { + & > .label { + margin-bottom: 16px; + color: var(--section-color); + font-weight: bold; + font-size: 1.25em; + } + + .connected-nodes { + display: flex; + align-items: start; + flex-flow: row wrap; + gap: 32px; + + .type-group { + display: grid; + grid-template-columns: 24px 1fr; + grid-gap: 8px; + align-items: center; + + .type-name { + font-weight: bold; + grid-column: 1 / -1; + } + } + } + + .type-icon { + img { + display: block; + height: 24px; + } + } +} diff --git a/views/pages/app.gotmpl b/views/pages/app.gotmpl index 0016739..34b2514 100644 --- a/views/pages/app.gotmpl +++ b/views/pages/app.gotmpl @@ -42,8 +42,7 @@ -
- References +
diff --git a/webserver.go b/webserver.go index bdcce9f..59af967 100644 --- a/webserver.go +++ b/webserver.go @@ -38,6 +38,7 @@ func initWebserver() (err error) { http.HandleFunc("/nodes/update/{nodeID}", actionNodeUpdate) http.HandleFunc("/nodes/rename/{nodeID}", actionNodeRename) http.HandleFunc("/nodes/delete/{nodeID}", actionNodeDelete) + http.HandleFunc("/nodes/connections/{nodeID}", actionNodeConnections) http.HandleFunc("/nodes/create", actionNodeCreate) http.HandleFunc("/nodes/move", actionNodeMove) http.HandleFunc("/types/{typeID}", actionType) @@ -235,6 +236,28 @@ func actionNodeDelete(w http.ResponseWriter, r *http.Request) { // {{{ j, _ := json.Marshal(out) w.Write(j) } // }}} +func actionNodeConnections(w http.ResponseWriter, r *http.Request) { // {{{ + nodeID := 0 + nodeIDStr := r.PathValue("nodeID") + nodeID, _ = strconv.Atoi(nodeIDStr) + + nodes, err := GetNodeConnections(nodeID) + if err != nil { + err = werr.Wrap(err) + httpError(w, err) + return + } + + out := struct { + OK bool + Nodes []Node + }{ + true, + nodes, + } + j, _ := json.Marshal(out) + w.Write(j) +} // }}} func actionNodeMove(w http.ResponseWriter, r *http.Request) { // {{{ var req struct { NewParentID int