Diagramming for trigger RB-93 (#664)

* be:evalTriggerTimes

* be: RbFormRefform

* be: ObservableService order

* er

* be: user delete checks

* be: row selected

* be: .dataTables_oper.invisible

* be: BatchOperator default selected

* feat: mermaid

* be: comp {@NOW}

* be: passwd 10

---------

Co-authored-by: devezhao <zhaofang123@gmail.com>
This commit is contained in:
REBUILD 企业管理系统 2023-09-26 21:32:55 +08:00 committed by GitHub
parent 95287da21a
commit ed05f15bd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 2003 additions and 207 deletions

2
@rbv

@ -1 +1 @@
Subproject commit e76e5aede15f6c8b2ec45ec6a06c690292466fe4
Subproject commit 9073b96b92d22bf4b6d9a09bb1d8d6e635f4d655

View file

@ -18,7 +18,6 @@ import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.DataSpecificationException;
import com.rebuild.core.service.InternalPersistService;
import com.rebuild.core.service.query.QueryHelper;
import com.rebuild.core.support.CommonsLock;
import com.rebuild.core.support.i18n.Language;
import org.apache.commons.lang3.ObjectUtils;
@ -43,11 +42,6 @@ public abstract class BaseConfigurationService extends InternalPersistService {
@Override
public Record update(Record record) {
ID locked = hasLock() ? CommonsLock.getLockedUser(record.getPrimary()) : null;
if (locked != null && !locked.equals(UserContextHolder.getUser())) {
throw new DataSpecificationException(Language.L("操作失败 (已被锁定)"));
}
throwIfNotSelf(record.getPrimary());
cleanCache(record.getPrimary());
return super.update(putCreateBy4ShareTo(record));
@ -55,11 +49,6 @@ public abstract class BaseConfigurationService extends InternalPersistService {
@Override
public int delete(ID recordId) {
ID locked = hasLock() ? CommonsLock.getLockedUser(recordId) : null;
if (locked != null && !locked.equals(UserContextHolder.getUser())) {
throw new DataSpecificationException(Language.L("操作失败 (已被锁定)"));
}
throwIfNotSelf(recordId);
cleanCache(recordId);
return super.delete(recordId);

View file

@ -132,9 +132,14 @@ public class UserService extends BaseService {
String dsql = String.format("delete from `layout_config` where `CREATED_BY` = '%s'", recordId);
Application.getSqlExecutor().execute(dsql);
// 2.删除并刷新缓存
// 2.删除三方
String dsql2 = String.format("delete from `external_user` where `BIND_USER` = '%s'", recordId);
Application.getSqlExecutor().execute(dsql2);
// 3.删除并刷新缓存
super.delete(recordId);
Application.getUserStore().removeUser(recordId);
return 1;
}
@ -214,9 +219,7 @@ public class UserService extends BaseService {
}
int policy = RebuildConfiguration.getInt(ConfigurationItem.PasswordPolicy);
if (policy <= 1) {
return;
}
if (policy <= 1) return;
int countUpper = 0;
int countLower = 0;
@ -237,8 +240,8 @@ public class UserService extends BaseService {
if (countUpper == 0 || countLower == 0 || countDigit == 0) {
throw new DataSpecificationException(Language.L("密码不能小于 6 位,且必须包含数字和大小写字母"));
}
if (policy >= 3 && (countSpecial == 0 || password.length() < 8)) {
throw new DataSpecificationException(Language.L("密码不能小于 8 位,且必须包含数字和大小写字母及特殊字符"));
if (policy >= 3 && (countSpecial == 0 || password.length() < 10)) {
throw new DataSpecificationException(Language.L("密码不能小于 10 位,且必须包含数字和大小写字母及特殊字符"));
}
}
@ -489,13 +492,6 @@ public class UserService extends BaseService {
"select user from LoginLog where user = ?")
.setParameter(1, user)
.unique();
if (hasLogin != null) return true;
// 绑定
Object[] hasBind = Application.createQueryNoFilter(
"select bindUser from ExternalUser where bindUser = ?")
.setParameter(1, user)
.unique();
return hasBind != null;
return hasLogin != null;
}
}

View file

@ -53,6 +53,7 @@ import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@ -63,6 +64,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observer;
import java.util.Set;
import java.util.TreeMap;
@ -88,11 +90,18 @@ public class GeneralEntityService extends ObservableService implements EntitySer
protected GeneralEntityService(PersistManagerFactory aPMFactory) {
super(aPMFactory);
}
// 通知
addObserver(new NotificationObserver());
@Override
protected Observer[] getOrderObservers() {
Observer[] obs = new Observer[] {
// 触发器
addObserver(new RobotTriggerObserver());
new RobotTriggerObserver(),
// 通知
new NotificationObserver(),
};
obs = ArrayUtils.addAll(obs, super.getOrderObservers());
return obs;
}
@Override

View file

@ -45,15 +45,21 @@ public abstract class ObservableService extends Observable implements ServiceSpe
protected ObservableService(PersistManagerFactory aPMFactory) {
this.delegateService = new BaseService(aPMFactory);
// 默认监听者
addObserver(new RevisionHistoryObserver());
addObserver(new AttachmentAwareObserver());
for (Observer o : getOrderObservers()) {
log.info("Add observer : {} for [ {} ] ", o, getEntityCode());
addObserver(o);
}
}
@Override
public synchronized void addObserver(Observer o) {
super.addObserver(o);
log.info("Add observer : {} for [ {} ] ", o, getEntityCode());
/**
* @return
*/
protected Observer[] getOrderObservers() {
// 默认监听者
return new Observer[] {
new RevisionHistoryObserver(), // 2
new AttachmentAwareObserver(), // 1
};
}
@Override

View file

@ -52,6 +52,13 @@ public class RecordDifference {
return diffMerge(after, false);
}
/**
* 获取不同
*
* @param after
* @param diffCommons
* @return
*/
protected JSON diffMerge(Record after, boolean diffCommons) {
if (record == null && after == null) {
throw new RebuildException("Both records cannot be null");
@ -107,10 +114,10 @@ public class RecordDifference {
}
/**
* 是否
* 是否
*
* @param diff
* @param diffCommons
* @param diffCommons 是否比较系统共用字段
* @return
* @see #diffMerge(Record)
*/

View file

@ -696,6 +696,7 @@ public class AdvFilterParser extends SetUser {
private static final String PATT_FIELDVAR = "\\{@([\\w.]+)}";
// `当前`变量当前日期时间用户
private static final String CURRENT_ANY = "CURRENT";
private static final String CURRENT_DATE = "NOW";
private String useValueOfVarRecord(String value, Field queryField) {
if (StringUtils.isBlank(value) || !value.matches(PATT_FIELDVAR)) return value;
@ -706,7 +707,7 @@ public class AdvFilterParser extends SetUser {
Object useValue = null;
// {@CURRENT} DATE
if (CURRENT_ANY.equals(fieldName)) {
if (CURRENT_ANY.equals(fieldName) || CURRENT_DATE.equals(fieldName)) {
DisplayType dt = EasyMetaFactory.getDisplayType(queryField);
if (dt == DisplayType.DATE || dt == DisplayType.DATETIME || dt == DisplayType.TIME) {
useValue = CalendarUtils.now();

View file

@ -23,7 +23,6 @@ import org.springframework.util.Assert;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@ -108,7 +107,7 @@ public class QueryHelper {
}
/**
* 获取明细完整记录
* 获取明细列表记录
*
* @param mainId
* @return
@ -129,7 +128,7 @@ public class QueryHelper {
}
/**
* 获取明细 ID
* 获取明细列表 ID
*
* @param mainId
* @return
@ -217,12 +216,9 @@ public class QueryHelper {
final ID primaryId = base.getPrimary();
Assert.notNull(primaryId, "Record primary cannot be null");
Set<String> fields = new HashSet<>();
for (Iterator<String> iter = base.getAvailableFieldIterator(); iter.hasNext(); ) {
fields.add(iter.next());
}
Set<String> fields = new HashSet<>(base.getAvailableFields());
fields.add(base.getEntity().getPrimaryField().getName());
Record snap = Application.getQueryFactory().recordNoFilter(primaryId, fields.toArray(new String[0]));
if (snap == null) throw new NoRecordFoundException(primaryId);

View file

@ -337,8 +337,7 @@ public class FieldAggregation extends TriggerAction {
protected boolean isCurrentSame(Record record) {
if (!ignoreSame) return false;
Record c = Application.getQueryFactory().recordNoFilter(
record.getPrimary(), record.getAvailableFields().toArray(new String[0]));
Record c = QueryHelper.querySnap(record);
return new RecordDifference(record).isSame(c, false);
}

View file

@ -43,17 +43,15 @@ public final class UpgradeDatabase {
try {
while (true) {
String[] sql = scripts.get(upgradeVer + 1);
if (sql == null) {
break;
} else if (sql.length == 0) {
if (sql == null) break;
upgradeVer++;
continue;
if (sql.length > 0) {
log.info("\n>> UPGRADE SQL (#" + upgradeVer + ") >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" + StringUtils.join(sql, "\n"));
Application.getSqlExecutor().executeBatch(sql, 60 * 2);
}
}
log.info("\n>> UPGRADE SQL (#" + (upgradeVer + 1) + ") >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" + StringUtils.join(sql, "\n"));
Application.getSqlExecutor().executeBatch(sql, 60 * 2);
upgradeVer++;
}
} finally {
if (currentVer != upgradeVer) {
RebuildConfiguration.set(ConfigurationItem.DBVer, upgradeVer);

View file

@ -16,6 +16,7 @@ import com.rebuild.api.RespBody;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.ConfigurationException;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.service.datareport.DataReportManager;
import com.rebuild.core.service.datareport.EasyExcelGenerator;
import com.rebuild.core.service.datareport.EasyExcelListGenerator;
@ -33,6 +34,7 @@ import com.rebuild.web.EntityParam;
import com.rebuild.web.IdParam;
import com.rebuild.web.admin.ConfigCommons;
import com.rebuild.web.commons.FileDownloader;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -42,7 +44,9 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -68,13 +72,26 @@ public class ReportTemplateController extends BaseController {
String entity = getParameter(request, "entity");
String q = getParameter(request, "q");
String sql = "select configId,belongEntity,belongEntity,name,isDisabled,modifiedOn,templateType,extraDefinition from DataReportConfig" +
String sql = "select configId,belongEntity,belongEntity,name,isDisabled,modifiedOn,templateType,extraDefinition,configId from DataReportConfig" +
" where (1=1) and (2=2)" +
" order by modifiedOn desc, name";
Object[][] list = ConfigCommons.queryListOfConfig(sql, entity, q);
for (Object[] o : list) {
o[7] = o[7] == null ? JSONUtils.EMPTY_OBJECT : JSON.parseObject((String) o[7]);
JSONObject extra = o[7] == null ? JSONUtils.EMPTY_OBJECT : JSON.parseObject((String) o[7]);
o[7] = extra;
o[8] = null;
String vu = extra.getString("visibleUsers");
if (StringUtils.isNotBlank(vu)) {
List<String> vuNames = new ArrayList<>();
for (String id : vu.split(",")) {
if (ID.isId(id)) {
vuNames.add(UserHelper.getName(ID.valueOf(id)));
}
}
o[8] = StringUtils.join(vuNames, ", ");
}
}
return RespBody.ok(list);

View file

@ -42,7 +42,7 @@ import com.rebuild.web.EntityController;
import com.rebuild.web.commons.FileDownloader;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@ -307,9 +307,9 @@ public class MetaEntityController extends EntityController {
public ModelAndView pageSheet(HttpServletRequest request) {
ModelAndView mv = createModelAndView("/admin/metadata/entities-sheet");
String spec = getParameter(request, "e");
String spec = getParameter(request, "s");
Set<String> specList = null;
if (org.apache.commons.lang3.StringUtils.isNotBlank(spec)) {
if (StringUtils.isNotBlank(spec)) {
specList = new HashSet<>();
for (String s : spec.split(",")) specList.add(s.trim().toUpperCase());
}
@ -342,7 +342,7 @@ public class MetaEntityController extends EntityController {
ConfigBean[] cbs = PickListManager.instance.getPickListRaw(f, Boolean.TRUE);
List<String> texts = new ArrayList<>();
for (ConfigBean cb : cbs) texts.add(cb.getID("id") + ":" + cb.getString("text"));
opt = org.apache.commons.lang3.StringUtils.join(texts, "//");
opt = StringUtils.join(texts, "//");
}
fields.add(new Object[] {

View file

@ -21,10 +21,8 @@ import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.service.trigger.ActionFactory;
import com.rebuild.core.service.trigger.ActionType;
import com.rebuild.core.service.trigger.TriggerAction;
import com.rebuild.core.support.CommonsLock;
import com.rebuild.core.support.License;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.rbv.trigger.TriggerByTimerJob;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
import com.rebuild.web.admin.ConfigCommons;
@ -88,7 +86,6 @@ public class TriggerAdminController extends BaseController {
mv.getModel().put("actionContent", config[4]);
mv.getModel().put("priority", config[5]);
mv.getModel().put("name", config[6]);
mv.getModel().put("lockedUser", JSON.toJSONString(CommonsLock.getLockedUserFormat(configId)));
mv.getModel().put("isDisabled", config[8] == null ? false : config[8]);
return mv;
@ -121,22 +118,28 @@ 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,priority,actionContent from RobotTriggerConfig" +
String sql = "select configId,belongEntity,belongEntity,name,isDisabled,modifiedOn,when,actionType,priority,actionContent from RobotTriggerConfig" +
" where (1=1) and (2=2)" +
" order by modifiedOn desc, name";
Object[][] array = ConfigCommons.queryListOfConfig(sql, belongEntity, q);
for (Object[] o : array) {
o[7] = Language.L(ActionType.valueOf((String) o[7]));
o[8] = CommonsLock.getLockedUserFormat((ID) o[8]);
// 目标实体
o[10] = tryParseTargetEntity((String) o[10], (String) o[1]);
o[9] = tryParseTargetEntity((String) o[9], (String) o[1], true);
}
return array;
}
private String tryParseTargetEntity(String config, String sourceEntity) {
/**
* 尝试解析目标实体
*
* @param config
* @param sourceEntity
* @param useLabel
* @return
*/
public static String tryParseTargetEntity(String config, String sourceEntity, boolean useLabel) {
if (!JSONUtils.wellFormat(config)) return null;
JSONObject configJson = JSON.parseObject(config);
@ -147,9 +150,9 @@ public class TriggerAdminController extends BaseController {
else if (targetEntity.contains(".")) targetEntity = targetEntity.split("\\.")[1];
if (MetadataHelper.containsEntity(targetEntity)) {
return EasyMetaFactory.getLabel(targetEntity);
return useLabel ? EasyMetaFactory.getLabel(targetEntity) : targetEntity;
} else {
return String.format("[%s]", targetEntity.toUpperCase());
return useLabel ? String.format("[%s]", targetEntity.toUpperCase()) : null;
}
}
@ -164,19 +167,12 @@ public class TriggerAdminController extends BaseController {
targetEntity = cb.getString("target");
if (MetadataHelper.containsEntity(targetEntity)) {
return EasyMetaFactory.getLabel(targetEntity);
return useLabel ? EasyMetaFactory.getLabel(targetEntity) : targetEntity;
} else {
return String.format("[%s]", targetEntity.toUpperCase());
return useLabel ? String.format("[%s]", targetEntity.toUpperCase()) : null;
}
}
return null;
}
@GetMapping("trigger/eval-trigger-times")
public JSON evalTriggerTimes(HttpServletRequest request) {
String whenTimer = getParameterNotNull(request, "whenTimer");
List<String> s = TriggerByTimerJob.getInTriggerTime(whenTimer);
return (JSON) JSON.toJSON(s);
}
}

View file

@ -711,7 +711,7 @@
"密码":"密码",
"密码不能小于 6 位":"密码不能小于 6 位",
"密码不能小于 6 位,且必须包含数字和大小写字母":"密码不能小于 6 位,且必须包含数字和大小写字母",
"密码不能小于 8 位,且必须包含数字和大小写字母及特殊字符":"密码不能小于 8 位,且必须包含数字和大小写字母及特殊字符",
"密码不能小于 10 位,且必须包含数字和大小写字母及特殊字符":"密码不能小于 10 位,且必须包含数字和大小写字母及特殊字符",
"密码将重置为 **%s** 是否确认?":"密码将重置为 **%s** 是否确认?",
"密码错误":"密码错误",
"对比图":"对比图",
@ -1664,7 +1664,7 @@
"验证码已发送至你的邮箱":"验证码已发送至你的邮箱",
"验证码无效":"验证码无效",
"验证码错误":"验证码错误",
"高 (最低8位必须同时包含数字、字母、特殊字符)":"高 (最低8位,必须同时包含数字、字母、特殊字符)",
"高 (最低10位必须同时包含数字、字母、特殊字符)":"高 (最低10位,必须同时包含数字、字母、特殊字符)",
"高级功能":"高级功能",
"高级控制":"高级控制",
"高级查询":"高级查询",

View file

@ -59,7 +59,8 @@
<thead>
<tr>
<th>[[${bundle.L('名称')}]]</th>
<th>[[${bundle.L('应用实体')}]]</th>
<th width="20%">[[${bundle.L('应用实体')}]]</th>
<th>[[${bundle.L('使用用户')}]]</th>
<th width="80" class="no-sort">[[${bundle.L('启用')}]]</th>
<th width="120" class="no-sort">[[${bundle.L('修改时间')}]]</th>
<th width="150" class="no-sort"></th>

View file

@ -68,7 +68,7 @@
<span th:text="${e[1]}" class="badge ml-1" title="Entity Code"></span>
<a class="float-right font-weight-normal fs-12" href="#index">top</a>
</div>
<table class="table table-sm table-fixed table-hover">
<table class="table table-sm table-hover table-fixed table-btm-line">
<thead>
<tr>
<th>Field Name</th>
@ -116,7 +116,7 @@
const opt = $item.text() || ''
if (opt.length < 10) return
const $ul = $('<ol class="pl-3 m-0"></ol>').appendTo($item.empty())
const $ul = $('<ul class="pl-3 m-0"></ul>').appendTo($item.empty())
$(opt.split('//')).each(function () {
const o = this.split(':')
$(`<li><span>${o[1]}</span> <code>${o[0]}</code></li>`).appendTo($ul)

View file

@ -60,7 +60,7 @@
<div>
<div class="float-right">
<div class="btn-group">
<button class="btn btn-light w-auto dropdown-toggle mr-1" type="button" data-toggle="dropdown">+ [[${bundle.L('添加')}]]</button>
<button class="btn btn-light w-auto dropdown-toggle mr-1" type="button" data-toggle="dropdown">[[${bundle.L('添加')}]] <i class="icon zmdi zmdi-more-vert"></i></button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item J_add-divider">[[${bundle.L('分栏')}]]</a>
<a class="dropdown-item J_add-refform">[[${bundle.L('表单引用')}]] <sup class="rbv"></sup></a>

View file

@ -195,7 +195,6 @@
whenTimer: '[[${whenTimer}]]',
whenFilter: [(${whenFilter ?:'null'})],
actionContent: [(${actionContent ?:'null'})],
lockedUser: [(${lockedUser ?:'null'})],
}
</script>
<script th:src="@{/assets/js/rb-advfilter.js}" type="text/babel"></script>

View file

@ -42,13 +42,13 @@
<div class="row rb-datatable-body">
<div class="col-sm-12">
<div class="rb-loading rb-loading-active data-list">
<table class="table table-hover table-striped table-fixed tablesort">
<table class="table table-hover table-striped tablesort">
<thead>
<tr>
<th>[[${bundle.L('名称')}]]</th>
<th width="15%">[[${bundle.L('源实体')}]]</th>
<th width="15%">[[${bundle.L('触发类型')}]]</th>
<th width="15%" class="no-sort">[[${bundle.L('触发动作')}]]</th>
<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>

View file

@ -102,7 +102,7 @@
<div>
<span>&copy; REBUILD ([[${Version}]])</span>
<b>·</b>
<a href="https://getrebuild.com/docs/admin/install" target="_blank" class="link">[[${bundle.L('安装指南')}]]</a>
<a href="https://getrebuild.com/docs/admin/install" target="_blank" class="link link-icon">[[${bundle.L('安装指南')}]]</a>
</div>
<div
class="mt-1 license link"

View file

@ -184,7 +184,7 @@
<td data-id="PasswordPolicy" th:data-value="${PasswordPolicy}">
<th:block th:if="${PasswordPolicy == '1'}">[[${bundle.L('低 (最低6位无字符类型限制)')}]]</th:block>
<th:block th:if="${PasswordPolicy == '2'}">[[${bundle.L('中 (最低6位必须同时包含数字、字母)')}]]</th:block>
<th:block th:if="${PasswordPolicy == '3'}">[[${bundle.L('高 (最低8位,必须同时包含数字、字母、特殊字符)')}]]</th:block>
<th:block th:if="${PasswordPolicy == '3'}">[[${bundle.L('高 (最低10位,必须同时包含数字、字母、特殊字符)')}]]</th:block>
</td>
</tr>
<tr>

View file

@ -650,6 +650,8 @@ See LICENSE and COMMERCIAL in the project root for license information.
bottom: 20px;
z-index: 10;
opacity: 0.6;
transition: opacity 0.2s linear;
cursor: default;
}
.zoom:hover {
@ -674,6 +676,11 @@ See LICENSE and COMMERCIAL in the project root for license information.
color: #777;
}
.zoom .zoom-in:hover i,
.zoom .zoom-out:hover i {
color: #222;
}
.fields-table .custom-control {
padding-left: 1.9377rem !important;
}

View file

@ -913,6 +913,20 @@ select.form-control:not([disabled]) {
background-size: 16px;
}
div.link a:hover,
p.link a:hover,
li.link a:hover,
a.link:hover,
a.column-url:hover {
text-decoration: underline !important;
}
a.link.link-icon::after {
content: '\f1a3';
font-family: 'Material-Design-Iconic-Font', serif;
margin-left: 3px;
}
/* rbform */
.form-layout > .row {
@ -1279,12 +1293,23 @@ select.form-control:not([disabled]) {
/* v35 */
.form-layout.refform {
display: block;
background-color: rgba(230, 230, 230, 0.3);
box-shadow: inset 0px 0px 10px rgba(230, 230, 230, 0.6);
border-radius: 2px;
background-color: rgba(230, 230, 230, 0.2);
box-shadow: inset 0 0 10px rgba(230, 230, 230, 0.5);
width: 100%;
margin: 10px 15px;
padding: 15px;
border-radius: 6px;
position: relative;
}
.form-layout.refform > .open-in-new {
position: absolute;
right: 3px;
top: 3px;
font-size: 1.45rem;
padding: 5px;
display: inline-block;
z-index: 1;
}
.input-group.has-append {
@ -1645,14 +1670,6 @@ i.dividing.ui-draggable-dragging {
border-bottom: 0 solid;
}
div.link a:hover,
p.link a:hover,
li.link a:hover,
a.link:hover,
a.column-url:hover {
text-decoration: underline !important;
}
.column-empty {
padding: 8px !important;
}
@ -2085,8 +2102,9 @@ th.column-fixed {
width: auto;
left: 10px;
margin: -11px 0 0;
padding: 3px 5px 3px 5px;
padding: 3px 5px 0;
color: #666;
border-radius: 4px;
}
.form-line.hover fieldset legend {
@ -2103,12 +2121,11 @@ th.column-fixed {
.form-line.hover fieldset legend::before {
font-family: 'Material-Design-Iconic-Font', serif;
font-size: 1.4rem;
font-size: 1.5rem;
content: '\f2f9';
width: 10px;
width: 13px;
display: inline-block;
float: left;
margin-right: 3px;
text-align: center;
line-height: 1;
transform: translateY(-1px);

View file

@ -32,6 +32,11 @@ class ReportList extends ConfigList {
{outputType.includes('html') && <span className="badge badge-secondary badge-sm ml-1">HTML</span>}
</td>
<td>{item[2] || item[1]}</td>
<td>
<div className="text-break" style={{ maxWidth: 300 }}>
{item[8] || $L('所有用户')}
</div>
</td>
<td>{ShowEnable(item[4])}</td>
<td>
<DateShow date={item[5]} />
@ -176,7 +181,9 @@ class ReportEditor extends ConfigFormDlg {
</div>
</div>
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('谁能使用这个报表')}</label>
<label className="col-sm-3 col-form-label text-sm-right">
{$L('谁能使用这个报表')} <sup className="rbv" />
</label>
<div className="col-sm-7">
<UserSelector
ref={(c) => (this._UserSelector = c)}
@ -326,7 +333,7 @@ class ReportEditor extends ConfigFormDlg {
outputType: output.length === 0 ? 'excel' : output.join(','),
templateVersion: (this.props.extraDefinition || {}).templateVersion || 2,
// v3.5
visibleUsers: this._UserSelector.val().join(','),
visibleUsers: rb.commercial < 1 ? null : this._UserSelector.val().join(','),
}
if (this.props.id) {

View file

@ -83,7 +83,7 @@ useEditComp = function (name) {
<select className="form-control form-control-sm">
<option value="1">{$L(' (最低6位无字符类型限制)')}</option>
<option value="2">{$L(' (最低6位必须同时包含数字字母)')}</option>
<option value="3">{$L(' (最低8必须同时包含数字字母特殊字符)')}</option>
<option value="3">{$L(' (最低10必须同时包含数字字母特殊字符)')}</option>
</select>
)
} else if ('DefaultLanguage' === name) {

View file

@ -265,6 +265,6 @@ class DlgEnableUser extends RbModalHandler {
}
const _reload = function (timeout) {
setTimeout(() => location.reload(), timeout || 1)
setTimeout(() => RbViewPage.reload(), timeout || 1)
parent && parent.RbListPage && parent.RbListPage.reload()
}

View file

@ -120,7 +120,7 @@ class RbPreview extends React.Component {
<a className="arrow float-left" onClick={this._rotateImage} title={$L('旋转')}>
<i className="mdi mdi-rotate-right" />
</a>
<a className="arrow float-right" onClick={this._screenImage} title={$L('放大')}>
<a className="arrow float-right" onClick={this._screenImage} title={$L('适合页面')}>
<i className="mdi mdi-fit-to-screen-outline" />
</a>
</div>

View file

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

View file

@ -16,15 +16,16 @@ class RbModal extends React.Component {
}
render() {
const props = this.props
const style1 = {}
if (this.props.zIndex) style1.zIndex = this.props.zIndex
if (props.zIndex) style1.zIndex = props.zIndex
const iframe = !this.props.children // No child
const style2 = { maxWidth: ~~(this.props.width || 680) }
const iframe = !props.children // No child
const style2 = { maxWidth: ~~(props.width || 680) }
return (
<div
className={`modal rbmodal colored-header colored-header-${this.props.colored || 'primary'}`}
className={`modal rbmodal colored-header colored-header-${props.colored || 'primary'}`}
style={style1}
ref={(c) => {
this._rbmodal = c
@ -33,14 +34,19 @@ class RbModal extends React.Component {
<div className="modal-dialog" style={style2}>
<div className="modal-content" style={style2}>
<div className="modal-header modal-header-colored">
{this.props.icon && <i className={`icon zmdi zmdi-${this.props.icon}`} />}
<h3 className="modal-title">{this.props.title || 'UNTITLED'}</h3>
{props.icon && <i className={`icon zmdi zmdi-${props.icon}`} />}
<h3 className="modal-title">{props.title || 'UNTITLED'}</h3>
{props.url && props.urlOpenInNew && (
<a className="close s fs-18" href={props.url} target="_blank" title={$L('在新页面打开')}>
<span className="zmdi zmdi-open-in-new" />
</a>
)}
<button className="close" type="button" onClick={() => this.hide()} title={$L('关闭')}>
<span className="zmdi zmdi-close" />
</button>
</div>
<div className={`modal-body ${iframe ? 'iframe rb-loading' : ''} ${iframe && this.state.frameLoad !== false ? 'rb-loading-active' : ''}`} id={this._htmlid}>
{this.props.children || <iframe src={this.props.url} frameBorder="0" scrolling="no" onLoad={() => this.resize()} />}
{props.children || <iframe src={props.url} frameBorder="0" scrolling="no" onLoad={() => this.resize()} />}
{iframe && <RbSpinner />}
</div>
</div>
@ -97,25 +103,25 @@ class RbModal extends React.Component {
/**
* @param {*} url
* @param {*} title
* @param {*} options
* @param {*} option
*/
static create(url, title, options) {
static create(url, title, option) {
// URL prefix
if (url.substr(0, 1) === '/' && rb.baseUrl) url = rb.baseUrl + url
options = options || {}
options.disposeOnHide = options.disposeOnHide === true // default false
option = option || {}
option.disposeOnHide = option.disposeOnHide === true // default false
this.__HOLDERs = this.__HOLDERs || {}
const that = this
if (options.disposeOnHide === false && !!that.__HOLDERs[url]) {
if (option.disposeOnHide === false && !!that.__HOLDERs[url]) {
that.__HOLDER = that.__HOLDERs[url]
that.__HOLDER.show()
that.__HOLDER.resize()
} else {
renderRbcomp(<RbModal url={url} title={title} width={options.width} disposeOnHide={options.disposeOnHide} zIndex={options.zIndex} />, null, function () {
renderRbcomp(<RbModal url={url} urlOpenInNew={option.urlOpenInNew} title={title} width={option.width} disposeOnHide={option.disposeOnHide} zIndex={option.zIndex} />, null, function () {
that.__HOLDER = this
if (options.disposeOnHide === false) that.__HOLDERs[url] = this
if (option.disposeOnHide === false) that.__HOLDERs[url] = this
})
}
}

View file

@ -220,7 +220,7 @@ const AdvFilters = {
class BatchOperator extends RbFormHandler {
constructor(props) {
super(props)
this.state.dataRange = 2
this.state.dataRange = props.listRef.getSelectedIds(true).length > 0 ? 1 : 2
}
render() {
@ -1094,16 +1094,23 @@ class RbList extends React.Component {
_clickRow(e) {
const $target = $(e.target)
if ($target.hasClass('custom-control-label')) return // ignored
const holdSelected = $target.hasClass('custom-checkbox') || $target.parents('.custom-checkbox').hasClass('custom-checkbox')
console.log($target)
const $tr = $target.parents('tr')
let holdSelected = true
if ($target.hasClass('column-checkbox')) {
const $chk = $tr.find('.custom-control-input')[0]
$chk.checked = !$chk.checked
holdSelected = true
} else {
holdSelected = $target.hasClass('custom-checkbox') || $target.parents('.custom-checkbox').hasClass('custom-checkbox')
}
if (holdSelected) {
if ($tr.find('.custom-control-input')[0].checked) $tr.addClass('active')
else $tr.removeClass('active')
} else {
this._toggleRows({ target: { checked: false } }, true)
$tr.addClass('active').find('.custom-control-input').prop('checked', true)
$tr.addClass('active').find('.custom-control-input')[0].checked = true
}
this._checkSelected()
@ -1165,7 +1172,7 @@ class RbList extends React.Component {
_tryActive($el) {
if ($el.length === 1) {
this._clickRow({ target: $el.find('.custom-checkbox') }, true)
this._clickRow({ target: $el.find('td:eq(1)') })
}
}

View file

@ -91,6 +91,7 @@ const RbListPage = {
if (ep.A !== true) $('.J_assign').remove()
if (ep.S !== true) $('.J_share, .J_unshare').remove()
$cleanMenu('.J_action')
$('.dataTables_oper.invisible').removeClass('invisible')
}
// Filter Pane

View file

@ -2767,11 +2767,15 @@ class RbFormRefform extends React.Component {
// 有错误
if (res.error_code > 0 || !!res.data.error) {
const err = res.data.error || res.error_msg
this.setState({ formComponent: <div>{err}</div> })
this.setState({ formComponent: <div className="text-danger">{err}</div> })
return
}
const VFORM = (
<RF>
<a title={$L('在新页面打开')} className="close open-in-new" href={`${rb.baseUrl}/app/entity/view?id=${props.id}`} target="_blank">
<i className="icon zmdi zmdi-open-in-new" />
</a>
<div className="row">
{res.data.elements.map((item) => {
item.$$$parent = this
@ -2779,6 +2783,7 @@ class RbFormRefform extends React.Component {
return detectViewElement(item, props.entity)
})}
</div>
</RF>
)
this.setState({ formComponent: VFORM })
})

View file

@ -28,24 +28,6 @@ $(document).ready(() => {
})
$('.J_startHour2').val('23')
$('.on-timers select, .on-timers input').on('change', () => {
const whenTimer = `${$('.J_whenTimer1').val() || 'D'}:${$('.J_whenTimer2').val() || 1}:${$('.J_startHour1').val() || 0}:${$('.J_startHour2').val() || 23}`
$.get(`/admin/robot/trigger/eval-trigger-times?whenTimer=${whenTimer}`, (res) => {
renderRbcomp(
<RbAlertBox
icon="time"
message={
<div>
<span className="mr-1">{$L('预计执行时间')}</span>
<code>{res.data.join(', ')}</code>
</div>
}
/>,
$('.eval-exec-times')[0]
)
})
})
if (wpc.when > 0) {
$([1, 2, 4, 16, 32, 64, 128, 256, 512, 1024, 2048]).each(function () {
let mask = this
@ -67,6 +49,29 @@ $(document).ready(() => {
})
}
// 评估具体执行时间
function evalTriggerTimes() {
const whenTimer = `${$('.J_whenTimer1').val() || 'D'}:${$('.J_whenTimer2').val() || 1}:${$('.J_startHour1').val() || 0}:${$('.J_startHour2').val() || 23}`
$.get(`/admin/robot/trigger/eval-trigger-times?whenTimer=${whenTimer}`, (res) => {
renderRbcomp(
<RbAlertBox
icon="time"
message={
<div>
<span className="mr-1">{$L('预计执行时间')}</span>
<code>{res.data.join(', ')}</code>
</div>
}
/>,
$('.eval-exec-times')[0]
)
})
}
if (rb.commercial >= 10) {
$('.on-timers select').on('change', () => $setTimeout(evalTriggerTimes, 500, 'eval-trigger-times'))
$('.on-timers input').on('input', () => $setTimeout(evalTriggerTimes, 500, 'eval-trigger-times'))
}
let advFilter
$('.J_whenFilter .btn').on('click', () => {
if (advFilter) {
@ -149,11 +154,6 @@ $(document).ready(() => {
})
})
if (wpc.lockedUser && wpc.lockedUser[0] !== rb.currentUser) {
$('.footer .alert-warning').removeClass('hide').find('.message').text($L('已被 %s 锁定其他人无法操作', wpc.lockedUser[1]))
$('.footer .btn').attr('disabled', true)
}
if (LastLogsViewer.renderLog && rb.commercial > 1) {
$.get(`/admin/robot/trigger/last-logs?id=${wpc.configId}`, (res) => {
const _data = res.data || []

View file

@ -65,9 +65,6 @@ class TriggerList extends ConfigList {
return (
<RF>
{(this.state.data || []).map((item) => {
const locked = item[8]
const disabled = locked && locked[0] !== rb.currentUser
return (
<tr key={item[0]}>
<td>
@ -76,34 +73,28 @@ class TriggerList extends ConfigList {
<td>{item[2] || item[1]}</td>
<td>
{item[7]}
{item[10] && (
{item[9] && (
<span title={$L('目标实体')} className="ml-1">
({item[10]})
({item[9]})
</span>
)}
</td>
<td className="text-wrap">{item[6] > 0 ? $L(' %s ', formatWhen(item[6])) : <span className="text-warning">({$L('无触发动作')})</span>}</td>
<td>
<span className="badge badge-light">{item[9]}</span>
<span className="badge badge-light">{item[8]}</span>
</td>
<td>{ShowEnable(item[4], item[0])}</td>
<td>
<DateShow date={item[5]} />
</td>
<td className="actions">
{locked ? (
<a className="icon" title={locked[0] === rb.currentUser ? $L('解锁') : $L('已被 %s 锁定', locked[1])} onClick={() => this.handleLock(item)}>
<i className={`zmdi zmdi-lock-outline text-${disabled ? 'danger' : 'warning'}`} />
<a className="icon" title={$L('触发过程')} onClick={() => this.handleShowChain(item[0])}>
<i className="zmdi mdi mdi-vector-polyline mdi-rotate-180" />
</a>
) : (
<a className="icon" title={$L('锁定')} onClick={() => this.handleLock(item, true)}>
<i className="zmdi zmdi-lock-open" />
</a>
)}
<a className="icon" title={$L('修改')} onClick={() => !disabled && this.handleEdit(item)} disabled={disabled}>
<a className="icon" title={$L('修改')} onClick={() => this.handleEdit(item)}>
<i className="zmdi zmdi-edit" />
</a>
<a className="icon danger-hover" title={$L('删除')} onClick={() => !disabled && this.handleDelete(item[0])} disabled={disabled}>
<a className="icon danger-hover" title={$L('删除')} onClick={() => this.handleDelete(item[0])}>
<i className="zmdi zmdi-delete" />
</a>
</td>
@ -114,27 +105,6 @@ class TriggerList extends ConfigList {
)
}
handleLock(item, lock) {
if (lock !== true && item[8][0] !== rb.currentUser) {
RbHighbar.create($L('请联系 %s 解锁', item[8][1]))
return
}
const tips = lock ? $L('锁定后只有你可以修改/删除其他人无法操作直到你解锁') : $L('确认解锁')
RbAlert.create(tips, {
type: 'warning',
onConfirm: function () {
this.disabled(true)
$.post(`/admin/lock/${lock ? 'lock' : 'unlock'}?id=${item[0]}`, (res) => {
this.hide()
if (res.error_code === 0) dlgActionAfter()
else RbHighbar.error(res.error_msg)
})
},
})
}
handleEdit(item) {
renderRbcomp(<TriggerEdit id={item[0]} name={item[3]} isDisabled={item[4]} />)
}
@ -150,6 +120,13 @@ class TriggerList extends ConfigList {
},
})
}
handleShowChain(id) {
if (rb.commercial < 10) {
return RbHighbar.error(WrapHtml($L('免费版不支持此功能 [(查看详情)](https://getrebuild.com/docs/rbv-features)')))
}
RbModal.create(`trigger/trigger-chain?id=${id}`, $L('触发过程'), { urlOpenInNew: true })
}
}
class TriggerEdit extends ConfigFormDlg {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:replace="~{/_include/header}" />
<link rel="stylesheet" type="text/css" th:href="@{/assets/css/approvals.css}" />
<title>[[${title ?:'REBUILD'}]]</title>
<style>
html,
body,
.main-content,
pre {
background-color: #fff;
height: 100%;
overflow: hidden;
text-align: center;
vertical-align: middle;
}
.main-content,
pre.mermaid {
min-height: 500px;
}
pre.mermaid {
color: #fff;
padding: 0;
margin: 0;
}
pre.mermaid .edgeLabel,
pre.mermaid .er.relationshipLabel {
background-color: #fff !important;
font-size: 14px;
}
pre.mermaid .relationshipLabelBox {
fill: none !important;
opacity: 1 !important;
}
pre.mermaid .nodeLabel {
font-size: 14px;
color: #222;
}
pre.mermaid .node rect {
fill: #e6eff8 !important;
stroke: #4285f4 !important;
}
pre.mermaid .mermaidTooltip {
display: none;
font-size: 0;
}
</style>
</head>
<body>
<div class="main-content m-0 p-0">
<pre class="mermaid">
[(${mermaidData})]
</pre>
</div>
<div class="zoom hide">
<a class="zoom-in">
<i class="zmdi zmdi-plus"></i>
</a>
<span>100%</span>
<a class="zoom-out">
<i class="zmdi zmdi-minus"></i>
</a>
</div>
<th:block th:replace="~{/_include/footer}" />
<script th:src="@{/assets/lib/charts/mermaid.min.js?v=10.4.0}"></script>
<script type="text/babel">
$(document).ready(() => {
const $node = $('.mermaid')
mermaid.initialize({
securityLevel: 'loose',
startOnLoad: false,
})
mermaid
.run({
nodes: $node,
})
.then(() => {
$('.zoom').removeClass('hide')
parent.RbModal.resize()
})
.catch((err) => {
console.error(err)
})
let zoom = 100
function setZoom(z) {
zoom += z
if (zoom < 20) zoom = 20
if (zoom > 500) zoom = 500
$('.zoom > span').text(zoom + '%')
$node.css('transform', `scale(${zoom / 100})`)
}
$('.zoom .zoom-in').on('click', () => setZoom(10))
$('.zoom .zoom-out').on('click', () => setZoom(-10))
$node.draggable({ cursor: 'move', scroll: false })
$(document).on('mousewheel', (e) => {
const value = e.originalEvent.wheelDelta || -e.originalEvent.detail
const delta = Math.max(-1, Math.min(1, value))
setZoom(delta < 0 ? -10 : 10)
})
})
function nodeClick(a) {
window.open(`../../entity/${a}/base`)
}
</script>
</body>
</html>

View file

@ -90,7 +90,7 @@
</div>
</div>
<div class="col-12 col-md-6">
<div class="dataTables_oper">
<div class="dataTables_oper invisible">
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> [[${bundle.L('打开')}]]</button>
<button class="btn btn-space btn-secondary J_edit" type="button" disabled="disabled"><i class="icon zmdi zmdi-edit"></i> [[${bundle.L('编辑')}]]</button>
<div class="btn-group btn-space J_action">

View file

@ -90,7 +90,7 @@
</div>
</div>
<div class="col-12 col-md-6">
<div class="dataTables_oper">
<div class="dataTables_oper invisible">
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> [[${bundle.L('打开')}]]</button>
<button class="btn btn-space btn-secondary J_edit" type="button" disabled="disabled"><i class="icon zmdi zmdi-edit"></i> [[${bundle.L('编辑')}]]</button>
<button class="btn btn-space btn-primary J_new" type="button" disabled="disabled"><i class="icon zmdi zmdi-plus"></i> [[${bundle.L('新建')}]]</button>