Feat approve editable (#947)

* Refactor batch update editor and improve NTEXT handling

* Enable batch approval and update approval UI

* Update table and treemap chart rendering options

* Add editable record mode to approval flow nodes

* Enable editable approval records for approvers

* Allow editing details during approval process

* Refactor form modal creation and extra button logic
This commit is contained in:
REBUILD 企业管理系统 2025-08-16 16:57:06 +08:00 committed by GitHub
parent 31730c4464
commit 0c43b36419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 399 additions and 171 deletions

View file

@ -159,5 +159,6 @@ module.exports = {
marked: true,
$focus2End: true,
RecordSelectorModal: true,
$fetchMetaInfo: true,
},
}

2
@rbv

@ -1 +1 @@
Subproject commit ffe18eeaa8ae3a8acd3de09e1dbed1dbf994e9b4
Subproject commit 34e48060b157be1bfbfb69d05506816ccccfbd48

View file

@ -34,6 +34,7 @@ import com.rebuild.core.privileges.UserService;
import com.rebuild.core.privileges.bizz.Department;
import com.rebuild.core.privileges.bizz.User;
import com.rebuild.core.service.NoRecordFoundException;
import com.rebuild.core.service.approval.ApprovalHelper;
import com.rebuild.core.service.approval.ApprovalState;
import com.rebuild.core.service.approval.RobotApprovalManager;
import com.rebuild.core.service.general.GeneralEntityService;
@ -143,6 +144,7 @@ public class FormsBuilder extends FormsManager {
ApprovalState approvalState;
// 提示
String readonlyMessage = null;
String readonlywMessage = null; // 可强制编辑
// 判断表单权限
@ -153,10 +155,15 @@ public class FormsBuilder extends FormsManager {
Assert.notNull(mainid, "CALL `FormBuilderContextHolder#setMainIdOfDetail` FIRST!");
approvalState = EntityHelper.isUnsavedId(mainid) ? null : getHadApproval(hasMainEntity, mainid);
if ((approvalState == ApprovalState.PROCESSING || approvalState == ApprovalState.APPROVED)) {
readonlyMessage = approvalState == ApprovalState.APPROVED
? Language.L("主记录已审批完成,不能添加明细")
: Language.L("主记录正在审批中,不能添加明细");
if (approvalState == ApprovalState.APPROVED) {
readonlyMessage = Language.L("主记录已审批完成,不能添加明细");
} else if (approvalState == ApprovalState.PROCESSING) {
boolean allow42 = ApprovalHelper.isAllowEditableRecord(mainid, user);
if (allow42) {
readonlywMessage = Language.L("主记录正在审批中,审批人允许编辑");
} else {
readonlyMessage = Language.L("主记录正在审批中,不能添加明细");
}
}
// 明细无需审批
approvalState = null;
@ -193,7 +200,13 @@ public class FormsBuilder extends FormsManager {
if (approvalState == ApprovalState.APPROVED) {
readonlyMessage = Language.L("%s已审批完成不能编辑", recordType);
} else if (approvalState == ApprovalState.PROCESSING) {
readonlyMessage = Language.L("%s正在审批中不能编辑", recordType);
// v4.2
boolean allow42 = ApprovalHelper.isAllowEditableRecord(recordId, user);
if (allow42) {
readonlywMessage = Language.L("%s正在审批中审批人允许编辑", recordType);
} else {
readonlyMessage = Language.L("%s正在审批中不能编辑", recordType);
}
}
}
}
@ -311,7 +324,8 @@ public class FormsBuilder extends FormsManager {
}
}
if (readonlyMessage != null) model.set("readonlyMessage", readonlyMessage);
if (readonlywMessage != null) model.set("readonlywMessage", readonlywMessage);
else if (readonlyMessage != null) model.set("readonlyMessage", readonlyMessage);
// v3.4
String disabledViewEditable = EasyMetaFactory.valueOf(entityMeta)

View file

@ -12,6 +12,7 @@ import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.ConfigurationException;
@ -237,6 +238,38 @@ public class ApprovalHelper {
return null;
}
/**
* @param recordId
* @return
*/
public static FlowNode getCurrentFlowNode(ID recordId) {
ApprovalStatus s = getApprovalStatus(recordId);
return getFlowNode(s.getApprovalId(), s.getCurrentStepNode());
}
/**
* @param recordId
* @param user
* @return
*/
public static boolean isAllowEditableRecord(ID recordId, ID user) {
// 明细需要使用主记录判断
if (MetadataHelper.getEntity(recordId.getEntityCode()).getMainEntity() != null) {
recordId = QueryHelper.getMainIdByDetail(recordId);
}
ApprovalStatus s = getApprovalStatus(recordId);
FlowNode node = getFlowNode(s.getApprovalId(), s.getCurrentStepNode());
if (node == null || node.getEditableMode() != FlowNode.EDITABLE_MODE_RECORD) return false;
JSONArray current = new ApprovalProcessor(recordId, s.getApprovalId()).getCurrentStep(s);
for (Object o : current) {
JSONObject step = (JSONObject) o;
if (StringUtils.equalsIgnoreCase(user.toLiteral(), step.getString("approver"))) return true;
}
return false;
}
/**
* 获取审批超时时间
*

View file

@ -20,6 +20,7 @@ import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.privileges.bizz.Department;
import com.rebuild.utils.JSONUtils;
import lombok.Getter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
@ -66,6 +67,12 @@ public class FlowNode {
public static final String SIGN_OR = "OR"; // 或签
public static final String SIGN_ALL = "ALL"; // 逐个审批暂未用
// 可修改记录
public static final int EDITABLE_MODE_NONE = 0;
public static final int EDITABLE_MODE_RECORD = 1;
public static final int EDITABLE_MODE_FIELDS = 10;
// --
@Getter
@ -153,8 +160,9 @@ public class FlowNode {
* @return
*/
public boolean allowBatch() {
Boolean b = getDataMap().getBoolean("allowBatch");
return b != null && b;
// Boolean b = getDataMap().getBoolean("allowBatch");
// return b != null && b;
return true; // v4.2 全面允许
}
/**
@ -166,7 +174,7 @@ public class FlowNode {
*/
public Set<ID> getSpecUsers(ID operator, ID record) {
JSONArray userDefs = getDataMap().getJSONArray("users");
if (userDefs == null || userDefs.isEmpty()) return Collections.emptySet();
if (CollectionUtils.isEmpty(userDefs)) return Collections.emptySet();
String userType = userDefs.getString(0);
if (USER_SELF.equalsIgnoreCase(userType)) {
@ -247,7 +255,7 @@ public class FlowNode {
*/
public Set<String> getCcAccounts(ID record) {
JSONArray accountFields = getDataMap().getJSONArray("accounts");
if (accountFields == null || accountFields.isEmpty()) return Collections.emptySet();
if (CollectionUtils.isEmpty(accountFields)) return Collections.emptySet();
Entity useEntity = MetadataHelper.getEntity(record.getEntityCode());
List<String> useFields = new ArrayList<>();
@ -291,20 +299,32 @@ public class FlowNode {
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == null) return false;
return obj instanceof FlowNode && obj.hashCode() == this.hashCode();
}
/**
* 节点可修改记录
*
* @return 0=不可修改, 1=可修改, 10=可修改字段
* @see #getEditableFields()
*/
public int getEditableMode() {
if (getDataMap().containsKey("editableMode")) {
return getDataMap().getIntValue("editableMode");
}
// v4.2 兼容
return CollectionUtils.isEmpty(getEditableFields()) ? EDITABLE_MODE_NONE : EDITABLE_MODE_FIELDS;
}
/**
* 节点可编辑字段
*
* @return
*/
public JSONArray getEditableFields() {
JSONArray editableFields = dataMap == null ? null : dataMap.getJSONArray("editableFields");
if (editableFields == null) return null;
JSONArray editableFields = getDataMap().getJSONArray("editableFields");
if (CollectionUtils.isEmpty(editableFields)) return JSONUtils.EMPTY_ARRAY;
editableFields = (JSONArray) JSONUtils.clone(editableFields);
for (Object o : editableFields) {
@ -321,7 +341,7 @@ public class FlowNode {
* @return
*/
public JSONObject getExpiresAuto() {
JSONObject expiresAuto = dataMap == null ? null : dataMap.getJSONObject("expiresAuto");
JSONObject expiresAuto = getDataMap().getJSONObject("expiresAuto");
if (expiresAuto == null) return null;
if (expiresAuto.getIntValue("expiresAuto") <= 0) return null;
return expiresAuto;

View file

@ -68,16 +68,12 @@ public class RobotApprovalManager implements ConfigManager {
// 实体的
FlowDefinition[] defs = getFlowDefinitions(entity);
for (FlowDefinition def : defs) {
if (!def.isDisabled()) {
return ApprovalState.DRAFT;
}
for (FlowDefinition d : defs) {
if (!d.isDisabled()) return ApprovalState.DRAFT;
}
return null;
}
/**
* 获取实体是否有流程
*

View file

@ -19,7 +19,6 @@ import org.apache.commons.lang3.ArrayUtils;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@ -113,7 +112,13 @@ public class TableChart extends ChartData {
}
String tableHtml = new TableBuilder(this, dataRaw).toHTML();
return JSONUtils.toJSONObject("html", tableHtml);
JSONObject renderOption = config.getJSONObject("option");
if (renderOption == null) renderOption = new JSONObject();
return JSONUtils.toJSONObject(
new String[]{"html", "_renderOption"},
new Object[]{tableHtml, renderOption});
}
protected boolean isShowLineNumber() {

View file

@ -48,7 +48,8 @@ public class TreemapChart extends ChartData {
JSONObject renderOption = config.getJSONObject("option");
if (renderOption == null) renderOption = new JSONObject();
renderOption.put("dataFlags", new String[] { getNumericalFlag(num1) });
renderOption.put("dataFlags", new String[]{getNumericalFlag(num1)});
renderOption.remove("useBgcolor");
return JSONUtils.toJSONObject(
new String[]{"data", "xLabel", "xAmount", "_renderOption"},

View file

@ -610,10 +610,13 @@ public class GeneralEntityService extends ObservableService implements EntitySer
}
ApprovalState state = ApprovalHelper.getApprovalState(dtmFieldValue);
if (state == ApprovalState.APPROVED || state == ApprovalState.PROCESSING) {
throw new DataSpecificationException(state == ApprovalState.APPROVED
? Language.L("主记录已审批完成,不能添加明细")
: Language.L("主记录正在审批中,不能添加明细"));
if (state == ApprovalState.APPROVED) {
throw new DataSpecificationException(Language.L("主记录已审批完成,不能添加明细"));
} else if (state == ApprovalState.PROCESSING) {
boolean allow42 = ApprovalHelper.isAllowEditableRecord(dtmFieldValue, getCurrentUser());
if (!allow42) {
throw new DataSpecificationException(Language.L("主记录正在审批中,不能添加明细"));
}
}
}
@ -645,6 +648,13 @@ public class GeneralEntityService extends ObservableService implements EntitySer
boolean unallow = false;
if (action == BizzPermission.DELETE) {
unallow = currentState == ApprovalState.APPROVED || currentState == ApprovalState.PROCESSING;
// v4.2 允许修改记录
if (unallow && currentState == ApprovalState.PROCESSING) {
boolean allow42 = ApprovalHelper.isAllowEditableRecord(checkRecordId, getCurrentUser());
if (allow42) unallow = false;
}
} else if (action == BizzPermission.UPDATE) {
unallow = currentState == ApprovalState.APPROVED || currentState == ApprovalState.PROCESSING;
@ -659,6 +669,15 @@ public class GeneralEntityService extends ObservableService implements EntitySer
boolean forceUpdate = GeneralEntityServiceContextHolder.isAllowForceUpdate(false);
if (forceUpdate) unallow = false;
}
// v4.2 允许修改记录
if (unallow) {
boolean is = ApprovalHelper.isAllowEditableRecord(checkRecordId, getCurrentUser());
if (is) {
unallow = false;
GeneralEntityServiceContextHolder.setAllowForceUpdate(checkRecordId);
}
}
}
if (unallow) {

View file

@ -38,6 +38,7 @@ import com.rebuild.core.service.trigger.DataValidateException;
import com.rebuild.core.support.RbvFunction;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
import com.rebuild.web.EntityParam;
import com.rebuild.web.IdParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
@ -85,11 +86,28 @@ public class ApprovalController extends BaseController {
return res;
}
@GetMapping("alist")
public RespBody getApprovalList(HttpServletRequest request, @EntityParam Entity entity) {
boolean valid = getBoolParameter(request, "valid");
FlowDefinition[] defs = RobotApprovalManager.instance.getFlowDefinitions(entity);
List<Object> res = new ArrayList<>();
for (FlowDefinition d : defs) {
if (d.isDisabled()) continue;
// 仅返回可用的
if (valid && !d.isWorkable()) continue;
res.add(JSONUtils.toJSONObject(new String[]{"id", "text"},
new Object[]{d.getID("id"), d.getString("name")}));
}
return RespBody.ok(res);
}
@GetMapping("state")
public RespBody getApprovalState(HttpServletRequest request, @IdParam(name = "record") ID recordId) {
final Entity approvalEntity = MetadataHelper.getEntity(recordId.getEntityCode());
if (!MetadataHelper.hasApprovalField(approvalEntity)) {
return RespBody.error("NOT AN APPROVAL ENTITY");
return RespBody.error("NONE APPROVAL ENTITY");
}
final ID user = getRequestUser(request);
@ -189,10 +207,14 @@ public class ApprovalController extends BaseController {
if (reqType < 2) data.put("remarkReq", reqType);
else data.put("remarkReq", expTime == null || expTime < 0 ? 0 : 1);
// 可修改字段
JSONArray editableFields = currentFlowNode.getEditableFields();
if (editableFields != null && !editableFields.isEmpty()) {
data.putAll(new EditableFields(editableFields).buildForms(recordId, user));
// 可修改记录
int editableMode = currentFlowNode.getEditableMode();
data.put("editableMode", editableMode);
if (editableMode ==FlowNode.EDITABLE_MODE_FIELDS) {
JSONArray editableFields = currentFlowNode.getEditableFields();
if (!CollectionUtils.isEmpty(editableFields)) {
data.putAll(new EditableFields(editableFields).buildForms(recordId, user));
}
}
return data;

View file

@ -1,36 +0,0 @@
/*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.web.robot.trigger;
import com.alibaba.fastjson.JSON;
import com.rebuild.core.Application;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @author devezhao
* @since 2019/05/25
*/
@RestController
@RequestMapping("/admin/robot/trigger/")
public class AutoApprovalController extends BaseController {
@RequestMapping("auto-approval-alist")
public JSON approvalList(HttpServletRequest request) {
Object[][] array = Application.createQueryNoFilter(
"select configId,name from RobotApprovalConfig where belongEntity = ? and isDisabled = ? order by name")
.setParameter(1, getParameterNotNull(request, "entity"))
.setParameter(2, false)
.array();
return JSONUtils.toJSONObjectArray(new String[] { "id", "text" }, array);
}
}

View file

@ -714,7 +714,8 @@ See LICENSE and COMMERCIAL in the project root for license information.
width: 100%;
}
.expires-notify-set {
.expires-notify-set,
.editable-mode-set {
background-color: #eee;
padding: 20px;
padding-top: 15px;

View file

@ -233,6 +233,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
background-color: #eceff1;
border-width: 1px;
font-weight: normal;
font-weight: bold;
}
.chart.ctable .table.line-number thead th:first-child,
@ -309,6 +310,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
padding-top: 0;
padding-bottom: 6px;
font-weight: normal;
font-weight: bold;
}
.chart.ApprovalList .table td,
@ -744,6 +746,14 @@ See LICENSE and COMMERCIAL in the project root for license information.
right: 0;
}
.chart-box.TABLE .chart.ctable .table {
background-color: transparent;
}
.chart-box.TABLE.gradient-bg .chart.ctable .table th {
background-color: rgba(255, 255, 255, 0.2);
}
/* gradient-backgrounds.css */
.gradient-bg-1 {

View file

@ -918,6 +918,7 @@ body.view-body {
textarea.row1x {
height: 37px !important;
resize: none;
padding-top: 7px;
}
textarea.row2x {

View file

@ -221,10 +221,10 @@ class SimpleNode extends NodeSpec {
if (this.nodeType === 'approver') {
if (data.allowReferral) descs.push($L('允许转审'))
if (data.allowCountersign) descs.push($L('允许加签'))
if (data.allowBatch) descs.push($L('允许批量'))
// if (data.allowBatch) descs.push($L('允许批量'))
descs.push(data.signMode === 'AND' ? $L('会签') : data.signMode === 'ALL' ? $L('依次审批') : $L('或签'))
if (data.expiresAuto && ~~data.expiresAuto.expiresAuto > 0) descs.push($L('限时审批'))
if (data.editableFields && data.editableFields.length > 0) descs.push($L('可修改字段'))
if (~~data.editableMode > 0) descs.push($L('可修改记录'))
} else if (this.nodeType === 'start') {
if (data.unallowCancel) descs.push($L('禁止撤回'))
}
@ -635,6 +635,10 @@ class ApproverNodeConfig extends StartNodeConfig {
if (!props.users || props.users.length === 0) this.state.users = 'SPEC'
else if (props.users[0] === 'SELF') this.state.users = 'SELF'
else this.state.users = 'SPEC'
// v4.2 兼容
if (props.editableFields && props.editableFields.length > 0 && !props.editableMode) {
this.state.editableMode = '10'
}
}
render() {
@ -682,7 +686,7 @@ class ApproverNodeConfig extends StartNodeConfig {
</span>
</label>
</div>
<div className="form-group mb-0">
<div className="form-group mb-0 hide disabled-on-4.2">
<label className="custom-control custom-control-sm custom-checkbox">
<input className="custom-control-input" type="checkbox" name="allowBatch" checked={this.state.allowBatch === true} onChange={this.handleChange} />
<span className="custom-control-label">
@ -804,43 +808,57 @@ class ApproverNodeConfig extends StartNodeConfig {
</div>
</div>
<div className="form-group mt-5">
<label className="text-bold">{$L('可修改字段')}</label>
<div style={{ position: 'relative' }}>
<table className={`table table-sm fields-table ${(this.state.editableFields || []).length === 0 && 'hide'}`}>
<tbody ref={(c) => (this._$editableFields = c)}>
{(this.state.editableFields || []).map((item) => {
return (
<tr key={`field-${item.field}`}>
<td>{this.__fieldLabel(item.field)}</td>
<td width="140" data-field={item.field}>
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input className="custom-control-input" type="checkbox" name="notNull" defaultChecked={item.notNull === true} />
<span className="custom-control-label">{$L('必填')}</span>
</label>
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline ml-3">
<input className="custom-control-input" type="checkbox" name="readOnly" defaultChecked={item.readOnly === true} />
<span className="custom-control-label">{$L('只读')}</span>
</label>
</td>
<td width="40">
<a className="close" title={$L('移除')} onClick={() => this.removeEditableField(item.field)}>
<i className="zmdi icon zmdi-close" />
</a>
</td>
</tr>
)
})}
</tbody>
</table>
<div className="pb-4">
<button className="btn btn-secondary btn-sm" onClick={() => renderRbcomp(<DlgFields selected={this.state.editableFields} call={(fs) => this.setEditableFields(fs)} />)}>
+ {$L('选择字段')}
</button>
<div className="form-group mt-5 pb-5">
<label className="text-bold">{$L('修改记录')}</label>
<div className="row">
<div className="col">
<select className="form-control form-control-sm" name="editableMode" defaultValue={this.state.editableMode || null} onChange={this.handleChange}>
<option value="0">{$L('不可修改')}</option>
<option value="1">{$L('可修改')}</option>
<option value="10">{$L('可修改指定字段')} </option>
</select>
</div>
<div className="col pl-0" />
</div>
<div className={`editable-mode-set mt-3 ${~~this.state.editableMode !== 10 && 'hide'}`}>
<label className="text-bold">{$L('指定字段')}</label>
<div className="position-relative">
<table className={`table table-sm fields-table ${(this.state.editableFields || []).length === 0 && 'hide'}`}>
<tbody ref={(c) => (this._$editableFields = c)}>
{(this.state.editableFields || []).map((item) => {
return (
<tr key={`field-${item.field}`}>
<td width="57%">{this.__fieldLabel(item.field)}</td>
<td width="35%" data-field={item.field}>
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input className="custom-control-input" type="checkbox" name="notNull" defaultChecked={item.notNull === true} />
<span className="custom-control-label">{$L('必填')}</span>
</label>
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline ml-3">
<input className="custom-control-input" type="checkbox" name="readOnly" defaultChecked={item.readOnly === true} />
<span className="custom-control-label">{$L('只读')}</span>
</label>
</td>
<td width="8%">
<a className="close" title={$L('移除')} onClick={() => this.removeEditableField(item.field)}>
<i className="zmdi icon zmdi-close" />
</a>
</td>
</tr>
)
})}
</tbody>
</table>
<div>
<button className="btn btn-secondary btn-sm" onClick={() => renderRbcomp(<DlgFields selected={this.state.editableFields} call={(fs) => this.setEditableFields(fs)} />)}>
+ {$L('选择字段')}
</button>
</div>
</div>
</div>
</div>
</div>
{this.renderButton()}
</div>
)
@ -907,14 +925,6 @@ class ApproverNodeConfig extends StartNodeConfig {
}
save = () => {
const editableFields = []
$(this._$editableFields)
.find('td[data-field]')
.each(function () {
const $this = $(this)
editableFields.push({ field: $this.data('field'), notNull: $this.find('input:eq(0)').prop('checked'), readOnly: $this.find('input:eq(1)').prop('checked') })
})
const expiresAuto = {}
$(this._$expiresAuto)
.find('input, select, textarea')
@ -929,11 +939,27 @@ class ApproverNodeConfig extends StartNodeConfig {
return
}
const editableFields = []
if (~~this.state.editableMode === 10) {
$(this._$editableFields)
.find('td[data-field]')
.each(function () {
const $this = $(this)
editableFields.push({ field: $this.data('field'), notNull: $this.find('input:eq(0)').prop('checked'), readOnly: $this.find('input:eq(1)').prop('checked') })
})
if (editableFields.length === 0) {
RbHighbar.create($L('请指定可修改字段'))
return
}
}
const d = {
nodeName: this.state.nodeName,
users: this.state.users === 'SPEC' ? this._UserSelector.getSelected() : [this.state.users],
signMode: this.state.signMode,
selfSelecting: this.state.selfSelecting,
editableMode: this.state.editableMode,
editableFields: editableFields,
allowReferral: this.state.allowReferral,
allowCountersign: this.state.allowCountersign,

View file

@ -52,7 +52,9 @@ class BaseChart extends React.Component {
)
return (
<div className={`chart-box ${this.props.type} ${this.state.useBgcolor && `gradient-bg-${this.state.useBgcolor}`} ${this.props.type === 'DATALIST2' && 'DataList'}`} ref={(c) => (this._$box = c)}>
<div
className={`chart-box ${this.props.type} ${this.state.useBgcolor && `gradient-bg gradient-bg-${this.state.useBgcolor}`} ${this.props.type === 'DATALIST2' && 'DataList'}`}
ref={(c) => (this._$box = c)}>
<div className="chart-head">
<div className="chart-title text-truncate">{this.state.title}</div>
{opActions}

View file

@ -484,6 +484,7 @@ class ApprovalApproveForm extends ApprovalUsersForm {
)}
{(this.state.aform || this.state.aform_details) && this.renderLiteForm()}
{this.state.editableMode === 1 && this.renderEditable()}
<div className="form-group">
<label>
@ -559,6 +560,44 @@ class ApprovalApproveForm extends ApprovalUsersForm {
)
}
renderEditable() {
return (
<div className="form-group">
<label>{$L('信息完善')}</label>
<div>
<button
type="button"
className="btn btn-primary btn-outline"
onClick={() => {
$fetchMetaInfo(this.props.entity, (res) => {
const editProps = {
entity: res.entity,
title: $L('编辑%s', res.entityLabel),
icon: res.icon,
id: this.props.id,
postAfter: (recordId, next, formObject) => {
// 刷新列表
const rlp = window.RbListPage || parent.RbListPage
if (rlp) rlp.reload(recordId)
RbAlert.create($L('数据可能已更改是否需要刷新页面'), {
onConfirm: () => {
// 刷新视图
if (window.RbViewPage) window.RbViewPage.reload()
},
})
},
}
RbFormModal.create(editProps, true)
})
}}>
<i className="icon zmdi zmdi-edit mr-1" />
{$L('编辑')}
</button>
</div>
</div>
)
}
componentDidMount = () => this.getNextStep()
reload = () => this.getNextStep()
@ -632,7 +671,7 @@ class ApprovalApproveForm extends ApprovalUsersForm {
}
// v4.0
if (this.state.remarkReq >= 1 && $empty(data.remark)) {
RbHighbar.createl('填写批注')
RbHighbar.createl('输入批注')
return false
}

View file

@ -419,7 +419,7 @@ class BatchUpdate extends BatchOperator {
<span className="badge badge-warning">{field.label}</span>
</div>
<div className="col-2 pl-0 pr-0">
<span className="badge badge-warning">{BUE_OPTYPES[item.op]}</span>
<span className="badge badge-warning">{BU_OPS[item.op]}</span>
</div>
<div className="col-6">
{item.op !== 'NULL' && <span className="badge badge-warning text-break text-left">{FieldValueSet.formatFieldText(item.value, field)}</span>}
@ -433,7 +433,7 @@ class BatchUpdate extends BatchOperator {
})}
</div>
<div className="batch-editor">
{this.state.fields && <BatchUpdateEditor ref={(c) => (this._editor = c)} fields={this.state.fields} entity={this.props.entity} />}
{this.state.fields && <BatchUpdateEntry ref={(c) => (this._buEntry = c)} fields={this.state.fields} entity={this.props.entity} />}
<div className="mt-1">
<button className="btn btn-primary btn-sm btn-outline" onClick={() => this.addItem()} type="button">
+ {$L('添加')}
@ -446,7 +446,7 @@ class BatchUpdate extends BatchOperator {
}
addItem() {
const item = this._editor.buildItem()
const item = this._buEntry.buildItem()
if (!item) return
const contents = this.state.updateContents || []
@ -537,19 +537,22 @@ class BatchUpdate extends BatchOperator {
}
}
// ~ 批量修改编辑器
const BUE_OPTYPES = {
const BU_OPS = {
SET: $L('修改为'),
NULL: $L('置空'),
// TODO 支持更多修改模式
// 250813 也可以触发器修改
// PREFIX: $L('前添加'),
// SUFFIX: $L('后添加'),
// REPLACE: $L('替换'),
// PLUS: $L('加上'),
// MINUS: $L('减去'),
// MULTIPLY: $L('乘以'),
// DIVIDE: $L('除以'),
}
class BatchUpdateEditor extends React.Component {
// 批量修改编辑器
class BatchUpdateEntry extends React.Component {
state = { ...this.props, selectOp: 'SET' }
componentDidMount() {
@ -598,8 +601,8 @@ class BatchUpdateEditor extends React.Component {
</div>
<div className="col-2 pl-0 pr-0">
<select className="form-control form-control-sm" ref={(c) => (this._$op = c)}>
<option value="SET">{BUE_OPTYPES['SET']}</option>
<option value="NULL">{BUE_OPTYPES['NULL']}</option>
<option value="SET">{BU_OPS['SET']}</option>
<option value="NULL">{BU_OPS['NULL']}</option>
</select>
<span className="text-muted">{$L('修改方式')}</span>
</div>
@ -636,7 +639,10 @@ class BatchUpdateEditor extends React.Component {
}
data.value = this._FieldValue.val()
if (!data.value) {
if (data.value === false) {
// 格式不正确
return null
} else if (!data.value) {
RbHighbar.create($L('请填写新值'))
return null
} else {
@ -661,10 +667,11 @@ class BatchApprove extends BatchOperator {
}
renderOperator() {
const approveState = ~~this.state.approveState
return (
<div>
<div className="form-group">
<label className="text-bold">{$L('审批结果')}</label>
<label className="text-bold">{$L('审批方式')}</label>
<div>
<label className="custom-control custom-control-sm custom-radio custom-control-inline mb-0">
<input className="custom-control-input" type="radio" name="approveState" value="10" onClick={this.handleChange} />
@ -674,36 +681,73 @@ class BatchApprove extends BatchOperator {
<input className="custom-control-input" type="radio" name="approveState" value="11" onClick={this.handleChange} />
<span className="custom-control-label">{$L('驳回')}</span>
</label>
<label className="custom-control custom-control-sm custom-radio custom-control-inline mb-0">
<input className="custom-control-input" type="radio" name="approveState" value="1" onClick={this.handleChange} />
<span className="custom-control-label">{$L('提交')}</span>
</label>
</div>
</div>
<div className="form-group">
<div className={`form-group ${approveState >= 10 ? '' : 'hide'}`}>
<label className="text-bold">{$L('批注')}</label>
<textarea className="form-control form-control-sm row2x" name="approveRemark" placeholder={$L('输入批注 (可选)')} maxLength="600" onChange={this.handleChange} />
<textarea className="form-control form-control-sm row2x" name="approveRemark" placeholder={$L('输入批注')} maxLength="600" onChange={this.handleChange} />
</div>
<RbAlertBox message={$L('仅处于待你审批且允许批量审批的记录才能审批成功')} type="info" className="mb-0" />
<div className={`form-group ${approveState === 1 ? '' : 'hide'}`}>
<label className="text-bold">{$L('审批流程')}</label>
<select className="form-control form-control-sm" ref={(c) => (this._$useApproval = c)} />
</div>
<RbAlertBox message={$L('仅允许你审批或提交的记录才能审批成功')} type="info" className="mb-0" />
</div>
)
}
componentDidMount() {
// super.componentDidMount()
$.get(`/app/entity/approval/alist?entity=${wpc.entity[0]}&valid=true`, (res) => {
$(this._$useApproval).select2({
placeholder: $L('无'),
allowClear: false,
language: {
noResults: () => $L('无适用流程'),
},
data: res.data || [],
})
})
}
handleConfirm() {
if (rb.commercial < 10) {
RbHighbar.error(WrapHtml($L('免费版不支持批量审批功能 [(查看详情)](https://getrebuild.com/docs/rbv-features)')))
return
}
if (!this.state.approveState) return RbHighbar.create($L('请选择审批结果'))
if (!this.state.approveState) return RbHighbar.create($L('请选择审批方式'))
const _data = {
queryData: this.getQueryData(),
approveContent: {
state: this.state.approveState,
remark: this.state.approveRemark,
remark: this.state.approveRemark || null,
approvalId: $val(this._$useApproval) || null,
},
}
if (~~this.state.approveState === 1) {
if (!_data.approveContent.approvalId) {
RbHighbar.create($L('请选择审批流程'))
return
}
} else {
if ($empty(this.state.approveRemark)) {
RbHighbar.create($L('请输入批注'))
return
}
}
if (rb.env === 'dev') console.log(JSON.stringify(_data))
const that = this
RbAlert.create(<b>{$L('请再次确认审批数据范围和审批结果开始审批吗')}</b>, {
RbAlert.create(<b>{$L('请再次确认审批数据范围和审批方式开始审批吗')}</b>, {
onConfirm: function () {
this.hide()
that.disabled(true, true)
@ -722,6 +766,7 @@ class BatchApprove extends BatchOperator {
}
})
},
countdown: 5,
})
}
@ -838,16 +883,16 @@ const RbListCommon = {
RbListPage.init(wpc.listConfig, entity, wpc.privileges)
if (wpc.advFilter !== false) AdvFilters.init('.adv-search', entity[0])
const newProps = { title: $L('新建%s', entity[1]), entity: entity[0], icon: entity[2] }
const newProps = { title: $L('新建%s', entity[1]), entity: entity[0], icon: entity[2], showExtraButton: true }
const $new = $('.J_new')
.attr('disabled', false)
.on('click', () => RbFormModal.create(newProps))
if (wpc.formsAttr) {
$new.next().removeClass('hide')
const $nn = $new.next().next()
const $next = $new.next().next()
wpc.formsAttr.map((n) => {
$(`<a class="dropdown-item" data-id="${n.id}">${n.name || $L('默认布局')}</a>`)
.appendTo($nn)
.appendTo($next)
.on('click', () => RbFormModal.create({ ...newProps, specLayout: n.id }, true))
})
} else {

View file

@ -39,10 +39,7 @@ const RbListPage = {
$('.J_edit').on('click', () => {
const ids = this._RbList.getSelectedIds()
if (ids.length >= 1) {
const _entity = entity[0]
const editProps = { id: ids[0], title: $L('编辑%s', entity[1]), entity: _entity, icon: entity[2] }
if ((window.__LAB40_EDIT_PROVIDERS || {})[_entity]) window.__LAB40_EDIT_PROVIDERS[_entity](editProps)
else RbFormModal.create(editProps, true)
RbFormModal.create({ id: ids[0], title: $L('编辑%s', entity[1]), entity: entity[0], icon: entity[2], showExtraButton: true }, true)
}
})
$('.J_delete').on('click', () => {
@ -111,7 +108,7 @@ class RbViewModal extends React.Component {
super(props)
this.state = { ...props, inLoad: true, isHide: true, destroy: false }
this._mcWidth = this.props.subView === true ? 1344 : 1404
this._mcWidth = props.subView === true ? 1344 : 1404
if ($(window).width() < 1464) this._mcWidth -= 184
}
@ -122,7 +119,14 @@ class RbViewModal extends React.Component {
<div className="modal-dialog">
<div className="modal-content" style={{ width: this._mcWidth }}>
<div className={`modal-body iframe rb-loading ${this.state.inLoad === true && 'rb-loading-active'}`}>
<iframe ref={(c) => (this._iframe = c)} className={this.state.isHide ? 'invisible' : ''} src={this.state.showAfterUrl || 'about:blank'} frameBorder="0" scrolling="no" />
<iframe
data-subview={this.props.subView || false}
ref={(c) => (this._iframe = c)}
className={this.state.isHide ? 'invisible' : ''}
src={this.state.showAfterUrl || 'about:blank'}
frameBorder="0"
scrolling="no"
/>
<RbSpinner />
</div>
</div>

View file

@ -134,7 +134,7 @@ class RbFormModal extends React.Component {
</RbForm>
)
that.setState({ formComponent: FORM, alertMessage: formModel.readonlyMessage || null }, () => {
that.setState({ formComponent: FORM, alertMessage: formModel.readonlywMessage || formModel.readonlyMessage || null }, () => {
that.setState({ inLoad: false })
if (window.FrontJS) {
window.FrontJS.Form._trigger('open', [formModel])
@ -254,6 +254,12 @@ class RbFormModal extends React.Component {
* @param {*} forceNew
*/
static create(props, forceNew) {
// 自定义编辑
if ((window.__LAB40_EDIT_PROVIDERS || {})[props.entity]) {
window.__LAB40_EDIT_PROVIDERS[props.entity](props, forceNew)
return
}
// `__CURRENT35`, `__HOLDER` 可能已 unmount
const that = this
if (forceNew === true) {
@ -559,7 +565,7 @@ class RbForm extends React.Component {
let moreActions = []
// 添加明细
if (props.rawModel.mainMeta) {
if (parentProps._nextAddDetail) {
if (parentProps.nextAddDetail) {
moreActions.push(
<a key="Action101" className="dropdown-item" onClick={() => this.post(RbForm.NEXT_NEWDETAIL)}>
{$L('保存并添加')}
@ -567,10 +573,8 @@ class RbForm extends React.Component {
)
}
} else {
if (parentProps._noExtraButton) {
// 无扩展按钮
} else {
// 保存并...
// 保存并...
if (parentProps.showExtraButton) {
if (props.rawModel.hadApproval && window.ApprovalSubmitForm) {
moreActions.push(
<a key="Action103" className="dropdown-item" onClick={() => this.post(RbForm.NEXT_SUBMIT37)}>
@ -896,12 +900,12 @@ class RbForm extends React.Component {
// 提交前调用
_postBeforeExec(data) {
if (typeof this._postBefore === 'function') {
let ret = this._postBefore(data, this)
const ret = this._postBefore(data, this)
if (ret === false) return false
}
if (window.FrontJS) {
let ret = window.FrontJS.Form._trigger('saveBefore', [data, this])
const ret = window.FrontJS.Form._trigger('saveBefore', [data, this])
if (ret === false) return false
}

View file

@ -20,6 +20,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 = {}
this._verticalLayout42 = window.__LAB_VERTICALLAYOUT
@ -46,6 +47,7 @@ class RbViewForm extends React.Component {
}
let hadApproval = res.data.hadApproval
if (hadApproval === 2 || hadApproval === 10) this.onViewEditable = false // be:4.2
let hadAlert = null
let hadSop = res.data.hadSop && rb.commercial > 1
if (wpc.type === 'DetailView') {
@ -503,10 +505,7 @@ class EntityRelatedList extends RelatedList {
_handleEdit(e, id) {
$stopEvent(e, true)
const _entity = this.__entity
const editProps = { id: id, entity: _entity, title: $L('编辑%s', this.props.entity2[0]), icon: this.props.entity2[1] }
if ((window.__LAB40_EDIT_PROVIDERS || {})[_entity]) window.__LAB40_EDIT_PROVIDERS[_entity](editProps)
else RbFormModal.create(editProps, true)
RbFormModal.create({ id: id, entity: this.__entity, title: $L('编辑%s', this.props.entity2[0]), icon: this.props.entity2[1] }, true)
}
_handleView(e) {
@ -663,10 +662,7 @@ const RbViewPage = {
})
$('.J_edit').on('click', () => {
const _entity = entity[0]
const editProps = { id: id, title: $L('编辑%s', entity[1]), entity: _entity, icon: entity[2] }
if ((window.__LAB40_EDIT_PROVIDERS || {})[_entity]) window.__LAB40_EDIT_PROVIDERS[_entity](editProps)
else RbFormModal.create(editProps, true)
RbFormModal.create({ id: id, title: $L('编辑%s', entity[1]), entity: entity[0], icon: entity[2] }, true)
})
$('.J_assign').on('click', () => DlgAssign.create({ entity: entity[0], ids: [id] }))
$('.J_share').on('click', () => DlgShare.create({ entity: entity[0], ids: [id] }))
@ -674,7 +670,7 @@ const RbViewPage = {
$('.J_add-detail-menu>a').on('click', function () {
const iv = { $MAINID$: id }
const $this = $(this)
RbFormModal.create({ title: $L('添加%s', $this.data('label')), entity: $this.data('entity'), icon: $this.data('icon'), initialValue: iv, _nextAddDetail: true })
RbFormModal.create({ title: $L('添加%s', $this.data('label')), entity: $this.data('entity'), icon: $this.data('icon'), initialValue: iv, nextAddDetail: true })
})
if (wpc.transformTos && wpc.transformTos.length > 0) {
@ -928,7 +924,13 @@ const RbViewPage = {
if (entity.length > 1) iv[entity[1]] = that.__id
else iv[`&${that.__entity[0]}`] = that.__id
RbFormModal.create({ title: $L('新建%s', item._entityLabel || item.entityLabel), entity: entity[0], icon: item.icon, initialValue: iv, _noExtraButton: true })
const newProps = {
title: $L('新建%s', item._entityLabel || item.entityLabel),
entity: entity[0],
icon: item.icon,
initialValue: iv,
}
RbFormModal.create(newProps)
}
})
@ -1035,13 +1037,17 @@ $(document).ready(() => {
$('.J_home').removeClass('hide')
}
// v4.2
if (parent && parent.RbListPage && parent.RbListPage._RbList && parent.RbListPage._RbList.jumpView) {
$('.J_record-next')
.removeClass('hide')
.on('click', () => parent.RbListPage._RbList.jumpView(1))
$('.J_record-prev')
.removeClass('hide')
.on('click', () => parent.RbListPage._RbList.jumpView(-1))
if (window.frameElement && parent && parent.RbListPage && parent.RbListPage._RbList && parent.RbListPage._RbList.jumpView) {
if ($(window.frameElement).data('subview')) {
// SubView
} else {
$('.J_record-next')
.removeClass('hide')
.on('click', () => parent.RbListPage._RbList.jumpView(1))
$('.J_record-prev')
.removeClass('hide')
.on('click', () => parent.RbListPage._RbList.jumpView(-1))
}
}
// iframe 点击穿透

View file

@ -41,6 +41,10 @@ class FieldValueSet extends React.Component {
)
}
if (field.type === 'NTEXT') {
return <textarea className="form-control form-control-sm row1x" ref={(c) => (this._$value = c)} key={field.name}></textarea>
}
return <input className="form-control form-control-sm" placeholder={this.props.placeholder} ref={(c) => (this._$value = c)} key={field.name} maxLength="255" />
}

View file

@ -610,7 +610,7 @@ var _initGlobalCreate = function () {
var $item = $('<a class="dropdown-item"><i class="icon zmdi zmdi-' + this.icon + '"></i>' + this.entityLabel + '</a>').appendTo($gc)
var _this = this
$item.on('click', function () {
RbFormModal.create({ title: $L('新建%s', _this.entityLabel), entity: _this.entity, icon: _this.icon })
RbFormModal.create({ title: $L('新建%s', _this.entityLabel), entity: _this.entity, icon: _this.icon, showExtraButton: true })
})
})
}
@ -1479,3 +1479,14 @@ function $focus2End(el, delay) {
el.setSelectionRange(len, len)
}, delay || 100)
}
// 获取实体元数据
function $fetchMetaInfo(name, cb) {
$.get('/commons/metadata/meta-info?name=' + $encode(name), function (res) {
if (res.error_code === 0) {
typeof cb === 'function' && cb(res.data || {})
} else {
RbHighbar.error(res.error_msg)
}
})
}

View file

@ -211,7 +211,7 @@
<option value="macarons">MACARONS</option>
</select>
</div>
<div class="hide bosskey-show-- J_opt-INDEX J_opt-LINE J_opt-BAR J_opt-BAR2 J_opt-BAR3 J_opt-PIE J_opt-FUNNEL J_opt-RADAR J_opt-SCATTER">
<div class="hide bosskey-show-- J_opt-TABLE J_opt-INDEX J_opt-LINE J_opt-BAR J_opt-BAR2 J_opt-BAR3 J_opt-PIE J_opt-FUNNEL J_opt-RADAR J_opt-SCATTER">
<label style="color: #444; margin-bottom: 8px">[[${bundle.L('背景色')}]]</label>
<div id="useBgcolor" class="rbcolors">
<a class="default" th:title="${bundle.L('默认')}"></a>