Merge branch 'master' into develop

This commit is contained in:
RB 2022-07-12 16:53:42 +08:00
commit 7a1bafc319
64 changed files with 888 additions and 196 deletions

2
@rbv

@ -1 +1 @@
Subproject commit ee65215d7afbc581b2c091c4e21ec7156d7937e2
Subproject commit 02795e06e887e1287f1abe8ca9d617d58df9edf9

View file

@ -22,8 +22,6 @@ public class DefinedException extends RebuildException {
public static final int CODE_RECORDS_REPEATED = 499;
// 审批警告
public static final int CODE_APPROVE_WARN = 498;
// 不在安全使用范围IP时段
public static final int CODE_UNSAFE_USE = 497;
// 错误码
private int errorCode = Controller.CODE_ERROR;

View file

@ -39,6 +39,7 @@ public class EasyBool extends EasyField implements MixValue {
@Override
public Object exprDefaultValue() {
String valueExpr = (String) getRawMeta().getDefaultValue();
if ("N".equals(valueExpr)) return null;
return StringUtils.isBlank(valueExpr) ? Boolean.FALSE : BooleanUtils.toBoolean(valueExpr);
}

View file

@ -60,6 +60,13 @@ public class EasyN2NReference extends EasyReference {
String valueExpr = (String) getRawMeta().getDefaultValue();
if (StringUtils.isBlank(valueExpr)) return null;
if (valueExpr.contains(VAR_CURRENT)) {
Object id = exprCurrent();
if (id == null) return null;
else if (id instanceof ID[]) return id;
else return new ID[] {(ID) id};
}
List<ID> idArray = new ArrayList<>();
for (String id : valueExpr.split(",")) {
if (ID.isId(id)) idArray.add(ID.valueOf(id));

View file

@ -7,12 +7,22 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.metadata.easymeta;
import cn.devezhao.bizz.security.member.Team;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.UserContextHolder;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.bizz.Department;
import com.rebuild.core.privileges.bizz.User;
import com.rebuild.core.support.general.FieldValueHelper;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @author devezhao
@ -21,6 +31,8 @@ import com.rebuild.core.support.general.FieldValueHelper;
public class EasyReference extends EasyField implements MixValue {
private static final long serialVersionUID = -5001745527956303569L;
protected static final String VAR_CURRENT = "{CURRENT}";
protected EasyReference(Field field, DisplayType displayType) {
super(field, displayType);
}
@ -44,8 +56,45 @@ public class EasyReference extends EasyField implements MixValue {
@Override
public Object exprDefaultValue() {
String valueExpr = (String) getRawMeta().getDefaultValue();
if (StringUtils.isBlank(valueExpr)) return null;
if (valueExpr.contains(VAR_CURRENT)) {
Object id = exprCurrent();
if (id instanceof ID[]) return ((ID[]) id)[0];
else return id;
} else {
return ID.isId(valueExpr) ? ID.valueOf(valueExpr) : null;
}
}
/**
* @see #VAR_CURRENT
* @return returns ID or ID[]
*/
protected Object exprCurrent() {
final ID cu = UserContextHolder.getUser(true);
if (cu == null) return null;
Entity ref = getRawMeta().getReferenceEntity();
if (ref.getEntityCode() == EntityHelper.User) return cu;
User user = Application.getUserStore().getUser(cu);
if (ref.getEntityCode() == EntityHelper.Department) {
Department dept = user.getOwningDept();
return dept == null ? null : dept.getIdentity();
}
if (ref.getEntityCode() == EntityHelper.Team) {
List<ID> ts = new ArrayList<>();
for (Team t : user.getOwningTeams()) {
ts.add((ID) t.getIdentity());
}
return ts.isEmpty() ? null : ts.toArray(new ID[0]);
}
return null;
}
@Override
public Object wrapValue(Object value) {

View file

@ -30,13 +30,15 @@ public class BulkDelete extends BulkOperator {
final ID[] records = prepareRecords();
this.setTotal(records.length);
String lastError = null;
for (ID id : records) {
if (Application.getPrivilegesManager().allowDelete(context.getOpUser(), id)) {
try {
ges.delete(id, context.getCascades());
this.addSucceeded();
} catch (DataSpecificationException ex) {
log.warn("Cannot delete `{}` because : {}", id, ex.getLocalizedMessage());
lastError = ex.getLocalizedMessage();
log.warn("Cannot delete `{}` because : {}", id, lastError);
}
} else {
log.warn("No have privileges to DELETE : {} < {}", id, context.getOpUser());
@ -44,6 +46,10 @@ public class BulkDelete extends BulkOperator {
this.addCompleted();
}
if (getSucceeded() == 0 && lastError != null) {
throw new DataSpecificationException(lastError);
}
return getSucceeded();
}
}

View file

@ -32,7 +32,7 @@ import com.rebuild.core.service.general.recyclebin.RecycleStore;
import com.rebuild.core.service.general.series.SeriesGeneratorFactory;
import com.rebuild.core.service.notification.NotificationObserver;
import com.rebuild.core.service.trigger.*;
import com.rebuild.core.support.HeavyStopWatcher;
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;
@ -104,10 +104,14 @@ public class GeneralEntityService extends ObservableService implements EntitySer
boolean checkDetailsRepeated = rcm == GeneralEntityServiceContextHolder.RCM_CHECK_DETAILS
|| rcm == GeneralEntityServiceContextHolder.RCM_CHECK_ALL;
// 明细实体 Service
final EntityService des = Application.getEntityService(record.getEntity().getDetailEntity().getEntityCode());
// 先删除
for (Record d : details) {
if (d instanceof DeleteRecord) delete(d.getPrimary());
if (d instanceof DeleteRecord) des.delete(d.getPrimary());
}
// 再保存
for (Record d : details) {
if (d instanceof DeleteRecord) continue;
@ -115,7 +119,7 @@ public class GeneralEntityService extends ObservableService implements EntitySer
if (checkDetailsRepeated) {
d.setID(dtf, mainid); // for check
List<Record> repeated = getAndCheckRepeated(d, 20);
List<Record> repeated = des.getAndCheckRepeated(d, 20);
if (!repeated.isEmpty()) {
throw new RepeatedRecordsException(repeated);
}
@ -123,9 +127,9 @@ public class GeneralEntityService extends ObservableService implements EntitySer
if (d.getPrimary() == null) {
d.setID(dtf, mainid);
create(d);
des.create(d);
} else {
update(d);
des.update(d);
}
}
@ -145,7 +149,36 @@ public class GeneralEntityService extends ObservableService implements EntitySer
if (!checkModifications(record, BizzPermission.UPDATE)) {
return record;
}
return super.update(record);
// 先更新
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) {
RobotTriggerManual triggerManual = new RobotTriggerManual();
ID opUser = UserService.SYSTEM_USER;
for (ID did : queryDetails(record.getPrimary(), de, 1)) {
Record dUpdate = EntityHelper.forUpdate(did, opUser, false);
triggerManual.onUpdate(
OperatingContext.create(opUser, BizzPermission.UPDATE, dUpdate, dUpdate));
}
}
}
return record;
}
@Override
@ -203,25 +236,12 @@ public class GeneralEntityService extends ObservableService implements EntitySer
return 0;
}
// 手动删除明细记录以执行触发器
Entity hasDetailEntity = MetadataHelper.getEntity(record.getEntityCode()).getDetailEntity();
if (hasDetailEntity != null) {
TriggerAction[] hasTriggers = RobotTriggerManager.instance.getActions(hasDetailEntity, TriggerWhen.DELETE);
if (hasTriggers != null && hasTriggers.length > 0) {
String sql = String.format("select %s from %s where %s = ?",
hasDetailEntity.getPrimaryField().getName(), hasDetailEntity.getName(),
MetadataHelper.getDetailToMainField(hasDetailEntity).getName());
Object[][] details = Application.getQueryFactory().createQueryNoFilter(sql)
.setParameter(1, record).array();
HeavyStopWatcher.createWatcher("PERFORMANCE ISSUE", "DELETE:" + details.length);
for (Object[] d : details) {
// 明细无约束检查
super.delete((ID) d[0]);
}
HeavyStopWatcher.clean();
// 手动删除明细记录以执行系统规则如触发器附件删除等
Entity de = MetadataHelper.getEntity(record.getEntityCode()).getDetailEntity();
if (de != null) {
for (ID did : queryDetails(record, de, 0)) {
// 明细无约束检查 checkModifications
super.delete(did);
}
}
@ -312,10 +332,11 @@ public class GeneralEntityService extends ObservableService implements EntitySer
}
} else {
// // 可以共享给自己
// if (to.equals(Application.getRecordOwningCache().getOwningUser(record))) {
// log.debug("Share to the same user as the record, ignore : {}", record);
// }
// 可以共享给自己
if (log.isDebugEnabled()
&& to.equals(Application.getRecordOwningCache().getOwningUser(record))) {
log.debug("Share to the same user as the record, ignore : {}", record);
}
delegateService.create(sharedAfter);
affected = 1;
@ -691,22 +712,14 @@ public class GeneralEntityService extends ObservableService implements EntitySer
ID opUser = UserContextHolder.getUser();
RobotTriggerManual triggerManual = new RobotTriggerManual();
// 需处理明细
// 传导给明细若有
Entity de = approvalRecord.getEntity().getDetailEntity();
TriggerAction[] hasTriggers = de == null ? null : RobotTriggerManager.instance.getActions(de,
state == ApprovalState.APPROVED ? TriggerWhen.APPROVED : TriggerWhen.REVOKED);
if (hasTriggers != null && hasTriggers.length > 0) {
String sql = String.format("select %s from %s where %s = ?",
de.getPrimaryField().getName(), de.getName(),
MetadataHelper.getDetailToMainField(de).getName());
Object[][] details = Application.createQueryNoFilter(sql)
.setParameter(1, record)
.array();
for (Object[] d : details) {
Record dAfter = EntityHelper.forUpdate((ID) d[0], UserService.SYSTEM_USER, false);
for (ID did : queryDetails(record, de, 0)) {
Record dAfter = EntityHelper.forUpdate(did, opUser, false);
triggerManual.onApproved(
OperatingContext.create(opUser, BizzPermission.UPDATE, null, dAfter));
}
@ -723,4 +736,19 @@ public class GeneralEntityService extends ObservableService implements EntitySer
OperatingContext.create(opUser, BizzPermission.UPDATE, before, approvalRecord));
}
}
private List<ID> queryDetails(ID mainid, Entity detail, int maxSize) {
String sql = String.format("select %s from %s where %s = ?",
detail.getPrimaryField().getName(), detail.getName(),
MetadataHelper.getDetailToMainField(detail).getName());
Query query = Application.createQueryNoFilter(sql).setParameter(1, mainid);
if (maxSize > 0) query.setLimit(maxSize);
Object[][] array = query.array();
List<ID> ids = new ArrayList<>();
for (Object[] o : array) ids.add((ID) o[0]);
return ids;
}
}

View file

@ -152,7 +152,10 @@ public abstract class ObservableService extends Observable implements ServiceSpe
log.warn("RecycleBin inactivated! DELETE {} by {}", recordId, currentUser);
}
if (recycleBin != null) recycleBin.add(recordId);
if (recycleBin != null && recycleBin.add(recordId)) {
return recycleBin;
} else {
return null;
}
}
}

View file

@ -106,9 +106,8 @@ public class OperatingContext {
@Override
public String toString() {
String clearTxt = "{ Operator: %s (), Action: %s, Record(s): %s(%d) }";
return String.format(clearTxt,
getOperator(), getAction().getName(), getAnyRecord().getPrimary(), getAffected().length);
return String.format("[ Action:%s, Record(s):%s(%d) ]",
getAction().getName(), getAnyRecord().getPrimary(), getAffected().length);
}
/**

View file

@ -17,6 +17,7 @@ import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.general.recyclebin.RecycleBinCleanerJob;
import com.rebuild.core.service.trigger.RobotTriggerObserver;
import com.rebuild.core.service.trigger.TriggerSource;
import com.rebuild.utils.JSONUtils;
import lombok.extern.slf4j.Slf4j;
@ -126,9 +127,9 @@ public class RevisionHistoryObserver extends OperatingObserver {
record.setString("revisionContent", JSONUtils.EMPTY_ARRAY_STR);
}
OperatingContext triggerSource = RobotTriggerObserver.getTriggerSource();
TriggerSource triggerSource = RobotTriggerObserver.getTriggerSource();
if (triggerSource != null) {
record.setID("channelWith", triggerSource.getAnyRecord().getPrimary());
record.setID("channelWith", triggerSource.getOriginRecord());
}
if (context.getOperationIp() != null) {

View file

@ -15,6 +15,7 @@ import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.MetadataHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.io.Serializable;
@ -26,6 +27,7 @@ import java.util.List;
* @author devezhao
* @since 2019/8/19
*/
@Slf4j
public class RecycleBean implements Serializable {
private static final long serialVersionUID = -1058552856844427594L;
@ -53,6 +55,11 @@ public class RecycleBean implements Serializable {
.append(" = ?")
.toString();
Record queryed = Application.createQueryNoFilter(sql).setParameter(1, this.recordId).record();
if (queryed == null) {
log.warn("Serialize record not exists : {}", this.recordId);
return null;
}
JSONObject s = (JSONObject) queryed.serialize();
Entity detailEntity = entity.getDetailEntity();

View file

@ -47,9 +47,10 @@ public class RecycleStore {
* 添加待转存记录
*
* @param recordId
* @return
*/
public void add(ID recordId) {
this.add(recordId, null);
public boolean add(ID recordId) {
return this.add(recordId, null);
}
/**
@ -57,10 +58,14 @@ public class RecycleStore {
*
* @param recordId
* @param with
* @return
*/
public void add(ID recordId, ID with) {
public boolean add(ID recordId, ID with) {
JSON s = new RecycleBean(recordId).serialize();
if (s == null) return false;
data.add(new Object[] { recordId, s, with });
return true;
}
/**
@ -76,13 +81,13 @@ public class RecycleStore {
* @return
*/
public int store() {
Record record = EntityHelper.forNew(EntityHelper.RecycleBin, UserService.SYSTEM_USER);
record.setID("deletedBy", this.user);
record.setDate("deletedOn", CalendarUtils.now());
final Record base = EntityHelper.forNew(EntityHelper.RecycleBin, UserService.SYSTEM_USER);
base.setID("deletedBy", this.user);
base.setDate("deletedOn", CalendarUtils.now());
int affected = 0;
for (Object[] o : data) {
Record clone = record.clone();
Record clone = base.clone();
ID recordId = (ID) o[0];
Entity belongEntity = MetadataHelper.getEntity(recordId.getEntityCode());
clone.setString("belongEntity", belongEntity.getName());

View file

@ -123,7 +123,7 @@ public class AdvFilterParser extends SetUser {
indexItemSqls.put(index, itemSql.trim());
this.includeFields.add(item.getString("field"));
}
if (Application.devMode()) log.info("Parse item : {} >> {}", item, itemSql);
if (Application.devMode()) log.info("[dev] Parse item : {} >> {}", item, itemSql);
}
if (indexItemSqls.isEmpty()) return null;

View file

@ -8,13 +8,19 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.query;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Query;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.MetadataHelper;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
/**
* @author devezhao
@ -55,4 +61,48 @@ public class QueryHelper {
}
return Application.createQueryNoFilter(sql);
}
/**
* 获取完整记录
*
* @param recordId
* @return
*/
public static Record recordNoFilter(ID recordId) {
Entity entity = MetadataHelper.getEntity(recordId.getEntityCode());
List<String> fields = new ArrayList<>();
for (Field field : entity.getFields()) {
fields.add(field.getName());
}
String sql = String.format("select %s from %s where %s = ?",
StringUtils.join(fields, ","), entity.getName(),
entity.getPrimaryField().getName());
Record record = Application.createQueryNoFilter(sql).setParameter(1, recordId).record();
Assert.notNull(record, "RECORD NOT EXISTS : " + recordId);
return record;
}
/**
* 获取明细完整记录
*
* @param mainId
* @return
*/
public static List<Record> detailsNoFilter(ID mainId) {
Entity detailEntity = MetadataHelper.getEntity(mainId.getEntityCode()).getDetailEntity();
List<String> fields = new ArrayList<>();
for (Field field : detailEntity.getFields()) {
fields.add(field.getName());
}
String sql = String.format("select %s from %s where %s = ?",
StringUtils.join(fields, ","), detailEntity.getName(),
MetadataHelper.getDetailToMainField(detailEntity).getName());
return Application.createQueryNoFilter(sql).setParameter(1, mainId).list();
}
}

View file

@ -34,4 +34,11 @@ public class RobotTriggerManual extends RobotTriggerObserver {
public void onRevoked(OperatingContext context) {
execAction(context, TriggerWhen.REVOKED);
}
// -- PUBLIC
@Override
public void onUpdate(OperatingContext context) {
super.onUpdate(context);
}
}

View file

@ -10,7 +10,6 @@ package com.rebuild.core.service.trigger;
import cn.devezhao.persist4j.engine.ID;
import cn.devezhao.persist4j.metadata.MissingMetaExcetion;
import com.googlecode.aviator.exception.ExpressionRuntimeException;
import com.rebuild.core.Application;
import com.rebuild.core.RebuildException;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.service.general.OperatingContext;
@ -19,8 +18,10 @@ import com.rebuild.core.service.general.RepeatedRecordsException;
import com.rebuild.core.support.CommonsLog;
import com.rebuild.core.support.i18n.Language;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NamedThreadLocal;
import java.util.Map;
import java.util.Observable;
import java.util.concurrent.ConcurrentHashMap;
import static com.rebuild.core.support.CommonsLog.TYPE_TRIGGER;
@ -34,8 +35,15 @@ import static com.rebuild.core.support.CommonsLog.TYPE_TRIGGER;
@Slf4j
public class RobotTriggerObserver extends OperatingObserver {
private static final ThreadLocal<OperatingContext> TRIGGER_SOURCE = new ThreadLocal<>();
private static final ThreadLocal<String> TRIGGER_SOURCE_LAST = new ThreadLocal<>();
private static final ThreadLocal<TriggerSource> TRIGGER_SOURCE = new NamedThreadLocal<>("Trigger source");
private static final ThreadLocal<Boolean> SKIP_TRIGGERS = new NamedThreadLocal<>("Skip triggers");
@Override
public void update(final Observable o, Object arg) {
if (isSkipTriggers(false)) return;
super.update(o, arg);
}
@Override
protected void onCreate(OperatingContext context) {
@ -102,7 +110,6 @@ public class RobotTriggerObserver extends OperatingObserver {
*/
protected void execAction(OperatingContext context, TriggerWhen when) {
final ID primaryId = context.getAnyRecord().getPrimary();
final String sourceName = primaryId + ":" + when.name().charAt(0);
TriggerAction[] beExecuted = when == TriggerWhen.DELETE
? DELETE_ACTION_HOLDS.get(primaryId)
@ -111,25 +118,33 @@ public class RobotTriggerObserver extends OperatingObserver {
return;
}
final boolean originTriggerSource = getTriggerSource() == null;
final TriggerSource triggerSource = getTriggerSource();
final boolean originTriggerSource = triggerSource == null;
// 设置原始触发源
if (originTriggerSource) {
TRIGGER_SOURCE.set(context);
TRIGGER_SOURCE.set(new TriggerSource(context, when));
} else {
// 自己触发自己避免无限执行
boolean x = primaryId.equals(getTriggerSource().getAnyRecord().getPrimary());
boolean xor = x || sourceName.equals(TRIGGER_SOURCE_LAST.get());
if (x || xor) {
if (Application.devMode()) log.warn("Self trigger, ignore : {}", sourceName);
// 是否自己触发自己避免无限执行
boolean isOriginRecord = primaryId.equals(triggerSource.getOriginRecord());
String lastKey = triggerSource.getLastSourceKey();
triggerSource.addNext(context, when);
String currentKey = triggerSource.getLastSourceKey();
if (isOriginRecord && lastKey.equals(currentKey)) {
if (!triggerSource.isSkipOnce()) {
log.warn("Self trigger, ignore : {} < {}", currentKey, lastKey);
return;
}
}
}
TRIGGER_SOURCE_LAST.set(sourceName);
int depth = triggerSource == null ? 1 : triggerSource.getSourceDepth();
try {
for (TriggerAction action : beExecuted) {
log.info("Trigger [ {} ] executing on record ({}) : {}", action.getType(), when.name(), primaryId);
log.info("Trigger.{} [ {} ] executing on record ({}) : {}", depth, action.getType(), when.name(), primaryId);
try {
action.execute(context);
@ -162,8 +177,8 @@ public class RobotTriggerObserver extends OperatingObserver {
} finally {
if (originTriggerSource) {
log.info("Clear trigger-source : {}", getTriggerSource());
TRIGGER_SOURCE.remove();
TRIGGER_SOURCE_LAST.remove();
}
}
}
@ -182,19 +197,39 @@ public class RobotTriggerObserver extends OperatingObserver {
return effectId;
}
// --
/**
* 获取当前线程触发源如有
*
* @return
*/
public static OperatingContext getTriggerSource() {
public static TriggerSource getTriggerSource() {
return TRIGGER_SOURCE.get();
}
/**
* 强制自执行
*/
public static void forceTriggerSelf() {
TRIGGER_SOURCE_LAST.set(null);
public static void forceTriggerSelfOnce() {
getTriggerSource().setSkipOnce();
}
/**
* 跳过触发器的执行
*/
public static void setSkipTriggers() {
SKIP_TRIGGERS.set(true);
}
/**
* @param once
* @return
* @see #setSkipTriggers()
*/
public static boolean isSkipTriggers(boolean once) {
Boolean is = SKIP_TRIGGERS.get();
if (is != null && once) SKIP_TRIGGERS.remove();
return is != null && is;
}
}

View file

@ -11,7 +11,7 @@ import com.rebuild.core.service.general.OperatingContext;
/**
* 触发动作/操作定义
* 注意如果是异步处理将没有事物同时会丢失一些线程量
* 注意如果是异步处理将没有事物同时会丢失一些线程量如果需要请手动设置
*
* @author devezhao zhaofang123@gmail.com
* @since 2019/05/23

View file

@ -0,0 +1,73 @@
/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or 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.core.service.trigger;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.service.general.OperatingContext;
import java.util.ArrayList;
import java.util.List;
/**
* @author RB
* @since 2022/07/04
*/
public class TriggerSource {
private final List<Object[]> sources = new ArrayList<>();
private boolean skipOnce = false;
protected TriggerSource(OperatingContext origin, TriggerWhen originAction) {
addNext(origin, originAction);
}
public void addNext(OperatingContext next, TriggerWhen nextAction) {
sources.add(new Object[] { next, nextAction });
}
public OperatingContext getOrigin() {
return (OperatingContext) sources.get(0)[0];
}
public ID getOriginRecord() {
return getOrigin().getAnyRecord().getPrimary();
}
public OperatingContext getLast() {
return (OperatingContext) sources.get(sources.size() - 1)[0];
}
public String getLastSourceKey() {
Object[] last = sources.get(sources.size() - 1);
return ((OperatingContext) last[0]).getAnyRecord().getPrimary()
+ ":" + ((TriggerWhen) last[1]).name().charAt(0);
}
public int getSourceDepth() {
return sources.size();
}
public void setSkipOnce() {
skipOnce = true;
}
public boolean isSkipOnce() {
boolean skipOnceHold = skipOnce;
skipOnce = false;
return skipOnceHold;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Object[] s : sources) {
sb.append(((TriggerWhen) s[1]).name().charAt(0)).append("#").append(s[0]).append(" >> ");
}
return sb.append("END").toString();
}
}

View file

@ -13,6 +13,10 @@ import com.rebuild.core.service.general.EntityService;
/**
* 触发动作定义
*
* 动作传导
* 1. 主记录删除/审批/撤销审批会传导至明细
* 2. 主记录更新仅分组聚合会传导至明细
*
* @author devezhao zhaofang123@gmail.com
* @see BizzPermission
* @since 2019/05/23

View file

@ -14,6 +14,8 @@ import java.util.Date;
import java.util.Map;
/**
* Wrap {@link Date}
*
* @author devezhao
* @since 2021/4/12
*/

View file

@ -44,6 +44,7 @@ public class AviatorUtils {
addCustomFunction(new CurrentDateFunction());
addCustomFunction(new LocationDistanceFunction());
addCustomFunction(new RequestFunctuin());
addCustomFunction(new TextFunction());
}
/**

View file

@ -18,7 +18,7 @@ import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* Usage: LOCATIONDISTANCE(location1, location2)
* Usage: LOCATIONDISTANCE($location1, $location2)
* Return: Number ()
*
* @author devezhao

View file

@ -18,7 +18,7 @@ import java.io.IOException;
import java.util.Map;
/**
* Usage: REQUEST(url, [defaultValue])
* Usage: REQUEST($url, [$defaultValue])
* Return: String
*
* @author devezhao

View file

@ -0,0 +1,53 @@
/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or 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.core.service.trigger.aviator;
import cn.devezhao.persist4j.engine.ID;
import com.googlecode.aviator.runtime.function.AbstractFunction;
import com.googlecode.aviator.runtime.type.AviatorObject;
import com.googlecode.aviator.runtime.type.AviatorString;
import com.rebuild.core.support.general.FieldValueHelper;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* Usage: TEXT($id, [$defaultValue])
* Return: String
*
* @author RB
* @since 2022/7/5
*/
@Slf4j
public class TextFunction extends AbstractFunction {
private static final long serialVersionUID = 8632984920156129174L;
private static final AviatorString BLANK = new AviatorString("");
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
return call(env, arg1, BLANK);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
Object o = arg1.getValue(env);
if (!ID.isId(o)) return arg2;
ID anyid = o instanceof ID ? (ID) o : ID.valueOf(o.toString());
String text = FieldValueHelper.getLabel(anyid, null);
return text == null ? arg2 : new AviatorString(text);
}
@Override
public String getName() {
return "TEXT";
}
}

View file

@ -57,7 +57,7 @@ public class FieldAggregation extends TriggerAction {
final protected int maxTriggerDepth;
// 此触发器可能产生连锁反应
// 如触发器 A 调用 B B 又调用了 C ... 以此类推此处记录其深度
protected static final ThreadLocal<List<String>> TRIGGERS_CHAIN = new ThreadLocal<>();
protected static final ThreadLocal<List<String>> TRIGGER_CHAIN = new ThreadLocal<>();
// 源实体
protected Entity sourceEntity;
@ -90,7 +90,7 @@ public class FieldAggregation extends TriggerAction {
* @return
*/
protected List<String> checkTriggerChain(String chainName) {
List<String> tschain = TRIGGERS_CHAIN.get();
List<String> tschain = TRIGGER_CHAIN.get();
if (tschain == null) {
tschain = new ArrayList<>();
} else {
@ -98,7 +98,7 @@ public class FieldAggregation extends TriggerAction {
// 在整个触发链上只触发一次避免循环调用
if (tschain.contains(chainName)) {
if (Application.devMode()) log.warn("Record triggered only once on trigger-chain : {}", chainName);
if (Application.devMode()) log.warn("[dev] Record triggered only once on trigger-chain : {}", chainName);
return null;
}
}
@ -174,7 +174,7 @@ public class FieldAggregation extends TriggerAction {
// 会关联触发下一触发器如有
tschain.add(chainName);
TRIGGERS_CHAIN.set(tschain);
TRIGGER_CHAIN.set(tschain);
ServiceSpec useService = MetadataHelper.isBusinessEntity(targetEntity)
? Application.getEntityService(targetEntity.getEntityCode())
@ -225,6 +225,6 @@ public class FieldAggregation extends TriggerAction {
@Override
public void clean() {
TRIGGERS_CHAIN.remove();
TRIGGER_CHAIN.remove();
}
}

View file

@ -118,7 +118,7 @@ public class FieldWriteback extends FieldAggregation {
if (!tschainAdded) {
tschainAdded = true;
tschain.add(chainName);
TRIGGERS_CHAIN.set(tschain);
TRIGGER_CHAIN.set(tschain);
}
Record targetRecord = targetRecordData.clone();

View file

@ -7,6 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.trigger.impl;
import cn.devezhao.bizz.privileges.impl.BizzPermission;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
@ -25,7 +26,6 @@ import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.core.service.trigger.ActionType;
import com.rebuild.core.service.trigger.RobotTriggerObserver;
import com.rebuild.core.service.trigger.TriggerException;
import com.rebuild.core.support.i18n.Language;
import lombok.extern.slf4j.Slf4j;
@ -43,6 +43,8 @@ import java.util.*;
@Slf4j
public class GroupAggregation extends FieldAggregation {
private GroupAggregationRefresh groupAggregationRefresh;
public GroupAggregation(ActionContext context) {
super(context);
}
@ -52,6 +54,15 @@ public class GroupAggregation extends FieldAggregation {
return ActionType.GROUPAGGREGATION;
}
@Override
public void clean() {
super.clean();
if (groupAggregationRefresh != null) {
groupAggregationRefresh.refresh();
}
}
@Override
public void prepare(OperatingContext operatingContext) throws TriggerException {
if (sourceEntity != null) return; // 已经初始化
@ -78,13 +89,18 @@ public class GroupAggregation extends FieldAggregation {
groupFieldsMapping.put(sourceField, targetField);
}
if (groupFieldsMapping.isEmpty()) {
log.warn("No group-fields specified");
return;
}
// 1.源记录数据
String ql = String.format("select %s from %s where %s = ?",
StringUtils.join(groupFieldsMapping.keySet().iterator(), ","),
sourceEntity.getName(), sourceEntity.getPrimaryField().getName());
Record sourceRecord = Application.getQueryFactory().createQueryNoFilter(ql)
Record sourceRecord = Application.createQueryNoFilter(ql)
.setParameter(1, actionContext.getSourceRecord())
.record();
@ -92,16 +108,24 @@ public class GroupAggregation extends FieldAggregation {
List<String> qFields = new ArrayList<>();
List<String> qFieldsFollow = new ArrayList<>();
List<String[]> qFieldsRefresh = new ArrayList<>();
boolean allNull = true;
for (Map.Entry<String, String> e : groupFieldsMapping.entrySet()) {
String sourceField = e.getKey();
String targetField = e.getValue();
Object val = sourceRecord.getObjectValue(sourceField);
if (val != null) {
if (val == null) {
qFields.add(String.format("%s is null", targetField));
qFieldsFollow.add(String.format("%s is null", sourceField));
} else {
//noinspection ConstantConditions
EasyField sourceFieldEasy = EasyMetaFactory.valueOf(
Objects.requireNonNull(MetadataHelper.getLastJoinField(sourceEntity, sourceField)));
MetadataHelper.getLastJoinField(sourceEntity, sourceField));
//noinspection ConstantConditions
EasyField targetFieldEasy = EasyMetaFactory.valueOf(
Objects.requireNonNull(MetadataHelper.getLastJoinField(targetEntity, targetField)));
MetadataHelper.getLastJoinField(targetEntity, targetField));
// @see Dimension#getSqlName
@ -110,7 +134,8 @@ public class GroupAggregation extends FieldAggregation {
|| sourceFieldEasy.getDisplayType() == DisplayType.DATETIME) {
String formatKey = sourceFieldEasy.getDisplayType() == DisplayType.DATE
? EasyFieldConfigProps.DATE_FORMAT : EasyFieldConfigProps.DATETIME_FORMAT;
? EasyFieldConfigProps.DATE_FORMAT
: EasyFieldConfigProps.DATETIME_FORMAT;
int sourceFieldLength = StringUtils.defaultIfBlank(
sourceFieldEasy.getExtraAttr(formatKey), sourceFieldEasy.getDisplayType().getDefaultFormat())
.length();
@ -154,10 +179,7 @@ public class GroupAggregation extends FieldAggregation {
// 需要匹配等级的值
if (sourceFieldLevel != targetFieldLevel) {
ID parent = getItemWithLevel((ID) val, targetFieldLevel);
if (parent == null) {
log.error("Bad source value of classification (Maybe levels?) : {}", val);
return;
}
Assert.isTrue(parent != null, Language.L("分类字段等级不兼容"));
val = parent;
sourceRecord.setID(sourceField, (ID) val);
@ -171,21 +193,28 @@ public class GroupAggregation extends FieldAggregation {
qFields.add(String.format("%s = '%s'", targetField, val));
qFieldsFollow.add(String.format("%s = '%s'", sourceField, val));
}
allNull = false;
}
if (qFields.isEmpty()) {
log.warn("Value(s) of group-field not specified");
qFieldsRefresh.add(new String[] { targetField, sourceField, val == null ? null : val.toString() });
}
if (allNull) {
log.warn("All group-fields are null");
return;
}
this.followSourceWhere = StringUtils.join(qFieldsFollow.iterator(), " and ");
if (operatingContext.getAction() == BizzPermission.UPDATE) {
this.groupAggregationRefresh = new GroupAggregationRefresh(this, qFieldsRefresh);
}
ql = String.format("select %s from %s where ( %s )",
targetEntity.getPrimaryField().getName(), targetEntity.getName(),
StringUtils.join(qFields.iterator(), " and "));
Object[] targetRecord = Application.getQueryFactory().createQueryNoFilter(ql).unique();
Object[] targetRecord = Application.createQueryNoFilter(ql).unique();
if (targetRecord != null) {
targetRecordId = (ID) targetRecord[0];
return;
@ -217,14 +246,13 @@ public class GroupAggregation extends FieldAggregation {
PrivilegesGuardContextHolder.getSkipGuardOnce();
}
RobotTriggerObserver.forceTriggerSelf();
targetRecordId = newTargetRecord.getPrimary();
}
private ID getItemWithLevel(ID itemId, int specLevel) {
ID current = itemId;
for (int i = 0; i < 4; i++) {
Object[] o = Application.getQueryFactory().createQueryNoFilter(
Object[] o = Application.createQueryNoFilter(
"select level,parent from ClassificationData where itemId = ?")
.setParameter(1, current)
.unique();

View file

@ -0,0 +1,105 @@
/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or 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.core.service.trigger.impl;
import cn.devezhao.bizz.privileges.impl.BizzPermission;
import cn.devezhao.persist4j.Entity;
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.service.general.OperatingContext;
import com.rebuild.core.service.trigger.ActionContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* 分组聚合目标数据刷新
* 场景举例
* 1. 新建产品A + 仓库A分组组合A+A
* 2. 之后修改了仓库A > B组合A+B此时原组合A+A纪录不会更新
* 3. 这里需要强制更新相关原纪录
* 4. NOTE 如果组合值均为空则无法匹配任何目标记录此时需要全量刷新通过任一字段必填解决
*
* @author RB
* @since 2022/7/8
*/
@Slf4j
public class GroupAggregationRefresh {
final private GroupAggregation parent;
final private List<String[]> fieldsRefresh;
// fieldsRefresh = [TargetField, SourceField, Value]
protected GroupAggregationRefresh(GroupAggregation parent, List<String[]> fieldsRefresh) {
this.parent = parent;
this.fieldsRefresh = fieldsRefresh;
}
public void refresh() {
List<String> targetFields = new ArrayList<>();
List<String> targetWhere = new ArrayList<>();
for (String[] s : fieldsRefresh) {
targetFields.add(s[0]);
if (s[2] != null) {
targetWhere.add(String.format("%s = '%s'", s[0], s[2]));
}
}
Entity entity = this.parent.targetEntity;
String sql = String.format("select %s,%s from %s where ( %s )",
StringUtils.join(targetFields, ","),
entity.getPrimaryField().getName(),
entity.getName(),
StringUtils.join(targetWhere, " or "));
Object[][] targetArray = Application.createQueryNoFilter(sql).array();
log.info("Effect {} target(s) ...", targetArray.length);
ID triggerUser = UserContextHolder.getUser();
ActionContext parentAc = parent.getActionContext();
for (Object[] o : targetArray) {
ID targetRecordId = (ID) o[o.length - 1];
if (targetRecordId.equals(parent.targetRecordId)) continue;
ActionContext actionContext = new ActionContext(null,
parentAc.getSourceEntity(), parentAc.getActionContent(), parentAc.getConfigId());
GroupAggregation ga = new GroupAggregation(actionContext);
ga.sourceEntity = parent.sourceEntity;
ga.targetEntity = parent.targetEntity;
ga.targetRecordId = targetRecordId;
List<String> qFieldsFollow = 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]));
}
}
ga.followSourceWhere = StringUtils.join(qFieldsFollow, " and ");
Record record = EntityHelper.forUpdate((ID) o[0], triggerUser, false);
OperatingContext oCtx = OperatingContext.create(triggerUser, BizzPermission.NONE, record, record);
try {
ga.execute(oCtx);
} catch (Exception ex) {
log.error("Error on refresh trigger ({}) record : {}", parentAc.getConfigId(), o[0], ex);
} finally {
ga.clean();
}
}
}
}

View file

@ -67,7 +67,7 @@ public class HeavyStopWatcher {
* @return
*/
public static StopWatch clean() {
return clean(0);
return clean(1000);
}
/**

View file

@ -23,6 +23,8 @@ import org.apache.commons.lang.StringUtils;
*/
public class KVStorage {
private static final Object SETNULL = new Object();
/**
* 存储
*
@ -43,6 +45,13 @@ public class KVStorage {
setValue("custom." + key, value);
}
/**
* @param key
*/
public static void removeCustomValue(String key) {
setCustomValue(key, SETNULL);
}
/**
* @param key
* @param value
@ -53,6 +62,15 @@ public class KVStorage {
.setParameter(1, key)
.unique();
// 删除
if (value == SETNULL) {
if (exists != null) {
Application.getCommonsService().delete((ID) exists[0]);
Application.getCommonsCache().evict(key);
}
return;
}
Record record;
if (exists == null) {
record = EntityHelper.forNew(EntityHelper.SystemConfig, UserService.SYSTEM_USER, false);

View file

@ -84,7 +84,7 @@ public class QiniuCloud {
this.auth = Auth.create(account[0], account[1]);
this.bucketName = account[2];
} else {
log.warn("No QiniuCloud configuration! Using local storage.");
log.info("No QiniuCloud configuration! Using local storage.");
}
}

View file

@ -23,6 +23,7 @@ import com.rebuild.core.configuration.NavBuilder;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.rbstore.BusinessModelImporter;
import com.rebuild.core.rbstore.ClassificationImporter;
import com.rebuild.core.support.License;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.distributed.UseRedis;
import com.rebuild.core.support.task.TaskExecutors;
@ -38,6 +39,10 @@ import redis.clients.jedis.JedisPool;
import javax.sql.DataSource;
import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.sql.*;
import java.time.DateTimeException;
@ -62,6 +67,8 @@ public class Installer implements InstallState {
private JSONObject installProps;
private String EXISTS_SN;
private Installer() { }
/**
@ -166,6 +173,12 @@ public class Installer implements InstallState {
if (!((BaseCacheTemplate<?>) o).reinjectJedisPool(pool)) break;
}
// L
if (EXISTS_SN != null) {
System.setProperty("SN", EXISTS_SN);
License.siteApiNoCache("api/authority/query");
}
Application.init();
}
@ -346,7 +359,7 @@ public class Installer implements InstallState {
return;
}
ub.setWhere("LOGIN_NAME = 'admin'");
ub.setWhere(String.format("USER_ID = '%s'", UserService.ADMIN_USER));
executeSql(ub.toSql());
}
@ -411,23 +424,29 @@ public class Installer implements InstallState {
*/
public boolean isRbDatabase() {
String rbSql = SqlBuilder.buildSelect("system_config")
.addColumn("VALUE")
.setWhere("ITEM = 'DBVer'")
.addColumns("ITEM", "VALUE")
.setWhere("ITEM = 'DBVer' or ITEM = 'SN'")
.toSql();
EXISTS_SN = null;
try (Connection conn = getConnection(null)) {
try (Statement stmt = conn.createStatement()) {
try (ResultSet rs = stmt.executeQuery(rbSql)) {
if (rs.next()) {
String dbVer = rs.getString(1);
log.info("Check exists REBUILD database version : " + dbVer);
return true;
while (rs.next()) {
String name = rs.getString(1);
String value = rs.getString(2);
if ("DBVer".equalsIgnoreCase(name)) {
log.info("Check exists REBUILD database version : {}", value);
} else if ("SN".equalsIgnoreCase(name)) {
EXISTS_SN = value;
}
}
return true;
}
}
} catch (SQLException ex) {
log.info("Check REBUILD database error : " + ex.getLocalizedMessage());
log.error("Check REBUILD database error : " + ex.getLocalizedMessage());
}
return false;
}

View file

@ -53,6 +53,9 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
private static final ThreadLocal<RequestEntry> REQUEST_ENTRY = new NamedThreadLocal<>("RequestEntry");
private static final int CODE_STARTING = 600;
private static final int CODE_DENIEDMSG = 601;
private static final int CODE_MAINTAIN = 602;
private static final int CODE_UNSAFE_USE = 603;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
@ -175,7 +178,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// Noting
// Notings
}
@Override
@ -295,13 +298,13 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
Object allowIp = CommonsUtils.invokeMethod(
"com.rebuild.rbv.commons.SafeUses#checkIp", ipAddr);
if (!(Boolean) allowIp) {
throw new DefinedException(DefinedException.CODE_UNSAFE_USE, Language.L("你的 IP 地址不在允许范围内"));
throw new DefinedException(CODE_UNSAFE_USE, Language.L("你的 IP 地址不在允许范围内"));
}
Object allowTime = CommonsUtils.invokeMethod(
"com.rebuild.rbv.commons.SafeUses#checkTime", CalendarUtils.now());
if (!(Boolean) allowTime) {
throw new DefinedException(DefinedException.CODE_UNSAFE_USE, Language.L("当前时间不允许使用"));
throw new DefinedException(CODE_UNSAFE_USE, Language.L("当前时间不允许使用"));
}
}

View file

@ -73,6 +73,8 @@ public class MetaFieldController extends BaseController {
map.put("fieldName", easyMeta.getName());
map.put("fieldLabel", easyMeta.getLabel());
map.put("comments", easyMeta.getComments());
map.put("displayType", Language.L(easyMeta.getDisplayType()));
map.put("displayTypeName", easyMeta.getDisplayType().name());
map.put("nullable", field.isNullable());
map.put("builtin", easyMeta.isBuiltin());
map.put("creatable", field.isCreatable());

View file

@ -22,6 +22,7 @@ import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
import com.rebuild.web.user.signup.LoginController;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@ -120,12 +121,13 @@ public class InstallController extends BaseController implements InstallState {
if (info.length() > 80) {
info = info.substring(0, 80) + "...";
}
pool.destroy();
return RespBody.ok(Language.L("连接成功 : %s", info));
} catch (Exception ex) {
return RespBody.errorl("连接错误 : %s", ThrowableUtils.getRootCause(ex).getLocalizedMessage());
} finally {
IOUtils.closeQuietly(pool);
}
}

View file

@ -15,7 +15,6 @@ import com.rebuild.utils.AppUtils;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.web.BaseController;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@ -47,12 +46,12 @@ public class CommonPageView extends BaseController {
@GetMapping("/*.txt")
public void txtSuffix(HttpServletRequest request, HttpServletResponse response) throws IOException {
String url = request.getRequestURI();
url = url.substring(url.lastIndexOf("/") + 1);
String name = url.substring(url.lastIndexOf("/") + 1);
String content = null;
// WXWORK
if (url.startsWith("WW_verify_")) {
if (name.startsWith("WW_verify_")) {
String fileKey = RebuildConfiguration.get(ConfigurationItem.WxworkAuthFile);
File file = RebuildConfiguration.getFileOfData(fileKey);
if (file.exists() && file.isFile()) {
@ -61,7 +60,12 @@ public class CommonPageView extends BaseController {
}
// OTHERS
else {
content = CommonsUtils.getStringOfRes("web/" + url);
File file = RebuildConfiguration.getFileOfData(name);
if (file.exists() && file.isFile()) {
content = FileUtils.readFileToString(file);
} else {
content = CommonsUtils.getStringOfRes("web/" + name);
}
}
if (content == null) {

View file

@ -41,6 +41,7 @@ import com.rebuild.web.KnownExceptionConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.transaction.UnexpectedRollbackException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@ -187,6 +188,9 @@ public class GeneralOperatingController extends BaseController {
} catch (AccessDeniedException | DataSpecificationException known) {
log.warn(">>>>> {}", known.getLocalizedMessage());
return RespBody.error(known.getLocalizedMessage());
} catch (UnexpectedRollbackException rolledback) {
log.error("ROLLEDBACK", rolledback);
return RespBody.error("ROLLEDBACK OCCURED");
}
return JSONUtils.toJSONObject(

View file

@ -11,6 +11,7 @@ import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.dialect.FieldType;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
@ -167,6 +168,21 @@ public class ApprovalAdminController extends BaseController {
fields.add(new String[] { field.getName(), EasyMetaFactory.getLabel(field)} );
}
}
// 引用实体字段
for (Field field : refFields) {
if (field.getType() != FieldType.REFERENCE) continue;
if (MetadataHelper.isCommonsField(field)) continue;
String parentName = field.getName() + ".";
String parentLabel = EasyMetaFactory.getLabel(field) + ".";
Field[] refFields2 = MetadataSorter.sortFields(field.getReferenceEntity(), DisplayType.REFERENCE, DisplayType.N2NREFERENCE);
for (Field field2 : refFields2) {
if (isRefUserOrDeptField(field2, filterNames, false)) {
fields.add(new String[] { parentName + field2.getName(), parentLabel + EasyMetaFactory.getLabel(field2)} );
}
}
}
return JSONUtils.toJSONObjectArray(
new String[] { "id", "text" }, fields.toArray(new String[0][]));
@ -212,7 +228,7 @@ public class ApprovalAdminController extends BaseController {
shows.add(new String[] { idOrField, fieldText });
}
} else if (entity.containsField(idOrField)) {
} else if (MetadataHelper.getLastJoinField(entity, idOrField) != null) {
String fieldLabel = EasyMetaFactory.getLabel(entity, idOrField);
shows.add(new String[] { idOrField, fieldLabel });
}

View file

@ -1087,7 +1087,7 @@
"未选中任何记录":"未选中任何记录",
"未配置":"未配置",
"本人":"本人",
"本公式仅做前端计算,如公式中所用字段未布局/未显示,则无法进行计算。你也可以通过 [触发器 (自动更新)](/admin/robot/triggers) 实现更强大的计算规则":"本公式仅做前端计算,如公式中所用字段未布局/未显示,则无法进行计算。你也可以通过 [触发器 (自动更新)](/admin/robot/triggers) 实现更强大的计算规则",
"本公式仅做前端计算,如公式中所用字段未布局/未显示,则无法进行计算。你也可以通过 [触发器 (字段更新)](/admin/robot/triggers) 实现更强大的计算规则":"本公式仅做前端计算,如公式中所用字段未布局/未显示,则无法进行计算。你也可以通过 [触发器 (字段更新)](/admin/robot/triggers) 实现更强大的计算规则",
"本周":"本周",
"本地存储":"本地存储",
"本季度":"本季度",
@ -1604,7 +1604,7 @@
"通用":"通用",
"通知":"通知",
"通知类型":"通知类型",
"通过 [触发器 (自动更新)](/admin/robot/triggers) 可以实现更加强大的回填规则":"通过 [触发器 (自动更新)](/admin/robot/triggers) 可以实现更加强大的回填规则",
"通过 [触发器 (字段更新)](/admin/robot/triggers) 可以实现更加强大的回填规则":"通过 [触发器 (字段更新)](/admin/robot/triggers) 可以实现更加强大的回填规则",
"通过明细实体可以更好的组织业务关系。例如订单明细通常依附于订单,而非独立存在":"通过明细实体可以更好的组织业务关系。例如订单明细通常依附于订单,而非独立存在",
"遇到重复记录时":"遇到重复记录时",
"邮件":"邮件",
@ -1821,7 +1821,7 @@
"无可用选项":"无可用选项",
"你的密码将在 **%d** 天后过期":"你的密码将在 **%d** 天后过期",
"图表加载失败":"图表加载失败",
"校验失败时的提示内容":"校验失败时的提示内容",
"校验未通过时的提示内容":"校验未通过时的提示内容",
"确认退出登录?":"确认退出登录?",
"输入新邮箱":"输入新邮箱",
"确认删除此筛选?":"确认删除此筛选?",
@ -1831,13 +1831,13 @@
"字段更新":"字段更新",
"正在加载":"正在加载",
"密码已被系统强制修改":"密码已被系统强制修改",
"不符合数据校验条件":"不符合数据校验条件",
"数据校验未通过":"数据校验未通过",
"发布成功":"发布成功",
"视图":"视图",
"原邮箱":"原邮箱",
"版本":"版本",
"点击复制分享链接":"点击复制分享链接",
"符合校验条件的数据/记录在操作时会失败":"符合校验条件的数据/记录在操作时会失败",
"符合校验条件的数据/记录在操作时会提示失败":"符合校验条件的数据/记录在操作时会提示失败",
"输入邮箱":"输入邮箱",
"输入验证码":"输入验证码",
"添加任务":"添加任务",
@ -2209,5 +2209,7 @@
"解锁":"解锁",
"列表模板":"列表模板",
"未启用":"未启用",
"请至少添加 1 个更新规则":"请至少添加 1 个更新规则"
"请至少添加 1 个更新规则":"请至少添加 1 个更新规则",
"确认移除此明细?":"确认移除此明细?",
"高度 (行数)":"高度 (行数)"
}

View file

@ -102,7 +102,7 @@
</div>
</div>
</div>
<p class="protips" th:utext="${bundle.L('通过 [触发器 (自动更新)](/admin/robot/triggers) 可以实现更加强大的回填规则')}"></p>
<p class="protips" th:utext="${bundle.L('通过 [触发器 (字段更新)](/admin/robot/triggers) 可以实现更加强大的回填规则')}"></p>
</div>
</div>
</div>

View file

@ -241,6 +241,7 @@
<select th:case="'BOOL'" class="form-control form-control-sm J_defaultValue" th:data-o="${fieldDefaultValue}">
<option value="T">[[${bundle.L('是')}]]</option>
<option value="F" selected>[[${bundle.L('否')}]]</option>
<option value="N">[[${bundle.L('无')}]]</option>
</select>
<div th:case="*" class="input-group">
<input
@ -264,7 +265,7 @@
<div class="form-control-plaintext formula" id="calcFormula2" th:_title="${bundle.L('无')}">[[${calcFormula ?: calcFormula}]]</div>
<p
class="form-text"
th:utext="${bundle.L('本公式仅做前端计算,如公式中所用字段未布局/未显示,则无法进行计算。你也可以通过 [触发器 (自动更新)](/admin/robot/triggers) 实现更强大的计算规则')}"
th:utext="${bundle.L('本公式仅做前端计算,如公式中所用字段未布局/未显示,则无法进行计算。你也可以通过 [触发器 (字段更新)](/admin/robot/triggers) 实现更强大的计算规则')}"
></p>
</div>
</div>
@ -345,7 +346,14 @@
</span>
</label>
<div>
<input type="text" class="form-control form-control-sm" id="advPattern" th:placeholder="${bundle.L('格式验证 (支持 Java 正则表达式)')}" data-toggle="dropdown" autocomplete="off" />
<input
type="text"
class="form-control form-control-sm"
id="advPattern"
th:placeholder="${bundle.L('格式验证 (支持 Java 正则表达式)')}"
data-toggle="dropdown"
autocomplete="off"
/>
<div class="dropdown-menu common-patt">
<h5>[[${bundle.L('常用')}]]</h5>
<a class="badge" data-patt="^([0-9A-Z]{15}|[0-9A-Z]{17}|[0-9A-Z]{18}|[0-9A-Z]{20})$">[[${bundle.L('税号')}]]</a>

View file

@ -45,7 +45,7 @@
<table class="table table-hover table-striped table-fixed">
<thead>
<tr>
<th>[[${bundle.L('名称')}]]</th>
<th class="use-sort pointer" data-sort-index="3">[[${bundle.L('名称')}]]</th>
<th>[[${bundle.L('源实体')}]]</th>
<th>[[${bundle.L('触发类型')}]]</th>
<th>[[${bundle.L('触发动作')}]]</th>

View file

@ -194,6 +194,12 @@ See LICENSE and COMMERCIAL in the project root for license information.
margin-top: 5px;
}
.chart.ApprovalList .table,
.chart.FeedsSchedule .table,
.chart.ProjectTasks .table {
border-bottom: 1px solid #dee2e6;
}
.chart.ApprovalList .table th,
.chart.FeedsSchedule .table th,
.chart.ProjectTasks .table th {

View file

@ -98,12 +98,13 @@ See LICENSE and COMMERCIAL in the project root for license information.
position: absolute;
display: inline-block;
height: 32px;
width: 16px;
font-size: 1.2rem;
width: 1.4rem;
right: 40px;
top: 2px;
text-align: center;
padding-top: 6px;
color: #737373 !important;
font-size: 1.308rem;
}
.J_defaultValue-clear:hover {
@ -232,3 +233,8 @@ a#entityIcon:hover {
.common-patt .badge:hover {
color: #4285f4 !important;
}
.input-group-append > .btn-secondary + .btn-secondary {
border-top-right-radius: 2px !important;
border-bottom-right-radius: 2px !important;
}

View file

@ -824,7 +824,7 @@ textarea.row2x {
}
textarea.row3x {
height: 70px !important;
height: 72px !important;
resize: none;
}
@ -946,8 +946,8 @@ select.form-control:not([disabled]) {
}
.rbform .type-BOOL .mt-1 + .form-text {
margin-top: -5px;
margin-bottom: 0.5rem;
margin-top: -7px;
margin-bottom: 0;
}
.rbform .select2-container--default .select2-selection--multiple .select2-selection__clear,
@ -4552,6 +4552,23 @@ pre.unstyle {
right: 0;
}
.protable .table thead th .tipping {
position: absolute;
font-size: 1.21rem;
margin-left: 6px;
margin-top: 1px;
cursor: help;
color: #777;
}
.protable .table thead th .tipping:hover {
opacity: 0.8;
}
.protable .table thead th.required .tipping {
margin-left: 16px;
}
.protable .table tbody .col-index[title] {
background-color: #ea4335;
color: #fff;
@ -4774,3 +4791,7 @@ pre.unstyle {
.rb-left-sidebar.v2 .sidebar-elements li.active > a {
background-color: rgba(0, 0, 0, 0.08);
}
.nav-tabs > li.nav-item a.nav-link .icon {
margin-left: 0;
}

View file

@ -72,6 +72,27 @@ class ConfigList extends React.Component {
// 搜索
const $btn = $('.input-search .btn').click(() => this.loadData())
$('.input-search .form-control').keydown((e) => (e.which === 13 ? $btn.trigger('click') : true))
$('.data-list .table th.use-sort').on('click', () => this._sortByName())
}
// 简单排序
_sortByName(data, callback) {
const $sort = $('.data-list .table th.use-sort')
const index = ~~($sort.data('sort-index') || 1)
if (!data) this.__asc = !this.__asc
const s = data || this.state.data
s.sort((a, b) => {
if (this.__asc) {
return (a[index] || '').localeCompare(b[index] || '')
} else {
return (b[index] || '').localeCompare(a[index] || '')
}
})
this.setState({ data: s }, callback)
}
// 加载数据
@ -86,7 +107,7 @@ class ConfigList extends React.Component {
if (res.error_code === 0) {
const data = res.data || []
if (this.renderEntityTree(data) !== false) {
this.setState({ data: data }, () => {
this._sortByName(res.data || [], () => {
$('.rb-loading-active').removeClass('rb-loading-active')
$('.dataTables_info').text($L(' %d ', this.state.data.length))

View file

@ -62,8 +62,15 @@ $(document).ready(() => {
render_preview()
}
let _AdvFilter
$('.J_filter').on('click', () => {
renderRbcomp(<AdvFilter title={$L('数据过滤条件')} entity={wpc.sourceEntity} filter={dataFilter} inModal={true} confirm={saveFilter} canNoFilters={true} />)
if (_AdvFilter) {
_AdvFilter.show()
} else {
renderRbcomp(<AdvFilter title={$L('数据过滤条件')} entity={wpc.sourceEntity} filter={dataFilter} onConfirm={saveFilter} inModal canNoFilters />, null, function () {
_AdvFilter = this
})
}
})
const $cts = $('.chart-type > a').on('click', function () {

View file

@ -5,6 +5,9 @@ rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
// in `chart-design`
const __PREVIEW = !!(window.__PageConfig || {}).chartConfig
// 图表基类
class BaseChart extends React.Component {
constructor(props) {
@ -141,7 +144,7 @@ class ChartIndex extends BaseChart {
<div className="chart index" ref={(c) => (this._chart = c)}>
<div className="data-item must-center text-truncate w-auto">
<p>{data.index.label || this.label}</p>
<a href={`${rb.baseUrl}/dashboard/view-chart-source?id=${this.props.id}`}>
<a href={__PREVIEW ? null : `${rb.baseUrl}/dashboard/view-chart-source?id=${this.props.id}`}>
<strong>{data.index.data}</strong>
</a>
</div>

View file

@ -237,9 +237,9 @@ const add_widget = function (item) {
const gsi = `<div class="grid-stack-item"><div id="${chid}" class="grid-stack-item-content"></div></div>`
// Use gridstar
if (item.size_x || item.size_y) {
gridstack.addWidget(gsi, (item.col || 1) - 1, (item.row || 1) - 1, item.size_x || 2, item.size_y || 2, 2, 12, 2, 12)
gridstack.addWidget(gsi, (item.col || 1) - 1, (item.row || 1) - 1, item.size_x || 2, item.size_y || 2, true, 2, 12, 2, 24)
} else {
gridstack.addWidget(gsi, item.x, item.y, item.w, item.h, item.x === undefined, 2, 12, 2, 12)
gridstack.addWidget(gsi, item.x, item.y, item.w, item.h, item.x === undefined, 2, 12, 2, 24)
}
item.editable = dash_editable

View file

@ -15,6 +15,8 @@ const SHOW_ADVDESENSITIZED = ['TEXT', 'PHONE', 'EMAIL', 'NUMBER', 'DECIMAL']
const SHOW_ADVPATTERN = ['TEXT', 'PHONE', 'EMAIL']
const SHOW_SCANCODE = ['TEXT']
const CURRENT_BIZZ = '{CURRENT}'
$(document).ready(function () {
const dt = wpc.fieldType
const extConfig = wpc.extConfig
@ -288,15 +290,13 @@ const _handleSeries = function () {
}
const _handleDate = function (dt) {
$('.J_defaultValue')
.datetimepicker({
$('.J_defaultValue').datetimepicker({
format: dt === 'DATE' ? 'yyyy-mm-dd' : 'yyyy-mm-dd hh:ii:ss',
minView: dt === 'DATE' ? 2 : 0,
clearBtn: true,
})
.attr('readonly', true)
$(`<button class="btn btn-secondary mw-auto" type="button" title="${$L('日期公式')}"><i class="icon zmdi zmdi-settings-square"></i></button>`)
$(`<button class="btn btn-secondary" type="button" title="${$L('日期公式')}"><i class="icon zmdi zmdi-settings-square"></i></button>`)
.appendTo('.J_defaultValue-append')
.on('click', () => renderRbcomp(<FormulaDate type={dt} onConfirm={(expr) => $('.J_defaultValue').val(expr)} />))
}
@ -361,7 +361,7 @@ const _handleClassification = function (useClassification) {
}
}
const $append = $(`<button class="btn btn-secondary mw-auto" type="button" title="${$L('选择默认值')}"><i class="icon zmdi zmdi-search"></i></button>`).appendTo('.J_defaultValue-append')
const $append = $(`<button class="btn btn-secondary" type="button" title="${$L('选择默认值')}"><i class="icon zmdi zmdi-search"></i></button>`).appendTo('.J_defaultValue-append')
$.get(`/admin/metadata/classification/info?id=${useClassification}`, (res) => {
$('#useClassification a')
@ -432,7 +432,7 @@ const _handleReference = function (isN2N) {
}
}
const $append = $(`<button class="btn btn-secondary mw-auto" type="button" title="${$L('选择默认值')}"><i class="icon zmdi zmdi-search"></i></button>`).appendTo('.J_defaultValue-append')
const $append = $(`<button class="btn btn-secondary" type="button" title="${$L('选择默认值')}"><i class="icon zmdi zmdi-search"></i></button>`).appendTo('.J_defaultValue-append')
$dv.attr('readonly', true)
$append.on('click', () => _showSearcher())
@ -456,13 +456,26 @@ const _handleReference = function (isN2N) {
_ReferenceSearcher.hide()
}
// Bizz
if (['User', 'Department', 'Team'].includes(referenceEntity)) {
const $current = $(`<button class="btn btn-secondary" type="button" title="${$L('当前用户')}"><i class="icon zmdi zmdi-account-o"></i></button>`).appendTo('.J_defaultValue-append')
$current.on('click', () => {
$dv.attr('data-value-id', CURRENT_BIZZ).val(CURRENT_BIZZ)
$dvClear.removeClass('hide')
})
$dvClear.css({ right: 75 })
}
_loadRefsLabel($dv, $dvClear)
}
const _loadRefsLabel = function ($dv, $dvClear) {
const dvid = $dv.val()
if (dvid) {
$.get(`/commons/search/read-labels?ids=${dvid}&ignoreMiss=true`, (res) => {
if (dvid === CURRENT_BIZZ) {
$dvClear && $dvClear.removeClass('hide')
} else if (dvid) {
$.get(`/commons/search/read-labels?ids=${encodeURIComponent(dvid)}&ignoreMiss=true`, (res) => {
if (res.data) {
const ids = []
const labels = []

View file

@ -52,7 +52,7 @@ $(document).ready(function () {
$item.remove()
})
} else {
render_item({ ...field, isFull: this.isFull || false, colspan: this.colspan, tip: this.tip || null })
render_item({ ...field, isFull: this.isFull || false, colspan: this.colspan, tip: this.tip || null, height: this.height || null })
AdvControl.set(this)
}
})
@ -128,7 +128,7 @@ $(document).ready(function () {
const item = { field: $this.data('field') }
if (item.field === DIVIDER_LINE) {
item.colspan = 4
item.label = $this.find('span').text()
item.label = $this.find('span').text() || ''
} else {
item.colspan = 2 // default
if ($this.parent().hasClass('w-100')) item.colspan = 4
@ -140,6 +140,8 @@ $(document).ready(function () {
if (tip) item.tip = tip
item.__newLabel = $this.find('span').text()
if (item.__newLabel === $this.data('label')) delete item.__newLabel
const height = $this.attr('data-height')
if (height) item.height = height
AdvControl.append(item)
}
@ -198,6 +200,8 @@ const render_item = function (data) {
else if (data.nullable === false) $handle.addClass('not-nullable')
// 填写提示
if (data.tip) $('<i class="J_tip zmdi zmdi-info-outline"></i>').appendTo($handle.find('span')).attr('title', data.tip)
// 高度
if (data.height) $handle.attr('data-height', data.height)
const $action = $('<div class="dd-action"></div>').appendTo($handle)
if (data.displayType) {
@ -219,7 +223,7 @@ const render_item = function (data) {
$(`<a title="${$L('修改')}"><i class="zmdi zmdi-edit"></i></a>`)
.appendTo($action)
.click(function () {
const call = function (nv) {
const _onConfirm = function (nv) {
// 字段名
if (nv.fieldLabel) $item.find('.dd-handle>span').text(nv.fieldLabel)
else $item.find('.dd-handle>span').text($item.find('.dd-handle').data('label'))
@ -232,15 +236,20 @@ const render_item = function (data) {
if ($tip.length === 0) $tip = $('<i class="J_tip zmdi zmdi-info-outline"></i>').appendTo($item.find('.dd-handle span'))
$tip.attr('title', nv.fieldTips)
}
// NTEXT 高度
if (data.displayTypeName === 'NTEXT') $item.find('.dd-handle').attr('data-height', nv.fieldHeight || '')
}
const ov = {
fieldTips: $item.find('.dd-handle>span>i').attr('title'),
fieldLabel: $item.find('.dd-handle>span').text(),
fieldLabelOld: $item.find('.dd-handle').data('label'),
fieldHeight: $item.find('.dd-handle').attr('data-height') || null,
}
if (ov.fieldLabelOld === ov.fieldLabel) ov.fieldLabel = null
renderRbcomp(<DlgEditField call={call} {...ov} />)
renderRbcomp(<DlgEditField onConfirm={_onConfirm} {...ov} displayType={data.displayTypeName} />)
})
$(`<a title="${$L('移除')}"><i class="zmdi zmdi-close"></i></a>`)
@ -256,11 +265,12 @@ const render_item = function (data) {
$(`<a title="${$L('修改')}"><i class="zmdi zmdi-edit"></i></a>`)
.appendTo($action)
.click(function () {
const call = function (nv) {
const _onConfirm = function (nv) {
$item.find('.dd-handle span').text(nv.dividerName || '')
}
const ov = $item.find('.dd-handle span').text()
renderRbcomp(<DlgEditDivider call={call} dividerName={ov || ''} />)
renderRbcomp(<DlgEditDivider onConfirm={_onConfirm} dividerName={ov || ''} />)
})
$(`<a title="${$L('移除')}"><i class="zmdi zmdi-close"></i></a>`)
@ -316,6 +326,12 @@ class DlgEditField extends RbAlert {
maxLength="200"
/>
</div>
{this.props.displayType === 'NTEXT' && (
<div className="form-group">
<label>{$L('高度 (行数)')}</label>
<input type="number" className="form-control form-control-sm" name="fieldHeight" value={this.state.fieldHeight || ''} onChange={this.handleChange} placeholder={$L('默认')} />
</div>
)}
<div className="form-group">
<label>
{$L('字段名称')} <span>({$L('部分内置字段不能修改')})</span>
@ -347,7 +363,7 @@ class DlgEditField extends RbAlert {
}
confirm = () => {
typeof this.props.call === 'function' && this.props.call(this.state || {})
typeof this.props.onConfirm === 'function' && this.props.onConfirm(this.state || {})
this.hide()
}
}
@ -397,6 +413,7 @@ const AdvControl = {
append: function (item) {
this.$controls.find(`tr[data-field="${item.field}"] input`).each(function () {
const $this = $(this)
if ($this.prop('disabled')) return
item[$this.attr('name')] = $this.prop('checked')
})
},
@ -404,6 +421,7 @@ const AdvControl = {
set: function (item) {
this.$controls.find(`tr[data-field="${item.field}"] input`).each(function () {
const $this = $(this)
if ($this.prop('disabled')) return
const v = item[$this.attr('name')]
if (v === true || v === false) $this.attr('checked', v)
})

View file

@ -27,7 +27,7 @@ $(document).ready(function () {
})
}
$.get(`/admin/field/picklist-gets?isAll=true&${query}`, function (res) {
$.get(`/admin/field/picklist-gets?isAll=true&${query}`, (res) => {
$(res.data).each(function () {
if (this.hide === true) {
render_unset([this.id, this.text])
@ -39,14 +39,20 @@ $(document).ready(function () {
})
$('.J_confirm').on('click', () => {
if ($('.J_config>li').length > maxOptions) {
RbHighbar.create($L('最多支持 %d 个选项', maxOptions))
return false
}
const text = $val('.J_text')
if (!text) {
RbHighbar.create($L('请输入选项值'))
return false
}
const id = $('.J_text').attr('attr-id')
const color = $('.colors >a>i').parent().data('color') || ''
const id = $('.J_text').attr('data-key')
let exists = null
$('.J_config .dd3-content, .unset-list .dd-handle>span').each(function () {
@ -57,7 +63,16 @@ $(document).ready(function () {
return false
}
// New
if (exists) {
if (exists !== id) {
RbHighbar.create($L('选项值重复'))
return false
}
}
$('.J_text').val('').attr('attr-id', '')
$('.J_confirm').text($L('添加'))
if (!id) {
if ($('.J_config>li').length >= maxOptions) {
RbHighbar.create($L('最多支持 %d 个选项', maxOptions))
@ -152,7 +167,7 @@ render_item_after = function (item, data) {
item.find('.dd3-action').prepend($edit)
$edit.on('click', () => {
$('.J_confirm').text($L('修改'))
$('.J_text').val(data[1]).attr('data-key', data[0]).focus()
$('.J_text').val($edit.parent().prev().text()).attr('attr-id', data[0]).focus()
data[3] = item.attr('data-color')
if (data[3]) $(`.colors>a[data-color="${data[3]}"]`).trigger('click')

View file

@ -29,6 +29,7 @@ class AdvFilter extends React.Component {
this.state = { items: [], ...props, ...extras }
this._itemsRef = []
this._htmlid = `useEquation-${$random()}`
}
render() {
@ -79,15 +80,15 @@ class AdvFilter extends React.Component {
<div className="mb-1">
<div className="item mt-1">
<label className="custom-control custom-control-sm custom-radio custom-control-inline mb-2">
<input className="custom-control-input" type="radio" name="useEquation" value="OR" checked={this.state.useEquation === 'OR'} onChange={this.handleChange} />
<input className="custom-control-input" type="radio" name={this._htmlid} data-id="useEquation" value="OR" checked={this.state.useEquation === 'OR'} onChange={this.handleChange} />
<span className="custom-control-label pl-1">{$L('或关系')}</span>
</label>
<label className="custom-control custom-control-sm custom-radio custom-control-inline mb-2">
<input className="custom-control-input" type="radio" name="useEquation" value="AND" checked={this.state.useEquation === 'AND'} onChange={this.handleChange} />
<input className="custom-control-input" type="radio" name={this._htmlid} data-id="useEquation" value="AND" checked={this.state.useEquation === 'AND'} onChange={this.handleChange} />
<span className="custom-control-label pl-1">{$L('且关系')}</span>
</label>
<label className="custom-control custom-control-sm custom-radio custom-control-inline mb-2">
<input className="custom-control-input" type="radio" name="useEquation" value="9999" checked={this.state.useEquation === '9999'} onChange={this.handleChange} />
<input className="custom-control-input" type="radio" name={this._htmlid} data-id="useEquation" value="9999" checked={this.state.useEquation === '9999'} onChange={this.handleChange} />
<span className="custom-control-label pl-1">
{$L('高级表达式')}
<a href="https://getrebuild.com/docs/manual/basic#%E9%AB%98%E7%BA%A7%E8%A1%A8%E8%BE%BE%E5%BC%8F" target="_blank">
@ -187,7 +188,12 @@ class AdvFilter extends React.Component {
handleChange = (e) => {
const name = e.target.dataset.id || e.target.name
this.setState({ [name]: e.target.value })
const value = e.target.value
this.setState({ [name]: value }, () => {
if (name === 'useEquation' && value === '9999') {
if (this.state.equation === 'AND') this.setState({ equation: null })
}
})
}
onRef = (c) => this._itemsRef.push(c)

View file

@ -438,7 +438,7 @@ var $stopEvent = function (e, preventDefault) {
}
/**
* 是否为 true 'true'
* 是否为 true
*/
var $isTrue = function (a) {
return a === true || a === 'true' || a === 'T'

View file

@ -23,13 +23,16 @@ const AdvFilters = {
this.__entity = entity
this.__el.find('.J_advfilter').on('click', () => {
this.showAdvFilter(null, this.current)
// this.showAdvFilter(null, this.current) // 2.9.4 取消 useCopyId可能引起误解
this.showAdvFilter()
this.current = null
})
const $all = $('.adv-search .dropdown-item:eq(0)')
$all.on('click', () => this._effectFilter($all, 'aside'))
this.loadFilters()
this.__savedCached = []
},
loadFilters() {
@ -184,10 +187,16 @@ const AdvFilters = {
}
} else {
this.current = id
if (this.__savedCached[id]) {
const res = this.__savedCached[id]
renderRbcomp(<AdvFilter {...props} title={$L('修改高级查询')} filter={res.filter} filterName={res.name} shareTo={res.shareTo} />)
} else {
this._getFilter(id, (res) => {
this.__savedCached[id] = res
renderRbcomp(<AdvFilter {...props} title={$L('修改高级查询')} filter={res.filter} filterName={res.name} shareTo={res.shareTo} />)
})
}
}
},
saveFilter(filter, name, shareTo) {
@ -201,6 +210,7 @@ const AdvFilters = {
if (res.error_code === 0) {
$storage.set(_RbList().__defaultFilterKey, res.data.id)
that.loadFilters()
if (that.current) that.__savedCached[that.current] = null
} else {
RbHighbar.error(res.error_msg)
}

View file

@ -673,7 +673,7 @@ CellRenders.addRender('STATE', function (v, s, k) {
}
})
CellRenders.addRender('DECIMAL', function (v, s, k) {
const renderNumber = function (v, s, k) {
if ((v + '').substr(0, 1) === '-') {
return (
<td key={k}>
@ -685,7 +685,9 @@ CellRenders.addRender('DECIMAL', function (v, s, k) {
} else {
return CellRenders.renderSimple(v, s, k)
}
})
}
CellRenders.addRender('DECIMAL', renderNumber)
CellRenders.addRender('NUMBER', renderNumber)
CellRenders.addRender('MULTISELECT', function (v, s, k) {
const vLen = (v.text || []).length

View file

@ -873,13 +873,22 @@ class RbFormDecimal extends RbFormNumber {
class RbFormTextarea extends RbFormElement {
constructor(props) {
super(props)
this._height = this.props.useMdedit ? 0 : ~~this.props.height
if (this._height && this._height > 0) {
if (this._height === 1) this._height = 37
else this._height = this._height * 20 + 12
}
}
renderElement() {
return (
<React.Fragment>
<textarea
ref={(c) => (this._fieldValue = c)}
ref={(c) => {
this._fieldValue = c
this._height > 0 && c && $(c).attr('style', `height:${this._height}px !important`)
}}
className={`form-control form-control-sm row3x ${this.state.hasError ? 'is-invalid' : ''} ${this.props.useMdedit && this.props.readonly ? 'cm-readonly' : ''}`}
title={this.state.hasError}
value={this.state.value || ''}
@ -896,12 +905,15 @@ class RbFormTextarea extends RbFormElement {
renderViewElement() {
if (!this.state.value) return super.renderViewElement()
const style = {}
if (this._height > 0) style.maxHeight = this._height
if (this.props.useMdedit) {
const md2html = SimpleMDE.prototype.markdown(this.state.value)
return <div className="form-control-plaintext mdedit-content" ref={(c) => (this._textarea = c)} dangerouslySetInnerHTML={{ __html: md2html }} />
return <div className="form-control-plaintext mdedit-content" ref={(c) => (this._textarea = c)} dangerouslySetInnerHTML={{ __html: md2html }} style={style} />
} else {
return (
<div className="form-control-plaintext" ref={(c) => (this._textarea = c)}>
<div className="form-control-plaintext" ref={(c) => (this._textarea = c)} style={style}>
{this.state.value.split('\n').map((line, idx) => {
return <p key={`line-${idx}`}>{line}</p>
})}
@ -1814,24 +1826,24 @@ class RbFormBool extends RbFormElement {
renderElement() {
return (
<div className="mt-1">
<label className="custom-control custom-radio custom-control-inline">
<label className="custom-control custom-radio custom-control-inline mb-1">
<input
className="custom-control-input"
name={`${this._htmlid}T`}
type="radio"
checked={$isTrue(this.state.value)}
checked={this.state.value === 'T'}
data-value="T"
onChange={this.changeValue}
disabled={this.props.readonly}
/>
<span className="custom-control-label">{this._Options['T']}</span>
</label>
<label className="custom-control custom-radio custom-control-inline">
<label className="custom-control custom-radio custom-control-inline mb-1">
<input
className="custom-control-input"
name={`${this._htmlid}F`}
type="radio"
checked={!$isTrue(this.state.value)}
checked={this.state.value === 'F'}
data-value="F"
onChange={this.changeValue}
disabled={this.props.readonly}

View file

@ -47,6 +47,7 @@ class ProTable extends React.Component {
return (
<th key={item.field} data-field={item.field} style={colStyles} className={item.nullable ? '' : 'required'}>
{item.label}
{item.tip && <i className="tipping zmdi zmdi-info-outline" title={item.tip} />}
<i className="dividing hide" />
</th>
)
@ -75,6 +76,13 @@ class ProTable extends React.Component {
})}
</tbody>
</table>
{(this.state.inlineForms || []).length === 0 && (
<div className="text-center text-muted mt-6" style={{ paddingTop: 2, paddingBottom: 1 }}>
<i className="x14 zmdi zmdi-playlist-plus mr-1 fs-16" />
{$L('请添加明细')}
</div>
)}
</div>
)
}
@ -96,6 +104,7 @@ class ProTable extends React.Component {
this._initModel = res.data // 新建用
this.setState({ formFields: res.data.elements }, () => {
$(this._$scroller).perfectScrollbar()
// $(this._$scroller).find('thead .tipping').tooltip({})
})
// 编辑

View file

@ -808,7 +808,7 @@ const RbViewPage = {
const entity = e.entity.split('.')
if (entity.length > 1) iv[entity[1]] = that.__id
else iv[`&${that.__entity[0]}`] = that.__id
RbFormModal.create({ title: `${title}`, entity: entity[0], icon: e.icon, initialValue: iv })
RbFormModal.create({ title: $L('新建%s', title), entity: entity[0], icon: e.icon, initialValue: iv })
}
})

View file

@ -200,7 +200,7 @@ function _handle512Change() {
// 立即执行
// eslint-disable-next-line no-unused-vars
function _useExecDirect() {
function useExecManual() {
$('.footer .btn-light').removeClass('hide')
$(`<a class="dropdown-item">${$L('立即执行')} <sup class="rbv" title="${$L('增值功能')}"></sup></a>`)
.appendTo('.footer .dropdown-menu')
@ -214,7 +214,7 @@ function _useExecDirect() {
confirm: function () {
this.disabled(true)
// eslint-disable-next-line no-undef
$.post(`/admin/robot/trigger/exec-direct?id=${wpc.configId}`, () => {
$.post(`/admin/robot/trigger/exec-manual?id=${wpc.configId}`, () => {
this.hide()
RbHighbar.success($L('执行成功'))
})

View file

@ -361,5 +361,5 @@ renderContentComp = function (props) {
})
// eslint-disable-next-line no-undef
_useExecDirect()
useExecManual()
}

View file

@ -546,5 +546,5 @@ renderContentComp = function (props) {
})
// eslint-disable-next-line no-undef
_useExecDirect()
useExecManual()
}

View file

@ -419,4 +419,7 @@ renderContentComp = function (props) {
contentComp = this
$('#react-content [data-toggle="tooltip"]').tooltip()
})
// eslint-disable-next-line no-undef
useExecManual()
}

View file

@ -13,19 +13,19 @@
.zmdi.err400,
.zmdi.err401,
.zmdi.err403,
.zmdi.err404,
.zmdi.err600 {
.zmdi.err404 {
color: #4285f4 !important;
}
.zmdi.err600::before {
content: '\f17a';
.zmdi.err600::before,
.zmdi.err602::before {
color: #fbbc05;
font-size: 5rem;
font-size: 6rem;
content: '\f17a';
}
.zmdi.err497::before {
.zmdi.err601::before,
.zmdi.err603::before {
content: '\f119';
}
.error-description > pre:empty {
display: none;
}