Merge pull request #178 from getrebuild/search-reference-594

RB-594 Search reference
This commit is contained in:
RB 2020-06-13 21:56:27 +08:00 committed by GitHub
commit 99726ebb59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 479 additions and 156 deletions

View file

@ -0,0 +1,95 @@
/*
Copyright (c) REBUILD <https://getrebuild.com/> and its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.server.helper.datalist;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.configuration.ConfigEntry;
import com.rebuild.server.configuration.portals.ChartManager;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.server.metadata.entity.EasyMeta;
import com.rebuild.server.service.query.AdvFilterParser;
import com.rebuild.utils.JSONUtils;
/**
* 解析已知的个性化过滤条件
*
* @author devezhao
* @since 2020/6/13
*/
public class ProtocolFilterParser {
final private String protocolExpr;
/**
* @param protocolExpr via:xxx ref:xxx
*/
public ProtocolFilterParser(String protocolExpr) {
this.protocolExpr = protocolExpr;
}
/**
* @return
*/
public String toSqlWhere() {
String[] ps = protocolExpr.split(":");
switch (ps[0]) {
case "via" : {
return parseVia(ps[1]);
}
case "ref" : {
return parseRef(ps[1]);
}
}
return null;
}
/**
* @param content
* @return
*/
public String parseVia(String content) {
final ID anyId = ID.isId(content) ? ID.valueOf(content) : null;
if (anyId == null) return null;
// via Charts
if (anyId.getEntityCode() == EntityHelper.ChartConfig) {
ConfigEntry chart = ChartManager.instance.getChart(anyId);
if (chart == null) return null;
JSONObject filterExp = ((JSONObject) chart.getJSON("config")).getJSONObject("filter");
return new AdvFilterParser(filterExp).toSqlWhere();
}
return null;
}
/**
* @param content
* @return
*/
public String parseRef(String content) {
String[] fieldAndEntity = content.split("\\.");
if (fieldAndEntity.length != 2 || !MetadataHelper.checkAndWarnField(fieldAndEntity[1], fieldAndEntity[0])) {
return null;
}
Field field = MetadataHelper.getField(fieldAndEntity[1], fieldAndEntity[0]);
String referenceDataFilter = EasyMeta.valueOf(field).getExtraAttr("referenceDataFilter");
if (JSONUtils.wellFormat(referenceDataFilter)) {
JSONObject advFilter = JSON.parseObject(referenceDataFilter);
if (advFilter.get("items") != null && !advFilter.getJSONArray("items").isEmpty()) {
return new AdvFilterParser(advFilter).toSqlWhere();
}
}
return null;
}
}

View file

@ -13,7 +13,6 @@ import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.configuration.ConfigEntry;
import com.rebuild.server.configuration.portals.AdvFilterManager;
import com.rebuild.server.configuration.portals.ChartManager;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.server.service.query.AdvFilterParser;
@ -172,33 +171,38 @@ public class QueryParser {
// 过滤器
final StringBuilder sqlWhere = new StringBuilder("(1=1)");
List<String> wheres = new ArrayList<>();
// Default
String defaultFilter = dataListControl == null ? null : dataListControl.getDefaultFilter();
if (StringUtils.isNotBlank(defaultFilter)) {
sqlWhere.append(" and (").append(defaultFilter).append(')');
wheres.add(defaultFilter);
}
// appends ProtocolFilter
String protocolFilter = queryExpr.getString("protocolFilter");
if (StringUtils.isNotBlank(protocolFilter)) {
String where = new ProtocolFilterParser(protocolFilter).toSqlWhere();
if (StringUtils.isNotBlank(where)) wheres.add(where);
}
// appends AdvFilter
String advFilter = queryExpr.getString("advFilter");
if (ID.isId(advFilter)) {
String where = parseAdvFilter(ID.valueOf(advFilter));
if (StringUtils.isNotBlank(where)) {
sqlWhere.append(" and ").append(where);
}
if (StringUtils.isNotBlank(where)) wheres.add(where);
}
// appends Quick
JSONObject quickFilter = queryExpr.getJSONObject("filter");
if (quickFilter != null) {
String where = new AdvFilterParser(entity, quickFilter).toSqlWhere();
if (StringUtils.isNotBlank(where)) {
sqlWhere.append(" and ").append(where);
}
if (StringUtils.isNotBlank(where)) wheres.add(where);
}
final String sqlWhere = wheres.isEmpty() ? "1=1" : StringUtils.join(wheres.iterator(), " and ");
fullSql.append(" where ").append(sqlWhere);
// 排序
StringBuilder sqlSort = new StringBuilder(" order by ");
@ -246,20 +250,11 @@ public class QueryParser {
* @return
*/
private String parseAdvFilter(ID filterId) {
// via Charts
if (filterId.getEntityCode() == EntityHelper.ChartConfig) {
ConfigEntry chart = ChartManager.instance.getChart(filterId);
JSONObject filterExp = ((JSONObject) chart.getJSON("config")).getJSONObject("filter");
return new AdvFilterParser(entity, filterExp).toSqlWhere();
}
// AdvFilter
ConfigEntry advFilter = AdvFilterManager.instance.getAdvFilter(filterId);
if (advFilter != null) {
JSONObject filterExp = (JSONObject) advFilter.getJSON("filter");
return new AdvFilterParser(entity, filterExp).toSqlWhere();
}
return null;
}
}

View file

@ -36,11 +36,13 @@ public enum DisplayType {
BOOL("布尔", FieldType.BOOL, -1),
BARCODE("条形码", FieldType.TEXT, 300),
// 内部用/未开放,
// 内部
ID("主键", FieldType.PRIMARY, -1),
ANYREFERENCE("任意引用", FieldType.ANY_REFERENCE, -1),
LOCATION("位置", FieldType.STRING, 70),
STATE("状态", FieldType.SMALL_INT, -1),
ANYREFERENCE("任意引用", FieldType.ANY_REFERENCE, -1),
N2NREFERENCE("多引用", FieldType.REFERENCE_LIST, -1),
;

View file

@ -14,11 +14,10 @@ import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.dialect.FieldType;
import cn.devezhao.persist4j.dialect.Type;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.helper.SetUser;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.server.metadata.entity.DisplayType;
@ -51,7 +50,7 @@ import static cn.devezhao.commons.DateFormatUtils.getUTCDateFormat;
* @author devezhao
* @since 09/29/2018
*/
public class AdvFilterParser {
public class AdvFilterParser extends SetUser<AdvFilterParser> {
private static final Log LOG = LogFactory.getLog(AdvFilterParser.class);
@ -117,8 +116,9 @@ public class AdvFilterParser {
return null;
}
String equationHold = equation;
if ((equation = validEquation(equation)) == null) {
throw new FilterParseException("无效高级表达式 : " + equation);
throw new FilterParseException("无效高级表达式 : " + equationHold);
}
if ("OR".equalsIgnoreCase(equation)) {
@ -269,8 +269,6 @@ public class AdvFilterParser {
// 自定义函数
final ID currentUser = Application.getCurrentUser();
if (ParserTokens.BFD.equalsIgnoreCase(op)) {
value = getUTCDateFormat().format(addDay(-NumberUtils.toInt(value))) + ParserTokens.FULL_TIME;
} else if (ParserTokens.BFM.equalsIgnoreCase(op)) {
@ -290,9 +288,9 @@ public class AdvFilterParser {
} else if (ParserTokens.REY.equalsIgnoreCase(op)) {
value = getUTCDateFormat().format(addMonth(-NumberUtils.toInt(value) * 12)) + ParserTokens.FULL_TIME;
} else if (ParserTokens.SFU.equalsIgnoreCase(op)) {
value = currentUser.toLiteral();
value = getUser().toLiteral();
} else if (ParserTokens.SFB.equalsIgnoreCase(op)) {
Department dept = UserHelper.getDepartment(currentUser);
Department dept = UserHelper.getDepartment(getUser());
if (dept != null) {
value = dept.getIdentity().toString();
int ref = fieldMeta.getReferenceEntity().getEntityCode();
@ -305,7 +303,7 @@ public class AdvFilterParser {
}
}
} else if (ParserTokens.SFD.equalsIgnoreCase(op)) {
Department dept = UserHelper.getDepartment(currentUser);
Department dept = UserHelper.getDepartment(getUser());
if (dept != null) {
int refe = fieldMeta.getReferenceEntity().getEntityCode();
if (refe == EntityHelper.Department) {

View file

@ -53,7 +53,7 @@ public class ViewAddonsControll extends BaseControll implements PortalsConfigura
JSON config = ServletUtils.getRequestJson(request);
ID configId = ViewAddonsManager.instance.detectUseConfig(user, entity, applyType);
Record record = null;
Record record;
if (configId == null) {
record = EntityHelper.forNew(EntityHelper.LayoutConfig, user);
record.setString("belongEntity", entity);

View file

@ -12,9 +12,10 @@ import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.dialect.FieldType;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.configuration.portals.ClassificationManager;
import com.rebuild.server.configuration.portals.DataListManager;
import com.rebuild.server.helper.datalist.ProtocolFilterParser;
import com.rebuild.server.helper.fieldvalue.FieldValueWrapper;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.metadata.MetadataHelper;
@ -23,17 +24,18 @@ import com.rebuild.server.metadata.entity.DisplayType;
import com.rebuild.server.metadata.entity.EasyMeta;
import com.rebuild.server.service.bizz.UserHelper;
import com.rebuild.server.service.bizz.UserService;
import com.rebuild.server.service.query.AdvFilterParser;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseControll;
import com.rebuild.web.BaseEntityControll;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
@ -51,7 +53,7 @@ import java.util.Set;
*/
@Controller
@RequestMapping("/commons/search/")
public class ReferenceSearch extends BaseControll {
public class ReferenceSearch extends BaseEntityControll {
// 快速搜索引用字段
@RequestMapping({ "reference", "quick" })
@ -77,20 +79,13 @@ public class ReferenceSearch extends BaseControll {
// 引用字段数据过滤仅在搜索时有效
// 启用数据过滤后最近搜索将不可用
JSONObject advFilter = null;
String referenceDataFilter = EasyMeta.valueOf(referenceField).getExtraAttr("referenceDataFilter");
if (JSONUtils.wellFormat(referenceDataFilter)) {
advFilter = JSON.parseObject(referenceDataFilter);
if (advFilter.get("items") == null || advFilter.getJSONArray("items").isEmpty()) {
advFilter = null;
}
}
final String protocolFilter = new ProtocolFilterParser(null).parseRef(field + "." + entity);
String q = getParameter(request, "q");
// 为空则加载最近使用的
if (StringUtils.isBlank(q)) {
ID[] recently = null;
if (advFilter == null) {
if (protocolFilter == null) {
String type = getParameter(request, "type");
recently = Application.getRecentlyUsedCache().gets(user, referenceEntity.getName(), type);
}
@ -127,11 +122,8 @@ public class ReferenceSearch extends BaseControll {
String like = " like '%" + q + "%'";
String searchWhere = StringUtils.join(searchFields.iterator(), like + " or ") + like;
if (advFilter != null) {
String advFilterSql = new AdvFilterParser(advFilter).toSqlWhere();
if (advFilterSql != null) {
searchWhere = "(" + searchWhere + ") and " + advFilterSql;
}
if (protocolFilter != null) {
searchWhere = "(" + searchWhere + ") and (" + protocolFilter + ')';
}
String sql = MessageFormat.format("select {0},{1} from {2} where ( {3} )",
@ -299,4 +291,35 @@ public class ReferenceSearch extends BaseControll {
}
return result;
}
/**
* @see com.rebuild.web.base.general.GeneralDataListControll#pageList(String, HttpServletRequest, HttpServletResponse)
*/
@RequestMapping("reference-search-list")
public ModelAndView pageListSearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
String[] fieldAndEntity = getParameterNotNull(request,"field").split("\\.");
if (!MetadataHelper.checkAndWarnField(fieldAndEntity[1], fieldAndEntity[0])) {
response.sendError(404);
return null;
}
Entity entity = MetadataHelper.getEntity(fieldAndEntity[1]);
Field field = entity.getField(fieldAndEntity[0]);
Entity searchEntity = field.getReferenceEntity();
ModelAndView mv = createModelAndView("/general-entity/reference-search.jsp");
putEntityMeta(mv, searchEntity);
JSON config = DataListManager.instance.getFieldsLayout(searchEntity.getName(), getRequestUser(request));
mv.getModel().put("DataListConfig", JSON.toJSONString(config));
// 是否启用了字段过滤
String referenceDataFilter = EasyMeta.valueOf(field).getExtraAttr("referenceDataFilter");
if (referenceDataFilter != null && referenceDataFilter.length() > 10) {
mv.getModel().put("referenceFilter", "ref:" + getParameter(request, "field"));
} else {
mv.getModel().put("referenceFilter", StringUtils.EMPTY);
}
return mv;
}
}

View file

@ -92,7 +92,7 @@ public class BatchUpdateControll extends BaseControll {
// 不支持的字段
if (dt == DisplayType.FILE || dt == DisplayType.IMAGE || dt == DisplayType.AVATAR
|| dt == DisplayType.LOCATION || dt == DisplayType.SERIES || dt == DisplayType.ANYREFERENCE
|| dt == DisplayType.NTEXT) {
|| dt == DisplayType.NTEXT || dt == DisplayType.BARCODE || dt == DisplayType.N2NREFERENCE) {
continue;
}

View file

@ -50,7 +50,7 @@ public class GeneralDataListControll extends BaseEntityControll {
response.sendError(404);
return null;
}
final Entity thatEntity = MetadataHelper.getEntity(entity);
if (!thatEntity.isQueryable()) {
@ -62,7 +62,7 @@ public class GeneralDataListControll extends BaseEntityControll {
response.sendError(403, "你没有访问此实体的权限");
return null;
}
ModelAndView mv;
if (thatEntity.getMasterEntity() != null) {
mv = createModelAndView("/general-entity/slave-list.jsp", entity, user);
@ -70,7 +70,7 @@ public class GeneralDataListControll extends BaseEntityControll {
mv = createModelAndView("/general-entity/record-list.jsp", entity, user);
}
JSON config = DataListManager.instance.getFieldsLayout(entity, getRequestUser(request));
JSON config = DataListManager.instance.getFieldsLayout(entity, user);
mv.getModel().put("DataListConfig", JSON.toJSONString(config));
// 列表相关权限

View file

@ -71,7 +71,7 @@ public class ChartDataControll extends BaseControll {
JSONObject config = (JSONObject) configEntry.getJSON("config");
String sourceEntity = config.getString("entity");
String url = MessageFormat.format("../app/{0}/list?via={1}", sourceEntity, id);
String url = MessageFormat.format("../app/{0}/list#via={1}", sourceEntity, id);
response.sendRedirect(url);
}
}

View file

@ -22010,7 +22010,7 @@ fieldset[disabled] .full-calendar .fc-button:hover {
.select2-container--default .select2-selection--multiple .select2-selection__rendered .select2-selection__clear {
right: 20px;
font-size: 1.35rem;
top: -2px;
top: -1px;
font-weight: normal;
}

View file

@ -587,6 +587,8 @@ div.dataTables_wrapper div.dataTables_filter {
padding: 0 !important;
height: 100%;
width: 100%;
line-height: 1;
font-size: 0;
}
.modal-body.iframe iframe {
@ -755,7 +757,7 @@ a {
margin-right: 20px;
}
.rbform .select2-container--default .select2-selection--multiple::after,
/* .rbform .select2-container--default .select2-selection--multiple::after,
.type-REFERENCE .select2-container--default .select2-selection--single .select2-selection__arrow b:after {
content: '\f1c3';
font: normal normal normal 14px/1 'Material-Design-Iconic-Font';
@ -765,7 +767,7 @@ a {
position: absolute;
right: 8px;
top: 9px;
}
} */
.type-REFERENCE .select2-container--default .select2-selection--single .select2-selection__arrow b:after {
right: 7px;
@ -978,7 +980,7 @@ a {
.datetime-field .clean {
position: absolute;
right: 45px;
top: 1px;
top: 0;
font-size: 1rem;
font-weight: 700;
color: #404040;
@ -3495,6 +3497,36 @@ a.icon-link>.zmdi {
margin-left: -15px;
}
.badge-border {
display: inline-block;
border-color: #fbbc05;
color: #fbbc05;
font-size: 1rem;
font-weight: normal;
padding: 3px 6px;
position: relative;
padding-right: 30px;
cursor: default;
overflow: hidden;
}
.badge-border .close {
position: absolute;
height: 100%;
width: 24px;
border-left: 1px solid #fbbc05;
top: 0;
right: 0;
padding-top: 3px;
opacity: 1;
text-shadow: none;
}
.badge-border:hover .close {
background-color: #fbbc05;
color: #fff !important;
}
/* text overflow */
.aside-header .title,

View file

@ -419,9 +419,9 @@ function __renderRichContent(e) {
}
{e.type === 3 && <div>
{contentMore.showWhere > 0 &&
<div><span>示位置 : </span> {__findMaskTexts(contentMore.showWhere, ANN_OPTIONS).join('、')}</div>}
<div><span>示位置 : </span> {__findMaskTexts(contentMore.showWhere, ANN_OPTIONS).join('、')}</div>}
{(contentMore.timeStart || contentMore.timeEnd) &&
<div><span>示时间 : </span> {contentMore.timeStart || ''} {contentMore.timeEnd}</div>}
<div><span>示时间 : </span> {contentMore.timeStart || ''} {contentMore.timeEnd}</div>}
</div>
}
</div>

View file

@ -448,7 +448,7 @@ class AnnouncementOptions extends React.Component {
render() {
return <div className="feed-options announcement">
<dl className="row mb-1">
<dt className="col-12 col-lg-3">同时示在</dt>
<dt className="col-12 col-lg-3">同时示在</dt>
<dd className="col-12 col-lg-9 mb-0" ref={(c) => this._showWhere = c}>
<label className="custom-control custom-checkbox custom-control-inline">
<input className="custom-control-input" name="showOn" type="checkbox" value={1} disabled={this.props.readonly} />
@ -460,12 +460,12 @@ class AnnouncementOptions extends React.Component {
</label>
<label className="custom-control custom-checkbox custom-control-inline">
<input className="custom-control-input" name="showOn" type="checkbox" value={4} disabled={this.props.readonly} />
<span className="custom-control-label">登录页 <i className="zmdi zmdi-help zicon down-3" data-toggle="tooltip" title="选择登录页示请注意不要发布敏感信息" /></span>
<span className="custom-control-label">登录页 <i className="zmdi zmdi-help zicon down-3" data-toggle="tooltip" title="选择登录页示请注意不要发布敏感信息" /></span>
</label>
</dd>
</dl>
<dl className="row">
<dt className="col-12 col-lg-3 pt-2">示时间</dt>
<dt className="col-12 col-lg-3 pt-2">示时间</dt>
<dd className="col-12 col-lg-9" ref={(c) => this._showTime = c}>
<div className="input-group">
<input type="text" className="form-control form-control-sm" placeholder="现在" />

View file

@ -96,7 +96,7 @@ class MessageList extends React.Component {
}, () => {
$.get(`/notification/messages?type=${this.state.type}&pageNo=${this.state.page}`, (res) => {
this.setState({ list: res.data || [] }, () => {
if (focusItem) setTimeout(() => $gotoSection($('.notification.focus').offset().top - 66), 200)
if (focusItem && $('.notification.focus').length > 0) setTimeout(() => $gotoSection($('.notification.focus').offset().top - 66), 200)
focusItem = null
})
})

View file

@ -33,12 +33,12 @@ class RbModal extends React.Component {
}
componentDidMount() {
const root = $(this._rbmodal).modal({ show: true, backdrop: this.props.backdrop === false ? false : 'static', keyboard: false })
const $root = $(this._rbmodal).modal({ show: true, backdrop: this.props.backdrop === false ? false : 'static', keyboard: false })
.on('hidden.bs.modal', () => {
$keepModalOpen()
if (this.props.disposeOnHide === true) {
root.modal('dispose')
$unmount(root.parent())
$root.modal('dispose')
$unmount($root.parent())
}
})
}
@ -56,13 +56,13 @@ class RbModal extends React.Component {
resize() {
if (this.props.children) return
const root = $(this._rbmodal)
const $root = $(this._rbmodal)
$setTimeout(() => {
let iframe = root.find('iframe')
let height = iframe.contents().find('.main-content').outerHeight()
if (height === 0) height = iframe.contents().find('body').height()
const $iframe = $root.find('iframe')
let height = $iframe.contents().find('.main-content').outerHeight()
if (height === 0) height = $iframe.contents().find('body').height()
// else height += 45 // .main-content's padding
root.find('.modal-body').height(height)
$root.find('.modal-body').height(height)
this.setState({ frameLoad: false })
}, 20, 'RbModal-resize')
}
@ -148,7 +148,7 @@ class RbFormHandler extends RbModalHandler {
const id = target.dataset.id || target.name
if (!id) return
const val = target.type === 'checkbox' ? target.checked : target.value
let s = {}
const s = {}
s[id] = val
this.setState(s, call)
this.handleChangeAfter(id, val)
@ -157,10 +157,9 @@ class RbFormHandler extends RbModalHandler {
componentWillUnmount() {
// destroy select2
const ss = this.__select2
if (ss) {
if ($.type(ss) === 'array') $(ss).each(function () { this.select2('destroy') })
else ss.select2('destroy')
if (this.__select2) {
if ($.type(this.__select2) === 'array') $(this.__select2).each(function () { this.select2('destroy') })
else this.__select2.select2('destroy')
this.__select2 = null
}
}
@ -181,21 +180,22 @@ class RbAlert extends React.Component {
}
render() {
let style = {}
if (this.props.width) style.maxWidth = ~~this.props.width
return <div className="modal rbalert" ref={(c) => this._dlg = c} tabIndex={this.state.tabIndex || -1}>
<div className="modal-dialog modal-dialog-centered" style={style}>
<div className="modal-content">
<div className="modal-header pb-0">
<button className="close" type="button" onClick={() => this.hide()}><span className="zmdi zmdi-close" /></button>
</div>
<div className="modal-body">
{this.renderContent()}
const styles = {}
if (this.props.width) styles.maxWidth = ~~this.props.width
return (
<div className="modal rbalert" ref={(c) => this._dlg = c} tabIndex={this.state.tabIndex || -1}>
<div className="modal-dialog modal-dialog-centered" style={styles}>
<div className="modal-content">
<div className="modal-header pb-0">
<button className="close" type="button" onClick={() => this.hide()}><span className="zmdi zmdi-close" /></button>
</div>
<div className="modal-body">
{this.renderContent()}
</div>
</div>
</div>
</div>
</div>
)
}
renderContent() {
@ -210,21 +210,23 @@ class RbAlert extends React.Component {
const cancel = (this.props.cancel || this.hide).bind(this)
const confirm = (this.props.confirm || this.hide).bind(this)
return <div className="text-center ml-6 mr-6">
<div className={`text-${type}`}><i className={`modal-main-icon zmdi zmdi-${icon}`} /></div>
{this.props.title && <h4 className="mb-2 mt-3">{this.props.title}</h4>}
<div className={this.props.title ? '' : 'mt-3'}>{content}</div>
<div className="mt-4 mb-3">
<button disabled={this.state.disable} className="btn btn-space btn-secondary" type="button" onClick={cancel}>{this.props.cancelText || '取消'}</button>
<button disabled={this.state.disable} className={`btn btn-space btn-${type}`} type="button" onClick={confirm}>{this.props.confirmText || '确定'}</button>
return (
<div className="text-center ml-6 mr-6">
<div className={`text-${type}`}><i className={`modal-main-icon zmdi zmdi-${icon}`} /></div>
{this.props.title && <h4 className="mb-2 mt-3">{this.props.title}</h4>}
<div className={this.props.title ? '' : 'mt-3'}>{content}</div>
<div className="mt-4 mb-3">
<button disabled={this.state.disable} className="btn btn-space btn-secondary" type="button" onClick={cancel}>{this.props.cancelText || '取消'}</button>
<button disabled={this.state.disable} className={`btn btn-space btn-${type}`} type="button" onClick={confirm}>{this.props.confirmText || '确定'}</button>
</div>
</div>
</div>
)
}
componentDidMount() {
const root = $(this._dlg).modal({ show: true, keyboard: true }).on('hidden.bs.modal', () => {
root.modal('dispose')
$unmount(root.parent())
const $root = $(this._dlg).modal({ show: true, keyboard: true }).on('hidden.bs.modal', () => {
$root.modal('dispose')
$unmount($root.parent())
})
}
@ -253,7 +255,7 @@ class RbAlert extends React.Component {
}
ext = ext || {}
let props = { ...ext, title: titleOrExt }
const props = { ...ext, title: titleOrExt }
if (ext.html === true) props.htmlMessage = message
else props.message = message
renderRbcomp(<RbAlert {...props} />, null, ext.call)
@ -275,13 +277,15 @@ class RbHighbar extends React.Component {
? <div className="message pl-0" dangerouslySetInnerHTML={{ __html: this.props.htmlMessage }} />
: <div className="message pl-0">{this.props.message}</div>
return <div ref={(c) => this._rbhighbar = c} className={`rbhighbar animated faster ${this.state.animatedClass}`}>
<div className={`alert alert-dismissible alert-${(this.props.type || 'warning')} mb-0`}>
<button className="close" type="button" onClick={this.close}><i className="zmdi zmdi-close" /></button>
<div className="icon"><i className={`zmdi zmdi-${icon}`} /></div>
{content}
return (
<div ref={(c) => this._rbhighbar = c} className={`rbhighbar animated faster ${this.state.animatedClass}`}>
<div className={`alert alert-dismissible alert-${(this.props.type || 'warning')} mb-0`}>
<button className="close" type="button" onClick={this.close}><i className="zmdi zmdi-close" /></button>
<div className="icon"><i className={`zmdi zmdi-${icon}`} /></div>
{content}
</div>
</div>
</div>
)
}
componentDidMount() {
@ -328,25 +332,29 @@ function RbAlertBox(props) {
const type = (props || {}).type || 'warning'
const icon = type === 'success' ? 'check' : (type === 'danger' ? 'close-circle-o' : 'info-outline')
return <div className={`alert alert-icon alert-icon-border alert-dismissible alert-sm alert-${type}`}>
<div className="icon"><i className={`zmdi zmdi-${icon}`} /></div>
<div className="message">
<a className="close" data-dismiss="alert"><i className="zmdi zmdi-close" /></a>
<p>{props.message || 'INMESSAGE'}</p>
return (
<div className={`alert alert-icon alert-icon-border alert-dismissible alert-sm alert-${type}`}>
<div className="icon"><i className={`zmdi zmdi-${icon}`} /></div>
<div className="message">
<a className="close" data-dismiss="alert"><i className="zmdi zmdi-close" /></a>
<p>{props.message || 'INMESSAGE'}</p>
</div>
</div>
</div>
)
}
// ~~
function RbSpinner(props) {
const spinner = <div className="rb-spinner">
{$.browser.msie
? <span className="spinner-border spinner-border-xl text-primary"></span>
: <svg width="40px" height="40px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle fill="none" strokeWidth="4" strokeLinecap="round" cx="33" cy="33" r="30" className="circle" />
</svg>
}
</div>
const spinner = (
<div className="rb-spinner">
{$.browser.msie
? <span className="spinner-border spinner-border-xl text-primary"></span>
: <svg width="40px" height="40px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle fill="none" strokeWidth="4" strokeLinecap="round" cx="33" cy="33" r="30" className="circle" />
</svg>
}
</div>
)
if (props && props.fully === true) return <div className="rb-loading rb-loading-active">{spinner}</div>
return spinner
}
@ -507,19 +515,26 @@ class UserSelector extends React.Component {
const UserShow = function (props) {
const viewUrl = props.id ? `#!/View/User/${props.id}` : null
const avatarUrl = `${rb.baseUrl}/account/user-avatar/${props.id}`
return <a href={viewUrl} className="user-show" title={props.name} onClick={props.onClick}>
<div className={`avatar ${props.showName === true ? ' float-left' : ''}`}>{props.icon ? <i className={props.icon} /> : <img src={avatarUrl} alt="Avatar" />}</div>
{props.showName && (<div className={`text-truncate name ${props.deptName ? 'vm' : ''}`}>{props.name}{props.deptName && <em>{props.deptName}</em>}</div>)}
</a>
return (
<a href={viewUrl} className="user-show" title={props.name} onClick={props.onClick}>
<div className={`avatar ${props.showName === true ? ' float-left' : ''}`}>{props.icon ? <i className={props.icon} /> : <img src={avatarUrl} alt="Avatar" />}</div>
{props.showName && (<div className={`text-truncate name ${props.deptName ? 'vm' : ''}`}>{props.name}{props.deptName && <em>{props.deptName}</em>}</div>)}
</a>
)
}
/**
* JSX 组件渲染
* @param {*} jsx
* @param {*} target id or object of element
* @param {*} target id or object of element (or function of callback)
* @param {*} call callback
*/
const renderRbcomp = function (jsx, target, call) {
if (typeof target === 'function') {
call = target
target = null
}
target = target || $random('react-comps-')
if ($.type(target) === 'string') { // element id
const container = document.getElementById(target)

View file

@ -191,6 +191,7 @@ class RbList extends React.Component {
pageSize: this.pageSize,
filter: this.lastFilter,
advFilter: this.advFilterId,
protocolFilter: wpc.protocolFilter,
sort: fieldSort,
reload: this.pageNo === 1
}
@ -751,12 +752,10 @@ const AdvFilters = {
/**
* @param {Element} el 控件
* @param {String} entity 实体
* @param {ID} viaFilter 默认高级过滤 ID
*/
init(el, entity, viaFilter) {
init(el, entity) {
this.__el = $(el)
this.__entity = entity
this.__viaFilter = viaFilter
this.__el.find('.J_advfilter').click(() => {
this.showAdvFilter(null, this.current)
@ -833,16 +832,8 @@ const AdvFilters = {
$ghost.appendTo($('#asideFilters').empty())
}
// 使
if (that.__viaFilter) {
RbListPage._RbList.setAdvFilter(that.__viaFilter)
that.__viaFilter = null
}
else {
if (!$defaultFilter) $defaultFilter = $('.adv-search .dropdown-item:eq(0)')
$defaultFilter.trigger('click')
}
if (!$defaultFilter) $defaultFilter = $('.adv-search .dropdown-item:eq(0)')
$defaultFilter.trigger('click')
})
},
@ -910,13 +901,22 @@ const AdvFilters = {
// init: DataList
$(document).ready(() => {
const gs = $urlp('gs', location.hash)
const viaFilter = $urlp('via')
const via = $urlp('via', location.hash)
if (via) {
wpc.protocolFilter = `via:${via}`
const $cleanVia = $('<div class="badge badge-border float-left ml-2 mt-1">当前数据已过滤<a class="close" title="查看全部数据">&times;</a></div>').appendTo('.dataTables_filter')
$cleanVia.find('a').click(() => {
wpc.protocolFilter = null
RbListPage.reload()
$cleanVia.remove()
})
}
const gs = $urlp('gs', location.hash)
if (gs) $('.search-input-gs, .input-search>input').val($decode(gs))
if (wpc.entity) {
RbListPage.init(wpc.listConfig, wpc.entity, wpc.privileges)
if (!(wpc.advFilter === false)) AdvFilters.init('.adv-search', wpc.entity[0], viaFilter)
if (wpc.advFilter !== false) AdvFilters.init('.adv-search', wpc.entity[0])
}
})

View file

@ -20,13 +20,15 @@ class RbFormModal extends React.Component {
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header modal-header-colored">
{this.state.icon && (<span className={'icon zmdi zmdi-' + this.state.icon} />)}
{this.state.icon
&& <span className={'icon zmdi zmdi-' + this.state.icon} />}
<h3 className="modal-title">{this.state.title || '新建'}</h3>
{rb.isAdminUser ? <a className="close s" href={rb.baseUrl + '/admin/entity/' + this.state.entity + '/form-design'} title="配置布局" target="_blank"><span className="zmdi zmdi-settings"></span></a> : null}
{rb.isAdminUser
&& <a className="close s" href={rb.baseUrl + '/admin/entity/' + this.state.entity + '/form-design'} title="配置布局" target="_blank"><span className="zmdi zmdi-settings"></span></a>}
<button className="close md-close" type="button" onClick={() => this.hide()}><span className="zmdi zmdi-close"></span></button>
</div>
<div className={'modal-body rb-loading' + (this.state.inLoad ? ' rb-loading-active' : '')}>
{this.state.alertMessage && (<div className="alert alert-warning rbform-alert">{this.state.alertMessage}</div>)}
{this.state.alertMessage && <div className="alert alert-warning rbform-alert">{this.state.alertMessage}</div>}
{this.state.formComponent}
{this.state.inLoad && <RbSpinner />}
</div>
@ -67,10 +69,12 @@ class RbFormModal extends React.Component {
}
renderFromError(message) {
const error = <div className="alert alert-danger alert-icon mt-5 w-75 mlr-auto">
<div className="icon"><i className="zmdi zmdi-alert-triangle"></i></div>
<div className="message" dangerouslySetInnerHTML={{ __html: '<strong>抱歉!</strong> ' + message }}></div>
</div>
const error = (
<div className="alert alert-danger alert-icon mt-5 w-75 mlr-auto">
<div className="icon"><i className="zmdi zmdi-alert-triangle"></i></div>
<div className="message" dangerouslySetInnerHTML={{ __html: '<strong>抱歉!</strong> ' + message }}></div>
</div>
)
this.setState({ formComponent: error }, () => this.setState({ inLoad: false }))
}
@ -832,7 +836,15 @@ class RbFormReference extends RbFormElement {
renderElement() {
if (this.props.readonly) return super.renderElement(this.props.value ? this.props.value.text : null)
return <select ref={(c) => this._fieldValue = c} className="form-control form-control-sm" />
// return <select ref={(c) => this._fieldValue = c} className="form-control form-control-sm" />
return (
<div className="input-group datetime-field">
<select ref={(c) => this._fieldValue = c} className="form-control form-control-sm" />
<div className="input-group-append">
<button className="btn btn-secondary" type="button" onClick={this.showSearch}><i className="icon zmdi zmdi-search" /></button>
</div>
</div>
)
}
renderViewElement() {
@ -911,6 +923,28 @@ class RbFormReference extends RbFormElement {
this.handleChange({ target: { value: val.id } }, true)
} else this.__select2.val(null).trigger('change')
}
showSearch = () => {
const that = this
referenceSearch__call = function (selected) {
selected = selected[0]
if ($(that._fieldValue).find(`option[value="${selected}"]`).length > 0) {
that.__select2.val(selected).trigger('change')
} else {
$.get(`/commons/search/read-labels?ids=${selected}`, (res) => {
const o = new Option(res.data[selected], selected, true, true)
that.__select2.append(o).trigger('change')
})
}
that.__searcher.hide()
}
if (this.__searcher) this.__searcher.show()
else {
const searchUrl = `${rb.baseUrl}/commons/search/reference-search-list?field=${this.props.field}.${this.props.$$$parent.props.entity}`
renderRbcomp(<ReferenceSearcher url={searchUrl} title={`查询${this.props.label}`} />, function () { that.__searcher = this })
}
}
}
class RbFormClassification extends RbFormElement {
@ -974,8 +1008,9 @@ class RbFormClassification extends RbFormElement {
if (this.__selector) this.__selector.show()
else {
const p = this.props
const that = this
renderRbcomp(<ClassificationSelector entity={p.$$$parent.state.entity} field={p.field} label={p.label} openLevel={p.openLevel} $$$parent={this} />,
null, function () { this.__selector = this })
null, function () { that.__selector = this })
}
}
@ -1081,7 +1116,7 @@ class RbFormBarcode extends RbFormElement {
}
renderViewElement() {
if (!this.state.value) return null
if (!this.state.value) return super.renderViewElement()
const codeUrl = `${rb.baseUrl}/commons/barcode/render${this.props.barcodeType === 'QRCODE' ? '-qr' : ''}?t=${$encode(this.state.value)}`
return <div className="img-field barcode"><a className="img-thumbnail" title={this.state.value}><img src={codeUrl} alt={this.state.value} /></a></div>
}
@ -1316,6 +1351,34 @@ class ClassificationSelector extends React.Component {
}
}
// see `reference-search.jsp`
// eslint-disable-next-line no-unused-vars
var referenceSearch__call = function (selected) {/* NOOP */ }
class ReferenceSearcher extends RbModal {
constructor(props) {
super(props)
}
render() {
return (
<div className="modal rbmodal colored-header colored-header-primary" ref={(c) => this._rbmodal = c}>
<div className="modal-dialog" style={{ maxWidth: 1220 }}>
<div className="modal-content" style={{ maxWidth: 1220 }}>
<div className="modal-header modal-header-colored">
<h3 className="modal-title">{this.props.title || '查询'}</h3>
<button className="close" type="button" onClick={() => this.hide()}><span className="zmdi zmdi-close" /></button>
</div>
<div className="modal-body iframe">
<iframe src={this.props.url} frameBorder="0" style={{ minHeight: 430, maxHeight: '100%' }} />
</div>
</div>
</div>
</div>
)
}
}
//
// eslint-disable-next-line no-unused-vars
class DeleteConfirm extends RbAlert {

View file

@ -484,7 +484,8 @@ var $initReferenceSelect2 = function (el, field) {
noResults: function () { return (search_input || '').length > 0 ? '未找到结果' : '输入关键词搜索' },
inputTooShort: function () { return '输入关键词搜索' },
searching: function () { return '搜索中...' },
maximumSelected: function () { return '只能选择 1 ' }
maximumSelected: function () { return '只能选择 1 ' },
removeAllItems: function () { return '清除' }
},
theme: `default ${field.appendClass || ''}`
})

View file

@ -61,7 +61,7 @@
<div class="input-group-append"><button title="高级查询" class="btn btn-secondary J_advfilter" type="button"><i class="icon zmdi zmdi-filter-list"></i></button></div>
</div>
</div>
<div class="input-group input-search">
<div class="input-group input-search float-left">
<input class="form-control" type="text" placeholder="查询${entityLabel}" maxlength="40">
<span class="input-group-btn"><button class="btn btn-secondary" type="button"><i class="icon zmdi zmdi-search"></i></button></span>
</div>

View file

@ -0,0 +1,69 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<%@ include file="/_include/Head.jsp"%>
<title>查询${entityLabel}</title>
<style type="text/css">
.rb-datatable-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #f7f7f7;
z-index: 10;
}
#react-list {
margin-top: 68px;
}
</style>
</head>
<body class="dialog">
<div class="main-content container-fluid p-0">
<div class="card card-table">
<div class="card-body">
<div class="dataTables_wrapper container-fluid">
<div class="row rb-datatable-header">
<div class="col-6">
<div class="dataTables_filter">
<div class="input-group input-search">
<input class="form-control" type="text" placeholder="查询${entityLabel}" maxlength="40">
<span class="input-group-btn"><button class="btn btn-secondary" type="button"><i class="icon zmdi zmdi-search"></i></button></span>
</div>
</div>
</div>
<div class="col-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-primary J_select"><i class="icon zmdi zmdi-check"></i> 确定</button>
</div>
</div>
</div>
<div id="react-list" class="rb-loading rb-loading-active data-list">
<%@ include file="/_include/Spinner.jsp"%>
</div>
</div>
</div>
</div>
</div>
<%@ include file="/_include/Foot.jsp"%>
<script>
window.__PageConfig = {
type: 'RecordList',
entity: ['${entityName}','${entityLabel}','${entityIcon}'],
listConfig: ${DataListConfig},
advFilter: false,
protocolFilter: '${referenceFilter}'
}
</script>
<script src="${baseUrl}/assets/js/rb-datalist.jsx" type="text/babel"></script>
<script type="text/babel">
$(document).ready(function () {
$('.J_select').click(function () {
const ss = RbListPage._RbList.getSelectedIds()
if (ss.length > 0 && parent && parent.referenceSearch__call) parent.referenceSearch__call(ss)
else console.log(ss)
})
})
</script>
</body>
</html>

View file

@ -59,7 +59,7 @@
<div class="input-group-append"><button title="高级查询" class="btn btn-secondary J_advfilter" type="button"><i class="icon zmdi zmdi-filter-list"></i></button></div>
</div>
</div>
<div class="input-group input-search">
<div class="input-group input-search float-left">
<input class="form-control" type="text" placeholder="查询${entityLabel}" maxlength="40">
<span class="input-group-btn"><button class="btn btn-secondary" type="button"><i class="icon zmdi zmdi-search"></i></button></span>
</div>

View file

@ -0,0 +1,30 @@
/*
Copyright (c) REBUILD <https://getrebuild.com/> and its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.server.helper.datalist;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.server.TestSupport;
import com.rebuild.server.metadata.EntityHelper;
import org.junit.Test;
/**
* @author devezhao
* @since 2020/6/13
*/
public class ProtocolFilterParserTest extends TestSupport {
@Test
public void parseVia() {
System.out.println(new ProtocolFilterParser(null).parseVia(ID.newId(EntityHelper.ChartConfig).toLiteral()));
}
@Test
public void parseRef() {
System.out.println(new ProtocolFilterParser(null).parseRef("REFERENCE.TESTALLFIELDS"));
}
}