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:
yumoqing 2026-05-25 14:02:58 +08:00
parent 5a94ae73d9
commit 431245648d
3 changed files with 290 additions and 1 deletions

View File

@ -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
View 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);

View File

@ -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){