revision details

This commit is contained in:
devezhao-mbp 2019-08-23 02:35:34 +08:00
parent 65a8ce9d08
commit 5a4513e40a
11 changed files with 159 additions and 52 deletions

View file

@ -89,7 +89,7 @@ public class OperatingContext {
* @return
*/
public Record getAnyRecord() {
return getBeforeRecord() != null ? getBeforeRecord() : getAfterRecord();
return getAfterRecord() != null ? getAfterRecord() : getBeforeRecord();
}
/**

View file

@ -28,9 +28,9 @@ import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.RebuildException;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.collections4.map.CaseInsensitiveMap;
import org.apache.commons.lang.StringUtils;
import java.util.Map;
@ -64,7 +64,7 @@ public class RecordMerger {
}
Entity entity = beforeRecord != null ? beforeRecord.getEntity() : afterRecord.getEntity();
Map<String, String[]> merged = new CaseInsensitiveMap<>();
Map<String, Object[]> merged = new CaseInsensitiveMap<>();
if (beforeRecord != null) {
JSONObject beforeSerialize = (JSONObject) beforeRecord.serialize();
@ -78,7 +78,7 @@ public class RecordMerger {
if (NullValue.is(beforeVal)) {
beforeVal = null;
}
merged.put(field, new String[]{ beforeVal == null ? null : JSON.toJSONString(beforeVal), null });
merged.put(field, new Object[]{ beforeVal, null });
}
}
@ -95,15 +95,15 @@ public class RecordMerger {
continue;
}
String[] mergedValue = merged.computeIfAbsent(field, k -> new String[]{null, null});
mergedValue[1] = afterVal == null ? null : JSON.toJSONString(afterVal);
Object[] mergedValue = merged.computeIfAbsent(field, k -> new Object[]{null, null});
mergedValue[1] = afterVal;
}
}
JSONArray array = new JSONArray();
for (Map.Entry<String, String[]> e : merged.entrySet()) {
String[] val = e.getValue();
if (StringUtils.isEmpty(val[0]) && StringUtils.isEmpty(val[1])) {
for (Map.Entry<String, Object[]> e : merged.entrySet()) {
Object[] val = e.getValue();
if (val[0] == null && val[1] == null) {
continue;
}
@ -125,6 +125,10 @@ public class RecordMerger {
String fieldName = field.getName();
return EntityHelper.ModifiedOn.equalsIgnoreCase(fieldName)
|| EntityHelper.ModifiedBy.equalsIgnoreCase(fieldName)
|| EntityHelper.CreatedOn.equalsIgnoreCase(fieldName)
|| EntityHelper.CreatedBy.equalsIgnoreCase(fieldName)
|| EntityHelper.QuickCode.equalsIgnoreCase((fieldName))
|| MetadataHelper.isApprovalField(fieldName)
|| field.getType() == FieldType.PRIMARY;
}
}

View file

@ -101,7 +101,7 @@ public class RevisionHistoryObserver extends OperatingObserver {
JSON revisionContent = new RecordMerger(before).merge(after);
record.setString("revisionContent", revisionContent.toJSONString());
} else {
record.setString("revisionContent", JSONUtils.EMPTY_OBJECT_STR);
record.setString("revisionContent", JSONUtils.EMPTY_ARRAY_STR);
}
OperatingContext source = RobotTriggerObserver.getTriggerSource();

View file

@ -18,12 +18,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
package com.rebuild.web.admin.audit;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.server.metadata.entity.EasyMeta;
import com.rebuild.web.BaseEntityControll;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
@ -37,7 +46,35 @@ import java.io.IOException;
public class RevisionHistoryControll extends BaseEntityControll {
@RequestMapping("revision-history")
public ModelAndView pageLogging(HttpServletRequest request) throws IOException {
public ModelAndView pageLogging() throws IOException {
return createModelAndView("/admin/audit/revision-history.jsp");
}
@RequestMapping("revision-history/details")
public void details(HttpServletRequest request, HttpServletResponse response) throws IOException {
ID id = getIdParameterNotNull(request, "id");
Object[] rev = Application.createQueryNoFilter(
"select revisionContent,belongEntity from RevisionHistory where revisionId = ?")
.setParameter(1, id)
.unique();
JSONArray data = JSON.parseArray((String) rev[0]);
// 字段名称
if (MetadataHelper.containsEntity((String) rev[1])) {
Entity entity = MetadataHelper.getEntity((String) rev[1]);
for (Object o : data) {
JSONObject item = (JSONObject) o;
String field = item.getString("field");
if (entity.containsField(field)) {
field = EasyMeta.getLabel(entity.getField(field));
} else {
field = "[" + field.toUpperCase() + "]";
}
item.put("field", field);
}
}
writeSuccess(response, data);
}
}

View file

@ -4,12 +4,6 @@
<head>
<%@ include file="/_include/Head.jsp"%>
<title>回收站</title>
<style type="text/css">
td .badge {
font-weight: normal;
color: #999;
}
</style>
</head>
<body>
<div class="rb-wrapper rb-fixed-sidebar rb-collapsible-sidebar rb-collapsible-sidebar-hide-logo rb-color-header">

View file

@ -5,9 +5,13 @@
<%@ include file="/_include/Head.jsp"%>
<title>变更历史</title>
<style type="text/css">
td .badge {
font-weight: normal;
color: #999;
.table.table-fixed td {
vertical-align: text-top;
}
.table.table-fixed td>div {
word-break: break-all;
max-height: 100px;
overflow: hidden;
}
</style>
</head>
@ -40,6 +44,7 @@ td .badge {
</div>
<div class="col-12 col-md-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_details"><i class="icon zmdi zmdi-flip"></i> 详情</button>
</div>
</div>
</div>

View file

@ -70,15 +70,16 @@
</div>
<div class="col-lg-3 col-12">
<div class="card">
<div class="card-header card-header-divider">关于 REBUILD</div>
<div class="card-header card-header-divider">关于</div>
<div class="card-body">
<p>版本 <a class="link" target="_blank" href="https://github.com/getrebuild/rebuild/releases"><%=Application.VER%></a></p>
<p class="mb-1">版本 <a class="link" target="_blank" href="https://getrebuild.com/download"><%=Application.VER%></a></p>
<p class="mb-2">授权 <a class="link" target="_blank" href="https://getrebuild.com#pricing-plans">开源社区版 (非商业用途)</a></p>
<ul style="line-height:2">
<li><a class="link" target="_blank" href="${baseUrl}/gw/server-status">系统状态</a></li>
<li><a class="link" target="_blank" href="https://getrebuild.com/docs/">帮助文档</a></li>
<li><a class="link" target="_blank" href="mailto:getrebuild@sina.com?subject=技术支持">技术支持</a></li>
<li><a class="link" target="_blank" href="https://getrebuild.com/contact#tech-supports">技术支持</a></li>
</ul>
<div class="text-muted"><i class="zmdi zmdi-info-outline"></i> 本软件系统使用 <a class="link" href="http://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank">GPL-3.0</a> 许可。请遵循许可协议。</div>
<div class="text-muted"><i class="zmdi zmdi-info-outline"></i> REBUILD 使用 GPL3.0 与 <a class="link" href="https://getrebuild.com#pricing-plans" target="_blank">商业授权</a> 双重许可,请遵循许可协议。</div>
</div>
</div>
</div>

View file

@ -94,6 +94,20 @@ html.admin .admin-show.row {
text-decoration: underline;
}
.badge.text-id {
font-weight: normal;
color: #777;
}
a.badge.text-id {
color: #4285f4;
cursor: pointer;
}
a.badge.text-id:hover {
border-color: #4285f4;
}
.btn {
min-height: 2.8rem;
min-width: 98px;

View file

@ -95,8 +95,8 @@ class DataList extends React.Component {
// eslint-disable-next-line react/display-name
CellRenders.renderSimple = function (v, s, k) {
if (k.endsWith('.channelWith')) v = v ? (<React.Fragment>关联删除 <span className="badge ml-1" title="关联主记录ID">{v.toUpperCase()}</span></React.Fragment>) : '直接删除'
else if (k.endsWith('.recordId')) v = <span className="badge">{v.toUpperCase()}</span>
if (k.endsWith('.channelWith')) v = v ? (<React.Fragment>关联删除 <span className="badge text-id ml-1" title="关联主记录ID">{v.toUpperCase()}</span></React.Fragment>) : '直接删除'
else if (k.endsWith('.recordId')) v = <span className="badge text-id">{v.toUpperCase()}</span>
else if (k.endsWith('.belongEntity')) v = _entities[v] || `[${v.toUpperCase()}]`
return <td key={k}><div style={s}>{v || ''}</div></td>
}

View file

@ -38,7 +38,7 @@ class DataList extends React.Component {
super(props)
}
render() {
return <RbList ref={(c) => this._List = c} config={ListConfig} uncheckbox={true}></RbList>
return <RbList ref={(c) => this._List = c} config={ListConfig}></RbList>
}
componentDidMount() {
@ -56,6 +56,8 @@ class DataList extends React.Component {
this._belongEntity = select2
this._recordName = input
$('.J_details').click(() => this.showDetails())
}
queryList() {
@ -72,13 +74,55 @@ class DataList extends React.Component {
}
this._List.search(JSON.stringify(q), true)
}
showDetails() {
let ids = this._List.getSelectedIds()
if (!ids || ids.length === 0) return
renderRbcomp(<DlgDetails id={ids[0]} width="701" />)
}
}
// eslint-disable-next-line react/display-name
CellRenders.renderSimple = function (v, s, k) {
if (k.endsWith('.channelWith')) v = v ? (<React.Fragment>关联操作 <span className="badge ml-1" title="关联主记录ID">{v.toUpperCase()}</span></React.Fragment>) : '直接操作'
else if (k.endsWith('.recordId')) v = <span className="badge">{v.toUpperCase()}</span>
if (k.endsWith('.channelWith')) v = v ? (<React.Fragment>关联操作 <span className="badge text-id ml-1" title="关联主记录ID">{v.toUpperCase()}</span></React.Fragment>) : '直接操作'
else if (k.endsWith('.recordId')) v = <span className="badge text-id">{v.toUpperCase()}</span>
else if (k.endsWith('.belongEntity')) v = _entities[v] || `[${v.toUpperCase()}]`
else if (k.endsWith('.revisionType')) v = RevTypes[v] || '未知'
return <td key={k}><div style={s}>{v || ''}</div></td>
}
class DlgDetails extends RbAlert {
constructor(props) {
super(props)
}
renderContent() {
if (!this.state.data) return <RbSpinner fully={true} />
if (this.state.data.length === 0) return <div className="text-center mt-3 mb-5">无变更详情</div>
return <table className="table table-fixed">
<thead>
<tr>
<th width="22%">字段</th>
<th>变更前</th>
<th>变更后</th>
</tr>
</thead>
<tbody>
{this.state.data.map((item) => {
return <tr key={`fk-${item.field}`}>
<td>{item.field}</td>
<td><div>{item.before || <span className="text-muted">空值</span>}</div></td>
<td><div>{item.after || <span className="text-muted">空值</span>}</div></td>
</tr>
})}
</tbody>
</table>
}
componentDidMount() {
super.componentDidMount()
$.get(`${rb.baseUrl}/admin/audit/revision-history/details?id=${this.props.id}`, (res) => {
this.setState({ data: res.data || [] })
})
}
}

View file

@ -155,6 +155,24 @@ class RbAlert extends React.Component {
this.state = { disable: false }
}
render() {
let style = {}
if (this.props.width) style.maxWidth = ~~this.props.width
return (
<div className="modal rbalert" ref={(c) => this._dlg = c} tabIndex={this.state.tabIndex || -1}>
<div className="modal-dialog modal-dialog-centered" style={style}>
<div className="modal-content">
<div className="modal-header pb-0">
<button className="close" type="button" onClick={() => this.hide()}><span className="zmdi zmdi-close" /></button>
</div>
<div className="modal-body">
{this.renderContent()}
</div>
</div>
</div>
</div>
)
}
renderContent() {
let icon = this.props.type === 'danger' ? 'alert-triangle' : 'help-outline'
if (this.props.type === 'warning') icon = 'alert-circle-o'
if (this.props.type === 'primary') icon = 'info-outline'
@ -165,34 +183,24 @@ class RbAlert extends React.Component {
let cancel = (this.props.cancel || this.hide).bind(this)
let confirm = (this.props.confirm || this.hide).bind(this)
return (
<div className="modal rbalert" ref={(c) => this._dlg = c} tabIndex={this.state.tabIndex || -1}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header pb-0">
<button className="close" type="button" onClick={() => this.hide()}><span className="zmdi zmdi-close" /></button>
</div>
<div className="modal-body">
<div className="text-center ml-6 mr-6">
{this.props.showIcon === false ? null :
<div className={'text-' + type}><span className={'modal-main-icon zmdi zmdi-' + icon} /></div>
}
{this.props.title && <h4 className="mb-2 mt-3">{this.props.title}</h4>}
<div className={this.props.title ? '' : 'mt-3'}>{content}</div>
<div className="mt-4 mb-3">
<button disabled={this.state.disable} className="btn btn-space btn-secondary" type="button" onClick={cancel}>{this.props.cancelText || '取消'}</button>
<button disabled={this.state.disable} className={'btn btn-space btn-' + type} type="button" onClick={confirm}>{this.props.confirmText || '确定'}</button>
</div>
</div>
</div>
</div>
</div>
return <div className="text-center ml-6 mr-6">
{this.props.showIcon === false ? null :
<div className={'text-' + type}><span className={'modal-main-icon zmdi zmdi-' + icon} /></div>
}
{this.props.title && <h4 className="mb-2 mt-3">{this.props.title}</h4>}
<div className={this.props.title ? '' : 'mt-3'}>{content}</div>
<div className="mt-4 mb-3">
<button disabled={this.state.disable} className="btn btn-space btn-secondary" type="button" onClick={cancel}>{this.props.cancelText || '取消'}</button>
<button disabled={this.state.disable} className={'btn btn-space btn-' + type} type="button" onClick={confirm}>{this.props.confirmText || '确定'}</button>
</div>
)
</div>
}
componentDidMount() {
$(this._dlg).modal({ show: true, keyboard: true })
}
hide() {
let root = $(this._dlg)
root.modal('hide')