mirror of
https://github.com/getrebuild/rebuild.git
synced 2025-02-25 23:05:06 +08:00
revision details
This commit is contained in:
parent
65a8ce9d08
commit
5a4513e40a
11 changed files with 159 additions and 52 deletions
|
@ -89,7 +89,7 @@ public class OperatingContext {
|
|||
* @return
|
||||
*/
|
||||
public Record getAnyRecord() {
|
||||
return getBeforeRecord() != null ? getBeforeRecord() : getAfterRecord();
|
||||
return getAfterRecord() != null ? getAfterRecord() : getBeforeRecord();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 || [] })
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue