diff --git a/example.html b/example.html index 2b68379..1342053 100644 --- a/example.html +++ b/example.html @@ -25,7 +25,7 @@ max-width: 400px; overflow: auto; } - #code, #fullcode { + .code { max-height: 800px; width: 400px; overflow: auto; @@ -37,12 +37,14 @@
-
- - +
+ + + +
-
- +
+
diff --git a/src/demo-config.js b/src/demo-config.js index 0005133..27b812d 100644 --- a/src/demo-config.js +++ b/src/demo-config.js @@ -1,4 +1,48 @@ export const demoConfig = [ + { + condition: "state", + entity_id: "binary_sensor.dark_outside", + state: "on", + }, + { + choose: [ + { + conditions: [ + { + condition: "state", + entity_id: "binary_sensor.door_open", + state: "on", + }, + ], + sequence: [ + { + service: "light.turn_on", + entity_id: "light.outdoors", + } + ], + }, + { + conditions: [ + { + condition: "state", + entity_id: "input_select.time_of_day", + state: "night", + }, + ], + sequence: [ + { + service: "light.turn_off", + entity_id: "light.outdoors", + } + ], + }, + ], + default: [ + ], + }, +] + +export const demoConfig2 = [ { service: "light.turn_on", data: { entity_id: "group.bedroom", diff --git a/src/main.js b/src/main.js index 9afd901..c33ad17 100644 --- a/src/main.js +++ b/src/main.js @@ -4,121 +4,18 @@ import "@vanillawc/wc-codemirror"; import "./script-graph"; import "./script-graph2"; +import { ActionHandler } from "./script-to-graph"; + import { mdiAsterisk, mdiArrowUp, mdiArrowDown } from "@mdi/js"; let index_counter = 0; let nodes = []; -const structure_tree = (inp) => { - if(!inp) return null; - if(Array.isArray(inp)) return inp.map(structure_tree); - let data = {}; - let type = "YAML"; - - const idx = index_counter++; - if("service" in inp) type = "service"; - if("condition" in inp) type = "condition"; - if("delay" in inp) type = "delay"; - if("wait_template" in inp) type = "wait"; - if("event" in inp) type = "event"; - if("repeat" in inp) { - type = "loop"; - data = {sequence: structure_tree(inp.repeat.sequence)}; - } - if("choose" in inp) { - type = "choose"; - let choices = []; - for (const [i,c] of inp.choose.entries()) { - const header = { - type: "chooseChoice", - idx: [idx, i], - } - choices.push([header].concat(structure_tree(c.sequence))); - } - choices.push([{ - type: "chooseDefault", - idx: [idx, -1], - }].concat(structure_tree(inp.default))); - data = { - choices, - } - } - nodes[idx] = inp; - return {type, - idx, - ...data}; - -} - -const structure_tree2 = () => { - return [ - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number 1"), - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number 2"), - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number 3"), - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number 4"), - children: [ - [ - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number A1"), - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number A2"), - }, - ], - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number B"), - end: false, - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number B"), - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number B"), - }, - ] - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number 4"), - children: [ - { - icon: mdiArrowUp, - clickCallback: () => console.log("Return"), - }, - { - icon: mdiArrowDown, - clickCallback: () => console.log("Return"), - }, - ], - }, - { - icon: mdiAsterisk, - clickCallback: () => console.log("Number 1"), - }, - ]; -} - window.onload = () => { let src = demoConfig; - const fullcode = document.querySelector("wc-codemirror"); + const fullcode = document.querySelector("#fullcode"); fullcode.mode = "yaml"; window.setTimeout(()=> fullcode.value = jsyaml.safeDump(src), 100); @@ -127,29 +24,28 @@ window.onload = () => { src = jsyaml.safeLoad(fullcode.value); index_counter = 0; nodes = []; - graph.tree = structure_tree(src); + // graph.tree = structure_tree(src); }); - const graph = document.createElement("script-graph"); - graph.tree = structure_tree(src); - const graph2 = document.createElement("script-graph2"); - graph2.tree = structure_tree2(src); + const tr = new ActionHandler(); + window.tr = tr; + tr.actions = src; + tr.updateCallback = (actions) => { + graph2.tree = tr.graph; + fullcode.value = jsyaml.safeDump(tr.actions); + }; + tr.selectCallback = (idx, action, update) => { + graph2.tree = tr.graph; + const code = document.querySelector("#snippet"); + code.value = jsyaml.safeDump(action); + console.log(update); + document.querySelector("#saveSnippet").onclick = () => update(jsyaml.safeLoad(code.value)); + document.querySelector("#deleteSnippet").onclick = () => update(jsyaml.safeLoad(null)); + } + + // graph2.tree = structure_tree2(src); + graph2.tree = tr.graph; document.querySelector("#graph2").appendChild(graph2); - graph.addEventListener("selected", (ev) => { - const idx = ev.detail; - const code = document.querySelector("code"); - let c; - if(Array.isArray(idx)) { - c = nodes[idx[0]].choose[idx[1]]; - } else { - c = nodes[idx]; - } - code.innerHTML = JSON.stringify(c, null, ' '); - - }) - document.querySelector("#graph").appendChild(graph); - - } diff --git a/src/script-graph2.ts b/src/script-graph2.ts index 795957c..fe72b98 100644 --- a/src/script-graph2.ts +++ b/src/script-graph2.ts @@ -34,6 +34,7 @@ class ScriptGraph2 extends LitElement { r="${this.nodeSize/2}" class="node" @click=${node.clickCallback} + style=${node.styles} /> @@ -177,13 +178,17 @@ class ScriptGraph2 extends LitElement { let tree = this._draw_tree(this.tree); return html` diff --git a/src/script-to-graph.ts b/src/script-to-graph.ts new file mode 100644 index 0000000..0041d1f --- /dev/null +++ b/src/script-to-graph.ts @@ -0,0 +1,257 @@ +import { + mdiCallSplit, + mdiAbTesting, + mdiCheck, + mdiClose, + mdiChevronRight, + mdiExclamation, + mdiTimerOutline, + mdiTrafficLight, + mdiRefresh, + mdiArrowUp, + mdiCodeJson, + mdiCheckBoxOutline, + mdiCheckboxBlankOutline, + mdiAsterisk, +} from "@mdi/js"; + +const ICONS = { + new: mdiAsterisk, + service: mdiChevronRight, + condition: mdiAbTesting, + TRUE: mdiCheck, + FALSE: mdiClose, + delay: mdiTimerOutline, + wait_template: mdiTrafficLight, + event: mdiExclamation, + repeat: mdiRefresh, + repeatReturn: mdiArrowUp, + choose: mdiCallSplit, + chooseChoice: mdiCheckBoxOutline, + chooseDefault: mdiCheckboxBlankOutline, + YAML: mdiCodeJson, +}; + +const OPTIONS = [ + "condition", + "delay", + "device_id", + "event", + "scene", + "service", + "wait_template", + "repeat", + "choose", +]; + +const SPECIAL = { + condition: (action, selected, select, update) => { + return { + icon: ICONS["choose"], + clickCallback: () => select([1], action, update), + children: [ + { + icon: ICONS["TRUE"], + clickCallback: () => select([2], action, update), + styles: selected[0] + ? "stroke: orange;" + : undefined, + }, + { + icon: ICONS["FALSE"], + end: false, + clickCallback: () => select([3], action, update), + styles: selected[0] + ? "stroke: orange;" + : undefined, + }, + ], + } + }, + + repeat: (action, selected, select, update) => { + let seq = action.repeat.sequence; + if(!seq || !seq.length) seq = [{}]; + const seqHandler = new ActionHandler(seq); + if(selected[0] !== undefined && selected[0] !== -1) + seqHandler.selected = selected; + seqHandler.selectCallback = select; + seqHandler.updateCallback = (a) => { + action.repeat["sequence"] = a; + update(action); + } + + return { + icon: ICONS["repeat"], + clickCallback: () => select([-1], action, update), + children: [ + { + icon: ICONS["repeatReturn"], + clickCallback: () => select([-1], action, update), + styles: selected[0] === -1 + ? "stroke: orange;" + : undefined, + }, + seqHandler.graph, + ], + }; + }, + + choose: (action, selected, select, update) => { + + let def = action.default || [{}]; + const defaultHandler = new ActionHandler(def); + console.log(selected) + if(selected[0] === -2) + defaultHandler.selected = selected.slice(1); + defaultHandler.selectCallback = (i, a, u) => { + select( [-2].concat(i), a, u); + }; + defaultHandler.updateCallback = (a) => { + console.log("UPDATE"); + console.log(defaultHandler.actions); + console.log(a); + action.default = defaultHandler.actions; + update(action); + }; + + + return { + icon: ICONS["choose"], + clickCallback: () => select([-1], action, update), + children: [ + ...action.choose.map((b,idx) => { + const handler = new ActionHandler(b.sequence || [{}]); + if(selected[0] === idx) + handler.selected = selected.slice(1); + handler.selectCallback = (i, a, u) => { + select([idx].concat(i), a, u); + }; + handler.updateCallback = (a) => { + b.sequence = handler.actions; + action.choose[idx] = b; + update(action); + }; + + return [ + { + icon: ICONS["chooseChoice"], + clickCallback: () => select([idx], b, (a) => { + action.choose[idx] = a; + update(action); + }), + styles: selected[0] === idx + ? "stroke: orange;" + : undefined, + }, + handler.graph, + ]; + + }), + + [ + { + icon: ICONS["chooseDefault"], + clickCallback: () => select([-2], def, (a) => { + action.default = a; + update(action); + }), + styles: selected[0] === -2 + ? "stroke: orange;" + : undefined, + }, + defaultHandler.graph, + ] + ], + }; + }, + +}; + +export class ActionHandler { + _actions = []; + updateCallback = null; + selectCallback = null; + selected = []; + + constructor(actions = []) { + this._actions = actions; + } + + set actions(actions) { + this._actions = actions; + } + + get actions() { + if(!this._actions.length) this._actions = [{}]; + return this._actions; + } + + get graph() { + return this.actions.map((_, idx) => this._make_graph_node(idx)); + } + + _update_action(idx, action) { + if(action === null) + this.actions.splice(idx, 1); + else + this.actions[idx] = action; + if (this.updateCallback) + this.updateCallback(this.actions); + } + + _add_action(idx) { + this.actions.splice(idx, 0, {}); + if (this.updateCallback) + this.updateCallback(this.actions); + this._select_node([idx], {}, (a) =>this._update_action(idx, a)); + } + + _select_node(idx, action, update=null) { + this.selected = idx; + if (this.selectCallback) + this.selectCallback(idx, action, update); + } + + _make_graph_node(idx) { + const action = this.actions[idx] || {}; + let _type = "yaml"; + if (Object.keys(action).length === 0) + _type = "new"; + else + _type = OPTIONS.find((option) => option in action) || "YAML"; + + const selected = this.selected.length >= 1 && this.selected[0] == idx; + let node = {}; + + if (_type in SPECIAL) { + node = SPECIAL[_type]( + action, + selected ? this.selected.slice(1): [], + (i, a, u) => this._select_node([idx].concat(i), a, u), + (a) => this._update_action(idx, a), + ); + } else { + node = { + icon: ICONS[_type], + clickCallback: () => { + this._select_node( + [idx], + action, + (a) => this._update_action(idx, a) + ) + } + } + } + return { + ...node, + addCallback: () => this._add_action(idx+1), + styles: selected + ? "stroke: orange" + : _type === "new" + ? "stroke: lightgreen;" + : undefined, + } + } + +}