Add auto-complete to flow/global typedInput types

This commit is contained in:
Nick O'Leary 2023-12-11 17:55:02 +00:00
parent 918943816f
commit b9c1dedab3
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
4 changed files with 169 additions and 39 deletions

View File

@ -33,6 +33,9 @@ module.exports = {
store: req.query['store'],
req: apiUtils.getRequestLogObject(req)
}
if (req.query['keysOnly'] !== undefined) {
opts.keysOnly = true
}
runtimeAPI.context.getValue(opts).then(function(result) {
res.json(result);
}).catch(function(err) {

View File

@ -46,6 +46,12 @@
opacity: 0.3
}).appendTo(container);
this.elementDiv.show();
if (!this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete({
search: contextAutoComplete({ input: that }),
minLength: 0
})
}
}
var mapDeprecatedIcon = function(icon) {
if (/^red\/images\/typedInput\/.+\.png$/.test(icon)) {
@ -54,26 +60,28 @@
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),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
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),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
}
const msgAutoComplete = function(options) {
return function(val) {
console.log('msgAutoComplete', val)
var matches = [];
options.forEach(opt => {
const optVal = opt.value;
@ -101,6 +109,86 @@
return matches;
}
}
const contextAutoComplete = function(options) {
const cache = {}
const knownKeys = {}
const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
knownKeys[store] = knownKeys[store] || new Set()
const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
if (cache[url]) {
console.log('CACHED', url)
done()
} else {
console.log('GET', url)
$.getJSON(url, function(data) {
cache[url] = true
const keys = data[store] || []
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
keys.forEach(key => {
knownKeys[store].add(keyPrefix + key)
})
done()
})
}
}
const getContextKeys = function(key, done) {
const keyParts = key.split('.')
const partialKey = keyParts.pop()
let scope = options.input.propertyType
if (scope === 'flow') {
// Get the flow id of the node we're editing
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done([])
return
}
const editingNode = editStack.pop()
if (editingNode.z) {
scope = `${scope}/${editingNode.z}`
} else {
done([])
return
}
}
const store = options.input.optionValue
const searchKey = keyParts.join('.')
getContextKeysFromRuntime(scope, store, searchKey, function() {
if (knownKeys[store].has(key)) {
getContextKeysFromRuntime(scope, store, key, function() {
done(knownKeys[store])
})
}
done(knownKeys[store])
})
}
return function(val, done) {
getContextKeys(val, function (keys) {
console.log(keys)
const matches = []
keys.forEach(v => {
let optVal = v
const valMatch = getMatch(optVal, val);
if (valMatch.found) {
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
matches.push({
value: optVal,
label: element,
});
}
})
done(matches)
})
}
}
// This is a hand-generated list of completions for the core nodes (based on the node help html).
var msgCompletions = [
@ -166,7 +254,7 @@
{ value: "_session", source: ["websocket out","tcp out"] },
]
var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
flow: {value:"flow",label:"flow.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
@ -544,7 +632,7 @@
that.element.trigger('paste',evt);
});
this.input.on('keydown', function(evt) {
if (that.typeMap[that.propertyType].autoComplete) {
if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
return
}
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@ -967,6 +1055,9 @@
// If previousType is !null, then this is a change of the type, rather than the initialisation
var previousType = this.typeMap[this.propertyType];
previousValue = this.input.val();
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
if (previousType && this.typeChanged) {
if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }

View File

@ -2082,6 +2082,7 @@ RED.editor = (function() {
}
},
editBuffer: function(options) { showTypeEditor("_buffer", options) },
getEditStack: function () { return [...editStack] },
buildEditForm: buildEditForm,
validateNode: validateNode,
updateNodeProperties: updateNodeProperties,

View File

@ -68,6 +68,7 @@ var api = module.exports = {
* @param {String} opts.store - the context store
* @param {String} opts.key - the context key
* @param {Object} opts.req - the request to log (optional)
* @param {Boolean} opts.keysOnly - whether to return keys only
* @return {Promise} - the node information
* @memberof @node-red/runtime_context
*/
@ -100,8 +101,16 @@ var api = module.exports = {
}
if (ctx) {
if (key) {
console.log('GET KEY', key)
store = store || availableStores.default;
ctx.get(key,store,function(err, v) {
if (opts.keysOnly) {
if (typeof v === 'object') {
resolve({ [store]: Object.keys(v) })
} else {
resolve({ [store]: [] })
}
}
var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) {
encoded.store = store;
@ -118,32 +127,58 @@ var api = module.exports = {
stores = [store];
}
var result = {};
var c = stores.length;
var errorReported = false;
stores.forEach(function(store) {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
if (opts.keysOnly) {
ctx.keys(store,function(err, keys) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return
}
result[store] = keys
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
}
}
})
} else {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return;
}
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
return;
}
}
});
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
}
}
});
}
})
}
} else {