Merge pull request #435 from getrebuild/better-2.8

Better v2.8
This commit is contained in:
RB 2022-02-18 17:21:59 +08:00 committed by GitHub
commit 03a73ef611
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 439 additions and 148 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 2c48b6768082e5930ad02c16d4f25dd767d4ee97
Subproject commit 45de87cc87af501e13783e4425d90ddaebef12b4

15
pom.xml
View file

@ -5,14 +5,14 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<version>2.6.3</version>
<relativePath/>
</parent>
<groupId>com.rebuild</groupId>
<artifactId>rebuild</artifactId>
<version>2.8.0-dev</version>
<name>rebuild</name>
<description>RB V2 use SpringBoot</description>
<description>Building your business-systems freely!</description>
<!-- UNCOMMENT USE TOMCAT -->
<!-- <packaging>war</packaging> -->
@ -238,7 +238,6 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
@ -289,6 +288,7 @@
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
@ -313,11 +313,12 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.9.1</version>
<version>7.9.3</version>
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
@ -327,7 +328,7 @@
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.0.0</version>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
@ -403,7 +404,7 @@
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.15</version>
<version>0.4.16</version>
</dependency>
<dependency>
<groupId>es.moki.ratelimitj</groupId>
@ -418,7 +419,7 @@
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.7</version>
<version>3.16.8</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>

View file

@ -31,7 +31,9 @@ import javax.management.ObjectName;
import javax.management.Query;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.SessionTrackingMode;
import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@ -123,6 +125,11 @@ public class BootApplication extends SpringBootServletInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
CONTEXT_PATH = StringUtils.defaultIfBlank(servletContext.getContextPath(), "");
// Remove `jsession`
servletContext.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.COOKIE));
servletContext.getSessionCookieConfig().setHttpOnly(true);
super.onStartup(servletContext);
}

View file

@ -26,6 +26,7 @@ import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -48,7 +49,8 @@ public class BootConfiguration implements InstallState {
* Fake instance
* FIXME 直接 `==` 比较不安全 ???
*/
public static final JedisPool USE_EHCACHE = new JedisPool();
public static final JedisPool USE_EHCACHE = new JedisPool(
KnownJedisPool.DEFAULT_CONFIG, "127.0.0.1", 6379);
@Bean
JedisPool createJedisPool() {

View file

@ -67,7 +67,7 @@ public class BootEnvironmentPostProcessor implements EnvironmentPostProcessor, I
try {
Properties temp = PropertiesLoaderUtils.loadProperties(new FileSystemResource(installed));
Properties filePs = new Properties();
// 兼容 V1
// compatible: v1.x
for (String name : temp.stringPropertyNames()) {
String value = temp.getProperty(name);
if (name.endsWith(".aes")) {

View file

@ -22,6 +22,7 @@ import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.HashSet;
@ -121,7 +122,7 @@ public class ViewAddonsManager extends BaseLayoutManager {
return JSONUtils.toJSONObject("items", useRefs);
}
// fix: v2.2 兼容
// compatible: v2.2
JSON configJson = config.getJSON("config");
if (configJson instanceof JSONArray) {
configJson = JSONUtils.toJSONObject("items", configJson);
@ -129,8 +130,18 @@ public class ViewAddonsManager extends BaseLayoutManager {
JSONArray addons = new JSONArray();
for (Object o : ((JSONObject) configJson).getJSONArray ("items")) {
String key;
String label = null;
// compatible: v2.8
if (o instanceof JSONArray) {
key = (String) ((JSONArray) o).get(0);
label = (String) ((JSONArray) o).get(1);
} else {
key = o.toString();
}
// Entity.Field (v1.9)
String[] ef = ((String) o).split("\\.");
String[] ef = key.split("\\.");
if (!MetadataHelper.containsEntity(ef[0])) {
continue;
}
@ -141,11 +152,12 @@ public class ViewAddonsManager extends BaseLayoutManager {
}
if (Application.getPrivilegesManager().allow(user, addonEntity.getEntityCode(), useAction)) {
if (ef.length > 1) {
addons.add(getEntityShow(addonEntity.getField(ef[1]), mfRefs, applyType));
} else {
addons.add(EasyMetaFactory.toJSON(addonEntity));
}
JSONObject show = ef.length > 1
? getEntityShow(addonEntity.getField(ef[1]), mfRefs, applyType)
: EasyMetaFactory.toJSON(addonEntity);
if (StringUtils.isNotBlank(label)) show.put("entityLabel", label);
addons.add(show);
}
}

View file

@ -439,7 +439,8 @@ public class GeneralEntityService extends ObservableService implements EntitySer
if (state == ApprovalState.APPROVED || state == ApprovalState.PROCESSING) {
throw new DataSpecificationException(state == ApprovalState.APPROVED
? Language.L("主记录已完成审批,不能添加明细") : Language.L("主记录正在审批中,不能添加明细"));
? Language.L("主记录已完成审批,不能添加明细")
: Language.L("主记录正在审批中,不能添加明细"));
}
}
@ -482,7 +483,8 @@ public class GeneralEntityService extends ObservableService implements EntitySer
}
throw new DataSpecificationException(currentState == ApprovalState.APPROVED
? Language.L("%s已完成审批禁止操作", recordType) : Language.L("%s正在审批中禁止操作", recordType));
? Language.L("%s已完成审批禁止操作", recordType)
: Language.L("%s正在审批中禁止操作", recordType));
}
}
}

View file

@ -75,7 +75,7 @@ public class FieldAggregation implements TriggerAction {
* @param context
*/
public FieldAggregation(ActionContext context) {
this(context, Boolean.TRUE, 5);
this(context, Boolean.TRUE, 9);
}
/**

View file

@ -64,7 +64,7 @@ public enum ConfigurationItem {
RecycleBinKeepingDays(180),
// 启用数据库备份
DBBackupsEnable(false),
DBBackupsEnable(true),
// 数据备份保留时间0为禁用
DBBackupsKeepingDays(180),
@ -88,6 +88,8 @@ public enum ConfigurationItem {
AllowUsesTime,
// 允许使用 IP
AllowUsesIp,
// 2FA
Login2FAMode(0),
// DingTalk
DingtalkAgentid, DingtalkAppkey, DingtalkAppsecret, DingtalkCorpid,

View file

@ -1,5 +1,8 @@
/*
Copyright (c) Ruifang Tech <http://ruifang-tech.com/> and/or its owners. All rights reserved.
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.core.support.distributed;

View file

@ -18,13 +18,20 @@ public class KnownJedisPool extends JedisPool {
public static final int TIMEOUT = 5000;
public static final JedisPoolConfig DEFAULT_CONFIG = new JedisPoolConfig() {
@Override
public boolean getJmxEnabled() {
return false;
}
};
private String host;
private int port;
private String password;
private int database;
public KnownJedisPool(String host, int port, String password, int database) {
super(new JedisPoolConfig(), host, port, TIMEOUT, password, database);
super(DEFAULT_CONFIG, host, port, TIMEOUT, password, database);
this.host = host;
this.port = port;
this.password = password;

View file

@ -89,7 +89,7 @@ public class DataListBuilderImpl implements DataListBuilder {
for (int i = 1; i < count.length; i++) {
Map<String, Object> cfg = countFields.get(i);
Field field = entity.getField((String) cfg.get("field"));
String label = (String) cfg.get("label");
String label = (String) cfg.get("label2");
if (StringUtils.isBlank(label)) {
String calc = (String) cfg.get("calc");
label = String.format("%s (%s)", Language.L(field), FormatCalc.valueOf(calc).getLabel());

View file

@ -1,5 +1,8 @@
/*
Copyright (c) Ruifang Tech <http://ruifang-tech.com/> and/or its owners. All rights reserved.
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.core.support.general;

View file

@ -1,5 +1,8 @@
/*
Copyright (c) Ruifang Tech <http://ruifang-tech.com/> and/or its owners. All rights reserved.
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.core.support.setup;

View file

@ -1,5 +1,8 @@
/*
Copyright (c) Ruifang Tech <http://ruifang-tech.com/> and/or its owners. All rights reserved.
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.core.support.setup;

View file

@ -24,7 +24,7 @@ import java.io.InputStreamReader;
/**
* 数据库备份
* - `mysqldump[.exe]` 命令必须在环境变量中
* - 除了本库还要有全局的 `RELOAD` 权限
* - 除了本库还要有全局的 `RELOAD` or `FLUSH_TABLES` and `PROCESS` 权限
*
* @author devezhao
* @since 2020/2/4
@ -62,39 +62,34 @@ public class DatabaseBackup {
File dest = new File(backups, destName);
String cmd = String.format(
"mysqldump -u%s -p%s -h%s -P%s --default-character-set=utf8 --opt --extended-insert=true --triggers -R --hex-blob -x %s>%s",
"-u%s -p%s -h%s -P%s --default-character-set=utf8 --opt --extended-insert=true --triggers --hex-blob -R -x %s>%s",
user, passwd, host, port, dbname, dest.getAbsolutePath());
Process process;
ProcessBuilder builder = new ProcessBuilder();
String encoding = "UTF-8";
if (SystemUtils.IS_OS_WINDOWS) {
cmd = cmd.replaceFirst("mysqldump", "cmd /c mysqldump.exe");
process = Runtime.getRuntime().exec(cmd);
builder.command("cmd.exe", "/c", "mysqldump.exe " + cmd);
encoding = "GBK";
}
// for Linux
else {
process = Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", cmd });
} else {
// for Linux/Unix
builder.command("/bin/sh", "-c", "mysqldump " + cmd);
}
BufferedReader readerError = null;
builder.redirectErrorStream(true);
Process process = builder.start();
BufferedReader reader = null;
StringBuilder echo = new StringBuilder();
try {
readerError = new BufferedReader(new InputStreamReader(process.getErrorStream(), encoding));
reader = new BufferedReader(new InputStreamReader(process.getInputStream(), encoding));
String line;
while ((line = readerError.readLine()) != null) {
echo.append(line).append("\n");
}
while ((line = reader.readLine()) != null) {
echo.append(line).append("\n");
}
} finally {
IOUtils.closeQuietly(readerError);
IOUtils.closeQuietly(reader);
process.destroy();
}
@ -113,7 +108,7 @@ public class DatabaseBackup {
File zip = new File(backups, destName + ".zip");
try {
CompressUtils.forceZip(dest, zip, null);
FileUtils.deleteQuietly(dest);
dest = zip;
} catch (Exception e) {

View file

@ -13,6 +13,8 @@ import com.rebuild.core.Application;
import com.rebuild.core.cache.CommonsCache;
import lombok.extern.slf4j.Slf4j;
import java.util.regex.Pattern;
/**
* 位置工具
*
@ -22,6 +24,9 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LocationUtils {
private static final Pattern PRIVATE_IP = Pattern.compile("(localhost)|" +
"(^127\\.)|(^10\\.)|(^172\\.1[6-9]\\.)|(^172\\.2[0-9]\\.)|(^172\\.3[0-1]\\.)|(^192\\.168\\.)");
/**
* 获取 IP 所在位置
*
@ -40,6 +45,10 @@ public class LocationUtils {
* @return
*/
public static JSON getLocation(String ip, boolean useCache) {
if (PRIVATE_IP.matcher(ip).find()) {
return JSONUtils.toJSONObject(new String[] { "ip", "country"}, new String[] { ip, "R" });
}
JSONObject result;
if (useCache) {
result = (JSONObject) Application.getCommonsCache().getx("IPLocation2" + ip);

View file

@ -140,15 +140,15 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
String sidebarCollapsed = ServletUtils.readCookie(request, "rb.sidebarCollapsed");
String sideCollapsedClazz = BooleanUtils.toBoolean(sidebarCollapsed) ? "rb-collapsible-sidebar-collapsed" : "";
// Aside collapsed
if (!(requestEntry.getRequestUri().contains("/admin/") || requestEntry.getRequestUri().contains("/setup/"))) {
if (!(requestUri.contains("/admin/") || requestUri.contains("/setup/"))) {
String asideCollapsed = ServletUtils.readCookie(request, "rb.asideCollapsed");
if (BooleanUtils.toBoolean(asideCollapsed)) sideCollapsedClazz += " rb-aside-collapsed";
}
request.setAttribute("sideCollapsedClazz", sideCollapsedClazz);
}
// 超管设置仍可访问
skipCheckSafeUse = adminVerified || UserHelper.isSuperAdmin(requestUser);
// 超管可访问
skipCheckSafeUse = UserHelper.isSuperAdmin(requestUser);
} else if (!isIgnoreAuth(requestUri)) {
// 外部表单特殊处理媒体字段上传/预览
@ -169,7 +169,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
skipCheckSafeUse = true;
}
if (!skipCheckSafeUse) checkSafeUse(ipAddr);
if (!skipCheckSafeUse) checkSafeUse(ipAddr, requestUri);
return true;
}
@ -285,11 +285,11 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
response.sendRedirect(fullUrl);
}
private void checkSafeUse(String ipAddr) throws DefinedException {
private void checkSafeUse(String ipAddr, String requestUri) throws DefinedException {
if (!License.isRbvAttached()) return;
if (ipAddr.equals("localhost") || ipAddr.equals("127.0.0.1")) {
log.warn("Allow localhost uses ");
log.warn("Allow localhost/127.0.0.1 use : {}", requestUri);
return;
}

View file

@ -71,7 +71,7 @@ public class ViewAddonsController extends BaseController {
String applyType = getParameter(request, "type", ViewAddonsManager.TYPE_TAB);
ConfigBean config = ViewAddonsManager.instance.getLayout(user, entity, applyType);
// fix: v2.2 兼容
// compatible: v2.2
JSON configJson = config == null ? null : config.getJSON("config");
if (configJson instanceof JSONArray) {
configJson = JSONUtils.toJSONObject("items", configJson);

View file

@ -51,16 +51,14 @@ public class GeneralListController extends EntityController {
if (listEntity == null) return null;
final EasyEntity easyEntity = EasyMetaFactory.valueOf(listEntity);
// 使用主实体列表配置
final EasyEntity mainEntity = listEntity.getMainEntity() == null
? easyEntity : EasyMetaFactory.valueOf(listEntity.getMainEntity());
String listPage = listEntity.getMainEntity() != null ? "/general/detail-list" : "/general/record-list";
Integer listMode = getIntParameter(request, "forceListMode");
if (listMode == null) {
if (listEntity.getMainEntity() == null) {
listMode = ObjectUtils.toInt(easyEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_MODE), 1);
} else {
listMode = ObjectUtils.toInt(EasyMetaFactory.valueOf(
listEntity.getMainEntity()).getExtraAttr(EasyEntityConfigProps.ADV_LIST_MODE), 1);
}
listMode = ObjectUtils.toInt(mainEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_MODE), 1);
}
if (listMode == 2) {
listPage = "/general/record-list-2"; // Mode2
@ -82,8 +80,8 @@ public class GeneralListController extends EntityController {
listConfig = DataListManager.instance.getFieldsLayout(entity, user);
// 扩展配置
String advListHideFilters = easyEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_HIDE_FILTERS);
String advListHideCharts = easyEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_HIDE_CHARTS);
String advListHideFilters = mainEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_HIDE_FILTERS);
String advListHideCharts = mainEntity.getExtraAttr(EasyEntityConfigProps.ADV_LIST_HIDE_CHARTS);
mv.getModel().put(EasyEntityConfigProps.ADV_LIST_HIDE_FILTERS, advListHideFilters);
mv.getModel().put(EasyEntityConfigProps.ADV_LIST_HIDE_CHARTS, advListHideCharts);
mv.getModel().put("hideAside",

View file

@ -2086,9 +2086,7 @@
"其他":"其他",
"擦除":"擦除",
"你的 IP 地址不在允许范围内":"你的 IP 地址不在允许范围内",
"每个时间(段)一行,如 `10` `9-18`":"每个时间(段)一行,如 `10` `9-18`",
"日期字段格式不兼容":"日期字段格式不兼容",
"每个 IP 一行,如 `192.168.*` `192.168.10.*`":"每个 IP 一行,如 `192.168.*` `192.168.10.*`",
"取消置顶":"取消置顶",
"我创建的":"我创建的",
"请选择要添加的常用查询":"请选择要添加的常用查询",
@ -2107,5 +2105,16 @@
"免费版不支持高级功能 [(查看详情)](https://getrebuild.com/docs/rbv-features)":"免费版不支持高级功能 [(查看详情)](https://getrebuild.com/docs/rbv-features)",
"常用查询方便以后使用":"常用查询方便以后使用",
"邮编":"邮编",
"高级":"高级"
"高级":"高级",
"标记已读":"标记已读",
"仅邮箱":"仅邮箱",
"输入名称保存":"输入名称保存",
"手机或邮箱":"手机或邮箱",
"请确保邮件/短信配置正确,否则无法发送/接收验证码,导致无法登录":"请确保邮件/短信配置正确,否则无法发送/接收验证码,导致无法登录",
"仅手机":"仅手机",
"仅指定时间可使用,每个时间一行,如 `10` `9-18` 等":"仅指定时间可使用,每个时间一行,如 `10` `9-18` 等",
"不启用":"不启用",
"启用两步验证":"启用两步验证",
"仅指定 IP 可使用,每个 IP 一行,如 `192.168.*` `192.168.10.*` 等":"仅指定 IP 可使用,每个 IP 一行,如 `192.168.*` `192.168.10.*` 等",
"或联系系统管理员":"或联系系统管理员"
}

View file

@ -3,6 +3,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="renderer" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="manifest" th:href="@{/assets/manifest.json}" />
<link rel="shortcut icon" th:href="@{/assets/img/favicon.png}" />
<link rel="apple-touch-icon" th:href="@{/assets/img/favicon.png}" />
<link rel="stylesheet" type="text/css" th:href="@{/assets/lib/material-design-iconic-font.min.css}" />

View file

@ -25,6 +25,10 @@
.set-items .item > span {
margin: 0 4px;
}
.axis-target .ui-sortable-placeholder.ui-state-highlight {
width: 7px;
height: 0;
}
</style>
</head>
<body class="dialog">
@ -54,6 +58,7 @@
</div>
</div>
<th:block th:replace="~{/_include/footer}" />
<script th:src="@{/assets/js/show-styles.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/list-stats.js}" type="text/babel"></script>
</body>
</html>

View file

@ -38,49 +38,7 @@
</div>
<th:block th:replace="~{/_include/footer}" />
<script th:src="@{/assets/js/sortable.js}"></script>
<script type="text/babel">
$(document).ready(function () {
const entity = $urlp('entity'),
type = $urlp('type')
const url = `/admin/entity/${entity}/view-addons?type=${type}`
$.get(url, function (res) {
$(res.data.refs).each(function () {
render_unset(this)
})
if (res.data.config) {
$(res.data.config.items).each(function () {
$('.unset-list li[data-key="' + this + '"]').trigger('click')
})
$('#relatedAutoExpand').attr('checked', res.data.config.autoExpand === true)
$('#relatedAutoHide').attr('checked', res.data.config.autoHide === true)
}
if (!res.data.refs || res.data.refs.length === 0) {
$(`<li class="dd-item nodata">${$L('暂无数据')}</li>`).appendTo('.unset-list')
}
})
const $btn = $('.J_save').click(function () {
let config = []
$('.J_config>li').each(function () {
config.push($(this).data('key'))
})
config = {
items: config,
autoExpand: $val('#relatedAutoExpand'),
autoHide: $val('#relatedAutoHide'),
}
$btn.button('loading')
$.post(url, JSON.stringify(config), function (res) {
$btn.button('reset')
if (res.error_code === 0) parent.location.reload()
else RbHighbar.error(res.error_msg)
})
})
})
</script>
<script th:src="@{/assets/js/show-styles.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/view-addons.js}" type="text/babel"></script>
</body>
</html>

View file

@ -160,11 +160,19 @@
</tr>
<tr>
<td>[[${bundle.L('登录密码过期时间')}]] <sup class="rbv"></sup></td>
<td th:data-id="${commercial > 0 ? 'PasswordExpiredDays' : ''}" th:data-value="${PasswordExpiredDays}">[[${PasswordExpiredDays}]] [[${bundle.L('天')}]]</td>
<td th:data-id="${commercial > 0 ? 'PasswordExpiredDays' : ''}" th:data-value="${PasswordExpiredDays}">
<th:block th:if="${PasswordExpiredDays == '0'}">[[${bundle.L('不启用')}]]</th:block>
<th:block th:if="${PasswordExpiredDays != '0'}">[[${PasswordExpiredDays}]] [[${bundle.L('天')}]]</th:block>
</td>
</tr>
<tr>
<td>[[${bundle.L('同一用户允许多地登录')}]] <sup class="rbv"></sup></td>
<td th:data-id="${commercial > 0 ? 'MultipleSessions' : ''}" th:data-value="${MultipleSessions}">[[${MultipleSessions ? bundle.L('是') : bundle.L('否')}]]</td>
<tr class="bosskey-show">
<td>[[${bundle.L('启用两步验证')}]] <sup class="rbv"></sup></td>
<td th:data-id="${commercial > 1 ? 'Login2FAMode' : ''}" th:data-value="${Login2FAMode}" th:data-form-text="${bundle.L('请确保邮件/短信配置正确,否则无法发送/接收验证码,导致无法登录')}">
<th:block th:if="${Login2FAMode == '0'}">[[${bundle.L('不启用')}]]</th:block>
<th:block th:if="${Login2FAMode == '1'}">[[${bundle.L('手机或邮箱')}]]</th:block>
<th:block th:if="${Login2FAMode == '2'}">[[${bundle.L('仅手机')}]]</th:block>
<th:block th:if="${Login2FAMode == '3'}">[[${bundle.L('仅邮箱')}]]</th:block>
</td>
</tr>
<tr>
<td>[[${bundle.L('允许使用时间')}]] <sup class="rbv"></sup></td>
@ -172,7 +180,7 @@
th:data-id="${commercial > 1 ? 'AllowUsesTime' : ''}"
th:data-value="${AllowUsesTime}"
data-optional="true"
th:data-form-text="${bundle.L('每个时间(段)一行,如 `10` `9-18`')}"
th:data-form-text="${bundle.L('仅指定时间可使用,每个时间一行,如 `10` `9-18`')}"
>
<pre class="unstyle">[[${AllowUsesTime ?:bundle.L('不限')}]]</pre>
</td>
@ -183,10 +191,14 @@
th:data-id="${commercial > 1 ? 'AllowUsesIp' : ''}"
th:data-value="${AllowUsesIp}"
data-optional="true"
th:data-form-text="${bundle.L('每个 IP 一行,如 `192.168.*` `192.168.10.*`')}"
th:data-form-text="${bundle.L('仅指定 IP 可使用,每个 IP 一行,如 `192.168.*` `192.168.10.*`')}"
>
<pre class="unstyle">[[${AllowUsesIp ?:bundle.L('不限')}]]</pre>
</td>
<tr class="bosskey-show">
<td>[[${bundle.L('同一用户允许多地登录')}]] <sup class="rbv"></sup></td>
<td th:data-id="${commercial > 0 ? 'MultipleSessions' : ''}" th:data-value="${MultipleSessions}">[[${MultipleSessions ? bundle.L('是') : bundle.L('否')}]]</td>
</tr>
</tr>
</tbody>
</table>
@ -202,15 +214,21 @@
</tr>
<tr>
<td>[[${bundle.L('备份保留时间')}]]</td>
<td data-id="DBBackupsKeepingDays" th:data-value="${DBBackupsKeepingDays}">[[${DBBackupsKeepingDays}]] [[${bundle.L('天')}]]</td>
<td data-id="DBBackupsKeepingDays" th:data-value="${DBBackupsKeepingDays}">
[[${DBBackupsKeepingDays}]] [[${bundle.L('天')}]]
</td>
</tr>
<tr>
<td>[[${bundle.L('变更历史保留时间')}]]</td>
<td data-id="RevisionHistoryKeepingDays" th:data-value="${RevisionHistoryKeepingDays}">[[${RevisionHistoryKeepingDays}]] [[${bundle.L('天')}]]</td>
<td data-id="RevisionHistoryKeepingDays" th:data-value="${RevisionHistoryKeepingDays}">
[[${RevisionHistoryKeepingDays}]] [[${bundle.L('天')}]]
</td>
</tr>
<tr>
<td>[[${bundle.L('回收站保留时间')}]]</td>
<td data-id="RecycleBinKeepingDays" th:data-value="${RecycleBinKeepingDays}">[[${RecycleBinKeepingDays}]] [[${bundle.L('天')}]]</td>
<td data-id="RecycleBinKeepingDays" th:data-value="${RecycleBinKeepingDays}">
[[${RecycleBinKeepingDays}]] [[${bundle.L('天')}]]
</td>
</tr>
</tbody>
</table>

View file

@ -108,7 +108,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
margin-right: 3px;
margin-bottom: 2px;
position: relative;
cursor: default;
cursor: pointer;
white-space: nowrap;
}
@ -154,7 +154,6 @@ See LICENSE and COMMERCIAL in the project root for license information.
}
.axis-target .dropdown-menu .dropdown-item {
cursor: pointer;
padding: 6px 14px;
}

View file

@ -36,6 +36,11 @@ code {
color: #e83e8c;
}
a,
button {
cursor: pointer;
}
/* 1280 */
.container-smart {
max-width: 1160px;
@ -48,6 +53,11 @@ code {
max-width: 300px;
}
.dropdown-menu .dropdown-item,
.dropdown-menu .dropdown-item > a {
cursor: default;
}
.dropdown-menu.auto-scroller {
max-height: 500px;
overflow-x: auto;
@ -621,7 +631,6 @@ div.dataTables_filter .filter-badge:hover .close {
.adv-search .dropdown-item {
position: relative;
cursor: pointer;
}
.adv-search .dropdown-item a {
@ -802,10 +811,6 @@ textarea.row3x {
resize: none;
}
a {
cursor: pointer;
}
.mlr-auto {
margin-left: auto !important;
margin-right: auto !important;
@ -1968,7 +1973,6 @@ th.column-fixed {
.aside-tree li > a {
display: block;
padding: 8px 10px;
cursor: pointer;
color: #444;
}
@ -3616,11 +3620,11 @@ a.icon-link > .zmdi {
.syscfg h5 {
background-color: #eee;
margin: 0;
padding: 10px;
padding: 12px 10px;
}
.syscfg .table td {
padding: 10px;
padding: 12px 10px;
}
.syscfg .table td p {
@ -4563,3 +4567,7 @@ pre.unstyle {
left: -48px;
box-shadow: inset -10px 0 8px -8px rgb(0 0 0 / 15%);
}
.pointer {
cursor: pointer !important;
}

View file

@ -215,6 +215,12 @@ See LICENSE and COMMERCIAL in the project root for license information.
color: #fff !important;
}
.formula-calc ul li > a[data-toggle]::after {
content: '\f2f9';
font-family: 'Material-Design-Iconic-Font';
margin-left: 2px;
}
.formula-calc .fields {
max-height: 186px;
margin-top: 1px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -62,6 +62,15 @@ useEditComp = function (name) {
)
} else if ('PageFooter' === name || 'AllowUsesTime' === name || 'AllowUsesIp' === name) {
return <textarea name={name} className="form-control form-control-sm row3x" maxLength="600" />
} else if ('Login2FAMode' === name) {
return (
<select className="form-control form-control-sm">
<option value="0">{$L('不启用')}</option>
<option value="1">{$L('手机或邮箱')}</option>
<option value="2">{$L('仅手机')}</option>
<option value="3">{$L('仅邮箱')}</option>
</select>
)
}
}

View file

@ -33,14 +33,15 @@ $(document).ready(function () {
parent.RbModal.resize()
})
// // 字段排序 FIXME 拖动布局错乱
// $('.set-items')
// .sortable({
// containment: 'parent',
// cursor: 'move',
// opacity: 0.8,
// })
// .disableSelection()
// // 字段排序
$('.set-items')
.sortable({
containment: 'parent',
cursor: 'move',
opacity: 0.8,
placeholder: 'ui-state-highlight',
})
.disableSelection()
const $btn = $('.J_save').on('click', () => {
if (rb.commercial < 1) {
@ -51,7 +52,11 @@ $(document).ready(function () {
const config = { items: [] }
$('.set-items > span').each(function () {
const $this = $(this)
config.items.push({ field: $this.attr('data-field'), calc: $this.attr('data-calc'), label: $this.attr('data-label') })
config.items.push({
field: $this.attr('data-field'),
calc: $this.attr('data-calc'),
label2: $this.attr('data-label'),
})
})
$btn.button('loading')
@ -71,6 +76,8 @@ const CALC_TYPES = {
'MIN': $L('最小值'),
}
const ShowStyles_Comps = {}
const render_set = function (item) {
const len = $('.set-items > span').length
if (len >= 3) $('.J_tips').removeClass('hide')
@ -83,7 +90,7 @@ const render_set = function (item) {
$to.find('>span.text-muted').remove()
const calc = item.calc || 'SUM'
const $item = $(`<span data-field="${item.name}" data-calc="${calc}" data-label="${item.specLabel || ''}"></span>`).appendTo($to)
const $item = $(`<span data-field="${item.name}" data-calc="${calc}" data-label="${item.label2 || ''}"></span>`).appendTo($to)
const $a = $(
`<div class="item" data-toggle="dropdown"><a><i class="zmdi zmdi-chevron-down"></i></a><span>${item.label} (${CALC_TYPES[calc]})</span><a class="del"><i class="zmdi zmdi-close-circle"></i></a></div>`
@ -97,11 +104,33 @@ const render_set = function (item) {
for (let k in CALC_TYPES) {
$(`<li class="dropdown-item" data-calc=${k}>${CALC_TYPES[k]}</li>`).appendTo($ul)
}
// $('<li class="dropdown-divider"></li>').appendTo($ul)
// $(`<li class="dropdown-item" data-calc='_LABEL'>${$L('显示样式')}</li>`).appendTo($ul)
$('<li class="dropdown-divider"></li>').appendTo($ul)
$(`<li class="dropdown-item" data-calc='_LABEL'>${$L('显示样式')}</li>`).appendTo($ul)
$ul.find('.dropdown-item').on('click', function () {
const calc = $(this).data('calc')
$item.attr('data-calc', calc).find('.item > span').text(`${item.label} (${CALC_TYPES[calc]})`)
if (calc === '_LABEL') {
if (ShowStyles_Comps[item.name]) {
ShowStyles_Comps[item.name].show()
} else {
renderRbcomp(
// eslint-disable-next-line react/jsx-no-undef
<ShowStyles
label={item.label2}
onConfirm={(s) => {
$item.attr({
'data-label': s.label || '',
})
}}
/>,
null,
function () {
ShowStyles_Comps[item.name] = this
}
)
}
} else {
$item.attr('data-calc', calc).find('.item > span').text(`${item.label} (${CALC_TYPES[calc]})`)
}
})
}

View file

@ -0,0 +1,90 @@
/*
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.
*/
const _configLabels = {}
$(document).ready(function () {
const entity = $urlp('entity'),
type = $urlp('type')
const url = `/admin/entity/${entity}/view-addons?type=${type}`
$.get(url, function (res) {
$(res.data.refs).each(function () {
// eslint-disable-next-line no-undef
render_unset(this)
})
if (res.data.config) {
$(res.data.config.items).each(function () {
let key = this
// compatible: v2.8
if (typeof this !== 'string') {
key = this[0]
_configLabels[key] = this[1]
}
$(`.unset-list li[data-key="${key}"]`).trigger('click')
})
$('#relatedAutoExpand').attr('checked', res.data.config.autoExpand === true)
$('#relatedAutoHide').attr('checked', res.data.config.autoHide === true)
}
if (!res.data.refs || res.data.refs.length === 0) {
$(`<li class="dd-item nodata">${$L('暂无数据')}</li>`).appendTo('.unset-list')
}
})
const $btn = $('.J_save').click(function () {
let config = []
$('.J_config>li').each(function () {
const $this = $(this)
config.push([$this.data('key'), $this.attr('data-label') || ''])
})
config = {
items: config,
autoExpand: $val('#relatedAutoExpand'),
autoHide: $val('#relatedAutoHide'),
}
$btn.button('loading')
$.post(url, JSON.stringify(config), function (res) {
$btn.button('reset')
if (res.error_code === 0) parent.location.reload()
else RbHighbar.error(res.error_msg)
})
})
})
const ShowStyles_Comps = {}
// eslint-disable-next-line no-undef
render_item_after = function ($item) {
const key = $item.data('key')
const $a = $(`<a class="mr-1" title="${$L('显示样式')}"><i class="zmdi zmdi-edit"></i></a>`)
$item.find('.dd3-action>a').before($a)
$a.on('click', function () {
if (ShowStyles_Comps[key]) {
ShowStyles_Comps[key].show()
} else {
renderRbcomp(
// eslint-disable-next-line react/jsx-no-undef
<ShowStyles
label={_configLabels[key]}
onConfirm={(s) => {
$item.attr({
'data-label': s.label || '',
})
_configLabels[key] = s.label
}}
/>,
null,
function () {
ShowStyles_Comps[key] = this
}
)
}
})
}

View file

@ -226,6 +226,8 @@ class AddCommonQuery extends RbFormHandler {
return (
<RbModal ref={(c) => (this._dlg = c)} title={$L('添加常用查询')}>
<RbAlertBox type="info" message={$L('可在添加后修改这些查询以便更适合自己的使用需要')} />
<div ref={(c) => (this._$chks = c)}>
{defs.map((item, idx) => {
if (item.length === 0) return <br key={idx} />
@ -237,7 +239,6 @@ class AddCommonQuery extends RbFormHandler {
)
})}
</div>
<div className="protips mb-3 text-left">{$L('可在添加后修改这些查询以便更适合自己的使用需要')}</div>
<div className="dialog-footer" ref={(c) => (this._btns = c)}>
<button className="btn btn-primary" type="button" onClick={() => this.saveAdd()}>
{$L('确定')}

View file

@ -729,7 +729,8 @@ const RbViewPage = {
const that = this
$(config).each(function () {
const e = this
const title = $L('新建%s', e.entityLabel)
// const title = $L('新建%s', e.entityLabel)
const title = e.entityLabel
const $item = $(`<a class="dropdown-item"><i class="icon zmdi zmdi-${e.icon}"></i>${title}</a>`)
$item.on('click', function () {
if (e.entity === 'Feeds.relatedRecord') {

View file

@ -0,0 +1,66 @@
/*
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.
*/
// 显示样式
// eslint-disable-next-line no-unused-vars
class ShowStyles extends React.Component {
render() {
return (
<div className="modal rbalert" ref={(c) => (this._$dlg = c)} tabIndex="-1">
<div className="modal-dialog">
<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="form">
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('别名')}</label>
<div className="col-sm-7">
<input className="form-control form-control-sm" placeholder={$L('默认')} defaultValue={this.props.label || ''} maxLength="50" ref={(c) => (this._$label = c)} />
</div>
</div>
<div className="form-group row footer">
<div className="col-sm-7 offset-sm-3">
<button className="btn btn-primary btn-space" type="button" onClick={() => this.saveProps()}>
{$L('确定')}
</button>
<a className="btn btn-link btn-space" onClick={() => this.hide()}>
{$L('取消')}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
componentDidMount() {
$(this._$dlg).modal({ show: true, keyboard: true })
}
show() {
$(this._$dlg).modal('show')
}
hide() {
$(this._$dlg).modal('hide')
}
saveProps() {
const data = {
label: $(this._$label).val() || '',
}
typeof this.props.onConfirm === 'function' && this.props.onConfirm(data)
this.hide()
}
}

View file

@ -419,9 +419,9 @@ class FormulaCalcWithCode extends FormulaCalc {
<a className="dropdown-item" onClick={() => this.handleInput('DATESUB')} title="DATESUB($DATE, $NUMBER[H|D|M|Y])">
DATESUB
</a>
<div className="dropdown-divider"></div>
<a className="dropdown-item" target="_blank" href="https://getrebuild.com/docs/admin/triggers#%E5%85%AC%E5%BC%8F%E7%BC%96%E8%BE%91%E5%99%A8">
<i className="zmdi zmdi-help icon"></i>
<div className="dropdown-divider" />
<a className="dropdown-item pointer" target="_blank" href="https://getrebuild.com/docs/admin/triggers#%E5%85%AC%E5%BC%8F%E7%BC%96%E8%BE%91%E5%99%A8">
<i className="zmdi zmdi-help icon" />
{$L('如何使用函数')}
</a>
</div>

View file

@ -0,0 +1,32 @@
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "REBUILD Web",
"short_name": "REBUILD",
"start_url": "/",
"background_color": "#eeeeee",
"theme_color": "#4285f4",
"display": "standalone",
"id": "id-rebuild-web",
"icons": [
{
"src": "img/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "img/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "img/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "img/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View file

@ -145,7 +145,7 @@
<div class="item" data-toggle="dropdown">
<a><i class="zmdi zmdi-chevron-down"></i></a>
<span></span>
<a class="del"><i class="zmdi zmdi-close-circle"></i></a>
<a class="del" th:title="${bundle.L('移除')}"><i class="zmdi zmdi-close-circle"></i></a>
</div>
<ul class="dropdown-menu">
<li class="dropdown-item J_num" data-calc="SUM">[[${bundle.L('求和')}]]</li>

View file

@ -22,9 +22,9 @@
<div class="row">
<div class="col-sm-6 dash-list">
<div class="dash-head">
<h4 class="J_dash-select">[[${bundle.L('仪表盘')}]]</h4>
<h4 class="J_dash-select" th:title="${bundle.L('切换仪表盘')}">[[${bundle.L('仪表盘')}]]</h4>
<div class="dash-action">
<a class="zicon J_dash-edit"><i class="zmdi zmdi-settings"></i></a>
<a class="zicon J_dash-edit" th:title="${bundle.L('设置')}"><i class="zmdi zmdi-settings"></i></a>
<a class="zicon J_dash-new" th:title="${bundle.L('添加仪表盘')}"><i class="zmdi zmdi-plus-circle-o"></i></a>
</div>
</div>

View file

@ -22,6 +22,10 @@
color: #fbbc05;
font-size: 5rem;
}
.zmdi.err497::before {
content: '\f119';
}
.error-description > pre:empty {
display: none;
}
@ -48,6 +52,7 @@
<button class="btn btn-xl btn-space btn-primary" type="button" onclick="location.reload()">[[${bundle.L('重试')}]]</button>
<div class="mt-4">
<a th:href="${'https://getrebuild.com/report-issue?title=error-page-' + error_code}" target="_blank">[[${bundle.L('报告问题')}]]</a>
<span>[[${bundle.L('或联系系统管理员')}]]</span>
</div>
</div>
</div>

View file

@ -72,7 +72,7 @@
<div class="card">
<div class="card-header" id="headingGroup">
<button class="btn" data-toggle="collapse" data-target="#collapseGroup"><i class="icon zmdi zmdi-chevron-right"></i> [[${bundle.L('团队')}]]</button>
<a class="add-group admin-show" th:href="@{/admin/bizuser/teams}" th:title="${bundle.L('管理团队')}"><i class="icon zmdi zmdi-settings"></i></a>
<a class="add-group admin-show" th:href="@{/admin/bizuser/teams}" th:title="${bundle.L('管理团队')}" target="_blank"><i class="icon zmdi zmdi-settings"></i></a>
</div>
<div class="collapse" id="collapseGroup">
<div class="card-body">

View file

@ -130,7 +130,7 @@ public class TestSupport {
if (!MetadataHelper.containsEntity(TestAllFields)) {
Entity2Schema entity2Schema = new Entity2Schema(UserService.ADMIN_USER);
String entityName = entity2Schema.createEntity(TestAllFields.toUpperCase(), null, null, false);
String entityName = entity2Schema.createEntity(TestAllFields.toUpperCase(), null, null, true);
Entity testEntity = MetadataHelper.getEntity(entityName);
for (DisplayType dt : DisplayType.values()) {

View file

@ -10,6 +10,7 @@ package com.rebuild.web;
import com.rebuild.TestSupport;
import com.rebuild.core.Application;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.context.WebApplicationContext;
@ -29,9 +30,15 @@ class BaseControllerTest extends TestSupport {
BaseController c = new BaseController() {
};
ApplicationContext context = Application.getContext();
if (!(context instanceof WebApplicationContext)) {
LOG.warn("None WebApplicationContext!");
return;
}
MockHttpServletRequest request = MockMvcRequestBuilders
.get("/user/login?name=a&int=123456&id=" + SIMPLE_USER)
.buildRequest(Objects.requireNonNull(((WebApplicationContext) Application.getContext()).getServletContext()));
.buildRequest(Objects.requireNonNull(((WebApplicationContext) context).getServletContext()));
assertEquals(c.getParameter(request, "name"), "a");