feat: add DataFilter widget for data_filter support in DataViewer
- New bricks.DataFilter widget (bricks/data_filter.js): parses data_filter JSON definition, renders search input fields for each var parameter, supports AND/OR/NOT nested structures, and UiCode dropdowns for fields with browserfields.alters uitype=code configuration. - Modified DataViewer (bricks/dataviewer.js): added build_datafilter_widget(), filter_event_handle(), filter_clear_handle() methods; extended merge_search_params() to send data_filter JSON + collected var values to the backend API. - Updated build.sh: added data_filter.js to the JS concatenation list. Backend integration: DataViewer sends data_filter (JSON string) and each var's user input value as URL params. Backend .dspy uses sqlor.filter.DBFilter to convert to SQL WHERE clause.
This commit is contained in:
parent
5a94ae73d9
commit
431245648d
@ -3,7 +3,7 @@ SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \
|
||||
jsoncall.js myoperator.js scroll.js menu.js popup.js recorder.js \
|
||||
modal.js running.js llmout.js glbviewer.js \
|
||||
markdown_viewer.js audio.js toolbar.js tab.js \
|
||||
input.js registerfunction.js button.js accordion.js searchbar.js dataviewer.js \
|
||||
input.js registerfunction.js button.js accordion.js searchbar.js data_filter.js dataviewer.js \
|
||||
tree.js multiple_state_image.js dynamiccolumn.js form.js message.js conform.js \
|
||||
paging.js datagrid.js iframe.js cols.js echartsext.js \
|
||||
floaticonbar.js miniform.js wterm.js dynamicaccordion.js \
|
||||
|
||||
247
bricks/data_filter.js
Normal file
247
bricks/data_filter.js
Normal file
@ -0,0 +1,247 @@
|
||||
var bricks = window.bricks || {};
|
||||
|
||||
/*
|
||||
* DataFilter widget — renders a search form from a data_filter definition.
|
||||
*
|
||||
* Usage:
|
||||
* new bricks.DataFilter({
|
||||
* data_filter: { "AND": [
|
||||
* {"field": "name", "op": "LIKE", "var": "name_input"},
|
||||
* {"field": "status", "op": "=", "var": "status_input"}
|
||||
* ]},
|
||||
* browserfields: { alters: { status: { uitype: "code", data: [...] } } } // optional
|
||||
* });
|
||||
*
|
||||
* Events:
|
||||
* filter_search: { values: { name_input: "x", status_input: "y" } }
|
||||
* filter_clear: {}
|
||||
*/
|
||||
bricks.DataFilter = class extends bricks.VBox {
|
||||
constructor(opts){
|
||||
opts = opts || {};
|
||||
opts.width = opts.width || '100%';
|
||||
opts.height = opts.height || 'auto';
|
||||
super(opts);
|
||||
this.set_style('alignItems', 'center');
|
||||
this.set_style('gap', opts.gap || '0.5rem');
|
||||
this.set_style('flexWrap', 'wrap');
|
||||
this.filter_values = {};
|
||||
this.var_fields = [];
|
||||
this._parse_filter();
|
||||
this._build_inputs();
|
||||
this._build_buttons();
|
||||
}
|
||||
|
||||
/* Recursively extract all {var, op, field} from the filter tree */
|
||||
_parse_filter(){
|
||||
this.var_fields = [];
|
||||
if (!this.opts.data_filter) return;
|
||||
this._extract_vars(this.opts.data_filter);
|
||||
}
|
||||
|
||||
_extract_vars(node){
|
||||
if (!node || typeof node !== 'object') return;
|
||||
var self = this;
|
||||
var keys = Object.keys(node);
|
||||
for (var i = 0; i < keys.length; i++){
|
||||
var key = keys[i];
|
||||
var val = node[key];
|
||||
if (key === 'AND' || key === 'OR'){
|
||||
if (Array.isArray(val)){
|
||||
val.forEach(function(item){ self._extract_vars(item); });
|
||||
}
|
||||
} else if (key === 'NOT'){
|
||||
this._extract_vars(val);
|
||||
} else if (key === 'field'){
|
||||
// This node is a leaf condition: {field, op, var|const}
|
||||
if (node.var){
|
||||
this.var_fields.push({
|
||||
var_name: node.var,
|
||||
field_name: node.field,
|
||||
op: node.op || '='
|
||||
});
|
||||
}
|
||||
return; // leaf node, don't recurse into siblings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_build_inputs(){
|
||||
var self = this;
|
||||
var alters = (this.opts.browserfields && this.opts.browserfields.alters) || {};
|
||||
this.var_fields.forEach(function(vf){
|
||||
var wrapper = new bricks.HBox({
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
});
|
||||
wrapper.set_style('alignItems', 'center');
|
||||
wrapper.set_style('gap', '0.25rem');
|
||||
|
||||
// Label
|
||||
var label_text = self.opts.field_labels && self.opts.field_labels[vf.var_name]
|
||||
? self.opts.field_labels[vf.var_name]
|
||||
: vf.field_name;
|
||||
var label_w = new bricks.Text({
|
||||
text: label_text,
|
||||
fontSize: '14px'
|
||||
});
|
||||
wrapper.add_widget(label_w);
|
||||
|
||||
// Input — choose type based on op and alters
|
||||
var alter = alters[vf.field_name];
|
||||
var input_w;
|
||||
if (alter && alter.uitype === 'code'){
|
||||
// Dropdown
|
||||
input_w = self._build_code_input(vf, alter);
|
||||
} else if (vf.op === 'IN' || vf.op === 'NOT IN'){
|
||||
input_w = self._build_text_input(vf, 'a,b,c');
|
||||
} else if (vf.op === 'LIKE' || vf.op === 'NOT LIKE'){
|
||||
input_w = self._build_text_input(vf, '');
|
||||
} else {
|
||||
// =, !=, >, >=, <, <= etc.
|
||||
input_w = self._build_text_input(vf, '');
|
||||
}
|
||||
input_w.set_style('minWidth', '120px');
|
||||
input_w.set_style('maxWidth', '200px');
|
||||
wrapper.add_widget(input_w);
|
||||
vf.input_widget = input_w;
|
||||
|
||||
self.add_widget(wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
_build_text_input(vf, placeholder){
|
||||
var input_w = new bricks.UiStr({
|
||||
name: vf.var_name,
|
||||
value: '',
|
||||
placeholder: placeholder || '',
|
||||
width: '100%'
|
||||
});
|
||||
input_w.set_style('padding', '0.35rem 0.5rem');
|
||||
input_w.set_style('boxSizing', 'border-box');
|
||||
input_w.set_style('height', 'auto');
|
||||
input_w.bind('changed', this._input_changed.bind(this));
|
||||
input_w.dom_element.addEventListener('keydown', this._keydown_handle.bind(this));
|
||||
return input_w;
|
||||
}
|
||||
|
||||
_build_code_input(vf, alter){
|
||||
var input_opts = {
|
||||
name: vf.var_name,
|
||||
value: '',
|
||||
placeholder: '',
|
||||
width: '100%',
|
||||
dataurl: alter.dataurl || null,
|
||||
datamethod: alter.datamethod || 'GET',
|
||||
dataparams: alter.dataparams || {}
|
||||
};
|
||||
if (alter.data && Array.isArray(alter.data)){
|
||||
input_opts.data = alter.data;
|
||||
}
|
||||
if (alter.data_field){
|
||||
input_opts.data_field = alter.data_field;
|
||||
}
|
||||
var input_w = new bricks.UiCode(input_opts);
|
||||
input_w.set_style('padding', '0.25rem');
|
||||
input_w.set_style('boxSizing', 'border-box');
|
||||
input_w.set_style('height', 'auto');
|
||||
input_w.bind('changed', this._input_changed.bind(this));
|
||||
return input_w;
|
||||
}
|
||||
|
||||
_build_buttons(){
|
||||
var btn_box = new bricks.HBox({
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
});
|
||||
btn_box.set_style('alignItems', 'center');
|
||||
btn_box.set_style('gap', '0.5rem');
|
||||
|
||||
this.search_btn = new bricks.Button({
|
||||
label: this.opts.search_label || '搜索',
|
||||
orientation: 'horizontal',
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
});
|
||||
this.search_btn.bind('click', this._do_search.bind(this));
|
||||
btn_box.add_widget(this.search_btn);
|
||||
|
||||
this.clear_btn = new bricks.Button({
|
||||
label: this.opts.clear_label || '清空',
|
||||
orientation: 'horizontal',
|
||||
width: 'auto',
|
||||
height: 'auto'
|
||||
});
|
||||
this.clear_btn.bind('click', this._clear_search.bind(this));
|
||||
btn_box.add_widget(this.clear_btn);
|
||||
|
||||
this.add_widget(btn_box);
|
||||
}
|
||||
|
||||
_input_changed(event){
|
||||
var d = event.params || {};
|
||||
for (var k in d){
|
||||
this.filter_values[k] = d[k];
|
||||
}
|
||||
}
|
||||
|
||||
_keydown_handle(event){
|
||||
if (event.key === 'Enter'){
|
||||
this._do_search(event);
|
||||
}
|
||||
}
|
||||
|
||||
_get_all_values(){
|
||||
var values = {};
|
||||
var self = this;
|
||||
this.var_fields.forEach(function(vf){
|
||||
var w = vf.input_widget;
|
||||
if (!w) return;
|
||||
if (w.resultValue){
|
||||
values[vf.var_name] = w.resultValue() || '';
|
||||
} else if (w.getValue){
|
||||
values[vf.var_name] = w.getValue() || '';
|
||||
} else {
|
||||
values[vf.var_name] = '';
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
_do_search(event){
|
||||
var values = this._get_all_values();
|
||||
this.filter_values = values;
|
||||
this.dispatch('filter_search', { values: values });
|
||||
if (event && event.stopPropagation){
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
_clear_search(event){
|
||||
var self = this;
|
||||
this.var_fields.forEach(function(vf){
|
||||
var w = vf.input_widget;
|
||||
if (!w) return;
|
||||
if (w.setValue){
|
||||
w.setValue('');
|
||||
}
|
||||
});
|
||||
this.filter_values = {};
|
||||
this.dispatch('filter_clear', {});
|
||||
this._do_search(event);
|
||||
}
|
||||
|
||||
/* Programmatically set a filter value */
|
||||
set_filter_value(var_name, value){
|
||||
var self = this;
|
||||
this.var_fields.forEach(function(vf){
|
||||
if (vf.var_name === var_name && vf.input_widget){
|
||||
if (vf.input_widget.setValue){
|
||||
vf.input_widget.setValue(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
bricks.Factory.register('DataFilter', bricks.DataFilter);
|
||||
@ -32,6 +32,7 @@ bricks.DataViewer = class extends bricks.VBox {
|
||||
this.build_description_widget();
|
||||
this.build_toolbar_widget();
|
||||
this.build_searchbar_widget();
|
||||
this.build_datafilter_widget();
|
||||
this.build_records_area();
|
||||
await this.build_other();
|
||||
this.check_changed_row = null;
|
||||
@ -107,6 +108,37 @@ bricks.DataViewer = class extends bricks.VBox {
|
||||
this.searchbar_w.bind('search', this.search_event_handle.bind(this));
|
||||
this.add_widget(this.searchbar_w);
|
||||
}
|
||||
build_datafilter_widget(){
|
||||
if (!this.opts.data_filter){
|
||||
return;
|
||||
}
|
||||
var opts = {
|
||||
data_filter: this.opts.data_filter,
|
||||
browserfields: this.opts.browserfields || {},
|
||||
field_labels: this.opts.filter_labels || {},
|
||||
search_label: this.opts.filter_search_label || '搜索',
|
||||
clear_label: this.opts.filter_clear_label || '清空'
|
||||
};
|
||||
this.datafilter_w = new bricks.DataFilter(opts);
|
||||
this.datafilter_w.bind('filter_search', this.filter_event_handle.bind(this));
|
||||
this.datafilter_w.bind('filter_clear', this.filter_clear_handle.bind(this));
|
||||
this.add_widget(this.datafilter_w);
|
||||
}
|
||||
async filter_event_handle(event){
|
||||
var d = event.params || {};
|
||||
this.filter_values = d.values || {};
|
||||
this.old_params = null;
|
||||
this.select_row = null;
|
||||
this.active_item = null;
|
||||
this.data_offset = 0;
|
||||
if (this.loader){
|
||||
this.loader.pages = [];
|
||||
}
|
||||
await this.render({});
|
||||
}
|
||||
async filter_clear_handle(event){
|
||||
this.filter_values = {};
|
||||
}
|
||||
merge_search_params(params){
|
||||
var merged = bricks.extend({}, params || {});
|
||||
if (this.searchable){
|
||||
@ -117,6 +149,16 @@ bricks.DataViewer = class extends bricks.VBox {
|
||||
delete merged[search_param];
|
||||
}
|
||||
}
|
||||
/* data_filter: send the filter JSON + collected var values */
|
||||
if (this.opts.data_filter){
|
||||
merged['data_filter'] = JSON.stringify(this.opts.data_filter);
|
||||
var filter_values = this.filter_values || {};
|
||||
for (var k in filter_values){
|
||||
if (filter_values[k] !== '' && filter_values[k] !== null && filter_values[k] !== undefined){
|
||||
merged[k] = filter_values[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
async search_event_handle(event){
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user