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'], store: req.query['store'],
req: apiUtils.getRequestLogObject(req) req: apiUtils.getRequestLogObject(req)
} }
if (req.query['keysOnly'] !== undefined) {
opts.keysOnly = true
}
runtimeAPI.context.getValue(opts).then(function(result) { runtimeAPI.context.getValue(opts).then(function(result) {
res.json(result); res.json(result);
}).catch(function(err) { }).catch(function(err) {

View File

@ -46,6 +46,12 @@
opacity: 0.3 opacity: 0.3
}).appendTo(container); }).appendTo(container);
this.elementDiv.show(); this.elementDiv.show();
if (!this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete({
search: contextAutoComplete({ input: that }),
minLength: 0
})
}
} }
var mapDeprecatedIcon = function(icon) { var mapDeprecatedIcon = function(icon) {
if (/^red\/images\/typedInput\/.+\.png$/.test(icon)) { if (/^red\/images\/typedInput\/.+\.png$/.test(icon)) {
@ -54,26 +60,28 @@
return icon; return icon;
} }
var autoComplete = function(options) { function getMatch(value, searchValue) {
function getMatch(value, searchValue) { const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase()); const len = idx > -1 ? searchValue.length : 0;
const len = idx > -1 ? searchValue.length : 0; return {
return { index: idx,
index: idx, found: idx > -1,
found: idx > -1, pre: value.substring(0,idx),
pre: value.substring(0,idx), match: value.substring(idx,idx+len),
match: value.substring(idx,idx+len), post: value.substring(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 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) { return function(val) {
console.log('msgAutoComplete', val)
var matches = []; var matches = [];
options.forEach(opt => { options.forEach(opt => {
const optVal = opt.value; const optVal = opt.value;
@ -101,6 +109,86 @@
return matches; 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). // This is a hand-generated list of completions for the core nodes (based on the node help html).
var msgCompletions = [ var msgCompletions = [
@ -166,7 +254,7 @@
{ value: "_session", source: ["websocket out","tcp out"] }, { value: "_session", source: ["websocket out","tcp out"] },
] ]
var allOptions = { 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, flow: {value:"flow",label:"flow.",hasValue:true,
options:[], options:[],
validate:RED.utils.validatePropertyExpression, validate:RED.utils.validatePropertyExpression,
@ -544,7 +632,7 @@
that.element.trigger('paste',evt); that.element.trigger('paste',evt);
}); });
this.input.on('keydown', function(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 return
} }
if (evt.keyCode >= 37 && evt.keyCode <= 40) { 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 // If previousType is !null, then this is a change of the type, rather than the initialisation
var previousType = this.typeMap[this.propertyType]; var previousType = this.typeMap[this.propertyType];
previousValue = this.input.val(); previousValue = this.input.val();
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
if (previousType && this.typeChanged) { if (previousType && this.typeChanged) {
if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) } 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) }, editBuffer: function(options) { showTypeEditor("_buffer", options) },
getEditStack: function () { return [...editStack] },
buildEditForm: buildEditForm, buildEditForm: buildEditForm,
validateNode: validateNode, validateNode: validateNode,
updateNodeProperties: updateNodeProperties, updateNodeProperties: updateNodeProperties,

View File

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