This commit is contained in:
REBUILD 企业管理系统 2024-09-13 03:41:52 +00:00 committed by GitHub
commit 666e4cbedc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 157 additions and 82 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 734c2297f22409c82bd1f70228362c245c4db246
Subproject commit a9ad9283ed576644ff18581919437314bd238672

View file

@ -15,18 +15,18 @@
> **福利:加入 REBUILD VIP 用户 QQ 交流群 819865721 1013051587 GET 使用技能**
## V3.7 新特性
## V3.8 新特性
本次更新为你带来众多功能增强与优化。
1. [新增] 限时审批
2. [新增] 新建任务(触发器)
3. [新增] 地图等多个图表
4. [新增] 自动明细记录导入(记录转换)
5. [新增] 数据列表之卡片模式
1. [新增] HTML 报表模版
2. [新增] 多表单布局
3. [新增] 明细支持 Excel 粘贴录入
4. [新增] 图片/附件支持摄像头上传模式
5. [新增] 手机版数据列表可导出报表
6. [新增] 多个 FrontJS 函数
7. [优化] 图表支持多轴显示、横向显示、显示背景
8. [优化] 手机版全新列表搜索组件
7. [新增] 用户支持批量操作
8. [优化] 20+ 细节/BUG/安全性更新
9. ...
更多更新详情请参见 [更新日志](https://getrebuild.com/docs/dev/changelog)

View file

@ -10,7 +10,7 @@
</parent>
<groupId>com.rebuild</groupId>
<artifactId>rebuild</artifactId>
<version>3.8.0-dev</version>
<version>3.8.0-beta1</version>
<name>rebuild</name>
<description>Building your business-systems freely!</description>
<url>https://getrebuild.com/</url>

View file

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

View file

@ -59,6 +59,7 @@ public enum ZeroEntry {
/**
* 允许撤销审批
* v3.8 起添加撤回权限
*/
AllowRevokeApproval(false),

View file

@ -583,10 +583,10 @@ public class ApprovalProcessor extends SetUser {
if (FlowNode.NODE_AUTOAPPROVAL.equals(nodeNo)) {
// No name
} else if (FlowNode.NODE_REVOKED.equals(nodeNo)) {
String nodeName = Language.L("管理员撤销");
String nodeName = Language.L("撤销");
s.put("nodeName", nodeName);
} else if (FlowNode.NODE_CANCELED.equals(nodeNo)) {
String nodeName = Language.L("提交人撤回");
String nodeName = Language.L("撤回");
s.put("nodeName", nodeName);
} else {
String nodeName = flowNode == null ? null : flowNode.getNodeName();

View file

@ -27,7 +27,6 @@ import com.rebuild.core.privileges.OperationDeniedException;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.privileges.bizz.InternalPermission;
import com.rebuild.core.privileges.bizz.ZeroEntry;
import com.rebuild.core.service.BaseService;
import com.rebuild.core.service.DataSpecificationNoRollbackException;
import com.rebuild.core.service.general.GeneralEntityServiceContextHolder;
@ -54,6 +53,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.rebuild.core.privileges.bizz.ZeroEntry.AllowRevokeApproval;
/**
* 审批流程此类所有方法不应直接调用而是通过 ApprovalProcessor 封装类
* <p>
@ -324,13 +325,14 @@ public class ApprovalStepService extends BaseService {
final boolean isAdmin = UserHelper.isAdmin(opUser);
if (isRevoke) {
boolean canRevoke = Application.getPrivilegesManager().allow(opUser, ZeroEntry.AllowRevokeApproval);
boolean canRevoke = Application.getPrivilegesManager().allow(opUser, AllowRevokeApproval);
if (!(isAdmin || canRevoke)) {
throw new OperationDeniedException(Language.L("你无权撤销审批"));
}
} else {
boolean canRevoke = Application.getPrivilegesManager().allow(opUser, AllowRevokeApproval);
ID s = ApprovalHelper.getSubmitter(recordId, approvalId);
if (!(isAdmin || opUser.equals(s))) {
if (!(canRevoke || opUser.equals(s))) {
throw new OperationDeniedException(Language.L("你无权撤回审批"));
}
}

View file

@ -105,8 +105,10 @@ public class DataExporter extends SetUser {
final List<String> head = this.buildHead(builder);
// Excel
if ("xls".equalsIgnoreCase(csvOrExcel)) {
File file = RebuildConfiguration.getFileOfTemp(String.format("RBEXPORT-%d.xls", System.currentTimeMillis()));
// xlsx: 65535 行数限制
if ("xls".equalsIgnoreCase(csvOrExcel) || "xlsx".equalsIgnoreCase(csvOrExcel)) {
File file = RebuildConfiguration.getFileOfTemp(
String.format("RBEXPORT-%d.%s", System.currentTimeMillis(), csvOrExcel.toLowerCase()));
List<List<String>> head4Excel = new ArrayList<>();
for (String h : head) {

View file

@ -147,26 +147,7 @@ public class AdvFilterParser extends SetUser {
// 自动确定查询项
if (MODE_QUICK.equalsIgnoreCase(filterExpr.getString("type"))) {
String quickFields = filterExpr.getString("quickFields");
JSONArray quickItems = buildQuickFilterItems(quickFields, 1);
// TODO v3.6-b4,3.7 值1|值2 UNTEST
// 转义可输入 \|
JSONObject values = filterExpr.getJSONObject("values");
String[] valuesPlus = values.values().iterator().next().toString().split("(?<!\\\\)\\|");
if (valuesPlus.length > 1) {
values.clear();
values.put("1", valuesPlus[0].trim());
for (int i = 2; i <= valuesPlus.length; i++) {
JSONArray quickItemsPlus = buildQuickFilterItems(quickFields, i);
values.put(String.valueOf(i), valuesPlus[i - 1].trim());
quickItems.addAll(quickItemsPlus);
}
filterExpr.put("values", values);
}
filterExpr.put("items", quickItems);
rebuildQuickFilter38();
}
JSONArray items = filterExpr.getJSONArray("items");
@ -653,6 +634,10 @@ public class AdvFilterParser extends SetUser {
// LIKE
if (op.equalsIgnoreCase(ParseHelper.LK) || op.equalsIgnoreCase(ParseHelper.NLK)) {
value = '%' + value + '%';
} else if (op.equalsIgnoreCase(ParseHelper.LK1)) {
value = '%' + value;
} else if (op.equalsIgnoreCase(ParseHelper.LK2)) {
value = value + '%';
}
sb.append(quoteValue(value, lastFieldMeta.getType()));
}
@ -781,13 +766,57 @@ public class AdvFilterParser extends SetUser {
/**
* 快速查询
*
*/
private void rebuildQuickFilter38() {
String quickFields = filterExpr.getString("quickFields");
JSONArray quickItems = buildQuickFilterItems(quickFields, 1);
JSONObject values = filterExpr.getJSONObject("values");
final String quickValue = values.values().iterator().next().toString();
// eg: =完全相等, *后匹配, 前匹配*
if (quickValue.length() > 2
&& (quickValue.startsWith("=") || quickValue.startsWith("*") || quickValue.endsWith("*"))) {
String op2 = ParseHelper.EQ;
String value2;
if (quickValue.startsWith("*")) op2 = ParseHelper.LK1;
else if (quickValue.endsWith("*")) op2 = ParseHelper.LK2;
if (quickValue.endsWith("*")) value2 = quickValue.substring(0, quickValue.length() - 1);
else value2 = quickValue.substring(1);
for (Object o : quickItems) {
JSONObject item = (JSONObject) o;
item.put("op", op2);
item.put("value", value2);
}
} else {
// v3.6-b4,3.7: 多值查询转义可输入 \|eg: 值1|值2
String[] m = quickValue.split("(?<!\\\\)\\|");
if (m.length > 1) {
values.clear();
values.put("1", m[0].trim());
for (int i = 2; i <= m.length; i++) {
JSONArray quickItemsPlus = buildQuickFilterItems(quickFields, i);
values.put(String.valueOf(i), m[i - 1].trim());
quickItems.addAll(quickItemsPlus);
}
filterExpr.put("values", values);
}
}
// 覆盖
filterExpr.put("items", quickItems);
}
/**
* @param quickFields
* @param valueIndex
* @return
*/
private JSONArray buildQuickFilterItems(String quickFields, int valueIndex) {
Set<String> usesFields = ParseHelper.buildQuickFields(rootEntity, quickFields);
JSONArray items = new JSONArray();
for (String field : usesFields) {
items.add(JSON.parseObject("{ op:'LK', value:'{" + valueIndex + "}', field:'" + field + "' }"));

View file

@ -43,6 +43,8 @@ public class ParseHelper {
public static final String NL = "NL";
public static final String NT = "NT";
public static final String LK = "LK";
public static final String LK1 = "LK1"; // *后匹配
public static final String LK2 = "LK2"; // 前匹配*
public static final String NLK = "NLK";
public static final String IN = "IN";
public static final String NIN = "NIN";
@ -132,7 +134,7 @@ public class ParseHelper {
return "is null";
} else if (NT.equalsIgnoreCase(token)) {
return "is not null";
} else if (LK.equalsIgnoreCase(token)) {
} else if (LK.equalsIgnoreCase(token) || LK1.equalsIgnoreCase(token) || LK2.equalsIgnoreCase(token)) {
return "like";
} else if (NLK.equalsIgnoreCase(token)) {
return "not like";
@ -214,7 +216,7 @@ public class ParseHelper {
if (dt == DisplayType.REFERENCE) {
Field nameField = field.getReferenceEntity().getNameField();
if (nameField.getType() == FieldType.REFERENCE) {
log.warn("Quick field cannot be circular-reference : " + nameField);
log.warn("Quick field cannot be circular-reference : {}", nameField);
return null;
}
@ -269,7 +271,7 @@ public class ParseHelper {
if (can != null) usesFields.add(can);
} else {
log.warn("No field found in `quickFields` : " + field + " in " + entity.getName());
log.warn("No field found in `quickFields` : {} in {}", field, entity.getName());
}
}
}
@ -302,7 +304,7 @@ public class ParseHelper {
}
if (usesFields.isEmpty()) {
log.warn("No fields of search found : " + usesFields);
log.warn("No fields of search found : {}", usesFields);
}
return usesFields;
}

View file

@ -97,7 +97,8 @@ public class CommonOperatingController extends BaseController {
Entity entityMate = MetadataHelper.getEntity(entity);
if (StringUtils.isBlank(fields)) fields = getAllFields(entityMate);
String sql = String.format("select %s from %s", fields, entityMate.getName());
String sql = String.format("select %s from %s",
StringUtils.join(fields.split("[,;]"), ","), entityMate.getName());
if (ParseHelper.validAdvFilter(filter)) {
String filterWhere = new AdvFilterParser(filter, entityMate).toSqlWhere();
if (filterWhere != null) sql += " where " + filterWhere;
@ -112,14 +113,16 @@ public class CommonOperatingController extends BaseController {
// 获取全部字段
private String getAllFields(Entity entity) {
List<String> ss = new ArrayList<>();
List<String> fs = new ArrayList<>();
for (Field field : entity.getFields()) {
if (!MetadataHelper.isSystemField(field.getName())) ss.add(field.getName());
if (!MetadataHelper.isSystemField(field.getName())) fs.add(field.getName());
}
return StringUtils.join(ss, ",");
return StringUtils.join(fs, ",");
}
/**
* 保存记录
*
* @param record
* @return
*/
@ -134,6 +137,8 @@ public class CommonOperatingController extends BaseController {
}
/**
* 删除记录
*
* @param recordId
* @return
*/

View file

@ -50,6 +50,8 @@ import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Set;
import static com.rebuild.core.privileges.bizz.ZeroEntry.AllowRevokeApproval;
/**
* @author devezhao zhaofang123@gmail.com
* @since 2019/07/05
@ -117,8 +119,9 @@ public class ApprovalController extends BaseController {
if (user.equals(ApprovalHelper.getSubmitter(recordId, useApproval))) {
data.put("canUrge", true);
data.put("canCancel", true);
} else if (UserHelper.isAdmin(user)) {
// v3.1 管理员也可撤回
} else if (Application.getPrivilegesManager().allow(user, AllowRevokeApproval)) {
// v3.1 管理员可撤回
// v3.8 有权限的可撤回
data.put("canCancel", true);
}
@ -128,8 +131,8 @@ public class ApprovalController extends BaseController {
}
if (stateVal == ApprovalState.APPROVED.getState()) {
// v3.4
data.put("canRevoke", Application.getPrivilegesManager().allow(user, ZeroEntry.AllowRevokeApproval));
// v3.4 有权限的可撤销
data.put("canRevoke", Application.getPrivilegesManager().allow(user, AllowRevokeApproval));
}
}

View file

@ -177,7 +177,8 @@ public class LoginAction extends BaseController {
uaSimple = "UNKNOW";
}
String ipAddr = StringUtils.defaultString(ServletUtils.getRemoteAddr(request), "127.0.0.1");
final String ipAddr = StringUtils.defaultString(ServletUtils.getRemoteAddr(request), "127.0.0.1");
final String reqUrl = request.getRequestURL() == null ? "" : request.getRequestURL().toString();
final Record llog = EntityHelper.forNew(EntityHelper.LoginLog, UserService.SYSTEM_USER);
llog.setID("user", user);
@ -191,10 +192,10 @@ public class LoginAction extends BaseController {
User u = Application.getUserStore().getUser(user);
String uid = StringUtils.defaultString(u.getEmail(), u.getName());
if (uid == null) uid = user.toLiteral();
String uaUrl = String.format("api/authority/user/echo?user=%s&ip=%s&ua=%s&source=%s",
CodecUtils.base64UrlEncode(uid), ipAddr, CodecUtils.urlEncode(userAgent),
CodecUtils.base64UrlEncode(request.getRequestURL().toString()));
CodecUtils.base64UrlEncode(reqUrl));
License.siteApiNoCache(uaUrl);
});
}

View file

@ -267,7 +267,7 @@
</tr>
<tr>
<td class="name">
<a data-name="AllowRevokeApproval">[[${bundle.L('允许撤销审批')}]]</a>
<a data-name="AllowRevokeApproval">[[${bundle.L('允许撤回、撤销审批')}]]</a>
<sup class="rbv"></sup>
</td>
<td><i data-action="Z" class="priv R0"></i></td>

View file

@ -104,11 +104,14 @@
<span>[[${bundle.L('执行')}]]</span>
<input type="text" class="J_whenTimer2" placeholder="1" />
<span>[[${bundle.L('次')}]]</span>
<span class="ml-2">[[${bundle.L('执行时段')}]]</span>
<select class="J_startHour1"></select>
<span>~</span>
<select class="J_startHour2"></select>
<span class="bosskey-show">
<span class="ml-2">[[${bundle.L('执行日期')}]] (LAB)</span>
<input type="text" class="J_whenTimer4 w-auto" placeholder="eg. 1,2,15,30" />
</span>
</div>
<p class="form-text">[[${bundle.L('具体执行时间将在你设定的周期内平均分布。例如每天执行 2 次,其执行时间为 00:00 和 12:00')}]]</p>
<div class="eval-exec-times"></div>

View file

@ -190,6 +190,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
.dataTables_oper.invisible2 > .btn.J_view {
display: inline-block;
margin-right: 0;
}
.record-merge-table {

View file

@ -1833,7 +1833,7 @@ th.column-fixed {
}
.column-fixed.col-action .btn.btn-sm {
line-height: 28px;
line-height: 26px;
min-width: 32px;
}

View file

@ -807,6 +807,7 @@ body {
.timeline.approved-steps .timeline-item.state0::before {
border-color: #fff;
background-color: #4285f4;
background-color: #33a451;
}
.timeline.approved-steps .timeline-item.state10::before {
@ -1149,6 +1150,7 @@ body {
font-size: 0;
background-color: #f7b904;
border: 4px solid #fff;
border: 6px double #fff;
}
.sop-steps li.state-2 > div > span {

View file

@ -5,9 +5,10 @@ rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
const userId = window.__PageConfig.recordId
const wpc = window.__PageConfig || {}
const userId = wpc.recordId
$(document).ready(function () {
$(document).ready(() => {
$('.J_delete')
.off('click')
.on('click', () => {
@ -154,6 +155,22 @@ $(document).ready(function () {
}
})
}
// v3.8
if (userId === '001-0000000000000001') {
const RbForm_renderAfter = RbForm.renderAfter
RbForm.renderAfter = function (formObject) {
typeof RbForm_renderAfter === 'function' && RbForm_renderAfter()
formObject.onFieldValueChange((nv) => {
if (nv.name === 'isDisabled') {
let c = formObject.getFieldComp('isDisabled') || {}
if (nv.value === 'T') c.setTip(<span className="text-warning">{$L('禁用将导致超级管理员无法登录')}</span>)
else c.setTip(null)
}
})
}
}
})
// 启用/禁用

View file

@ -47,9 +47,12 @@ class ApprovalProcessor extends React.Component {
window.RbViewPage && window.RbViewPage.setReadonly(true)
let aMsg = $L('当前记录正在审批中')
let imApproverCurrent = false
if (this.state.imApprover) {
if (this.state.imApproveSatate === 1) aMsg = $L('当前记录正在等待你审批')
else if (this.state.imApproveSatate === 10) aMsg = $L('你已审批同意正在等待其他人审批')
if (this.state.imApproveSatate === 1) {
aMsg = $L('当前记录正在等待你审批')
imApproverCurrent = true
} else if (this.state.imApproveSatate === 10) aMsg = $L('你已审批同意正在等待其他人审批')
else if (this.state.imApproveSatate === 11) aMsg = $L('你已驳回审批')
}
@ -61,19 +64,15 @@ class ApprovalProcessor extends React.Component {
{$L('审批')}
</button>
)}
{(this.state.canCancel || this.state.canUrge) && (
<RF>
{this.state.canUrge && (
<button className="btn btn-secondary" onClick={this.urge}>
{$L('催审')}
</button>
)}
{this.state.canCancel && (
<button className="btn btn-secondary" onClick={this.cancel}>
{$L('撤回')}
</button>
)}
</RF>
{this.state.canUrge && imApproverCurrent === false && (
<button className="btn btn-secondary" onClick={this.urge}>
{$L('催审')}
</button>
)}
{this.state.canCancel && (
<button className="btn btn-secondary" onClick={this.cancel}>
{$L('撤回')}
</button>
)}
{this.state.canCancel38 && (
<button className="btn btn-secondary bosskey-show" onClick={this.cancel38}>

View file

@ -912,7 +912,8 @@ class RbList extends React.Component {
render() {
const lastIndex = this.state.fields.length
const rowActions = window.FrontJS ? window.FrontJS.DataList.__rowActions : []
let rowActions = window.FrontJS ? window.FrontJS.DataList.__rowActions : []
if (wpc.type !== 'RecordList') rowActions = []
return (
<RF>

View file

@ -76,7 +76,7 @@ const RbListPage = {
// Privileges
if (ep) {
if (ep.C === false) $('.J_new').remove()
if (ep.C === false) $('.J_new, .J_new_group').remove()
if (ep.D === false) $('.J_delete').remove()
if (ep.U === false) $('.J_edit, .J_batch-update').remove()
if (ep.A !== true) $('.J_assign').remove()

View file

@ -1148,7 +1148,10 @@ class RbFormElement extends React.Component {
}
// 可空/非空
setNullable(nullable) {
this.setState({ nullable: nullable === true })
this.setState({ nullable: nullable === true }, () => {
// fix:v3.8 通过此方法强制检查非空属性
this.setValue(this.state.value || null)
})
}
// 只读/非只读
// 部分字段有效且如字段属性为只读即使填写值也无效

View file

@ -19,6 +19,7 @@ class RbViewForm extends React.Component {
this.onViewEditable = this.props.onViewEditable
if (this.onViewEditable) this.onViewEditable = wpc.onViewEditable !== false
if (window.__LAB_VIEWEDITABLE === false) this.onViewEditable = false
// temp for `saveSingleFieldValue`
this.__FormData = {}
}

View file

@ -219,6 +219,7 @@ class MediaCapturer extends RbModal {
ctx2d.fillStyle = 'white'
ctx2d.fillText(moment().format('YYYY-MM-DD HH:mm:ss'), 20, 40)
ctx2d.fillText('Device : ' + this.__currentDeviceId || '', 20, 40 + 30)
ctx2d.fillText('User : ***' + rb.currentUser.substr(7), 20, 40 + 30 + 30)
}
this._capturedData = this._$resImage.toDataURL('image/jpeg', 1.0)

View file

@ -46,6 +46,8 @@ $(document).ready(() => {
// v2.9
if (wt[2]) $('.J_startHour1').val(wt[2])
if (wt[3]) $('.J_startHour2').val(wt[3])
// v3.8
if (wt[4]) $('.J_whenTimer4').val(wt[4]).parents('.bosskey-show').removeClass('bosskey-show')
$('.J_whenTimer1').trigger('change')
}
@ -55,7 +57,7 @@ $(document).ready(() => {
// 评估具体执行时间
function evalTriggerTimes() {
const whenTimer = `${$('.J_whenTimer1').val() || 'D'}:${$('.J_whenTimer2').val() || 1}:${$('.J_startHour1').val() || 0}:${$('.J_startHour2').val() || 23}`
const whenTimer = `${$('.J_whenTimer1').val() || 'D'}:${$('.J_whenTimer2').val() || 1}:${$('.J_startHour1').val() || 0}:${$('.J_startHour2').val() || 23}:${$('.J_whenTimer4').val() || ''}`
$.get(`/admin/robot/trigger/eval-trigger-times?whenTimer=${whenTimer}`, (res) => {
renderRbcomp(
<RbAlertBox
@ -141,7 +143,7 @@ $(document).ready(() => {
return
}
const whenTimer = `${$('.J_whenTimer1').val() || 'D'}:${$('.J_whenTimer2').val() || 1}:${$('.J_startHour1').val() || 0}:${$('.J_startHour2').val() || 23}`
const whenTimer = `${$('.J_whenTimer1').val() || 'D'}:${$('.J_whenTimer2').val() || 1}:${$('.J_startHour1').val() || 0}:${$('.J_startHour2').val() || 23}:${$('.J_whenTimer4').val() || ''}`
const content = contentComp.buildContent()
if (content === false) return

View file

@ -93,7 +93,7 @@
<div class="dataTables_oper invisible2">
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon mdi mdi-folder-open"></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">
<div class="btn-group btn-space J_new_group">
<button class="btn btn-primary J_new" type="button"><i class="icon zmdi zmdi-plus"></i> [[${bundle.L('新建')}]]</button>
<button class="btn btn-primary dropdown-toggle w-auto hide" type="button" data-toggle="dropdown"><span class="icon zmdi zmdi-chevron-down"></span></button>
<div class="dropdown-menu dropdown-menu-primary dropdown-menu-right"></div>