Connected nodes
This commit is contained in:
parent
2b8472bcd1
commit
ca0659a368
9 changed files with 581 additions and 89 deletions
|
|
@ -229,6 +229,10 @@ select:focus {
|
|||
font-weight: bold;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
#connected-nodes .connected-nodes .type-group .type-icon,
|
||||
#connected-nodes .connected-nodes .type-group .node-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
#connected-nodes .type-icon img {
|
||||
display: block;
|
||||
height: 24px;
|
||||
|
|
@ -276,3 +280,23 @@ select:focus {
|
|||
cursor: pointer;
|
||||
height: 24px;
|
||||
}
|
||||
dialog#connection-data {
|
||||
padding: 24px;
|
||||
}
|
||||
dialog#connection-data .label {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
color: var(--section-color);
|
||||
}
|
||||
dialog#connection-data img {
|
||||
height: 32px;
|
||||
}
|
||||
dialog#connection-data textarea {
|
||||
margin-top: 16px;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
dialog#connection-data div.button {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Editor } from '@editor'
|
||||
import { MessageBus } from '@mbus'
|
||||
import { SelectType, SelectNode } from '@select_node'
|
||||
import { SelectType, SelectNodeDialog, ConnectionDataDialog } from '@select_node'
|
||||
|
||||
export class App {
|
||||
constructor() {// {{{
|
||||
|
|
@ -41,9 +41,9 @@ export class App {
|
|||
break
|
||||
|
||||
case 'NODE_CONNECT':
|
||||
const selectnode = new SelectNode(selectedNode => {
|
||||
const selectnode = new SelectNodeDialog(selectedNode => {
|
||||
this.nodeConnect(this.currentNode, selectedNode)
|
||||
.then(() => this.edit(this.currentNode))
|
||||
.then(() => this.edit(this.currentNode.ID))
|
||||
})
|
||||
selectnode.render()
|
||||
break
|
||||
|
|
@ -202,23 +202,11 @@ export class App {
|
|||
// 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'
|
||||
})
|
||||
.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)
|
||||
const connectedNodes = new ConnectedNodes(json.Node.ConnectedNodes)
|
||||
document.getElementById('connected-nodes').replaceChildren(connectedNodes.render())
|
||||
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
|
||||
}// }}}
|
||||
async nodeRename(nodeID, name) {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -318,12 +306,28 @@ export class App {
|
|||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
async nodeConnect(parentNode, nodeToConnect) {
|
||||
return new Promise((resolve, reject)=>{
|
||||
// XXX - here
|
||||
//fetch('/nodes/)
|
||||
async nodeConnect(parentNode, nodeToConnect) {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = {
|
||||
ParentNodeID: parentNode.ID,
|
||||
ChildNodeID: nodeToConnect.ID,
|
||||
}
|
||||
|
||||
fetch(`/nodes/connect`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req),
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}
|
||||
}// }}}
|
||||
typeSort(a, b) {// {{{
|
||||
if (a.Schema['x-group'] === undefined)
|
||||
a.Schema['x-group'] = 'No group'
|
||||
|
|
@ -763,6 +767,12 @@ class ConnectedNode {
|
|||
<div class="type-icon"><img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.node.TypeIcon}.svg" /></div>
|
||||
<div class="node-name">${this.node.Name}</div>
|
||||
`
|
||||
|
||||
for (const el of tmpl.content.children) {
|
||||
el.addEventListener('click', () => {
|
||||
new ConnectionDataDialog(this.node, () => _app.edit(_app.currentNode.ID)).render()
|
||||
})
|
||||
}
|
||||
return tmpl.content
|
||||
}// }}}
|
||||
}
|
||||
|
|
|
|||
256
static/js/select_node.mjs
Normal file
256
static/js/select_node.mjs
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
export class SelectNodeDialog {
|
||||
constructor(callback) {// {{{
|
||||
this.selectType = new SelectType
|
||||
this.searchResults = null
|
||||
this.searchText = null
|
||||
this.selectType = null
|
||||
this.nodeTable = null
|
||||
this.moreExist = null
|
||||
|
||||
if (callback !== undefined)
|
||||
this.callback = callback
|
||||
else
|
||||
this.callback = () => { }
|
||||
}// }}}
|
||||
async render() {// {{{
|
||||
const dlg = document.createElement('dialog')
|
||||
dlg.id = 'select-node'
|
||||
dlg.addEventListener('close', () => dlg.remove())
|
||||
|
||||
dlg.innerHTML = `
|
||||
<div class="label">Search for node</div>
|
||||
<input class="search-text" type="text" placeholder="Search" />
|
||||
<select></select>
|
||||
<div style="display: grid; grid-template-columns: min-content 1fr; align-items: center; grid-gap: 16px;">
|
||||
<button>Search</button>
|
||||
<div class="more-exist"></div>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
`
|
||||
|
||||
this.nodeTable = new NodeTable((_node, node) => {
|
||||
this.callback(node)
|
||||
dlg.close()
|
||||
})
|
||||
|
||||
this.searchText = dlg.querySelector('.search-text')
|
||||
this.searchResults = dlg.querySelector('.search-results')
|
||||
this.moreExist = dlg.querySelector('.more-exist')
|
||||
const button = dlg.querySelector('button')
|
||||
button.addEventListener('click', () => this.search())
|
||||
|
||||
this.searchText.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter')
|
||||
this.search()
|
||||
})
|
||||
this.searchText.focus()
|
||||
this.searchText.value = '%'
|
||||
|
||||
new SelectType(true).render()
|
||||
.then(select => {
|
||||
this.selectType = select
|
||||
dlg.querySelector('select').replaceWith(this.selectType)
|
||||
})
|
||||
|
||||
document.body.appendChild(dlg)
|
||||
dlg.showModal()
|
||||
}// }}}
|
||||
search() {// {{{
|
||||
const type_id = this.selectType.value
|
||||
const search = this.searchText.value
|
||||
this.moreExist.innerText = ''
|
||||
|
||||
fetch(`/nodes/search?` + new URLSearchParams({ type_id, search }))
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
this.nodeTable.clearNodes()
|
||||
this.nodeTable.addNodes(json.Nodes)
|
||||
this.searchResults.replaceChildren(this.nodeTable.render())
|
||||
|
||||
if (json.MoreExistThan > 0)
|
||||
this.moreExist.innerText = `Only displaying ${json.MoreExistThan} nodes. There are more matching the given criteria.`
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class SelectType {
|
||||
constructor(allowNoType) {// {{{
|
||||
this.allowNoType = allowNoType
|
||||
}// }}}
|
||||
async render() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
this.fetchTypes()
|
||||
.then(types => {
|
||||
const select = document.createElement('select')
|
||||
|
||||
if (this.allowNoType) {
|
||||
const option = document.createElement('option')
|
||||
option.setAttribute('value', -1)
|
||||
option.innerText = '[ No specific type ]'
|
||||
select.appendChild(option)
|
||||
}
|
||||
|
||||
types.sort(_app.typeSort)
|
||||
let prevGroup = null
|
||||
for (const t of types) {
|
||||
if (t.Name == 'root_node')
|
||||
continue
|
||||
|
||||
if (t.Schema['x-group'] != prevGroup) {
|
||||
prevGroup = t.Schema['x-group']
|
||||
const group = document.createElement('optgroup')
|
||||
group.setAttribute('label', t.Schema['x-group'])
|
||||
select.appendChild(group)
|
||||
}
|
||||
|
||||
const opt = document.createElement('option')
|
||||
opt.setAttribute('value', t.ID)
|
||||
opt.innerHTML = ' ' + (t.Schema.title || t.Name)
|
||||
select.appendChild(opt)
|
||||
}
|
||||
|
||||
resolve(select)
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
async fetchTypes() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/types/')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
resolve(json.Types)
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class NodeTable {
|
||||
constructor(callback) {// {{{
|
||||
this.nodes = new Map()
|
||||
|
||||
if (callback !== undefined)
|
||||
this.callback = callback
|
||||
else
|
||||
this.callback = () => { }
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('node-table')
|
||||
|
||||
for (const k of Array.from(this.nodes.keys())) {
|
||||
const group = document.createElement('div')
|
||||
group.classList.add('group')
|
||||
group.innerHTML = `
|
||||
<div class="label">${k}</div>
|
||||
<div class="children"></div>
|
||||
`
|
||||
|
||||
const groupChildren = group.querySelector('.children')
|
||||
for (const n of this.nodes.get(k)) {
|
||||
const icon = document.createElement('img')
|
||||
icon.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${n.TypeIcon}.svg`)
|
||||
icon.addEventListener('click', event => this.callback(event, n))
|
||||
|
||||
const node = document.createElement('div')
|
||||
node.classList.add('node')
|
||||
node.innerText = n.Name
|
||||
node.addEventListener('click', event => this.callback(event, n))
|
||||
|
||||
groupChildren.appendChild(icon)
|
||||
groupChildren.appendChild(node)
|
||||
}
|
||||
|
||||
div.appendChild(group)
|
||||
}
|
||||
|
||||
return div
|
||||
}// }}}
|
||||
clearNodes() {// {{{
|
||||
this.nodes = new Map()
|
||||
}// }}}
|
||||
addNodes(nodes) {// {{{
|
||||
for (const n of nodes) {
|
||||
let tableNodes = this.nodes.get(n.TypeSchema.title)
|
||||
if (tableNodes === undefined) {
|
||||
tableNodes = []
|
||||
this.nodes.set(n.TypeSchema.title, tableNodes)
|
||||
}
|
||||
tableNodes.push(n)
|
||||
}
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class ConnectionDataDialog {
|
||||
constructor(node, callback) {// {{{
|
||||
this.node = node
|
||||
this.callback = callback
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const dlg = document.createElement('dialog')
|
||||
dlg.id = 'connection-data'
|
||||
dlg.addEventListener('close', () => dlg.remove())
|
||||
|
||||
dlg.innerHTML = `
|
||||
<div>
|
||||
<div style="float: left;" class="label">Connection data</div>
|
||||
<div style="float: right;"><img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/trash-can.svg" /></div>
|
||||
</div>
|
||||
<div style="clear: both;"><b>${this.node.Name}</b></div>
|
||||
<div><textarea></textarea></div>
|
||||
<div class="button"><button>Update</button></div>
|
||||
`
|
||||
dlg.querySelector('textarea').value = JSON.stringify(this.node.ConnectionData, null, 4)
|
||||
|
||||
dlg.querySelector('img').addEventListener('click', ()=>{
|
||||
if(!confirm('Do you want to delete the connection?'))
|
||||
return
|
||||
|
||||
fetch(`/connection/delete/${this.node.ConnectionID}`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
dlg.close()
|
||||
this.callback()
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
})
|
||||
|
||||
dlg.querySelector('button').addEventListener('click', () => {
|
||||
// Connection data is updated.
|
||||
fetch(`/connection/update/${this.node.ConnectionID}`, {
|
||||
method: 'POST',
|
||||
body: dlg.querySelector('textarea').value,
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
dlg.close()
|
||||
this.callback()
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
})
|
||||
|
||||
document.body.appendChild(dlg)
|
||||
dlg.showModal()
|
||||
}// }}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
@ -304,6 +304,10 @@ select:focus {
|
|||
font-weight: bold;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.type-icon, .node-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,3 +372,28 @@ select:focus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog#connection-data {
|
||||
padding: 24px;
|
||||
|
||||
.label {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
color: var(--section-color);
|
||||
}
|
||||
|
||||
img {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-top: 16px;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
div.button {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue