Skip to content

Commit 9ee387e

Browse files
committed
Feature: Multi-select menus in plugin and event parameters.
1 parent 8ebe888 commit 9ee387e

8 files changed

Lines changed: 109 additions & 28 deletions

File tree

docs/plugins.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,8 @@ Alpha [a1], Beta [b2], Gamma [c3]
965965

966966
This would show only the labels in the menu ("Alpha", "Beta", "Gamma"), but in the data the values would be specified instead (`a1`, `b2`, `c3`). Note that the values may only contain alphanumerics, underscores, dashes and dots, and when this feature is used the visual labels are **not** passed into the data at all.
967967

968+
Note that if you check the "Multi-Select" checkbox when configuring the menu field, your parameter value will be an array of selected values, as opposed to a string for a single-select menu.
969+
968970
### Bucket Menu
969971

970972
A "bucket menu" is a dynamically populated menu, which automatically loads its items from a global [Storage Bucket](buckets.md) that you configure. Using this feature you can have menus across multiple Plugins or events that all share the same item pool.
@@ -1068,6 +1070,8 @@ Finally, you can define groups of items in the menu by including an object with
10681070
}
10691071
```
10701072

1073+
Note that if you check the "Multi-Select" checkbox when configuring the bucket menu field, your parameter value will be an array of selected values, as opposed to a string for a single-select menu.
1074+
10711075
### System Menu
10721076

10731077
A "system menu" is a dynamically populated menu, similar to a [Bucket Menu](#bucket-menu), but the menu items are pulled from xyOps itself. This is useful when a Plugin needs the user to select an existing xyOps object, such as an event, category, server, server group, plugin, user, role, web hook or monitor.
@@ -1108,6 +1112,8 @@ The "Targets" menu is a combined list which includes both server groups and indi
11081112

11091113
API Keys and Secrets are intentionally not offered as system menu sources.
11101114

1115+
Note that if you check the "Multi-Select" checkbox when configuring the system menu field, your parameter value will be an array of selected values, as opposed to a string for a single-select menu.
1116+
11111117
### Checkbox
11121118

11131119
A checkbox is displayed with a label, and the "checked" state is stored as a Boolean parameter value (`true` or `false`).

htdocs/css/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,10 @@ body.dark .plugin_param_editor_cont div.button.danger:hover {
16471647
border-color: var(--red);
16481648
}
16491649

1650+
body.dark .plugin_param_editor_cont .multiselect.multi {
1651+
background: var(--form-background-color);
1652+
}
1653+
16501654
.dialog .plugin_param_editor_cont {
16511655
background: rgba(233, 236, 240, 0.3);
16521656
}

htdocs/js/pages/Base.class.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2603,6 +2603,12 @@ Page.Base = class Base extends Page {
26032603
html += '<div class="info_label">' + (param.locked ? '<i class="mdi mdi-lock-outline">&nbsp;</i>' : '') + strip_html(param.title) + '</div>';
26042604
html += '<div class="info_value">';
26052605

2606+
if (param.type.match(/^(select|bucket|system)$/)) {
2607+
// special value handling for menus
2608+
if (param.multiple) elem_value = (param.id in params) ? always_array(params[param.id]).join(', ') : '';
2609+
else elem_value = (param.id in params) ? always_string(params[param.id]) : '';
2610+
}
2611+
26062612
switch (param.type) {
26072613
case 'text':
26082614
if (param.id in params) {
@@ -2642,20 +2648,20 @@ Page.Base = class Base extends Page {
26422648

26432649
case 'select':
26442650
html += '<i class="link mdi mdi-' + elem_icon + '" onClick="$P().copyPluginParamValue(this)" title="Copy to Clipboard">&nbsp;</i>';
2645-
html += '<span class="data_value">' + encode_entities( elem_value.toString().replace(/\,.*$/, '').replace(/^.+\[([\w\-\.]+)\]\s*$/, '$1') ) + '</span>';
2651+
html += '<span class="data_value">' + encode_entities( str_value(elem_value) || '(None)') + '</span>';
26462652
break;
26472653

26482654
case 'bucket':
26492655
if (elem_value.toString().length) {
26502656
html += '<i class="link mdi mdi-' + elem_icon + '" onClick="$P().copyPluginParamValue(this)" title="Copy to Clipboard">&nbsp;</i>';
2651-
html += '<span class="data_value">' + encode_entities( elem_value ) + '</span>';
2657+
html += '<span class="data_value">' + encode_entities( str_value(elem_value) || '(None)' ) + '</span>';
26522658
}
26532659
else html += self.getNiceBucket(param.bucket_id);
26542660
break;
26552661

26562662
case 'system':
26572663
html += '<i class="link mdi mdi-' + elem_icon + '" onClick="$P().copyPluginParamValue(this)" title="Copy to Clipboard">&nbsp;</i>';
2658-
html += '<span class="data_value">' + encode_entities( elem_value.toString() || '(None)' ) + '</span>';
2664+
html += '<span class="data_value">' + encode_entities( str_value(elem_value) || '(None)' ) + '</span>';
26592665
break;
26602666

26612667
case 'toolset':

htdocs/js/pages/PageUtils.class.js

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2671,25 +2671,44 @@ Page.PageUtils = class PageUtils extends Page.Base {
26712671
break;
26722672

26732673
case 'select':
2674-
elem_value = (param.id in params) ? params[param.id] : param.value.replace(/\,.*$/, '').replace(/^.+\[([\w\-\.]+)\]\s*$/, '$1');
2675-
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.csvToMenuItems(param.value), disabled: elem_dis, title: param.title });
2674+
if (param.multiple) {
2675+
elem_value = (param.id in params) ? always_array(params[param.id]) : [];
2676+
html += self.getFormMenuMulti({ id: elem_id, values: elem_value, options: self.csvToMenuItems(param.value), disabled: elem_dis, title: param.title, placeholder: '(None)', 'data-hold': 1, 'data-select-all': 1 });
2677+
}
2678+
else {
2679+
elem_value = (param.id in params) ? always_string(params[param.id]) : '';
2680+
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.csvToMenuItems(param.value), disabled: elem_dis, title: param.title });
2681+
}
26762682
break;
26772683

26782684
case 'bucket':
26792685
var bucket = find_object( app.buckets, { id: param.bucket_id } );
26802686
if (bucket) {
2681-
elem_value = (param.id in params) ? params[param.id] : '';
2682-
var menu_opts = self.getBucketMenuItems(bucket.id, param.bucket_path, elem_id, elem_value);
2683-
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: menu_opts, disabled: elem_dis, title: param.title });
2687+
if (param.multiple) {
2688+
elem_value = (param.id in params) ? always_array(params[param.id]) : [];
2689+
var menu_opts = self.getBucketMenuItems(bucket.id, param.bucket_path, elem_id, elem_value);
2690+
html += self.getFormMenuMulti({ id: elem_id, values: elem_value, options: menu_opts, disabled: elem_dis, title: param.title, placeholder: '(None)', 'data-hold': 1, 'data-select-all': 1 });
2691+
}
2692+
else {
2693+
elem_value = (param.id in params) ? always_string(params[param.id]) : '';
2694+
var menu_opts = self.getBucketMenuItems(bucket.id, param.bucket_path, elem_id, elem_value);
2695+
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: menu_opts, disabled: elem_dis, title: param.title });
2696+
}
26842697
}
26852698
else {
2686-
html += self.getFormMenuSingle({ id: elem_id, value: '', options: [ { id: '', title: "(Bucket Not Found)" } ], disabled: elem_dis, title: param.title });
2699+
html += `<input type="hidden" id="${elem_id}" value="">(Bucket Not Found)`;
26872700
}
26882701
break;
26892702

26902703
case 'system':
2691-
elem_value = (param.id in params) ? params[param.id] : '';
2692-
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.getSystemMenuItems(param.list_id), disabled: elem_dis, title: param.title });
2704+
if (param.multiple) {
2705+
elem_value = (param.id in params) ? always_array(params[param.id]) : [];
2706+
html += self.getFormMenuMulti({ id: elem_id, value: elem_value, options: self.getSystemMenuItems(param.list_id, 'multi'), disabled: elem_dis, title: param.title, placeholder: '(None)', 'data-hold': 1, 'data-select-all': 1 });
2707+
}
2708+
else {
2709+
elem_value = (param.id in params) ? always_string(params[param.id]) : '';
2710+
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.getSystemMenuItems(param.list_id), disabled: elem_dis, title: param.title });
2711+
}
26932712
break;
26942713

26952714
case 'toolset':
@@ -3698,6 +3717,12 @@ Page.PageUtils = class PageUtils extends Page.Base {
36983717
html += '<div class="info_label">' + (param.locked ? '<i class="mdi mdi-lock-outline">&nbsp;</i>' : '') + strip_html(param.title) + '</div>';
36993718
html += '<div class="info_value">';
37003719

3720+
if (param.type.match(/^(select|bucket|system)$/)) {
3721+
// special value handling for menus
3722+
if (param.multiple) elem_value = (param.id in params) ? always_array(params[param.id]).join(', ') : '';
3723+
else elem_value = (param.id in params) ? always_string(params[param.id]) : '';
3724+
}
3725+
37013726
switch (param.type) {
37023727
case 'text':
37033728
case 'textarea':
@@ -3731,20 +3756,20 @@ Page.PageUtils = class PageUtils extends Page.Base {
37313756

37323757
case 'select':
37333758
html += '<i class="mdi mdi-' + elem_icon + '">&nbsp;</i>';
3734-
html += strip_html( str_value(elem_value).replace(/\,.*$/, '').replace(/^.+\[([\w\-\.]+)\]\s*$/, '$1') );
3759+
html += encode_entities( str_value(elem_value) || '(None)');
37353760
break;
37363761

37373762
case 'bucket':
37383763
if (str_value(elem_value).length) {
37393764
html += '<i class="mdi mdi-' + elem_icon + '">&nbsp;</i>';
3740-
html += strip_html( elem_value );
3765+
html += encode_entities( str_value(elem_value) || '(None)')
37413766
}
37423767
else html += self.getNiceBucket( param.bucket_id );
37433768
break;
37443769

37453770
case 'system':
37463771
html += '<i class="mdi mdi-' + elem_icon + '">&nbsp;</i>';
3747-
html += strip_html( str_value(elem_value) || '(None)' );
3772+
html += encode_entities( str_value(elem_value) || '(None)');
37483773
break;
37493774

37503775
case 'toolset':
@@ -4820,6 +4845,18 @@ Page.PageUtils = class PageUtils extends Page.Base {
48204845
caption: 'Optionally enter a caption for the parameter, which will be displayed below it.'
48214846
});
48224847

4848+
// required
4849+
html += this.getFormRow({
4850+
id: 'd_epa_multiple',
4851+
label: 'Menu Options:',
4852+
content: this.getFormCheckbox({
4853+
id: 'fe_epa_multiple',
4854+
label: 'Multi-Select Menu',
4855+
checked: !!param.multiple
4856+
}),
4857+
caption: 'Check this box to enable multi-select in the menu (param value will be an array).'
4858+
});
4859+
48234860
// required
48244861
html += this.getFormRow({
48254862
id: 'd_epa_required',
@@ -4906,18 +4943,21 @@ Page.PageUtils = class PageUtils extends Page.Base {
49064943

49074944
case 'select':
49084945
param.value = $('#fe_epa_value_select').val();
4946+
param.multiple = !!$('#fe_epa_multiple').is(':checked');
49094947
delete param.required;
49104948
break;
49114949

49124950
case 'bucket':
49134951
param.bucket_id = $('#fe_epa_bucket_id').val();
49144952
if (!param.bucket_id) return app.badField('#fe_epa_bucket_id', "Please select a storage bucket containing the menu items.");
49154953
param.bucket_path = $('#fe_epa_bucket_path').val();
4954+
param.multiple = !!$('#fe_epa_multiple').is(':checked');
49164955
delete param.required;
49174956
break;
49184957

49194958
case 'system':
49204959
param.list_id = $('#fe_epa_list_id').val();
4960+
param.multiple = !!$('#fe_epa_multiple').is(':checked');
49214961
delete param.required;
49224962
break;
49234963

@@ -4963,6 +5003,7 @@ Page.PageUtils = class PageUtils extends Page.Base {
49635003
$('#d_epa_locked').toggle( !new_type.match(/^(toolset)$/) );
49645004
$('#d_epa_bucket_id, #d_epa_bucket_path').toggle( !!new_type.match(/^(bucket)$/) );
49655005
$('#d_epa_list_id').toggle( !!new_type.match(/^(system)$/) );
5006+
$('#d_epa_multiple').toggle( !!new_type.match(/^(select|bucket|system)$/) );
49665007
Dialog.autoResize();
49675008
}; // change_action_type
49685009

@@ -5051,7 +5092,7 @@ Page.PageUtils = class PageUtils extends Page.Base {
50515092
return items;
50525093
}
50535094

5054-
getSystemMenuItems(list_id) {
5095+
getSystemMenuItems(list_id, multi) {
50555096
// get items for menu given list id
50565097
var items = [];
50575098
var list_def = find_object( config.ui.list_list, { id: list_id } );
@@ -5088,7 +5129,7 @@ Page.PageUtils = class PageUtils extends Page.Base {
50885129
break;
50895130
}
50905131

5091-
items.unshift({ "id": "", "title": "(None)" });
5132+
if (!multi) items.unshift({ "id": "", "title": "(None)" });
50925133
return items;
50935134
}
50945135

@@ -5304,25 +5345,44 @@ Page.PageUtils = class PageUtils extends Page.Base {
53045345
break;
53055346

53065347
case 'select':
5307-
elem_value = (param.id in params) ? params[param.id] : param.value.replace(/\,.*$/, '').replace(/^.+\[([\w\-\.]+)\]\s*$/, '$1');
5308-
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.csvToMenuItems(param.value), disabled: elem_dis, title: param.title });
5348+
if (param.multiple) {
5349+
elem_value = (param.id in params) ? always_array(params[param.id]) : [];
5350+
html += self.getFormMenuMulti({ id: elem_id, values: elem_value, options: self.csvToMenuItems(param.value), disabled: elem_dis, title: param.title, placeholder: '(None)', 'data-hold': 1, 'data-select-all': 1 });
5351+
}
5352+
else {
5353+
elem_value = (param.id in params) ? always_string(params[param.id]) : '';
5354+
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.csvToMenuItems(param.value), disabled: elem_dis, title: param.title });
5355+
}
53095356
break;
53105357

53115358
case 'bucket':
53125359
var bucket = find_object( app.buckets, { id: param.bucket_id } );
53135360
if (bucket) {
5314-
elem_value = (param.id in params) ? params[param.id] : '';
5315-
var menu_opts = self.getBucketMenuItems(bucket.id, param.bucket_path, elem_id, elem_value);
5316-
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: menu_opts, disabled: elem_dis, title: param.title });
5361+
if (param.multiple) {
5362+
elem_value = (param.id in params) ? always_array(params[param.id]) : [];
5363+
var menu_opts = self.getBucketMenuItems(bucket.id, param.bucket_path, elem_id, elem_value);
5364+
html += self.getFormMenuMulti({ id: elem_id, values: elem_value, options: menu_opts, disabled: elem_dis, title: param.title, placeholder: '(None)', 'data-hold': 1, 'data-select-all': 1 });
5365+
}
5366+
else {
5367+
elem_value = (param.id in params) ? always_string(params[param.id]) : '';
5368+
var menu_opts = self.getBucketMenuItems(bucket.id, param.bucket_path, elem_id, elem_value);
5369+
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: menu_opts, disabled: elem_dis, title: param.title });
5370+
}
53175371
}
53185372
else {
5319-
html += self.getFormMenuSingle({ id: elem_id, value: '', options: [ { id: '', title: "(Bucket Not Found)" } ], disabled: elem_dis, title: param.title });
5373+
html += `<input type="hidden" id="${elem_id}" value="">(Bucket Not Found)`;
53205374
}
53215375
break;
53225376

53235377
case 'system':
5324-
elem_value = (param.id in params) ? params[param.id] : '';
5325-
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.getSystemMenuItems(param.list_id), disabled: elem_dis, title: param.title });
5378+
if (param.multiple) {
5379+
elem_value = (param.id in params) ? always_array(params[param.id]) : [];
5380+
html += self.getFormMenuMulti({ id: elem_id, value: elem_value, options: self.getSystemMenuItems(param.list_id, 'multi'), disabled: elem_dis, title: param.title, placeholder: '(None)', 'data-hold': 1, 'data-select-all': 1 });
5381+
}
5382+
else {
5383+
elem_value = (param.id in params) ? always_string(params[param.id]) : '';
5384+
html += self.getFormMenuSingle({ id: elem_id, value: elem_value, options: self.getSystemMenuItems(param.list_id), disabled: elem_dis, title: param.title });
5385+
}
53265386
break;
53275387

53285388
} // switch type

htdocs/js/utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,11 @@ function interval_hits_per_minute(trigger, epoch) {
492492
return hits;
493493
};
494494

495+
function always_string(thing) {
496+
// convert thing to string -- if array, grab first element
497+
return Array.isArray(thing) ? String(thing[0]) : String(thing);
498+
}
499+
495500
CodeMirror.defineMode("mustache", function(config, parserConfig) {
496501
// define custom mode for double-curly-brace "mustache" syntax to use as overlay
497502
var mustacheOverlay = {

lib/api/jobs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ class Jobs {
709709
Tools.mergeHashInto( job.params, params.params );
710710

711711
for (var key in job.params) {
712-
if (key.match(/^_xy_override_(\w+)$/) && job.params[key]) {
712+
if (key.match(/^_xy_override_(\w+)$/) && job.params[key] && job.params[key].length) {
713713
var sub_key = RegExp.$1;
714714
self.logDebug(9, "Overriding job property with param: " + key, { key: sub_key, value: job.params[key] } );
715715
job[sub_key] = job.params[key];

lib/job.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class Jobs {
169169
}
170170

171171
// allow special prefixed params to override top-level props
172-
if (key.match(/^_xy_override_(\w+)$/) && job.params[key]) {
172+
if (key.match(/^_xy_override_(\w+)$/) && job.params[key] && job.params[key].length) {
173173
var sub_key = RegExp.$1;
174174
this.logJob(9, "Overriding job property with param: " + key, { key: sub_key, value: job.params[key] } );
175175
job[sub_key] = job.params[key];

lib/workflow.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ class Workflows {
225225
// pass workflow-level _xy_override_ params into sub-job props
226226
if (job.params) {
227227
for (var key in job.params) {
228-
if (key.match(/^_xy_override_(\w+)$/) && job.params[key]) {
228+
if (key.match(/^_xy_override_(\w+)$/) && job.params[key] && job.params[key].length) {
229229
var sub_key = RegExp.$1;
230230
new_job[sub_key] = job.params[key];
231231
}
@@ -319,7 +319,7 @@ class Workflows {
319319
// pass workflow-level _xy_override_ params into sub-job props
320320
if (job.params) {
321321
for (var key in job.params) {
322-
if (key.match(/^_xy_override_(\w+)$/) && job.params[key]) {
322+
if (key.match(/^_xy_override_(\w+)$/) && job.params[key] && job.params[key].length) {
323323
var sub_key = RegExp.$1;
324324
new_job[sub_key] = job.params[key];
325325
}

0 commit comments

Comments
 (0)