Better v3.0 (#506)

* N2NReferenceSupport

* better tips

* fix: triggers

* fix: AutoApproval trigger

* entity import

* Update @rbv

* fix: #504

* print size #505
This commit is contained in:
RB 2022-08-08 17:28:49 +08:00 committed by GitHub
parent 68c9cc505f
commit ead5219ece
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 442 additions and 277 deletions

View file

@ -17,9 +17,9 @@ git:
submodules: false
before_script:
- mysql -e "CREATE DATABASE rebuild20 COLLATE utf8mb4_general_ci;"
- mysql -e "CREATE USER 'rebuild'@'127.0.0.1' IDENTIFIED BY 'rebuild'; GRANT ALL PRIVILEGES ON rebuild20.* TO 'rebuild'@'127.0.0.1'; FLUSH PRIVILEGES;"
- mysql -D rebuild20 < src/main/resources/scripts/db-init.sql
- mysql -e "CREATE DATABASE rebuild30 COLLATE utf8mb4_general_ci;"
- mysql -e "CREATE USER 'rebuild'@'127.0.0.1' IDENTIFIED BY 'rebuild'; GRANT ALL PRIVILEGES ON rebuild30.* TO 'rebuild'@'127.0.0.1'; FLUSH PRIVILEGES;"
- mysql -D rebuild30 < src/main/resources/scripts/db-init.sql
# codecov
after_success:

2
@rbv

@ -1 +1 @@
Subproject commit 263dc325c7903bb3115ba30a9032cf182ee7e4f9
Subproject commit 5c05ac9fdc812ca7e218732dbaf553605d0ffb5a

View file

@ -10,7 +10,7 @@
</parent>
<groupId>com.rebuild</groupId>
<artifactId>rebuild</artifactId>
<version>2.10.0-dev</version>
<version>3.0.0-dev</version>
<name>rebuild</name>
<description>Building your business-systems freely!</description>
<!-- UNCOMMENT USE TOMCAT -->

View file

@ -65,11 +65,11 @@ public class Application implements ApplicationListener<ApplicationStartedEvent>
/**
* Rebuild Version
*/
public static final String VER = "2.10.0-dev";
public static final String VER = "3.0.0-dev";
/**
* Rebuild Build [MAJOR]{1}[MINOR]{2}[PATCH]{2}[BUILD]{2}
* Rebuild Build [MAJOR]{1}[MINOR]{2}[PATCH]{2}[BUILD]{1}
*/
public static final int BUILD = 210000;
public static final int BUILD = 300000;
static {
// Driver for DB

View file

@ -107,7 +107,7 @@ public class BootEnvironmentPostProcessor implements EnvironmentPostProcessor, I
// `application-bean.xml` 占位符必填
if (dbUrl == null) {
dbUrl = "jdbc:mysql://127.0.0.1:3306/rebuild20?characterEncoding=UTF8";
dbUrl = "jdbc:mysql://127.0.0.1:3306/rebuild30?characterEncoding=UTF8";
confPs.put("db.url", dbUrl);
}
if (env.getProperty("db.user") == null) confPs.put("db.user", "rebuild");

View file

@ -29,22 +29,20 @@ import java.util.Comparator;
import java.util.List;
/**
* 分类数据
* 列表字段分类数据
*
* @author ZHAO
* @since 07/23/2022
*/
public class DataListClass {
public class DataListCategory {
/**
*
*
* @param entity
* @param user
* @return
*/
public static JSON datas(Entity entity, ID user) {
final Field classField = getFieldOfClass(entity);
final Field classField = getFieldOfCategory(entity);
if (classField == null) return null;
final String ckey = String.format("DLC1.%s.%s", entity.getName(), classField.getName());
@ -107,9 +105,9 @@ public class DataListClass {
* @param entity
* @return
*/
public static Field getFieldOfClass(Entity entity) {
String classField = EasyMetaFactory.valueOf(entity).getExtraAttr(EasyEntityConfigProps.ADV_LIST_SHOWCLASS);
if (StringUtils.isBlank(classField) || !entity.containsField(classField)) return null;
return entity.getField(classField);
public static Field getFieldOfCategory(Entity entity) {
String categoryField = EasyMetaFactory.valueOf(entity).getExtraAttr(EasyEntityConfigProps.ADV_LIST_SHOWCATEGORY);
if (StringUtils.isBlank(categoryField) || !entity.containsField(categoryField)) return null;
return entity.getField(categoryField);
}
}

View file

@ -11,6 +11,7 @@ import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.PersistManagerFactory;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.dialect.FieldType;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
@ -18,8 +19,6 @@ import com.rebuild.core.Application;
import com.rebuild.core.UserContextHolder;
import com.rebuild.core.configuration.BaseConfigurationService;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.privileges.AdminGuard;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@ -89,7 +88,7 @@ public class PickListService extends BaseConfigurationService implements AdminGu
// MultiSelect 专用
long nextMaskValue = 0;
if (EasyMetaFactory.getDisplayType(field) == DisplayType.MULTISELECT) {
if (field.getType() == FieldType.LONG) {
Object[] max = Application.createQueryNoFilter(
"select max(maskValue) from PickList where belongEntity = ? and belongField = ?")
.setParameter(1, field.getOwnEntity().getName())

View file

@ -47,7 +47,7 @@ public class CopyEntity extends Entity2Schema {
// 导出
JSONObject schemadata = (JSONObject) new MetaSchemaGenerator(sourceEntity).generate();
JSONObject schemadata = (JSONObject) new MetaSchemaGenerator(sourceEntity, false).generate();
String uniqueEntityName = clearConfig(schemadata, entityName);
JSONObject detailSchema = schemadata.getJSONObject("detail");
@ -74,10 +74,6 @@ public class CopyEntity extends Entity2Schema {
schema.remove(MetaSchemaGenerator.CFG_TRIGGERS);
schema.remove(MetaSchemaGenerator.CFG_FILTERS);
// 以下保留
// schema.remove(MetaSchemaGenerator.CFG_FILLINS);
// schema.remove(MetaSchemaGenerator.CFG_LAYOUTS);
String uniqueEntityName = toPinyinName(entityName);
for (int i = 0; i < 5; i++) {
if (MetadataHelper.containsEntity(uniqueEntityName)) {

View file

@ -37,7 +37,7 @@ public class EasyEntityConfigProps {
/**
* 列表分类
*/
public static final String ADV_LIST_SHOWCLASS = "advListShowClass";
public static final String ADV_LIST_SHOWCATEGORY = "advListShowCategory";
/**
* 列表查询面板
*/

View file

@ -17,6 +17,7 @@ import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.Application;
import com.rebuild.core.UserContextHolder;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.service.CommonsService;
@ -26,7 +27,6 @@ import com.rebuild.core.support.i18n.Language;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
@ -111,8 +111,9 @@ public class PrivilegesGuardInterceptor implements MethodInterceptor, Guard {
// 跳过
ID skipGuardId;
if ((skipGuardId = PrivilegesGuardContextHolder.getSkipGuardOnce()) != null) {
log.info("Allow no permission({}) passed once : {}",
action.getName(), ObjectUtils.defaultIfNull(recordId, skipGuardId));
if (!EntityHelper.isUnsavedId(skipGuardId)) {
log.info("Allow no permission({}) passed once : {}", action.getName(), skipGuardId);
}
return;
}

View file

@ -38,6 +38,9 @@ import java.io.IOException;
*/
public class MetaSchemaGenerator {
// 保持 ID
public static final String KEEP_ID = "_id";
public static final String CFG_FILLINS = "fillins";
public static final String CFG_LAYOUTS = "layouts";
public static final String CFG_FILTERS = "filters";
@ -46,12 +49,14 @@ public class MetaSchemaGenerator {
public static final String CFG_TRANSFORMS = "transforms";
final private Entity mainEntity;
final private boolean keepId;
/**
* @param entity
*/
public MetaSchemaGenerator(Entity entity) {
public MetaSchemaGenerator(Entity entity, boolean keepId) {
this.mainEntity = entity;
this.keepId = keepId;
}
/**
@ -109,12 +114,12 @@ public class MetaSchemaGenerator {
schemaEntity.put(CFG_FILTERS, performFilters(entity));
// 触发器
schemaEntity.put(CFG_TRIGGERS, performTriggers(entity));
// 记录转换
schemaEntity.put(CFG_TRANSFORMS, performTransforms(entity));
if (!detail) {
// 审批流程
schemaEntity.put(CFG_APPROVALS, performApprovals(entity));
// 字段转换
schemaEntity.put(CFG_TRANSFORMS, performTransforms(entity));
}
// TODO 报表模板?
@ -163,7 +168,8 @@ public class MetaSchemaGenerator {
JSONArray items = new JSONArray();
for (ConfigBean e : entries) {
items.add(new Object[] {
e.getString("text"), e.getBoolean("default"), e.getLong("mask"), e.getString("color")
e.getString("text"), e.getBoolean("default"), e.getLong("mask"),
e.getString("color"), this.keepId ? e.getID("id") : null
});
}
return items;
@ -267,7 +273,7 @@ public class MetaSchemaGenerator {
private JSON performTransforms(Entity entity) {
Object[][] array = Application.createQueryNoFilter(
"select targetEntity,name,config from TransformConfig where belongEntity = ? and isDisabled = 'F'")
"select targetEntity,name,config,configId from TransformConfig where belongEntity = ? and isDisabled = 'F'")
.setParameter(1, entity.getName())
.array();
@ -276,9 +282,11 @@ public class MetaSchemaGenerator {
JSON mappingConfig = parseJSON(o[2]);
if (mappingConfig == null) continue;
JSON config = JSONUtils.toJSONObject(
JSONObject config = JSONUtils.toJSONObject(
new String[] { "targetEntity", "name", "config" },
new Object[] { o[0], o[1], mappingConfig });
if (this.keepId) config.put(KEEP_ID, o[3]);
transforms.add(config);
}
return transforms;

View file

@ -11,6 +11,7 @@ import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import cn.devezhao.persist4j.engine.PersistManagerImpl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
@ -29,8 +30,6 @@ import com.rebuild.core.metadata.impl.Field2Schema;
import com.rebuild.core.metadata.impl.MetadataModificationException;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.approval.RobotApprovalConfigService;
import com.rebuild.core.service.trigger.ActionFactory;
import com.rebuild.core.service.trigger.ActionType;
import com.rebuild.core.service.trigger.RobotTriggerConfigService;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.task.HeavyTask;
@ -42,6 +41,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.rebuild.core.rbstore.MetaSchemaGenerator.KEEP_ID;
/**
* 元数据模型导入
*
@ -147,14 +148,32 @@ public class MetaschemaImporter extends HeavyTask<String> {
try {
for (Map.Entry<Field, JSONObject> e : picklistHolders.entrySet()) {
Field field = e.getKey();
Application.getBean(PickListService.class).updateBatch(
MetadataHelper.getField(field.getOwnEntity().getName(), field.getName()), e.getValue());
JSONObject config = e.getValue();
JSONArray options = config.getJSONArray("show");
for (Object o : options) {
String keepId = ((JSONObject) o).getString(KEEP_ID);
if (ID.isId(keepId)) {
Record c = EntityHelper.forNew(EntityHelper.PickList, UserService.SYSTEM_USER, true);
c.setString("belongEntity", field.getOwnEntity().getName());
c.setString("belongField", field.getName());
c.setString("text", "temp");
((PersistManagerImpl) Application.getPersistManagerFactory().createPersistManager())
.saveInternal(c, ID.valueOf(keepId));
((JSONObject) o).put("id", keepId);
}
} finally {
if (sessionUser == null) UserContextHolder.clear();
}
Application.getBean(PickListService.class).updateBatch(field, config);
}
} catch (Exception ex) {
log.warn("Importing PickList error : {}", ex.getLocalizedMessage());
}
setCompleted(100);
if (sessionUser == null) UserContextHolder.clear();
return entityName;
}
@ -325,10 +344,10 @@ public class MetaschemaImporter extends HeavyTask<String> {
if (item.size() > 2) {
option.put("mask", item.getLongValue(2));
}
// v2.10: Color
if (item.size() > 3) {
option.put("color", item.getString(3));
}
// v2.10: Color, Id
if (item.size() > 3) option.put("color", item.getString(3));
if (item.size() > 4) option.put(KEEP_ID, item.getString(4));
shown.add(option);
}
@ -372,20 +391,6 @@ public class MetaschemaImporter extends HeavyTask<String> {
config.put("metadata", JSONUtils.toJSONObject("entity", configEntity.getName()));
config.put("belongEntity", entity);
String actionType = config.getString("actionType");
boolean available = false;
for (ActionType type : ActionFactory.getAvailableActions()) {
if (type.name().equalsIgnoreCase(actionType)) {
available = true;
break;
}
}
if (!available) {
log.warn("Trigger `{}` unavailable", actionType);
return;
}
Record record = new EntityRecordCreator(configEntity, config, getUser())
.create();
Application.getBean(RobotTriggerConfigService.class).create(record);
@ -408,6 +413,15 @@ public class MetaschemaImporter extends HeavyTask<String> {
Record record = new EntityRecordCreator(configEntity, config, getUser())
.create();
String keepId = config.getString(KEEP_ID);
if (ID.isId(keepId)) {
((PersistManagerImpl) Application.getPersistManagerFactory().createPersistManager())
.saveInternal(record, ID.valueOf(keepId));
record = EntityHelper.forUpdate(record.getPrimary(), UserService.SYSTEM_USER, false);
Application.getBean(TransformConfigService.class).update(record);
} else {
Application.getBean(TransformConfigService.class).create(record);
}
}
}

View file

@ -48,7 +48,7 @@ public class RBStore {
*/
public static JSON fetchMetaschema(String fileUri) {
return fetchRemoteJson("metaschemas/" +
StringUtils.defaultIfBlank(fileUri, "index-3.0.json"));
StringUtils.defaultIfBlank(fileUri, "index-4.0.json"));
}
/**

View file

@ -235,10 +235,10 @@ public abstract class ChartData extends SetUser implements ChartSpec {
sorts.add(dim.getSqlName() + " " + fs.toString().toLowerCase());
}
}
// NOTE 优先维度排序
if (!sorts.isEmpty()) {
return String.join(", ", sorts);
}
// // NOTE 优先维度排序
// if (!sorts.isEmpty()) {
// return String.join(", ", sorts);
// }
for (Numerical num : getNumericals()) {
FormatSort fs = num.getFormatSort();

View file

@ -35,7 +35,6 @@ import com.rebuild.core.service.notification.NotificationObserver;
import com.rebuild.core.service.query.QueryHelper;
import com.rebuild.core.service.trigger.*;
import com.rebuild.core.service.trigger.impl.AutoApproval;
import com.rebuild.core.service.trigger.impl.GroupAggregation;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.task.TaskExecutors;
import lombok.extern.slf4j.Slf4j;
@ -51,7 +50,7 @@ import java.util.*;
* <br>- 会带有系统设置规则的执行
* <br>- 会开启一个事务详见 <tt>application-bean.xml</tt> 配置
*
* 如有需要其他实体可根据自身业务继承并复写
* <p>如有需要其他实体可根据自身业务继承并复写</p>
*
* FIXME 删除主记录时会关联删除明细记录持久层实现但明细记录不会触发业务规则
*
@ -99,13 +98,14 @@ public class GeneralEntityService extends ObservableService implements EntitySer
// 含明细
final boolean hasDetails = details != null && !details.isEmpty();
boolean hasAutoApprovalForDetails = false;
boolean lazyAutoApprovalForDetails = false;
if (hasDetails) {
Entity de = record.getEntity().getDetailEntity();
TriggerAction[] hasTriggers = de == null ? null
: RobotTriggerManager.instance.getActions(de, TriggerWhen.APPROVED);
hasAutoApprovalForDetails = hasTriggers != null && hasTriggers.length > 0;
AutoApproval.setLazyAutoApproval();
TriggerAction[] hasTriggers = getSpecTriggers(
record.getEntity().getDetailEntity(), null, TriggerWhen.APPROVED);
lazyAutoApprovalForDetails = hasTriggers.length > 0;
// 自动审批延迟执行因为明细尚未保存好
if (lazyAutoApprovalForDetails) AutoApproval.setLazyAutoApproval();
}
try {
@ -149,7 +149,7 @@ public class GeneralEntityService extends ObservableService implements EntitySer
return record;
} finally {
if (hasAutoApprovalForDetails) {
if (lazyAutoApprovalForDetails) {
AutoApproval.executeLazyAutoApproval();
}
}
@ -173,18 +173,9 @@ public class GeneralEntityService extends ObservableService implements EntitySer
record = super.update(record);
// 主记录修改时传导给明细若有以便触发分组聚合触发器
Entity de = record.getEntity().getDetailEntity();
if (de != null) {
TriggerAction[] hasTriggers = RobotTriggerManager.instance.getActions(de, TriggerWhen.UPDATE);
boolean hasGroupAggregation = false;
for (TriggerAction ta : hasTriggers) {
if (ta instanceof GroupAggregation) {
hasGroupAggregation = true;
break;
}
}
if (hasGroupAggregation) {
TriggerAction[] hasTriggers = getSpecTriggers(record.getEntity().getDetailEntity(),
ActionType.GROUPAGGREGATION, TriggerWhen.UPDATE);
if (hasTriggers.length > 0) {
RobotTriggerManual triggerManual = new RobotTriggerManual();
ID opUser = UserService.SYSTEM_USER;
@ -194,7 +185,6 @@ public class GeneralEntityService extends ObservableService implements EntitySer
OperatingContext.create(opUser, BizzPermission.UPDATE, dUpdate, dUpdate));
}
}
}
return record;
}
@ -723,16 +713,21 @@ public class GeneralEntityService extends ObservableService implements EntitySer
// 传导给明细若有
// FIXME 此时明细可能尚未做好变更例如新建自动审批
Entity de = approvalRecord.getEntity().getDetailEntity();
TriggerAction[] hasTriggers = de == null ? null : RobotTriggerManager.instance.getActions(de,
TriggerAction[] hasTriggers = getSpecTriggers(approvalRecord.getEntity().getDetailEntity(), null,
state == ApprovalState.APPROVED ? TriggerWhen.APPROVED : TriggerWhen.REVOKED);
if (hasTriggers != null && hasTriggers.length > 0) {
if (hasTriggers.length > 0) {
for (ID did : QueryHelper.detailIdsNoFilter(recordId, 0)) {
Record dAfter = EntityHelper.forUpdate(did, approvalUser, false);
if (state == ApprovalState.REVOKED) {
triggerManual.onRevoked(
OperatingContext.create(approvalUser, BizzPermission.UPDATE, null, dAfter));
} else {
triggerManual.onApproved(
OperatingContext.create(approvalUser, BizzPermission.UPDATE, null, dAfter));
}
}
}
Record before = approvalRecord.clone();
if (state == ApprovalState.REVOKED) {
@ -749,4 +744,18 @@ public class GeneralEntityService extends ObservableService implements EntitySer
new RevisionHistoryObserver().onApprovalManual(
OperatingContext.create(approvalUser, InternalPermission.APPROVAL, before, approvalRecord));
}
// 获取指定的触发器
private TriggerAction[] getSpecTriggers(Entity entity, ActionType specType, TriggerWhen... when) {
if (entity == null || when.length == 0) return new TriggerAction[0];
TriggerAction[] triggers = RobotTriggerManager.instance.getActions(entity, when);
if (triggers.length == 0 || specType == null) return triggers;
List<TriggerAction> specTriggers = new ArrayList<>();
for (TriggerAction t : triggers) {
if (t.getType() == specType) specTriggers.add(t);
}
return specTriggers.toArray(new TriggerAction[0]);
}
}

View file

@ -23,13 +23,12 @@ import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.privileges.PrivilegesGuardContextHolder;
import com.rebuild.core.service.TransactionManual;
import com.rebuild.core.service.general.GeneralEntityService;
import com.rebuild.core.service.general.GeneralEntityServiceContextHolder;
import com.rebuild.core.service.query.FilterRecordChecker;
import com.rebuild.core.support.SetUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.transaction.TransactionStatus;
import java.util.*;
@ -49,6 +48,9 @@ public class RecordTransfomer extends SetUser {
final private JSONObject transConfig;
final private boolean skipGuard;
// 所有新建的记录
private List<ID> newIds = new ArrayList<>();
/**
* @param trnasid
*/
@ -70,6 +72,13 @@ public class RecordTransfomer extends SetUser {
this.skipGuard = skipGuard;
}
/**
* @return
*/
public List<ID> getNewIds() {
return newIds;
}
/**
* @param sourceRecordId
* @return
@ -95,61 +104,83 @@ public class RecordTransfomer extends SetUser {
* @see #checkFilter(ID)
*/
public ID transform(ID sourceRecordId, ID mainId) {
// 手动事务因为可能要转换多条记录
TransactionStatus tx = TransactionManual.newTransaction();
try {
// 主记录
Map<String, Object> map = null;
if (mainId != null) {
Field targetDtf = MetadataHelper.getDetailToMainField(targetEntity);
map = Collections.singletonMap(targetDtf.getName(), mainId);
}
// 检查配置
Entity sourceEntity = MetadataHelper.getEntity(sourceRecordId.getEntityCode());
Entity sourceDetailEntity = null;
//
JSONObject fieldsMapping = transConfig.getJSONObject("fieldsMapping");
if (fieldsMapping == null || fieldsMapping.isEmpty()) {
throw new ConfigurationException("Invalid config of transform : " + transConfig);
}
final Entity sourceEntity = MetadataHelper.getEntity(sourceRecordId.getEntityCode());
final ID newId = saveRecord(sourceEntity, targetEntity, fieldsMapping, sourceRecordId, map);
if (newId == null) {
throw new ConfigurationException("Cannot transform record of main : " + transConfig);
}
// 明细记录如有
// 明细
JSONObject fieldsMappingDetail = transConfig.getJSONObject("fieldsMappingDetail");
Object[][] sourceDetails = null;
if (fieldsMappingDetail != null && !fieldsMappingDetail.isEmpty()) {
Entity sourceDetailEntity = sourceEntity.getDetailEntity();
Field sourceDtf = MetadataHelper.getDetailToMainField(sourceDetailEntity);
sourceDetailEntity = sourceEntity.getDetailEntity();
Field sourceRefField;
// v2.10 1 > 2+明细
if (sourceDetailEntity == null) {
sourceDetailEntity = sourceEntity;
sourceRefField = sourceDetailEntity.getPrimaryField();
} else {
sourceRefField = MetadataHelper.getDetailToMainField(sourceDetailEntity);
}
String sql = String.format(
"select %s from %s where %s = '%s'",
sourceDetailEntity.getPrimaryField().getName(), sourceDetailEntity.getName(), sourceDtf.getName(), sourceRecordId);
Object[][] details = Application.createQueryNoFilter(sql).array();
sourceDetailEntity.getPrimaryField().getName(), sourceDetailEntity.getName(), sourceRefField.getName(), sourceRecordId);
sourceDetails = Application.createQueryNoFilter(sql).array();
}
Map<String, Object> dvMap = null;
if (mainId != null) {
Field targetDtf = MetadataHelper.getDetailToMainField(targetEntity);
dvMap = Collections.singletonMap(targetDtf.getName(), mainId);
}
Record main = transformRecord(sourceEntity, targetEntity, fieldsMapping, sourceRecordId, dvMap);
ID newId;
// 有多条+明细
if (sourceDetails != null && sourceDetails.length > 0) {
Entity targetDetailEntity = targetEntity.getDetailEntity();
if (details.length > 0) {
Field targetDtf = MetadataHelper.getDetailToMainField(targetDetailEntity);
map = Collections.singletonMap(targetDtf.getName(), newId);
List<Record> detailsList = new ArrayList<>();
for (Object[] d : sourceDetails) {
detailsList.add(transformRecord(sourceDetailEntity, targetDetailEntity, fieldsMappingDetail, (ID) d[0], null));
}
for (Object[] o : details) {
saveRecord(sourceDetailEntity, targetDetailEntity, fieldsMappingDetail, (ID) o[0], map);
}
newId = saveRecord(main, detailsList);
} else {
newId = saveRecord(main, null);
}
// 回填
fillback(sourceRecordId, newId);
TransactionManual.commit(tx);
return newId;
}
} catch (Exception ex) {
TransactionManual.rollback(tx);
throw ex;
private ID saveRecord(Record record, List<Record> detailsList) {
if (this.skipGuard) {
PrivilegesGuardContextHolder.setSkipGuard(EntityHelper.UNSAVED_ID);
}
if (detailsList != null && !detailsList.isEmpty()) {
record.setObjectValue(GeneralEntityService.HAS_DETAILS, detailsList);
GeneralEntityServiceContextHolder.setRepeatedCheckMode(GeneralEntityServiceContextHolder.RCM_CHECK_DETAILS);
} else {
GeneralEntityServiceContextHolder.setRepeatedCheckMode(GeneralEntityServiceContextHolder.RCM_CHECK_ALL);
}
try {
record = Application.getEntityService(targetEntity.getEntityCode()).createOrUpdate(record);
return record.getPrimary();
} finally {
GeneralEntityServiceContextHolder.getRepeatedCheckModeOnce();
if (this.skipGuard) PrivilegesGuardContextHolder.getSkipGuardOnce();
}
}
@ -193,24 +224,19 @@ public class RecordTransfomer extends SetUser {
return true;
}
private ID saveRecord(
Entity sourceEntity, Entity targetEntity, JSONObject fieldsMapping,
ID sourceRecordId, Map<String, Object> defaultValue) {
return (ID) transformRecord(sourceEntity, targetEntity, fieldsMapping, sourceRecordId, defaultValue, true);
}
/**
* 转换
*
* @param sourceEntity
* @param targetEntity
* @param fieldsMapping
* @param sourceRecordId
* @param defaultValue
* @param save
* @return Returns ID or Record
* @return
*/
protected Object transformRecord(
protected Record transformRecord(
Entity sourceEntity, Entity targetEntity, JSONObject fieldsMapping,
ID sourceRecordId, Map<String, Object> defaultValue, boolean save) {
ID sourceRecordId, Map<String, Object> defaultValue) {
Record target = EntityHelper.forNew(targetEntity.getEntityCode(), getUser());
@ -222,7 +248,7 @@ public class RecordTransfomer extends SetUser {
List<String> validFields = checkAndWarnFields(sourceEntity, fieldsMapping.values());
if (validFields.isEmpty()) {
log.warn("No fields for transform");
log.warn("No fields for transform : {}", fieldsMapping);
return null;
}
@ -255,20 +281,7 @@ public class RecordTransfomer extends SetUser {
}
}
if (!save) return target;
if (this.skipGuard) {
PrivilegesGuardContextHolder.setSkipGuard(EntityHelper.UNSAVED_ID);
}
GeneralEntityServiceContextHolder.setRepeatedCheckMode(GeneralEntityServiceContextHolder.RCM_CHECK_MAIN);
try {
target = Application.getEntityService(targetEntity.getEntityCode()).createOrUpdate(target);
return target.getPrimary();
} finally {
GeneralEntityServiceContextHolder.getRepeatedCheckModeOnce();
if (this.skipGuard) PrivilegesGuardContextHolder.getSkipGuardOnce();
}
return target;
}
private List<String> checkAndWarnFields(Entity entity, Collection<?> fields) {

View file

@ -17,8 +17,8 @@ import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.configuration.ConfigurationException;
import com.rebuild.core.configuration.general.FormsBuilderContextHolder;
import com.rebuild.core.configuration.general.FormsBuilder;
import com.rebuild.core.configuration.general.FormsBuilderContextHolder;
import com.rebuild.core.configuration.general.TransformManager;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
@ -88,8 +88,8 @@ public class TransformerPreview {
FormsBuilderContextHolder.setMainIdOfDetail(fakeMainid);
try {
for (ID did : ids) {
Record targetRecord = (Record) transfomer.transformRecord(
sourceEntity, targetEntity, fieldsMapping, did, null, false);
Record targetRecord = transfomer.transformRecord(
sourceEntity, targetEntity, fieldsMapping, did, null);
fillLabelOfReference(targetRecord);
JSON model = UseFormsBuilder.instance.buildNewForm(targetEntity, targetRecord, user);
@ -113,8 +113,8 @@ public class TransformerPreview {
throw new ConfigurationException("Invalid config of transform : " + transConfig);
}
Record targetRecord = (Record) transfomer.transformRecord(
sourceEntity, targetEntity, fieldsMapping, sourceId, null, false);
Record targetRecord = transfomer.transformRecord(
sourceEntity, targetEntity, fieldsMapping, sourceId, null);
fillLabelOfReference(targetRecord);
// 转为明细

View file

@ -78,8 +78,9 @@ public class ActionFactory {
}
@Override
public void execute(OperatingContext operatingContext) throws TriggerException {
public Object execute(OperatingContext operatingContext) throws TriggerException {
log.warn("@rbv not attached");
return "@rbv";
}
}
}

View file

@ -15,6 +15,7 @@ import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.general.OperatingObserver;
import com.rebuild.core.service.general.RepeatedRecordsException;
import com.rebuild.core.service.trigger.impl.FieldAggregation;
import com.rebuild.core.support.CommonsLog;
import com.rebuild.core.support.i18n.Language;
import lombok.extern.slf4j.Slf4j;
@ -113,7 +114,7 @@ public class RobotTriggerObserver extends OperatingObserver {
TriggerAction[] beExecuted = when == TriggerWhen.DELETE
? DELETE_BEFORE_HOLD.get(primaryId)
: RobotTriggerManager.instance.getActions(getEffectedId(context), when);
: RobotTriggerManager.instance.getActions(getRealRecordId(context), when);
if (beExecuted == null || beExecuted.length == 0) {
return;
}
@ -125,6 +126,10 @@ public class RobotTriggerObserver extends OperatingObserver {
if (originTriggerSource) {
TRIGGER_SOURCE.set(new TriggerSource(context, when));
// 强制清理一次正常不会出现此情况
Object o = FieldAggregation.cleanTriggerChain();
if (o != null) log.warn("Force clean last trigger-chain : {}", o);
} else {
// 是否自己触发自己避免无限执行
boolean isOriginRecord = primaryId.equals(triggerSource.getOriginRecord());
@ -144,24 +149,35 @@ public class RobotTriggerObserver extends OperatingObserver {
int depth = triggerSource == null ? 1 : triggerSource.getSourceDepth();
try {
for (TriggerAction action : beExecuted) {
log.info("Trigger.{} [ {} ] executing on record ({}) : {}", depth, action.getType(), when.name(), primaryId);
String w = String.format("Trigger.%d [ %s ] executed on record (%s) : %s",
depth, action.getType(), when.name(), primaryId);
System.out.println("[dev] " + w.replace("executed", "executing"));
try {
action.execute(context);
CommonsLog.createLog(TYPE_TRIGGER, context.getOperator(), action.getActionContext().getConfigId());
Object ret = action.execute(context);
log.info(w + " > " + (ret == null ? "N" : ret));
CommonsLog.createLog(TYPE_TRIGGER,
context.getOperator(), action.getActionContext().getConfigId(), String.valueOf(ret));
} catch (Throwable ex) {
log.info(w);
// DataValidate 直接抛出
if (ex instanceof DataValidateException) throw ex;
log.error("Trigger execution failed : {} << {}", action, context, ex);
CommonsLog.createLog(TYPE_TRIGGER, context.getOperator(), action.getActionContext().getConfigId(), ex);
CommonsLog.createLog(TYPE_TRIGGER,
context.getOperator(), action.getActionContext().getConfigId(), ex);
// FIXME 触发器执行失败是否抛出
if (ex instanceof MissingMetaExcetion
|| ex instanceof ExpressionRuntimeException
|| ex instanceof RepeatedRecordsException) {
throw new TriggerException(Language.L("触发器执行失败 : %s", ex.getLocalizedMessage()));
String errMsg = ex.getLocalizedMessage();
if (ex instanceof RepeatedRecordsException) errMsg = Language.L("存在重复记录");
throw new TriggerException(Language.L("触发器执行失败 : %s", errMsg));
} else if (ex instanceof TriggerException) {
throw (TriggerException) ex;
} else {
@ -169,16 +185,16 @@ public class RobotTriggerObserver extends OperatingObserver {
}
} finally {
if (originTriggerSource) {
action.clean();
}
}
}
} finally {
if (originTriggerSource) {
log.info("Clear trigger-source : {}", getTriggerSource());
TRIGGER_SOURCE.remove();
FieldAggregation.cleanTriggerChain();
}
}
}
@ -189,12 +205,12 @@ public class RobotTriggerObserver extends OperatingObserver {
*
* @return
*/
private ID getEffectedId(OperatingContext context) {
ID effectId = context.getAnyRecord().getPrimary();
if (effectId.getEntityCode() == EntityHelper.ShareAccess) {
effectId = context.getAnyRecord().getID("recordId");
private ID getRealRecordId(OperatingContext context) {
ID recordId = context.getAnyRecord().getPrimary();
if (recordId.getEntityCode() == EntityHelper.ShareAccess) {
recordId = context.getAnyRecord().getID("recordId");
}
return effectId;
return recordId;
}
// --

View file

@ -30,7 +30,7 @@ public abstract class TriggerAction {
abstract public ActionType getType();
abstract public void execute(OperatingContext operatingContext) throws TriggerException;
abstract public Object execute(OperatingContext operatingContext) throws TriggerException;
/**
* 如果是删除动作会先调用此方法可在此方法中保持一些数据状态以便删除后还可继续使用
@ -59,6 +59,6 @@ public abstract class TriggerAction {
@Override
public String toString() {
return super.toString() + "#" + actionContext.getConfigId();
return getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + "#" + actionContext.getConfigId();
}
}

View file

@ -8,7 +8,9 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.trigger;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.Application;
import com.rebuild.core.service.general.OperatingContext;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@ -17,6 +19,7 @@ import java.util.List;
* @author RB
* @since 2022/07/04
*/
@Slf4j
public class TriggerSource {
private final List<Object[]> sources = new ArrayList<>();
@ -24,6 +27,7 @@ public class TriggerSource {
protected TriggerSource(OperatingContext origin, TriggerWhen originAction) {
addNext(origin, originAction);
if (Application.devMode()) log.warn("[dev] New trigger-source : {}", this);
}
public void addNext(OperatingContext next, TriggerWhen nextAction) {

View file

@ -51,20 +51,19 @@ public class AutoApproval extends TriggerAction {
}
@Override
public void execute(OperatingContext operatingContext) throws TriggerException {
public Object execute(OperatingContext operatingContext) throws TriggerException {
this.operatingContext = operatingContext;
List<AutoApproval> lazyed;
if ((lazyed = isLazyAutoApproval(false)) != null) {
lazyed.add(this);
log.info("Lazy AutoApproval : {}", lazyed);
return;
return "lazy";
}
ID recordId = operatingContext.getAnyRecord().getPrimary();
String useApproval = ((JSONObject) actionContext.getActionContent()).getString("useApproval");
// ID approver = operatingContext.getOperator();
ID approver = UserService.SYSTEM_USER;
ID approvalId = ID.isId(useApproval) ? ID.valueOf(useApproval) : null;
@ -77,6 +76,7 @@ public class AutoApproval extends TriggerAction {
} else {
Application.getBean(ApprovalStepService.class).txAutoApproved(recordId, approver, approvalId);
}
return "approval:" + recordId;
}
@Override

View file

@ -49,14 +49,15 @@ public class AutoAssign extends TriggerAction {
}
@Override
public void execute(OperatingContext operatingContext) throws TriggerException {
public Object execute(OperatingContext operatingContext) throws TriggerException {
final JSONObject content = (JSONObject) actionContext.getActionContent();
final ID recordId = operatingContext.getAnyRecord().getPrimary();
JSONArray assignTo = content.getJSONArray("assignTo");
Set<ID> toUsers = UserHelper.parseUsers(assignTo, recordId, true);
if (toUsers.isEmpty()) {
return;
log.warn("No andy users found : {}", assignTo);
return null;
}
ID toUser = null;
@ -113,5 +114,6 @@ public class AutoAssign extends TriggerAction {
PrivilegesGuardContextHolder.getSkipGuardOnce();
GeneralEntityServiceContextHolder.isFromTrigger(true);
}
return "assign:" + toUser;
}
}

View file

@ -42,14 +42,15 @@ public class AutoShare extends AutoAssign {
}
@Override
public void execute(OperatingContext operatingContext) throws TriggerException {
public Object execute(OperatingContext operatingContext) throws TriggerException {
final JSONObject content = (JSONObject) actionContext.getActionContent();
final ID recordId = operatingContext.getAnyRecord().getPrimary();
JSONArray shareTo = content.getJSONArray("shareTo");
Set<ID> toUsers = UserHelper.parseUsers(shareTo, recordId, true);
if (toUsers.isEmpty()) {
return;
log.warn("No andy users found : {}", shareTo);
return null;
}
String hasCascades = ((JSONObject) actionContext.getActionContent()).getString("cascades");
@ -75,5 +76,6 @@ public class AutoShare extends AutoAssign {
GeneralEntityServiceContextHolder.isAllowForceUpdateOnce();
}
}
return "share:" + toUsers;
}
}

View file

@ -93,13 +93,17 @@ public class FieldAggregation extends TriggerAction {
List<String> tschain = TRIGGER_CHAIN.get();
if (tschain == null) {
tschain = new ArrayList<>();
if (Application.devMode()) log.warn("[dev] New trigger-chain : {}", this);
} else {
log.info("Occured trigger-chain : {} > {} (current)", StringUtils.join(tschain, " > "), chainName);
String w = String.format("Occured trigger-chain : %s > %s (current)", StringUtils.join(tschain, " > "), chainName);
// 在整个触发链上只触发一次避免循环调用
// 在整个触发链上只触发1次避免循环调用
// FIXME 20220804 某些场景是否允许2次而非1次???
if (tschain.contains(chainName)) {
if (Application.devMode()) log.warn("[dev] Record triggered only once on trigger-chain : {}", chainName);
log.warn(w + "! TRIGGER ONCE ONLY : {}", chainName);
return null;
} else {
log.info(w);
}
}
@ -111,15 +115,15 @@ public class FieldAggregation extends TriggerAction {
}
@Override
public void execute(OperatingContext operatingContext) throws TriggerException {
public Object execute(OperatingContext operatingContext) throws TriggerException {
final String chainName = actionContext.getConfigId() + ":" + operatingContext.getAction().getName();
final List<String> tschain = checkTriggerChain(chainName);
if (tschain == null) return;
if (tschain == null) return "trigger-once";
this.prepare(operatingContext);
if (targetRecordId == null) {
log.warn("No target record found");
return;
return null;
}
// 聚合数据过滤
@ -187,7 +191,10 @@ public class FieldAggregation extends TriggerAction {
PrivilegesGuardContextHolder.getSkipGuardOnce();
GeneralEntityServiceContextHolder.isAllowForceUpdateOnce();
}
return "target:" + targetRecord.getPrimary();
}
return "target-empty";
}
@Override
@ -223,8 +230,12 @@ public class FieldAggregation extends TriggerAction {
this.followSourceWhere = String.format("%s = '%s'", followSourceField, targetRecordId);
}
@Override
public void clean() {
/**
* @return
*/
public static Object cleanTriggerChain() {
Object o = TRIGGER_CHAIN.get();
TRIGGER_CHAIN.remove();
return o;
}
}

View file

@ -80,17 +80,17 @@ public class FieldWriteback extends FieldAggregation {
}
@Override
public void execute(OperatingContext operatingContext) throws TriggerException {
public Object execute(OperatingContext operatingContext) throws TriggerException {
final String chainName = actionContext.getConfigId() + ":" + operatingContext.getAction().getName();
final List<String> tschain = checkTriggerChain(chainName);
if (tschain == null) return;
if (tschain == null) return "trigger-once";
this.prepare(operatingContext);
if (targetRecordIds.isEmpty()) return;
if (targetRecordIds.isEmpty()) return null;
if (targetRecordData.isEmpty()) {
log.info("No data of target record available : {}", targetRecordId);
return;
return "target-empty";
}
final ServiceSpec useService = MetadataHelper.isBusinessEntity(targetEntity)
@ -100,6 +100,7 @@ public class FieldWriteback extends FieldAggregation {
final boolean forceUpdate = ((JSONObject) actionContext.getActionContent()).getBooleanValue("forceUpdate");
boolean tschainAdded = false;
List<ID> affected = new ArrayList<>();
for (ID targetRecordId : targetRecordIds) {
if (operatingContext.getAction() == BizzPermission.DELETE
&& targetRecordId.equals(operatingContext.getAnyRecord().getPrimary())) {
@ -129,12 +130,14 @@ public class FieldWriteback extends FieldAggregation {
try {
useService.createOrUpdate(targetRecord);
affected.add(targetRecord.getPrimary());
} finally {
PrivilegesGuardContextHolder.getSkipGuardOnce();
GeneralEntityServiceContextHolder.isAllowForceUpdateOnce();
GeneralEntityServiceContextHolder.getRepeatedCheckModeOnce();
}
}
return "target:" + affected;
}
@Override

View file

@ -25,10 +25,10 @@ import java.util.List;
/**
* 分组聚合目标数据刷新
* 场景举例
* 1. 新建产品A + 仓库A分组组合A+A
* 2. 之后修改了仓库A > B组合A+B此时原组合A+A纪录不会更新
* 3. 这里需要强制更新相关原纪录
* 4. NOTE 如果组合值均为空则无法匹配任何目标记录此时需要全量刷新通过任一字段必填解决
* 1.1 新建产品A + 仓库A分组组合A+A
* 1.2 修改仓库A > B组合A+B此时原组合A+A纪录不会更新
* 2. 这里需要强制更新相关原纪录
* 3. NOTE 如果组合值均为空则无法匹配任何目标记录此时需要全量刷新通过任一字段必填解决
*
* @author RB
* @since 2022/7/8

View file

@ -56,7 +56,7 @@ public class SendNotification extends TriggerAction {
}
@Override
public void execute(OperatingContext operatingContext) {
public Object execute(OperatingContext operatingContext) {
ThreadPool.exec(() -> {
try {
// FIXME 等待事物完成
@ -67,6 +67,7 @@ public class SendNotification extends TriggerAction {
log.error(null, ex);
}
});
return "async";
}
private void executeAsync(OperatingContext operatingContext) {

View file

@ -14,6 +14,9 @@ import com.rebuild.core.Application;
import com.rebuild.core.metadata.MetadataHelper;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.Set;
/**
* 多引用字段支持
@ -78,4 +81,25 @@ public class N2NReferenceSupport {
Field lastField = father.getField(paths[paths.length - 1]);
return items(lastField, fatherRecordId);
}
/**
* 获取指定字段的全部引用项不含排除
*
* @param field
* @param excluded 排除指定记录
* @return
*/
public static Set<ID> itemsUsed(Field field, ID excluded) {
String sql = "select referenceId from NreferenceItem where belongEntity = ? and belongField = ?";
if (excluded != null) sql += String.format(" and recordId <> '%s'", excluded);
Object[][] array = Application.createQueryNoFilter(sql)
.setParameter(1, field.getOwnEntity().getName())
.setParameter(2, field.getName())
.array();
Set<ID> set = new HashSet<>();
for (Object[] o : array) set.add((ID) o[0]);
return set;
}
}

View file

@ -15,7 +15,7 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.configuration.general.AdvFilterManager;
import com.rebuild.core.configuration.general.DataListClass;
import com.rebuild.core.configuration.general.DataListCategory;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.DisplayType;
@ -46,7 +46,7 @@ public class ProtocolFilterParser {
final private String protocolExpr;
/**
* @param protocolExpr via:xxx:[field] ref:xxx:[id] class:entity:value
* @param protocolExpr via:xxx:[field] ref:xxx:[id] category:entity:value
*/
public ProtocolFilterParser(String protocolExpr) {
this.protocolExpr = protocolExpr;
@ -64,8 +64,8 @@ public class ProtocolFilterParser {
case "ref": {
return parseRef(ps[1], ps.length > 2 ? ps[2] : null);
}
case "class": {
return parseClass(ps[1], ps[2]);
case "category": {
return parseCategory(ps[1], ps[2]);
}
default: {
log.warn("Unknown protocol expr : {}", protocolExpr);
@ -160,9 +160,9 @@ public class ProtocolFilterParser {
* @return
* @see AdvFilterParser#parseItem(JSONObject, JSONObject)
*/
public String parseClass(String entity, String value) {
public String parseCategory(String entity, String value) {
Entity rootEntity = MetadataHelper.getEntity(entity);
Field classField = DataListClass.getFieldOfClass(rootEntity);
Field classField = DataListCategory.getFieldOfCategory(rootEntity);
if (classField == null) return "(9=9)";
DisplayType dt = EasyMetaFactory.getDisplayType(classField);

View file

@ -231,7 +231,7 @@ public class MetaEntityController extends BaseController {
File dest = RebuildConfiguration.getFileOfTemp("schema-" + entity.getName() + ".json");
if (dest.exists()) FileUtils.deleteQuietly(dest);
new MetaSchemaGenerator(entity).generate(dest);
new MetaSchemaGenerator(entity, true).generate(dest);
if (ServletUtils.isAjaxRequest(request)) {
writeSuccess(response, JSONUtils.toJSONObject("file", dest.getName()));

View file

@ -15,7 +15,7 @@ import com.rebuild.api.RespBody;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.configuration.general.BaseLayoutManager;
import com.rebuild.core.configuration.general.DataListClass;
import com.rebuild.core.configuration.general.DataListCategory;
import com.rebuild.core.configuration.general.DataListManager;
import com.rebuild.core.configuration.general.LayoutConfigService;
import com.rebuild.core.metadata.EntityHelper;
@ -66,9 +66,9 @@ public class WidgetController extends BaseController implements ShareTo {
return RespBody.ok(config == null ? null : config.toJSON());
}
@GetMapping("widget-class-data")
public RespBody getClassData(@PathVariable String entity) {
JSON data = DataListClass.datas(MetadataHelper.getEntity(entity), null);
@GetMapping("widget-category-data")
public RespBody getCategoryData(@PathVariable String entity) {
JSON data = DataListCategory.datas(MetadataHelper.getEntity(entity), null);
return RespBody.ok(data);
}
}

View file

@ -83,13 +83,13 @@ public class GeneralListController extends EntityController {
// 扩展配置
String advListHideFilters = easyEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_HIDE_FILTERS);
String advListHideCharts = easyEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_HIDE_CHARTS);
String advListShowClass = easyEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_SHOWCLASS);
String advListShowCategory = easyEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_SHOWCATEGORY);
mv.getModel().put(EasyEntityConfigProps.ADV_LIST_HIDE_FILTERS, advListHideFilters);
mv.getModel().put(EasyEntityConfigProps.ADV_LIST_HIDE_CHARTS, advListHideCharts);
mv.getModel().put(EasyEntityConfigProps.ADV_LIST_SHOWCLASS, StringUtils.isNotBlank(advListShowClass));
mv.getModel().put(EasyEntityConfigProps.ADV_LIST_SHOWCATEGORY, StringUtils.isNotBlank(advListShowCategory));
mv.getModel().put("hideAside",
BooleanUtils.toBoolean(advListHideFilters) && BooleanUtils.toBoolean(advListHideCharts) && StringUtils.isBlank(advListShowClass));
BooleanUtils.toBoolean(advListHideFilters) && BooleanUtils.toBoolean(advListHideCharts) && StringUtils.isBlank(advListShowCategory));
// 查询面板

View file

@ -77,9 +77,13 @@ public class TransformConfigController extends BaseController {
mv.getModelMap().put("sourceEntity", buildEntity(sourceEntity, true));
mv.getModelMap().put("targetEntity", buildEntity(targetEntity, false));
// 明细两个实体均有明细才返回
if (sourceEntity.getDetailEntity() != null && targetEntity.getDetailEntity() != null) {
// v2.10 目标为主实体
if (targetEntity.getDetailEntity() != null) {
if (sourceEntity.getDetailEntity() != null) {
mv.getModelMap().put("sourceDetailEntity", buildEntity(sourceEntity.getDetailEntity(), true));
} else {
mv.getModelMap().put("sourceDetailEntity", buildEntity(sourceEntity, true));
}
mv.getModelMap().put("targetDetailEntity", buildEntity(targetEntity.getDetailEntity(), false));
}

View file

@ -107,7 +107,7 @@ public class TriggerAdminController extends BaseController {
public Object[][] triggerList(HttpServletRequest request) {
String belongEntity = getParameter(request, "entity");
String q = getParameter(request, "q");
String sql = "select configId,belongEntity,belongEntity,name,isDisabled,modifiedOn,when,actionType,configId from RobotTriggerConfig" +
String sql = "select configId,belongEntity,belongEntity,name,isDisabled,modifiedOn,when,actionType,configId,priority from RobotTriggerConfig" +
" where (1=1) and (2=2)" +
" order by modifiedOn desc, name";

View file

@ -1,5 +1,5 @@
# !!! `db.*` ONLY FOR DEV MODE !!!
db.url: jdbc:mysql://127.0.0.1:3306/rebuild20?characterEncoding=UTF8&useUnicode=true&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B08:00
db.url: jdbc:mysql://127.0.0.1:3306/rebuild30?characterEncoding=UTF8&useUnicode=true&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B08:00
db.user: rebuild
db.passwd: rebuild
# Use built-in ehcache if redis not defined or unavailable

View file

@ -2264,5 +2264,16 @@
"请选择驳回方式":"请选择驳回方式",
"系统用户":"系统用户",
"下载模板":"下载模板",
"退回至":"退回至"
"退回至":"退回至",
"提交模式":"提交模式",
"启用提交模式必须选择审批流程":"启用提交模式必须选择审批流程",
"显示侧栏“分类”":"显示侧栏“分类”",
"显示侧栏“常用查询”":"显示侧栏“常用查询”",
"仅提交不做自动审批。选择的审批流程至少配置一个审批人,否则会提交失败":"仅提交不做自动审批。选择的审批流程至少配置一个审批人,否则会提交失败",
"需要先添加 [记录转换](../transforms) 才能在此处选择":"需要先添加 [记录转换](../transforms) 才能在此处选择",
"打开新建页面而非直接转换":"打开新建页面而非直接转换",
"显示侧栏“图表”":"显示侧栏“图表”",
"选择的审批流程至少配置一个审批人":"选择的审批流程至少配置一个审批人",
"需要先添加 [审批流程](../approvals) 才能在此处选择":"需要先添加 [审批流程](../approvals) 才能在此处选择",
"Category":"分类"
}

View file

@ -7,12 +7,12 @@
-- #1 database/user
-- 首次使用请移除以下注释以创建数据库和用户
/*
CREATE DATABASE rebuild20 COLLATE utf8mb4_general_ci;
CREATE DATABASE rebuild30 COLLATE utf8mb4_general_ci;
CREATE USER 'rebuild'@'127.0.0.1' IDENTIFIED BY 'rebuild';
GRANT ALL PRIVILEGES ON rebuild20.* TO 'rebuild'@'127.0.0.1';
GRANT ALL PRIVILEGES ON rebuild30.* TO 'rebuild'@'127.0.0.1';
GRANT RELOAD ON *.* TO 'rebuild'@'127.0.0.1';
FLUSH PRIVILEGES;
USE rebuild20;
USE rebuild30;
*/
-- #2 schemas

View file

@ -191,6 +191,6 @@
<script th:src="@{/assets/js/metadata/field-valueset.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/field-formula.js}" type="text/babel"></script>
<script th:src="@{/assets/js/trigger/trigger-design.js}" type="text/babel"></script>
<script th:src="|${baseUrl}/assets/js/trigger/trigger.${actionType}.js?v=2.3.0416|" type="text/babel"></script>
<script th:src="|${baseUrl}/assets/js/trigger/trigger.${actionType}.js?v=2.10|" type="text/babel"></script>
</body>
</html>

View file

@ -49,6 +49,7 @@
<th>[[${bundle.L('源实体')}]]</th>
<th>[[${bundle.L('触发类型')}]]</th>
<th class="no-sort">[[${bundle.L('触发动作')}]]</th>
<th width="80" class="int-sort">[[${bundle.L('优先级')}]]</th>
<th width="80" class="no-sort">[[${bundle.L('启用')}]]</th>
<th width="120" class="no-sort">[[${bundle.L('修改时间')}]]</th>
<th width="120" class="no-sort"></th>

View file

@ -3377,6 +3377,11 @@ form {
overflow: hidden;
}
.page-aside.widgets .nav-tabs > li.nav-item a.nav-link {
padding-left: 16px;
padding-right: 16px;
}
.page-aside.widgets .tab-container {
margin-top: 2px;
min-width: 279px;

View file

@ -76,7 +76,16 @@ class ConfigList extends React.Component {
})
// 简单排序
if ($.tablesort) $('.tablesort').tablesort()
if ($.tablesort) {
$('.tablesort').tablesort()
// 数字排序
$('table th.int-sort').each(function () {
$(this).data('sortBy', (th, td) => {
return ~~$(td).text()
})
})
}
}
// 加载数据

View file

@ -161,7 +161,7 @@ class DatabaseConf extends React.Component {
<div className="form-group row">
<div className="col-sm-3 col-form-label text-sm-right">{$L('数据库名称')}</div>
<div className="col-sm-7">
<input type="text" className="form-control form-control-sm" name="dbName" value={this.state.dbName || ''} onChange={this.handleValue} placeholder="rebuild20" />
<input type="text" className="form-control form-control-sm" name="dbName" value={this.state.dbName || ''} onChange={this.handleValue} placeholder="rebuild30" />
<div className="form-text">{$L('如数据库不存在系统将自动创建')}</div>
</div>
</div>
@ -228,7 +228,7 @@ class DatabaseConf extends React.Component {
dbType: 'mysql',
dbHost: this.state.dbHost || '127.0.0.1',
dbPort: this.state.dbPort || 3306,
dbName: this.state.dbName || 'rebuild20',
dbName: this.state.dbName || 'rebuild30',
dbUser: this.state.dbUser || 'rebuild',
dbPassword: this.state.dbPassword || 'rebuild',
}

View file

@ -122,16 +122,16 @@ class DlgMode1Option extends RbFormHandler {
<label className="col-sm-3 col-form-label text-sm-right">{$L('显示侧栏分类')}</label>
<div className="col-sm-7">
<div className="switch-button switch-button-xs">
<input type="checkbox" id="advListShowClass" defaultChecked={wpc.extConfig && wpc.extConfig.advListShowClass} />
<input type="checkbox" id="advListShowCategory" defaultChecked={wpc.extConfig && wpc.extConfig.advListShowCategory} />
<span>
<label htmlFor="advListShowClass" />
<label htmlFor="advListShowCategory" />
</span>
</div>
<div className="clearfix"></div>
<div className={`J_advListShowClass mt-2 ${this.state.advListShowClass ? '' : 'hide'}`}>
<div className={`J_advListShowCategory mt-2 ${this.state.advListShowCategory ? '' : 'hide'}`}>
<select className="form-control form-control-sm">
{this.state.advListShowClassFields &&
this.state.advListShowClassFields.map((item) => {
{this.state.advListShowCategoryFields &&
this.state.advListShowCategoryFields.map((item) => {
return (
<option key={item.name} value={item.name}>
{item.label}
@ -182,33 +182,33 @@ class DlgMode1Option extends RbFormHandler {
componentDidMount() {
const that = this
let $class2
$('#advListShowClass').on('change', function () {
$('#advListShowCategory').on('change', function () {
if ($val(this)) {
that.setState({ advListShowClass: true })
that.setState({ advListShowCategory: true })
} else {
that.setState({ advListShowClass: null })
that.setState({ advListShowCategory: null })
}
if (!$class2) {
$class2 = $('.J_advListShowClass select')
$class2 = $('.J_advListShowCategory select')
$.get(`/commons/metadata/fields?entity=${wpc.entityName}`, (res) => {
const _data = []
res.data.forEach((item) => {
if (CLASS_TYPES.includes(item.type)) _data.push(item)
})
that.setState({ advListShowClassFields: _data }, () => {
that.setState({ advListShowCategoryFields: _data }, () => {
$class2
.select2({ placeholder: $L('选择分类字段') })
.val((wpc.extConfig && wpc.extConfig.advListShowClass) || null)
.val((wpc.extConfig && wpc.extConfig.advListShowCategory) || null)
.trigger('change')
})
})
}
})
if (wpc.extConfig && wpc.extConfig.advListShowClass) {
$('#advListShowClass').trigger('change')
if (wpc.extConfig && wpc.extConfig.advListShowCategory) {
$('#advListShowCategory').trigger('change')
}
}
@ -216,7 +216,7 @@ class DlgMode1Option extends RbFormHandler {
const o = {
advListHideFilters: !$val('#advListHideFilters'),
advListHideCharts: !$val('#advListHideCharts'),
advListShowClass: this.state.advListShowClass ? $val('.J_advListShowClass select') : null,
advListShowCategory: this.state.advListShowCategory ? $val('.J_advListShowCategory select') : null,
advListFilterPane: $val('#advListFilterPane'),
}

View file

@ -9,6 +9,14 @@ See LICENSE and COMMERCIAL in the project root for license information.
const wpc = window.__PageConfig
$(document).ready(() => {
renderRbcomp(<PreviewTable data={wpc.content} />, 'preview-table')
const $size = $('.preview-tools select').on('change', function () {
const s = $(this).val()
$('.preview-content').css('font-size', (13 * ~~s) / 10)
$storage.set('PRINTSIZE', s)
})
const s = $storage.get('PRINTSIZE')
if (s) $size.val(s).trigger('change')
})
class PreviewTable extends React.Component {

View file

@ -986,7 +986,7 @@ UserPopup.create = function (el) {
}
// ~~ HTML 内容
const WrapHtml = (htmlContent) => <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
const WrapHtml = (htmlContent) => <span dangerouslySetInnerHTML={{ __html: htmlContent }} />
// ~~ short React.Fragment
const RF = ({ children }) => <React.Fragment>{children}</React.Fragment>

View file

@ -1230,7 +1230,7 @@ const ClassWidget = {
},
loadClass() {
$.get(`/app/${wpc.entity[0]}/widget-class-data`, (res) => {
$.get(`/app/${wpc.entity[0]}/widget-category-data`, (res) => {
this._classLoaded = true
res.data &&
@ -1247,7 +1247,7 @@ const ClassWidget = {
const v = $(this).data('id')
if (v === '$ALL$') wpc.protocolFilter = null
else wpc.protocolFilter = `class:${wpc.entity[0]}:${v}`
else wpc.protocolFilter = `category:${wpc.entity[0]}:${v}`
RbListPage.reload()
})

View file

@ -59,6 +59,9 @@ class TriggerList extends ConfigList {
<td>{item[2] || item[1]}</td>
<td>{item[7]}</td>
<td>{item[6] > 0 ? $L(' %s ', formatWhen(item[6])) : <span className="text-warning">({$L('无触发动作')})</span>}</td>
<td>
<span className="badge badge-light font-weight-light">{item[9]}</span>
</td>
<td>{ShowEnable(item[4])}</td>
<td>
<DateShow date={item[5]} />

View file

@ -16,7 +16,7 @@
<div class="tab-container">
<ul class="nav nav-tabs">
<li class="nav-item" th:if="${advListHideFilters != 'true'}"><a class="nav-link" href="#asideFilters" data-toggle="tab">[[${bundle.L('常用查询')}]]</a></li>
<li class="nav-item" th:if="${advListShowClass}"><a class="nav-link J_load-class" href="#asideClass" data-toggle="tab">[[${bundle.L('分类')}]]</a></li>
<li class="nav-item" th:if="${advListShowCategory}"><a class="nav-link J_load-class" href="#asideClass" data-toggle="tab">[[${bundle.L('Category')}]]</a></li>
<li class="nav-item" th:if="${advListHideCharts != 'true'}"><a class="nav-link J_load-charts" href="#asideWidgets" data-toggle="tab">[[${bundle.L('图表')}]]</a></li>
</ul>
<div class="tab-content rb-scroller">

View file

@ -12,6 +12,14 @@
padding: 10px 15px 5px;
text-align: right;
}
.preview-tools select.form-control-sm {
width: 100px;
display: inline-block;
height: 36px !important;
min-height: 36px;
margin-right: 6px;
transform: translateY(-1.5px);
}
.preview-content {
margin: 0 20px 20px;
}
@ -22,9 +30,9 @@
}
.table th {
background-color: #eee !important;
width: auto;
min-width: 110px;
max-width: 180px;
width: 13.33%;
min-width: 120px;
max-width: 240px;
}
.table th.divider {
width: 100%;
@ -55,6 +63,20 @@
</head>
<body>
<div class="preview-tools d-print-none">
<select class="form-control form-control-sm">
<option value="10">[[${bundle.L('默认大小')}]]</option>
<option value="10">100%</option>
<option value="11">110%</option>
<option value="12">120%</option>
<option value="13">130%</option>
<option value="14">140%</option>
<option value="15">150%</option>
<option value="16">160%</option>
<option value="17">170%</option>
<option value="18">180%</option>
<option value="19">190%</option>
<option value="20">200%</option>
</select>
<button class="btn btn-space btn-primary" onclick="window.print()"><i class="icon zmdi zmdi-print"></i> [[${bundle.L('打印')}]]</button>
<button class="btn btn-space btn-secondary" onclick="window.close()">[[${bundle.L('关闭')}]]</button>
</div>

View file

@ -16,7 +16,7 @@
<div class="tab-container">
<ul class="nav nav-tabs">
<li class="nav-item" th:if="${advListHideFilters != 'true'}"><a class="nav-link" href="#asideFilters" data-toggle="tab">[[${bundle.L('常用查询')}]]</a></li>
<li class="nav-item" th:if="${advListShowClass}"><a class="nav-link J_load-class" href="#asideClass" data-toggle="tab">[[${bundle.L('分类')}]]</a></li>
<li class="nav-item" th:if="${advListShowCategory}"><a class="nav-link J_load-class" href="#asideClass" data-toggle="tab">[[${bundle.L('Category')}]]</a></li>
<li class="nav-item" th:if="${advListHideCharts != 'true'}"><a class="nav-link J_load-charts" href="#asideWidgets" data-toggle="tab">[[${bundle.L('图表')}]]</a></li>
</ul>
<div class="tab-content rb-scroller">

View file

@ -23,7 +23,7 @@ public class MetaSchemaGeneratorTest extends TestSupport {
public void testGenerate() {
if (MetadataHelper.containsEntity(Account)) {
Entity test = MetadataHelper.getEntity(Account);
MetaSchemaGenerator generator = new MetaSchemaGenerator(test);
MetaSchemaGenerator generator = new MetaSchemaGenerator(test, true);
JSON schema = generator.generate();
System.out.println(JSON.toJSONString(schema, true));
}
@ -33,7 +33,7 @@ public class MetaSchemaGeneratorTest extends TestSupport {
public void testGenerateHaveDetail() {
if (MetadataHelper.containsEntity(SalesOrder)) {
Entity test = MetadataHelper.getEntity(SalesOrder);
MetaSchemaGenerator generator = new MetaSchemaGenerator(test);
MetaSchemaGenerator generator = new MetaSchemaGenerator(test, true);
JSON schema = generator.generate();
System.out.println(JSON.toJSONString(schema, true));
}