Reworked tree
This commit is contained in:
parent
c0255fadb8
commit
a6bb845c9d
9 changed files with 314 additions and 117 deletions
BIN
datagraph
BIN
datagraph
Binary file not shown.
5
main.go
5
main.go
|
|
@ -1,6 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
// External
|
||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||
|
||||
// Standard
|
||||
"embed"
|
||||
"flag"
|
||||
|
|
@ -41,6 +44,8 @@ func initCmdline() {
|
|||
flag.Parse()
|
||||
}
|
||||
func main() {
|
||||
werr.Init()
|
||||
|
||||
initLog()
|
||||
initCmdline()
|
||||
|
||||
|
|
|
|||
47
node.go
47
node.go
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
// External
|
||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// Standard
|
||||
|
|
@ -49,26 +50,30 @@ func GetNode(nodeID int) (node Node, err error) {
|
|||
|
||||
err = row.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
|
||||
}
|
||||
|
||||
err = json.Unmarshal(node.DataRaw, &node.Data)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
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)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
|
@ -78,6 +83,7 @@ func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) {
|
|||
var node Node
|
||||
err = rows.StructScan(&node)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -90,9 +96,8 @@ 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
|
||||
|
|
@ -141,10 +146,14 @@ func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) {
|
|||
startNodeID,
|
||||
maxDepth,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func ComposeTree(nodes map[int]*Node, node *Node) {
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
}
|
||||
|
||||
return
|
||||
}// }}}
|
||||
func ComposeTree(nodes map[int]*Node, node *Node) {// {{{
|
||||
if node.Children == nil {
|
||||
node.Children = []*Node{}
|
||||
}
|
||||
|
|
@ -159,15 +168,24 @@ 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) {// {{{
|
||||
_, err = db.Exec(`UPDATE node SET name=$2 WHERE id=$1`, nodeID, name)
|
||||
return
|
||||
}// }}}
|
||||
|
||||
func CreateNode(parentNodeID, typeID int, name string) (err error) {
|
||||
j, _ := json.Marshal(struct { Name string }{name})
|
||||
func CreateNode(parentNodeID, typeID int, name string) (err error) {// {{{
|
||||
j, _ := json.Marshal(
|
||||
struct {
|
||||
New bool `json:"x-new"`
|
||||
}{
|
||||
true,
|
||||
})
|
||||
|
||||
row := db.QueryRow(`
|
||||
INSERT INTO node(type_id, name, data)
|
||||
|
|
@ -179,10 +197,13 @@ func CreateNode(parentNodeID, typeID int, name string) (err error) {
|
|||
var id int
|
||||
err = row.Scan(&id)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Exec(`INSERT INTO connection("parent", "child") VALUES($1, $2)`, parentNodeID, id)
|
||||
|
||||
return
|
||||
}
|
||||
}// }}}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
|
|
@ -65,8 +65,20 @@ body {
|
|||
}
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
}
|
||||
#editor-node.show {
|
||||
display: grid;
|
||||
}
|
||||
#editor-node > div.ops {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
align-items: center;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
#editor-node > div.ops #editor-node-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
#types {
|
||||
grid-area: navigation;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ export class App {
|
|||
window.mbus = new MessageBus()
|
||||
this.editor = null
|
||||
this.typesList = null
|
||||
this.currentNode = null
|
||||
this.currentNodeID = null
|
||||
this.types = []
|
||||
this.currentPage = null
|
||||
this.tree = new Tree(document.getElementById('nodes'))
|
||||
|
||||
const events = [
|
||||
'MENU_ITEM_SELECTED',
|
||||
|
|
@ -16,7 +18,7 @@ export class App {
|
|||
'EDITOR_NODE_SAVE',
|
||||
'TYPES_LIST_FETCHED',
|
||||
'NODE_CREATE_DIALOG',
|
||||
'NODE_CREATE',
|
||||
'NODE_EDIT_NAME',
|
||||
]
|
||||
for (const eventName of events)
|
||||
mbus.subscribe(eventName, event => this.eventHandler(event))
|
||||
|
|
@ -26,7 +28,7 @@ export class App {
|
|||
mbus.dispatch('MENU_ITEM_SELECTED', 'node')
|
||||
}// }}}
|
||||
|
||||
async eventHandler(event) {// {{{
|
||||
eventHandler(event) {// {{{
|
||||
switch (event.type) {
|
||||
case 'MENU_ITEM_SELECTED':
|
||||
const item = document.querySelector(`#menu [data-section="${event.detail}"]`)
|
||||
|
|
@ -40,8 +42,7 @@ export class App {
|
|||
for (const n of document.querySelectorAll(`#nodes .node[data-node-id="${event.detail}"]`))
|
||||
n.classList?.add('selected')
|
||||
|
||||
this.currentNodeID = event.detail
|
||||
this.edit(this.currentNodeID)
|
||||
this.edit(event.detail)
|
||||
break
|
||||
|
||||
case 'EDITOR_NODE_SAVE':
|
||||
|
|
@ -51,18 +52,24 @@ export class App {
|
|||
case 'TYPES_LIST_FETCHED':
|
||||
const types = document.getElementById('types')
|
||||
types.replaceChildren(this.typesList.render())
|
||||
|
||||
case 'NODE_CREATE_DIALOG':
|
||||
if (this.currentPage !== 'node' || this.currentNodeID === null)
|
||||
return
|
||||
|
||||
new NodeCreateDialog(this.currentNodeID)
|
||||
break
|
||||
|
||||
case 'NODE_CREATE':
|
||||
case 'NODE_CREATE_DIALOG':
|
||||
if (this.currentPage !== 'node' || this.currentNode === null)
|
||||
return
|
||||
|
||||
new NodeCreateDialog(this.currentNode.ID)
|
||||
break
|
||||
|
||||
case 'NODE_EDIT_NAME':
|
||||
const newName = prompt('Rename node', this.currentNode.Name)
|
||||
if (newName === null)
|
||||
return
|
||||
this.nodeRename(this.currentNode.ID, newName)
|
||||
break
|
||||
|
||||
default:
|
||||
alert(`Unhandled event: ${event.type}`)
|
||||
console.log(event)
|
||||
}
|
||||
}// }}}
|
||||
|
|
@ -72,7 +79,7 @@ export class App {
|
|||
switch (event.key.toUpperCase()) {
|
||||
case 'N':
|
||||
if (!event.shiftKey || !event.altKey)
|
||||
break
|
||||
return
|
||||
mbus.dispatch('NODE_CREATE_DIALOG')
|
||||
break
|
||||
|
||||
|
|
@ -114,8 +121,8 @@ export class App {
|
|||
break
|
||||
}
|
||||
}// }}}
|
||||
|
||||
edit(nodeID) {// {{{
|
||||
console.log(nodeID)
|
||||
fetch(`/nodes/${nodeID}`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
|
|
@ -124,10 +131,43 @@ export class App {
|
|||
return
|
||||
}
|
||||
|
||||
this.currentNode = json.Node
|
||||
|
||||
// The JSON editor is created each time. Could probably be reused.
|
||||
const editorEl = document.querySelector('#editor-node .editor')
|
||||
this.editor = new Editor(json.Node.TypeSchema)
|
||||
editorEl.replaceChildren(this.editor.render(json.Node.Data))
|
||||
|
||||
if (json.Node.Data['x-new'])
|
||||
editorEl.replaceChildren(this.editor.render(null))
|
||||
else
|
||||
editorEl.replaceChildren(this.editor.render(json.Node.Data))
|
||||
|
||||
// Name is separate from the JSON node.
|
||||
const name = document.getElementById('editor-node-name')
|
||||
name.innerText = json.Node.Name
|
||||
})
|
||||
}// }}}
|
||||
nodeRename(nodeID, name) {// {{{
|
||||
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)
|
||||
})
|
||||
}// }}}
|
||||
nodeUpdate() {// {{{
|
||||
|
|
@ -140,7 +180,7 @@ export class App {
|
|||
|
||||
const nodeData = this.editor.data()
|
||||
|
||||
fetch(`/nodes/update/${this.currentNodeID}`, {
|
||||
fetch(`/nodes/update/${this.currentNode.ID}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(nodeData),
|
||||
})
|
||||
|
|
@ -186,7 +226,7 @@ class NodeCreateDialog {
|
|||
<div style="padding: 16px">
|
||||
<select></select>
|
||||
<input type="text" placeholder="Name">
|
||||
<button onclick="mbus.dispatch('NODE_CREATE', ()=>this.commit())">Create</button>
|
||||
<button onclick="this.commit()">Create</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
|
|
@ -273,61 +313,83 @@ class SelectType {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
export class TreeNode {
|
||||
constructor(parent, data) {// {{{
|
||||
this.data = data
|
||||
this.parent = parent
|
||||
this.childrenFetched = false
|
||||
this.children = null
|
||||
export class Tree {
|
||||
constructor() {// {{{
|
||||
this.treeNodes = new Map()
|
||||
|
||||
this.sortChildren()
|
||||
const events = [
|
||||
'NODE_EXPAND',
|
||||
]
|
||||
for (const e of events)
|
||||
mbus.subscribe(e, event => this.eventHandler(event))
|
||||
|
||||
|
||||
this.fetchNodes(0)
|
||||
.then(node => {
|
||||
const top = document.getElementById('nodes')
|
||||
const topNode = new TreeNode(node)
|
||||
this.treeNodes.set(node.ID, topNode)
|
||||
top.appendChild(topNode.render())
|
||||
this.updateNode(0)
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
eventHandler(event) {// {{{
|
||||
switch (event.type) {
|
||||
case 'NODE_EXPAND':
|
||||
this.updateNode(event.detail.node.ID)
|
||||
break
|
||||
|
||||
render() {// {{{
|
||||
const nodeHTML = `
|
||||
<div class="node" data-node-id="${this.data.ID}">
|
||||
<div class="expand-status"><img /></div>
|
||||
<div class="type-icon"><img /></div>
|
||||
<div class="name">${this.name()}</div>
|
||||
<div class="children"></div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const tmpl = document.createElement('template')
|
||||
tmpl.innerHTML = nodeHTML
|
||||
this.children = tmpl.content.querySelector('.children')
|
||||
|
||||
tmpl.content.querySelector('.name').addEventListener('click', () => mbus.dispatch('NODE_SELECTED', this.data.ID))
|
||||
|
||||
// data.NumChildren is set regardless of having fetched the children or not.
|
||||
if (this.hasChildren()) {
|
||||
const img = tmpl.content.querySelector('.expand-status img')
|
||||
img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box-outline.svg`)
|
||||
img.addEventListener('click', event => this.toggleExpand(event))
|
||||
} else
|
||||
tmpl.content.querySelector('.expand-status').classList.add('leaf')
|
||||
|
||||
if (this.data.TypeIcon) {
|
||||
const img = tmpl.content.querySelector('.type-icon img')
|
||||
img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.data.TypeIcon}.svg`)
|
||||
}
|
||||
|
||||
this.parent.appendChild(tmpl.content)
|
||||
|
||||
for (const c of this.data.Children || []) {
|
||||
(new TreeNode(this.children, c)).render()
|
||||
default:
|
||||
alert(`Unhandled event: ${event.type}`)
|
||||
console.log(event)
|
||||
}
|
||||
}// }}}
|
||||
name() {// {{{
|
||||
if (this.data.TypeName === 'root_node')
|
||||
return 'Start'
|
||||
return this.data.Name
|
||||
async fetchNodes(topNode) {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`/nodes/tree/${topNode}?depth=1`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
reject(json.Error)
|
||||
return
|
||||
}
|
||||
resolve(json.Nodes)
|
||||
})
|
||||
})
|
||||
}// }}}
|
||||
hasChildren() {// {{{
|
||||
return this.data.NumChildren > 0
|
||||
updateNode(nodeID) {// {{{
|
||||
// updateNode retrieves a node and its' immediate children.
|
||||
// Node and each child is found in the treeNodes map and the names are updated.
|
||||
// If not found, created and added.
|
||||
//
|
||||
// Newly created nodes are found and added, existing but renamed nodes are modified, and unchanged are left as is.
|
||||
this.fetchNodes(nodeID)
|
||||
.then(node => {
|
||||
const thisTreeNode = this.treeNodes.get(nodeID)
|
||||
thisTreeNode.childrenFetched = true
|
||||
|
||||
// Children are sorted according to type and name.
|
||||
this.sortChildren(node.Children)
|
||||
|
||||
// Update or add children
|
||||
for (const n of node.Children) {
|
||||
if (this.treeNodes.has(n.ID)) {
|
||||
const treenode = this.treeNodes.get(n.ID)
|
||||
treenode.node = n
|
||||
treenode.element.querySelector('.name').innerText = n.Name
|
||||
} else {
|
||||
const treenode = new TreeNode(n)
|
||||
this.treeNodes.set(n.ID, treenode)
|
||||
thisTreeNode.children.appendChild(treenode.render())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
sortChildren() {// {{{
|
||||
this.data.Children.sort((a, b) => {
|
||||
sortChildren(children) {// {{{
|
||||
children.sort((a, b) => {
|
||||
if (a.TypeName < b.TypeName) return -1
|
||||
if (a.TypeName > b.TypeName) return 1
|
||||
|
||||
|
|
@ -337,7 +399,57 @@ export class TreeNode {
|
|||
return 0
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class TreeNode {
|
||||
constructor(data) {// {{{
|
||||
this.node = data
|
||||
this.childrenFetched = false
|
||||
this.element = null
|
||||
this.children = null
|
||||
}// }}}
|
||||
|
||||
render() {// {{{
|
||||
const nodeHTML = `
|
||||
<div class="expand-status"><img /></div>
|
||||
<div class="type-icon"><img /></div>
|
||||
<div class="name">${this.name()}</div>
|
||||
<div class="children"></div>
|
||||
`
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('node')
|
||||
div.setAttribute('data-node-id', this.node.ID)
|
||||
div.innerHTML = nodeHTML
|
||||
|
||||
this.children = div.querySelector('.children')
|
||||
|
||||
div.querySelector('.name').addEventListener('click', () => mbus.dispatch('NODE_SELECTED', this.node.ID))
|
||||
|
||||
// data.NumChildren is set regardless of having fetched the children or not.
|
||||
if (this.hasChildren()) {
|
||||
const img = div.querySelector('.expand-status img')
|
||||
img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box-outline.svg`)
|
||||
img.addEventListener('click', event => this.toggleExpand(event))
|
||||
} else
|
||||
div.querySelector('.expand-status').classList.add('leaf')
|
||||
|
||||
if (this.node.TypeIcon) {
|
||||
const img = div.querySelector('.type-icon img')
|
||||
img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.node.TypeIcon}.svg`)
|
||||
}
|
||||
|
||||
this.element = div
|
||||
return div
|
||||
}// }}}
|
||||
name() {// {{{
|
||||
if (this.node.TypeName === 'root_node')
|
||||
return 'Start'
|
||||
return this.node.Name
|
||||
}// }}}
|
||||
hasChildren() {// {{{
|
||||
return this.node.NumChildren > 0
|
||||
}// }}}
|
||||
toggleExpand(event) {// {{{
|
||||
const node = event.target.closest('.node')
|
||||
node?.classList.toggle('expanded')
|
||||
|
|
@ -345,27 +457,14 @@ export class TreeNode {
|
|||
const img = node?.classList.contains('expanded') ? 'minus-box-outline' : 'plus-box-outline'
|
||||
event.target.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${img}.svg`)
|
||||
|
||||
if (!this.childrenFetched && this.data.NumChildren > 0 && this.data.Children.length == 0) {
|
||||
this.fetchChildren()
|
||||
.then(data => {
|
||||
this.childrenFetched = true
|
||||
this.data.Children = data.Children
|
||||
this.sortChildren()
|
||||
|
||||
for (const nodeData of this.data.Children) {
|
||||
const node = new TreeNode(this.children, nodeData)
|
||||
node.render()
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err)
|
||||
console.error(err)
|
||||
})
|
||||
if (!this.childrenFetched && this.node.NumChildren > 0 && this.node.Children.length == 0) {
|
||||
console.log(`fetching for ${this.node.Name}`)
|
||||
mbus.dispatch('NODE_EXPAND', this)
|
||||
}
|
||||
}// }}}
|
||||
async fetchChildren() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`/nodes/tree/${this.data.ID}?depth=2`)
|
||||
fetch(`/nodes/tree/${this.node.ID}?depth=1`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (json.OK)
|
||||
|
|
|
|||
|
|
@ -5,22 +5,39 @@ export class Editor {
|
|||
}
|
||||
|
||||
render(data) {
|
||||
const div = document.createElement('div')
|
||||
this.editor = new JSONEditor(div, {
|
||||
const options = {
|
||||
theme: 'spectre',
|
||||
iconlib: 'spectre',
|
||||
disable_collapse: true,
|
||||
disable_properties: true,
|
||||
schema: this.schema,
|
||||
});
|
||||
}
|
||||
|
||||
this.editor.on('ready', () => {
|
||||
this.editor.setValue(data)
|
||||
})
|
||||
// startval isn't set if this is a newly created node.
|
||||
// When setValue is called (or startval set), all widgets/fields are hidden if not defined in the JSON data.
|
||||
// When startval isn't set, the schema properties are displayed instead.
|
||||
if (data !== undefined && data !== null)
|
||||
options.startval = data
|
||||
|
||||
const div = document.createElement('div')
|
||||
this.editor = new JSONEditor(div, options);
|
||||
|
||||
|
||||
// this.editor.on('ready', ()=>{
|
||||
// })
|
||||
div.addEventListener('keydown', event=>this.keyHandler(event))
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
keyHandler(event) {
|
||||
if (!event.ctrlKey || event.key != 's')
|
||||
return
|
||||
mbus.dispatch('EDITOR_NODE_SAVE')
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
data() {
|
||||
return this.editor.getValue()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,8 +86,27 @@ body {
|
|||
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-rows:
|
||||
min-content
|
||||
1fr
|
||||
min-content
|
||||
;
|
||||
|
||||
&.show {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
& > div.ops {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
align-items: center;
|
||||
grid-gap: 8px;
|
||||
|
||||
#editor-node-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#types {
|
||||
|
|
|
|||
|
|
@ -14,20 +14,6 @@
|
|||
import {App, TreeNode} from '/js/{{ .VERSION }}/app.mjs'
|
||||
window._VERSION = '{{ .VERSION }}'
|
||||
window._app = new App()
|
||||
|
||||
fetch('/nodes/tree/0?depth=2')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
const top = document.getElementById('nodes')
|
||||
const topNode = new TreeNode(top, json.Nodes)
|
||||
topNode.render()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div id="layout">
|
||||
|
|
@ -39,7 +25,9 @@
|
|||
|
||||
<div class="page section" id="nodes"></div>
|
||||
<div class="page" id="editor-node">
|
||||
<div class="section">
|
||||
<div class="section ops">
|
||||
<img onclick="mbus.dispatch('NODE_EDIT_NAME')" src="/images/{{ .VERSION }}/node_modules/@mdi/svg/svg/tag-text-outline.svg" style="display: block; height: 32px" />
|
||||
<div onclick="mbus.dispatch('NODE_EDIT_NAME')" id="editor-node-name"></div>
|
||||
<img onclick="mbus.dispatch('NODE_CREATE_DIALOG')" src="/images/{{ .VERSION }}/node_modules/@mdi/svg/svg/plus-box.svg" style="display: block; height: 32px" />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
36
webserver.go
36
webserver.go
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
// External
|
||||
"git.ahall.se/go/html_template"
|
||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||
|
||||
// Standard
|
||||
"encoding/json"
|
||||
|
|
@ -26,6 +27,7 @@ func initWebserver() (err error) {
|
|||
|
||||
engine, err = HTMLTemplate.NewEngine(subViewFS, subStaticFS, flagDev)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +36,7 @@ func initWebserver() (err error) {
|
|||
http.HandleFunc("/nodes/tree/{startNode}", actionNodesTree)
|
||||
http.HandleFunc("/nodes/{nodeID}", actionNode)
|
||||
http.HandleFunc("/nodes/update/{nodeID}", actionNodeUpdate)
|
||||
http.HandleFunc("/nodes/rename/{nodeID}", actionNodeRename)
|
||||
http.HandleFunc("/nodes/create", actionNodeCreate)
|
||||
http.HandleFunc("/types/{typeID}", actionType)
|
||||
http.HandleFunc("/types/", actionTypesAll)
|
||||
|
|
@ -66,6 +69,7 @@ func pageApp(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
|
||||
ts, err := GetTypes()
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -74,6 +78,7 @@ func pageApp(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
|
||||
err = engine.Render(page, w, r)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
w.Write([]byte(err.Error()))
|
||||
}
|
||||
} // }}}
|
||||
|
|
@ -92,6 +97,7 @@ func actionNodesTree(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
|
||||
topNode, err := GetNodeTree(startNode, maxDepth)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -114,6 +120,7 @@ func actionNode(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
|
||||
node, err := GetNode(nodeID)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -137,6 +144,31 @@ func actionNodeUpdate(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
|
||||
err := UpdateNode(nodeID, data)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeRename(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
nodeID := 0
|
||||
nodeIDStr := r.PathValue("nodeID")
|
||||
nodeID, _ = strconv.Atoi(nodeIDStr)
|
||||
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
var req struct { Name string }
|
||||
err := json.Unmarshal(data, &req)
|
||||
|
||||
err = RenameNode(nodeID, req.Name)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -158,12 +190,14 @@ func actionNodeCreate(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
data, _ := io.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &req)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = CreateNode(req.ParentNodeID, req.TypeID, req.Name)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -183,6 +217,7 @@ func actionType(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
|
||||
typ, err := GetType(typeID)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -193,6 +228,7 @@ func actionType(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
func actionTypesAll(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
types, err := GetTypes()
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue