diff --git a/node.go b/node.go
index d44b82b..e121410 100644
--- a/node.go
+++ b/node.go
@@ -275,5 +275,66 @@ func GetNodeConnections(nodeID int) (connections []Node, err error) { // {{{
return
} // }}}
+func SearchNodes(typeID int, search string, maxResults int) (nodes []Node, err error) { // {{{
+ nodes = []Node{}
+
+ var rows *sqlx.Rows
+ rows, err = db.Queryx(`
+ SELECT
+ n.id,
+ n.name,
+ n.updated,
+ n.data,
+ COALESCE(n.parent_id, 0) AS parent_id,
+
+ t.id AS type_id,
+ t.name AS type_name,
+ t.schema AS type_schema_raw,
+ t.schema->>'icon' AS type_icon
+ FROM public.node n
+ INNER JOIN public.type t ON n.type_id = t.id
+ WHERE
+ n.id > 0 AND
+ n.name ILIKE $2 AND
+ (CASE
+ WHEN $1 = -1 THEN true
+ ELSE
+ type_id = $1
+ END)
+
+ ORDER BY
+ t.schema->>'title' ASC,
+ UPPER(n.name) ASC
+ LIMIT $3
+ `,
+ typeID,
+ search,
+ maxResults+1,
+ )
+ if err != nil {
+ err = werr.Wrap(err)
+ return
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var node Node
+ err = rows.StructScan(&node)
+ if err != nil {
+ err = werr.Wrap(err)
+ return
+ }
+
+ err = json.Unmarshal(node.TypeSchemaRaw, &node.TypeSchema)
+ if err != nil {
+ err = werr.Wrap(err)
+ return
+ }
+
+ nodes = append(nodes, node)
+ }
+
+ return
+} // }}}
// vim: foldmethod=marker
diff --git a/static/css/main.css b/static/css/main.css
index b531d85..978bd2d 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -30,6 +30,9 @@ body {
[onClick] {
cursor: pointer;
}
+button {
+ padding: 4px 8px;
+}
.page {
display: none;
}
@@ -199,10 +202,16 @@ select:focus {
outline-offset: -2px;
}
#connected-nodes > .label {
- margin-bottom: 16px;
color: var(--section-color);
font-weight: bold;
font-size: 1.25em;
+ margin-bottom: 8px;
+}
+#connected-nodes > .add {
+ margin-bottom: 8px;
+}
+#connected-nodes > .add img {
+ height: 24px;
}
#connected-nodes .connected-nodes {
display: flex;
@@ -224,3 +233,46 @@ select:focus {
display: block;
height: 24px;
}
+#select-node {
+ padding: 32px;
+ display: grid;
+ grid-template-columns: min-content;
+ grid-gap: 8px;
+ justify-items: start;
+}
+#select-node > * {
+ min-width: 300px;
+ width: 100%;
+}
+#select-node button {
+ width: 100px !important;
+}
+#select-node .more-exist {
+ color: #a44;
+}
+#select-node .search-results .node-table {
+ display: flex;
+ align-items: start;
+ gap: 32px;
+}
+#select-node .search-results .node-table .group {
+ margin-top: 16px;
+}
+#select-node .search-results .node-table .group .label {
+ font-weight: bold;
+ color: var(--section-color);
+ margin-bottom: 8px;
+ white-space: nowrap;
+}
+#select-node .search-results .node-table .group .children {
+ display: grid;
+ grid-template-columns: min-content 1fr;
+ grid-gap: 8px;
+}
+#select-node .search-results .node-table .group .children .node {
+ cursor: pointer;
+}
+#select-node .search-results .node-table .group .children img {
+ cursor: pointer;
+ height: 24px;
+}
diff --git a/static/js/app.mjs b/static/js/app.mjs
index 3657f7a..44da61f 100644
--- a/static/js/app.mjs
+++ b/static/js/app.mjs
@@ -1,5 +1,6 @@
import { Editor } from '@editor'
import { MessageBus } from '@mbus'
+import { SelectType, SelectNode } from '@select_node'
export class App {
constructor() {// {{{
@@ -15,6 +16,7 @@ export class App {
const events = [
'EDITOR_NODE_SAVE',
'MENU_ITEM_SELECTED',
+ 'NODE_CONNECT',
'NODE_CREATE_DIALOG',
'NODE_DELETE',
'NODE_EDIT_NAME',
@@ -38,6 +40,14 @@ export class App {
this.page(item, event.detail)
break
+ case 'NODE_CONNECT':
+ const selectnode = new SelectNode(selectedNode => {
+ this.nodeConnect(this.currentNode, selectedNode)
+ .then(() => this.edit(this.currentNode))
+ })
+ selectnode.render()
+ break
+
case 'NODE_SELECTED':
for (const n of document.querySelectorAll('#nodes .node.selected'))
n.classList.remove('selected')
@@ -147,6 +157,7 @@ export class App {
case 'type':
document.getElementById('types').classList.add('show')
document.getElementById('editor-type-schema').classList.add('show')
+ document.getElementById('editor-node').style.display = 'none'
if (this.typesList === null)
this.typesList = new TypesList()
@@ -201,7 +212,7 @@ export class App {
showError(err)
return
}
-
+
const connectedNodes = new ConnectedNodes(json.Nodes)
document.getElementById('connected-nodes').replaceChildren(connectedNodes.render())
@@ -307,6 +318,27 @@ export class App {
})
.catch(err => showError(err))
}// }}}
+ async nodeConnect(parentNode, nodeToConnect) {
+ return new Promise((resolve, reject)=>{
+ // XXX - here
+ //fetch('/nodes/)
+ })
+ }
+ 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
+ }// }}}
}
class NodeCreateDialog {
@@ -319,18 +351,13 @@ class NodeCreateDialog {
this.createElements()
- this.fetchTypes()
- .then(() => {
- const st = new SelectType(this.types)
- this.select.replaceChildren(st.render())
- })
-
this.dialog.showModal()
- this.select.focus()
+ //this.select.focus()
}// }}}
createElements() {// {{{
this.dialog = document.createElement('dialog')
this.dialog.id = 'create-type'
+ this.dialog.addEventListener('close', () => this.dialog.remove())
this.dialog.innerHTML = `
@@ -339,6 +366,13 @@ class NodeCreateDialog {
`
+ new SelectType().render()
+ .then(select => {
+ this.select = select
+ this.dialog.querySelector('select').replaceWith(this.select)
+ this.select.focus()
+ })
+
this.dialog.querySelector('button').addEventListener('click', () => this.commit())
this.select = this.dialog.querySelector('select')
this.input = this.dialog.querySelector('input')
@@ -379,51 +413,6 @@ class NodeCreateDialog {
})
.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 Tree {
@@ -696,7 +685,7 @@ export class TypesList {
render() {// {{{
const div = document.createElement('div')
- this.types.sort(typeSort)
+ this.types.sort(_app.typeSort)
let prevGroup = null
@@ -726,33 +715,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
-}// }}}
-
class ConnectedNodes {
- constructor(nodes) {
+ constructor(nodes) {// {{{
this.nodes = nodes
- }
- render() {
+ }// }}}
+ render() {// {{{
const div = document.createElement('template')
div.innerHTML = `
Connected nodes
+
`
+ div.content.querySelector('.add').addEventListener('click', () => mbus.dispatch('NODE_CONNECT'))
+
const types = new Map()
for (const n of this.nodes) {
let typeGroup = types.get(n.TypeSchema.title)
@@ -774,23 +750,21 @@ class ConnectedNodes {
}
return div.content
- }
+ }// }}}
}
-
-
class ConnectedNode {
- constructor(node) {
+ constructor(node) {// {{{
this.node = node
- }
- render() {
+ }// }}}
+ render() {// {{{
const tmpl = document.createElement('template')
tmpl.innerHTML = `
${this.node.Name}
`
return tmpl.content
- }
+ }// }}}
}
// vim: foldmethod=marker
diff --git a/static/less/main.less b/static/less/main.less
index c594876..3cac2e8 100644
--- a/static/less/main.less
+++ b/static/less/main.less
@@ -38,6 +38,10 @@ body {
cursor: pointer;
}
+button {
+ padding: 4px 8px;
+}
+
.page {
display: none;
@@ -270,10 +274,18 @@ select:focus {
#connected-nodes {
& > .label {
- margin-bottom: 16px;
color: var(--section-color);
font-weight: bold;
font-size: 1.25em;
+ margin-bottom: 8px;
+ }
+
+ & > .add {
+ margin-bottom: 8px;
+
+ img {
+ height: 24px;
+ }
}
.connected-nodes {
@@ -302,3 +314,57 @@ select:focus {
}
}
}
+
+#select-node {
+ padding: 32px;
+ display: grid;
+ grid-template-columns: min-content;
+ grid-gap: 8px;
+ justify-items: start;
+
+ & > * {
+ min-width: 300px;
+ width: 100%;
+ }
+
+ button {
+ width: 100px !important;
+ }
+
+ .more-exist {
+ color: #a44;
+ }
+
+ .search-results {
+ .node-table {
+ display: flex;
+ align-items: start;
+ gap: 32px;
+
+ .group {
+ margin-top: 16px;
+ .label {
+ font-weight: bold;
+ color: var(--section-color);
+ margin-bottom: 8px;
+ white-space: nowrap;
+ }
+
+ .children {
+ display: grid;
+ grid-template-columns: min-content 1fr;
+ grid-gap: 8px;
+
+ .node {
+ cursor: pointer;
+ }
+
+ img {
+ cursor: pointer;
+ height: 24px;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/views/pages/app.gotmpl b/views/pages/app.gotmpl
index 34b2514..fbaf4b9 100644
--- a/views/pages/app.gotmpl
+++ b/views/pages/app.gotmpl
@@ -4,8 +4,9 @@
diff --git a/webserver.go b/webserver.go
index 59af967..5e283f7 100644
--- a/webserver.go
+++ b/webserver.go
@@ -41,6 +41,7 @@ func initWebserver() (err error) {
http.HandleFunc("/nodes/connections/{nodeID}", actionNodeConnections)
http.HandleFunc("/nodes/create", actionNodeCreate)
http.HandleFunc("/nodes/move", actionNodeMove)
+ http.HandleFunc("/nodes/search", actionNodeSearch)
http.HandleFunc("/types/{typeID}", actionType)
http.HandleFunc("/types/", actionTypesAll)
@@ -286,6 +287,47 @@ func actionNodeMove(w http.ResponseWriter, r *http.Request) { // {{{
j, _ := json.Marshal(out)
w.Write(j)
} // }}}
+func actionNodeSearch(w http.ResponseWriter, r *http.Request) { // {{{
+ maxResults := 25
+
+ typeIDStr := r.URL.Query().Get("type_id")
+ typeID, err := strconv.Atoi(typeIDStr)
+ if err != nil {
+ typeID = -1
+ }
+
+ searchText := r.URL.Query().Get("search")
+
+ var nodes []Node
+ nodes, err = SearchNodes(typeID, searchText, maxResults)
+ if err != nil {
+ err = werr.Wrap(err)
+ httpError(w, err)
+ return
+ }
+
+ moreExist := len(nodes) > maxResults
+ if moreExist {
+ nodes = nodes[:maxResults]
+ }
+
+ out := struct {
+ OK bool
+ Nodes []Node
+ MoreExistThan int
+ }{
+ true,
+ nodes,
+ 0,
+ }
+
+ if moreExist {
+ out.MoreExistThan = maxResults
+ }
+
+ j, _ := json.Marshal(out)
+ w.Write(j)
+} // }}}
func actionType(w http.ResponseWriter, r *http.Request) { // {{{
typeID := 0
typeIDStr := r.PathValue("typeID")