Approval urge (#526)

* styled: filterpane

* approval urge
This commit is contained in:
RB 2022-09-21 23:53:27 +08:00 committed by GitHub
parent 673e07baee
commit c6c404c382
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 299 additions and 341 deletions

2
@rbv

@ -1 +1 @@
Subproject commit ebcc6549551569bebdb6f7afeaef09d8d967366b
Subproject commit d8bfac898caa63ba19f870450e8002467e7c0b77

View file

@ -17,9 +17,11 @@ import com.rebuild.core.Application;
import com.rebuild.core.configuration.ConfigurationException;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.privileges.PrivilegesGuardContextHolder;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.service.general.EntityService;
import com.rebuild.core.service.notification.MessageBuilder;
import com.rebuild.core.support.SetUser;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.JSONUtils;
@ -147,7 +149,7 @@ public class ApprovalProcessor extends SetUser {
.unique();
if (stepApprover == null || (Integer) stepApprover[1] != ApprovalState.DRAFT.getState()) {
throw new ApprovalException(Language.L(stepApprover == null
? Language.L("当前流程已经被他人审批") : Language.L("你已经审批过当前流程")));
? Language.L("当前流程已经被他人审批") : Language.L("你已经审批过当前流程")));
}
Record approvedStep = EntityHelper.forUpdate((ID) stepApprover[0], approver);
@ -211,6 +213,34 @@ public class ApprovalProcessor extends SetUser {
this.record, status.getApprovalId(), getCurrentNodeId(status), false);
}
/**
* 3.1.催审
*
* @return
*/
public boolean urge() {
if (this.approval == null) {
Object[] o = Application.getQueryFactory().unique(this.record, EntityHelper.ApprovalId);
this.approval = (ID) o[0];
}
int sent = 0;
String entityLabel = EasyMetaFactory.getLabel(MetadataHelper.getEntity(this.record.getEntityCode()));
JSONArray step = getCurrentStep(null);
for (Object o : step) {
JSONObject s = (JSONObject) o;
if (s.getIntValue("state") != 1) continue;
ID approver = ID.valueOf(s.getString("approver"));
String urgeMsg = Language.L("有一条 %s 记录正在等待你审批,请尽快审批", entityLabel);
Application.getNotifications().send(MessageBuilder.createApproval(approver, urgeMsg, this.record));
sent++;
}
return sent > 0;
}
/**
* 3.撤销管理员
*

View file

@ -132,8 +132,8 @@ public class RecordDifference {
|| EntityHelper.CreatedOn.equalsIgnoreCase(fieldName)
|| EntityHelper.CreatedBy.equalsIgnoreCase(fieldName)
|| EntityHelper.QuickCode.equalsIgnoreCase((fieldName))
|| (MetadataHelper.isApprovalField(fieldName) && !EntityHelper.ApprovalState.equalsIgnoreCase(fieldName))
|| field.getType() == FieldType.PRIMARY;
|| field.getType() == FieldType.PRIMARY
|| MetadataHelper.isApprovalField(fieldName);
}
/**

View file

@ -22,6 +22,7 @@ import com.rebuild.core.service.trigger.RobotTriggerObserver;
import com.rebuild.core.service.trigger.TriggerSource;
import com.rebuild.utils.JSONUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
/**
* 记录变更历史
@ -59,7 +60,10 @@ public class RevisionHistoryObserver extends OperatingObserver {
@Override
public void onUpdate(OperatingContext context) {
Record revision = newRevision(context, true);
Application.getCommonsService().create(revision);
// v3.1 无变更不记录
if (StringUtils.length(revision.getString("revisionContent")) > 2 /* [] */) {
Application.getCommonsService().create(revision);
}
}
@Override

View file

@ -181,32 +181,38 @@ public class ParseHelper {
* 字段是否可用于快速查询
*
* @param field
* @param fieldPath
* @return
*/
public static String useQuickField(Field field) {
protected static String useQuickField(Field field, String fieldPath) {
DisplayType dt = EasyMetaFactory.getDisplayType(field);
// 引用字段要保证其兼容 LIKE 条件的语法要求
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;
}
String can = useQuickField(nameField);
return can == null ? null : (QueryCompiler.NAME_FIELD_PREFIX + field.getName());
String can = useQuickField(nameField, fieldPath);
if (can == null) return null;
else if (can.startsWith("&")) return can;
else return QueryCompiler.NAME_FIELD_PREFIX + can;
} else if (dt == DisplayType.PICKLIST
|| dt == DisplayType.CLASSIFICATION) {
return QueryCompiler.NAME_FIELD_PREFIX + field.getName();
String can = StringUtils.defaultIfEmpty(fieldPath, field.getName());
if (can.startsWith("&")) return can;
else return QueryCompiler.NAME_FIELD_PREFIX + can;
} else if (dt == DisplayType.TEXT
|| dt == DisplayType.EMAIL
|| dt == DisplayType.URL
|| dt == DisplayType.PHONE
|| dt == DisplayType.SERIES) {
return field.getName();
|| dt == DisplayType.SERIES
|| dt == DisplayType.LOCATION) {
return StringUtils.defaultIfEmpty(fieldPath, field.getName());
} else {
return null;
@ -233,7 +239,7 @@ public class ParseHelper {
for (String field : quickFields.split(",")) {
Field validField = MetadataHelper.getLastJoinField(entity, field);
if (validField != null) {
String can = useQuickField(validField);
String can = useQuickField(validField, field);
if (can != null) {
usesFields.add(can);
}
@ -247,7 +253,7 @@ public class ParseHelper {
if (usesFields.isEmpty()) {
// 名称字段
Field nameField = entity.getNameField();
String can = useQuickField(nameField);
String can = useQuickField(nameField, null);
if (can != null) {
usesFields.add(can);
} else {

View file

@ -39,7 +39,6 @@ import java.util.Map;
* @since 2020/6/5
*/
@Slf4j
@SuppressWarnings({"unused", "UnnecessaryLocalVariable"})
public class BarCodeSupport {
// 二维码默认
@ -147,7 +146,7 @@ public class BarCodeSupport {
}
/**
* 保存
* 保存文件
*
* @param content
* @param format
@ -159,6 +158,7 @@ public class BarCodeSupport {
String fileName = String.format("BarCode-%d.png", System.currentTimeMillis());
File dest = RebuildConfiguration.getFileOfTemp(fileName);
try {
MatrixToImageWriter.writeToPath(bitMatrix, "png", dest.toPath());
return dest;

View file

@ -59,14 +59,12 @@ public class OnlineSessionStore implements HttpSessionListener {
if (log.isDebugEnabled()) log.info("Destroyed session : {}", event.getSession().getId());
HttpSession s = event.getSession();
if (ONLINE_SESSIONS.contains(s)) {
ONLINE_SESSIONS.remove(s);
} else {
for (Map.Entry<ID, HttpSession> e : ONLINE_USERS.entrySet()) {
if (s.equals(e.getValue())) {
ONLINE_USERS.remove(e.getKey());
break;
}
ONLINE_SESSIONS.remove(s);
for (Map.Entry<ID, HttpSession> e : ONLINE_USERS.entrySet()) {
if (s.equals(e.getValue())) {
ONLINE_USERS.remove(e.getKey());
break;
}
}
}
@ -113,7 +111,7 @@ public class OnlineSessionStore implements HttpSessionListener {
if (!RebuildConfiguration.getBool(ConfigurationItem.MultipleSessions)) {
HttpSession previous = getSession((ID) loginUser);
if (previous != null) {
log.warn("Kill previous session : {} < {}", loginUser, previous.getId());
log.warn("Kill previous session : {} ({})", previous.getId(), loginUser);
try {
previous.invalidate();
@ -122,7 +120,6 @@ public class OnlineSessionStore implements HttpSessionListener {
}
}
ONLINE_SESSIONS.remove(s);
ONLINE_USERS.put((ID) loginUser, s);
}
}

View file

@ -64,8 +64,8 @@ public class LoginLogController extends EntityController {
}
JSONObject item = JSONUtils.toJSONObject(
new String[] { "user", "fullName", "activeTime", "activeUrl", "activeIp" },
new Object[] { user, UserHelper.getName(user), active[0], active[1], active[2] });
new String[] { "user", "fullName", "activeTime", "activeUrl", "activeIp", "sid" },
new Object[] { user, UserHelper.getName(user), active[0], active[1], active[2], s.getId() });
users.add(item);
}
return users;
@ -73,14 +73,16 @@ public class LoginLogController extends EntityController {
@RequestMapping("/admin/audit/kill-session")
public RespBody killSession(HttpServletRequest request) {
final ID user = getIdParameterNotNull(request, "user");
String sessionId = getParameterNotNull(request, "user");
HttpSession s = Application.getSessionStore().getSession(user);
if (s != null) {
log.warn("Admin kill session : {} > {} ", s.getId(), user);
try {
s.invalidate();
} catch (Exception ignored) {
for (HttpSession s : Application.getSessionStore().getAllSession()) {
if (s.getId().equals(sessionId)) {
log.warn("Admin kill session : {} ({})", sessionId, s.getAttribute(WebUtils.CURRENT_USER));
try {
s.invalidate();
} catch (Exception ignored) {
}
break;
}
}
return RespBody.ok();

View file

@ -53,16 +53,18 @@ public class BarCodeGeneratorController extends BaseController {
String content = getParameterNotNull(request, "t");
int w = getIntParameter(request, "w", 0);
// 4小时缓存
ServletUtils.addCacheHead(response, 240);
BufferedImage bi;
if (request.getRequestURI().endsWith("render-qr")) {
writeTo(BarCodeSupport.createQRCode(content, w), response);
bi = BarCodeSupport.createQRCode(content, w);
} else {
// 条形码文字
boolean showText = getBoolParameter(request, "b", true);
writeTo(BarCodeSupport.createBarCode(content, w, showText), response);
bi = BarCodeSupport.createBarCode(content, w, showText);
}
// 4小时缓存
ServletUtils.addCacheHead(response, 240);
writeTo(bi, response);
}
private void writeTo(BufferedImage image, HttpServletResponse response) throws IOException {

View file

@ -93,7 +93,7 @@ public class ApprovalController extends BaseController {
}
}
// 审批中提交人可撤回
// 审批中提交人可撤回催审
if (stateVal == ApprovalState.PROCESSING.getState()
&& user.equals(ApprovalHelper.getSubmitter(recordId, useApproval))) {
data.put("canCancel", true);
@ -220,6 +220,12 @@ public class ApprovalController extends BaseController {
}
}
@RequestMapping("urge")
public RespBody doUrge(@IdParam(name = "record") ID recordId) {
boolean s = new ApprovalProcessor(recordId).urge();
return s ? RespBody.ok() : RespBody.errorl("无法发送催审通知");
}
@RequestMapping("revoke")
public RespBody doRevoke(@IdParam(name = "record") ID recordId) {
try {

View file

@ -351,7 +351,7 @@
"你可以选择来自 [RB 仓库](https://getrebuild.com/market/go/1220-rb-store) 的业务实体使用,或在安装完成后自行添加":"你可以选择来自 [RB 仓库](https://getrebuild.com/market/go/1220-rb-store) 的业务实体使用,或在安装完成后自行添加",
"你可在导入后进行适当调整。开始导入吗?":"你可在导入后进行适当调整。开始导入吗?",
"你已完成所有审批":"你已完成所有审批",
"你已审批同意,正在等待他人审批":"你已审批同意,正在等待他人审批",
"你已审批同意,正在等待他人审批":"你已审批同意,正在等待他人审批",
"你已经审批过当前流程":"你已经审批过当前流程",
"你已驳回审批":"你已驳回审批",
"你无权查看此任务":"你无权查看此任务",
@ -783,7 +783,7 @@
"当前数据已过滤":"当前数据已过滤",
"当前日期":"当前日期",
"当前时间":"当前时间",
"当前流程已经被他人审批":"当前流程已经被他人审批",
"当前流程已经被他人审批":"当前流程已经被他人审批",
"当前用户":"当前用户",
"当前用户处于未激活状态,因为其 %s":"当前用户处于未激活状态,因为其 %s",
"当前记录不符合转换条件":"当前记录不符合转换条件",

View file

@ -31,7 +31,7 @@
</div>
<div class="col-12 col-md-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_view-online" type="button"><i class="icon mdi mdi-account-network-outline"></i> [[${bundle.L('查看在线用户')}]]</button>
<button class="btn btn-space btn-secondary J_view-online" type="button"><i class="icon mdi mdi-account-network-outline"></i> [[${bundle.L('在线用户')}]]</button>
</div>
</div>
</div>

View file

@ -2905,25 +2905,24 @@ form {
padding: 10px 10px 10px 0;
}
.approval-pane .alert .close.btn {
.approval-pane .alert > .close {
top: 0;
opacity: 1;
padding: 0;
}
.approval-pane .alert > .close .btn {
padding: 8px 0;
font-size: 12px;
opacity: 1;
font-weight: normal;
line-height: 1.1;
background-color: #fff;
min-height: 0;
min-width: 78px;
margin-top: 8px;
box-shadow: 0 0 0 #fff;
border: 0 none;
}
.approval-pane .alert .close.btn + .btn {
margin-right: 88px;
}
.approval-pane .alert .close.btn + .btn + .btn {
margin-right: 176px;
.approval-pane .alert > .close > .btn + .btn {
margin-left: 10px;
}
.form.approval-form {
@ -4698,18 +4697,35 @@ pre.unstyle {
.quick-filter-pane .col {
padding-left: 8px;
padding-right: 8px;
max-width: 20%;
flex: 0 0 20%;
display: none;
}
.quick-filter-pane .col.show,
.quick-filter-pane.extended .col {
display: block;
}
.quick-filter-pane .col:nth-child(1),
.quick-filter-pane .col:nth-child(2),
.quick-filter-pane .col:nth-child(3),
.quick-filter-pane .col:last-child {
display: block;
@media (max-width: 1400px) {
.quick-filter-pane .col {
max-width: 25%;
flex: 0 0 25%;
}
}
@media (max-width: 1200px) {
.quick-filter-pane .col {
max-width: 33.3333%;
flex: 0 0 33.3333%;
}
}
@media (max-width: 992px) {
.quick-filter-pane .col {
max-width: 50%;
flex: 0 0 50%;
}
}
.quick-filter-pane .col > div > label {
@ -4723,13 +4739,15 @@ pre.unstyle {
display: none;
}
.quick-filter-pane .operating-btn {
.quick-filter-pane .col.operating-btn {
min-height: 50px;
display: block;
}
.quick-filter-pane .operating-btn > div {
position: absolute;
bottom: 0;
white-space: nowrap;
}
.quick-filter-pane .operating-btn a {
@ -4740,8 +4758,13 @@ pre.unstyle {
line-height: 1;
}
.quick-filter-pane .operating-btn .btn {
min-height: 37px;
}
.quick-filter-pane .operating-btn a .icon {
font-size: 1.231rem;
display: inline-block;
}
.quick-filter-pane .operating-btn a:hover {
@ -4749,11 +4772,11 @@ pre.unstyle {
}
.quick-filter-pane .operating-btn a.admin-show {
display: none;
visibility: hidden;
}
.quick-filter-pane .operating-btn:hover a.admin-show {
display: inline-block;
visibility: visible;
}
.quick-filter-pane .operating-btn .dropdown-menu {
@ -4763,6 +4786,14 @@ pre.unstyle {
align-items: center;
}
.quick-filter-pane .form-control {
max-width: 227px;
}
.quick-filter-pane .form-control:focus {
border-color: #bababa;
}
/* col-sm-8 */
.quick-filter-pane .col > div .filter-items .col-sm-5.val {
-webkit-box-flex: 0;

View file

@ -5,10 +5,12 @@ rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
$(document).ready(function () {
renderRbcomp(<DataList />, 'react-list')
$(document).ready(() => {
renderRbcomp(<DataList />, 'react-list', function () {
RbListPage._RbList = this._List
})
$('.J_view-online').on('click', () => renderRbcomp(<OnlineUserViewer />))
$('.J_view-online').on('click', () => renderRbcomp(<OnlineUserViewer width="681" />))
})
// 列表配置
@ -18,7 +20,7 @@ const ListConfig = {
{ field: 'user', label: $L('登录用户'), type: 'REFERENCE' },
{ field: 'loginTime', label: $L('登录时间'), type: 'DATETIME' },
{ field: 'ipAddr', label: $L('IP 地址') },
{ field: 'userAgent', label: $L('客户端') },
{ field: 'userAgent', label: $L('客户端'), width: 250 },
],
sort: 'loginTime:desc',
}
@ -27,13 +29,6 @@ class DataList extends React.Component {
render() {
return <RbList ref={(c) => (this._List = c)} config={ListConfig} />
}
componentDidMount() {
const $btn = $('.input-search .btn'),
$input = $('.input-search input')
$btn.click(() => this._List.searchQuick())
$input.keydown((e) => (e.which === 13 ? $btn.trigger('click') : true))
}
}
const _pageIps = []
@ -65,48 +60,50 @@ RbList.renderAfter = function () {
}
// ~ 在线用户
class OnlineUserViewer extends RbModalHandler {
render() {
class OnlineUserViewer extends RbAlert {
renderContent() {
return (
<RbModal ref={(c) => (this._dlg = c)} title={$L('查看在线用户')} disposeOnHide={true}>
<table className="table table-striped table-hover table-sm dialog-table">
<thead>
<tr>
<th style={{ minWidth: 150 }}>{$L('用户')}</th>
<th style={{ minWidth: 150 }}>{$L('最近活跃')}</th>
<th width="90" />
</tr>
</thead>
<tbody>
{(this.state.users || []).map((item) => {
return (
<tr key={`user-${item.user}`}>
<td className="user-avatar cell-detail user-info">
<img src={`${rb.baseUrl}/account/user-avatar/${item.user}`} alt="Avatar" />
<span className="pt-1">{item.fullName}</span>
</td>
<td className="cell-detail">
<code className="text-break text-primary">{item.activeUrl || 'n/a'}</code>
<span className="cell-detail-description">
<DateShow date={item.activeTime} />
<span className="ml-1">{item.activeIp}</span>
</span>
</td>
<td className="actions text-right">
<button className="btn btn-danger btn-sm btn-outline" type="button" onClick={() => this._killSession(item.user)}>
{$L('强退')}
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</RbModal>
<table className="table table-striped table-hover">
<thead>
<tr>
<th width="30%">{$L('用户')}</th>
<th>{$L('最近活跃')}</th>
<th width="90" />
</tr>
</thead>
<tbody>
{(this.state.users || []).map((item) => {
return (
<tr key={item.sid}>
<td className="user-avatar cell-detail user-info">
<img src={`${rb.baseUrl}/account/user-avatar/${item.user}`} alt="Avatar" />
<span className="pt-1">{item.fullName}</span>
</td>
<td className="cell-detail">
<code className="text-break text-primary">{item.activeUrl || 'n/a'}</code>
<span className="cell-detail-description">
<DateShow date={item.activeTime} />
<span className="ml-1">{item.activeIp}</span>
</span>
</td>
<td className="actions text-right">
<button className="btn btn-danger btn-sm btn-outline" type="button" onClick={() => this._killSession(item.sid)}>
{$L('强退')}
</button>
</td>
</tr>
)
})}
</tbody>
</table>
)
}
componentDidMount = () => this._load()
componentDidMount() {
super.componentDidMount()
this._load()
}
_load() {
$.get('/admin/audit/online-users', (res) => {
if (res.error_code === 0) this.setState({ users: res.data })

View file

@ -17,7 +17,9 @@ $(document).ready(() => {
_ENTITIES[this.name] = this.label
})
renderRbcomp(<DataList />, 'react-list')
renderRbcomp(<DataList />, 'react-list', function () {
RbListPage._RbList = this._List
})
})
})
@ -52,13 +54,13 @@ class DataList extends React.Component {
const $btn = $('.input-search .btn'),
$input = $('.input-search input')
$btn.click(() => this.queryList())
$input.keydown((e) => (e.which === 13 ? $btn.trigger('click') : true))
$btn.off('click').on('click', () => this.queryList())
$input.off('keydown').on('keydown', (e) => (e.which === 13 ? $btn.trigger('click') : true))
this._$belongEntity = $s2
this._$recordName = $input
$('.J_restore').click(() => this.restore())
$('.J_restore').on('click', () => this.restore())
}
queryList() {

View file

@ -20,7 +20,9 @@ $(document).ready(() => {
$(`<option value="${name}">${_ENTITIES[name]}</option>`).appendTo('#belongEntity')
}
renderRbcomp(<DataList />, 'react-list')
renderRbcomp(<DataList />, 'react-list', function () {
RbListPage._RbList = this._List
})
})
})
@ -47,7 +49,7 @@ const RevTypes = {
32: $L('共享'),
64: $L('取消共享'),
991: $L('审批通过'),
992: $L('审批撤销')
992: $L('审批撤销'),
}
class DataList extends React.Component {
@ -65,17 +67,18 @@ class DataList extends React.Component {
})
.val('$ALL$')
.trigger('change')
$s2.on('change', () => this.queryList())
const $btn = $('.input-search .btn'),
$input = $('.input-search input')
$btn.click(() => this.queryList())
$input.keydown((e) => (e.which === 13 ? $btn.trigger('click') : true))
$btn.off('click').on('click', () => this.queryList())
$input.off('keydown').on('keydown', (e) => (e.which === 13 ? $btn.trigger('click') : true))
this._$belongEntity = $s2
this._$recordName = $input
$('.J_details').click(() => this.showDetails())
$('.J_details').on('click', () => this.showDetails())
}
queryList() {
@ -141,7 +144,7 @@ class DlgDetails extends RbAlert {
<table className="table table-fixed">
<thead>
<tr>
<th width="22%">{$L('字段')}</th>
<th width="25%">{$L('字段')}</th>
<th>{$L('变更前')}</th>
<th>{$L('变更后')}</th>
</tr>

View file

@ -85,6 +85,7 @@ $(document).ready(function () {
item.type === 'URL' ||
item.type === 'PHONE' ||
item.type === 'SERIES' ||
item.type === 'LOCATION' ||
item.type === 'PICKLIST' ||
item.type === 'CLASSIFICATION' ||
item.type === 'DATE' ||
@ -117,6 +118,7 @@ $(document).ready(function () {
item.type === 'URL' ||
item.type === 'PHONE' ||
item.type === 'SERIES' ||
item.type === 'LOCATION' ||
item.type === 'PICKLIST' ||
item.type === 'CLASSIFICATION' ||
// item.type === 'DATE' ||

View file

@ -659,7 +659,9 @@ class FilterItem extends React.Component {
renderPickListAfter() {
const that = this
const $s2val = $(this._filterVal)
.select2({})
.select2({
width: this.props.select2Width,
})
.on('change.select2', function () {
that.setState({ value: $s2val.val().join('|') })
})
@ -693,6 +695,7 @@ class FilterItem extends React.Component {
const that = this
const $s2val = $(this._filterVal)
.select2({
width: this.props.select2Width,
minimumInputLength: 1,
ajax: {
url: '/commons/search/search',
@ -791,6 +794,7 @@ class FilterItem extends React.Component {
const that = this
const $s2val = $(this._filterVal)
.select2({
width: this.props.select2Width,
allowClear: this.props.allowClear === true,
placeholder: this.props.allowClear === true ? $L('全部') : null,
})

View file

@ -29,9 +29,12 @@ class ApprovalProcessor extends React.Component {
renderStateDraft() {
return (
<div className="alert alert-warning shadow-sm">
<button className="close btn btn-secondary" onClick={this.submit}>
{$L('提交')}
</button>
<span className="close">
<button className="btn btn-secondary" onClick={this.submit}>
{$L('提交')}
</button>
</span>
<div className="icon">
<span className="zmdi zmdi-info-outline" />
</div>
@ -46,25 +49,33 @@ class ApprovalProcessor extends React.Component {
let aMsg = $L('当前记录正在审批中')
if (this.state.imApprover) {
if (this.state.imApproveSatate === 1) aMsg = $L('当前记录正在等待你审批')
else if (this.state.imApproveSatate === 10) aMsg = $L('你已审批同意正在等待他人审批')
else if (this.state.imApproveSatate === 10) aMsg = $L('你已审批同意正在等待他人审批')
else if (this.state.imApproveSatate === 11) aMsg = $L('你已驳回审批')
}
return (
<div className="alert alert-warning shadow-sm">
<button className="close btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
{this.state.canCancel && (
<button className="close btn btn-secondary" onClick={this.cancel}>
{$L('撤回')}
<span className="close">
{this.state.imApprover && this.state.imApproveSatate === 1 && (
<button className="btn btn-secondary" onClick={this.approve}>
{$L('审批')}
</button>
)}
{this.state.canCancel && (
<RF>
<button className="btn btn-secondary" onClick={this.urge}>
{$L('催审')}
</button>
<button className="btn btn-secondary" onClick={this.cancel}>
{$L('撤回')}
</button>
</RF>
)}
<button className="btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
)}
{this.state.imApprover && this.state.imApproveSatate === 1 && (
<button className="close btn btn-secondary" onClick={this.approve}>
{$L('审批')}
</button>
)}
</span>
<div className="icon">
<span className="zmdi zmdi-hourglass-alt" />
</div>
@ -78,14 +89,17 @@ class ApprovalProcessor extends React.Component {
return (
<div className="alert alert-success shadow-sm">
<button className="close btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
{rb.isAdminUser && (
<button className="close btn btn-secondary" onClick={this.revoke}>
{$L('撤销')}
<span className="close">
{rb.isAdminUser && (
<button className="btn btn-secondary" onClick={this.revoke}>
{$L('撤销')}
</button>
)}
<button className="btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
)}
</span>
<div className="icon">
<span className="zmdi zmdi-check" />
</div>
@ -97,12 +111,15 @@ class ApprovalProcessor extends React.Component {
renderStateRejected() {
return (
<div className="alert alert-danger shadow-sm">
<button className="close btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
<button className="close btn btn-secondary" onClick={this.submit}>
{$L('再次提交')}
</button>
<span className="close">
<button className="btn btn-secondary" onClick={this.submit}>
{$L('再次提交')}
</button>
<button className="btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
</span>
<div className="icon">
<span className="zmdi zmdi-close-circle-o" />
</div>
@ -114,12 +131,15 @@ class ApprovalProcessor extends React.Component {
renderStateCanceled() {
return (
<div className="alert alert-warning shadow-sm">
<button className="close btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
<button className="close btn btn-secondary" onClick={this.submit}>
{$L('再次提交')}
</button>
<span className="close">
<button className="btn btn-secondary" onClick={this.submit}>
{$L('再次提交')}
</button>
<button className="btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
</span>
<div className="icon">
<span className="zmdi zmdi-rotate-left" />
</div>
@ -131,12 +151,15 @@ class ApprovalProcessor extends React.Component {
renderStateRevoked() {
return (
<div className="alert alert-warning shadow-sm">
<button className="close btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
<button className="close btn btn-secondary" onClick={this.submit}>
{$L('再次提交')}
</button>
<span className="close">
<button className="btn btn-secondary" onClick={this.submit}>
{$L('再次提交')}
</button>
<button className="btn btn-secondary" onClick={this.viewSteps}>
{$L('详情')}
</button>
</span>
<div className="icon">
<span className="zmdi zmdi-rotate-left" />
</div>
@ -179,7 +202,7 @@ class ApprovalProcessor extends React.Component {
cancel = () => {
const that = this
RbAlert.create($L('确认撤回当前审批'), {
RbAlert.create($L('将要撤回已提交审批是否继续'), {
confirm: function () {
this.disabled(true)
$.post(`/app/entity/approval/cancel?record=${that.props.id}`, (res) => {
@ -191,9 +214,27 @@ class ApprovalProcessor extends React.Component {
})
}
urge = () => {
const that = this
RbAlert.create($L('将向当前审批人发送催审通知是否继续'), {
confirm: function () {
this.disabled(true)
$.post(`/app/entity/approval/urge?record=${that.props.id}`, (res) => {
if (res.error_code > 0) {
RbHighbar.error(res.error_msg)
this.disabled()
} else {
RbHighbar.success($L('通知已发送'))
this.hide()
}
})
},
})
}
revoke = () => {
const that = this
RbAlert.create($L('将要撤销已通过审批确认吗'), {
RbAlert.create($L('将要撤销已通过审批是否继续'), {
type: 'danger',
confirm: function () {
this.disabled(true)

View file

@ -699,6 +699,7 @@ const RbListCommon = {
}
const entity = wpc.entity
if (!entity) return
RbListPage.init(wpc.listConfig, entity, wpc.privileges)
if (wpc.advFilter !== false) AdvFilters.init('.adv-search', entity[0])
@ -907,7 +908,7 @@ class RbList extends React.Component {
if (wpc.advFilter !== true) this.fetchList(this._buildQuick())
// 按键操作
if (wpc.type === 'RecordList') $(document).on('keydown', (e) => this._keyEvent(e))
if (wpc.type === 'RecordList' || wpc.type === 'DetailList') $(document).on('keydown', (e) => this._keyEvent(e))
}
fetchList(filter) {
@ -1092,6 +1093,7 @@ class RbList extends React.Component {
}
_openView($tr) {
if (!wpc.type) return
const id = $($tr).data('id')
if (!wpc.forceSubView) {
location.hash = `!/View/${this._entity}/${id}`

View file

@ -93,7 +93,7 @@ const RbListPage = {
}
// Filter Pane
if ($('.quick-filter-pane').length > 0) {
if ($('.quick-filter-pane')[0]) {
// eslint-disable-next-line react/jsx-no-undef
renderRbcomp(<AdvFilterPane entity={entity[0]} fields={wpc.paneFields} onSearch={(s) => RbListPage._RbList.search(s)} />, $('.quick-filter-pane')[0])
}

View file

@ -1,171 +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.
*/
// deps: rb-advfilter.js
const REFENTITY_CACHE = window.REFENTITY_CACHE || {}
const IS_N2NREF = window.IS_N2NREF || {}
const BIZZ_ENTITIES = window.BIZZ_ENTITIES || []
// eslint-disable-next-line no-unused-vars
class AdvFilterPane extends React.Component {
constructor(props) {
super(props)
this.state = {}
this._itemsRef = []
}
onRef = (c) => this._itemsRef.push(c)
componentDidMount() {
const items = (this.props.fields || []).map((item) => {
if (item.type === 'REFERENCE' || item.type === 'N2NREFERENCE') {
REFENTITY_CACHE[`${this.props.entity}.${item.name}`] = item.ref
if (item.type === 'N2NREFERENCE') IS_N2NREF.push(item.name)
// NOTE: Use `NameField` field-type
if (!BIZZ_ENTITIES.includes(item.ref[0])) {
item.type = item.ref[1]
}
}
return item
})
this.setState({ items }, () => {
setTimeout(() => this.clearFilter(), 200)
})
}
render() {
if (!this.state.items) return null
return (
<form className="row" onSubmit={(e) => this.searchNow(e)}>
{this.state.items.map((item, i) => {
return (
<div className="col col-6 col-lg-4 col-xl-3" key={i}>
<div>
<label>{item.label}</label>
<div className="adv-filter">
<div className="filter-items">
<FilterItemExt onRef={this.onRef} $$$parent={this} fields={[item]} allowClear />
</div>
</div>
</div>
</div>
)
})}
<div className="col col-6 col-lg-4 col-xl-3 operating-btn">
<div>
<div className="btn-group">
<button className="btn btn-secondary" type="submit">
<i className="icon zmdi zmdi-search"></i> {$L('查询')}
</button>
<button className="btn btn-secondary dropdown-toggle w-auto" type="button" data-toggle="dropdown">
<i className="icon zmdi zmdi-chevron-down" />
</button>
<div className="dropdown-menu dropdown-menu-right">
<label className="custom-control custom-control-sm custom-radio custom-control-inline mb-0 mr-2">
<input className="custom-control-input" type="radio" name="useEquation" value="OR" defaultChecked />
<span className="custom-control-label">{$L('符合任一')}</span>
</label>
<label className="custom-control custom-control-sm custom-radio custom-control-inline mb-0 mr-0">
<input className="custom-control-input" type="radio" name="useEquation" value="AND" ref={(c) => (this._$useEquationAnd = c)} />
<span className="custom-control-label">{$L('符合全部')}</span>
</label>
</div>
</div>
<a className="ml-3 down-1" onClick={() => this.clearFilter(true)}>
<i className="icon zmdi zmdi-replay down-1" />
</a>
{(this.props.fields || []).length > 4 && (
<a className="ml-2 down-1" onClick={() => this.toggleExtended()}>
<i className={`icon down-1 zmdi ${this.state.extended ? 'zmdi-unfold-less text-bold' : 'zmdi-unfold-more'}`} title={$L('展开/收起')} />
</a>
)}
{rb.isAdminUser && (
<a
className="ml-2 down-2 admin-show"
title={$L('配置查询面板字段')}
onClick={() => {
RbModal.create(
`/p/admin/metadata/list-filterpane?entity=${this.props.entity}`,
<RF>
{$L('配置查询面板字段')}
<sup className="rbv" title={$L('增值功能')} />
</RF>
)
}}>
<i className="icon zmdi zmdi-settings" />
</a>
)}
</div>
</div>
</form>
)
}
searchNow(e) {
$stopEvent(e, true)
const filters = []
for (let i = 0; i < this._itemsRef.length; i++) {
const item = this._itemsRef[i].getFilterJson()
if (item) filters.push(item)
}
const s = {
entity: this.props.entity,
equation: this._$useEquationAnd.checked ? 'AND' : 'OR',
items: filters,
}
if (rb.env === 'dev') console.log(JSON.stringify(s))
typeof this.props.onSearch === 'function' && this.props.onSearch(s)
}
clearFilter(searchNow) {
this._itemsRef.forEach((i) => i.clear())
searchNow === true && setTimeout(() => this.searchNow(), 200)
}
toggleExtended() {
this.setState({ extended: !this.state.extended }, () => {
if (this.state.extended) $('.quick-filter-pane').addClass('extended')
else $('.quick-filter-pane').removeClass('extended')
})
}
}
// eslint-disable-next-line no-undef
class FilterItemExt extends FilterItem {
constructor(props) {
super(props)
}
componentDidMount() {
super.componentDidMount()
const $s2op = this.__select2[1]
setTimeout(() => {
if (this.state.type === 'DATE' || this.state.type === 'DATETIME' || this.state.type === 'TIME') {
$s2op.val('EQ').trigger('change')
}
}, 200)
}
// @e = el or event
valueCheck(e) {
const v = e.target ? e.target.value : e.val()
if (!v) return
super.valueCheck(e)
// $el.removeClass('is-invalid')
}
}

View file

@ -132,7 +132,7 @@
<script th:src="@{/assets/js/rb-forms.protable.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-advfilter.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-filterpane.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-assignshare.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-assignshare.js}" type="text/babel" th:if="${commercial > 1}"></script>
<script th:src="@{/assets/js/rb-approval.js}" type="text/babel"></script>
<script th:src="@{/assets/js/settings-share2.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-datalist.common.js}" type="text/babel"></script>

View file

@ -135,7 +135,7 @@
<script th:src="@{/assets/js/rb-forms.append.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-forms.protable.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-advfilter.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-filterpane.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-filterpane.js}" type="text/babel" th:if="${commercial > 1}"></script>
<script th:src="@{/assets/js/rb-assignshare.js}" type="text/babel"></script>
<script th:src="@{/assets/js/rb-approval.js}" type="text/babel"></script>
<script th:src="@{/assets/js/settings-share2.js}" type="text/babel"></script>