parent
99ea870ebc
commit
4a342ba2fa
|
@ -91,6 +91,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
||||||
position : { row : mci.position[0], col : mci.position[1] },
|
position : { row : mci.position[0], col : mci.position[1] },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// :TODO: These should use setPropertyValue()!
|
||||||
function setOption(pos, name) {
|
function setOption(pos, name) {
|
||||||
if(mci.args.length > pos && mci.args[pos].length > 0) {
|
if(mci.args.length > pos && mci.args[pos].length > 0) {
|
||||||
options[name] = mci.args[pos];
|
options[name] = mci.args[pos];
|
||||||
|
|
|
@ -40,29 +40,21 @@ function login(callingMenu, formData, extraArgs) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function recordLoginHistory(callback) {
|
function recordLoginHistory(callback) {
|
||||||
userDb.run(
|
|
||||||
'INSERT INTO user_login_history (user_id, user_name, timestamp) ' +
|
|
||||||
'VALUES(?, ?, ?);', [ user.userId, user.username, now.toISOString() ], function inserted(err) {
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
userDb.serialize(function serialized() {
|
||||||
|
userDb.run(
|
||||||
|
'INSERT INTO user_login_history (user_id, user_name, timestamp) ' +
|
||||||
|
'VALUES(?, ?, ?);', [ user.userId, user.username, now.toISOString() ]
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
// keep 30 days of records
|
||||||
userDb.run(
|
userDb.run(
|
||||||
'DELETE FROM last_caller ' +
|
'DELETE FROM user_login_history ' +
|
||||||
'WHERE id NOT IN (' +
|
'WHERE timestamp <= DATETIME("now", "-30 day");'
|
||||||
' SELECT id ' +
|
);
|
||||||
' FROM last_caller ' +
|
});
|
||||||
' ORDER BY timestamp DESC ' +
|
|
||||||
' LIMIT 100);');
|
|
||||||
|
|
||||||
userDb.run(
|
callback(null);
|
||||||
'DELETE FROM last_caller ' +
|
|
||||||
'WHERE user_id IN (' +
|
|
||||||
' SELECT user_id ' +
|
|
||||||
' ORDER BY timestamp DESC ' +
|
|
||||||
'LIMIT 1;')
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err, results) {
|
function complete(err, results) {
|
||||||
|
|
|
@ -114,7 +114,6 @@ TextView.prototype.getData = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
TextView.prototype.setText = function(text) {
|
TextView.prototype.setText = function(text) {
|
||||||
|
|
||||||
var widthDelta = 0;
|
var widthDelta = 0;
|
||||||
if(this.text && this.text !== text) {
|
if(this.text && this.text !== text) {
|
||||||
widthDelta = Math.abs(this.text.length - text.length);
|
widthDelta = Math.abs(this.text.length - text.length);
|
||||||
|
|
11
core/view.js
11
core/view.js
|
@ -67,7 +67,10 @@ function View(options) {
|
||||||
this.setDimension(options.dimens);
|
this.setDimension(options.dimens);
|
||||||
this.autoScale = { height : false, width : false };
|
this.autoScale = { height : false, width : false };
|
||||||
} else {
|
} else {
|
||||||
this.dimens = { width : 0, height : 0 };
|
this.dimens = {
|
||||||
|
width : options.width || 0,
|
||||||
|
height : 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: Just use styleSGRx for these, e.g. styleSGR0, styleSGR1 = norm/focus
|
// :TODO: Just use styleSGRx for these, e.g. styleSGR0, styleSGR1 = norm/focus
|
||||||
|
@ -214,6 +217,12 @@ View.prototype.setPropertyValue = function(propName, value) {
|
||||||
*/
|
*/
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'resizable' :
|
||||||
|
if(_.isBoolean(value)) {
|
||||||
|
this.resizable = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'argName' : this.submitArgName = value; break;
|
case 'argName' : this.submitArgName = value; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
var MenuModule = require('../core/menu_module.js').MenuModule;
|
var MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
var userDb = require('../core/database.js').dbs.user;
|
var userDb = require('../core/database.js').dbs.user;
|
||||||
var ViewController = require('../core/view_controller.js').ViewController;
|
var ViewController = require('../core/view_controller.js').ViewController;
|
||||||
|
var TextView = require('../core/text_view.js').TextView;
|
||||||
|
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
|
@ -12,18 +13,16 @@ var assert = require('assert');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Last Callers',
|
name : 'Last Callers',
|
||||||
desc : 'Last 10 callers to the system',
|
desc : 'Last callers to the system',
|
||||||
author : 'NuSkooler',
|
author : 'NuSkooler',
|
||||||
|
packageName : 'codes.l33t.enigma.lastcallers'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = LastCallersModule;
|
exports.getModule = LastCallersModule;
|
||||||
|
|
||||||
// :TODO:
|
// :TODO:
|
||||||
// * Order should be menu/theme defined
|
// * config.evenRowSGR (optional)
|
||||||
// * Text needs overflow defined (optional), e.g. "..."
|
|
||||||
// * Date/time format should default to theme short date + short time
|
|
||||||
// *
|
|
||||||
|
|
||||||
function LastCallersModule(options) {
|
function LastCallersModule(options) {
|
||||||
MenuModule.call(this, options);
|
MenuModule.call(this, options);
|
||||||
|
@ -31,66 +30,29 @@ function LastCallersModule(options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.menuConfig = options.menuConfig;
|
this.menuConfig = options.menuConfig;
|
||||||
|
|
||||||
this.menuMethods = {
|
this.rows = 10;
|
||||||
getLastCaller : function(formData, extraArgs) {
|
|
||||||
//console.log(self.lastCallers[self.lastCallerIndex])
|
if(this.menuConfig.config) {
|
||||||
var lc = self.lastCallers[self.lastCallerIndex++];
|
if(_.isNumber(this.menuConfig.config.rows)) {
|
||||||
var when = moment(lc.timestamp).format(self.menuConfig.config.dateTimeFormat);
|
this.rows = Math.max(1, this.menuConfig.config.rows);
|
||||||
return util.format('%s %s %s %s', lc.name, lc.location, lc.affiliation, when);
|
|
||||||
}
|
}
|
||||||
};
|
if(_.isString(this.menuConfig.config.dateTimeFormat)) {
|
||||||
|
this.dateTimeFormat = this.menuConfig.config.dateTimeFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
util.inherits(LastCallersModule, MenuModule);
|
util.inherits(LastCallersModule, MenuModule);
|
||||||
|
|
||||||
/*
|
|
||||||
LastCallersModule.prototype.enter = function(client) {
|
LastCallersModule.prototype.enter = function(client) {
|
||||||
LastCallersModule.super_.prototype.enter.call(this, client);
|
LastCallersModule.super_.prototype.enter.call(this, client);
|
||||||
|
|
||||||
var self = this;
|
// we need the client to init this for theming
|
||||||
self.lastCallers = [];
|
if(!_.isString(this.dateTimeFormat)) {
|
||||||
self.lastCallerIndex = 0;
|
this.dateTimeFormat = this.client.currentTheme.helpers.getDateFormat('short') +
|
||||||
|
this.client.currentTheme.helpers.getTimeFormat('short');
|
||||||
var userInfoStmt = userDb.prepare(
|
}
|
||||||
'SELECT prop_name, prop_value ' +
|
|
||||||
'FROM user_property ' +
|
|
||||||
'WHERE user_id=? AND (prop_name=? OR prop_name=?);');
|
|
||||||
|
|
||||||
var caller;
|
|
||||||
|
|
||||||
userDb.each(
|
|
||||||
'SELECT user_id, user_name, timestamp ' +
|
|
||||||
'FROM user_login_history ' +
|
|
||||||
'ORDER BY timestamp DESC ' +
|
|
||||||
'LIMIT 10;',
|
|
||||||
function userRows(err, userEntry) {
|
|
||||||
caller = {
|
|
||||||
who : userEntry.user_name,
|
|
||||||
when : userEntry.timestamp,
|
|
||||||
};
|
|
||||||
|
|
||||||
userInfoStmt.each( [ userEntry.user_id, 'location', 'affiliation' ], function propRow(err, propEntry) {
|
|
||||||
if(!err) {
|
|
||||||
caller[propEntry.prop_name] = propEntry.prop_value;
|
|
||||||
}
|
|
||||||
}, function complete(err) {
|
|
||||||
if(!err) {
|
|
||||||
self.lastCallers.push(caller);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
LastCallersModule.prototype.mciReady = function(mciData) {
|
|
||||||
LastCallersModule.super_.prototype.mciReady.call(this, mciData);
|
|
||||||
|
|
||||||
// we do this so other modules can be both customized and still perform standard tasks
|
|
||||||
LastCallersModule.super_.prototype.standardMCIReadyHandler.call(this, mciData);
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
LastCallersModule.prototype.mciReady = function(mciData) {
|
LastCallersModule.prototype.mciReady = function(mciData) {
|
||||||
LastCallersModule.super_.prototype.mciReady.call(this, mciData);
|
LastCallersModule.super_.prototype.mciReady.call(this, mciData);
|
||||||
|
@ -98,12 +60,7 @@ LastCallersModule.prototype.mciReady = function(mciData) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var vc = self.viewControllers.lastCallers = new ViewController( { client : self.client } );
|
var vc = self.viewControllers.lastCallers = new ViewController( { client : self.client } );
|
||||||
var lc = [];
|
var lc = [];
|
||||||
var count = _.size(mciData.menu) / 4;
|
var rows = self.rows;
|
||||||
|
|
||||||
if(count < 1) {
|
|
||||||
// :TODO: Log me!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
|
@ -118,12 +75,13 @@ LastCallersModule.prototype.mciReady = function(mciData) {
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// :TODO: a public method of getLastCallers(count) would be better
|
||||||
function fetchHistory(callback) {
|
function fetchHistory(callback) {
|
||||||
userDb.each(
|
userDb.each(
|
||||||
'SELECT user_id, user_name, timestamp ' +
|
'SELECT user_id, user_name, timestamp ' +
|
||||||
'FROM user_login_history ' +
|
'FROM user_login_history ' +
|
||||||
'ORDER BY timestamp DESC ' +
|
'ORDER BY timestamp DESC ' +
|
||||||
'LIMIT ' + count + ';',
|
'LIMIT ' + rows + ';',
|
||||||
function historyRow(err, histEntry) {
|
function historyRow(err, histEntry) {
|
||||||
lc.push( {
|
lc.push( {
|
||||||
userId : histEntry.user_id,
|
userId : histEntry.user_id,
|
||||||
|
@ -132,7 +90,7 @@ LastCallersModule.prototype.mciReady = function(mciData) {
|
||||||
} );
|
} );
|
||||||
},
|
},
|
||||||
function complete(err, recCount) {
|
function complete(err, recCount) {
|
||||||
count = recCount; // adjust to retrieved
|
rows = recCount; // adjust to retrieved
|
||||||
callback(err);
|
callback(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -156,124 +114,65 @@ LastCallersModule.prototype.mciReady = function(mciData) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function createAndPopulateViews(callback) {
|
function createAndPopulateViews(callback) {
|
||||||
assert(lc.length === count);
|
|
||||||
|
|
||||||
var rowsPerColumn = count / 4;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// TL1...count = who
|
// TL1 = who
|
||||||
// TL<count>...<count*2> = location
|
// TL2 = location
|
||||||
|
// TL3 = affiliation
|
||||||
|
// TL4 = when
|
||||||
//
|
//
|
||||||
var i;
|
// These form the order/layout for a row. Additional rows
|
||||||
var v;
|
// will use them as a template.
|
||||||
for(i = 0; i < rowsPerColumn; ++i) {
|
|
||||||
v = vc.getView(i + 1);
|
|
||||||
v.setText(lc[i].who);
|
|
||||||
}
|
|
||||||
|
|
||||||
for( ; i < rowsPerColumn * 2; ++i) {
|
|
||||||
v = vc.getView(i + 1);
|
|
||||||
v.setText(lc[i].location);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
|
var views = {
|
||||||
// 1..count/4 = who
|
who : vc.getView(1),
|
||||||
// count/10
|
location : vc.getView(2),
|
||||||
|
affils : vc.getView(3),
|
||||||
/*
|
when : vc.getView(4),
|
||||||
var viewOpts = {
|
|
||||||
client : self.client,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var rowViewId = 1;
|
var row = views.who.position.row;
|
||||||
var v;
|
|
||||||
lc.forEach(function lcEntry(caller) {
|
|
||||||
v = vc.getView(rowViewId++);
|
|
||||||
|
|
||||||
self.menuConfig.config.fields.forEach(function field(f) {
|
var nextId = 5;
|
||||||
switch(f.name) {
|
|
||||||
case 'who' :
|
|
||||||
|
|
||||||
}
|
function addView(templateView, text) {
|
||||||
});
|
// :TODO: Is there a better way to clone this when dealing with instances?
|
||||||
|
var v = new TextView( {
|
||||||
|
client : self.client,
|
||||||
|
id : nextId++,
|
||||||
|
position : { row : row, col : templateView.position.col },
|
||||||
|
ansiSGR : templateView.ansiSGR,
|
||||||
|
textStyle : templateView.textStyle,
|
||||||
|
textOverflow : templateView.textOverflow,
|
||||||
|
dimens : templateView.dimens,
|
||||||
|
resizable : templateView.resizable,
|
||||||
|
} );
|
||||||
|
|
||||||
v.setText(caller.who)
|
v.id = nextId++;
|
||||||
|
v.position.row = row;
|
||||||
|
|
||||||
|
v.setPropertyValue('text', text);
|
||||||
|
vc.addView(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
lc.forEach(function lastCaller(c) {
|
||||||
|
if(row === views.who.position.row) {
|
||||||
|
views.who.setText(c.who);
|
||||||
|
views.location.setText(c.location);
|
||||||
|
views.affils.setText(c.affiliation);
|
||||||
|
views.when.setText(moment(c.when).format(self.dateTimeFormat));
|
||||||
|
} else {
|
||||||
|
addView(views.who, c.who);
|
||||||
|
addView(views.location, c.location);
|
||||||
|
addView(views.affils, c.affiliation);
|
||||||
|
addView(views.when, moment(c.when).format(self.dateTimeFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
row++;
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
console.log(lc)
|
self.client.log.error(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
LastCallersModule.prototype.mciReady = function(mciData) {
|
|
||||||
LastCallersModule.super_.prototype.mciReady.call(this, mciData);
|
|
||||||
|
|
||||||
var lastCallers = [];
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// :TODO: durp... need a table just for this so dupes are possible
|
|
||||||
|
|
||||||
var userInfoStmt = userDb.prepare(
|
|
||||||
'SELECT prop_name, prop_value ' +
|
|
||||||
'FROM user_property ' +
|
|
||||||
'WHERE user_id=? AND (prop_name=? OR prop_name=?);');
|
|
||||||
|
|
||||||
var caller;
|
|
||||||
|
|
||||||
userDb.each(
|
|
||||||
'SELECT id, user_name, timestamp ' +
|
|
||||||
'FROM user_last_login ' +
|
|
||||||
'ORDER BY timestamp DESC ' +
|
|
||||||
'LIMIT 10;',
|
|
||||||
function userRows(err, userEntry) {
|
|
||||||
caller = { name : userEntry.user_name };
|
|
||||||
|
|
||||||
userInfoStmt.each(userEntry.id, 'location', 'affiliation', function propRow(err, propEntry) {
|
|
||||||
console.log(propEntry)
|
|
||||||
if(!err) {
|
|
||||||
caller[propEntry.prop_name] = propEntry.prop_value;
|
|
||||||
}
|
|
||||||
}, function complete(err) {
|
|
||||||
lastCallers.push(caller);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function complete(err) {
|
|
||||||
//
|
|
||||||
// TL1=name, TL2=location, TL3=affils
|
|
||||||
// TL4=name, TL5=location, ...
|
|
||||||
// ...
|
|
||||||
// TL28=name, TL29=location, TL30=affils
|
|
||||||
//
|
|
||||||
var lc = self.viewControllers.lastCallers = new ViewController( { client : self.client });
|
|
||||||
|
|
||||||
var loadOpts = {
|
|
||||||
callingMenu : self,
|
|
||||||
mciMap : mciData.menu,
|
|
||||||
noInput : true,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.viewControllers.lastCallers.loadFromMenuConfig(loadOpts, function viewsReady(err) {
|
|
||||||
console.log(lastCallers);
|
|
||||||
var callerIndex = 0;
|
|
||||||
for(var i = 1; i < 30; i += 3) {
|
|
||||||
if(lastCallers.length > callerIndex) {
|
|
||||||
lc.getView(i).setText(lastCallers[callerIndex].name);
|
|
||||||
lc.getView(i + 1).setText(lastCallers[callerIndex].location);
|
|
||||||
lc.getView(i + 2).setText(lastCallers[callerIndex].affiliation);
|
|
||||||
++callerIndex;
|
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
*/
|
|
|
@ -123,12 +123,10 @@
|
||||||
},
|
},
|
||||||
"ME3" : {
|
"ME3" : {
|
||||||
"argName" : "birthdate",
|
"argName" : "birthdate",
|
||||||
//"width" : 8,
|
|
||||||
"maskPattern" : "####/##/##"
|
"maskPattern" : "####/##/##"
|
||||||
},
|
},
|
||||||
"ET4" : {
|
"ET4" : {
|
||||||
"argName" : "sex",
|
"argName" : "sex",
|
||||||
//"width" : 1,
|
|
||||||
"maxLength" : 1
|
"maxLength" : 1
|
||||||
},
|
},
|
||||||
"ET5" : {
|
"ET5" : {
|
||||||
|
@ -212,37 +210,26 @@
|
||||||
"art" : "LASTCALL.ANS",
|
"art" : "LASTCALL.ANS",
|
||||||
"options" : { "cls" : true },
|
"options" : { "cls" : true },
|
||||||
"config" : {
|
"config" : {
|
||||||
"dateTimeFormat" : "ddd MMM Do H:mm a",
|
"dateTimeFormat" : "ddd MMM Do H:mm a"
|
||||||
"fields" : [
|
|
||||||
{
|
|
||||||
"name" : "who",
|
|
||||||
"width" : 17
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name" : "location",
|
|
||||||
"width" : 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name" : "affiliation",
|
|
||||||
"width" : 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name" : "when",
|
|
||||||
"width" : 20
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"form" : {
|
"form" : {
|
||||||
"0" : {
|
"0" : {
|
||||||
"TLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTLTL" : {
|
"TLTLTLTL" : {
|
||||||
"mci" : {
|
"mci" : {
|
||||||
"TL1" : {
|
// :TODO: Bug: Without any keys here, theme customization does not apply!!!!
|
||||||
//"text" : "@method:getLastCaller"
|
"TL1" : {
|
||||||
|
"styleSGR1" : "|00|24"
|
||||||
},
|
},
|
||||||
"TL2" : {
|
"TL2" : {
|
||||||
//"text" : "@method:getLastCaller"
|
"styleSGR1" : "|00|24"
|
||||||
|
},
|
||||||
|
"TL3" : {
|
||||||
|
"styleSGR1" : "|00|24"
|
||||||
|
},
|
||||||
|
"TL4" : {
|
||||||
|
"styleSGR1" : "|00|24"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -29,7 +29,7 @@
|
||||||
"focusTextStyle" : "l33t"
|
"focusTextStyle" : "l33t"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apply" : {
|
"apply" : {
|
||||||
"ET1" : { "width" : 21 },
|
"ET1" : { "width" : 21 },
|
||||||
"ET2" : { "width" : 21 },
|
"ET2" : { "width" : 21 },
|
||||||
//"ET3" : { "width" : 21 },
|
//"ET3" : { "width" : 21 },
|
||||||
|
@ -45,7 +45,24 @@
|
||||||
"ET8" : { "width" : 21 },
|
"ET8" : { "width" : 21 },
|
||||||
"ET9" : { "width" : 21 },
|
"ET9" : { "width" : 21 },
|
||||||
"ET10" : { "width" : 21 }
|
"ET10" : { "width" : 21 }
|
||||||
}
|
},
|
||||||
|
"lastCallers" : {
|
||||||
|
"TL1" : {
|
||||||
|
"resizable" : false,
|
||||||
|
"width" : 16,
|
||||||
|
"textOverflow" : "..."
|
||||||
|
},
|
||||||
|
"TL2" : {
|
||||||
|
"resizable" : false,
|
||||||
|
"width" : 19,
|
||||||
|
"textOverflow" : "..."
|
||||||
|
},
|
||||||
|
"TL3" : {
|
||||||
|
"resizable" : false,
|
||||||
|
"width" : 17,
|
||||||
|
"textOverflow" : "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"prompts" : {
|
"prompts" : {
|
||||||
"userCredentials" : {
|
"userCredentials" : {
|
||||||
|
|
Loading…
Reference in New Issue