Wip
This commit is contained in:
parent
a6bb845c9d
commit
cfd5bfd719
9 changed files with 213 additions and 111 deletions
BIN
datagraph
BIN
datagraph
Binary file not shown.
79
node.go
79
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
|
||||
|
|
|
|||
6
sql/0003.sql
Normal file
6
sql/0003.sql
Normal file
|
|
@ -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;
|
||||
1
sql/0004.sql
Normal file
1
sql/0004.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE public.node RENAME COLUMN parent_node_id TO parent_id;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<div style="padding: 16px">
|
||||
<select></select>
|
||||
<input type="text" placeholder="Name">
|
||||
<button onclick="this.commit()">Create</button>
|
||||
<button>Create</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
this.dialog.querySelector('button').addEventListener('click', () => this.commit())
|
||||
this.select = this.dialog.querySelector('select')
|
||||
this.input = this.dialog.querySelector('input')
|
||||
this.input.addEventListener('keydown', event => {
|
||||
|
|
@ -261,6 +304,13 @@ class NodeCreateDialog {
|
|||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
mbus.dispatch('TREE_RELOAD_NODE', {
|
||||
parentNodeID: this.parentNodeID,
|
||||
callback: () => {
|
||||
console.log('hum foo')
|
||||
mbus.dispatch('NODE_SELECTED', json.NodeID)
|
||||
},
|
||||
})
|
||||
this.dialog.close()
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
|
|
@ -359,34 +409,41 @@ export class Tree {
|
|||
})
|
||||
}// }}}
|
||||
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
|
||||
return new Promise((resolve, reject) => {
|
||||
// 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
|
||||
thisTreeNode.node = node
|
||||
thisTreeNode.updateExpandImages()
|
||||
thisTreeNode.toggleExpand(true)
|
||||
|
||||
// Children are sorted according to type and name.
|
||||
this.sortChildren(node.Children)
|
||||
// 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())
|
||||
// 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
|
||||
treenode.updateExpandImages()
|
||||
} else {
|
||||
const treenode = new TreeNode(n)
|
||||
this.treeNodes.set(n.ID, treenode)
|
||||
thisTreeNode.children.appendChild(treenode.render())
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
sortChildren(children) {// {{{
|
||||
children.sort((a, b) => {
|
||||
|
|
@ -407,6 +464,7 @@ export class TreeNode {
|
|||
this.childrenFetched = false
|
||||
this.element = null
|
||||
this.children = null
|
||||
this.expandEventListenerAdded = false
|
||||
}// }}}
|
||||
|
||||
render() {// {{{
|
||||
|
|
@ -423,16 +481,13 @@ export class TreeNode {
|
|||
div.innerHTML = nodeHTML
|
||||
|
||||
this.children = div.querySelector('.children')
|
||||
this.expandImg = div.querySelector('.expand-status img')
|
||||
|
||||
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')
|
||||
this.expandStatus = div.querySelector('.expand-status img')
|
||||
this.updateExpandImages()
|
||||
|
||||
if (this.node.TypeIcon) {
|
||||
const img = div.querySelector('.type-icon img')
|
||||
|
|
@ -450,15 +505,32 @@ export class TreeNode {
|
|||
hasChildren() {// {{{
|
||||
return this.node.NumChildren > 0
|
||||
}// }}}
|
||||
toggleExpand(event) {// {{{
|
||||
const node = event.target.closest('.node')
|
||||
node?.classList.toggle('expanded')
|
||||
updateExpandImages() {// {{{
|
||||
if (this.hasChildren()) {
|
||||
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box-outline.svg`)
|
||||
|
||||
if (!this.expandEventListenerAdded) {
|
||||
this.expandStatus.addEventListener('click', () => this.toggleExpand())
|
||||
this.expandEventListenerAdded = true
|
||||
}
|
||||
} else {
|
||||
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/circle-medium.svg`)
|
||||
}
|
||||
}// }}}
|
||||
toggleExpand(expanded) {// {{{
|
||||
const node = this.element
|
||||
|
||||
if (expanded === undefined)
|
||||
node?.classList.toggle('expanded')
|
||||
else if (expanded === true)
|
||||
node?.classList.add('expanded')
|
||||
else
|
||||
node?.classList.remove('expanded')
|
||||
|
||||
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`)
|
||||
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${img}.svg`)
|
||||
|
||||
if (!this.childrenFetched && this.node.NumChildren > 0 && this.node.Children.length == 0) {
|
||||
console.log(`fetching for ${this.node.Name}`)
|
||||
mbus.dispatch('NODE_EXPAND', this)
|
||||
}
|
||||
}// }}}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ body {
|
|||
|
||||
& > 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;
|
||||
|
||||
|
|
@ -153,6 +153,7 @@ body {
|
|||
display: grid;
|
||||
grid-template-columns: min-content min-content 100%;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
|
|
@ -160,11 +161,7 @@ body {
|
|||
|
||||
&.selected {
|
||||
& > .name {
|
||||
color: #a02c2c;
|
||||
}
|
||||
|
||||
& > .type-icon {
|
||||
filter: invert(.7) sepia(.5) hue-rotate(0deg) saturate(750%) brightness(0.85) !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,11 +24,12 @@
|
|||
</div>
|
||||
|
||||
<div class="page section" id="nodes"></div>
|
||||
<div class="page" id="editor-node">
|
||||
<div class="page" id="editor-node" style="display: none">
|
||||
<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" />
|
||||
<img onclick="mbus.dispatch('NODE_DELETE')" src="/images/{{ .VERSION }}/node_modules/@mdi/svg/svg/trash-can.svg" style="display: block; height: 32px" />
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
|
|
|
|||
28
webserver.go
28
webserver.go
|
|
@ -37,6 +37,7 @@ func initWebserver() (err error) {
|
|||
http.HandleFunc("/nodes/{nodeID}", actionNode)
|
||||
http.HandleFunc("/nodes/update/{nodeID}", actionNodeUpdate)
|
||||
http.HandleFunc("/nodes/rename/{nodeID}", actionNodeRename)
|
||||
http.HandleFunc("/nodes/delete/{nodeID}", actionNodeDelete)
|
||||
http.HandleFunc("/nodes/create", actionNodeCreate)
|
||||
http.HandleFunc("/types/{typeID}", actionType)
|
||||
http.HandleFunc("/types/", actionTypesAll)
|
||||
|
|
@ -163,7 +164,7 @@ func actionNodeRename(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
nodeID, _ = strconv.Atoi(nodeIDStr)
|
||||
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
var req struct { Name string }
|
||||
var req struct{ Name string }
|
||||
err := json.Unmarshal(data, &req)
|
||||
|
||||
err = RenameNode(nodeID, req.Name)
|
||||
|
|
@ -195,7 +196,30 @@ func actionNodeCreate(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
return
|
||||
}
|
||||
|
||||
err = CreateNode(req.ParentNodeID, req.TypeID, req.Name)
|
||||
var nodeID int
|
||||
nodeID, err = CreateNode(req.ParentNodeID, req.TypeID, req.Name)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
NodeID int
|
||||
}{
|
||||
true,
|
||||
nodeID,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeDelete(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
nodeID := 0
|
||||
nodeIDStr := r.PathValue("nodeID")
|
||||
nodeID, _ = strconv.Atoi(nodeIDStr)
|
||||
|
||||
err := DeleteNode(nodeID)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue