diff --git a/bricks/build.sh b/bricks/build.sh index a59afea..fafe5d7 100755 --- a/bricks/build.sh +++ b/bricks/build.sh @@ -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 \ diff --git a/bricks/data_filter.js b/bricks/data_filter.js new file mode 100644 index 0000000..ab70aca --- /dev/null +++ b/bricks/data_filter.js @@ -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); diff --git a/bricks/dataviewer.js b/bricks/dataviewer.js index 6ecdfab..059466c 100644 --- a/bricks/dataviewer.js +++ b/bricks/dataviewer.js @@ -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){