Rudimentary creating of nodes
This commit is contained in:
parent
08fd2cf4e9
commit
c0255fadb8
7 changed files with 363 additions and 55 deletions
BIN
datagraph
BIN
datagraph
Binary file not shown.
21
node.go
21
node.go
|
|
@ -165,3 +165,24 @@ func UpdateNode(nodeID int, data []byte) (err error) {
|
|||
_, err = db.Exec(`UPDATE public.node SET data=$2 WHERE id=$1`, nodeID, data)
|
||||
return
|
||||
}
|
||||
|
||||
func CreateNode(parentNodeID, typeID int, name string) (err error) {
|
||||
j, _ := json.Marshal(struct { Name string }{name})
|
||||
|
||||
row := db.QueryRow(`
|
||||
INSERT INTO node(type_id, name, data)
|
||||
VALUES($1, $2, $3::jsonb)
|
||||
RETURNING id
|
||||
`,
|
||||
typeID, name, j)
|
||||
|
||||
var id int
|
||||
err = row.Scan(&id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Exec(`INSERT INTO connection("parent", "child") VALUES($1, $2)`, parentNodeID, id)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,16 @@ body {
|
|||
[onClick] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
.page.show {
|
||||
display: block;
|
||||
}
|
||||
.section {
|
||||
background-color: #fff;
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
display: none;
|
||||
}
|
||||
.section.show {
|
||||
display: block;
|
||||
}
|
||||
#layout {
|
||||
display: grid;
|
||||
|
|
@ -43,7 +45,7 @@ body {
|
|||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
#menu.section {
|
||||
#menu.page {
|
||||
display: grid;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
|
@ -63,6 +65,8 @@ body {
|
|||
}
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
}
|
||||
#types {
|
||||
grid-area: navigation;
|
||||
|
|
@ -103,6 +107,12 @@ body {
|
|||
.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;
|
||||
}
|
||||
.node.expanded > .children {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -123,7 +133,7 @@ body {
|
|||
padding-right: 4px;
|
||||
}
|
||||
.node .type-icon img {
|
||||
filter: invert(0.7) sepia(0.5) hue-rotate(50deg) saturate(300%) brightness(0.85) !important;
|
||||
filter: invert(0.7) sepia(0.5) hue-rotate(50deg) saturate(300%) brightness(0.85);
|
||||
}
|
||||
.node .name {
|
||||
margin-bottom: 8px;
|
||||
|
|
@ -136,9 +146,33 @@ body {
|
|||
border-left: 1px solid #ccc;
|
||||
padding-left: 12px;
|
||||
margin-left: 19px;
|
||||
/*
|
||||
&.expanded {
|
||||
display: block;
|
||||
}
|
||||
*/
|
||||
}
|
||||
select {
|
||||
font-size: 1em;
|
||||
border: 1px solid #bcc3ce;
|
||||
background: #fff;
|
||||
color: #444;
|
||||
padding: 4px;
|
||||
}
|
||||
select optgroup {
|
||||
color: #a22;
|
||||
}
|
||||
datalist div:before {
|
||||
display: block;
|
||||
content: 'group';
|
||||
font-weight: bold;
|
||||
}
|
||||
dialog#create-type {
|
||||
min-width: 400px;
|
||||
}
|
||||
dialog#create-type > div {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
select:focus {
|
||||
outline: 2px solid #888;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,27 +7,39 @@ export class App {
|
|||
this.editor = null
|
||||
this.typesList = null
|
||||
this.currentNodeID = null
|
||||
this.types = []
|
||||
this.currentPage = null
|
||||
|
||||
const events = [
|
||||
'MENU_ITEM_SELECTED',
|
||||
'NODE_SELECTED',
|
||||
'EDITOR_NODE_SAVE',
|
||||
'TYPES_LIST_FETCHED',
|
||||
'NODE_CREATE_DIALOG',
|
||||
'NODE_CREATE',
|
||||
]
|
||||
for (const eventName of events)
|
||||
mbus.subscribe(eventName, event => this.eventHandler(event))
|
||||
|
||||
document.addEventListener('keydown', event => this.keyHandler(event))
|
||||
|
||||
mbus.dispatch('MENU_ITEM_SELECTED', 'node')
|
||||
}// }}}
|
||||
|
||||
eventHandler(event) {// {{{
|
||||
async eventHandler(event) {// {{{
|
||||
switch (event.type) {
|
||||
case 'MENU_ITEM_SELECTED':
|
||||
const item = document.querySelector(`#menu [data-section="${event.detail}"]`)
|
||||
this.section(item, event.detail)
|
||||
this.page(item, event.detail)
|
||||
break
|
||||
|
||||
case 'NODE_SELECTED':
|
||||
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')
|
||||
|
||||
this.currentNodeID = event.detail
|
||||
this.edit(this.currentNodeID)
|
||||
break
|
||||
|
|
@ -40,20 +52,49 @@ export class App {
|
|||
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':
|
||||
break
|
||||
|
||||
default:
|
||||
console.log(event)
|
||||
}
|
||||
}// }}}
|
||||
section(item, name) {// {{{
|
||||
keyHandler(event) {// {{{
|
||||
let handled = true
|
||||
|
||||
switch (event.key.toUpperCase()) {
|
||||
case 'N':
|
||||
if (!event.shiftKey || !event.altKey)
|
||||
break
|
||||
mbus.dispatch('NODE_CREATE_DIALOG')
|
||||
break
|
||||
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}// }}}
|
||||
page(item, name) {// {{{
|
||||
for (const el of document.querySelectorAll('#menu .item'))
|
||||
el.classList.remove('selected')
|
||||
item.classList.add('selected')
|
||||
|
||||
for (const el of document.querySelectorAll('.section.show'))
|
||||
for (const el of document.querySelectorAll('.page.show'))
|
||||
el.classList.remove('show')
|
||||
|
||||
this.currentPage = name
|
||||
|
||||
switch (name) {
|
||||
case 'node':
|
||||
document.getElementById('nodes').classList.add('show')
|
||||
|
|
@ -112,13 +153,126 @@ export class App {
|
|||
|
||||
const timePassed = Date.now() - buttonPressed
|
||||
if (timePassed < 250)
|
||||
setTimeout(()=>btn.disabled = false, 250 - timePassed)
|
||||
setTimeout(() => btn.disabled = false, 250 - timePassed)
|
||||
else
|
||||
btn.disabled = false
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
class NodeCreateDialog {
|
||||
constructor(parentNodeID) {// {{{
|
||||
this.parentNodeID = parentNodeID
|
||||
this.dialog = null
|
||||
this.types = null
|
||||
this.select = null
|
||||
this.input = null
|
||||
|
||||
this.createElements()
|
||||
|
||||
this.fetchTypes()
|
||||
.then(() => {
|
||||
const st = new SelectType(this.types)
|
||||
this.select.replaceChildren(st.render())
|
||||
})
|
||||
|
||||
this.dialog.showModal()
|
||||
this.select.focus()
|
||||
}// }}}
|
||||
createElements() {// {{{
|
||||
this.dialog = document.createElement('dialog')
|
||||
this.dialog.id = 'create-type'
|
||||
this.dialog.innerHTML = `
|
||||
<div style="padding: 16px">
|
||||
<select></select>
|
||||
<input type="text" placeholder="Name">
|
||||
<button onclick="mbus.dispatch('NODE_CREATE', ()=>this.commit())">Create</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
this.select = this.dialog.querySelector('select')
|
||||
this.input = this.dialog.querySelector('input')
|
||||
this.input.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter')
|
||||
this.commit()
|
||||
})
|
||||
|
||||
document.body.appendChild(this.dialog)
|
||||
}// }}}
|
||||
commit() {// {{{
|
||||
if (this.input.value.trim().length === 0) {
|
||||
alert('Give a name.')
|
||||
return
|
||||
}
|
||||
|
||||
const req = {
|
||||
ParentNodeID: this.parentNodeID,
|
||||
TypeID: parseInt(this.select.value),
|
||||
Name: this.input.value.trim(),
|
||||
}
|
||||
|
||||
fetch('/nodes/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req),
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
this.dialog.close()
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
async fetchTypes() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('/types/')
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
this.types = json.Types
|
||||
resolve()
|
||||
})
|
||||
.catch(err => reject(err))
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
|
||||
class SelectType {
|
||||
constructor(types) {// {{{
|
||||
this.types = types
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
const tmpl = document.createElement('template')
|
||||
|
||||
this.types.sort(typeSort)
|
||||
let prevGroup = null
|
||||
for (const t of this.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'])
|
||||
tmpl.content.appendChild(group)
|
||||
}
|
||||
|
||||
const opt = document.createElement('option')
|
||||
opt.setAttribute('value', t.ID)
|
||||
opt.innerText = t.Schema.title || t.Name
|
||||
tmpl.content.appendChild(opt)
|
||||
}
|
||||
|
||||
return tmpl.content
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class TreeNode {
|
||||
constructor(parent, data) {// {{{
|
||||
this.data = data
|
||||
|
|
@ -131,7 +285,7 @@ export class TreeNode {
|
|||
|
||||
render() {// {{{
|
||||
const nodeHTML = `
|
||||
<div class="node">
|
||||
<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>
|
||||
|
|
@ -246,21 +400,7 @@ export class TypesList {
|
|||
render() {// {{{
|
||||
const div = document.createElement('div')
|
||||
|
||||
this.types.sort((a,b)=> {
|
||||
if (a.Schema['x-group'] === undefined)
|
||||
a.Schema['x-group'] = 'No group'
|
||||
|
||||
if (b.Schema['x-group'] === undefined)
|
||||
b.Schema['x-group'] = 'No group'
|
||||
|
||||
if (a.Schema['x-group'] < b.Schema['x-group']) return -1
|
||||
if (a.Schema['x-group'] > b.Schema['x-group']) return 1
|
||||
|
||||
if ((a.Schema.title || a.Name) < (b.Schema.title || b.Name)) return -1
|
||||
if ((a.Schema.title || a.Name) > (b.Schema.title || b.Name)) return 1
|
||||
|
||||
return 0
|
||||
})
|
||||
this.types.sort(typeSort)
|
||||
|
||||
let prevGroup = null
|
||||
|
||||
|
|
@ -290,4 +430,20 @@ export class TypesList {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
function typeSort(a, b) {// {{{
|
||||
if (a.Schema['x-group'] === undefined)
|
||||
a.Schema['x-group'] = 'No group'
|
||||
|
||||
if (b.Schema['x-group'] === undefined)
|
||||
b.Schema['x-group'] = 'No group'
|
||||
|
||||
if (a.Schema['x-group'] < b.Schema['x-group']) return -1
|
||||
if (a.Schema['x-group'] > b.Schema['x-group']) return 1
|
||||
|
||||
if ((a.Schema.title || a.Name) < (b.Schema.title || b.Name)) return -1
|
||||
if ((a.Schema.title || a.Name) > (b.Schema.title || b.Name)) return 1
|
||||
|
||||
return 0
|
||||
}// }}}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ body {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
.page {
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
|
|
@ -37,6 +34,12 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
|
|
@ -54,7 +57,7 @@ body {
|
|||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
&.section {
|
||||
&.page {
|
||||
display: grid;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
|
@ -83,6 +86,8 @@ body {
|
|||
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
}
|
||||
|
||||
#types {
|
||||
|
|
@ -134,6 +139,16 @@ body {
|
|||
height: 24px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
& > .name {
|
||||
color: #a02c2c;
|
||||
}
|
||||
|
||||
& > .type-icon {
|
||||
filter: invert(.7) sepia(.5) hue-rotate(0deg) saturate(750%) brightness(0.85) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
&>.children {
|
||||
display: block;
|
||||
|
|
@ -160,7 +175,7 @@ body {
|
|||
padding-right: 4px;
|
||||
|
||||
img {
|
||||
filter: invert(.7) sepia(.5) hue-rotate(50deg) saturate(300%) brightness(0.85) !important;
|
||||
filter: invert(.7) sepia(.5) hue-rotate(50deg) saturate(300%) brightness(0.85);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,11 +191,45 @@ body {
|
|||
border-left: 1px solid #ccc;
|
||||
padding-left: 12px;
|
||||
margin-left: 19px;
|
||||
|
||||
/*
|
||||
&.expanded {
|
||||
display: block;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
font-size: 1em;
|
||||
border: 1px solid #bcc3ce;
|
||||
background: #fff;
|
||||
color: #444;
|
||||
padding: 4px;
|
||||
|
||||
optgroup {
|
||||
color: #a22;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
datalist {
|
||||
div:before {
|
||||
display: block;
|
||||
content: 'group';
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
dialog#create-type {
|
||||
min-width: 400px;
|
||||
|
||||
& > div {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: 2px solid #888;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,22 +31,33 @@
|
|||
</script>
|
||||
|
||||
<div id="layout">
|
||||
<div class="section" id="menu">
|
||||
<div class="page section" id="menu">
|
||||
<div class="item" id="logo"><img src="/images/{{ .VERSION }}/logo.svg" /></div>
|
||||
<div class="item" data-section='node' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'node')">Nodes</div>
|
||||
<div class="item" data-section='type' onclick="mbus.dispatch('MENU_ITEM_SELECTED', 'type')">Types</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="nodes"></div>
|
||||
<div class="section" id="editor-node">
|
||||
<div class="editor"></div>
|
||||
<div class="controls">
|
||||
<button onclick="mbus.dispatch('EDITOR_NODE_SAVE')">Save</button>
|
||||
<div class="page section" id="nodes"></div>
|
||||
<div class="page" id="editor-node">
|
||||
<div class="section">
|
||||
<img onclick="mbus.dispatch('NODE_CREATE_DIALOG')" src="/images/{{ .VERSION }}/node_modules/@mdi/svg/svg/plus-box.svg" style="display: block; height: 32px" />
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="editor"></div>
|
||||
<div class="controls">
|
||||
<button onclick="mbus.dispatch('EDITOR_NODE_SAVE')">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<b>References</b>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="section" id="types"></div>
|
||||
<div class="section" id="editor-type-schema"></div>
|
||||
<div class="page section" id="types"></div>
|
||||
<div class="page section" id="editor-type-schema"></div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="/css/{{ .VERSION }}/spectre.min.css">
|
||||
|
|
|
|||
43
webserver.go
43
webserver.go
|
|
@ -34,6 +34,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/create", actionNodeCreate)
|
||||
http.HandleFunc("/types/{typeID}", actionType)
|
||||
http.HandleFunc("/types/", actionTypesAll)
|
||||
|
||||
|
|
@ -62,7 +63,16 @@ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
} // }}}
|
||||
func pageApp(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
page := NewPage("app")
|
||||
err := engine.Render(page, w, r)
|
||||
|
||||
ts, err := GetTypes()
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
page.Data["Types"] = ts
|
||||
|
||||
err = engine.Render(page, w, r)
|
||||
if err != nil {
|
||||
w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
|
@ -132,7 +142,34 @@ func actionNodeUpdate(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
j, _ := json.Marshal(out)
|
||||
w.Write(j)
|
||||
} // }}}
|
||||
func actionNodeCreate(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
var req struct {
|
||||
ParentNodeID int
|
||||
TypeID int
|
||||
Name string
|
||||
}
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &req)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = CreateNode(req.ParentNodeID, req.TypeID, req.Name)
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
}{
|
||||
true,
|
||||
}
|
||||
|
|
@ -161,7 +198,7 @@ func actionTypesAll(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
}
|
||||
|
||||
out := struct {
|
||||
OK bool
|
||||
OK bool
|
||||
Types []NodeType
|
||||
}{
|
||||
true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue