diff --git a/script.go b/script.go new file mode 100644 index 0000000..6d73ad0 --- /dev/null +++ b/script.go @@ -0,0 +1,110 @@ +package main + +import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + "github.com/jmoiron/sqlx" + + // Standard + "encoding/json" + "strings" + "time" +) + +type Script struct { + ID int + Group string + Name string + Source string + Updated time.Time +} + +func GetScripts() (scripts []Script, err error) { + scripts = []Script{} + + var rows *sqlx.Rows + rows, err = db.Queryx(` + SELECT * + FROM script + ORDER BY + "group" ASC, + name ASC + `) + if err != nil { + err = werr.Wrap(err) + return + } + defer rows.Close() + + for rows.Next() { + var script Script + err = rows.StructScan(&script) + if err != nil { + err = werr.Wrap(err) + return + } + scripts = append(scripts, script) + } + + return +} +func UpdateScript(scriptID int, data []byte) (script Script, err error) { + err = json.Unmarshal(data, &script) + if err != nil { + err = werr.Wrap(err) + return + } + + script.ID = scriptID + script.Group = strings.TrimSpace(script.Group) + script.Name = strings.TrimSpace(script.Name) + + if script.Group == "" || script.Name == "" { + err = werr.New("Group and name must be provided.") + return + } + + if script.ID < 1 { + row := db.QueryRowx(` + INSERT INTO script("group", "name", "source") + VALUES($1, $2, $3) + RETURNING + id + `, + strings.TrimSpace(script.Group), + strings.TrimSpace(script.Name), + script.Source, + ) + err = row.Scan(&script.ID) + } else { + _, err = db.Exec(` + UPDATE script + SET + "group" = $2, + "name" = $3, + "source" = $4 + WHERE + id = $1 + `, + scriptID, + strings.TrimSpace(script.Group), + strings.TrimSpace(script.Name), + script.Source, + ) + } + + if err != nil { + err = werr.Wrap(err) + return + } + + return +} +func DeleteScript(scriptID int) (err error) { + _, err = db.Exec(`DELETE FROM script WHERE id = $1`, scriptID) + if err != nil { + err = werr.Wrap(err) + return + } + return +} diff --git a/sql/0010.sql b/sql/0010.sql index 4f4cf84..794e00b 100644 --- a/sql/0010.sql +++ b/sql/0010.sql @@ -1,5 +1,6 @@ CREATE TABLE public.script ( id serial NOT NULL, + "group" varchar NOT NULL, name varchar NOT NULL, "source" text NOT NULL, updated timestamptz DEFAULT NOW() NOT NULL, diff --git a/static/css/main.css b/static/css/main.css index 62dd257..7dc5907 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -107,6 +107,7 @@ button { white-space: nowrap; margin-top: 32px; margin-bottom: 8px; + color: var(--section-color); } #types .group:first-child { margin-top: 0px; @@ -361,3 +362,55 @@ dialog#connection-data div.button { color: #fff; z-index: 8192; } +#scripts .group { + font-weight: bold; + margin-top: 32px; + color: var(--section-color); +} +#scripts .group:first-child { + margin-top: 0px; +} +#scripts .script { + display: grid; + grid-template-columns: min-content 1fr; + grid-gap: 4px; + align-items: center; + user-select: none; + cursor: pointer; +} +#scripts .script.selected { + font-weight: bold; +} +#scripts .script img { + display: block; + height: 24px; +} +#scripts .script div { + white-space: nowrap; +} +#editor-script > div { + display: grid; + grid-template-columns: 200px 1fr min-content; + grid-gap: 16px; + align-items: end; +} +#editor-script .label { + margin-top: 16px; + font-weight: bold; + color: var(--section-color); +} +#editor-script .label:first-child { + margin-top: 0px; +} +#editor-script input[type="text"] { + width: 100%; +} +#editor-script textarea { + width: 100%; + height: 400px; + font-family: monospace; + font-size: 0.85em; +} +#editor-script button { + margin-top: 8px; +} diff --git a/static/js/app.mjs b/static/js/app.mjs index 45becb6..4fd4c09 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -8,12 +8,17 @@ export class App { window.mbus = new MessageBus() this.editor = null this.typesList = null + this.scriptsList = null + this.scriptEditor = null + this.currentNode = null this.currentNodeID = null this.types = [] this.currentPage = null this.tree = new Tree(document.getElementById('nodes')) + this.createdScript = null + const events = [ 'EDITOR_NODE_SAVE', 'MENU_ITEM_SELECTED', @@ -24,6 +29,11 @@ export class App { 'NODE_EDIT_NAME', 'NODE_MOVE', 'NODE_SELECTED', + 'SCRIPT_CREATED', + 'SCRIPT_DELETED', + 'SCRIPT_EDIT', + 'SCRIPTS_LIST_FETCHED', + 'SCRIPT_UPDATED', 'TREE_RELOAD_NODE', 'TYPE_EDIT', 'TYPES_LIST_FETCHED', @@ -92,6 +102,42 @@ export class App { types.replaceChildren(this.typesList.render()) break + case 'SCRIPTS_LIST_FETCHED': + const scripts = document.getElementById('scripts') + scripts.replaceChildren(this.scriptsList.render()) + + if (this.createdScript !== null) { + mbus.dispatch('SCRIPT_EDIT', this.createdScript) + this.createdScript = null + } + + break + + case 'SCRIPT_CREATED': + this.createdScript = event.detail + this.scriptsList.fetchScripts() + .catch(err => showError(err)) + break + + case 'SCRIPT_DELETED': + this.scriptsList.setSelected(null) + this.scriptsList.fetchScripts().catch(err=>showError(err)) + break + + case 'SCRIPT_EDIT': + const scriptEditor = document.getElementById('editor-script') + this.scriptEditor.script = event.detail + + this.scriptsList.setSelected(event.detail.ID) + + scriptEditor.replaceChildren(this.scriptEditor.render()) + break + + case 'SCRIPT_UPDATED': + this.scriptsList.fetchScripts() + .catch(err => showError(err)) + break + case 'NODE_CREATE_DIALOG': if (this.currentPage !== 'node' || this.currentNode === null) return @@ -175,6 +221,9 @@ export class App { this.currentPage = name + // This one is special and needs to be hidden separately since the HTML elements are built differently. + document.getElementById('editor-node').style.display = 'none' + switch (name) { case 'node': document.getElementById('nodes').classList.add('show') @@ -184,7 +233,6 @@ 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() @@ -196,6 +244,14 @@ export class App { case 'script': document.getElementById('scripts').classList.add('show') document.getElementById('editor-script').classList.add('show') + + if (this.scriptsList === null) { + this.scriptsList = new ScriptsList() + this.scriptEditor = new ScriptEditor() + } + + this.scriptsList.fetchScripts() + .catch(err => showError(err)) break } }// }}} @@ -913,4 +969,217 @@ class ConnectedNode { }// }}} } +class ScriptsList extends Component { + constructor() {// {{{ + super() + this.scripts = [] + this.selectedID = 0 + }// }}} + + async fetchScripts() {// {{{ + return new Promise((resolve, reject) => { + fetch('/scripts/') + .then(data => data.json()) + .then(json => { + if (!json.OK) { + reject(json.Error) + return + } + this.scripts = json.Scripts + mbus.dispatch('SCRIPTS_LIST_FETCHED') + resolve(this.scripts) + }) + }) + }// }}} + renderComponent() {// {{{ + let prevGroup = null + const elements = [] + const imgAdd = document.createElement('img') + imgAdd.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box.svg`) + imgAdd.style.height = '32px' + imgAdd.style.cursor = 'pointer' + imgAdd.addEventListener('click', () => this.createScript()) + elements.push(imgAdd) + + for (const s of this.scripts) { + if (prevGroup != s.Group) { + const gEl = document.createElement('div') + gEl.classList.add('group') + gEl.innerText = s.Group + elements.push(gEl) + prevGroup = s.Group + } + + const sEl = document.createElement('div') + sEl.classList.add('script') + if (s.ID === this.selectedID) + sEl.classList.add('selected') + sEl.setAttribute('data-script-id', s.ID) + sEl.innerHTML = ` +