Fix 3.9 beta3 (#849)

* fix

* feat: 记录历史快速查看

* backup

* func HTML

* CLEAR
This commit is contained in:
REBUILD 企业管理系统 2024-12-23 20:17:18 +08:00 committed by GitHub
parent 54695b77f9
commit ae2538757f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 181 additions and 42 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 0695a8c143e8e5e000bce5b0985a2949ead8f518
Subproject commit 3d440c1411799e3f45c51a53a4d9696824fe33e1

View file

@ -365,7 +365,7 @@ public class EasyExcelGenerator extends SetUser {
data.put(varName, buildBarcodeData(easyField.getRawMeta(), record.getPrimary()));
} else if (fieldValue == null) {
// v3.8
Object funcValue = ValueConvertFunc.convert(easyField, null, varName);
Object funcValue = ValueConvertFunc.convert(easyField, null, varName, this.getClass());
data.put(varName, funcValue == null ? StringUtils.EMPTY : funcValue);
} else {
@ -418,7 +418,7 @@ public class EasyExcelGenerator extends SetUser {
}
// v3.7.0
fieldValue = ValueConvertFunc.convert(easyField, fieldValue, varName);
fieldValue = ValueConvertFunc.convert(easyField, fieldValue, varName, this.getClass());
}
data.put(varName, fieldValue);

View file

@ -17,7 +17,9 @@ import com.rebuild.core.configuration.general.MultiSelectManager;
import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.metadata.easymeta.EasyDecimal;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.rbv.data.Html5ReportGenerator;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.utils.MarkdownUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
@ -53,14 +55,16 @@ public class ValueConvertFunc {
private static final String PICKAT_4CLASS_DATE = "PICKAT"; // eg. PICKAT:2
private static final String SIZE_4IMG = "SIZE"; // eg. SIZE:100*200, SIZE:100
private static final String EMPTY = "EMPTY"; // eg. EMPTY:
public static final String CLEAR_4NTEXT = "CLEAR";
/**
* @param field
* @param value
* @param varName
* @param fromClazz
* @return
*/
public static Object convert(EasyField field, Object value, String varName) {
public static Object convert(EasyField field, Object value, String varName, Class<?> fromClazz) {
String thatFunc = splitFunc(varName);
if (thatFunc == null) return value;
@ -153,6 +157,15 @@ public class ValueConvertFunc {
return m[m.length - 1]; // last
}
} else if (type == DisplayType.NTEXT) {
if (thatFunc.equals(CLEAR_4NTEXT)) {
if (fromClazz == Html5ReportGenerator.class) {
String md2html = MarkdownUtils.render((String) value);
return "<div class='mdedit-content md2html'>" + md2html + "</div>";
} else {
return MarkdownUtils.cleanMarks((String) value);
}
}
}
return value;

View file

@ -122,7 +122,7 @@ public class KVStorage {
*/
protected static String getValue(final String key, boolean noCache, Object defaultValue) {
String value = null;
// be:v3.8
// be:3.8
if (ConfigurationItem.SN.name().equals(key)) {
value = BootEnvironmentPostProcessor.getProperty(key);
} else if (ConfigurationItem.inJvmArgs(key)) {
@ -174,7 +174,7 @@ public class KVStorage {
private static final Object THROTTLED_QUEUE_LOCK = new Object();
private static final Map<String, Object> THROTTLED_QUEUE = new ConcurrentHashMap<>();
private static final Timer THROTTLED_TIMER = new Timer("KVStorage-Timer");
private static final Timer THROTTLED_TIMER = new Timer("KVStorage-Syncer");
static {
final TimerTask localTimerTask = new TimerTask() {

View file

@ -89,6 +89,7 @@ public class MarkdownUtils {
* @return
*/
public static String cleanMarks(String md) {
md = md.replaceAll("!\\[.*?]\\((.*?)\\)", "[$1]"); // 替换图片
String html = render(md);
return Jsoup.parse(html).body().text();
}

View file

@ -13,6 +13,7 @@ import com.rebuild.core.metadata.impl.TsetEntity;
import com.rebuild.core.rbstore.RbSystemImporter;
import com.rebuild.core.service.approval.ApprovalFields2Schema;
import com.rebuild.core.support.ConfigurationItem;
import com.rebuild.core.support.License;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.setup.DatabaseBackup;
import com.rebuild.core.support.setup.DatafileBackup;
@ -186,7 +187,15 @@ public class AdminCli3 {
// Setter
else {
String value = commands[2];
RebuildConfiguration.set(item, value);
if (item == ConfigurationItem.SN) {
String usql = String.format("update system_config set `VALUE` = '%s' where `ITEM` = 'SN'", value);
Application.getSqlExecutor().execute(usql);
// reset: RB NEED RESTART
Application.getCommonsCache().evict(ConfigurationItem.SN.name());
License.siteApiNoCache("api/authority/query");
} else {
RebuildConfiguration.set(item, value);
}
return "OK";
}

View file

@ -27,6 +27,8 @@ import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.integration.QiniuCloud;
import com.rebuild.core.support.integration.SMSender;
import com.rebuild.core.support.setup.DatabaseBackup;
import com.rebuild.core.support.setup.DatafileBackup;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.utils.JSONUtils;
import com.rebuild.utils.RbAssert;
@ -46,6 +48,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
@ -132,6 +135,34 @@ public class ConfigurationController extends BaseController {
return RespBody.ok();
}
@PostMapping("systems/backup")
public RespBody postSystemsBackup(HttpServletRequest request) {
RbAssert.isSuperAdmin(getRequestUser(request));
final int type = getIntParameter(request, "type", 3);
final File backups = RebuildConfiguration.getFileOfData("_backups");
String dbFile = null, fileFile = null;
if (type == 1 || type == 3) {
try {
dbFile = "_backups/" + new DatabaseBackup().backup(backups).getName();
} catch (Exception e) {
dbFile = "ERR:" + e.getMessage();
log.error("Executing [DatabaseBackup] fails", e);
}
}
if (type == 2 || type == 3) {
try {
fileFile = "_backups/" + new DatafileBackup().backup(backups).getName();
} catch (Exception e) {
fileFile = "ERR:" + e.getMessage();
log.error("Executing [DataFileBackup] fails", e);
}
}
JSON res = JSONUtils.toJSONObject(new String[]{"db","file"}, new Object[]{dbFile, fileFile});
return RespBody.ok(res);
}
@GetMapping("integration/storage")
public ModelAndView pageIntegrationStorage() {
ModelAndView mv = createModelAndView("/admin/integration/storage-qiniu");

View file

@ -3335,5 +3335,13 @@
"飞书侧配置":"飞书侧配置",
"已有记录":"已有记录",
"存在同名字段,是否继续添加?":"存在同名字段,是否继续添加?",
"安装系统模版将清空您现有系统的所有数据,包括系统配置、业务实体、数据等。安装前强烈建议您做好系统备份。":"安装系统模版将清空您现有系统的所有数据,包括系统配置、业务实体、数据等。安装前强烈建议您做好系统备份。"
"安装系统模版将清空您现有系统的所有数据,包括系统配置、业务实体、数据等。安装前强烈建议您做好系统备份。":"安装系统模版将清空您现有系统的所有数据,包括系统配置、业务实体、数据等。安装前强烈建议您做好系统备份。",
"请勿在业务高峰时段执行备份":"请勿在业务高峰时段执行备份",
"无需提交":"无需提交",
"数据目录文件":"数据目录文件",
"开始备份":"开始备份",
"立即备份":"立即备份",
"数据库":"数据库",
"未备份":"未备份",
"选择要备份哪些数据":"选择要备份哪些数据"
}

View file

@ -374,7 +374,7 @@
<index field-list="recordId,channelWith"/>
</entity>
<entity name="RevisionHistory" type-code="034" name-field="recordName" description="变更历史" queryable="false" parent="false">
<entity name="RevisionHistory" type-code="034" name-field="recordId" description="变更历史" queryable="false" parent="false">
<field name="revisionId" type="primary"/>
<field name="belongEntity" type="string" max-length="100" nullable="false" updatable="false" description="所属实体"/>
<field name="recordId" type="any-reference" nullable="false" updatable="false" cascade="ignore" description="记录 ID"/>

View file

@ -66,7 +66,7 @@
</div>
</div>
<div class="input-group input-search">
<input class="form-control" type="text" th:placeholder="${bundle.L('查询')}" />
<input class="form-control" type="text" th:placeholder="${bundle.L('快速查询')}" />
<span class="input-group-btn">
<button class="btn btn-secondary" type="button"><i class="icon zmdi zmdi-search"></i></button>
</span>

View file

@ -4,6 +4,13 @@
<th:block th:replace="~{/_include/header}" />
<meta name="page-help" content="https://getrebuild.com/docs/admin/integration-dingtalk" />
<title>[[${bundle.L('钉钉')}]]</title>
<style>
.J_syncUsers {
position: absolute;
margin-top: -6px;
margin-left: 6px;
}
</style>
</head>
<body>
<div class="rb-wrapper rb-fixed-sidebar rb-collapsible-sidebar rb-collapsible-sidebar-hide-logo rb-color-header" th:classappend="${sideCollapsedClazz}">
@ -70,8 +77,8 @@
<tr>
<td width="40%">[[${bundle.L('自动同步部门用户')}]]</td>
<td data-id="DingtalkSyncUsers" th:data-value="${DingtalkSyncUsers}">
<span class="mr-1">[[${DingtalkSyncUsers ? bundle.L('是') : bundle.L('否')}]]</span>
<button class="btn btn-light btn-sm J_syncUsers up-1" type="button"><i class="icon zmdi zmdi-refresh up-1"></i> [[${bundle.L('立即同步')}]]</button>
<span>[[${DingtalkSyncUsers ? bundle.L('是') : bundle.L('否')}]]</span>
<button class="btn btn-light btn-sm J_syncUsers" type="button"><i class="icon zmdi zmdi-refresh up-1"></i> [[${bundle.L('立即同步')}]]</button>
</td>
</tr>
<tr>

View file

@ -4,6 +4,13 @@
<th:block th:replace="~{/_include/header}" />
<meta name="page-help" content="https://getrebuild.com/docs/admin/integration-dingtalk" />
<title>[[${bundle.L('飞书')}]]</title>
<style>
.J_syncUsers {
position: absolute;
margin-top: -6px;
margin-left: 6px;
}
</style>
</head>
<body>
<div class="rb-wrapper rb-fixed-sidebar rb-collapsible-sidebar rb-collapsible-sidebar-hide-logo rb-color-header" th:classappend="${sideCollapsedClazz}">
@ -65,8 +72,8 @@
<tr>
<td width="40%">[[${bundle.L('自动同步部门用户')}]]</td>
<td data-id="FeishuSyncUsers" th:data-value="${FeishuSyncUsers}">
<span class="mr-1">[[${FeishuSyncUsers ? bundle.L('是') : bundle.L('否')}]]</span>
<button class="btn btn-light btn-sm J_syncUsers up-1" type="button"><i class="icon zmdi zmdi-refresh up-1"></i> [[${bundle.L('立即同步')}]]</button>
<span>[[${FeishuSyncUsers ? bundle.L('是') : bundle.L('否')}]]</span>
<button class="btn btn-light btn-sm J_syncUsers" type="button"><i class="icon zmdi zmdi-refresh up-1"></i> [[${bundle.L('立即同步')}]]</button>
</td>
</tr>
<tr>

View file

@ -4,6 +4,13 @@
<th:block th:replace="~{/_include/header}" />
<meta name="page-help" content="https://getrebuild.com/docs/admin/integration-wxwork" />
<title>[[${bundle.L('企业微信')}]]</title>
<style>
.J_syncUsers {
position: absolute;
margin-top: -6px;
margin-left: 6px;
}
</style>
</head>
<body>
<div class="rb-wrapper rb-fixed-sidebar rb-collapsible-sidebar rb-collapsible-sidebar-hide-logo rb-color-header" th:classappend="${sideCollapsedClazz}">
@ -70,8 +77,8 @@
<tr>
<td width="40%">[[${bundle.L('自动同步部门用户')}]]</td>
<td data-id="WxworkSyncUsers" th:data-value="${WxworkSyncUsers}">
<span class="mr-1">[[${WxworkSyncUsers ? bundle.L('是') : bundle.L('否')}]]</span>
<button class="btn btn-light btn-sm J_syncUsers up-1" type="button"><i class="icon zmdi zmdi-refresh up-1"></i> [[${bundle.L('立即同步')}]]</button>
<span>[[${WxworkSyncUsers ? bundle.L('是') : bundle.L('否')}]]</span>
<button class="btn btn-light btn-sm J_syncUsers" type="button"><i class="icon zmdi zmdi-refresh up-1"></i> [[${bundle.L('立即同步')}]]</button>
</td>
</tr>
<tr>

View file

@ -64,11 +64,15 @@
padding: 10px 15px;
display: none;
}
td.td-MobileAppPath .J_MobileAppPath-del {
display: none;
.J_backDb,
button.J_MobileAppPath,
button.J_MobileAppPath-del {
position: absolute;
margin-top: -6px;
margin-left: 6px;
}
td.td-MobileAppPath:hover .J_MobileAppPath-del {
display: inline-block;
button.J_MobileAppPath + button.J_MobileAppPath-del {
margin-left: 70px;
}
</style>
</head>
@ -242,7 +246,8 @@
<tr>
<td width="40%">[[${bundle.L('数据自动备份')}]]</td>
<td data-id="DBBackupsEnable" th:data-value="${DBBackupsEnable}" th:data-form-text="${bundle.L('每日 0 点备份到数据目录,请预留足够磁盘空间')}">
[[${DBBackupsEnable ? bundle.L('是') : bundle.L('否')}]]
<span>[[${DBBackupsEnable ? bundle.L('是') : bundle.L('否')}]]</span>
<button class="btn btn-light btn-sm J_backDb" type="button"><i class="icon mdi mdi-database-export-outline"></i> [[${bundle.L('立即备份')}]]</button>
</td>
</tr>
<tr>
@ -291,10 +296,12 @@
<tr>
<td>[[${bundle.L('APP 安装包')}]] <sup class="rbv"></sup></td>
<td class="td-MobileAppPath">
<a class="J_MobileAppPath hide-empty mr-1 link" target="_blank" href="../h5app-download">[[${MobileAppPath}]]</a>
<input type="file" class="hide J_MobileAppPath" accept=".apk" />
<button class="btn btn-light btn-sm J_MobileAppPath" type="button"><i class="icon zmdi zmdi-upload"></i> [[${bundle.L('上传')}]]<span></span></button>
<button class="btn btn-light w-auto btn-sm J_MobileAppPath-del hide" type="button" th:title="${bundle.L('删除')}"><i class="icon zmdi zmdi-delete"></i></button>
<a>[[${MobileAppPath ?:bundle.L('无')}]]</a>
<button class="btn btn-light btn-sm J_MobileAppPath" type="button"><i class="icon mdi mdi-upload"></i> [[${bundle.L('上传')}]]<span></span></button>
<button class="btn btn-light btn-sm J_MobileAppPath-del w-auto hide" type="button" th:title="${bundle.L('删除')}"><i class="icon mdi mdi-delete"></i></button>
<div class="hide">
<input type="file" class="hide J_MobileAppPath" accept=".apk" />
</div>
</td>
</tr>
</tbody>

View file

@ -69,6 +69,8 @@ $(document).ready(() => {
})
}
})
$('.J_backDb').on('click', () => renderRbcomp(<DlgBackup />))
})
useEditComp = function (name) {
@ -285,13 +287,15 @@ class DlgMM extends RbAlert {
$(document).ready(() => {
if (rb.commercial < 1) {
$('button.J_MobileAppPath').attr('disabled', true)
$('.td-MobileAppPath button').remove()
return
}
const renderMobileAppPath = function (key) {
const file = $fileCutName(key)
$('a.J_MobileAppPath').text(file)
$('.td-MobileAppPath>a').text($fileCutName(key)).attr({
href: '../h5app-download',
target: '_blank',
})
$('button.J_MobileAppPath-del').removeClass('hide')
}
@ -316,22 +320,66 @@ $(document).ready(() => {
)
$('button.J_MobileAppPath').on('click', () => $input[0].click())
const $del = $('button.J_MobileAppPath-del').on('click', () => {
$('button.J_MobileAppPath-del').on('click', () => {
RbAlert.create($L('确认删除 APP 安装包'), {
onConfirm: function () {
this.hide()
$.post(location.href, JSON.stringify({ MobileAppPath: '' }), (res) => {
if (res.error_code === 0) {
$('a.J_MobileAppPath').removeAttr('href').text('')
$del.addClass('hide')
} else {
RbHighbar.error(res.error_msg)
}
$.post(location.href, JSON.stringify({ MobileAppPath: '' }), () => {
location.reload()
})
},
})
})
const key = $('a.J_MobileAppPath').text()
if (key) renderMobileAppPath(key)
const apk = $('.td-MobileAppPath>a').text()
if (apk && apk.length > 20) renderMobileAppPath(apk)
})
class DlgBackup extends RbAlert {
state = { ...this.props }
renderContent() {
return (
<form className="rbalert-form-sm">
<div className="form-group mb-0">
<label className="text-bold">{$L('选择要备份哪些数据')}</label>
<div ref={(c) => (this._$bkType = c)}>
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input className="custom-control-input" type="checkbox" defaultChecked />
<span className="custom-control-label">{$L('数据库')}</span>
</label>
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input className="custom-control-input" type="checkbox" defaultChecked />
<span className="custom-control-label">{$L('数据目录文件')}</span>
</label>
</div>
</div>
<div className="form-group mb-1">
<div className="text-warning mb-1" ref={(c) => (this._$tips = c)}>
<i className="mdi-alert-outline mdi" /> {$L('请勿在业务高峰时段执行备份')}
</div>
<button type="button" className="btn btn-space btn-primary" onClick={this.confirm} disabled={this.state.disable}>
{$L('开始备份')}
</button>
</div>
</form>
)
}
confirm = () => {
let type = ($(this._$bkType).find('input:eq(0)').prop('checked') ? 1 : 0) + ($(this._$bkType).find('input:eq(1)').prop('checked') ? 2 : 0)
if (type === 0) return
this.disabled(true, true)
$.post(`systems/backup?type=${type}`, (res) => {
if (res.error_code === 0) {
const data = res.data || {}
const tips = [$L('数据库'), ' <code>', data.db || $L('未备份'), '</code><br>', $L('数据目录文件'), ' <code>', data.file || $L('未备份'), '</code>']
$(this._$tips).html(tips.join(''))
} else {
RbHighbar.error(res.error_msg)
}
this.disabled(false, false)
})
}
}

View file

@ -1090,9 +1090,8 @@ class RbList extends React.Component {
},
})
// 首次由 AdvFilter 加载
// 首次由外部查询 eg.AdvFilter
if (wpc.advFilter !== true) this.fetchList(this._buildQuick())
// 按键操作
if (wpc.type === 'RecordList' || wpc.type === 'DetailList') $(document).on('keydown', (e) => this._keyEvent(e))
}

View file

@ -785,8 +785,8 @@ const RbViewPage = {
$(`<li>${$L('无')}</li>`).appendTo($into)
}
// v3.6
$('.view-history.invisible2').removeClass('invisible2')
$('.view-history legend a').attr('href', `${rb.baseUrl}/admin/audit/revision-history#gs=${this.__id}`)
})
},

View file

@ -104,7 +104,9 @@
<div class="view-history invisible2" th:if="${ShowViewHistory}">
<div class="form-line">
<fieldset>
<legend>[[${bundle.L('修改历史')}]] <i class="zmdi zmdi-help zicon" data-toggle="tooltip" th:title="${bundle.L('修改历史明细可至管理中心 (变更历史) 查看')}"></i></legend>
<legend>
[[${bundle.L('修改历史')}]] <a class="admin-show inline" target="_blank" th:title="${bundle.L('查看详情')}"><i class="zicon mdi mdi-clock-edit-outline down-1"></i></a>
</legend>
</fieldset>
</div>
<ul class="view-history-items list-unstyled"></ul>