Fix 3.5.0 beta2 (#683)

1. 表单引用支持任意引用
2. 分组关联触发器设置 (LAB),必填,格式验证
3. ANYREF 允许重复选项
4. 去重连接*N

* export fields

* refform 3

* be: validAdvFilter

* fix: GroupAggregationRefresh fake源记录

* be: escapeSql
This commit is contained in:
REBUILD 企业管理系统 2023-11-25 22:32:45 +08:00 committed by GitHub
parent 67e12fb774
commit 87e64ab0d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 174 additions and 110 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 15d7f439a84cef4be39dadada2d51e2e6ab4f375
Subproject commit 222f88ffd578ce0a87622e472a855dc061ae97a7

View file

@ -27,6 +27,7 @@ import com.rebuild.core.metadata.impl.EasyFieldConfigProps;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.query.AdvFilterParser;
import com.rebuild.core.service.query.ParseHelper;
import com.rebuild.core.support.SetUser;
import com.rebuild.core.support.general.FieldValueHelper;
import com.rebuild.core.support.i18n.Language;
@ -217,17 +218,17 @@ public abstract class ChartData extends SetUser implements ChartSpec {
}
}
JSONObject filterExp = config.getJSONObject("filter");
if (filterExp == null || filterExp.isEmpty()) {
return previewFilter + "(1=1)";
JSONObject filterExpr = config.getJSONObject("filter");
if (ParseHelper.validAdvFilter(filterExpr)) {
AdvFilterParser filterParser = new AdvFilterParser(filterExpr);
String sqlWhere = filterParser.toSqlWhere();
if (sqlWhere != null) {
sqlWhere = previewFilter + sqlWhere;
}
return StringUtils.defaultIfBlank(sqlWhere, "(1=1)");
}
AdvFilterParser filterParser = new AdvFilterParser(filterExp);
String sqlWhere = filterParser.toSqlWhere();
if (sqlWhere != null) {
sqlWhere = previewFilter + sqlWhere;
}
return StringUtils.defaultIfBlank(sqlWhere, "(1=1)");
return previewFilter + "(1=1)";
}
/**

View file

@ -231,7 +231,7 @@ public class RecordCheckout {
if (refEntity.getEntityCode() == EntityHelper.User) {
String sql = MessageFormat.format(
"select userId from User where loginName = ''{0}'' or email = ''{0}'' or fullName = ''{0}''",
CommonsUtils.escapeSql(val2Text.toString()));
CommonsUtils.escapeSql(val2Text));
query = Application.createQueryNoFilter(sql);
} else {
// 查找引用实体的名称字段和自动编号字段
@ -248,7 +248,7 @@ public class RecordCheckout {
String.format("select %s from %s where ", refEntity.getPrimaryField().getName(), refEntity.getName()));
for (String qf : queryFields) {
sql.append(
String.format("%s = '%s' or ", qf, CommonsUtils.escapeSql(val2Text.toString())));
String.format("%s = '%s' or ", qf, CommonsUtils.escapeSql(val2Text)));
}
sql = new StringBuilder(sql.substring(0, sql.length() - 4));

View file

@ -105,6 +105,12 @@ public class AdvFilterParser extends SetUser {
this.filterExpr = filterExpr;
this.rootEntity = rootEntity;
this.varRecord = null;
String entityName = filterExpr.getString("entity");
if (entityName != null) {
Assert.isTrue(entityName.equalsIgnoreCase(this.rootEntity.getName()),
"Filter#2 uses different entities : " + entityName + "/" + this.rootEntity.getName());
}
}
/**
@ -115,6 +121,12 @@ public class AdvFilterParser extends SetUser {
this.filterExpr = filterExpr;
this.rootEntity = MetadataHelper.getEntity(varRecord.getEntityCode());
this.varRecord = License.isRbvAttached() ? varRecord : null;
String entityName = filterExpr.getString("entity");
if (entityName != null) {
Assert.isTrue(entityName.equalsIgnoreCase(this.rootEntity.getName()),
"Filter#3 uses different entities : " + entityName + "/" + this.rootEntity.getName());
}
}
/**

View file

@ -32,10 +32,7 @@ public class FilterRecordChecker {
* @return
*/
public boolean check(ID recordId) {
if (filterExpr == null || filterExpr.isEmpty()
|| filterExpr.getJSONArray("items") == null || filterExpr.getJSONArray("items").isEmpty()) {
return true;
}
if (!ParseHelper.validAdvFilter(filterExpr)) return true;
Entity entity = MetadataHelper.getEntity(recordId.getEntityCode());

View file

@ -29,6 +29,7 @@ import com.rebuild.core.service.general.GeneralEntityServiceContextHolder;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.general.RecordDifference;
import com.rebuild.core.service.query.AdvFilterParser;
import com.rebuild.core.service.query.ParseHelper;
import com.rebuild.core.service.query.QueryHelper;
import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.core.service.trigger.ActionType;
@ -161,7 +162,7 @@ public class FieldAggregation extends TriggerAction {
// 聚合数据过滤
JSONObject dataFilter = ((JSONObject) actionContext.getActionContent()).getJSONObject("dataFilter");
String dataFilterSql = null;
if (dataFilter != null && !dataFilter.isEmpty()) {
if (ParseHelper.validAdvFilter(dataFilter)) {
dataFilterSql = new AdvFilterParser(dataFilter, operatingContext.getFixedRecordId()).toSqlWhere();
}

View file

@ -50,7 +50,8 @@ public class FieldWritebackRefresh {
fa.targetEntity = parent.targetEntity;
fa.targetRecordIds = Collections.singleton(beforeValue);
Record fakeSourceRecord = EntityHelper.forUpdate(EntityHelper.newUnsavedId(fa.sourceEntity.getEntityCode()), triggerUser, false);
ID fakeSourceId = EntityHelper.newUnsavedId(fa.sourceEntity.getEntityCode());
Record fakeSourceRecord = EntityHelper.forUpdate(fakeSourceId, triggerUser, false);
OperatingContext oCtx = OperatingContext.create(triggerUser, BizzPermission.NONE, fakeSourceRecord, fakeSourceRecord);
fa.targetRecordData = fa.buildTargetRecordData(oCtx, true);

View file

@ -29,6 +29,7 @@ import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.core.service.trigger.ActionType;
import com.rebuild.core.service.trigger.TriggerException;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
@ -223,8 +224,8 @@ public class GroupAggregation extends FieldAggregation {
}
}
qFields.add(String.format("%s = '%s'", targetField, val));
qFieldsFollow.add(String.format("%s = '%s'", sourceField, val));
qFields.add(String.format("%s = '%s'", targetField, CommonsUtils.escapeSql(val)));
qFieldsFollow.add(String.format("%s = '%s'", sourceField, CommonsUtils.escapeSql(val)));
allNull = false;
}
@ -232,11 +233,11 @@ public class GroupAggregation extends FieldAggregation {
}
if (allNull) {
// 如果分组字段值全空将会触发全量更新
if (!valueChanged.isEmpty()) {
this.groupAggregationRefresh = new GroupAggregationRefresh(this, qFieldsRefresh);
} else {
if (valueChanged.isEmpty()) {
log.warn("All values of group-fields are null, ignored");
} else {
// 如果分组字段值全空将会触发全量更新
this.groupAggregationRefresh = new GroupAggregationRefresh(this, qFieldsRefresh, operatingContext.getFixedRecordId());
}
return;
}
@ -244,7 +245,7 @@ public class GroupAggregation extends FieldAggregation {
this.followSourceWhere = StringUtils.join(qFieldsFollow.iterator(), " and ");
if (isGroupUpdate) {
this.groupAggregationRefresh = new GroupAggregationRefresh(this, qFieldsRefresh);
this.groupAggregationRefresh = new GroupAggregationRefresh(this, qFieldsRefresh, operatingContext.getFixedRecordId());
}
sql = String.format("select %s from %s where ( %s )",

View file

@ -16,10 +16,14 @@ import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.util.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 分组聚合目标数据刷新
@ -39,15 +43,17 @@ public class GroupAggregationRefresh {
final private GroupAggregation parent;
final private List<String[]> fieldsRefresh;
final private ID originSourceId;
// fieldsRefresh = [TargetField, SourceField, Value]
protected GroupAggregationRefresh(GroupAggregation parent, List<String[]> fieldsRefresh) {
protected GroupAggregationRefresh(GroupAggregation parent, List<String[]> fieldsRefresh, ID originSourceId) {
this.parent = parent;
this.fieldsRefresh = fieldsRefresh;
this.originSourceId = originSourceId;
}
/**
* TODO 存在性能问题可能刷新过多
* NOTE 存在性能问题因为可能刷新过多
*/
public void refresh() {
List<String> targetFields = new ArrayList<>();
@ -55,79 +61,52 @@ public class GroupAggregationRefresh {
for (String[] s : fieldsRefresh) {
targetFields.add(s[0]);
if (s[2] != null) {
targetWhere.add(String.format("%s = '%s'", s[0], s[2]));
targetWhere.add(String.format("%s = '%s'", s[0], CommonsUtils.escapeSql(s[2])));
}
}
// 全量刷新性能较低
// 只有1个字段会全量刷新性能较低
if (targetWhere.size() <= 1) {
targetWhere.clear();
targetWhere.add("(1=1)");
log.warn("Force refresh all aggregation target(s)");
}
Entity entity = this.parent.targetEntity;
// 1.获取待刷新的目标
Entity targetEntity = this.parent.targetEntity;
String sql = String.format("select %s,%s from %s where ( %s )",
StringUtils.join(targetFields, ","),
entity.getPrimaryField().getName(),
entity.getName(),
targetEntity.getPrimaryField().getName(),
targetEntity.getName(),
StringUtils.join(targetWhere, " or "));
Object[][] targetArray = Application.createQueryNoFilter(sql).array();
log.info("Maybe refresh target record(s) : {}", targetArray.length);
Object[][] targetRecords4Refresh = Application.createQueryNoFilter(sql).array();
log.info("Maybe refresh target record(s) : {}", targetRecords4Refresh.length);
ID triggerUser = UserService.SYSTEM_USER;
ActionContext parentAc = parent.getActionContext();
// 避免重复的无意义更新
// NOTE 220905 不能忽略触发源本身
// NOTE 220905 更新时不能忽略触发源本身的更新
Set<ID> refreshedIds = new HashSet<>();
for (Object[] o : targetArray) {
// 2.逐一刷新目标
for (Object[] o : targetRecords4Refresh) {
final ID targetRecordId = (ID) o[o.length - 1];
// 排重
if (refreshedIds.contains(targetRecordId)) continue;
else refreshedIds.add(targetRecordId);
List<String> qFieldsFollow = new ArrayList<>();
List<String> qFieldsFollow2 = new ArrayList<>();
for (int i = 0; i < o.length - 1; i++) {
String[] source = fieldsRefresh.get(i);
if (o[i] == null) {
qFieldsFollow.add(String.format("%s is null", source[1]));
} else {
qFieldsFollow.add(String.format("%s = '%s'", source[1], o[i]));
qFieldsFollow2.add(String.format("%s = '%s'", source[1], o[i]));
qFieldsFollow.add(String.format("%s = '%s'", source[1], CommonsUtils.escapeSql(o[i])));
}
}
ID fakeUpdateReferenceId = null;
// 1.尝试获取触发源
for (int i = 0; i < o.length - 1; i++) {
Object mayId = o[i];
if (ID.isId(mayId) && ((ID) mayId).getEntityCode() > 100) {
fakeUpdateReferenceId = (ID) mayId;
break;
}
}
// 2.强制获取
if (fakeUpdateReferenceId == null) {
sql = String.format("select %s from %s where %s",
parent.sourceEntity.getPrimaryField().getName(),
parent.sourceEntity.getName(),
StringUtils.join(qFieldsFollow2, " or "));
Object[] found = Application.getQueryFactory().unique(sql);
fakeUpdateReferenceId = found == null ? null : (ID) found[0];
} else {
// 1.1.排重
if (refreshedIds.contains(targetRecordId)) continue;
else refreshedIds.add(fakeUpdateReferenceId);
}
if (fakeUpdateReferenceId == null) {
log.warn("No any source-id found, ignored : {}", Arrays.toString(o));
continue;
}
ActionContext actionContext = new ActionContext(null,
parentAc.getSourceEntity(), parentAc.getActionContent(), parentAc.getConfigId());
@ -137,7 +116,8 @@ public class GroupAggregationRefresh {
ga.targetRecordId = targetRecordId;
ga.followSourceWhere = StringUtils.join(qFieldsFollow, " and ");
Record fakeSourceRecord = EntityHelper.forUpdate(fakeUpdateReferenceId, triggerUser, false);
// FIXME v35 可能导致数据聚合条件中的字段变量不准
Record fakeSourceRecord = EntityHelper.forUpdate(originSourceId, triggerUser, false);
OperatingContext oCtx = OperatingContext.create(triggerUser, BizzPermission.NONE, fakeSourceRecord, fakeSourceRecord);
try {

View file

@ -20,8 +20,10 @@ import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.EasyFieldConfigProps;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.CommonsUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
@ -70,6 +72,12 @@ public class TargetWithMatchFields {
return o == null ? new ID[0] : (ID[]) o;
}
/**
* @param actionContext
* @param m
* @return
* @see GroupAggregation#prepare(OperatingContext)
*/
private Object match(ActionContext actionContext, boolean m) {
if (sourceEntity != null) return targetRecordId; // 已做匹配
@ -186,6 +194,7 @@ public class TargetWithMatchFields {
}
}
val = CommonsUtils.escapeSql(val);
qFields.add(String.format("%s = '%s'", targetField, val));
qFieldsFollow.add(String.format("%s = '%s'", sourceField, val));
allNull = false;

View file

@ -14,8 +14,8 @@ import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.ObjectUtils;
import java.util.Calendar;
@ -63,7 +63,7 @@ public class ShortUrls {
*/
public static boolean invalid(String shortKey) {
String dsql = String.format(
"delete from `short_url` where SHORT_KEY = '%s'", StringEscapeUtils.escapeSql(shortKey));
"delete from `short_url` where SHORT_KEY = '%s'", CommonsUtils.escapeSql(shortKey));
int a = Application.getSqlExecutor().execute(dsql);
return a > 0;
}

View file

@ -18,11 +18,18 @@ import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.query.AdvFilterParser;
import com.rebuild.core.service.query.ParseHelper;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 列表查询解析
@ -200,13 +207,13 @@ public class QueryParser {
// append: QuickQuery
JSONObject quickFilter = queryExpr.getJSONObject("filter");
if (quickFilter != null) {
if (ParseHelper.validAdvFilter(quickFilter)) {
String where = new AdvFilterParser(quickFilter, entity).toSqlWhere();
if (StringUtils.isNotBlank(where)) wheres.add(where);
}
// v3.3
JSONObject quickFilterAnd = queryExpr.getJSONObject("filterAnd");
if (quickFilterAnd != null) {
if (ParseHelper.validAdvFilter(quickFilterAnd)) {
String where = new AdvFilterParser(quickFilterAnd, entity).toSqlWhere();
if (StringUtils.isNotBlank(where)) wheres.add(where);
}
@ -270,7 +277,9 @@ public class QueryParser {
ConfigBean advFilter = AdvFilterManager.instance.getAdvFilter(filterId);
if (advFilter != null) {
JSONObject filterExpr = (JSONObject) advFilter.getJSON("filter");
return new AdvFilterParser(filterExpr, entity).toSqlWhere();
if (ParseHelper.validAdvFilter(filterExpr)) {
return new AdvFilterParser(filterExpr, entity).toSqlWhere();
}
}
return null;
}

View file

@ -110,10 +110,10 @@ public class CommonsUtils {
* @return
* @see StringEscapeUtils#escapeSql(String)
*/
public static String escapeSql(String text) {
public static String escapeSql(Object text) {
// https://github.com/getrebuild/rebuild/issues/594
text = text.replace("\\'", "'");
return StringEscapeUtils.escapeSql(text);
text = text.toString().replace("\\'", "'");
return StringEscapeUtils.escapeSql((String) text);
}
/**

View file

@ -18,8 +18,8 @@ import com.rebuild.core.Application;
import com.rebuild.core.configuration.RebuildApiService;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.support.i18n.I18nUtils;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.web.BaseController;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@ -88,7 +88,7 @@ public class ApisManagerController extends BaseController {
String sql = "select remoteIp,requestTime,responseTime,requestUrl,requestBody,responseBody,requestId from RebuildApiRequest" +
" where appId = ? and requestTime > ? and (1=1) order by requestTime desc";
if (StringUtils.isNotBlank(q)) {
q = StringEscapeUtils.escapeSql(q);
q = CommonsUtils.escapeSql(q);
sql = sql.replace("(1=1)", String.format("(requestBody like '%%%s%%' or responseBody like '%%%s%%')", q, q));
}

View file

@ -56,13 +56,17 @@ public class ListAndViewRedirection extends BaseController {
|| entity.getEntityCode() == EntityHelper.ProjectTaskComment) {
Object[] found = findProjectAndTaskId(anyId);
if (found != null) {
url = MessageFormat.format(
"../project/{0}/tasks#!/View/ProjectTask/{1}", found[1], found[0]);
url = MessageFormat.format("../project/{0}/tasks#!/View/ProjectTask/{1}", found[1], found[0]);
}
} else if (entity.getEntityCode() == EntityHelper.User) {
url = MessageFormat.format(
"../admin/bizuser/users#!/View/{0}/{1}", entity.getName(), anyId);
url = MessageFormat.format("../admin/bizuser/users#!/View/{0}/{1}", entity.getName(), anyId);
} else if (entity.getEntityCode() == EntityHelper.Department) {
url = MessageFormat.format("../admin/bizuser/departments#!/View/{0}/{1}", entity.getName(), anyId);
} else if (entity.getEntityCode() == EntityHelper.Team) {
url = MessageFormat.format("../admin/bizuser/teams#!/View/{0}/{1}", entity.getName(), anyId);
} else if (entity.getEntityCode() == EntityHelper.Role) {
url = MessageFormat.format("../admin/bizuser/role/{0}", anyId);
} else if (MetadataHelper.isBusinessEntity(entity)) {
if ("dock".equalsIgnoreCase(type)) {

View file

@ -24,6 +24,7 @@ import com.rebuild.core.service.NoRecordFoundException;
import com.rebuild.core.service.project.ProjectHelper;
import com.rebuild.core.service.project.ProjectManager;
import com.rebuild.core.service.query.AdvFilterParser;
import com.rebuild.core.service.query.ParseHelper;
import com.rebuild.core.support.general.FieldValueHelper;
import com.rebuild.core.support.i18n.I18nUtils;
import com.rebuild.core.support.i18n.Language;
@ -106,7 +107,7 @@ public class ProjectTaskController extends BaseController {
// 高级查询
JSON advFilter = ServletUtils.getRequestJson(request);
if (advFilter != null) {
if (ParseHelper.validAdvFilter((JSONObject) advFilter)) {
String filterSql = new AdvFilterParser((JSONObject) advFilter).toSqlWhere();
if (filterSql != null) {
queryWhere += " and (" + filterSql + ")";

View file

@ -84,7 +84,7 @@ public class FieldAggregationController extends BaseController {
// 源字段
JSONArray sourceFields = MetaFormatter.buildFieldsWithRefs(sourceEntity, 3, field -> {
JSONArray sourceFields = MetaFormatter.buildFieldsWithRefs(sourceEntity, 3, true, field -> {
if (field instanceof EasyField) {
EasyField easyField = (EasyField) field;
return !easyField.isQueryable() || easyField.getDisplayType() == DisplayType.BARCODE;
@ -104,6 +104,7 @@ public class FieldAggregationController extends BaseController {
for (Field field : MetadataSorter.sortFields(targetEntity,
DisplayType.NUMBER, DisplayType.DECIMAL, DisplayType.DATE, DisplayType.DATETIME,
DisplayType.N2NREFERENCE, DisplayType.NTEXT, DisplayType.FILE)) {
EasyField easyField = EasyMetaFactory.valueOf(field);
if (easyField.isBuiltin()) continue;

View file

@ -117,7 +117,7 @@ public class FieldWritebackController extends BaseController {
// 源字段
JSONArray sourceFields = MetaFormatter.buildFieldsWithRefs(sourceEntity, 3, field -> {
JSONArray sourceFields = MetaFormatter.buildFieldsWithRefs(sourceEntity, 3, true, field -> {
if (field instanceof EasyField) {
EasyField easyField = (EasyField) field;
return easyField.getDisplayType() == DisplayType.BARCODE;
@ -134,7 +134,7 @@ public class FieldWritebackController extends BaseController {
JSONArray targetFields = new JSONArray();
if (targetEntity != null) {
targetFields = MetaFormatter.buildFieldsWithRefs(targetEntity, 1, field -> {
targetFields = MetaFormatter.buildFieldsWithRefs(targetEntity, 1, true, field -> {
EasyField easyField = (EasyField) field;
return easyField.getDisplayType() == DisplayType.SERIES
|| easyField.getDisplayType() == DisplayType.BARCODE

View file

@ -54,7 +54,7 @@ public class GroupAggregationController extends BaseController {
paddingType2(sourceGroupFields, sourceEntity);
// -聚合字段
JSONArray sourceFields = MetaFormatter.buildFieldsWithRefs(sourceEntity, 3, field -> {
JSONArray sourceFields = MetaFormatter.buildFieldsWithRefs(sourceEntity, 3, true, field -> {
if (field instanceof EasyField) {
EasyField easyField = (EasyField) field;
return !easyField.isQueryable() || easyField.getDisplayType() == DisplayType.BARCODE;

View file

@ -2177,6 +2177,10 @@ th.column-fixed {
content: '\f2fb';
}
.form-line.hover fieldset legend > span {
margin-left: 3px;
}
.form-line.v33 {
background-color: #ebebeb;
border-radius: 2px;
@ -5014,7 +5018,7 @@ pre.unstyle {
.tablesort thead th.sorted.ascending::after,
.tablesort thead th.sorted.descending::after {
font: normal normal normal 18px/1 "Material Design Icons";
font: normal normal normal 18px/1 'Material Design Icons';
content: '\F0140';
position: absolute;
right: 10px;
@ -5377,4 +5381,4 @@ div.dataTables_wrapper.compact div.dataTables_oper .btn-space {
.dataTables_oper.invisible2 > .btn.J_view {
display: inline-block;
}
}

View file

@ -9,7 +9,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
const wpc = window.__PageConfig
const __gExtConfig = {}
const SHOW_REPEATABLE = ['TEXT', 'DATE', 'EMAIL', 'URL', 'PHONE', 'REFERENCE', 'CLASSIFICATION']
const SHOW_REPEATABLE = ['TEXT', 'DATE', 'EMAIL', 'URL', 'PHONE', 'REFERENCE', 'CLASSIFICATION', 'ANYREFERENCE']
const SHOW_DEFAULTVALUE = ['TEXT', 'NTEXT', 'EMAIL', 'PHONE', 'URL', 'NUMBER', 'DECIMAL', 'DATETIME', 'DATE', 'TIME', 'BOOL', 'CLASSIFICATION', 'REFERENCE', 'N2NREFERENCE']
const SHOW_ADVDESENSITIZED = ['TEXT', 'PHONE', 'EMAIL', 'NUMBER', 'DECIMAL']
const SHOW_ADVPATTERN = ['TEXT']

View file

@ -67,11 +67,12 @@ const renderList = function () {
}
function exportFields() {
const rows = [[$L('内部标识'), $L('字段名称'), $L('类型'), $L('必填'), $L('备注')].join(', ')]
const rows = [[$L('内部标识'), $L('字段名称'), $L('类型'), $L('必填'), $L('只读'), $L('备注')].join(', ')]
rows.push([`${wpc.entityName}Id`, $L('主键'), 'ID', 'N', 'Y', ''].join(', '))
fields_data.forEach((item) => {
let type = item.displayType
if (item.displayTypeRef) type += ` (${item.displayTypeRef[1]})`
rows.push([item.fieldName, item.fieldLabel, type, item.nullable ? 'N' : 'Y', item.comments ? item.comments.replace(/[,;]/, ' ') : ''].join(', '))
rows.push([item.fieldName, item.fieldLabel, type, item.nullable ? 'N' : 'Y', item.creatable ? 'N' : 'Y', item.comments ? item.comments.replace(/[,;]/, ' ') : ''].join(', '))
})
const encodedUri = encodeURI('data:text/csv;charset=utf-8,\ufeff' + rows.join('\n'))

View file

@ -514,12 +514,13 @@ class DlgEditRefform extends DlgEditField {
<option value="">{$L('无')}</option>
{Object.keys(_ValidFields).map((k) => {
const field = _ValidFields[k]
if (field.displayTypeName === 'REFERENCE' && !(field.fieldName === 'approvalId'))
if (['REFERENCE', 'ANYREFERENCE'].includes(field.displayTypeName) && field.fieldName !== 'approvalId') {
return (
<option key={field.fieldName} value={field.fieldName}>
{field.fieldLabel}
</option>
)
}
return null
})}
</select>

View file

@ -2765,7 +2765,7 @@ class RbFormDivider extends React.Component {
<div className={`form-line hover -v33 ${this.state.collapsed && 'collapsed'}`} ref={(c) => (this._$formLine = c)}>
<fieldset>
<legend onClick={() => this._toggle()} title={$L('展开/收起')}>
{this.props.label || ''}
{this.props.label && <span>{this.props.label}</span>}
</legend>
</fieldset>
</div>
@ -2805,8 +2805,11 @@ class RbFormRefform extends React.Component {
const $$$parent = this.props.$$$parent
if ($$$parent && $$$parent.__ViewData && $$$parent.__ViewData[this.props.reffield]) {
const s = $$$parent.__ViewData[this.props.reffield]
this._renderViewFrom({ ...s })
// 避免循环嵌套死循环
if (($$$parent.__nestDepth || 0) < 3) {
const s = $$$parent.__ViewData[this.props.reffield]
this._renderViewFrom({ ...s })
}
}
}
@ -2819,6 +2822,10 @@ class RbFormRefform extends React.Component {
return
}
// 支持嵌套
this.__ViewData = {}
this.__nestDepth = (this.props.$$$parent.__nestDepth || 0) + 1
const VFORM = (
<RF>
<a title={$L('在新页面打开')} className="close open-in-new" href={`${rb.baseUrl}/app/redirect?id=${props.id}&type=newtab`} target="_blank">
@ -2826,6 +2833,7 @@ class RbFormRefform extends React.Component {
</a>
<div className="row">
{res.data.elements.map((item) => {
if (![TYPE_DIVIDER, TYPE_REFFORM].includes(item.field)) this.__ViewData[item.field] = item.value
item.$$$parent = this
// eslint-disable-next-line no-undef
return detectViewElement(item, props.entity)
@ -2840,7 +2848,7 @@ class RbFormRefform extends React.Component {
// 确定元素类型
var detectElement = function (item, entity) {
if (!item.key) item.key = `field-${item.field === TYPE_DIVIDER ? $random() : item.field}`
if (!item.key) item.key = `field-${item.field === TYPE_DIVIDER || item.field === TYPE_REFFORM ? $random() : item.field}`
if (entity && window._CustomizedForms) {
const c = window._CustomizedForms.useFormElement(entity, item)
@ -2898,6 +2906,7 @@ var detectElement = function (item, entity) {
} else if (item.field === TYPE_DIVIDER || item.field === '$LINE$') {
return <RbFormDivider {...item} />
} else if (item.field === TYPE_REFFORM) {
console.log(item)
return <RbFormRefform {...item} />
} else {
return <RbFormUnsupportted {...item} />

View file

@ -8,8 +8,8 @@ See LICENSE and COMMERCIAL in the project root for license information.
/* !!! KEEP IT ES5 COMPATIBLE !!! */
// GA
;(function () {
const gaScript = document.createElement('script')
(function () {
var gaScript = document.createElement('script')
gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-ZCZHJPMEG7'
gaScript.async = true
gaScript.onload = function () {
@ -20,7 +20,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
gtag('js', new Date())
gtag('config', 'G-ZCZHJPMEG7')
}
const s = document.getElementsByTagName('script')[0]
var s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore(gaScript, s)
})()
@ -259,7 +259,7 @@ var _initNav = function () {
$sidebar.toggleClass('rb-collapsible-sidebar-collapsed')
$('.sidebar-elements>li>a').tooltip('toggleEnabled')
const collapsed = $sidebar.hasClass('rb-collapsible-sidebar-collapsed')
var collapsed = $sidebar.hasClass('rb-collapsible-sidebar-collapsed')
$.cookie('rb.sidebarCollapsed', collapsed, { expires: 180 })
if (collapsed) {

View file

@ -61,7 +61,7 @@ class RbViewForm extends React.Component {
{hadApproval && <ApprovalProcessor id={this.props.id} entity={this.props.entity} />}
<div className="row">
{res.data.elements.map((item) => {
if (!(item.field === TYPE_DIVIDER || item.field === TYPE_REFFORM)) this.__ViewData[item.field] = item.value
if (![TYPE_DIVIDER, TYPE_REFFORM].includes(item.field)) this.__ViewData[item.field] = item.value
if (item.field === TYPE_REFFORM) this.__hasRefform = true
item.$$$parent = this
return detectViewElement(item, this.props.entity)

View file

@ -10,7 +10,7 @@ const CALC_MODES2 = {
...FormulaAggregation.CALC_MODES,
RBJOIN: $L('连接'),
RBJOIN2: $L('去重连接'),
// RBJOIN3: $L('去重连接*'),
// RBJOIN3: $L('去重连接*N'),
}
const __LAB_MATCHFIELDS = false
@ -70,7 +70,7 @@ class ContentFieldAggregation extends ActionContentSpec {
<label className="col-md-12 col-lg-3 col-form-label text-lg-right"></label>
<div className="col-md-12 col-lg-9">
<div>
<h5 className="mt-0 text-bold">{$L('目标实体/记录匹配规则')}</h5>
<h5 className="mt-0 text-bold">{$L('目标实体/记录匹配规则')} (LAB)</h5>
<textarea className="formula-code" style={{ height: 72 }} ref={(c) => (this._$matchFields = c)} placeholder="## [{ sourceField:XXX, targetField:XXX }]" />
</div>
</div>
@ -406,6 +406,22 @@ class ContentFieldAggregation extends ActionContentSpec {
return false
}
if (this.state.showMatchFields) {
let badFormat = !content.targetEntityMatchFields
if (!badFormat) {
try {
badFormat = JSON.parse(content.targetEntityMatchFields)
badFormat = !$.isArray(badFormat)
} catch (err) {
badFormat = true
}
}
if (badFormat) {
RbHighbar.create($L('请正确填写目标实体/记录匹配规则'))
return false
}
}
return content
}
}

View file

@ -78,7 +78,7 @@ class ContentFieldWriteback extends ActionContentSpec {
<label className="col-md-12 col-lg-3 col-form-label text-lg-right"></label>
<div className="col-md-12 col-lg-9">
<div>
<h5 className="mt-0 text-bold">{$L('目标实体/记录匹配规则')}</h5>
<h5 className="mt-0 text-bold">{$L('目标实体/记录匹配规则')} (LAB)</h5>
<textarea className="formula-code" style={{ height: 72 }} ref={(c) => (this._$matchFields = c)} placeholder="## [{ sourceField:XXX, targetField:XXX }]" />
</div>
</div>
@ -377,6 +377,22 @@ class ContentFieldWriteback extends ActionContentSpec {
return false
}
if (this.state.showMatchFields) {
let badFormat = !content.targetEntityMatchFields
if (!badFormat) {
try {
badFormat = JSON.parse(content.targetEntityMatchFields)
badFormat = !$.isArray(badFormat)
} catch (err) {
badFormat = true
}
}
if (badFormat) {
RbHighbar.create($L('请正确填写目标实体/记录匹配规则'))
return false
}
}
const one2one = this.state.targetEntities.find((x) => `${x[2]}.${x[0]}` === content.targetEntity)
if (one2one && one2one[3] === 'one2one') content.one2one = true

View file

@ -10,7 +10,7 @@ const CALC_MODES2 = {
...FormulaAggregation.CALC_MODES,
RBJOIN: $L('连接'),
RBJOIN2: $L('去重连接'),
// RBJOIN3: $L('去重连接*'),
// RBJOIN3: $L('去重连接*N'),
}
// ~~ 分组聚合