Moveing of objects
This commit is contained in:
parent
cfd5bfd719
commit
25bbc0c748
7 changed files with 167 additions and 16 deletions
BIN
datagraph
BIN
datagraph
Binary file not shown.
25
node.go
25
node.go
|
|
@ -9,6 +9,9 @@ import (
|
|||
// Standard
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -120,7 +123,7 @@ func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) { // {{
|
|||
SELECT
|
||||
COALESCE(n.parent_id, -1) AS parent_id,
|
||||
n.id,
|
||||
CONCAT(REPEAT(' ', ns.depth), n.name) AS name,
|
||||
n.name,
|
||||
n.type_id,
|
||||
t.name AS type_name,
|
||||
COALESCE(t.schema->>'icon', '') AS type_icon,
|
||||
|
|
@ -208,5 +211,25 @@ func DeleteNode(nodeID int) (err error) { // {{{
|
|||
}
|
||||
return
|
||||
} // }}}
|
||||
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?
|
||||
|
||||
var nodeIDStr []string
|
||||
for _, n := range nodeIDs {
|
||||
nodeIDStr = append(nodeIDStr, strconv.Itoa(n))
|
||||
}
|
||||
|
||||
joinedIDs := strings.Join(nodeIDStr, ",")
|
||||
sql := fmt.Sprintf(
|
||||
`UPDATE node
|
||||
SET parent_id=$1
|
||||
WHERE id IN (%s)`,
|
||||
joinedIDs,
|
||||
)
|
||||
|
||||
_, err = db.Exec(sql, newParentID)
|
||||
return
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ body {
|
|||
}
|
||||
#editor-node > div.ops {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content min-content;
|
||||
grid-template-columns: min-content 1fr repeat(3, min-content);
|
||||
align-items: center;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
|
|
@ -123,6 +123,9 @@ body {
|
|||
.node.selected > .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.node.marked > .name {
|
||||
color: #a00;
|
||||
}
|
||||
.node.expanded > .children {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export class App {
|
|||
'NODE_CREATE_DIALOG',
|
||||
'NODE_DELETE',
|
||||
'NODE_EDIT_NAME',
|
||||
'NODE_MOVE',
|
||||
'NODE_REMOVED',
|
||||
'NODE_SELECTED',
|
||||
'TREE_RELOAD_NODE',
|
||||
'TYPES_LIST_FETCHED',
|
||||
|
|
@ -41,8 +43,9 @@ export class App {
|
|||
for (const n of document.querySelectorAll('#nodes .node.selected'))
|
||||
n.classList.remove('selected')
|
||||
|
||||
for (const n of document.querySelectorAll(`#nodes .node[data-node-id="${event.detail}"]`))
|
||||
n.classList?.add('selected')
|
||||
if (event.detail !== null)
|
||||
for (const n of document.querySelectorAll(`#nodes .node[data-node-id="${event.detail}"]`))
|
||||
n.classList?.add('selected')
|
||||
|
||||
this.edit(event.detail)
|
||||
break
|
||||
|
|
@ -53,6 +56,20 @@ export class App {
|
|||
this.nodeDelete(this.currentNode.ID)
|
||||
break
|
||||
|
||||
case 'NODE_MOVE':
|
||||
const nodes = this.tree.markedNodes()
|
||||
if (!confirm(`Are you sure you want to move ${nodes.length} nodes here?`))
|
||||
return
|
||||
this.nodesMove(nodes, this.currentNode.ID)
|
||||
break
|
||||
|
||||
case 'NODE_REMOVED':
|
||||
// Event dispatched when a tree node is removed after an update.
|
||||
if (this.currentNode.ID !== event.detail)
|
||||
return
|
||||
mbus.dispatch('NODE_SELECTED', null)
|
||||
break
|
||||
|
||||
case 'EDITOR_NODE_SAVE':
|
||||
this.nodeUpdate()
|
||||
break
|
||||
|
|
@ -79,12 +96,12 @@ export class App {
|
|||
break
|
||||
|
||||
case 'TREE_RELOAD_NODE':
|
||||
this.tree.updateNode(event.detail.parentNodeID)
|
||||
this.tree.updateNode(parseInt(event.detail.parentNodeID))
|
||||
.then(() => {
|
||||
if (event.detail.callback)
|
||||
event.detail.callback()
|
||||
.catch(err => showError(err))
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
break
|
||||
|
||||
default:
|
||||
|
|
@ -93,16 +110,20 @@ export class App {
|
|||
}
|
||||
}// }}}
|
||||
keyHandler(event) {// {{{
|
||||
let handled = true
|
||||
if (!event.shiftKey || !event.altKey)
|
||||
return
|
||||
|
||||
let handled = true
|
||||
switch (event.key.toUpperCase()) {
|
||||
case 'D':
|
||||
mbus.dispatch('NODE_DELETE')
|
||||
break
|
||||
|
||||
case 'M':
|
||||
mbus.dispatch('NODE_MOVE')
|
||||
break
|
||||
|
||||
case 'N':
|
||||
if (!event.shiftKey || !event.altKey)
|
||||
return
|
||||
mbus.dispatch('NODE_CREATE_DIALOG')
|
||||
break
|
||||
|
||||
|
|
@ -146,6 +167,12 @@ export class App {
|
|||
}// }}}
|
||||
|
||||
edit(nodeID) {// {{{
|
||||
if (nodeID === null) {
|
||||
document.getElementById('editor-node').style.display = 'none'
|
||||
this.currentNode = null
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`/nodes/${nodeID}`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
|
|
@ -240,6 +267,29 @@ export class App {
|
|||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
nodesMove(nodes, newParentID) {// {{{
|
||||
const req = {
|
||||
NewParentID: parseInt(newParentID),
|
||||
NodeIDs: nodes.map(n => n.ID),
|
||||
}
|
||||
fetch(`/nodes/move`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req),
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
const newParentElement = this.tree.treeNodes.get(newParentID).children
|
||||
for (const n of nodes)
|
||||
newParentElement.append(this.tree.treeNodes.get(n.ID).element)
|
||||
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class NodeCreateDialog {
|
||||
|
|
@ -306,10 +356,7 @@ class NodeCreateDialog {
|
|||
}
|
||||
mbus.dispatch('TREE_RELOAD_NODE', {
|
||||
parentNodeID: this.parentNodeID,
|
||||
callback: () => {
|
||||
console.log('hum foo')
|
||||
mbus.dispatch('NODE_SELECTED', json.NodeID)
|
||||
},
|
||||
callback: () => mbus.dispatch('NODE_SELECTED', json.NodeID)
|
||||
})
|
||||
this.dialog.close()
|
||||
})
|
||||
|
|
@ -374,6 +421,21 @@ export class Tree {
|
|||
mbus.subscribe(e, event => this.eventHandler(event))
|
||||
|
||||
|
||||
// click on the empty tree list to unmark all nodes.
|
||||
const nodesEl = document.getElementById('nodes')
|
||||
nodesEl.addEventListener('click', event => {
|
||||
// To prevent accidentally removing all node marks,
|
||||
// shift is required to be unpressed, since it is required to
|
||||
// be pressed when marking nodes.
|
||||
if (event.shiftKey)
|
||||
return
|
||||
|
||||
const markedElements = document.querySelectorAll('#nodes .node.marked')
|
||||
for (const e of markedElements)
|
||||
e.classList.remove('marked')
|
||||
})
|
||||
|
||||
// Fetch the top node to start
|
||||
this.fetchNodes(0)
|
||||
.then(node => {
|
||||
const top = document.getElementById('nodes')
|
||||
|
|
@ -426,6 +488,17 @@ export class Tree {
|
|||
// Children are sorted according to type and name.
|
||||
this.sortChildren(node.Children)
|
||||
|
||||
// Deleted or moved children
|
||||
for (const c of thisTreeNode.children.children) {
|
||||
const nodeID = parseInt(c.dataset.nodeId)
|
||||
const nodeStillExist = node.Children.some(n => n.ID === nodeID)
|
||||
if (!nodeStillExist) {
|
||||
c.remove()
|
||||
mbus.dispatch('NODE_REMOVED', nodeID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update or add children
|
||||
for (const n of node.Children) {
|
||||
if (this.treeNodes.has(n.ID)) {
|
||||
|
|
@ -439,10 +512,11 @@ export class Tree {
|
|||
thisTreeNode.children.appendChild(treenode.render())
|
||||
}
|
||||
}
|
||||
|
||||
resolve()
|
||||
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
.catch(err => { showError(err); reject(err) })
|
||||
})
|
||||
}// }}}
|
||||
sortChildren(children) {// {{{
|
||||
|
|
@ -456,6 +530,15 @@ export class Tree {
|
|||
return 0
|
||||
})
|
||||
}// }}}
|
||||
markedNodes() {// {{{
|
||||
const markedElements = document.querySelectorAll('#nodes .node.marked')
|
||||
const marked = []
|
||||
for (const n of markedElements) {
|
||||
const nodeID = n.getAttribute('data-node-id')
|
||||
marked.push(this.treeNodes.get(parseInt(nodeID)).node)
|
||||
}
|
||||
return marked
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class TreeNode {
|
||||
|
|
@ -483,7 +566,13 @@ export class TreeNode {
|
|||
this.children = div.querySelector('.children')
|
||||
this.expandImg = div.querySelector('.expand-status img')
|
||||
|
||||
div.querySelector('.name').addEventListener('click', () => mbus.dispatch('NODE_SELECTED', this.node.ID))
|
||||
div.querySelector('.name').addEventListener('click', event => {
|
||||
if (!event.shiftKey)
|
||||
mbus.dispatch('NODE_SELECTED', this.node.ID)
|
||||
else
|
||||
this.element.classList.toggle('marked')
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
// data.NumChildren is set regardless of having fetched the children or not.
|
||||
this.expandStatus = div.querySelector('.expand-status img')
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ body {
|
|||
|
||||
& > div.ops {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content min-content;
|
||||
grid-template-columns: min-content 1fr repeat(3, min-content);
|
||||
align-items: center;
|
||||
grid-gap: 8px;
|
||||
|
||||
|
|
@ -164,6 +164,12 @@ body {
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.marked {
|
||||
& > .name {
|
||||
color: #a00;
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
&>.children {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<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" />
|
||||
<img onclick="mbus.dispatch('NODE_MOVE')" src="/images/{{ .VERSION }}/node_modules/@mdi/svg/svg/file-move-outline.svg" style="display: block; height: 32px" />
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
|
|
|
|||
29
webserver.go
29
webserver.go
|
|
@ -39,6 +39,7 @@ func initWebserver() (err error) {
|
|||
http.HandleFunc("/nodes/rename/{nodeID}", actionNodeRename)
|
||||
http.HandleFunc("/nodes/delete/{nodeID}", actionNodeDelete)
|
||||
http.HandleFunc("/nodes/create", actionNodeCreate)
|
||||
http.HandleFunc("/nodes/move", actionNodeMove)
|
||||
http.HandleFunc("/types/{typeID}", actionType)
|
||||
http.HandleFunc("/types/", actionTypesAll)
|
||||
|
||||
|
|
@ -234,6 +235,34 @@ func actionNodeDelete(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeMove(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
var req struct {
|
||||
NewParentID int
|
||||
NodeIDs []int
|
||||
}
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &req)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = MoveNodes(req.NewParentID, req.NodeIDs)
|
||||
if err != nil {
|
||||
err = werr.Wrap(err)
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionType(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
typeID := 0
|
||||
typeIDStr := r.PathValue("typeID")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue