From e7c7621b16847133ca3c0210ed18dc23d4157031 Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Sun, 10 Dec 2023 09:40:49 +0100 Subject: [PATCH] Add autoComplete for `flow` and `global` types --- .../src/js/ui/common/typedInput.js | 178 +++++++++++++++--- 1 file changed, 151 insertions(+), 27 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js index 9aa27c710..90aa73e2d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js @@ -54,35 +54,157 @@ return icon; } - var autoComplete = function(options) { - function getMatch(value, searchValue) { - const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); - const len = idx > -1 ? searchValue.length : 0; - return { - index: idx, - found: idx > -1, - pre: value.substring(0,idx), - match: value.substring(idx,idx+len), - post: value.substring(idx+len), + const getMatch = function (value, searchValue) { + const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); + const len = idx > -1 ? searchValue.length : 0; + return { + index: idx, + found: idx > -1, + pre: value.substring(0, idx), + match: value.substring(idx, idx + len), + post: value.substring(idx + len), + }; + }; + + const generateSpans = function (match) { + const els = []; + if (match.pre) { els.push($('').text(match.pre)); } + if (match.match) { els.push($('', { style: "font-weight: bold; color: var(--red-ui-text-color-link);" }).text(match.match)); } + if (match.post) { els.push($('').text(match.post)); } + return els; + }; + + /** + * Autocomplete for flow and global types + * @param {"flow"|"global"} type Either `flow` or `global` + * @returns void + */ + const contextAutoComplete = function (type) { + const currentSearch = { + completeKey: "", + searchKey: "", + store: "", + options: [], + }; + + function getContext(contextUrl) { + return new Promise((resolve, _reject) => { + $.getJSON(`context/${contextUrl}`) + .done((data) => { + try { + const decoded = Object.entries(data).reduce((acc, [store, value]) => { + acc[store] = Object.entries(value).reduce((acc, [k, v]) => { + acc[k] = RED.utils.decodeObject(v.msg, v.format); + return acc; + }, {}); + return acc; + }, {}); + + resolve(decoded); + } catch (error) { + console.error("Failed to load context:", error); + resolve({}); + } + }) + .fail(() => resolve({})); + }); + } + + function getContextOptions(contextUrl, store, keyParts) { + return new Promise(async (resolve, _reject) => { + const data = await getContext(contextUrl); + + if (!(store in data)) { + return []; + } + + const options = keyParts.reduce((options, key, i) => { + if (typeof options === "object" && key in options) { + options = options[key]; + } else { + options = {}; + } + + return options; + }, data[store]); + + resolve(Object.keys(options || {})); + }); + } + + function updateSearch(value, contextUrl) { + return new Promise(async (resolve, _reject) => { + const { key, store } = RED.utils.parseContextKey(value); + const keyParts = key.split("."); + const searchKey = keyParts.pop(); + const completeKey = keyParts.join("."); + + // "" or "foo." + const valueEnd = /^.?$|\.$/.test(value); + const completeKeyDiff = completeKey !== currentSearch.completeKey; + const storeDiff = store !== currentSearch.store; + if (valueEnd || completeKeyDiff || storeDiff) { + currentSearch.completeKey = completeKey; + currentSearch.store = store; + currentSearch.options = await getContextOptions(contextUrl, store, keyParts); + } + + currentSearch.searchKey = searchKey; + resolve(); + }); + } + + return function (value, done) { + let contextUrl; + + if (type === "global") { + contextUrl = "global"; + } else if (type === "flow") { + contextUrl = `flow/${RED.workspaces.active()}`; + } else { + return; } + + updateSearch(value, contextUrl).then(() => { + const { completeKey, options, searchKey } = currentSearch; + const matches = options + .reduce((opts, optKey) => { + const keyMatch = getMatch(optKey, searchKey); + + if (keyMatch.found) { + const element = $("
", { style: "display: flex" }); + + $("
", { style: "font-family: var(--red-ui-monospace-font); white-space: nowrap; overflow: hidden; flex-grow: 1" }) + .append(generateSpans(keyMatch)) + .appendTo(element); + + opts.push({ + value: `${completeKey}${completeKey && "."}${optKey}`, + label: element, + i: keyMatch.index, + }); + } + + return opts; + }, []) + .sort(function (a, b) { return a.i - b.i }); + + done(matches); + }).catch(); } - function generateSpans(match) { - const els = []; - if(match.pre) { els.push($('').text(match.pre)); } - if(match.match) { els.push($('',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); } - if(match.post) { els.push($('').text(match.post)); } - return els; - } - return function(val) { + } + + const autoComplete = function (options) { + return function (val) { var matches = []; options.forEach(opt => { const optVal = opt.value; - const optSrc = (opt.source||[]).join(","); + const optSrc = (opt.source || []).join(","); const valMatch = getMatch(optVal, val); const srcMatch = getMatch(optSrc, val); if (valMatch.found || srcMatch.found) { - const element = $('
',{style: "display: flex"}); - const valEl = $('
',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"}); + const element = $('
', { style: "display: flex" }); + const valEl = $('
', { style: "font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1" }); valEl.append(generateSpans(valMatch)); valEl.appendTo(element); if (optSrc) { @@ -97,7 +219,7 @@ }); } }) - matches.sort(function(A,B){return A.i-B.i}) + matches.sort(function (A, B) { return A.i - B.i }) return matches; } } @@ -168,18 +290,20 @@ var allOptions = { msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)}, flow: {value:"flow",label:"flow.",hasValue:true, - options:[], + //options:[], validate:RED.utils.validatePropertyExpression, parse: contextParse, export: contextExport, - valueLabel: contextLabel + valueLabel: contextLabel, + autoComplete: contextAutoComplete("flow"), }, global: {value:"global",label:"global.",hasValue:true, - options:[], + //options:[], validate:RED.utils.validatePropertyExpression, parse: contextParse, export: contextExport, - valueLabel: contextLabel + valueLabel: contextLabel, + autoComplete: contextAutoComplete("global"), }, str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"}, num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) { @@ -1184,7 +1308,7 @@ this.elementDiv.show(); if (opt.autoComplete) { this.input.autoComplete({ - search: opt.autoComplete, + search: (v, d) => opt.autoComplete(this.value(), d), minLength: 0 }) }