First steps to creating a new node

This commit is contained in:
Magnus Åhall 2025-06-28 09:13:26 +02:00
parent 1ce8e29e37
commit 989542be91
6 changed files with 101 additions and 44 deletions

10
main.go
View file

@ -334,9 +334,15 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
return return
} }
db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData) _, err = db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
if err != nil {
Log.Error("sync", "error", err)
httpError(w, err)
return
}
responseData(w, map[string]interface{}{
responseData(w, map[string]any{
"OK": true, "OK": true,
}) })
} // }}} } // }}}

View file

@ -1,6 +1,6 @@
import { ROOT_NODE } from 'node_store' import { ROOT_NODE } from 'node_store'
import { TreeNative } from 'tree' import { TreeNative } from 'tree'
import { NodeUINative } from 'node' import { NodeUINative, Node } from 'node'
export class App { export class App {
constructor() {// {{{ constructor() {// {{{
@ -29,6 +29,10 @@ export class App {
}) })
window.addEventListener('keydown', event => this.keyHandler(event)) window.addEventListener('keydown', event => this.keyHandler(event))
document.getElementById('notes2').addEventListener('click', event => {
if (event.target.id === 'notes2')
document.getElementById('node-content')?.focus()
})
window._sync = new Sync() window._sync = new Sync()
window._sync.run() window._sync.run()
@ -45,7 +49,6 @@ export class App {
switch (event.key.toUpperCase()) { switch (event.key.toUpperCase()) {
case 'T': case 'T':
console.log(document.activeElement.id)
if (document.activeElement.id === 'tree-nodes') if (document.activeElement.id === 'tree-nodes')
document.getElementById('node-content').focus() document.getElementById('node-content').focus()
else else
@ -68,10 +71,12 @@ export class App {
this.toggleMarkdown() this.toggleMarkdown()
break break
*/
case 'N': case 'N':
this.createNode() this.createNode()
break break
/*
case 'P': case 'P':
this.showPage('node-properties') this.showPage('node-properties')
break break
@ -145,6 +150,18 @@ export class App {
await Promise.all([history, sendQueue, nodeStoreAdding]) await Promise.all([history, sendQueue, nodeStoreAdding])
}//}}} }//}}}
async createNode() {//{{{
let name = prompt("Name")
if (!name)
return
const nn = Node.create(name, this.currentNode.UUID)
nn.save()
nodeStore.sendQueue.add(nn)
nodeStore.add([nn])
}//}}}
async goToNode(nodeUUID, dontPush, dontExpand) {//{{{ async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
if (nodeUUID === null || nodeUUID === undefined) if (nodeUUID === null || nodeUUID === undefined)
return return
@ -220,38 +237,37 @@ class Crumb {
}// }}} }// }}}
} }
function tmpl(html) {// {{{
const el = document.createElement('template')
el.innerHTML = html
return el.content.children
}// }}}
class Op { class Op {
constructor(id) { constructor(id) {// {{{
this.id = id this.id = id
_mbus.subscribe(this.id, p => this.render(p)) _mbus.subscribe(this.id, p => this.render(p))
} }// }}}
render(html) { render(html) {// {{{
const op = document.getElementById('op') const op = document.getElementById('op')
const t = document.createElement('template') const t = document.createElement('template')
t.innerHTML = `<dialog id="${this.id}" class="op">${html}</dialog>` t.innerHTML = `<dialog id="${this.id}" class="op">${html}</dialog>`
op.replaceChildren(t.content) op.replaceChildren(t.content)
document.getElementById(this.id).showModal() document.getElementById(this.id).showModal()
} }// }}}
get(selector) { get(selector) {// {{{
return document.querySelector(`#${this.id} ${selector}`) return document.querySelector(`#${this.id} ${selector}`)
} }// }}}
bind(selector, event, fn) { bind(selector, event, fn) {// {{{
this.get(selector).addEventListener(event, evt => fn(evt)) this.get(selector).addEventListener(event, evt => fn(evt))
} }// }}}
}
function tmpl(html) {
const el = document.createElement('template')
el.innerHTML = html
return el.content.children
} }
class OpSearch extends Op { class OpSearch extends Op {
constructor() { constructor() {// {{{
super('op-search') super('op-search')
} }// }}}
render() {// {{{
render() {
super.render(` super.render(`
<div class="header">Search</div> <div class="header">Search</div>
<div> <div>
@ -262,18 +278,16 @@ class OpSearch extends Op {
`) `)
this.bind('input[type="text"]', 'keydown', evt => this.search(evt)) this.bind('input[type="text"]', 'keydown', evt => this.search(evt))
} }// }}}
search(event) {// {{{
search(event) {
if (event.key !== 'Enter') if (event.key !== 'Enter')
return return
const searchFor = document.querySelector('#op-search input').value const searchFor = document.querySelector('#op-search input').value
nodeStore.search(searchFor, ROOT_NODE) nodeStore.search(searchFor, ROOT_NODE)
.then(res => this.displayResults(res)) .then(res => this.displayResults(res))
} }// }}}
displayResults(results) {// {{{
displayResults(results) {
const rs = [] const rs = []
for (const r of results) { for (const r of results) {
const ancestors = r.ancestry.reverse().map(a => { const ancestors = r.ancestry.reverse().map(a => {
@ -292,7 +306,7 @@ class OpSearch extends Op {
rs.push(ancDev[0]) rs.push(ancDev[0])
} }
this.get('.results').replaceChildren(...rs) this.get('.results').replaceChildren(...rs)
} }// }}}
} }
// vim: foldmethod=marker // vim: foldmethod=marker

View file

@ -392,7 +392,20 @@ export class Node {
if (a.data.Name > b.data.Name) return 0 if (a.data.Name > b.data.Name) return 0
return 0 return 0
}//}}} }//}}}
static create(name, parentUUID) {
return new Node({
UUID: uuidv7(),
Created: (new Date()).toISOString(),
Content: '',
Name: name,
ParentUUID: parentUUID,
Markdown: false,
History: false,
})
}
constructor(nodeData, level) {//{{{ constructor(nodeData, level) {//{{{
this.Level = level this.Level = level
this.data = nodeData this.data = nodeData
this.UUID = nodeData.UUID this.UUID = nodeData.UUID
@ -525,4 +538,30 @@ export class Node {
}//}}} }//}}}
} }
function uuidv7() {
// random bytes
const value = new Uint8Array(16)
crypto.getRandomValues(value)
// current timestamp in ms
const timestamp = BigInt(Date.now())
// timestamp
value[0] = Number((timestamp >> 40n) & 0xffn)
value[1] = Number((timestamp >> 32n) & 0xffn)
value[2] = Number((timestamp >> 24n) & 0xffn)
value[3] = Number((timestamp >> 16n) & 0xffn)
value[4] = Number((timestamp >> 8n) & 0xffn)
value[5] = Number(timestamp & 0xffn)
// version and variant
value[6] = (value[6] & 0x0f) | 0x70
value[8] = (value[8] & 0x3f) | 0x80
const str = Array.from(value)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
}
// vim: foldmethod=marker // vim: foldmethod=marker

View file

@ -159,6 +159,7 @@ export class NodeStore {
}) })
}//}}} }//}}}
/* TODO - Remove?
async storeNode(node) {//{{{ async storeNode(node) {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite') const t = this.db.transaction('nodes', 'readwrite')
@ -181,6 +182,7 @@ export class NodeStore {
} }
}) })
}//}}} }//}}}
*/
async upsertNodeRecords(records) {//{{{ async upsertNodeRecords(records) {//{{{
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -149,7 +149,7 @@ export class Sync {
const res = await API.query('POST', '/sync/to_server', request) const res = await API.query('POST', '/sync/to_server', request)
if (!res.OK) { if (!res.OK) {
// TODO - implement better error management here. // TODO - implement better error management here.
console.log(res) console.error(res)
alert(res) alert(res)
return return
} }

View file

@ -19,18 +19,16 @@ export class TreeNative {
<div id="tree-nodes" tabindex=0> <div id="tree-nodes" tabindex=0>
<div id="logo"><img src="/images/${_VERSION}/logo.svg" /></div> <div id="logo"><img src="/images/${_VERSION}/logo.svg" /></div>
<div class="icons"> <div class="icons">
<img src="/images/${_VERSION}/icon_search.svg" style="height: 22px" /> <img class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
<img src="/images/${_VERSION}/icon_refresh.svg" /> <img class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
</div> </div>
<div>` <div>`
/*
onclick=${() => _mbus.dispatch('op-search')}
onclick=${() => _sync.run()}
*/
const treeEl = tmpl.content.getElementById('tree-nodes') const treeEl = tmpl.content.getElementById('tree-nodes')
treeEl.addEventListener('keydown', event=>this.keyHandler(event)) treeEl.addEventListener('keydown', event=>this.keyHandler(event))
tmpl.content.querySelector('.icons .search').addEventListener('click', ()=>_mbus.dispatch('op-search'))
tmpl.content.querySelector('.icons .sync').addEventListener('click', ()=>_sync.run())
tmpl.content.getElementById('logo').addEventListener('click', ()=>_app.goToNode(ROOT_NODE, false, false)) tmpl.content.getElementById('logo').addEventListener('click', ()=>_app.goToNode(ROOT_NODE, false, false))
@ -38,8 +36,6 @@ export class TreeNative {
const treenode = new TreeNodeNative(this, node) const treenode = new TreeNodeNative(this, node)
this.treeNodeComponents[node.UUID] = treenode this.treeNodeComponents[node.UUID] = treenode
treeEl.appendChild(treenode.render()) treeEl.appendChild(treenode.render())
//return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />`
} }
this.rendered = true this.rendered = true