Merge pull request #389 from getrebuild/trigger-fields-726

feat
This commit is contained in:
devezhao 2021-09-26 13:18:04 +08:00 committed by GitHub
commit 348dbe53c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 762 additions and 694 deletions

14
pom.xml
View file

@ -252,7 +252,7 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>commons</artifactId>
<version>1.3.4</version>
<version>1.3.5</version>
<exclusions>
<exclusion>
<artifactId>httpclient</artifactId>
@ -267,12 +267,12 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>bizz</artifactId>
<version>1.2.0</version>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>persist4j</artifactId>
<version>1.5.3</version>
<version>28559f7dea</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
@ -316,13 +316,13 @@
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>EasyCaptcha</artifactId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-system</artifactId>
<version>5.7.9</version>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>5.8.2</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>

View file

@ -37,6 +37,7 @@ import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.setup.Installer;
import com.rebuild.core.support.setup.UpgradeDatabase;
import com.rebuild.utils.JSONable;
import com.rebuild.utils.OshiUtils;
import com.rebuild.utils.RebuildBanner;
import com.rebuild.utils.codec.RbDateCodec;
import com.rebuild.utils.codec.RbRecordCodec;
@ -131,7 +132,7 @@ public class Application implements ApplicationListener<ApplicationStartedEvent>
" License : " + License.queryAuthority(false).values(),
"Access URLs : ",
" Local : " + localUrl,
" External : " + localUrl.replace("localhost", ServerStatus.getLocalIp()),
" External : " + localUrl.replace("localhost", OshiUtils.getLocalIp()),
" Public : " + RebuildConfiguration.getHomeUrl());
log.info(banner);
}

View file

@ -9,12 +9,7 @@ package com.rebuild.core;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.ThrowableUtils;
import cn.devezhao.commons.runtime.MemoryInformationBean;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.system.HostInfo;
import cn.hutool.system.SystemUtil;
import com.rebuild.core.cache.CommonsCache;
import com.rebuild.core.support.setup.Installer;
import lombok.extern.slf4j.Slf4j;
@ -188,40 +183,4 @@ public final class ServerStatus {
return new Status(name, false, error);
}
}
// --
/**
* JVM 内存用量
*
* @return [总计M, 已用%]
*/
public static double[] getJvmMemoryUsed() {
long maxMemory = RuntimeUtil.getMaxMemory();
long usableMemory = RuntimeUtil.getUsableMemory();
return new double[] {
(int) (maxMemory / MemoryInformationBean.MEGABYTES),
ObjectUtils.round(100 - (usableMemory * 100d / maxMemory), 2)
};
}
/**
* TODO CPU 负载
*
* @return
*/
public static double getSystemLoad() {
return 0d;
}
/**
* 本机 IP
*
* @return
*/
public static String getLocalIp() {
HostInfo host = SystemUtil.getHostInfo();
if (host == null || host.getAddress() == null) return "127.0.0.1";
else return host.getAddress();
}
}

View file

@ -23,6 +23,7 @@ import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyFile;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.easymeta.MixValue;
import com.rebuild.core.metadata.impl.EasyFieldConfigProps;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.collections4.map.CaseInsensitiveMap;
import org.apache.commons.lang.StringUtils;
@ -50,16 +51,40 @@ public class AutoFillinManager implements ConfigManager {
* @return
*/
public JSONArray getFillinValue(Field field, ID source) {
// @see field-edit.html 内置字段无配置
if (EasyMetaFactory.valueOf(field).isBuiltin()) {
return JSONUtils.EMPTY_ARRAY;
}
final EasyField easyField = EasyMetaFactory.valueOf(field);
// 内置字段无配置 @see field-edit.html
if (easyField.isBuiltin()) return JSONUtils.EMPTY_ARRAY;
final List<ConfigBean> config = getConfig(field);
if (config.isEmpty()) {
return JSONUtils.EMPTY_ARRAY;
// 父级级联
// 利用表单回填做父级级联字段回填
String cascadingField = easyField.getExtraAttr(EasyFieldConfigProps.REFERENCE_CASCADINGFIELD);
if (StringUtils.isNotBlank(cascadingField)) {
String[] fs = cascadingField.split(MetadataHelper.SPLITER2);
ConfigBean fake = new ConfigBean()
.set("source", fs[1])
.set("target", fs[0])
.set("whenCreate", true)
.set("whenUpdate", true)
.set("fillinForce", true);
// 移除冲突的表单回填配置
for (Iterator<ConfigBean> iter = config.iterator(); iter.hasNext(); ) {
ConfigBean cb = iter.next();
if (cb.getString("source").equals(fake.getString("source"))
&& cb.getString("target").equals(fake.getString("target"))) {
iter.remove();
break;
}
}
config.add(fake);
}
if (config.isEmpty()) return JSONUtils.EMPTY_ARRAY;
Entity sourceEntity = MetadataHelper.getEntity(source.getEntityCode());
Entity targetEntity = field.getOwnEntity();
Set<String> sourceFields = new HashSet<>();

View file

@ -19,6 +19,7 @@ import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.DynamicMetadataFactory;
import com.rebuild.core.metadata.impl.GhostEntity;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.util.Assert;
@ -36,6 +37,10 @@ import java.util.List;
@Slf4j
public class MetadataHelper {
// 通用分隔符
public static final String SPLITER = "$$$$";
public static final String SPLITER2 = "\\$\\$\\$\\$";
/**
* 元数据工厂
*
@ -365,11 +370,9 @@ public class MetadataHelper {
* @return
*/
public static boolean checkAndWarnField(Entity entity, String fieldName) {
if (entity.containsField(fieldName)) {
return true;
}
log.warn("Unknown field `" + fieldName + "` in `" + entity.getName() + "`");
if (entity.containsField(fieldName)) return true;
log.warn("Unknown field `{}` in `{}`", fieldName, entity.getName());
CommonsUtils.printStackTrace();
return false;
}

View file

@ -23,6 +23,12 @@ import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import java.util.HashSet;
import java.util.Set;
import static com.rebuild.core.metadata.MetadataHelper.SPLITER;
import static com.rebuild.core.metadata.MetadataHelper.SPLITER2;
/**
* @author zhaofang123@gmail.com
* @since 08/04/2018
@ -93,16 +99,18 @@ public class DynamicMetadataFactory extends ConfigurationMetadataFactory {
entity.addAttribute("extra-attrs", extraAttrs.toJSONString());
}
Set<String> cascadingFieldsChild = new HashSet<>();
Object[][] customFields = Application.createQueryNoFilter(
"select belongEntity,fieldName,physicalName,fieldLabel,displayType,nullable,creatable,updatable,"
+ "maxLength,defaultValue,refEntity,cascade,fieldId,comments,extConfig,repeatable,queryable from MetaField")
.array();
for (Object[] c : customFields) {
String entityName = (String) c[0];
String fieldName = (String) c[1];
final String entityName = (String) c[0];
final String fieldName = (String) c[1];
Element entityElement = (Element) rootElement.selectSingleNode("entity[@name='" + entityName + "']");
if (entityElement == null) {
log.warn("No entity found : " + entityName + "." + fieldName);
log.warn("No entity `{}` found for field `{}`", entityName, fieldName);
continue;
}
@ -159,12 +167,34 @@ public class DynamicMetadataFactory extends ConfigurationMetadataFactory {
extraAttrs.put("metaId", c[12]);
extraAttrs.put("comments", c[13]);
extraAttrs.put("displayType", dt.name());
String cascadingField = extraAttrs.getString(EasyFieldConfigProps.REFERENCE_CASCADINGFIELD);
if (StringUtils.isNotBlank(cascadingField)
&& (dt == DisplayType.REFERENCE || dt == DisplayType.N2NREFERENCE)) {
extraAttrs.put("_cascadingFieldParent", cascadingField);
String[] fs = cascadingField.split(SPLITER2);
cascadingFieldsChild.add(entityName + SPLITER + fs[0] + SPLITER + fieldName + SPLITER + fs[1]);
}
field.addAttribute("extra-attrs", extraAttrs.toJSONString());
}
if (log.isDebugEnabled()) {
XmlHelper.dump(rootElement);
// 处理父级级联的父子级关系
for (String child : cascadingFieldsChild) {
String[] fs = child.split(SPLITER2);
Element fieldElement = (Element) rootElement.selectSingleNode(
String.format("entity[@name='%s']/field[@name='%s']", fs[0], fs[1]));
if (fieldElement == null) {
log.warn("No field found: {}.{}", fs[0], fs[1]);
continue;
}
JSONObject extraAttrs = JSON.parseObject(fieldElement.valueOf("@extra-attrs"));
extraAttrs.put("_cascadingFieldChild", fs[2] + SPLITER + fs[3]);
fieldElement.addAttribute("extra-attrs", extraAttrs.toJSONString());
}
if (log.isDebugEnabled()) XmlHelper.dump(rootElement);
}
@Override

View file

@ -26,7 +26,7 @@ public class EasyFieldConfigProps {
/**
* 表单公式
*/
public static final String NUMBER_CALC_FORMULA = "calcFormula";
public static final String NUMBER_CALCFORMULA = "calcFormula";
/**
* 是否允许负数
@ -39,7 +39,7 @@ public class EasyFieldConfigProps {
/**
* 表单公式
*/
public static final String DECIMAL_CALC_FORMULA = NUMBER_CALC_FORMULA;
public static final String DECIMAL_CALCFORMULA = NUMBER_CALCFORMULA;
/**
* 日期格式
@ -89,17 +89,26 @@ public class EasyFieldConfigProps {
* 引用字段数据过滤
*/
public static final String REFERENCE_DATAFILTER = "referenceDataFilter";
/**
* 父级级联字段
*/
public static final String REFERENCE_CASCADINGFIELD = "referenceCascadingField";
/**
* 多引用字段数据过滤
* @see #REFERENCE_DATAFILTER
*/
public static final String N2NREFERENCE_DATAFILTER = REFERENCE_DATAFILTER;
/**
* 父级级联字段
* @see #REFERENCE_CASCADINGFIELD
*/
public static final String N2NREFERENCE_CASCADINGFIELD = REFERENCE_CASCADINGFIELD;
/**
* 多行文本使用 MD 编辑器
*/
public static final String NTEXT_USE_MDEDIT = "useMdedit";
public static final String NTEXT_USEMDEDIT = "useMdedit";
/**
* 信息脱敏

View file

@ -1,293 +0,0 @@
/*
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.metadata.impl;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.dialect.FieldType;
import cn.devezhao.persist4j.dialect.Type;
import cn.devezhao.persist4j.engine.ID;
import cn.devezhao.persist4j.metadata.BaseMeta;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.RebuildException;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.service.trigger.RobotTriggerManager;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
import java.util.Set;
/**
* 元数据Entity/Field封装
*
* @author zhaofang123@gmail.com
* @since 08/13/2018
*/
@SuppressWarnings("unused")
@Deprecated
public class EasyMeta implements BaseMeta {
private static final long serialVersionUID = -6463919098111506968L;
final private BaseMeta baseMeta;
public EasyMeta(BaseMeta baseMeta) {
this.baseMeta = baseMeta;
}
/**
* @return Returns Entity or Field
*/
public BaseMeta getBaseMeta() {
return baseMeta;
}
@Override
public String getName() {
return baseMeta.getName();
}
@Override
public String getPhysicalName() {
return baseMeta.getPhysicalName();
}
/**
* Use {@link #getLabel()}
*/
@Deprecated
@Override
public String getDescription() {
return baseMeta.getDescription();
}
@Override
public JSONObject getExtraAttrs() {
return getExtraAttrs(false);
}
/**
* @param clean
* @return
*/
public JSONObject getExtraAttrs(boolean clean) {
// see DynamicMetadataFactory
if (clean) {
JSONObject clone = (JSONObject) JSONUtils.clone(baseMeta.getExtraAttrs());
clone.remove("metaId");
clone.remove("comments");
clone.remove("icon");
clone.remove("displayType");
return clone;
}
return baseMeta.getExtraAttrs() == null ? JSONUtils.EMPTY_OBJECT : baseMeta.getExtraAttrs();
}
@Override
public boolean isCreatable() {
return baseMeta.isCreatable();
}
@Override
public boolean isUpdatable() {
if (isField()) {
if (!baseMeta.isUpdatable()) {
return false;
}
Field field = (Field) baseMeta;
Set<String> set = RobotTriggerManager.instance.getAutoReadonlyFields(field.getOwnEntity().getName());
return !set.contains(field.getName());
}
return baseMeta.isUpdatable();
}
@Override
public boolean isQueryable() {
return baseMeta.isQueryable();
}
/**
* 获取扩展属性
*
* @param name
* @return
* @see EasyFieldConfigProps
*/
public String getExtraAttr(String name) {
return getExtraAttrs().getString(name);
}
/**
* 系统内置字段/实体不可更改
*
* @return
* @see MetadataHelper#isCommonsField(Field)
*/
public boolean isBuiltin() {
if (this.getMetaId() == null) {
return true;
}
if (isField()) {
Field field = (Field) baseMeta;
if (MetadataHelper.isCommonsField(field)) {
return true;
} else if (getDisplayType() == DisplayType.REFERENCE) {
// 明细-引用主记录的字段也是内置
// @see MetadataHelper#getDetailToMainField
Entity hasMain = field.getOwnEntity().getMainEntity();
return hasMain != null && hasMain.equals(field.getReferenceEntity()) && !field.isCreatable();
}
}
return false;
}
/**
* @return
* @see #getDescription()
*/
public String getLabel() {
if (isField() && ((Field) baseMeta).getType() == FieldType.PRIMARY) {
return "ID";
}
return Language.L(this.baseMeta);
}
/**
* 自定义实体/字段 ID
*
* @return
*/
public ID getMetaId() {
String metaId = getExtraAttr("metaId");
return metaId == null ? null : ID.valueOf(metaId);
}
/**
* 取代 persist4j 中的 description persist4j 中的 description 则表示 label
*
* @return
*/
public String getComments() {
String comments = getExtraAttr("comments");
if (getMetaId() != null) {
return comments;
}
return StringUtils.defaultIfBlank(comments, Language.L("系统内置"));
}
@Override
public String toString() {
return "EASY#" + baseMeta.toString();
}
// -- ENTITY
/**
* 实体图标
*
* @return
*/
public String getIcon() {
Assert.isTrue(!isField(), "Entity supports only");
return StringUtils.defaultIfBlank(getExtraAttr("icon"), "texture");
}
/**
* 具有和业务实体一样的特性除权限以外因为无权限字段
*
* @return
*/
public boolean isPlainEntity() {
Assert.isTrue(!isField(), "Entity supports only");
return getExtraAttrs().getBooleanValue("plainEntity");
}
// -- FIELD
/**
* @return
*/
private boolean isField() {
return baseMeta instanceof Field;
}
/**
* @param fullName
* @return
*/
public String getDisplayType(boolean fullName) {
DisplayType dt = getDisplayType();
if (fullName) {
return dt.getDisplayName() + " (" + dt.name() + ")";
} else {
return dt.name();
}
}
/**
* @return
*/
public DisplayType getDisplayType() {
Assert.isTrue(isField(), "Field supports only");
String displayType = getExtraAttr("displayType");
DisplayType dt = displayType != null
? DisplayType.valueOf(displayType) : converBuiltinFieldType((Field) baseMeta);
if (dt != null) {
return dt;
}
throw new RebuildException("Unsupported field type : " + baseMeta);
}
/**
* 将字段类型转成 DisplayType
*
* @param field
* @return
*/
private DisplayType converBuiltinFieldType(Field field) {
Type ft = field.getType();
if (ft == FieldType.PRIMARY) {
return DisplayType.ID;
} else if (ft == FieldType.REFERENCE) {
int typeCode = field.getReferenceEntity().getEntityCode();
if (typeCode == EntityHelper.PickList) {
return DisplayType.PICKLIST;
} else if (typeCode == EntityHelper.Classification) {
return DisplayType.CLASSIFICATION;
} else {
return DisplayType.REFERENCE;
}
} else if (ft == FieldType.ANY_REFERENCE) {
return DisplayType.ANYREFERENCE;
} else if (ft == FieldType.REFERENCE_LIST) {
return DisplayType.N2NREFERENCE;
} else if (ft == FieldType.TIMESTAMP) {
return DisplayType.DATETIME;
} else if (ft == FieldType.DATE) {
return DisplayType.DATE;
} else if (ft == FieldType.STRING) {
return DisplayType.TEXT;
} else if (ft == FieldType.TEXT || ft == FieldType.NTEXT) {
return DisplayType.NTEXT;
} else if (ft == FieldType.BOOL) {
return DisplayType.BOOL;
} else if (ft == FieldType.INT || ft == FieldType.SMALL_INT || ft == FieldType.LONG) {
return DisplayType.NUMBER;
} else if (ft == FieldType.DOUBLE || ft == FieldType.DECIMAL) {
return DisplayType.DECIMAL;
}
return null;
}
}

View file

@ -153,7 +153,7 @@ public class FeedsHelper {
}
/**
* URL 提取
* URL 提取 FIXME 会匹配 " 符号
*/
public static final Pattern URL_PATTERN = Pattern.compile("((www|https?://)[-a-zA-Z0-9+&@#/%?=~_|!:,.;]{5,300})");
@ -164,7 +164,7 @@ public class FeedsHelper {
* @return
*/
public static String formatContent(String content) {
return formatContent(content, false);
return formatContent(content, true);
}
/**

View file

@ -41,7 +41,7 @@ public abstract class BaseTaskService extends ObservableService {
Assert.notNull(taskOrProject, "taskOrProject");
ConfigBean c = taskOrProject.getEntityCode() == EntityHelper.ProjectTask
? ProjectManager.instance.getProjectByTask(taskOrProject, null)
? ProjectManager.instance.getProjectByX(taskOrProject, null)
: ProjectManager.instance.getProject(taskOrProject, null);
if (c != null && c.get("members", Set.class).contains(user)) return true;

View file

@ -36,7 +36,7 @@ public class ProjectHelper {
taskOrComment = convert2Task(taskOrComment);
// 能访问就有读取权限
ProjectManager.instance.getProjectByTask(taskOrComment, user);
ProjectManager.instance.getProjectByX(taskOrComment, user);
return true;
} catch (ConfigurationException | AccessDeniedException ex) {
return false;
@ -60,9 +60,9 @@ public class ProjectHelper {
Object[] projectId = Application.getQueryFactory().uniqueNoFilter(taskOrCommentOrTag, "projectId");
pcfg = ProjectManager.instance.getProject((ID) projectId[0], null);
} else {
pcfg = ProjectManager.instance.getProjectByTask(convert2Task(taskOrCommentOrTag), null);
pcfg = ProjectManager.instance.getProjectByX(convert2Task(taskOrCommentOrTag), null);
}
// 负责人
if (user.equals(pcfg.getID("principal"))) return true;
// 非成员

View file

@ -38,8 +38,8 @@ public class ProjectManager implements ConfigManager {
}
private static final String CKEY_PROJECTS = "ProjectManager";
private static final String CKEY_PLAN = "ProjectPlan-";
private static final String CKEY_TASK = "Task2Project-";
private static final String CKEY_PLANS = "ProjectPlan-";
private static final String CKEY_TP2P = "TP2Project-";
/**
* @param user
@ -148,40 +148,6 @@ public class ProjectManager implements ConfigManager {
throw new ConfigurationException(Language.L("无权访问该项目或项目已删除"));
}
/**
* @param taskId
* @param user
* @return
* @throws ConfigurationException
* @throws AccessDeniedException
*/
public ConfigBean getProjectByTask(ID taskId, ID user) throws ConfigurationException, AccessDeniedException {
final String ckey = CKEY_TASK + taskId;
ID projectId = (ID) Application.getCommonsCache().getx(ckey);
if (projectId == null) {
Object[] task = Application.createQueryNoFilter(
"select projectId from ProjectTask where taskId = ?")
.setParameter(1, taskId)
.unique();
projectId = task == null ? null : (ID) task[0];
if (projectId != null) {
Application.getCommonsCache().putx(ckey, projectId);
}
}
if (projectId == null) {
throw new ConfigurationException(Language.L("任务不存在或已被删除"));
}
try {
return getProject(projectId, user);
} catch (ConfigurationException ex) {
throw new AccessDeniedException(Language.L("无权访问该任务"), ex);
}
}
/**
* 获取项目的任务面板
*
@ -191,7 +157,7 @@ public class ProjectManager implements ConfigManager {
public ConfigBean[] getPlansOfProject(ID projectId) {
Assert.notNull(projectId, "[projectId] cannot be null");
final String ckey = CKEY_PLAN + projectId;
final String ckey = CKEY_PLANS + projectId;
ConfigBean[] cache = (ConfigBean[]) Application.getCommonsCache().getx(ckey);
if (cache == null) {
@ -240,20 +206,50 @@ public class ProjectManager implements ConfigManager {
throw new ConfigurationException(Language.L("无效任务面板 (%s)", planId));
}
/**
* @param taskOrPlan
* @param checkUser
* @return
*/
public ConfigBean getProjectByX(ID taskOrPlan, ID checkUser) {
final String ckey = CKEY_TP2P + taskOrPlan;
ID projectId = (ID) Application.getCommonsCache().getx(ckey);
if (projectId == null) {
Object[] x = Application.getQueryFactory().uniqueNoFilter(taskOrPlan, "projectId");
projectId = x == null ? null : (ID) x[0];
if (projectId != null) {
Application.getCommonsCache().putx(ckey, projectId);
}
}
if (projectId == null) {
throw new ConfigurationException(Language.L("任务/面板不存在或已被删除"));
}
try {
return getProject(projectId, checkUser);
} catch (ConfigurationException ex) {
throw new AccessDeniedException(Language.L("无权访问该项目"), ex);
}
}
@Override
public void clean(Object nullOrAnyProjectId) {
int ec = nullOrAnyProjectId == null ? -1 : ((ID) nullOrAnyProjectId).getEntityCode();
int e = nullOrAnyProjectId == null ? -1 : ((ID) nullOrAnyProjectId).getEntityCode();
// 清理项目
if (ec == -1) {
if (e == -1) {
Application.getCommonsCache().evict(CKEY_PROJECTS);
}
// 清理面板
else if (ec == EntityHelper.ProjectConfig) {
Application.getCommonsCache().evict(CKEY_PLAN + nullOrAnyProjectId);
else if (e == EntityHelper.ProjectConfig) {
Application.getCommonsCache().evict(CKEY_PLANS + nullOrAnyProjectId);
}
// 清理任务
else if (ec == EntityHelper.ProjectTask) {
Application.getCommonsCache().evict(CKEY_TASK + nullOrAnyProjectId);
else if (e == EntityHelper.ProjectTask || e == EntityHelper.ProjectPlanConfig) {
Application.getCommonsCache().evict(CKEY_TP2P + nullOrAnyProjectId);
Application.getCommonsCache().evict(CKEY_TP2P + nullOrAnyProjectId);
}
}
}

View file

@ -82,7 +82,7 @@ public class AggregationEvaluator {
List<String[]> fields = new ArrayList<>();
for (String m : matchsVars) {
String[] fieldAndFunc = m.split("\\$\\$\\$\\$");
String[] fieldAndFunc = m.split(MetadataHelper.SPLITER2);
if (MetadataHelper.getLastJoinField(sourceEntity, fieldAndFunc[0]) == null) {
throw new MissingMetaExcetion(fieldAndFunc[0], sourceEntity.getName());
}
@ -114,7 +114,7 @@ public class AggregationEvaluator {
String[] field = fields.get(i);
Object value = useSourceData[i] == null ? "0" : useSourceData[i];
String replace = "{" + StringUtils.join(field, "$$$$") + "}";
String replace = "{" + StringUtils.join(field, MetadataHelper.SPLITER) + "}";
clearFormual = clearFormual.replace(replace.toUpperCase(), value.toString());
}

View file

@ -176,7 +176,7 @@ public class EvaluatorUtils {
}
}
// 日期加 `date = dateadd(date, interval[H|D|M|Y])`
// 日期加 `date = DATEADD(date, interval[H|D|M|Y])`
static class DateAddFunction extends AbstractFunction {
private static final long serialVersionUID = 8286269123891483078L;

View file

@ -7,6 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.support.general;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
@ -21,9 +22,13 @@ import com.rebuild.core.service.dashboard.ChartManager;
import com.rebuild.core.service.query.AdvFilterParser;
import com.rebuild.core.service.query.ParseHelper;
import com.rebuild.utils.JSONUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 解析已知的个性化过滤条件
@ -31,12 +36,13 @@ import java.util.Collections;
* @author devezhao
* @since 2020/6/13
*/
@Slf4j
public class ProtocolFilterParser {
final private String protocolExpr;
/**
* @param protocolExpr via:xxx ref:xxx
* @param protocolExpr via:xxx:[field] ref:xxx:[id]
*/
public ProtocolFilterParser(String protocolExpr) {
this.protocolExpr = protocolExpr;
@ -52,10 +58,12 @@ public class ProtocolFilterParser {
return parseVia(ps[1], ps.length > 2 ? ps[2] : null);
}
case "ref": {
return parseRef(ps[1]);
return parseRef(ps[1], ps.length > 2 ? ps[2] : null);
}
default:
default: {
log.warn("Unknown protocol expr : {}", protocolExpr);
return null;
}
}
}
@ -80,7 +88,7 @@ public class ProtocolFilterParser {
ConfigBean filter = AdvFilterManager.instance.getAdvFilter(anyId);
if (filter != null) filterExp = (JSONObject) filter.getJSON("filter");
}
// via others
// via OTHERS
else if (refField != null) {
String[] entityAndField = refField.split("\\.");
Assert.isTrue(entityAndField.length == 2, "Bad `via` filter defined");
@ -98,24 +106,53 @@ public class ProtocolFilterParser {
/**
* @param content
* @param cascadingValue
* @return
*/
public String parseRef(String content) {
public String parseRef(String content, String cascadingValue) {
String[] fieldAndEntity = content.split("\\.");
if (fieldAndEntity.length != 2 || !MetadataHelper.checkAndWarnField(fieldAndEntity[1], fieldAndEntity[0])) {
return null;
}
Field field = MetadataHelper.getField(fieldAndEntity[1], fieldAndEntity[0]);
final Entity entity = MetadataHelper.getEntity(fieldAndEntity[1]);
final Field field = entity.getField(fieldAndEntity[0]);
List<String> sqls = new ArrayList<>();
JSONObject advFilter = getFieldDataFilter(field);
return advFilter == null ? null : new AdvFilterParser(advFilter).toSqlWhere();
if (advFilter != null) sqls.add(new AdvFilterParser(advFilter).toSqlWhere());
if (hasFieldCascadingField(field) && ID.isId(cascadingValue)) {
String cascadingFieldParent = field.getExtraAttrs().getString("_cascadingFieldParent");
String cascadingFieldChild = field.getExtraAttrs().getString("_cascadingFieldChild");
if (StringUtils.isNotBlank(cascadingFieldParent)) {
String[] fs = cascadingFieldParent.split(MetadataHelper.SPLITER2);
sqls.add(String.format("%s = '%s'", fs[1], cascadingValue));
}
if (StringUtils.isNotBlank(cascadingFieldChild)) {
String[] fs = cascadingFieldChild.split(MetadataHelper.SPLITER2);
Entity refEntity = entity.getField(fs[0]).getReferenceEntity();
String sql = String.format("exists (select %s from %s where ^%s = %s and %s = '%s')",
fs[1], refEntity.getName(),
field.getReferenceEntity().getPrimaryField().getName(), fs[1],
refEntity.getPrimaryField().getName(), cascadingValue);
sqls.add(sql);
}
}
return sqls.isEmpty() ? null
: "( " + StringUtils.join(sqls, " and ") + " )";
}
/**
* 是否启用了数据过滤
* 附加过滤条件
*
* @param field
* @return
* @see #parseRef(String, String)
*/
public static JSONObject getFieldDataFilter(Field field) {
String dataFilter = EasyMetaFactory.valueOf(field).getExtraAttr(EasyFieldConfigProps.REFERENCE_DATAFILTER);
@ -127,4 +164,16 @@ public class ProtocolFilterParser {
}
return null;
}
/**
* 是否级联字段
*
* @param field
* @return
* @see #parseRef(String, String)
*/
public static boolean hasFieldCascadingField(Field field) {
return field.getExtraAttrs().containsKey("_cascadingFieldParent")
|| field.getExtraAttrs().containsKey("_cascadingFieldChild");
}
}

View file

@ -18,8 +18,6 @@ import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.i18n.LanguageBundle;
import com.rebuild.web.admin.AdminVerfiyController;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import javax.servlet.http.HttpServletRequest;
@ -149,32 +147,6 @@ public class AppUtils {
return UA != null && UA.startsWith("RB/Mobile-");
}
/**
* 请求类型
*
* @param request
* @return
* @see MimeTypeUtils#parseMimeType(String)
*/
public static MimeType parseMimeType(HttpServletRequest request) {
try {
String acceptType = request.getHeader("Accept");
if (acceptType == null || "*/*".equals(acceptType)) acceptType = request.getContentType();
// Via Spider?
if (StringUtils.isBlank(acceptType)) return MimeTypeUtils.TEXT_HTML;
acceptType = acceptType.split("[,;]")[0];
// Accpet ALL?
if ("*/*".equals(acceptType)) return MimeTypeUtils.TEXT_HTML;
return MimeTypeUtils.parseMimeType(acceptType);
} catch (Exception ignore) {
}
return null;
}
/**
* 是否 IE11加载 polyfill
*

View file

@ -8,6 +8,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.utils;
import cn.devezhao.commons.ObjectUtils;
import com.rebuild.core.Application;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
@ -145,9 +146,11 @@ public class CommonsUtils {
* @return
*/
public static void printStackTrace() {
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
for (StackTraceElement traceElement : trace) {
System.err.println("\tat " + traceElement);
if (Application.devMode() || log.isDebugEnabled()) {
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
for (StackTraceElement traceElement : trace) {
System.err.println("\tat " + traceElement);
}
}
}
}

View file

@ -110,9 +110,7 @@ public class JSONUtils {
* @return
*/
public static boolean wellFormat(String text) {
if (StringUtils.isBlank(text)) {
return false;
}
if (StringUtils.isBlank(text)) return false;
text = text.trim();
return (text.startsWith("{") && text.endsWith("}")) || (text.startsWith("[") && text.endsWith("]"));
}

View file

@ -10,6 +10,7 @@ package com.rebuild.utils;
import com.vladsch.flexmark.ext.tables.TablesExtension;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.parser.ParserEmulationProfile;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.data.MutableDataSet;
@ -26,20 +27,21 @@ public class MarkdownUtils {
private static final MutableDataSet OPTIONS = new MutableDataSet();
static {
OPTIONS.set(Parser.EXTENSIONS, Collections.singletonList(TablesExtension.create()));
// OPTIONS.set(HtmlRenderer.SOFT_BREAK, "<br/>");
OPTIONS.setFrom(ParserEmulationProfile.MARKDOWN)
.set(Parser.EXTENSIONS, Collections.singletonList(TablesExtension.create()));
}
private static final Parser PARSER = Parser.builder(OPTIONS).build();
private static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS).build();
/**
* MD 渲染支持表格
* MD 渲染支持表格HTML 代码会转义
*
* @param md
* @return
*/
public static String render(String md) {
md = CommonsUtils.escapeHtml(md);
Node document = PARSER.parse(md);
return RENDERER.render(document);
}

View file

@ -0,0 +1,74 @@
/*
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.utils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.runtime.MemoryInformationBean;
import oshi.SystemInfo;
import oshi.hardware.GlobalMemory;
import oshi.hardware.NetworkIF;
import java.util.List;
/**
* @author devezhao
* @since 2021/9/22
*/
public class OshiUtils {
private static SystemInfo SI;
/**
* @return
*/
synchronized public static SystemInfo getSI() {
if (SI == null) SI = new SystemInfo();
return SI;
}
/**
* OS 内存
*
* @return
*/
public static double[] getOsMemoryUsed() {
GlobalMemory memory = getSI().getHardware().getMemory();
long memoryTotal = memory.getTotal();
double memoryUsage = (memoryTotal - memory.getAvailable()) * 1.0 / memoryTotal;
return new double[]{
(int) (memoryTotal / MemoryInformationBean.MEGABYTES),
ObjectUtils.round(memoryUsage * 100, 2)
};
}
/**
* CPU 负载
*
* @return
*/
public static double getSystemLoad() {
double[] loadAverages = getSI().getHardware().getProcessor().getSystemLoadAverage(2);
return ObjectUtils.round(loadAverages[1], 2);
}
/**
* 本机 IP
*
* @return
*/
public static String getLocalIp() {
List<NetworkIF> nets = getSI().getHardware().getNetworkIFs();
if (nets == null || nets.isEmpty()) return "localhost";
for (NetworkIF net : nets) {
String[] ipsv4 = net.getIPv4addr();
if (ipsv4 != null && ipsv4.length > 0) return ipsv4[0];
}
return "127.0.0.1";
}
}

View file

@ -27,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.NamedThreadLocal;
import org.springframework.http.HttpStatus;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@ -67,13 +68,9 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
final RequestEntry requestEntry = new RequestEntry(request, locale);
REQUEST_ENTRY.set(requestEntry);
// 页面变量
// 请求页面时如果是非 HTML 请求头可能导致失败因为页面中需要用到语言包变量
if (requestEntry.isHtmlRequest()) {
// Lang
request.setAttribute(WebConstants.LOCALE, requestEntry.getLocale());
request.setAttribute(WebConstants.$BUNDLE, Application.getLanguage().getBundle(requestEntry.getLocale()));
}
// Lang
request.setAttribute(WebConstants.LOCALE, requestEntry.getLocale());
request.setAttribute(WebConstants.$BUNDLE, Application.getLanguage().getBundle(requestEntry.getLocale()));
final String requestUri = requestEntry.getRequestUri();
@ -108,7 +105,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
// 管理中心
if (requestUri.contains("/admin/") && !AppUtils.isAdminVerified(request)) {
if (requestEntry.isHtmlRequest()) {
if (isHtmlRequest(request)) {
sendRedirect(response, "/user/admin-verify", requestUri);
} else {
ServletUtils.writeJson(response, RespBody.error(HttpStatus.FORBIDDEN.value()).toJSONString());
@ -119,7 +116,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
UserContextHolder.setUser(requestUser);
// 页面变量登录后
if (requestEntry.isHtmlRequest()) {
if (isHtmlRequest(request)) {
// Last active
Application.getSessionStore().storeLastActive(request);
@ -148,7 +145,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
log.warn("Unauthorized access {}", RebuildWebConfigurer.getRequestUrls(request));
if (requestEntry.isHtmlRequest()) {
if (isHtmlRequest(request)) {
sendRedirect(response, "/user/login", requestUri);
} else {
ServletUtils.writeJson(response, RespBody.error(HttpStatus.UNAUTHORIZED.value()).toJSONString());
@ -228,19 +225,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
}
/**
* @param response
* @param url
* @param nexturl
* @throws IOException
*/
private void sendRedirect(HttpServletResponse response, String url, String nexturl) throws IOException {
String fullUrl = AppUtils.getContextPath() + url;
if (nexturl != null) fullUrl += "?nexturl=" + CodecUtils.urlEncode(nexturl);
response.sendRedirect(fullUrl);
}
/**
* 忽略认证
* 忽略认证的资源
*
* @param requestUri
* @return
@ -270,27 +255,54 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
|| requestUri.startsWith("/rbmob/env");
}
/**
* 是否 HTML 请求
*
* @param request
* @return
*/
private boolean isHtmlRequest(HttpServletRequest request) {
if (ServletUtils.isAjaxRequest(request) || request.getRequestURI().contains("/assets/")) {
return false;
}
try {
MimeType mimeType = MimeTypeUtils.parseMimeType(
StringUtils.defaultIfBlank(request.getHeader("Accept"), MimeTypeUtils.TEXT_HTML_VALUE).split("[,;]")[0]);
return MimeTypeUtils.TEXT_HTML.equals(mimeType);
} catch (Exception ignored) {
return false;
}
}
/**
* @param response
* @param url
* @param nexturl
* @throws IOException
*/
private void sendRedirect(HttpServletResponse response, String url, String nexturl) throws IOException {
String fullUrl = AppUtils.getContextPath(url);
if (nexturl != null) fullUrl += "?nexturl=" + CodecUtils.urlEncode(nexturl);
response.sendRedirect(fullUrl);
}
/**
* 请求参数封装
*/
@Data
private static class RequestEntry {
final long requestTime;
final ID requestUser;
final String requestUri;
final boolean htmlRequest;
final ID requestUser;
final String locale;
RequestEntry(HttpServletRequest request, String locale) {
this.requestTime = System.currentTimeMillis();
this.requestUri = request.getRequestURI()
+ (request.getQueryString() != null ? ("?" + request.getQueryString()) : "");
this.htmlRequest = !ServletUtils.isAjaxRequest(request)
&& MimeTypeUtils.TEXT_HTML.equals(AppUtils.parseMimeType(request));
this.locale = locale;
this.requestUser = AppUtils.getRequestUser(request, true);
this.locale = locale;
}
@Override

View file

@ -272,7 +272,7 @@ public class ConfigurationController extends BaseController {
ConfigurationItem item = ConfigurationItem.valueOf(e.getKey());
RebuildConfiguration.set(item, e.getValue());
} catch (Exception ex) {
log.error("Invalid item : " + e.getKey() + " = " + e.getValue());
log.error("Invalid item : {} = {}", e.getKey(), e.getValue(), ex);
}
}
}

View file

@ -39,10 +39,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
/**
* @author zhaofang123@gmail.com
@ -124,7 +121,13 @@ public class MetaFieldController extends BaseController {
}
// 扩展配置
mv.getModel().put("fieldExtConfig", easyField.getExtraAttrs(true));
JSONObject extraAttrs = new JSONObject();
for (Map.Entry<String, Object> e : easyField.getExtraAttrs(true).entrySet()) {
String name = e.getKey();
// 排除私有
if (!name.startsWith("_")) extraAttrs.put(name, e.getValue());
}
mv.getModel().put("fieldExtConfig", extraAttrs);
return mv;
}
@ -190,4 +193,32 @@ public class MetaFieldController extends BaseController {
boolean drop = new Field2Schema(user).dropField(field, false);
return drop ? RespBody.ok() : RespBody.error();
}
@RequestMapping("field-cascading-fields")
public RespBody fieldCascadingFields(@EntityParam Entity currentEntity, HttpServletRequest request) {
Field refField = currentEntity.getField(getParameterNotNull(request, "field"));
Entity referenceEntity = refField.getReferenceEntity();
// 找到共同的引用字段
Field[] currentEntityFields = MetadataSorter.sortFields(currentEntity, DisplayType.REFERENCE);
Field[] referenceEntityFields = MetadataSorter.sortFields(referenceEntity, DisplayType.REFERENCE);
List<JSONObject> together = new ArrayList<>();
for (Field foo : currentEntityFields) {
if (MetadataHelper.isCommonsField(foo)) continue;
Entity fooEntity = foo.getReferenceEntity();
for (Field bar : referenceEntityFields) {
if (fooEntity.equals(bar.getReferenceEntity())) {
// 当前实体字段$$$$引用实体字段
String name = foo.getName() + MetadataHelper.SPLITER + bar.getName();
String label = String.format("%s (%s)", EasyMetaFactory.getLabel(foo), EasyMetaFactory.getLabel(bar));
together.add(JSONUtils.toJSONObject(
new String[] { "name", "label" }, new String[] { name, label } ));
}
}
}
return RespBody.ok(together);
}
}

View file

@ -13,6 +13,7 @@ import com.rebuild.core.Application;
import com.rebuild.core.ServerStatus;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.AppUtils;
import com.rebuild.utils.OshiUtils;
import com.rebuild.web.BaseController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@ -57,8 +58,8 @@ public class ErrorPageView extends BaseController {
ModelAndView mv = createModelAndView("/error/server-status");
mv.getModel().put("ok", ServerStatus.isStatusOK() && Application.isReady());
mv.getModel().put("status", ServerStatus.getLastStatus(realtime));
mv.getModel().put("MemoryUsage", ServerStatus.getJvmMemoryUsed());
mv.getModel().put("SystemLoad", ServerStatus.getSystemLoad());
mv.getModel().put("MemoryUsage", OshiUtils.getOsMemoryUsed());
mv.getModel().put("SystemLoad", OshiUtils.getSystemLoad());
mv.getModelMap().put("isAdminVerified", AppUtils.isAdminVerified(request));
return mv;
}
@ -76,8 +77,8 @@ public class ErrorPageView extends BaseController {
for (ServerStatus.Status item : ServerStatus.getLastStatus(realtime)) {
status.put(item.name, item.success ? true : item.error);
}
status.put("MemoryUsage", ServerStatus.getJvmMemoryUsed()[1]);
status.put("SystemLoad", ServerStatus.getSystemLoad());
status.put("MemoryUsage", OshiUtils.getOsMemoryUsed()[1]);
status.put("SystemLoad", OshiUtils.getSystemLoad());
ServletUtils.writeJson(response, s.toJSONString());
}

View file

@ -74,7 +74,8 @@ public class ReferenceSearchController extends EntityController {
// 引用字段数据过滤
// 启用数据过滤后最近搜索将不可用
String protocolFilter = new ProtocolFilterParser(null).parseRef(field + "." + entity.getName());
String protocolFilter = new ProtocolFilterParser(null)
.parseRef(field + "." + entity.getName(), request.getParameter("cascadingValue"));
String q = getParameter(request, "q");
// 为空则加载最近使用的
@ -272,8 +273,12 @@ public class ReferenceSearchController extends EntityController {
mv.getModel().put("canCreate",
Application.getPrivilegesManager().allowCreate(user, searchEntity.getEntityCode()));
if (ProtocolFilterParser.getFieldDataFilter(field) != null) {
mv.getModel().put("referenceFilter", "ref:" + getParameter(request, "field"));
if (ProtocolFilterParser.getFieldDataFilter(field) != null
|| ProtocolFilterParser.hasFieldCascadingField(field)) {
String protocolExpr = String.format("ref:%s:%s",
getParameterNotNull(request, "field"),
StringUtils.defaultString(getParameter(request, "cascadingValue"), ""));
mv.getModel().put("referenceFilter", protocolExpr);
} else {
mv.getModel().put("referenceFilter", StringUtils.EMPTY);
}

View file

@ -50,7 +50,7 @@ public class ProjectAdminController extends BaseController {
}
Object[] p = Application.createQuery(
"select projectName,scope,principal,members from ProjectConfig where configId = ?")
"select projectName,scope,principal,members,extraDefinition from ProjectConfig where configId = ?")
.setParameter(1, projectId2)
.unique();
@ -59,6 +59,7 @@ public class ProjectAdminController extends BaseController {
mv.getModelMap().put("scope", p[1]);
mv.getModelMap().put("principal", p[2]);
mv.getModelMap().put("members", p[3]);
mv.getModelMap().put("extraDefinition", p[4]);
return mv;
}

View file

@ -9,6 +9,7 @@ package com.rebuild.web.project;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Query;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
@ -41,6 +42,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
@ -73,7 +75,7 @@ public class ProjectTaskController extends BaseController {
return null;
}
ConfigBean project = ProjectManager.instance.getProjectByTask(taskId2, user);
ConfigBean project = ProjectManager.instance.getProjectByX(taskId2, user);
ModelAndView mv = createModelAndView("/project/task-view");
mv.getModel().put("id", taskId2.toLiteral());
@ -85,7 +87,7 @@ public class ProjectTaskController extends BaseController {
@RequestMapping("tasks/list")
public JSON taskList(@IdParam(name = "plan") ID planId, HttpServletRequest request) {
final ID user = getRequestUser(request);
String queryWhere = "projectPlanId = ?";
String queryWhere = "projectPlanId = '" + planId + "'";
// 关键词搜索
String search = getParameter(request, "search");
@ -108,9 +110,7 @@ public class ProjectTaskController extends BaseController {
int count = -1;
if (pageNo == 1) {
String countSql = "select count(taskId) from ProjectTask where " + queryWhere;
Object[] count2 = Application.createQueryNoFilter(countSql)
.setParameter(1, planId)
.unique();
Object[] count2 = Application.createQueryNoFilter(countSql).unique();
count = ObjectUtils.toInt(count2[0]);
if (count == 0) {
@ -119,19 +119,7 @@ public class ProjectTaskController extends BaseController {
}
queryWhere += " order by " + buildQuerySort(request);
String querySql = "select " + BASE_FIELDS + " from ProjectTask where " + queryWhere;
Object[][] tasks = Application.createQueryNoFilter(querySql)
.setParameter(1, planId)
.setLimit(pageSize, pageNo * pageSize - pageSize)
.array();
JSONArray alist = new JSONArray();
for (Object[] o : tasks) {
JSONObject item = formatTask(o, user);
item.remove("planName");
alist.add(item);
}
JSONArray alist = queryCardDatas(planId, user, queryWhere, new int[] { pageSize, pageNo * pageSize - pageSize });
return JSONUtils.toJSONObject(
new String[] { "count", "tasks" },
@ -139,12 +127,72 @@ public class ProjectTaskController extends BaseController {
}
@GetMapping("tasks/get")
public JSON taskGet(@IdParam(name = "task") ID taskId) {
Object[] task = Application.createQueryNoFilter(
"select " + BASE_FIELDS + " from ProjectTask where taskId = ?")
.setParameter(1, taskId)
.unique();
return formatTask(task, null);
public JSON taskGet(@IdParam(name = "task") ID taskId, HttpServletRequest request) {
String where = "taskId = '" + taskId + "'";
JSONArray a = queryCardDatas(taskId, getRequestUser(request), where, null);
return (JSON) a.get(0);
}
private JSONArray queryCardDatas(ID taskOrPlan, ID user, String queryWhere, int[] limits) {
// 卡片显示字段
ConfigBean project = ProjectManager.instance.getProjectByX(taskOrPlan, user);
JSON cardFields = project.getJSON("cardFields");
final Set<String> fields2show = new HashSet<>();
if (cardFields == null) {
fields2show.add("createdOn");
fields2show.add("endTime");
fields2show.add("_tag");
} else {
for (Object o : (JSONArray) cardFields) {
fields2show.add(o.toString());
}
}
String queryFields = FMT_FIELDS11 + ",";
if (fields2show.contains("createdBy")) queryFields += "createdBy,";
else queryFields += "taskId,";
if (fields2show.contains("modifiedOn")) queryFields += "modifiedOn,";
else queryFields += "taskId,";
if (fields2show.contains("description")) queryFields += "description,";
else queryFields += "taskId,";
if (fields2show.contains("attachments")) queryFields += "attachments,";
else queryFields += "taskId,";
queryFields = queryFields.substring(0, queryFields.length() - 1);
String querySql = String.format("select %s from ProjectTask where %s", queryFields, queryWhere);
Query query = Application.createQueryNoFilter(querySql);
if (limits != null) query.setLimit(limits[0], limits[1]);
Object[][] tasks = query.array();
JSONArray alist = new JSONArray();
for (Object[] o : tasks) {
JSONObject item = formatTask(o, user, fields2show.contains("_tag"));
if (fields2show.contains("createdBy")) {
item.put("createdBy", new Object[] { o[12], UserHelper.getName((ID) o[12]) });
}
if (!fields2show.contains("createdOn")) {
item.remove("createdOn");
}
if (fields2show.contains("modifiedOn")) {
item.put("modifiedOn", I18nUtils.formatDate((Date) o[13]));
}
if (!fields2show.contains("endTime")) {
item.remove("endTime");
}
if (fields2show.contains("description")) {
item.put("description", StringUtils.isNotBlank((String) o[14]));
}
if (fields2show.contains("attachments")) {
item.put("attachments", o[15] != null && ((String) o[15]).length() > 10);
}
item.remove("planName");
alist.add(item);
}
return alist;
}
@GetMapping("tasks/details")
@ -152,10 +200,10 @@ public class ProjectTaskController extends BaseController {
final ID user = getRequestUser(request);
Object[] task = Application.createQueryNoFilter(
"select " + BASE_FIELDS + ",description,attachments,relatedRecord from ProjectTask where taskId = ?")
String.format("select %s,description,attachments,relatedRecord from ProjectTask where taskId = ?", FMT_FIELDS11))
.setParameter(1, taskId)
.unique();
JSONObject details = formatTask(task, user);
JSONObject details = formatTask(task, user, true);
details.put("description", task[12]);
String attachments = (String) task[13];
@ -172,30 +220,33 @@ public class ProjectTaskController extends BaseController {
return details;
}
private static final String BASE_FIELDS =
private static final String FMT_FIELDS11 =
"projectId,projectPlanId,taskNumber,taskId,taskName,createdOn,deadline,executor,status,seq,priority,endTime";
/**
* @param o
* @param user
* @param putTags
* @return
* @throws ConfigurationException 如果指定用户无权限
* @see #FMT_FIELDS11
*/
private JSONObject formatTask(Object[] o, ID user) throws ConfigurationException {
private JSONObject formatTask(Object[] o, ID user, boolean putTags) throws ConfigurationException {
final ConfigBean project = ProjectManager.instance.getProject((ID) o[0], user);
String taskNumber = String.format("%s-%s", project.getString("projectCode"), o[2]);
String createdOn = I18nUtils.formatDate((Date) o[5]);
String deadline = I18nUtils.formatDate((Date) o[6]);
String endTime = I18nUtils.formatDate((Date) o[11]);
Object[] executor = o[7] == null ? null : new Object[]{o[7], UserHelper.getName((ID) o[7])};
JSONObject data = JSONUtils.toJSONObject(
new String[] { "id", "taskNumber", "taskName", "createdOn", "deadline", "executor", "status", "seq", "priority", "endTime", "projectId" },
new Object[] { o[3], taskNumber, o[4], createdOn, deadline, executor, o[8], o[9], o[10], endTime, o[0] });
// 标签
data.put("tags", TaskTagController.getTaskTags((ID) o[3]));
if (putTags) {
data.put("tags", TaskTagController.getTaskTags((ID) o[3]));
}
if (user != null) {
// 项目信息
@ -219,7 +270,8 @@ public class ProjectTaskController extends BaseController {
return sort;
}
// for View of Entity
// -- for General Entity
@GetMapping("alist")
public RespBody getProjectAndPlans(HttpServletRequest request) {
final ID user = getRequestUser(request);
@ -269,15 +321,15 @@ public class ProjectTaskController extends BaseController {
queryWhere = String.format("taskId = '%s'", taskId);
}
String querySql = "select " + BASE_FIELDS + " from ProjectTask where " + queryWhere;
Object[][] tasks = Application.createQueryNoFilter(querySql)
Object[][] tasks = Application.createQueryNoFilter(
String.format("select %s from ProjectTask where %s", FMT_FIELDS11, queryWhere))
.setLimit(pageSize, pageNo * pageSize - pageSize)
.array();
JSONArray array = new JSONArray();
for (Object[] o : tasks) {
try {
array.add(formatTask(o, user));
array.add(formatTask(o, user, false));
} catch (ConfigurationException ex) {
// FIXME 无项目权限会报错考虑任务在相关项中是否无权限也显示
log.warn(ex.getLocalizedMessage());

View file

@ -36,7 +36,7 @@
</tr>
<tr>
<td>X.509 Certificate</td>
<td data-id="SamlIdPCert" th:data-value="${SamlIdPCert ?:''}">[[${SamlIdPCert ?:bundle.L('未设置')}]]</td>
<td data-id="SamlIdPCert" th:data-value="${SamlIdPCert}"><pre class="unstyle">[[${SamlIdPCert ?:bundle.L('未设置')}]]</pre></td>
</tr>
</tbody>
</table>

View file

@ -3,7 +3,7 @@
<head>
<th:block th:replace="~{/_include/header}" />
<link rel="stylesheet" type="text/css" th:href="@{/assets/lib/widget/bootstrap-slider.min.css}" />
<meta name="page-help" content="https://getrebuild.com/docs/admin/meta-entity#%E5%BC%95%E7%94%A8%E5%AD%97%E6%AE%B5%E7%9A%84%E6%9B%B4%E5%A4%9A%E8%AF%B4%E6%98%8E" />
<meta name="page-help" content="https://getrebuild.com/docs/admin/meta-entity#%E5%BC%95%E7%94%A8%E5%AD%97%E6%AE%B5" />
<title>[[${bundle.L('表单回填')}]]</title>
<style type="text/css">
.dataTables_wrapper .rb-datatable-header {

View file

@ -84,6 +84,15 @@
</div>
</div>
</div>
<div class="form-group row">
<label class="col-md-12 col-xl-3 col-lg-4 col-form-label text-lg-right">[[${bundle.L('父级级联字段')}]]</label>
<div class="col-md-12 col-xl-6 col-lg-8">
<select class="form-control form-control-sm" id="referenceCascadingField">
<option value="" selected>[[${bundle.L('无')}]]</option>
</select>
<p class="form-text">[[${bundle.L('选择的字段将与本字段产生级联关系')}]]</p>
</div>
</div>
<div class="form-group row pt-0 pb-2 J_for-REFERENCE-filter">
<label class="col-md-12 col-xl-3 col-lg-4 col-form-label text-lg-right">[[${bundle.L('附加过滤条件')}]]</label>
<div class="col-md-12 col-xl-6 col-lg-8">

View file

@ -31,6 +31,13 @@
#members {
min-height: 37px;
}
#cardFields {
margin-top: 6px;
}
#cardFields > label {
min-width: 130px;
margin-bottom: 6px;
}
</style>
</head>
<body>
@ -84,6 +91,41 @@
<button class="btn btn-secondary btn-sm J_add-plan" type="button"><i class="zmdi zmdi-plus"></i> [[${bundle.L('添加')}]]</button>
</div>
</div>
<div class="form-group row">
<label class="col-12 col-lg-3 col-form-label text-lg-right">[[${bundle.L('任务卡显示字段')}]]</label>
<div class="col-12 col-lg-9">
<div id="cardFields">
<label class="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input class="custom-control-input" type="checkbox" value="createdBy" />
<span class="custom-control-label">[[${bundle.L('创建人')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input class="custom-control-input" type="checkbox" value="createdOn" checked />
<span class="custom-control-label">[[${bundle.L('创建时间')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input class="custom-control-input" type="checkbox" value="modifiedOn" />
<span class="custom-control-label">[[${bundle.L('更新时间')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input class="custom-control-input" type="checkbox" value="endTime" checked />
<span class="custom-control-label">[[${bundle.L('完成时间')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input class="custom-control-input" type="checkbox" value="description" />
<span class="custom-control-label">[[${bundle.L('详情')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input class="custom-control-input" type="checkbox" value="attachments" />
<span class="custom-control-label">[[${bundle.L('附件')}]]</span>
</label>
<label class="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input class="custom-control-input" type="checkbox" value="_tag" checked />
<span class="custom-control-label">[[${bundle.L('标签')}]]</span>
</label>
</div>
</div>
</div>
<div class="form-group row footer">
<label class="col-12 col-lg-3 col-form-label text-lg-right"> </label>
<div class="col-12 col-lg-9">
@ -103,6 +145,7 @@
scope: ~~'[[${scope}]]',
principal: '[[${principal}]]',
members: '[[${members}]]',
extraDefinition: [(${extraDefinition ?:'null'})],
}
</script>
<script th:src="@{/assets/js/project/project-editor.js}" type="text/babel"></script>

View file

@ -5,23 +5,29 @@
<meta name="page-help" content="https://getrebuild.com/docs/admin/projects" />
<title>[[${bundle.L('项目管理')}]]</title>
<style type="text/css">
.project-icon {
display: inline-block;
width: 36px;
height: 36px;
background-color: #e3e3e3;
text-align: center;
border-radius: 2px;
}
.project-icon .icon,
.project-icon > .icon,
.card-body > .icon {
font-size: 26px;
color: #555;
display: inline-block;
text-align: center;
border-radius: 2px;
}
.project-icon > .icon {
width: 36px;
height: 36px;
line-height: 36px;
background-color: #e3e3e3;
}
.project-icon:hover {
opacity: 0.8;
}
.card-body > .icon {
width: 24px;
height: 24px;
line-height: 24px;
margin-right: -9px;
}
.card-body {
position: relative;
}

View file

@ -58,7 +58,7 @@
[[${bundle.L('页脚')}]]
<p class="link">[(${bundle.L('仅适用于外部页面,支持 [MD 语法](https://getrebuild.com/docs/markdown-guide)')})]</p>
</td>
<td data-id="PageFooter" th:data-value="${PageFooter}" data-optional="true">[(${PageFooter ?:bundle.L('无')})]</td>
<td data-id="PageFooter" th:data-value="${PageFooter}" data-optional="true"><pre class="unstyle">[[${PageFooter ?:bundle.L('无')}]]</pre></td>
</tr>
<tr>
<td>[[${bundle.L('默认语言')}]]</td>

View file

@ -184,11 +184,23 @@ See LICENSE and COMMERCIAL in the project root for license information.
margin-top: 4px;
}
.task-content .task-extras > .avatar,
.task-content .task-extras > .icon {
float: left;
cursor: default;
}
.task-content .task-extras > .avatar img {
width: 24px;
height: 24px;
}
.task-content .task-extras > .icon {
font-size: 1.3rem;
color: #999;
margin-top: 5px;
}
.task-content .task-extras > .badge {
background-color: #f7f7f7;
color: #595959;

View file

@ -2670,9 +2670,8 @@ form {
}
.card-list .card-body > a {
display: inline-block;
overflow: hidden;
padding-top: 3px;
display: block;
padding: 3px 0 5px;
}
.card-list .card-body > p {
@ -4348,4 +4347,12 @@ html.external-auth .auth-body.must-center .login {
content: '\f26b' !important;
color: #34a853;
font-weight: bold;
}
pre.unstyle {
padding: 0;
margin: 0;
background-color: transparent;
font-size: 1rem;
font-family: inherit;
}

View file

@ -29,7 +29,7 @@ $(document).ready(() => {
const __data = {}
const changeValue = function (e) {
const name = e.target.name
__data[name] = e.target.value
__data[name] = (e.target.value || '').trim()
}
// 激活编辑模式

View file

@ -343,6 +343,15 @@ const _handleClassification = function (useClassification) {
const _handleReference = function (isN2N) {
const referenceEntity = $('.J_referenceEntity').data('refentity')
// 父级字段
$.get(`/admin/entity/field-cascading-fields?entity=${wpc.entityName}&field=${wpc.fieldName}`, (res) => {
res.data &&
res.data.forEach((item) => {
$(`<option value="${item.name}">${item.label}</option>`).appendTo('#referenceCascadingField')
})
wpc.extConfig.referenceCascadingField && $('#referenceCascadingField').val(wpc.extConfig.referenceCascadingField)
})
// 数据过滤
let dataFilter = (wpc.extConfig || {}).referenceDataFilter
const saveFilter = function (res) {

View file

@ -24,9 +24,19 @@ $(document).ready(() => {
renderRbcomp(<PlanList />, 'plans', function () {
_PlanList = this
})
$('.J_add-plan').click(() => renderRbcomp(<PlanEdit projectId={wpc.id} flowNexts={_PlanList.getPlans()} seq={_PlanList.getMaxSeq() + 1000} />))
const $btn = $('.J_save').click(() => {
$('.J_add-plan').on('click', () => {
renderRbcomp(<PlanEdit projectId={wpc.id} flowNexts={_PlanList.getPlans()} seq={_PlanList.getMaxSeq() + 1000} />)
})
if (wpc.extraDefinition && wpc.extraDefinition.cardFields) {
$('#cardFields input').each(function () {
const $chk = $(this)
$chk.attr('checked', wpc.extraDefinition.cardFields.includes($chk.val()))
})
}
const $btn = $('.J_save').on('click', () => {
const data = {
scope: $('#scope_2').prop('checked') ? 2 : 1,
principal: _Principal.val().join(','),
@ -35,6 +45,14 @@ $(document).ready(() => {
}
if (!data.members) return RbHighbar.create($L('请选择成员'))
const fs = []
$('#cardFields input:checked').each(function () {
fs.push($(this).val())
})
const extra = wpc.extraDefinition || {}
extra.cardFields = fs
data.extraDefinition = extra
$btn.button('loading')
$.post('/admin/projects/post', JSON.stringify(data), (res) => {
if (res.error_code === 0) location.href = '../projects'
@ -62,10 +80,10 @@ class PlanList extends React.Component {
</div>
<div className="card-footer card-footer-contrast">
<a onClick={() => this._handleEdit(item)}>
<i className="zmdi zmdi-edit"></i>
<i className="zmdi zmdi-edit" />
</a>
<a onClick={() => this._handleDelete(item[0])} className="danger danger-hover">
<i className="zmdi zmdi-delete"></i>
<i className="zmdi zmdi-delete" />
</a>
</div>
</div>
@ -162,55 +180,26 @@ class PlanEdit extends RbFormHandler {
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('面板名称')}</label>
<div className="col-sm-7">
<input
type="text"
className="form-control form-control-sm"
name="planName"
value={this.state.planName || ''}
onChange={this.handleChange}
maxLength="60"
autoFocus
/>
<input type="text" className="form-control form-control-sm" name="planName" value={this.state.planName || ''} onChange={this.handleChange} maxLength="60" autoFocus />
</div>
</div>
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('工作流状态')}</label>
<div className="col-sm-7">
<label className="custom-control custom-control-sm custom-radio mb-1 mt-1">
<input
className="custom-control-input"
type="radio"
name="flowStatus"
value="1"
checked={~~this.state.flowStatus === 1}
onChange={this.handleChange}
/>
<input className="custom-control-input" type="radio" name="flowStatus" value="1" checked={~~this.state.flowStatus === 1} onChange={this.handleChange} />
<span className="custom-control-label">
{$L('开始状态')} <p className="text-muted mb-0 fs-12">{$L('该状态下可新建任务')}</p>
</span>
</label>
<label className="custom-control custom-control-sm custom-radio mb-1">
<input
className="custom-control-input"
type="radio"
name="flowStatus"
value="2"
checked={~~this.state.flowStatus === 2}
onChange={this.handleChange}
/>
<input className="custom-control-input" type="radio" name="flowStatus" value="2" checked={~~this.state.flowStatus === 2} onChange={this.handleChange} />
<span className="custom-control-label">
{$L('进行中')} <p className="text-muted mb-0 fs-12">{$L('该状态下不可新建任务不可完成任务')}</p>
</span>
</label>
<label className="custom-control custom-control-sm custom-radio mb-1">
<input
className="custom-control-input"
type="radio"
name="flowStatus"
value="3"
checked={~~this.state.flowStatus === 3}
onChange={this.handleChange}
/>
<input className="custom-control-input" type="radio" name="flowStatus" value="3" checked={~~this.state.flowStatus === 3} onChange={this.handleChange} />
<span className="custom-control-label">
{$L('结束状态')} <p className="text-muted mb-0 fs-12">{$L('该状态下任务自动标记完成')}</p>
</span>

View file

@ -93,13 +93,7 @@ class DlgEdit extends RbFormHandler {
<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"
value={this.state.projectName || ''}
data-id="projectName"
onChange={this.handleChange}
maxLength="60"
/>
<input className="form-control form-control-sm" value={this.state.projectName || ''} data-id="projectName" onChange={this.handleChange} maxLength="60" />
</div>
</div>
{!this.props.id && (
@ -107,13 +101,7 @@ class DlgEdit extends RbFormHandler {
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('项目 ID')}</label>
<div className="col-sm-7">
<input
className="form-control form-control-sm "
value={this.state.projectCode || ''}
data-id="projectCode"
onChange={this.handleChange}
maxLength="6"
/>
<input className="form-control form-control-sm " value={this.state.projectCode || ''} data-id="projectCode" onChange={this.handleChange} maxLength="6" />
<div className="form-text">{$L('任务编号将以项目 ID 作为前缀用以区别不同项目支持 2-6 位字母')}</div>
</div>
</div>
@ -149,7 +137,7 @@ class DlgEdit extends RbFormHandler {
that.setState({ iconName: s })
RbModal.hide()
}
RbModal.create('/p/common/search-icon', $L('选择图标'))
RbModal.create('/p/common/search-icon', $L('选择图标'), { zIndex: 1051 })
}
save = () => {

View file

@ -289,7 +289,7 @@ class PlanBox extends React.Component {
const $boxes = document.getElementById('plan-boxes')
$addResizeHandler(() => {
let mh = $(window).height() - 210 + (this.creatableTask ? 0 : 44)
if ($boxes.scrollWidth > $boxes.clientWidth) mh -= 5 // 横向滚动条高度
if ($boxes.scrollWidth > $boxes.clientWidth) mh -= 13 // 横向滚动条高度
$scroller.css({ 'max-height': mh })
$scroller.perfectScrollbar('update')
@ -395,13 +395,14 @@ class PlanBox extends React.Component {
}
}
// 任务
// 任务
class Task extends React.Component {
state = { ...this.props }
render() {
let deadlineState = -1
if (!this.state.endTime && this.state.deadline) {
// 未完成且设置了到期时间
if (this.state.status !== 1 && this.state.deadline) {
if ($expired(this.state.deadline)) deadlineState = 2
else if ($expired(this.state.deadline, -60 * 60 * 24)) deadlineState = 1
else deadlineState = 0
@ -424,21 +425,29 @@ class Task extends React.Component {
defaultChecked={this.state.status === 1}
onChange={(e) => this._toggleStatus(e)}
disabled={!this.props.$$$parent.performableTask}
ref={(c) => (this._status = c)}
ref={(c) => (this._$status = c)}
/>
<span className="custom-control-label" />
</label>
</div>
<div className="task-content">
<div className="task-title text-wrap">{this.state.taskName}</div>
{this.state.endTime && (
<div className="task-time">
{$L('完成时间')} <DateShow date={this.state.endTime} />
</div>
)}
<div className="task-time">
{$L('创建时间')} <DateShow date={this.state.createdOn} />
</div>
{this.state.modifiedOn && (
<div className="task-time">
{$L('更新时间')} <DateShow date={this.state.modifiedOn} />
</div>
)}
{this.state.createdOn && (
<div className="task-time">
{$L('创建时间')} <DateShow date={this.state.createdOn} />
</div>
)}
{deadlineState > -1 && (
<div className="task-time">
<span className={`badge badge-${deadlineState === 2 ? 'danger' : deadlineState === 1 ? 'warning' : 'primary'}`}>
@ -446,6 +455,7 @@ class Task extends React.Component {
</span>
</div>
)}
{(this.state.tags || []).length > 0 && (
<div className="task-tags">
{this.state.tags.map((item) => {
@ -459,12 +469,22 @@ class Task extends React.Component {
})}
</div>
)}
<div className="task-extras">
{this.state.createdBy && (
<a className="avatar mr-1" title={`${$L('创建人')} ${this.state.createdBy[1]}`}>
<img src={`${rb.baseUrl}/account/user-avatar/${this.state.createdBy[0]}`} alt="Avatar" />
</a>
)}
{this.state.executor && (
<a className="avatar float-left" title={`${$L('执行人')} ${this.state.executor[1]}`}>
<a className="avatar mr-1" title={`${$L('执行人')} ${this.state.executor[1]}`}>
<img src={`${rb.baseUrl}/account/user-avatar/${this.state.executor[0]}`} alt="Avatar" />
</a>
)}
{this.state.description && <i className="ml-2 icon zmdi zmdi-comment-more" title={$L('详情')} />}
{this.state.attachments && <i className="ml-3 icon zmdi zmdi-attachment-alt zmdi-hc-rotate-45" title={$L('附件')} />}
<span className="badge float-right">{this.state.taskNumber}</span>
<div className="clearfix" />
</div>
@ -481,7 +501,7 @@ class Task extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (prevState && prevState.status !== this.state.status) {
$(this._status).prop('checked', this.state.status === 1)
$(this._$status).prop('checked', this.state.status === 1)
}
// __TaskRefs[this.props.id] = this
}

View file

@ -15,9 +15,13 @@ class RbModal extends React.Component {
}
render() {
const inFrame = !this.props.children
const styles = {}
if (this.props.zIndex) styles.zIndex = this.props.zIndex
const iframe = !this.props.children // No child
return (
<div className={`modal rbmodal colored-header colored-header-${this.props.colored || 'primary'}`} ref={(c) => (this._rbmodal = c)}>
<div className={`modal rbmodal colored-header colored-header-${this.props.colored || 'primary'}`} style={styles} ref={(c) => (this._rbmodal = c)}>
<div className="modal-dialog" style={{ maxWidth: `${this.props.width || 680}px` }}>
<div className="modal-content">
<div className="modal-header modal-header-colored">
@ -26,9 +30,9 @@ class RbModal extends React.Component {
<span className="zmdi zmdi-close" />
</button>
</div>
<div className={`modal-body ${inFrame ? 'iframe rb-loading' : ''} ${inFrame && this.state.frameLoad !== false ? 'rb-loading-active' : ''}`}>
<div className={`modal-body ${iframe ? 'iframe rb-loading' : ''} ${iframe && this.state.frameLoad !== false ? 'rb-loading-active' : ''}`}>
{this.props.children || <iframe src={this.props.url} frameBorder="0" scrolling="no" onLoad={() => this.resize()} />}
{inFrame && <RbSpinner />}
{iframe && <RbSpinner />}
</div>
</div>
</div>
@ -100,7 +104,7 @@ class RbModal extends React.Component {
that.__HOLDER.show()
that.__HOLDER.resize()
} else {
renderRbcomp(<RbModal url={url} title={title} width={options.width} disposeOnHide={options.disposeOnHide} />, null, function () {
renderRbcomp(<RbModal url={url} title={title} width={options.width} disposeOnHide={options.disposeOnHide} zIndex={options.zIndex} />, null, function () {
that.__HOLDER = this
if (options.disposeOnHide === false) that.__HOLDERs[url] = this
})

View file

@ -224,15 +224,10 @@ class DeleteConfirm extends RbAlert {
{!this.props.entity ? null : (
<div className="mt-2">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mb-2">
<input
className="custom-control-input"
type="checkbox"
checked={this.state.enableCascade === true}
onChange={() => this.enableCascade()}
/>
<input className="custom-control-input" type="checkbox" checked={this.state.enableCascade === true} onChange={() => this.enableCascade()} />
<span className="custom-control-label"> {$L('同时删除关联记录')}</span>
</label>
<div className={' ' + (this.state.enableCascade ? '' : 'hide')}>
<div className={this.state.enableCascade ? '' : 'hide'}>
<select className="form-control form-control-sm" ref={(c) => (this._cascades = c)} multiple>
{(this.state.cascadesEntity || []).map((item) => {
return (

View file

@ -6,6 +6,8 @@ See LICENSE and COMMERCIAL in the project root for license information.
*/
/* global SimpleMDE */
const TYPE_DIVIDER = '$DIVIDER$'
// ~~ 表单窗口
class RbFormModal extends React.Component {
constructor(props) {
@ -205,7 +207,7 @@ class RbForm extends React.Component {
<div className="rbform">
<div className="form" ref={(c) => (this._form = c)}>
{this.props.children.map((fieldComp) => {
const refid = `fieldcomp-${fieldComp.props.field}`
const refid = fieldComp.props.field === TYPE_DIVIDER ? null : `fieldcomp-${fieldComp.props.field}`
return React.cloneElement(fieldComp, { $$$parent: this, ref: refid })
})}
{this.renderFormAction()}
@ -278,13 +280,12 @@ class RbForm extends React.Component {
// 表单回填
setAutoFillin(data) {
if (!data || data.length === 0) return
const that = this
data.forEach((item) => {
// eslint-disable-next-line react/no-string-refs
const fieldComp = that.refs[`fieldcomp-${item.target}`]
const fieldComp = this.refs[`fieldcomp-${item.target}`]
if (fieldComp) {
if (!item.fillinForce && fieldComp.getValue()) return
if ((that.isNew && item.whenCreate) || (!that.isNew && item.whenUpdate)) fieldComp.setValue(item.value)
if ((this.isNew && item.whenCreate) || (!this.isNew && item.whenUpdate)) fieldComp.setValue(item.value)
}
})
}
@ -1198,6 +1199,7 @@ class RbFormReference extends RbFormElement {
constructor(props) {
super(props)
this._hasDataFilter = props.referenceDataFilter && (props.referenceDataFilter.items || []).length > 0
this._hasCascadingField = !!(props._cascadingFieldParent || props._cascadingFieldChild)
}
renderElement() {
@ -1255,20 +1257,20 @@ class RbFormReference extends RbFormElement {
label: this.props.label,
entity: this.props.$$$parent.props.entity,
// appendClass: this._hasDataFilter ? 'data-filter-tip' : null,
wrapQuery: (query) => {
const cascadingValue = this._getCascadingFieldValue()
return cascadingValue ? { cascadingValue, ...query } : query
},
})
const val = this.state.value
if (val) {
this.setValue(val)
// const o = new Option(val.text, val.id, true, true)
// this.__select2.append(o).trigger('change')
}
if (val) this.setValue(val)
const that = this
this.__select2.on('change', function (e) {
const v = $(e.target).val()
if (v && typeof v === 'string') {
$.post(`/commons/search/recently-add?id=${v}`)
__addRecentlyUse(v)
that.triggerAutoFillin(v)
}
that.handleChange({ target: { value: v } }, true)
@ -1278,12 +1280,31 @@ class RbFormReference extends RbFormElement {
}
}
_getCascadingFieldValue() {
let cascadingField
if (this.props._cascadingFieldParent) {
cascadingField = this.props._cascadingFieldParent.split('$$$$')[0]
} else if (this.props._cascadingFieldChild) {
cascadingField = this.props._cascadingFieldChild.split('$$$$')[0]
}
if (!cascadingField) return null
if (this.props.onView) {
const v = (this.props.$$$parent.__ViewData || {})[cascadingField]
return v ? v.id : null
} else {
const fieldComp = this.props.$$$parent.refs[`fieldcomp-${cascadingField}`]
return fieldComp ? fieldComp.getValue() : null
}
}
// 字段回填
triggerAutoFillin(value) {
if (this.props.onView) return
const $$$parent = this.props.$$$parent
$.get(`/app/entity/extras/fillin-value?entity=${$$$parent.props.entity}&field=${this.props.field}&source=${value}`, (res) => {
const url = `/app/entity/extras/fillin-value?entity=${$$$parent.props.entity}&field=${this.props.field}&source=${value}`
$.get(url, (res) => {
res.error_code === 0 && res.data.length > 0 && $$$parent.setAutoFillin(res.data)
})
}
@ -1296,8 +1317,7 @@ class RbFormReference extends RbFormElement {
setValue(val) {
if (val) {
const o = new Option(val.text, val.id, true, true)
this.__select2.append(o)
this.handleChange({ target: { value: val.id } }, true)
this.__select2.append(o).trigger('change')
} else {
this.__select2.val(null).trigger('change')
}
@ -1307,27 +1327,27 @@ class RbFormReference extends RbFormElement {
const that = this
window.referenceSearch__call = function (selected) {
that.showSearcher_call(selected, that)
that.__searcher.hide()
that._ReferenceSearcher.hide()
}
if (this.__searcher) {
this.__searcher.show()
if (this._ReferenceSearcher && !this._hasCascadingField) {
this._ReferenceSearcher.show()
} else {
const searchUrl = `${rb.baseUrl}/commons/search/reference-search?field=${this.props.field}.${this.props.$$$parent.props.entity}`
const url = `${rb.baseUrl}/commons/search/reference-search?field=${this.props.field}.${this.props.$$$parent.props.entity}&cascadingValue=${this._getCascadingFieldValue() || ''}`
// eslint-disable-next-line react/jsx-no-undef
renderRbcomp(<ReferenceSearcher url={searchUrl} title={$L('选择%s', this.props.label)} />, function () {
that.__searcher = this
renderRbcomp(<ReferenceSearcher url={url} title={$L('选择%s', this.props.label)} disposeOnHide={this._hasCascadingField} />, function () {
that._ReferenceSearcher = this
})
}
}
showSearcher_call(selected, that) {
const first = selected[0]
if ($(that._fieldValue).find(`option[value="${first}"]`).length > 0) {
that.__select2.val(first).trigger('change')
const s = selected[0]
if ($(that._fieldValue).find(`option[value="${s}"]`).length > 0) {
that.__select2.val(s).trigger('change')
} else {
$.get(`/commons/search/read-labels?ids=${first}`, (res) => {
const o = new Option(res.data[first], first, true, true)
$.get(`/commons/search/read-labels?ids=${s}`, (res) => {
const o = new Option(res.data[s], s, true, true)
that.__select2.append(o).trigger('change')
})
}
@ -1402,20 +1422,15 @@ class RbFormN2NReference extends RbFormReference {
}
that.setValue(val, true)
})
this._recentlyAdd(ids)
__addRecentlyUse(ids)
}
onEditModeChanged(destroy) {
super.onEditModeChanged(destroy)
if (!destroy && this.__select2) {
this.__select2.on('select2:select', (e) => this._recentlyAdd(e.params.data.id))
this.__select2.on('select2:select', (e) => __addRecentlyUse(e.params.data.id))
}
}
_recentlyAdd(id) {
if (!id) return
$.post(`/commons/search/recently-add?id=${id}`)
}
}
class RbFormClassification extends RbFormElement {
@ -1761,8 +1776,6 @@ class RbFormDivider extends React.Component {
}
}
const TYPE_DIVIDER = '$DIVIDER$'
// 确定元素类型
var detectElement = function (item) {
if (!item.key) item.key = 'field-' + (item.field === TYPE_DIVIDER ? $random() : item.field)
@ -1831,6 +1844,12 @@ const __findMultiTexts = function (options, maskValue) {
return texts
}
// 最近使用
const __addRecentlyUse = function (id) {
if (!id) return
$.post(`/commons/search/recently-add?id=${id}`)
}
// ~ 重复记录查看
class RepeatedViewer extends RbModalHandler {
constructor(props) {

View file

@ -580,18 +580,24 @@ var $unmount = function (container, delay, keepContainer) {
/**
* 初始化引用字段搜索
*/
var $initReferenceSelect2 = function (el, field) {
var $initReferenceSelect2 = function (el, options) {
var search_input = null
return $(el).select2({
placeholder: field.placeholder || $L('选择%s', field.label),
placeholder: options.placeholder || $L('选择%s', options.label),
minimumInputLength: 0,
maximumSelectionLength: $(el).attr('multiple') ? 999 : 2,
ajax: {
url: '/commons/search/' + (field.searchType || 'reference'),
url: '/commons/search/' + (options.searchType || 'reference'),
delay: 300,
data: function (params) {
search_input = params.term
return { entity: field.entity, field: field.name, q: params.term }
const query = {
entity: options.entity,
field: options.name,
q: params.term,
}
if (options && typeof options.wrapQuery === 'function') return options.wrapQuery(query)
else return query
},
processResults: function (data) {
return { results: data.data }
@ -614,7 +620,7 @@ var $initReferenceSelect2 = function (el, field) {
return $L('清除')
},
},
theme: 'default ' + (field.appendClass || ''),
theme: 'default ' + (options.appendClass || ''),
})
}

View file

@ -6,6 +6,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
*/
const wpc = window.__PageConfig || {}
const TYPE_DIVIDER = '$DIVIDER$'
//~~ 视图
class RbViewForm extends React.Component {
@ -41,11 +42,13 @@ class RbViewForm extends React.Component {
hadApproval = null
}
const viewData = {}
const VFORM = (
<div>
{hadApproval && <ApprovalProcessor id={this.props.id} entity={this.props.entity} />}
<div className="row">
{res.data.elements.map((item) => {
if (item.field !== TYPE_DIVIDER) viewData[item.field] = item.value
item.$$$parent = this
return detectViewElement(item)
})}
@ -58,6 +61,8 @@ class RbViewForm extends React.Component {
window.FrontJS.View._trigger('open', [res.data])
}
})
this.__ViewData = viewData
this.__lastModified = res.data.lastModified || 0
})
}
@ -124,11 +129,12 @@ class RbViewForm extends React.Component {
[fieldName]: fieldValue.value,
}
const $btns = $(fieldComp._fieldText).find('.edit-oper .btn').button('loading')
const $btn = $(fieldComp._fieldText).find('.edit-oper .btn').button('loading')
$.post('/app/entity/record-save?single=true', JSON.stringify(data), (res) => {
$btns.button('reset')
$btn.button('reset')
if (res.error_code === 0) {
this.setFieldUnchanged(fieldName)
this.__ViewData[fieldName] = res.data[fieldName]
fieldComp.toggleEditMode(false, res.data[fieldName])
// 刷新列表
parent && parent.RbListPage && parent.RbListPage.reload()
@ -147,7 +153,7 @@ const detectViewElement = function (item) {
if (!window.detectElement) throw 'detectElement undef'
item.onView = true
item.editMode = false
item.key = `col-${item.field === '$DIVIDER$' ? $random() : item.field}`
item.key = `col-${item.field === TYPE_DIVIDER ? $random() : item.field}`
return (
<div className={`col-12 col-sm-${item.isFull ? 12 : 6}`} key={item.key}>
{window.detectElement(item)}

View file

@ -83,7 +83,7 @@
</tr>
<tr>
<th>Local IP</th>
<td>[[${T(com.rebuild.core.ServerStatus).getLocalIp()}]]</td>
<td>[[${T(com.rebuild.utils.OshiUtils).getLocalIp()}]]</td>
</tr>
<tr>
<th>System Time</th>

View file

@ -65,7 +65,7 @@
parent && parent.referenceSearch__dlg && parent.referenceSearch__dlg.resize()
}
$(document).ready(function () {
$('.J_select').click(function () {
$('.J_select').on('click', () => {
const ss = RbListPage._RbList.getSelectedIds()
if (ss.length > 0 && parent && parent.referenceSearch__call) parent.referenceSearch__call(ss)
})

View file

@ -1,15 +0,0 @@
package com.rebuild.core;
import org.junit.jupiter.api.Test;
class ServerStatusTest {
@Test
void oshi() {
double[] vmMemory = ServerStatus.getJvmMemoryUsed();
System.out.println(vmMemory[0] + ", " + vmMemory[1]);
System.out.println(ServerStatus.getSystemLoad());
System.out.println(ServerStatus.getLocalIp());
}
}

View file

@ -11,8 +11,8 @@ import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONArray;
import com.rebuild.TestSupport;
import com.rebuild.core.Application;
import com.rebuild.core.ServerStatus;
import com.rebuild.core.privileges.bizz.Department;
import com.rebuild.utils.OshiUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -44,7 +44,7 @@ public class UserHelperTest extends TestSupport {
public void generateAvatar() throws Exception {
for (int i = 0; i < 100; i++) {
UserHelper.generateAvatar("你好", true);
System.out.println(ServerStatus.getJvmMemoryUsed()[1]);
System.out.println(OshiUtils.getOsMemoryUsed()[1]);
}
}

View file

@ -37,7 +37,7 @@ public class FieldWritebackTest extends TestSupport {
triggerConfig.setInt("when", TriggerWhen.CREATE.getMaskValue());
triggerConfig.setString("actionType", ActionType.FIELDWRITEBACK.name());
// 更新自己新建时将修改时间改为createdOn+1天
String content = "{targetEntity:'$PRIMARY$.Account999', items:[{targetField:'modifiedOn', updateMode:'FORMULA', sourceField:'dateadd(`{createdOn}`, `1D`)' }]}";
String content = "{targetEntity:'$PRIMARY$.Account999', items:[{targetField:'modifiedOn', updateMode:'FORMULA', sourceField:'DATEADD(`{createdOn}`, `1D`)' }]}";
triggerConfig.setString("actionContent", content);
Application.getBean(RobotTriggerConfigService.class).create(triggerConfig);

View file

@ -27,6 +27,6 @@ public class ProtocolFilterParserTest extends TestSupport {
@Test
public void parseRef() {
System.out.println(new ProtocolFilterParser(null)
.parseRef("REFERENCE.TESTALLFIELDS"));
.parseRef("REFERENCE.TESTALLFIELDS", null));
}
}

View file

@ -17,7 +17,7 @@ import org.junit.jupiter.api.Test;
public class BlockListTest {
@Test
public void test() {
void isBlock() {
Assertions.assertTrue(BlockList.isBlock("admin"));
Assertions.assertFalse(BlockList.isBlock("imnotadmin"));
}

View file

@ -0,0 +1,17 @@
package com.rebuild.utils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MarkdownUtilsTest {
@Test
void render() {
System.out.println(MarkdownUtils.render("> content"));
System.out.println(MarkdownUtils.render(
"上海锐昉科技有限公司 [沪ICP备20020345号-3](https://beian.miit.gov.cn/)" +
"<script>alert(1)</script>" +
"<iframe url=\"http://baidu.com\"></iframe>"));
}
}

View file

@ -0,0 +1,28 @@
package com.rebuild.utils;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
class OshiUtilsTest {
@Test
void getSI() {
System.out.println(OshiUtils.getSI());
}
@Test
void getOsMemoryUsed() {
System.out.println(Arrays.toString(OshiUtils.getOsMemoryUsed()));
}
@Test
void getSystemLoad() {
System.out.println(OshiUtils.getSystemLoad());
}
@Test
void getLocalIp() {
System.out.println(OshiUtils.getLocalIp());
}
}

View file

@ -7,18 +7,13 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.utils;
import cn.hutool.system.SystemUtil;
import org.junit.jupiter.api.Test;
import java.util.regex.Pattern;
/**
*/
public class Tests {
@Test
void test() {
System.out.println(SystemUtil.getHostInfo().getAddress());
System.out.println(SystemUtil.getRuntimeInfo());
}
}