feat: data_filter support via toolbar search button + popup Form

When CRUD JSON has data_filter in params:
- Toolbar shows a '搜索' button (name:'filter')
- Click opens PopupWindow with Form widget
- Form fields are dynamically generated from data_filter definition
  - Extracts all {var, field, op} from AND/OR/NOT nested structure
  - Supports dropdown (uitype:'code') via browserfields.alters config
  - Customizable labels via filter_labels option
- Form submit collects values → stored in this.filter_values
- merge_search_params() sends data_filter (JSON string) + each var value
  to backend API as URL parameters
- Backend .dspy uses sqlor.filter.DBFilter.gen(ns) for SQL WHERE clause

Deleted previous inline DataFilter widget approach; replaced with
popup Form pattern that matches existing CRUD edit/add form UX.
This commit is contained in:
yumoqing 2026-05-25 14:15:59 +08:00
parent 431245648d
commit 6d47eeb795
3 changed files with 102 additions and 281 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 data_filter.js dataviewer.js \
input.js registerfunction.js button.js accordion.js searchbar.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 \

View File

@ -1,247 +0,0 @@
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,7 +32,6 @@ 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;
@ -108,37 +107,6 @@ 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){
@ -239,9 +207,18 @@ bricks.DataViewer = class extends bricks.VBox {
edit_names.push(t.name);
});
}
/* data_filter: add search tool to toolbar */
if (this.opts.data_filter){
tbdesc.tools.push({
name:'filter',
label: this.opts.filter_label || '搜索',
tip: 'filter records',
icon: this.opts.filter_icon || bricks_resource('imgs/search.svg')
});
}
if (this.toolbar){
this.toolbar.tools.forEach(t => {
if (! edit_names.includes(t.name)){
if (! edit_names.includes(t.name) && t.name !== 'filter'){
tbdesc.tools.push(t);
}
});
@ -275,6 +252,10 @@ bricks.DataViewer = class extends bricks.VBox {
this.delete_record(this.select_row);
return;
}
if (tdesc.name == 'filter'){
await this.show_filter_form();
return;
}
var data = null;
if (this.select_row){
var r = this.select_row;
@ -283,6 +264,93 @@ bricks.DataViewer = class extends bricks.VBox {
console.log(tdesc.name, 'clicked ==================', tdesc.name, data)
this.dispatch(tdesc.name, data);
}
/* Recursively extract {var, field, op} from data_filter JSON tree */
get_filter_fields(){
var fields = [];
var self = this;
var alters = (this.opts.browserfields && this.opts.browserfields.alters) || {};
var labels = this.opts.filter_labels || {};
function extract(node){
if (!node || typeof node !== 'object') return;
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(extract);
}
} else if (key === 'NOT'){
extract(val);
} else if (key === 'field'){
if (node.var){
var alter = alters[node.field] || {};
var f = {
name: node.var,
label: labels[node.var] || node.field,
op: node.op || '='
};
if (alter.uitype === 'code'){
f.uitype = 'code';
if (alter.data && Array.isArray(alter.data)){
f.data = alter.data;
}
if (alter.dataurl){
f.dataurl = alter.dataurl;
f.datamethod = alter.datamethod || 'GET';
f.dataparams = alter.dataparams || {};
}
if (alter.data_field){
f.data_field = alter.data_field;
}
}
fields.push(f);
}
return;
}
}
}
extract(this.opts.data_filter);
return fields;
}
async show_filter_form(){
var fields = this.get_filter_fields();
var submit_url = this.opts.data_url || '';
var form_opts = {
submit_url: '#',
width: '100%',
height: '100%',
fields: fields
};
var form = new bricks.Form(form_opts);
var popup = new bricks.PopupWindow({
title: this.opts.filter_title || '搜索过滤',
icon: bricks_resource('imgs/search.svg'),
widget: this,
archor: 'cc',
movable: true,
resizable: true,
width: '50%',
height: 'auto'
});
form.bind('cancel', popup.dismiss.bind(popup));
form.bind('submited', this.filter_form_submited.bind(this, popup, form));
popup.add_widget(form);
popup.open();
}
async filter_form_submited(popup, form, event){
popup.dismiss();
this.filter_values = form._getValue();
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({});
}
get_edit_fields(){
var fs = this.row_options.fields;
this.fields = [];