Fix 3.1.1 (#544)

* fix: checkInstalled and clearAllCache

* enh: api

* enh: rbcolor

* fix: fieldParent del

* enh: table chart mergeCell

* J_menuUrl

* enh: form _maximize

* fix: query VF_ACU

* fix: form getValue

* fix: SK_DAVATAR

* enh: copy detail

* $empty

* func: SQLQUERY

* feat: ProxyTriggerAction

* fix: CNVD-C-2022-425724

* fix: putx

* data-v
This commit is contained in:
RB 2022-11-13 14:09:32 +08:00 committed by GitHub
parent c010f49c88
commit 34e9138faa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 544 additions and 303 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 017c99f7496cc86adb2e0b5cc4ca277f0dcc6d61
Subproject commit f256231e8f0d67ee326d47b65ed9408a0ce9320f

View file

@ -4,9 +4,10 @@
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| 3.x | :white_check_mark: |
| 1.x | :x: |
| 2.x | :x: |
## Reporting a Vulnerability
Please report security issues to `rebuild@ruifang-tech.com`
Please report security issues to `rebuild@ruifang-tech.com`

View file

@ -10,7 +10,7 @@
</parent>
<groupId>com.rebuild</groupId>
<artifactId>rebuild</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
<name>rebuild</name>
<description>Building your business-systems freely!</description>
<!-- UNCOMMENT USE TOMCAT -->

View file

@ -156,7 +156,16 @@ public class ApiGateway extends Controller implements Initialization {
final ConfigBean apiConfig = RebuildApiManager.instance.getApp(appid);
if (apiConfig == null) {
throw new ApiInvokeException(ApiInvokeException.ERR_BADAUTH, "Invalid [appid] " + appid);
throw new ApiInvokeException(ApiInvokeException.ERR_BADAUTH, "Invalid [appid] : " + appid);
}
// v3.1.1
final String bindIps = apiConfig.getString("bindIps");
if (StringUtils.isNotBlank(bindIps)) {
String clientIp = ServletUtils.getRemoteAddr(request);
if (!bindIps.contains(clientIp)) {
throw new ApiInvokeException(ApiInvokeException.ERR_BADAUTH, "Client ip not in whitelist : " + clientIp);
}
}
// 验证签名

View file

@ -61,7 +61,7 @@ public class AuthTokenManager {
System.nanoTime());
String token = EncryptUtils.toSHA1Hex(desc);
Application.getCommonsCache().putx(TOKEN_PREFIX + token, desc, expires);
Application.getCommonsCache().put(TOKEN_PREFIX + token, desc, expires);
return token;
}

View file

@ -67,11 +67,11 @@ public class Application implements ApplicationListener<ApplicationStartedEvent>
/**
* Rebuild Version
*/
public static final String VER = "3.1.0";
public static final String VER = "3.1.1";
/**
* Rebuild Build [MAJOR]{1}[MINOR]{2}[PATCH]{2}[BUILD]{2}
*/
public static final int BUILD = 3010008;
public static final int BUILD = 3010109;
static {
// Driver for DB

View file

@ -7,11 +7,14 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.configuration;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.persist4j.PersistManagerFactory;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.AdminGuard;
import org.apache.commons.lang.math.RandomUtils;
import org.springframework.stereotype.Service;
/**
@ -32,6 +35,20 @@ public class RebuildApiService extends BaseConfigurationService implements Admin
return EntityHelper.RebuildApi;
}
@Override
public Record create(Record record) {
record.setString("appId", (100000000 + RandomUtils.nextInt(899999999)) + "");
record.setString("appSecret", CodecUtils.randomCode(40));
return super.create(record);
}
@Override
public Record update(Record record) {
record.removeValue("appId");
record.removeValue("appSecret");
return super.update(record);
}
@Override
protected void cleanCache(ID cfgid) {
Object[] cfg = Application.createQueryNoFilter(

View file

@ -678,6 +678,13 @@ public class FormsBuilder extends FormsManager {
fieldParent = dtf.getName() + "." + pfs[0].split("\\.")[1];
}
// v3.1.1 父级已删除
Entity entity = MetadataHelper.getEntity(record.getEntityCode());
if (MetadataHelper.getLastJoinField(entity, fieldParent) == null) {
log.warn("Unknow field : {} in {}", fieldParent, entity.getName());
return null;
}
Object[] o = Application.getQueryFactory().uniqueNoFilter(record, fieldParent);
return o == null ? null : (ID) o[0];
}

View file

@ -58,7 +58,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
final boolean isNew = record.getPrimary() == null;
// 明细关联主记录
if (isNew && isDTF(field)) return true;
if (isNew && isDtmField(field)) return true;
// 公共字段前台可能会布局出来
// 此处忽略检查没问题因为最后还会复写 #bindCommonsFieldsValue
@ -107,7 +107,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
}
} else {
if (field.isCreatable()) {
if (!matchsPattern(easyField, hasVal)) {
if (!patternMatches(easyField, hasVal)) {
notWells.add(easyField.getLabel());
}
} else {
@ -135,7 +135,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
}
} else {
if (field.isUpdatable()) {
if (!matchsPattern(easyField, hasVal)) {
if (!patternMatches(easyField, hasVal)) {
notWells.add(easyField.getLabel());
}
} else {
@ -157,7 +157,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
}
// 明细关联主记录字段
private boolean isDTF(Field field) {
private boolean isDtmField(Field field) {
if (field.getType() == FieldType.REFERENCE && entity.getMainEntity() != null) {
return field.equals(MetadataHelper.getDetailToMainField(entity));
}
@ -166,7 +166,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
// 强制可新建
private boolean isForceCreateable(Field field) {
if (isDTF(field)) return true;
if (isDtmField(field)) return true;
// 自定定位
EasyField easyField = EasyMetaFactory.valueOf(field);
@ -178,7 +178,8 @@ public class EntityRecordCreator extends JsonRecordCreator {
}
// 正则匹配
private boolean matchsPattern(EasyField easyField, Object val) {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean patternMatches(EasyField easyField, Object val) {
if (!(easyField instanceof EasyText)) return true;
Pattern patt = ((EasyText) easyField).getPattern();

View file

@ -314,18 +314,17 @@ public class UserHelper {
*/
public static File generateAvatar(String name, boolean forceMake) {
if (StringUtils.isBlank(name)) name = "RB";
File avatarFile = RebuildConfiguration.getFileOfData("avatar-" + name + "29.jpg");
if (avatarFile.exists()) {
File avatar = RebuildConfiguration.getFileOfData("avatar-" + name + "29.jpg");
if (avatar.exists()) {
if (forceMake) {
FileUtils.deleteQuietly(avatarFile);
FileUtils.deleteQuietly(avatar);
} else {
return avatarFile;
return avatar;
}
}
if (name.length() > 2) {
name = name.substring(name.length() - 2);
}
if (name.length() > 2) name = name.substring(name.length() - 2);
name = name.toUpperCase();
BufferedImage bi = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
@ -347,7 +346,7 @@ public class UserHelper {
g2d.drawString("wbr", 0, 62);
g2d.dispose();
try (FileOutputStream fos = new FileOutputStream(avatarFile)) {
try (FileOutputStream fos = new FileOutputStream(avatar)) {
ImageIO.write(bi, "png", fos);
fos.flush();
}
@ -360,7 +359,7 @@ public class UserHelper {
is = CommonsUtils.getStreamOfRes("/web" + DEFAULT_AVATAR);
bi = ImageIO.read(is);
try (FileOutputStream fos = new FileOutputStream(avatarFile)) {
try (FileOutputStream fos = new FileOutputStream(avatar)) {
ImageIO.write(bi, "png", fos);
fos.flush();
}
@ -370,7 +369,7 @@ public class UserHelper {
}
}
return avatarFile;
return avatar;
}
private static Font createFont() {
@ -408,7 +407,7 @@ public class UserHelper {
* @see #sortUsers(boolean)
*/
public static User[] sortUsers() {
return sortUsers(false);
return sortUsers(Boolean.FALSE);
}
/**

View file

@ -144,11 +144,12 @@ public class ChartManager implements ConfigManager {
}
ch.put("title", e.getString("title"));
ch.put("type", e.getString("type"));
String type = e.getString("type");
ch.put("type", type);
try {
String c = ((JSONObject) e.getJSON("config")).getJSONObject("option").getString("useColor");
if (StringUtils.isNotBlank(c)) ch.put("color", c);
if ("INDEX".equals(type) && StringUtils.isNotBlank(c)) ch.put("color", c);
} catch (Exception ignore) {}
if (user != null) {

View file

@ -90,33 +90,35 @@ public class TableBuilder {
}
// 合并纬度单元格
for (int i = 0; i < chart.getDimensions().length; i++) {
// 行号
if (chart.isShowLineNumber() && i == 0) continue;
if (chart.isMergeCell()) {
for (int i = 0; i < chart.getDimensions().length; i++) {
// 行号
if (chart.isShowLineNumber() && i == 0) continue;
TD last = null;
int sumMinus = 0; // 合并的单元格
int childrenLen = tbody.children.size();
for (TR tr : tbody.children) {
TD current = tr.children.get(i);
if (last == null || childrenLen-- <= 1) {
last = current;
continue;
TD last = null;
int sumMinus = 0; // 合并的单元格
int childrenLen = tbody.children.size();
for (TR tr : tbody.children) {
TD current = tr.children.get(i);
if (last == null || childrenLen-- <= 1) {
last = current;
continue;
}
if (last.content.equals(current.content)) {
last.rowspan++;
current.rowspan = 0;
sumMinus++;
} else {
last = current;
}
}
if (last.content.equals(current.content)) {
last.rowspan++;
current.rowspan = 0;
sumMinus++;
} else {
last = current;
if (chart.isShowSums() && sumMinus > 0 && StringUtils.isNotBlank(last.content)) {
int num = ObjectUtils.toInt(last.content) - sumMinus;
last.content = String.valueOf(num);
}
}
if (chart.isShowSums() && sumMinus > 0 && StringUtils.isNotBlank(last.content)) {
int num = ObjectUtils.toInt(last.content) - sumMinus;
last.content = String.valueOf(num);
}
}
String tClazz = (chart.isShowLineNumber() ? "line-number " : "") + (chart.isShowSums() ? "sums" : "");

View file

@ -28,6 +28,7 @@ public class TableChart extends ChartData {
private boolean showLineNumber = false;
private boolean showSums = false;
private boolean mergeCell = true;
protected TableChart(JSONObject config) {
super(config);
@ -36,6 +37,7 @@ public class TableChart extends ChartData {
if (option != null) {
this.showLineNumber = option.getBooleanValue("showLineNumber");
this.showSums = option.getBooleanValue("showSums");
if (option.containsKey("mergeCell")) this.mergeCell = option.getBooleanValue("mergeCell");
}
}
@ -96,6 +98,10 @@ public class TableChart extends ChartData {
return showSums;
}
protected boolean isMergeCell() {
return mergeCell;
}
protected String wrapSumValue(Axis sumAxis, Object value) {
if (ChartsHelper.isZero(value)) {
return ChartsHelper.VALUE_ZERO;

View file

@ -180,11 +180,11 @@ public class RecordCheckout {
// 支持ID
if (ID.isId(val) && ID.valueOf(val).getEntityCode() == EntityHelper.ClassificationData) {
ID iid = ID.valueOf(val);
if (ClassificationManager.instance.getName(iid) != null) {
return iid;
ID cid = ID.valueOf(val);
if (ClassificationManager.instance.getName(cid) != null) {
return cid;
} else {
log.warn("No item of Classification found by ID : " + iid);
log.warn("No item of Classification found by ID : " + cid);
return null;
}
} else {

View file

@ -486,7 +486,7 @@ public class AdvFilterParser extends SetUser {
if (VF_ACU.equals(field)) {
return String.format(
"(exists (select recordId from RobotApprovalStep where ^%s = recordId and state = 1 and %s) and approvalState = 2)",
"(exists (select recordId from RobotApprovalStep where ^%s = recordId and state = 1 and isCanceled = 'F' and %s) and approvalState = 2)",
specRootEntity.getPrimaryField().getName(), sb.toString().replace(VF_ACU, "approver"));
} else {
return sb.toString();

View file

@ -73,4 +73,9 @@ public class ActionContext {
public ID getConfigId() {
return configId;
}
@Override
public String toString() {
return super.toString() + "#" + getConfigId();
}
}

View file

@ -33,6 +33,7 @@ public enum ActionType {
AUTOASSIGN("自动分派", AutoAssign.class),
SENDNOTIFICATION("发送通知", SendNotification.class),
HOOKURL("回调 URL", "com.rebuild.rbv.trigger.HookUrl"),
PROXYTRIGGERACTION("自定义触发器", "com.rebuild.rbv.trigger.ProxyTriggerAction"),
;

View file

@ -49,9 +49,10 @@ public class AviatorUtils {
addCustomFunction(new CurrentBizunitFunction());
addCustomFunction(new CurrentDateFunction());
addCustomFunction(new LocationDistanceFunction());
addCustomFunction(new RequestFunctuin());
addCustomFunction(new TextFunction());
addCustomFunction(new ChineseYuanFunction());
addCustomFunction(new TextFunction());
addCustomFunction(new RequestFunctuin());
addCustomFunction(new SqlQueryFunction());
}
/**

View file

@ -0,0 +1,101 @@
/*!
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.service.trigger.aviator;
import cn.devezhao.persist4j.Query;
import cn.devezhao.persist4j.engine.ID;
import com.googlecode.aviator.runtime.function.AbstractFunction;
import com.googlecode.aviator.runtime.type.AviatorDecimal;
import com.googlecode.aviator.runtime.type.AviatorNil;
import com.googlecode.aviator.runtime.type.AviatorObject;
import com.googlecode.aviator.runtime.type.AviatorString;
import com.rebuild.core.Application;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.Map;
/**
* Usage: SQLQUERY($sql, [ $param...{5} ])
* Return: Number|Date|String
*
* @author RB
* @since 2022/11/10
*/
@Slf4j
public class SqlQueryFunction extends AbstractFunction {
private static final long serialVersionUID = 6408510455471045309L;
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2, AviatorObject arg3, AviatorObject arg4, AviatorObject arg5, AviatorObject arg6) {
String $sql = arg1.getValue(env).toString();
Query query = Application.createQueryNoFilter($sql);
Object param1 = getValue(env, arg2);
if (param1 != null) query.setParameter(1, param1);
Object param2 = getValue(env, arg3);
if (param2 != null) query.setParameter(2, param2);
Object param3 = getValue(env, arg4);
if (param3 != null) query.setParameter(3, param3);
Object param4 = getValue(env, arg5);
if (param4 != null) query.setParameter(4, param4);
Object param5 = getValue(env, arg6);
if (param5 != null) query.setParameter(5, param5);
Object[] o = query.unique();
if (o == null || o[0] == null) return AviatorNil.NIL;
final Object value = o[0];
if (value instanceof Number) {
return new AviatorDecimal((Number) value);
} else if (value instanceof Date) {
return new AviatorDate((Date) value);
} else {
return new AviatorString(value.toString());
}
}
private Object getValue(Map<String, Object> env, AviatorObject arg) {
Object value = arg.getValue(env);
if (value == null) return null;
if (value instanceof Number || value instanceof Date || value instanceof ID) return value;
else return value.toString();
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2, AviatorObject arg3, AviatorObject arg4, AviatorObject arg5) {
return this.call(env, arg1, arg2, arg3, arg4, arg5, AviatorNil.NIL);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2, AviatorObject arg3, AviatorObject arg4) {
return this.call(env, arg1, arg2, arg3, arg4, AviatorNil.NIL);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2, AviatorObject arg3) {
return this.call(env, arg1, arg2, arg3, AviatorNil.NIL);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
return this.call(env, arg1, arg2, AviatorNil.NIL);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
return this.call(env, arg1, AviatorNil.NIL);
}
@Override
public String getName() {
return "SQLQUERY";
}
}

View file

@ -37,6 +37,7 @@ import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.args.FlushMode;
import javax.sql.DataSource;
import java.io.File;
@ -518,9 +519,12 @@ public class Installer implements InstallState {
public static void clearAllCache() {
if (isUseRedis()) {
try (Jedis jedis = Application.getCommonsCache().getJedisPool().getResource()) {
// // Delete all the keys of the currently selected DB
// jedis.flushDB(FlushMode.SYNC);
jedis.flushAll();
// https://redis.io/commands/flushdb/
try {
jedis.flushDB(FlushMode.SYNC); // v6.2.0
} catch (Exception v620) {
jedis.flushDB();
}
}
} else {
Application.getCommonsCache().getEhcacheCache().clear();

View file

@ -92,9 +92,7 @@ public class AppUtils {
*/
public static ID getRequestUser(HttpServletRequest request, boolean refreshToken) {
Object user = request.getSession().getAttribute(WebUtils.CURRENT_USER);
if (user == null) {
user = getRequestUserViaToken(request, refreshToken);
}
if (user == null) user = getRequestUserViaToken(request, refreshToken);
return user == null ? null : (ID) user;
}

View file

@ -39,7 +39,7 @@ public class CommonsUtils {
private static final char[] SPECIAL_CHARS = "`~!@#$%^&*()_+=-{}|[];':\",./<>?".toCharArray();
/**
* 不含特殊字符不允许除 数字 字母 中文 _ - 以外的字符包括空格
* 不含特殊字符不允许除 `数字` `字母` `中文` `_` `-` 及空格以外的字符
*
* @param text
* @return

View file

@ -7,6 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.web.admin;
import com.rebuild.core.Application;
import com.rebuild.core.support.ConfigurationItem;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.setup.DatabaseBackup;
@ -64,7 +65,7 @@ public class AdminCLI2 {
case C_HELP:
case "?" : {
result = " Usage : " +
" \ncache clean" +
" \ncache [clean|get] [KEY]" +
" \nsyscfg NAME [VALUE]" +
" \nsyscfg clean-qiniu|clean-sms|clean-email" +
" \nbackup [database|datafile]" +
@ -108,6 +109,12 @@ public class AdminCLI2 {
String name = commands[1];
if ("clean".equals(name)) {
Installer.clearAllCache();
} else if ("get".equals(name)) {
if (commands.length < 3) return "WRAN: Bad arguments";
String key = commands[2];
Object value = Application.getCommonsCache().getx(key);
if (value == null) result = "/NULL/";
else result = value.toString();
} else {
result = "WRAN: Bad arguments";
}

View file

@ -17,8 +17,8 @@ import com.rebuild.core.configuration.RebuildApiService;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.support.i18n.I18nUtils;
import com.rebuild.web.BaseController;
import org.apache.commons.lang.math.RandomUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@ -39,10 +39,10 @@ public class ApisManagerController extends BaseController {
return createModelAndView("/admin/integration/apis-manager");
}
@RequestMapping("apis-manager/app-list")
@GetMapping("apis-manager/app-list")
public RespBody appList() {
Object[][] apps = Application.createQueryNoFilter(
"select uniqueId,appId,appSecret,bindUser,bindUser.fullName,createdOn,appId from RebuildApi")
"select uniqueId,appId,appSecret,bindUser,bindUser.fullName,createdOn,appId,bindIps from RebuildApi order by createdOn")
.array();
// 近30日用量
@ -60,24 +60,15 @@ public class ApisManagerController extends BaseController {
return RespBody.ok(apps);
}
@RequestMapping("apis-manager/app-create")
public RespBody appCreate(HttpServletRequest request) {
ID user = getRequestUser(request);
ID bindUser = getIdParameter(request, "bind");
Record record = EntityHelper.forNew(EntityHelper.RebuildApi, user);
record.setString("appId", (100000000 + RandomUtils.nextInt(899999999)) + "");
@PostMapping("apis-manager/reset-secret")
public RespBody resetSecret(HttpServletRequest request) {
ID appId = getIdParameterNotNull(request, "id");
Record record = EntityHelper.forUpdate(appId, getRequestUser(request));
record.setString("appSecret", CodecUtils.randomCode(40));
record.setID("bindUser", bindUser);
Application.getBean(RebuildApiService.class).create(record);
Application.getCommonsService().update(record, false);
return RespBody.ok();
}
@RequestMapping("apis-manager/app-delete")
public RespBody appDelete(HttpServletRequest request) {
ID id = getIdParameterNotNull(request, "id");
Application.getBean(RebuildApiService.class).delete(id);
// cache
Application.getBean(RebuildApiService.class).update(record);
return RespBody.ok();
}
}

View file

@ -13,6 +13,7 @@ import cn.devezhao.commons.web.ServletUtils;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.api.RespBody;
import com.rebuild.core.Application;
import com.rebuild.core.RebuildException;
import com.rebuild.core.support.License;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.setup.InstallState;
@ -34,7 +35,6 @@ import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@ -50,13 +50,17 @@ import java.sql.SQLException;
@RequestMapping("/setup/")
public class InstallController extends BaseController implements InstallState {
@GetMapping("install")
public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (Application.isReady() && !Application.devMode()) {
response.sendError(404);
return null;
@Override
public boolean checkInstalled() {
if (InstallState.super.checkInstalled() && Application.isReady()) {
throw new RebuildException("NOT ALLOWED");
}
return false;
}
@GetMapping("install")
public ModelAndView index(HttpServletRequest request) throws IOException {
checkInstalled();
ModelAndView mv = createModelAndView("/admin/setup/install");
mv.getModel().put("Version", Application.VER);
@ -68,6 +72,7 @@ public class InstallController extends BaseController implements InstallState {
@PostMapping("test-connection")
public RespBody testConnection(HttpServletRequest request) {
checkInstalled();
JSONObject dbProps = (JSONObject) ServletUtils.getRequestJson(request);
JSONObject props = JSONUtils.toJSONObject("databaseProps", dbProps);
@ -108,6 +113,7 @@ public class InstallController extends BaseController implements InstallState {
@PostMapping("test-cache")
public RespBody testCache(HttpServletRequest request) {
checkInstalled();
JSONObject cacheProps = (JSONObject) ServletUtils.getRequestJson(request);
JedisPool pool = new JedisPool(new JedisPoolConfig(),
@ -133,6 +139,7 @@ public class InstallController extends BaseController implements InstallState {
@PostMapping("install-rebuild")
public RespBody installExec(HttpServletRequest request) {
checkInstalled();
JSONObject installProps = (JSONObject) ServletUtils.getRequestJson(request);
try {
@ -146,6 +153,7 @@ public class InstallController extends BaseController implements InstallState {
@GetMapping("request-sn")
public RespBody requestSn() {
checkInstalled();
return RespBody.ok(License.SN());
}
}

View file

@ -16,14 +16,15 @@ import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.api.user.AuthTokenManager;
import com.rebuild.core.Application;
import com.rebuild.core.cache.CommonsCache;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.privileges.bizz.User;
import com.rebuild.core.support.KVStorage;
import com.rebuild.core.support.License;
import com.rebuild.core.support.task.TaskExecutors;
import com.rebuild.utils.AES;
import com.rebuild.web.BaseController;
import com.rebuild.web.user.UserAvatar;
import eu.bitwalker.useragentutils.DeviceType;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;
@ -49,7 +50,8 @@ public class LoginAction extends BaseController {
protected static final String SK_NEED_VCODE = "needLoginVCode";
protected static final String SK_START_TOUR = "needStartTour";
protected static final String PREFIX_2FA = "2FA";
protected static final String PREFIX_2FA = "2FA:";
protected static final String PREFIX_ALT = "ALT:";
/**
* 登录成功
@ -63,8 +65,9 @@ public class LoginAction extends BaseController {
protected Integer loginSuccessed(HttpServletRequest request, HttpServletResponse response, ID user, boolean autoLogin) {
// 自动登录
if (autoLogin) {
String alt = user + "," + System.currentTimeMillis() + ",v1";
ServletUtils.addCookie(response, CK_AUTOLOGIN, AES.encrypt(alt));
final String altToken = CodecUtils.randomCode(60);
Application.getCommonsCache().putx(PREFIX_ALT + altToken, user, CommonsCache.TS_DAY * 14);
ServletUtils.addCookie(response, CK_AUTOLOGIN, altToken);
} else {
ServletUtils.removeCookie(request, response, CK_AUTOLOGIN);
}
@ -75,6 +78,9 @@ public class LoginAction extends BaseController {
ServletUtils.setSessionAttribute(request, SK_USER_THEME, KVStorage.getCustomValue("THEME." + user));
Application.getSessionStore().storeLoginSuccessed(request);
// 头像缓存
ServletUtils.setSessionAttribute(request, UserAvatar.SK_DAVATAR, System.currentTimeMillis());
// TOUR 显示规则
Object[] initLoginTimes = Application.createQueryNoFilter(
"select count(loginTime) from LoginLog where user = ? and loginTime > '2022-01-01'")

View file

@ -8,7 +8,6 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.web.user.signup;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.RegexUtils;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Record;
@ -28,9 +27,7 @@ import com.rebuild.core.service.DataSpecificationException;
import com.rebuild.core.support.*;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.integration.SMSender;
import com.rebuild.utils.AES;
import com.rebuild.utils.AppUtils;
import com.rebuild.web.user.UserAvatar;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
@ -85,28 +82,9 @@ public class LoginController extends LoginAction {
// 记住登录
final String useAlt = ServletUtils.readCookie(request, CK_AUTOLOGIN);
if (StringUtils.isNotBlank(useAlt)) {
ID altUser = null;
try {
String[] alts = AES.decrypt(useAlt).split(",");
altUser = alts.length == 3 && ID.isId(alts[0]) ? ID.valueOf(alts[0]) : null;
final ID altUser = (ID) Application.getCommonsCache().getx(PREFIX_ALT + useAlt);
// 最大30天有效期
if (altUser != null) {
long t = ObjectUtils.toLong(alts[1]);
if ((System.currentTimeMillis() - t) / 1000 > 30 * 24 * 60 * 60) {
altUser = null;
}
if (altUser != null && !UserHelper.isActive(altUser)) {
altUser = null;
}
}
} catch (Exception ex) {
ServletUtils.removeCookie(request, response, CK_AUTOLOGIN);
log.error("Cannot decode User from alt : {}", useAlt, ex);
}
if (altUser != null && Application.getUserStore().existsUser(altUser)) {
if (altUser != null && UserHelper.isActive(altUser)) {
Integer ed = loginSuccessed(request, response, altUser, true);
String nexturl = getParameter(request, "nexturl", homeUrl);
@ -183,8 +161,6 @@ public class LoginController extends LoginAction {
// 清理验证码
getLoginRetryTimes(user, -1);
ServletUtils.setSessionAttribute(request, SK_NEED_VCODE, null);
// 头像缓存
ServletUtils.setSessionAttribute(request, UserAvatar.SK_DAVATAR, System.currentTimeMillis());
final User loginUser = Application.getUserStore().getUser(user);
final boolean isRbMobile = AppUtils.isRbMobile(request);
@ -197,8 +173,8 @@ public class LoginController extends LoginAction {
if (faMode > 0 && !faModeSkip) {
resMap.put("login2FaMode", faMode);
String userToken = CodecUtils.randomCode(40);
Application.getCommonsCache().putx(PREFIX_2FA + userToken, loginUser.getId(), 15 * 60); // 15m
final String userToken = CodecUtils.randomCode(40);
Application.getCommonsCache().putx(PREFIX_2FA + userToken, loginUser.getId(), CommonsCache.TS_MINTE * 15);
resMap.put("login2FaUserToken", userToken);
if (isRbMobile) {

View file

@ -2369,5 +2369,15 @@
"批量添加字段":"批量添加字段",
"支持 H5":"支持 H5",
"手机登录":"手机登录",
"批量添加":"批量添加"
"批量添加":"批量添加",
"触发类 (TriggerAction)":"触发类 (TriggerAction)",
"无 (不限制)":"无 (不限制)",
"重置 APP SECRET":"重置 APP SECRET",
"自定义触发器":"自定义触发器",
"白名单内的 IP 才可以通过此 API 秘钥调用接口,如有多个 IP 请使用逗号或空格分开,留空则不限制":"白名单内的 IP 才可以通过此 API 秘钥调用接口,如有多个 IP 请使用逗号或空格分开,留空则不限制",
"配色":"配色",
"APP SECRET 已重置":"APP SECRET 已重置",
"修改 API 秘钥":"修改 API 秘钥",
"自动合并单元格":"自动合并单元格",
"重置后第三方应用需更换新的 APP SECRET 使用":"重置后第三方应用需更换新的 APP SECRET 使用"
}

View file

@ -27,12 +27,13 @@
<table class="table table-hover table-striped table-fixed">
<thead>
<tr>
<th width="10%">APP ID</th>
<th width="30%">APP SECRET</th>
<th>[[${bundle.L('绑定用户 (权限)')}]]</th>
<th>[[${bundle.L('调用量 (30 天)')}]]</th>
<th width="12%">APP ID</th>
<th>APP SECRET</th>
<th width="12%">[[${bundle.L('绑定用户 (权限)')}]]</th>
<th>[[${bundle.L('IP 白名单')}]]</th>
<th width="12%">[[${bundle.L('调用量 (30 天)')}]]</th>
<th width="120">[[${bundle.L('创建时间')}]]</th>
<th width="50"></th>
<th width="120"></th>
</tr>
</thead>
<tbody id="appList"></tbody>
@ -57,6 +58,7 @@
</div>
</div>
<th:block th:replace="~{/_include/footer}" />
<script th:src="@{/assets/js/admin/config-comps.js}" type="text/babel"></script>
<script th:src="@{/assets/js/admin/apis-manager.js}" type="text/babel"></script>
</body>
</html>

View file

@ -17,35 +17,8 @@
.unset-list .dd-item:hover a.action {
color: #fff;
}
.colors {
line-height: 1;
font-size: 0;
}
.colors > a {
width: 24px;
height: 24px;
display: inline-block;
background-color: #ccc;
border-radius: 50%;
color: #fff;
overflow: hidden;
font-size: 1.3rem;
padding-top: 4px;
text-align: center;
}
.colors > a:hover {
opacity: 0.8;
color: rgb(117, 0, 234);
}
.colors > a + a {
margin-left: 9px;
}
.colors > a > i {
color: #fff !important;
}
.colors > a:first-child > i {
color: #aaa !important;
.rbcolors > a:first-child > i {
color: #777 !important;
}
</style>
</head>
@ -64,7 +37,7 @@
<button class="btn btn-secondary J_confirm" type="submit" style="min-width: 0">[[${bundle.L('添加')}]]</button>
</div>
</div>
<div class="colors mt-2">
<div class="rbcolors mt-2">
<a style="background-color: #fff; border: 1px solid #ccc" th:title="${bundle.L('默认')}"></a>
</div>
</form>

View file

@ -429,3 +429,11 @@ a.ui-draggable.ui-draggable-dragging {
.chart-type > a.active > i.C243 {
background-position: -1290px -1288px;
}
.rbcolors > a:first-child > i {
color: #777 !important;
}
.chart-option label {
margin-bottom: 10px;
}

View file

@ -122,6 +122,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
font-size: 1.1rem;
margin-bottom: 1px;
opacity: 0.8;
line-height: 1;
}
.chart.index > .data-item strong {
@ -129,6 +130,15 @@ See LICENSE and COMMERCIAL in the project root for license information.
font-weight: 400;
}
.grid-stack-item.fullscreen .chart.index > .data-item p {
font-size: 3rem;
}
.grid-stack-item.fullscreen .chart.index > .data-item strong {
font-size: 8.8rem;
zoom: 1 !important;
}
.chart.index > .data-item a {
color: #404040;
}
@ -453,17 +463,30 @@ See LICENSE and COMMERCIAL in the project root for license information.
display: none !important;
}
/* useColor */
/* Colors */
.grid-stack-item.color .grid-stack-item-content,
.grid-stack-item.color .chart.index > .data-item strong {
.grid-stack-item.bgcolor .grid-stack-item-content,
.grid-stack-item.bgcolor .chart.index > .data-item strong {
color: #fff;
}
.grid-stack-item.color .rb-loading:after {
.grid-stack-item.bgcolor .rb-loading:after {
background-color: transparent;
}
.grid-stack-item.color .rb-spinner svg {
.grid-stack-item.bgcolor .rb-spinner svg {
stroke: #fff;
}
body.fullscreen {
background: #0a1b3b url(../img/datav-bg.png) no-repeat 0 0;
background-size: 100% 100%;
}
body.fullscreen .grid-stack .grid-stack-item .grid-stack-item-content {
background-color: rgba(6, 30, 93, 0.5);
}
body.fullscreen .grid-stack .grid-stack-item.fullscreen .grid-stack-item-content {
background-color: #0a1b3b;
}

View file

@ -4979,3 +4979,34 @@ pre.unstyle {
.modal-title > .support-plat2 {
margin-top: 7px;
}
.rbcolors {
line-height: 1;
font-size: 0;
}
.rbcolors > a {
width: 24px;
height: 24px;
display: inline-block;
background-color: #ccc;
border-radius: 50%;
color: #fff;
overflow: hidden;
font-size: 1.3rem;
padding-top: 4px;
text-align: center;
}
.rbcolors > a:hover {
opacity: 0.8;
color: rgb(117, 0, 234);
}
.rbcolors > a + a {
margin-left: 7px;
}
.rbcolors > a > i {
color: #fff !important;
}

View file

@ -194,6 +194,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
padding: 4px 9px 0;
margin-right: 4px;
margin-top: 1px;
cursor: default;
}
.task-tags > span.tag-value > a {
@ -219,12 +220,13 @@ See LICENSE and COMMERCIAL in the project root for license information.
.task-tags a.tag-add {
color: #999;
display: inline-block;
padding: 2px 8px;
padding: 2px;
font-size: 1.231rem;
background-color: #eee;
border-radius: 50%;
width: 26px;
height: 26px;
text-align: center;
}
.task-tags a.tag-add:hover {
@ -249,28 +251,10 @@ See LICENSE and COMMERCIAL in the project root for license information.
opacity: 0.8;
}
.tags-editor .colors > a {
.tags-editor .rbcolors > a {
width: 22px;
height: 22px;
display: inline-block;
background-color: #ccc;
border-radius: 50%;
color: #fff;
overflow: hidden;
font-size: 1.3rem;
padding-top: 4px;
}
.tags-editor .colors > a:hover {
opacity: 0.8;
}
.tags-editor .colors > a + a {
margin-left: 7px;
}
.tags-editor .colors > a > i {
color: #fff !important;
font-size: 1.2rem;
}
.dropdown-menu.tags {
@ -326,6 +310,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
padding-left: 4px;
font-size: 1.25rem;
display: none;
cursor: pointer;
}
.tags-list .dropdown-item:hover > a {

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View file

@ -4,41 +4,50 @@ Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights re
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
/* global dlgActionAfter */
$(document).ready(function () {
$('.J_add').on('click', () => renderRbcomp(<DlgEdit />))
$('.J_add').on('click', () => renderRbcomp(<AppEdit />))
renderRbcomp(<AppList />, 'appList')
})
class AppList extends React.Component {
class AppList extends ConfigList {
constructor(props) {
super(props)
this.state = { ...props, secretShows: [] }
this.requestUrl = '/admin/apis-manager/app-list'
this.state.secretShows = []
}
render() {
return (
<React.Fragment>
{(this.state.list || []).map((item) => {
let secret = item[2].substr(0, 8) + '...' + item[2].substr(32)
{(this.state.data || []).map((item) => {
let secret = `${item[2].substr(0, 8)}...${item[2].substr(32)}`
secret = (
<a href="#" title={$L('点击显示')} onClick={() => this.showSecret(item[2])}>
<a href="###" title={$L('点击显示')} onClick={(e) => this.showSecret(e, item[2])}>
{secret}
</a>
)
if (this.state.secretShows.includes(item[2])) secret = item[2]
return (
<tr key={'api-' + item[0]}>
<tr key={item[0]}>
<td>{item[1]}</td>
<td>{secret}</td>
<td>{item[4] || $L(' (拥有全部权限)')}</td>
<td>{item[7] || $L(' (不限制)')}</td>
<td>{item[6] || 0}</td>
<td>
<DateShow date={item[5]} />
</td>
<td className="actions">
<a className="icon danger-hover" onClick={() => this.delete(item)}>
<a className="icon" title={$L('重置 APP SECRET')} onClick={() => this.resetSecret(item[0])}>
<i className="mdi mdi-lock-reset" />
</a>
<a className="icon" title={$L('修改')} onClick={() => this.handleEdit(item)}>
<i className="zmdi zmdi-edit" />
</a>
<a className="icon danger-hover" title={$L('删除')} onClick={() => this.handleDelete(item)}>
<i className="zmdi zmdi-delete" />
</a>
</td>
@ -49,84 +58,85 @@ class AppList extends React.Component {
)
}
componentDidMount = () => this._componentDidMount()
_componentDidMount() {
$.get('/admin/apis-manager/app-list', (res) => {
const _data = res.data || []
this.setState({ list: _data }, () => {
$('.rb-loading-active').removeClass('rb-loading-active')
$('.dataTables_info').text($L(' %d ', _data.length))
if (_data.length === 0) $('.list-nodata').removeClass('hide')
})
})
handleEdit(app) {
renderRbcomp(<AppEdit id={app[0]} bindIps={app[7]} bindUser={app[3]} />)
}
showSecret(s) {
event.preventDefault()
const shows = this.state.secretShows
shows.push(s)
this.setState({ secretShows: shows })
}
delete(app) {
const that = this
handleDelete(app) {
const handle = super.handleDelete
RbAlert.create($L('删除后使用此 API 秘钥的第三方应用功能将会失败'), {
type: 'danger',
confirmText: $L('删除'),
confirm: function () {
onConfirm: function () {
this.disabled(true)
$.post(`/admin/apis-manager/app-delete?id=${app[0]}`, (res) => {
if (res.error_code === 0) {
RbHighbar.success($L('删除成功'))
that._componentDidMount()
this.hide()
} else {
RbHighbar.error(res.error_msg)
}
handle(app[0], () => dlgActionAfter(this))
},
})
}
resetSecret(id) {
RbAlert.create($L('重置后第三方应用需更换新的 APP SECRET 使用'), {
type: 'danger',
confirmText: $L('重置'),
onConfirm: function () {
this.disabled(true)
$.post(`/admin/apis-manager/reset-secret?id=${id}`, () => {
RbHighbar.success($L('APP SECRET 已重置'))
dlgActionAfter(this)
})
},
})
}
showSecret(e, s) {
$stopEvent(e, true)
const shows = this.state.secretShows
shows.push(s)
this.setState({ secretShows: shows })
}
}
class DlgEdit extends RbFormHandler {
class AppEdit extends ConfigFormDlg {
constructor(props) {
super(props)
this.state = { ...props }
this.title = props.id ? $L('修改 API 秘钥') : $L('添加 API 秘钥')
}
render() {
renderFrom() {
return (
<RbModal title={$L('添加 API 秘钥')} ref={(c) => (this._dlg = c)}>
<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">
<UserSelector hideDepartment={true} hideRole={true} hideTeam={true} multiple={false} ref={(c) => (this._UserSelector = c)} />
<p className="form-text mb-0">{$L('强烈建议为 API 秘钥绑定一个用户此秘钥将拥有和其一样的权限如不绑定则拥有全部权限')}</p>
</div>
</div>
<div className="form-group row footer">
<div className="col-sm-7 offset-sm-3" ref={(c) => (this._btns = c)}>
<button className="btn btn-primary" type="button" onClick={this.save}>
{$L('确定')}
</button>
<a className="btn btn-link" onClick={this.hide}>
{$L('取消')}
</a>
</div>
<React.Fragment>
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('绑定用户 (权限)')}</label>
<div className="col-sm-7">
<UserSelector hideDepartment hideRole hideTeam multiple={false} ref={(c) => (this._UserSelector = c)} defaultValue={this.props.bindUser} />
<p className="form-text">{$L('强烈建议为 API 秘钥绑定一个用户此秘钥将拥有和其一样的权限如不绑定则拥有全部权限')}</p>
</div>
</div>
</RbModal>
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('IP 白名单')}</label>
<div className="col-sm-7">
<textarea className="form-control form-control-sm row2x" ref={(c) => (this._$bindIps = c)} defaultValue={this.props.bindIps} placeholder={$L('(可选)')} />
<p className="form-text">{$L('白名单内的 IP 才可以通过此 API 秘钥调用接口如有多个 IP 请使用逗号或空格分开留空则不限制')}</p>
</div>
</div>
</React.Fragment>
)
}
save = () => {
const bindUser = this._UserSelector.val()
confirm = () => {
const post = {
bindUser: (this._UserSelector.val() || [])[0] || null,
bindIps: $val(this._$bindIps) || null,
}
post.metadata = {
entity: 'RebuildApi',
id: this.props.id || null,
}
this.disabled(true)
$.post(`/admin/apis-manager/app-create?bind=${bindUser || ''}`, (res) => {
if (res.error_code === 0) location.reload()
$.post('/app/entity/common-save', JSON.stringify(post), (res) => {
if (res.error_code === 0) dlgActionAfter(this)
else RbHighbar.error(res.error_msg)
this.disabled()
})

View file

@ -68,7 +68,8 @@ $(document).ready(() => {
}
let _AdvFilter
$('.J_filter').on('click', () => {
$('.J_filter').on('click', (e) => {
$stopEvent(e, true)
if (_AdvFilter) {
_AdvFilter.show()
} else {
@ -116,6 +117,17 @@ $(document).ready(() => {
.find('.zmdi')
.addClass('zmdi-arrow-left')
// Colors
const $cs = $('.rbcolors')
RBCOLORS.forEach((c) => {
$(`<a style="background-color:${c}" data-color="${c}"></a>`).appendTo($cs)
})
$cs.find('>a').on('click', function () {
$cs.find('>a .zmdi').remove()
$('<i class="zmdi zmdi-check"></i>').appendTo(this)
render_preview()
})
// Load
if (wpc.chartConfig && wpc.chartConfig.axis) {
$(wpc.chartConfig.axis.dimension).each((idx, item) => add_axis('.J_axis-dim', item))
@ -137,6 +149,10 @@ $(document).ready(() => {
opt.val(option[k])
}
}
if (k === 'useColor') {
$cs.find(`a[data-color="${option[k]}"]`).trigger('click')
}
}
}
@ -381,7 +397,10 @@ const build_config = () => {
const name = $(this).data('name')
if (name) option[name] = $val(this)
})
if (option.useColor === '#000000') delete option.useColor
const color = $('.rbcolors >a>i').parent().data('color') || ''
option.useColor = color || ''
cfg.option = option
if (dataFilter) cfg.filter = dataFilter

View file

@ -140,12 +140,15 @@ class ChartIndex extends BaseChart {
}
renderChart(data) {
const color = __PREVIEW ? this.props.config.option.useColor : this.props.config.color
const style2 = { color: color || null }
const chartdata = (
<div className="chart index color" ref={(c) => (this._$chart = c)}>
<div className="chart index" ref={(c) => (this._$chart = c)}>
<div className="data-item must-center text-truncate w-auto">
<p>{data.index.label || this.label}</p>
<p style={style2}>{data.index.label || this.label}</p>
<a href={__PREVIEW ? null : `${rb.baseUrl}/dashboard/view-chart-source?id=${this.props.id}`}>
<strong>{data.index.data}</strong>
<strong style={style2}>{data.index.data}</strong>
</a>
</div>
</div>
@ -159,7 +162,7 @@ class ChartIndex extends BaseChart {
_resize() {
const ch = $(this._$chart).height()
const zoom = ch > 100 ? 1.3 : 1
const zoom = ch > 100 ? (ch > 330 ? 2.831 : 1.3) : 1
$(this._$chart).find('strong').css('zoom', zoom)
// const $text = $(this._$chart).find('strong')

View file

@ -234,7 +234,7 @@ const add_widget = function (item) {
const chart_add = $('#chart-add')
if (chart_add.length > 0) gridstack.removeWidget(chart_add.parent())
const gsi = `<div class="grid-stack-item ${item.color && 'color'}"><div id="${chid}" class="grid-stack-item-content" ${item.color ? `style="background-color:${item.color}` : ''}"></div></div>`
const gsi = `<div class="grid-stack-item ${item.bgcolor && 'bgcolor'}"><div id="${chid}" class="grid-stack-item-content" ${item.bgcolor ? `style="background-color:${item.bgcolor}` : ''}"></div></div>`
// Use gridstar
if (item.size_x || item.size_y) {
gridstack.addWidget(gsi, (item.col || 1) - 1, (item.row || 1) - 1, item.size_x || 2, item.size_y || 2, true, 2, 12, 2, 24)

View file

@ -6,8 +6,8 @@ See LICENSE and COMMERCIAL in the project root for license information.
*/
/* global InitModels */
$(document).ready(function () {
const $new = $('.btn-primary.new').on('click', function () {
$(document).ready(() => {
const $bnew = $('.btn-primary.new').on('click', () => {
const entityLabel = $val('#entityLabel'),
comments = $val('#comments')
if (!entityLabel) return RbHighbar.create($L('请输入实体名称'))
@ -22,14 +22,14 @@ $(document).ready(function () {
if (!data.mainEntity) return RbHighbar.create($L('请选择主实体'))
}
$new.button('loading')
$bnew.button('loading')
$.post(`/admin/entity/entity-new?nameField=${$val('#nameField')}&seriesField=${$val('#seriesField')}`, JSON.stringify(data), (res) => {
if (res.error_code === 0) parent.location.href = `${rb.baseUrl}/admin/entity/${res.data}/base`
else RbHighbar.error(res.error_msg)
})
})
const $copy = $('.btn-primary.copy').on('click', () => {
const $bcopy = $('.btn-primary.copy').on('click', () => {
const sourceEntity = $val('#copySourceEntity')
if (!sourceEntity) return RbHighbar.create($L('请选择从哪个实体复制'))
@ -43,7 +43,7 @@ $(document).ready(function () {
keepConfig: [],
}
$copy.button('loading')
$bcopy.button('loading')
$.post('/admin/entity/entity-copy', JSON.stringify(data), (res) => {
if (res.error_code === 0) parent.location.href = `${rb.baseUrl}/admin/entity/${res.data}/base`
else RbHighbar.error(res.error_msg)
@ -51,19 +51,19 @@ $(document).ready(function () {
})
let entities
function _loadEntities(call) {
function _loadEntities(c) {
if (entities) {
typeof call === 'function' && call(entities)
typeof c === 'function' && c(entities)
} else {
$.get('/admin/entity/entity-list', (res) => {
$.get('/admin/entity/entity-list?detail=true', (res) => {
entities = res.data
typeof call === 'function' && call(entities)
typeof c === 'function' && c(entities)
})
}
}
_loadEntities((e) => {
e.forEach(function (item) {
e.forEach((item) => {
$(`<option value="${item.entityName}" data-detail="${item.detailEntity || ''}">${item.entityLabel}</option>`).appendTo('#copySourceEntity')
})
@ -80,17 +80,19 @@ $(document).ready(function () {
if (e.length === 0) $(`<option value="">${$L('无可用实体')}</option>`).appendTo('#copySourceEntity')
})
$('#isDetail').on('click', function () {
$('#isDetail').on('click', () => {
$('.J_mainEntity').toggleClass('hide')
parent.RbModal.resize()
if ($('#mainEntity option').length === 0) {
_loadEntities((e) => {
e.forEach(function (item) {
if (!item.detailEntity) $(`<option value="${item.entityName}">${item.entityLabel}</option>`).appendTo('#mainEntity')
e.forEach((item) => {
if (!item.detailEntity && !item.mainEntity) {
$(`<option value="${item.entityName}">${item.entityLabel}</option>`).appendTo('#mainEntity')
}
})
if (e.length === 0) $(`<option value="">${$L('无可用实体')}</option>`).appendTo('#mainEntity')
if ($('#mainEntity option').length === 0) $(`<option value="">${$L('无可用实体')}</option>`).appendTo('#mainEntity')
})
}
})

View file

@ -81,7 +81,7 @@ $(document).ready(function () {
if (dt === 'REFERENCE' || dt === 'N2NREFERENCE') {
if (referenceLoaded === false) {
referenceLoaded = true
$.get('/admin/entity/entity-list?detail=true&bizz=false', (res) => {
$.get('/admin/entity/entity-list?detail=true', (res) => {
const d = res.data || []
d.push({ entityName: 'User', entityLabel: $L('用户') })
d.push({ entityName: 'Department', entityLabel: $L('部门') })

View file

@ -235,7 +235,7 @@ const render_item = function (data) {
fieldLabelOld: $item.find('.dd-handle').data('label'),
fieldHeight: $item.find('.dd-handle').attr('data-height') || null,
}
if (ov.fieldLabelOld === ov.fieldLabel) ov.fieldLabel = null
// if (ov.fieldLabelOld === ov.fieldLabel) ov.fieldLabel = null
renderRbcomp(<DlgEditField onConfirm={_onConfirm} {...ov} displayType={data.displayTypeName} />)
})

View file

@ -12,7 +12,7 @@ const maxOptions = isMulti ? 20 : 100
$(document).ready(function () {
const query = `entity=${$urlp('entity')}&field=${$urlp('field')}`
const $cs = $('.colors')
const $cs = $('.rbcolors')
RBCOLORS.forEach((c) => {
$(`<a style="background-color:${c}" data-color="${c}"></a>`).appendTo($cs)
})
@ -46,7 +46,7 @@ $(document).ready(function () {
}
const id = $('.J_text').attr('attr-id')
const color = $('.colors >a>i').parent().data('color') || ''
const color = $('.rbcolors >a>i').parent().data('color') || ''
let exists = null
$('.J_config .dd3-content, .unset-list .dd-handle>span').each(function () {

View file

@ -775,7 +775,7 @@ class ValueTagsEditor extends React.Component {
onKeyDown={(e) => e.keyCode === 13 && this._saveTag()}
/>
</div>
<div className="colors pt-2 pb-2 text-center">
<div className="rbcolors pt-2 pb-2 text-center">
{RBCOLORS.map((color) => {
return (
<a key={color} style={{ backgroundColor: color }} onClick={() => this.setState({ useColor: color })}>

View file

@ -419,13 +419,14 @@ var $same = function (a, b) {
var $is = $same
/**
* 是否为空兼容对象或数组
* 值是否为空兼容对象和数组
*/
var $empty = function (a) {
if (a === undefined || a === null || a === '') return true
var type = $.type(a)
if (type === 'array' && a.length === 0) return true
else return type === 'object' && Object.keys(a).length === 0
if (a === null || a === '' || typeof a === 'undefined') return true
var atype = $.type(a)
if (atype === 'array' && a.length === 0) return true
if (atype === 'object' && Object.keys(a).length === 0) return true
return !$.trim(a + '')
}
/**

View file

@ -200,6 +200,9 @@ class RbViewModal extends React.Component {
}
// -- Usage
static mode = 1
/**
* @param {*} props
* @param {Boolean} subView

View file

@ -21,6 +21,10 @@ class RbFormModal extends React.Component {
constructor(props) {
super(props)
this.state = { ...props, inLoad: true, _maximize: false }
this.__maximizeKey = `FormMaximize-${props.entity}`
this.state._maximize = $isTrue($storage.get(this.__maximizeKey))
if (!props.id) this.state.id = null
}
@ -44,7 +48,15 @@ class RbFormModal extends React.Component {
<span className="zmdi zmdi-settings up-1" />
</a>
)}
<button className="close md-close" type="button" title={this.state._maximize ? $L('向下还原') : $L('最大化')} onClick={() => this.setState({ _maximize: !this.state._maximize })}>
<button
className="close md-close"
type="button"
title={this.state._maximize ? $L('向下还原') : $L('最大化')}
onClick={() => {
this.setState({ _maximize: !this.state._maximize }, () => {
$storage.set(this.__maximizeKey, this.state._maximize)
})
}}>
<span className={`mdi ${this.state._maximize ? 'mdi mdi-window-restore' : 'mdi mdi-window-maximize'}`} />
</button>
<button className="close md-close" type="button" title={$L('关闭')} onClick={() => this.hide()}>
@ -784,9 +796,7 @@ class RbFormElement extends React.Component {
*/
isValueError() {
if (this.props.nullable === false) {
const v = this.state.value
if (v && $.type(v) === 'array') return v.length === 0 ? $L('不能为空') : null
else return !v ? $L('不能为空') : null
return $empty(this.state.value) ? $L('不能为空') : null
}
}
@ -857,6 +867,13 @@ class RbFormText extends RbFormElement {
constructor(props) {
super(props)
}
getValue() {
const v = super.getValue()
// fix: 3.1.1
if (v && v === $L('自动值')) return null
else return v
}
}
class RbFormUrl extends RbFormText {

View file

@ -17,6 +17,7 @@ const RBV_TRIGGERS = {
'DATAVALIDATE': $L('数据校验'),
'AUTOREVOKE': $L('自动撤销'),
'AUTODELETE': $L('自动删除'),
'PROXYTRIGGERACTION': $L('自定义触发器'),
}
const WHENS = {
@ -204,7 +205,12 @@ class TriggerEdit extends ConfigFormDlg {
this.__select2 = []
// #1
$.get('/admin/robot/trigger/available-actions', (res) => {
this.setState({ actions: res.data }, () => {
let actions = res.data || []
if (!$urlp('bosskey-show')) {
actions = actions.filter((item) => item[0] !== 'PROXYTRIGGERACTION')
}
this.setState({ actions }, () => {
const s2ot = $(this._$actionType)
.select2({
placeholder: $L('选择触发类型'),

View file

@ -46,7 +46,7 @@
<div class="data-info J_c-type">
<h5>[[${bundle.L('图表类型')}]]</h5>
<div class="chart-type">
<a th:title="${bundle.L('表格')}" data-type="TABLE" data-allow-dims="0|3" data-allow-nums="0|9"><i class="C200"></i></a>
<a th:title="${bundle.L('表格')}" data-type="TABLE" data-allow-dims="0|5" data-allow-nums="0|9"><i class="C200"></i></a>
<a th:title="${bundle.L('指标卡')}" data-type="INDEX" data-allow-dims="0|0" data-allow-nums="1|1"><i class="C310"></i></a>
<a th:title="${bundle.L('折线图')}" data-type="LINE" data-allow-dims="1|1" data-allow-nums="1|9"><i class="C220"></i></a>
<a th:title="${bundle.L('柱状图')}" data-type="BAR" data-allow-dims="1|1" data-allow-nums="1|9"><i class="C210"></i></a>
@ -69,55 +69,62 @@
<div class="J_opt-UNDEF">[[${bundle.L('当前图表无选项')}]]</div>
<div class="hide J_opt-ALL">
<label>
<a class="J_filter" href="javascript:;">
<a class="J_filter" href="###">
<i class="zmdi zmdi-filter-list down-2" style="font-size: 1.43rem; width: 22px; padding-left: 2px; color: #888"></i>
<span>[[${bundle.L('附加过滤条件')}]]</span>
<span></span>
</a>
</label>
<label class="custom-control custom-control-sm custom-checkbox mb-2 admin-show">
<label class="custom-control custom-control-sm custom-checkbox admin-show">
<input class="custom-control-input" type="checkbox" data-name="noPrivileges" />
<span class="custom-control-label"> [[${bundle.L('使用全部数据')}]] <i class="zmdi zmdi-help zicon" th:title="${bundle.L('不启用则仅能查看登录用户权限内的数据')}"></i></span>
<span class="custom-control-label">[[${bundle.L('使用全部数据')}]] <i class="zmdi zmdi-help zicon" th:title="${bundle.L('不启用则仅能查看登录用户权限内的数据')}"></i></span>
</label>
<label class="custom-control custom-control-sm custom-checkbox mb-2">
<label class="custom-control custom-control-sm custom-checkbox">
<input class="custom-control-input" type="checkbox" data-name="shareChart" />
<span class="custom-control-label"> [[${bundle.L('共享此图表')}]] <i class="zmdi zmdi-help zicon" th:title="${bundle.L('共享后其他用户也可以使用 (不能修改)')}"></i></span>
<span class="custom-control-label">[[${bundle.L('共享此图表')}]] <i class="zmdi zmdi-help zicon" th:title="${bundle.L('共享后其他用户也可以使用 (不能修改)')}"></i></span>
</label>
<label class="custom-control custom-control-sm custom-checkbox mb-2 hide">
<label class="custom-control custom-control-sm custom-checkbox hide">
<input class="custom-control-input" type="checkbox" data-name="noZero" />
<span class="custom-control-label"> [[${bundle.L('排除空数据 (数值为 0 不显示)')}]]</span>
<span class="custom-control-label">[[${bundle.L('排除空数据 (数值为 0 不显示)')}]]</span>
</label>
</div>
<div class="hide J_opt-TABLE">
<label class="custom-control custom-control-sm custom-checkbox mb-2">
<label class="custom-control custom-control-sm custom-checkbox">
<input class="custom-control-input" type="checkbox" data-name="showLineNumber" />
<span class="custom-control-label"> [[${bundle.L('显示行号')}]]</span>
<span class="custom-control-label">[[${bundle.L('显示行号')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox">
<input class="custom-control-input" type="checkbox" data-name="showSums" />
<span class="custom-control-label"> [[${bundle.L('显示汇总')}]]</span>
<span class="custom-control-label">[[${bundle.L('显示汇总')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox">
<input class="custom-control-input" type="checkbox" data-name="mergeCell" checked />
<span class="custom-control-label">[[${bundle.L('自动合并单元格')}]]</span>
</label>
</div>
<div class="hide J_opt-LINE J_opt-BAR J_opt-PIE J_opt-FUNNEL J_opt-TREEMAP J_opt-RADAR J_opt-SCATTER">
<label class="custom-control custom-control-sm custom-checkbox mb-2">
<label class="custom-control custom-control-sm custom-checkbox">
<input class="custom-control-input" type="checkbox" data-name="showNumerical" />
<span class="custom-control-label"> [[${bundle.L('在图表上显示数值')}]]</span>
<span class="custom-control-label">[[${bundle.L('在图表上显示数值')}]]</span>
</label>
</div>
<div class="hide J_opt-LINE J_opt-BAR J_opt-SCATTER">
<label class="custom-control custom-control-sm custom-checkbox mb-2">
<label class="custom-control custom-control-sm custom-checkbox">
<input class="custom-control-input" type="checkbox" data-name="showGrid" />
<span class="custom-control-label"> [[${bundle.L('显示参考线')}]]</span>
<span class="custom-control-label">[[${bundle.L('显示参考线')}]]</span>
</label>
</div>
<div class="hide J_opt-LINE J_opt-BAR J_opt-PIE J_opt-FUNNEL J_opt-RADAR J_opt-SCATTER">
<label class="custom-control custom-control-sm custom-checkbox mb-2">
<label class="custom-control custom-control-sm custom-checkbox">
<input class="custom-control-input" type="checkbox" data-name="showLegend" />
<span class="custom-control-label"> [[${bundle.L('显示图例')}]]</span>
<span class="custom-control-label">[[${bundle.L('显示图例')}]]</span>
</label>
</div>
<div class="hide J_opt-INDEX bosskey-show">
<input type="color" data-name="useColor" />
<div class="hide J_opt-INDEX">
<label style="color: #444">[[${bundle.L('配色')}]]</label>
<div id="useColor" class="rbcolors">
<a style="background-color: #ddd; border: 1px solid #ccc" th:title="${bundle.L('默认')}"></a>
</div>
</div>
</div>
</div>

View file

@ -66,7 +66,7 @@
<script th:src="@{/assets/js/rb-forms.append.js}" type="text/babel"></script>
<script type="text/babel">
CellRenders.clickView = function (v, e) {
if (parent && parent.RbViewModal) {
if (parent && parent.RbViewModal && parent.RbViewModal.mode === 1) {
parent.RbViewModal.create({ id: v.id, entity: v.entity })
} else {
const openUrl = `${rb.baseUrl}/app/${v.entity}/list/#!/View/${v.entity}/${v.id}`

View file

@ -79,7 +79,7 @@
</select>
</div>
<div class="tab-pane" id="URL">
<input type="text" class="form-control form-control-sm J_menuUrl" th:placeholder="${bundle.L('输入外部地址')}" />
<input type="text" class="form-control form-control-sm J_menuUrl" th:placeholder="${bundle.L('输入外部地址')}" maxlength="600" />
<div class="form-text">[[${bundle.L('支持绝对地址或相对地址')}]]</div>
</div>
</div>