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:
parent
431245648d
commit
6d47eeb795
@ -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 \
|
jsoncall.js myoperator.js scroll.js menu.js popup.js recorder.js \
|
||||||
modal.js running.js llmout.js glbviewer.js \
|
modal.js running.js llmout.js glbviewer.js \
|
||||||
markdown_viewer.js audio.js toolbar.js tab.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 \
|
tree.js multiple_state_image.js dynamiccolumn.js form.js message.js conform.js \
|
||||||
paging.js datagrid.js iframe.js cols.js echartsext.js \
|
paging.js datagrid.js iframe.js cols.js echartsext.js \
|
||||||
floaticonbar.js miniform.js wterm.js dynamicaccordion.js \
|
floaticonbar.js miniform.js wterm.js dynamicaccordion.js \
|
||||||
|
|||||||
@ -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);
|
|
||||||
@ -32,7 +32,6 @@ bricks.DataViewer = class extends bricks.VBox {
|
|||||||
this.build_description_widget();
|
this.build_description_widget();
|
||||||
this.build_toolbar_widget();
|
this.build_toolbar_widget();
|
||||||
this.build_searchbar_widget();
|
this.build_searchbar_widget();
|
||||||
this.build_datafilter_widget();
|
|
||||||
this.build_records_area();
|
this.build_records_area();
|
||||||
await this.build_other();
|
await this.build_other();
|
||||||
this.check_changed_row = null;
|
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.searchbar_w.bind('search', this.search_event_handle.bind(this));
|
||||||
this.add_widget(this.searchbar_w);
|
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){
|
merge_search_params(params){
|
||||||
var merged = bricks.extend({}, params || {});
|
var merged = bricks.extend({}, params || {});
|
||||||
if (this.searchable){
|
if (this.searchable){
|
||||||
@ -239,9 +207,18 @@ bricks.DataViewer = class extends bricks.VBox {
|
|||||||
edit_names.push(t.name);
|
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){
|
if (this.toolbar){
|
||||||
this.toolbar.tools.forEach(t => {
|
this.toolbar.tools.forEach(t => {
|
||||||
if (! edit_names.includes(t.name)){
|
if (! edit_names.includes(t.name) && t.name !== 'filter'){
|
||||||
tbdesc.tools.push(t);
|
tbdesc.tools.push(t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -275,6 +252,10 @@ bricks.DataViewer = class extends bricks.VBox {
|
|||||||
this.delete_record(this.select_row);
|
this.delete_record(this.select_row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (tdesc.name == 'filter'){
|
||||||
|
await this.show_filter_form();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var data = null;
|
var data = null;
|
||||||
if (this.select_row){
|
if (this.select_row){
|
||||||
var r = 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)
|
console.log(tdesc.name, 'clicked ==================', tdesc.name, data)
|
||||||
this.dispatch(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(){
|
get_edit_fields(){
|
||||||
var fs = this.row_options.fields;
|
var fs = this.row_options.fields;
|
||||||
this.fields = [];
|
this.fields = [];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user