Connected nodes

This commit is contained in:
Magnus Åhall 2025-07-09 19:30:54 +02:00
parent 2b8472bcd1
commit ca0659a368
9 changed files with 581 additions and 89 deletions

View file

@ -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;
}

View file

@ -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
View 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 = '&nbsp;&nbsp;&nbsp;&nbsp;' + (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

View file

@ -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;
}
}