Fix 4.1 beta5 (#931)

* Delete old file after report update

* Refactor form element entity assignment logic

* style

* pwa

* lang

* themeColor

* Update privilege checks in TransformManager

* Update @rbv

* fix: readonly state

* beta5

* be

* Update Entity2Schema.java

* be: file access

* Update AdvFilterParser.java

* fix:添加明细

* Enhance reference data filter to support view data

* Update chart-design.js

* Refactor multi-record report generation to use ReportsFile

* Update EasyExcelGenerator33.java

* Refactor ReportsFile and update report generation logic

* Update @rbv
This commit is contained in:
REBUILD 企业管理系统 2025-07-19 17:09:52 +08:00 committed by GitHub
parent ccf11033e5
commit be10888e5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 560 additions and 238 deletions

2
@rbv

@ -1 +1 @@
Subproject commit f5eb69d2eb613d5fc8bb353996aa795b9905fb8a
Subproject commit 86858da73f2a983ff14bd02af205256d4dbd366d

View file

@ -10,7 +10,7 @@
</parent>
<groupId>com.rebuild</groupId>
<artifactId>rebuild</artifactId>
<version>4.1.0-beta4</version>
<version>4.1.0-beta5</version>
<name>rebuild</name>
<description>Building your business-systems freely!</description>
<url>https://getrebuild.com/</url>

View file

@ -76,11 +76,11 @@ public class Application implements ApplicationListener<ApplicationStartedEvent>
/**
* Rebuild Version
*/
public static final String VER = "4.1.0-beta4";
public static final String VER = "4.1.0-beta5";
/**
* Rebuild Build [MAJOR]{1}[MINOR]{2}[PATCH]{2}[BUILD]{2}
*/
public static final int BUILD = 4010004;
public static final int BUILD = 4010005;
static {
// Driver for DB

View file

@ -395,19 +395,19 @@ public class FormsBuilder extends FormsManager {
// Check and clean
for (Iterator<Object> iter = elements.iterator(); iter.hasNext(); ) {
JSONObject el = (JSONObject) iter.next();
String fieldName = el.getString("field");
JSONObject field = (JSONObject) iter.next();
String fieldName = field.getString("field");
if (DIVIDER_LINE.equalsIgnoreCase(fieldName)) continue;
if (REFFORM_LINE.equalsIgnoreCase(fieldName)) {
// v3.6
if (viewModel && recordData != null) {
String reffield = el.getString("reffield");
String reffield = field.getString("reffield");
Object v = recordData.getObjectValue(reffield);
if (v == null && entity.containsField(reffield)) {
v = Application.getQueryFactory().unique(recordData.getPrimary(), reffield)[0];
}
if (v != null) {
el.put("refvalue", new Object[]{ v, entity.getField(reffield).getReferenceEntity().getName() });
field.put("refvalue", new Object[]{ v, entity.getField(reffield).getReferenceEntity().getName() });
}
}
continue;
@ -422,18 +422,18 @@ public class FormsBuilder extends FormsManager {
// v2.2 高级控制
// v3.8.4 视图下也有效单字段编辑也算编辑
if (useAdvControl) {
Object hiddenOnCreate = el.remove("hiddenOnCreate");
Object hiddenOnUpdate = el.remove("hiddenOnUpdate");
Object hiddenOnCreate = field.remove("hiddenOnCreate");
Object hiddenOnUpdate = field.remove("hiddenOnUpdate");
if (hiddenOnCreate == null) {
Object displayOnCreate39 = el.remove("displayOnCreate");
Object displayOnUpdate39 = el.remove("displayOnUpdate");
Object displayOnCreate39 = field.remove("displayOnCreate");
Object displayOnUpdate39 = field.remove("displayOnUpdate");
if (displayOnCreate39 != null && !(Boolean) displayOnCreate39) hiddenOnCreate = true;
if (displayOnUpdate39 != null && !(Boolean) displayOnUpdate39) hiddenOnUpdate = true;
}
final Object requiredOnCreate = el.remove("requiredOnCreate");
final Object requiredOnUpdate = el.remove("requiredOnUpdate");
final Object readonlyOnCreate = el.remove("readonlyOnCreate");
final Object readonlyOnUpdate = el.remove("readonlyOnUpdate");
final Object requiredOnCreate = field.remove("requiredOnCreate");
final Object requiredOnUpdate = field.remove("requiredOnUpdate");
final Object readonlyOnCreate = field.remove("readonlyOnCreate");
final Object readonlyOnUpdate = field.remove("readonlyOnUpdate");
// fix v3.3.4 跟随主记录新建/更新
boolean isNewState = isNew;
if (entity.getMainEntity() != null) {
@ -457,108 +457,109 @@ public class FormsBuilder extends FormsManager {
}
// 必填
if (requiredOnCreate != null && (Boolean) requiredOnCreate && isNewState) {
el.put("nullable", false);
field.put("nullable", false);
}
if (requiredOnUpdate != null && (Boolean) requiredOnUpdate && !isNewState) {
el.put("nullable", false);
field.put("nullable", false);
}
// 只读 v3.6
if (readonlyOnCreate != null && (Boolean) readonlyOnCreate && isNewState) {
el.put("readonly", true);
field.put("readonly", true);
}
if (readonlyOnUpdate != null && (Boolean) readonlyOnUpdate && !isNewState) {
el.put("readonly", true);
field.put("readonly", true);
}
}
// 自动只读的
final boolean roViaAuto = el.getBooleanValue("readonly");
final boolean roViaAuto = field.getBooleanValue("readonly");
final Field fieldMeta = entity.getField(fieldName);
final EasyField easyField = EasyMetaFactory.valueOf(fieldMeta);
final DisplayType dt = easyField.getDisplayType();
el.put("label", easyField.getLabel());
el.put("type", dt.name());
el.put("readonly", (!isNew && !fieldMeta.isUpdatable()) || roViaAuto);
field.put("label", easyField.getLabel());
field.put("type", dt.name());
field.put("readonly", (!isNew && !fieldMeta.isUpdatable()) || roViaAuto);
field.put("entity", entity.getName());
// 优先使用指定值
final Boolean nullable = el.getBoolean("nullable");
final Boolean nullable = field.getBoolean("nullable");
if (nullable != null) {
el.put("nullable", nullable);
field.put("nullable", nullable);
} else {
el.put("nullable", fieldMeta.isNullable());
field.put("nullable", fieldMeta.isNullable());
}
// 字段扩展配置 FieldExtConfigProps
JSONObject fieldExtAttrs = easyField.getExtraAttrs(true);
el.putAll(fieldExtAttrs);
field.putAll(fieldExtAttrs);
// 不同字段类型的处理
if (dt == DisplayType.PICKLIST) {
JSONArray options = PickListManager.instance.getPickList(fieldMeta);
el.put("options", options);
field.put("options", options);
} else if (dt == DisplayType.STATE) {
JSONArray options = StateManager.instance.getStateOptions(fieldMeta);
el.put("options", options);
el.remove(EasyFieldConfigProps.STATE_CLASS);
field.put("options", options);
field.remove(EasyFieldConfigProps.STATE_CLASS);
} else if (dt == DisplayType.MULTISELECT) {
JSONArray options = MultiSelectManager.instance.getSelectList(fieldMeta);
el.put("options", options);
field.put("options", options);
} else if (dt == DisplayType.TAG) {
el.put("options", ObjectUtils.defaultIfNull(el.remove("tagList"), JSONUtils.EMPTY_ARRAY));
field.put("options", ObjectUtils.defaultIfNull(field.remove("tagList"), JSONUtils.EMPTY_ARRAY));
} else if (dt == DisplayType.DATETIME) {
String format = StringUtils.defaultIfBlank(
easyField.getExtraAttr(EasyFieldConfigProps.DATETIME_FORMAT), dt.getDefaultFormat());
el.put(EasyFieldConfigProps.DATETIME_FORMAT, format);
field.put(EasyFieldConfigProps.DATETIME_FORMAT, format);
} else if (dt == DisplayType.DATE) {
String format = StringUtils.defaultIfBlank(
easyField.getExtraAttr(EasyFieldConfigProps.DATE_FORMAT), dt.getDefaultFormat());
el.put(EasyFieldConfigProps.DATE_FORMAT, format);
field.put(EasyFieldConfigProps.DATE_FORMAT, format);
} else if (dt == DisplayType.TIME) {
String format = StringUtils.defaultIfBlank(
easyField.getExtraAttr(EasyFieldConfigProps.TIME_FORMAT), dt.getDefaultFormat());
el.put(EasyFieldConfigProps.TIME_FORMAT, format);
field.put(EasyFieldConfigProps.TIME_FORMAT, format);
} else if (dt == DisplayType.CLASSIFICATION) {
el.put("openLevel", ClassificationManager.instance.getOpenLevel(fieldMeta));
field.put("openLevel", ClassificationManager.instance.getOpenLevel(fieldMeta));
} else if (dt == DisplayType.REFERENCE || dt == DisplayType.N2NREFERENCE) {
Entity refEntity = fieldMeta.getReferenceEntity();
boolean quickNew = el.getBooleanValue(EasyFieldConfigProps.REFERENCE_QUICKNEW);
boolean quickNew = field.getBooleanValue(EasyFieldConfigProps.REFERENCE_QUICKNEW);
if (quickNew) {
el.put(EasyFieldConfigProps.REFERENCE_QUICKNEW,
field.put(EasyFieldConfigProps.REFERENCE_QUICKNEW,
Application.getPrivilegesManager().allowCreate(user, refEntity.getEntityCode()));
el.put("referenceEntity", EasyMetaFactory.toJSON(refEntity));
field.put("referenceEntity", EasyMetaFactory.toJSON(refEntity));
}
if (dt == DisplayType.REFERENCE && License.isRbvAttached()) {
el.put("fillinWithFormData", true);
field.put("fillinWithFormData", true);
}
}
// 新建记录
if (isNew) {
if (!fieldMeta.isCreatable()) {
el.put("readonly", true);
field.put("readonly", true);
switch (fieldName) {
case EntityHelper.CreatedOn:
case EntityHelper.ModifiedOn:
el.put("value", CalendarUtils.getUTCDateTimeFormat().format(now));
field.put("value", CalendarUtils.getUTCDateTimeFormat().format(now));
break;
case EntityHelper.CreatedBy:
case EntityHelper.ModifiedBy:
case EntityHelper.OwningUser:
el.put("value", FieldValueHelper.wrapMixValue(formUser.getId(), formUser.getFullName()));
field.put("value", FieldValueHelper.wrapMixValue(formUser.getId(), formUser.getFullName()));
break;
case EntityHelper.OwningDept:
Department dept = formUser.getOwningDept();
Assert.notNull(dept, "Department of user is unset : " + formUser.getId());
el.put("value", FieldValueHelper.wrapMixValue((ID) dept.getIdentity(), dept.getName()));
field.put("value", FieldValueHelper.wrapMixValue((ID) dept.getIdentity(), dept.getName()));
break;
case EntityHelper.ApprovalId:
el.put("value", FieldValueHelper.wrapMixValue(null, Language.L("未提交")));
field.put("value", FieldValueHelper.wrapMixValue(null, Language.L("未提交")));
break;
case EntityHelper.ApprovalState:
el.put("value", ApprovalState.DRAFT.getState());
field.put("value", ApprovalState.DRAFT.getState());
break;
default:
break;
@ -566,12 +567,12 @@ public class FormsBuilder extends FormsManager {
}
// 默认值
if (el.get("value") == null) {
if (field.get("value") == null) {
if (dt == DisplayType.SERIES
|| EntityHelper.ApprovalLastTime.equals(fieldName) || EntityHelper.ApprovalLastRemark.equals(fieldName)
|| EntityHelper.ApprovalLastUser.equals(fieldName) || EntityHelper.ApprovalStepUsers.equals(fieldName)
|| EntityHelper.ApprovalStepNodeName.equals(fieldName)) {
el.put("readonlyw", READONLYW_RO);
field.put("readonlyw", READONLYW_RO);
} else {
Object defaultValue = easyField.exprDefaultValue();
if (defaultValue != null) {
@ -580,13 +581,13 @@ public class FormsBuilder extends FormsManager {
if (dt == DisplayType.DECIMAL || dt == DisplayType.NUMBER) {
defaultValue = EasyDecimal.clearFlaged(defaultValue);
}
el.put("value", defaultValue);
field.put("value", defaultValue);
}
}
}
// 自动值
if (roViaAuto && el.get("value") == null) {
if (roViaAuto && field.get("value") == null) {
if (dt == DisplayType.EMAIL
|| dt == DisplayType.PHONE
|| dt == DisplayType.URL
@ -597,8 +598,8 @@ public class FormsBuilder extends FormsManager {
|| dt == DisplayType.SERIES
|| dt == DisplayType.TEXT
|| dt == DisplayType.NTEXT) {
Integer s = el.getInteger("readonlyw");
if (s == null) el.put("readonlyw", READONLYW_RO);
Integer s = field.getInteger("readonlyw");
if (s == null) field.put("readonlyw", READONLYW_RO);
}
}
@ -608,7 +609,7 @@ public class FormsBuilder extends FormsManager {
ID parentValue = EntityHelper.isUnsavedId(mainid) ? null
: getCascadingFieldParentValue(easyField, mainid, true);
if (parentValue != null) {
el.put("_cascadingFieldParentValue", parentValue);
field.put("_cascadingFieldParentValue", parentValue);
}
}
}
@ -621,40 +622,40 @@ public class FormsBuilder extends FormsManager {
if (!viewModel && (dt == DisplayType.DECIMAL || dt == DisplayType.NUMBER)) {
value = EasyDecimal.clearFlaged(value);
}
el.put("value", value);
field.put("value", value);
}
// 父级级联
if ((dt == DisplayType.REFERENCE || dt == DisplayType.N2NREFERENCE) && recordData.getPrimary() != null) {
ID parentValue = getCascadingFieldParentValue(easyField, recordData.getPrimary(), false);
if (parentValue != null) {
el.put("_cascadingFieldParentValue", parentValue);
field.put("_cascadingFieldParentValue", parentValue);
}
}
}
// Clean
el.remove(EasyFieldConfigProps.ADV_PATTERN);
el.remove(EasyFieldConfigProps.ADV_DESENSITIZED);
el.remove("barcodeFormat");
el.remove("seriesFormat");
field.remove(EasyFieldConfigProps.ADV_PATTERN);
field.remove(EasyFieldConfigProps.ADV_DESENSITIZED);
field.remove("barcodeFormat");
field.remove("seriesFormat");
String decimalType = el.getString("decimalType");
String decimalType = field.getString("decimalType");
if (decimalType != null && decimalType.contains("%s")) {
el.put("decimalType", decimalType.replace("%s", ""));
field.put("decimalType", decimalType.replace("%s", ""));
}
// v3.8 字段权限
if (isNew) {
if (!fp.isCreatable(fieldMeta, user)) el.put("readonly", true);
if (!fp.isCreatable(fieldMeta, user)) field.put("readonly", true);
} else {
// v4.0 保留占位
if (!fp.isReadable(fieldMeta, user)) {
el.put("unreadable", true);
el.put("readonly", true);
el.remove("value");
field.put("unreadable", true);
field.put("readonly", true);
field.remove("value");
}
else if (!fp.isUpdatable(fieldMeta, user)) el.put("readonly", true);
else if (!fp.isUpdatable(fieldMeta, user)) field.put("readonly", true);
}
}
}

View file

@ -43,7 +43,8 @@ public class TransformManager implements ConfigManager {
// 任何修改都会清空
private static final Map<Object, Object> WEAK_CACHED = new ConcurrentHashMap<>();
private TransformManager() { }
private TransformManager() {
}
/**
* 前端使用
@ -65,12 +66,15 @@ public class TransformManager implements ConfigManager {
String target = cb.getString("target");
Entity targetEntity = MetadataHelper.getEntity(target);
// 普通或主实体
if (targetEntity.getMainEntity() == null) {
if (!Application.getPrivilegesManager().allowCreate(user, targetEntity.getEntityCode())) {
// 允许创建或更新
if (!Application.getPrivilegesManager().allowCreate(user, targetEntity.getEntityCode())
&& !Application.getPrivilegesManager().allowUpdate(user, targetEntity.getEntityCode())) {
continue;
}
} else {
// To 明细
// 明细实体-允许更新主记录
if (!Application.getPrivilegesManager().allowUpdate(user, targetEntity.getMainEntity().getEntityCode())) {
continue;
}

View file

@ -70,7 +70,7 @@ public class Entity2Schema extends Field2Schema {
* @return Returns 实体名称
*/
public String createEntity(String entityName, String entityLabel, String comments, String mainEntity, boolean haveNameField, boolean haveSeriesField) {
if (!License.isRbvAttached() && MetadataHelper.getEntities().length >= 120) {
if (!License.isRbvAttached() && MetadataHelper.getEntities().length >= 150) {
throw new NeedRbvException(Language.L("实体数量超出免费版限制"));
}

View file

@ -93,7 +93,7 @@ public class EasyExcelGenerator extends SetUser {
protected Integer writeSheetAt = null;
protected ID recordId;
@Setter
private ID reportId;
protected ID reportId;
protected int phNumber = 1;
protected Map<String, Object> phValues = new HashMap<>();

View file

@ -67,6 +67,9 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
// 支持多记录导出会合并到一个 Excel 文件
final private List<ID> recordIdMulti;
// 默认合并到一个文件也可以打包成一个 zip
@Setter
private boolean recordIdMultiMerge2Sheets = true;
private Set<String> inShapeVars;
private Map<String, Object> recordMainHolder;
@ -284,8 +287,28 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
public File generate() {
if (recordIdMulti == null) return superGenerate();
// init
File targetFile = super.getTargetFile();
// v4.1-b5
if (!recordIdMultiMerge2Sheets) {
ReportsFile reportsFile = new ReportsFile();
for (ID recordId : this.recordIdMulti) {
this.recordId = recordId;
this.phNumber = 1;
this.phValues.clear();
String reportName = DataReportManager.getPrettyReportName(reportId, recordId, templateFile.getName());
try {
reportsFile.addFile(superGenerate(), reportName);
} catch (IOException e) {
throw new ReportsException(e);
}
}
return reportsFile;
}
// init
try {
FileUtils.copyFile(templateFile, targetFile);
} catch (IOException e) {
@ -299,7 +322,7 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
// 1.复制模板
Sheet newSheet = wb.cloneSheet(0);
newSheetAt = wb.getSheetIndex(newSheet);
String newSheetName = "A" + newSheetAt;
String newSheetName = "" + newSheetAt;
try {
wb.setSheetName(newSheetAt, newSheetName);
} catch (IllegalArgumentException ignored) {

View file

@ -0,0 +1,89 @@
/*!
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.datareport;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.utils.CompressUtils;
import com.rebuild.utils.PdfConverter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* 导出报表打包
*
* @author devezhao
* @since 2025/7/19
*/
public class ReportsFile extends File {
private static final long serialVersionUID = -8876458376733911086L;
private List<File> files = new ArrayList<>();
public ReportsFile(File parent, String fileName) {
super(ObjectUtils.defaultIfNull(parent, RebuildConfiguration.getFileOfTemp("/")),
StringUtils.defaultIfBlank(fileName, "RBREPORT-" + System.currentTimeMillis()));
}
public ReportsFile() {
this(null, null);
}
public ReportsFile addFile(File file, String reportName) throws IOException {
if (!this.exists()) FileUtils.forceMkdir(this);
if (reportName == null) reportName = file.getName();
String fileName = (files.size() + 1) + "-" + reportName;
File dest = new File(this, fileName);
FileUtils.moveFile(file, dest);
files.add(dest);
return this;
}
public File[] getFiles() {
return files.toArray(new File[0]);
}
public File toZip(boolean makePdf) throws IOException {
return toZip(makePdf, false);
}
public File toZip(boolean makePdf, boolean keepOrigin) throws IOException {
FileFilter filter = null;
if (makePdf) {
for (File file : files) {
File pdfFile = convertPdf(file);
FileUtils.copyFile(pdfFile, new File(this, pdfFile.getName()));
}
filter = file -> file.getName().endsWith(".pdf");
if (!keepOrigin) filter = null;
}
File zipFile = RebuildConfiguration.getFileOfTemp(this.getName() + ".zip");
try {
CompressUtils.forceZip(zipFile, this, filter);
return zipFile;
} catch (IOException ex) {
throw new ReportsException("Cannot make zip for reports", ex);
}
}
public File convertPdf(File file) {
Path pdf = PdfConverter.convert(file.toPath(), PdfConverter.TYPE_PDF);
return pdf.toFile();
}
}

View file

@ -194,7 +194,7 @@ public class FilesHelper {
* @return
*/
public static boolean isFileAccessable(ID user, ID fileId) {
Object[] o = Application.getQueryFactory().uniqueNoFilter(fileId, "folderId");
Object[] o = Application.getQueryFactory().uniqueNoFilter(fileId, "inFolder");
if (o == null) return true;
return getAccessableFolders(user).contains((ID) o[0]);
}

View file

@ -20,7 +20,6 @@ import cn.devezhao.persist4j.query.compiler.QueryCompiler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.UserContextHolder;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;

View file

@ -261,7 +261,11 @@ public class RobotTriggerObserver extends OperatingObserver {
if (ex instanceof DataValidateException) throw ex;
// throw of Aviator 抛出
//noinspection ConstantValue
if (ex instanceof StandardError) throw new DataValidateException(ex.getLocalizedMessage());
if (ex instanceof StandardError) {
String exMsg = StringUtils.defaultIfBlank(ex.getLocalizedMessage(), ex.getMessage());
if (StringUtils.isBlank(exMsg)) exMsg = Language.L("系统繁忙,请稍后重试") + " (StandardError)";
throw new DataValidateException(exMsg);
}
}
log.error("Trigger execution failed : {} << {}", action, context, ex);

View file

@ -11,6 +11,7 @@ import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.CodecUtils;
import cn.hutool.core.io.FileUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Client;
@ -28,6 +29,7 @@ import com.rebuild.core.cache.CommonsCache;
import com.rebuild.core.support.ConfigurationItem;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.utils.JSONUtils;
import com.rebuild.utils.OkHttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
@ -235,7 +237,8 @@ public class QiniuCloud {
resp = bucketManager.delete(this.bucketName, key);
if (resp.isOK()) return true;
throw new RebuildException("Failed to delete file : " + this.bucketName + " < " + key + " : " + resp.bodyString());
log.warn("Cannot delete file : {} < {} : {}", this.bucketName, key, resp.bodyString());
return false;
} catch (QiniuException e) {
throw new RebuildException("Failed to delete file : " + this.bucketName + " < " + key, e);
}
@ -497,4 +500,36 @@ public class QiniuCloud {
}
return fileKey;
}
/**
* 删除文件
*
* @param filesValue
* @return
*/
public static int deleteFiles(String filesValue) {
if (StringUtils.isBlank(filesValue)) return 0;
if (!JSONUtils.wellFormat(filesValue)) {
if (filesValue.startsWith("rb/")) {
filesValue = "[\"" + filesValue + "\"]";
} else {
return 0;
}
}
int del = 0;
JSONArray fileKeys = JSON.parseArray(filesValue);
for (Object fileKey : fileKeys) {
if (QiniuCloud.instance().available()) {
del += QiniuCloud.instance().delete(fileKey.toString()) ? 1 : 0;
} else {
File file = RebuildConfiguration.getFileOfData(fileKey.toString());
if (file.exists()) {
del += file.delete() ? 1 : 0;
}
}
}
return del;
}
}

View file

@ -28,6 +28,7 @@ import com.rebuild.core.support.setup.InstallState;
import com.rebuild.utils.AppUtils;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.web.admin.ProtectedAdmin;
import com.rebuild.web.user.signup.LoginController;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
@ -43,6 +44,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.rebuild.web.commons.UseThemeController.THEMES_COLORS;
/**
* 请求拦截
* - 检查授权
@ -94,6 +97,12 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
// Lang
request.setAttribute(WebConstants.LOCALE, requestEntry.getLocale());
request.setAttribute(WebConstants.$BUNDLE, Application.getLanguage().getBundle(requestEntry.getLocale()));
// v4.1 theme
String theme = (String) ServletUtils.getSessionAttribute(request, LoginController.SK_USER_THEME);
if (theme != null) {
theme = THEMES_COLORS.get(theme);
if (theme != null) request.setAttribute(WebConstants.THEME_COLOR, theme);
}
final String requestUri = requestEntry.getRequestUri();

View file

@ -67,6 +67,11 @@ public class WebConstants {
*/
public static final String LOCALE = "locale";
/**
* v4.1 主题色
*/
public static final String THEME_COLOR = "themeColor";
/**
* CSRF-Token
* @see com.rebuild.api.user.AuthTokenManager#TYPE_CSRF_TOKEN

View file

@ -85,14 +85,14 @@ public class ProtectedAdmin {
public enum PaEntry {
SYS("/systems", "通用配置"),
SSI("/integration/", "服务集成"),
API("/apis-manager", "API 秘钥"),
API("/apis-manager", "OpenAPI 秘钥"),
AIB("/integration/aibot", "AI 助手"),
ENT("/entities;/entity/;/metadata/", "实体管理"),
APR("/robot/approval", "审批流程"),
TRA("/robot/transform", "记录转换"),
TRI("/robot/trigger", "触发器"),
SOP("/robot/sop", "业务进度"),
REP("/data/report-template", "报表设计"),
REP("/data/report-template", "报表模版"),
IMP("/data/data-imports", "数据导入"),
EXF("/extform", "外部表单"),
PRO("/project", "项目"),

View file

@ -35,6 +35,8 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
/**
* @author devezhao
@ -49,6 +51,15 @@ public class UseThemeController extends BaseController {
public static final String[] THEMES = {
"default", "dark", "red", "green", "blue", "blue2", "purple"
};
public static final Map<String, String> THEMES_COLORS = new HashMap<>();
static {
THEMES_COLORS.put("dark", "#2d333b");
THEMES_COLORS.put("red", "#f7615e");
THEMES_COLORS.put("green", "#16a88f");
THEMES_COLORS.put("blue", "#4873c0");
THEMES_COLORS.put("blue2", "#38adff");
THEMES_COLORS.put("purple", "#9b52de");
}
@GetMapping("use-theme")
public void useTheme(HttpServletRequest request, HttpServletResponse response) throws IOException {

View file

@ -25,6 +25,7 @@ import com.rebuild.core.service.files.FilesHelper;
import com.rebuild.core.service.project.ProjectManager;
import com.rebuild.core.support.i18n.I18nUtils;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.integration.QiniuCloud;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
@ -65,7 +66,7 @@ public class FileListController extends BaseController {
path = ServletUtils.readCookie(request, CK_LASTPATH);
path = "attachment".equals(path) ? path : "docs";
}
// 记住最后一次访问的文件类型
ServletUtils.addCookie(response, CK_LASTPATH, path);
@ -168,7 +169,7 @@ public class FileListController extends BaseController {
sqlWhere.add(String.format("relatedRecord = '%s'", related));
}
String sql = "select attachmentId,filePath,fileType,fileSize,createdBy,modifiedOn,inFolder,relatedRecord from Attachment where (1=1) and (isDeleted = ?)";
String sql = "select attachmentId,filePath,fileType,fileSize,createdBy,modifiedOn,inFolder,relatedRecord,fileName from Attachment where (1=1) and (isDeleted = ?)";
sql = sql.replace("(1=1)", StringUtils.join(sqlWhere.iterator(), " and "));
if ("size".equals(sort)) {
sql += " order by fileSize desc";
@ -188,7 +189,9 @@ public class FileListController extends BaseController {
for (Object[] o : array) {
JSONObject item = new JSONObject();
item.put("id", o[0]);
item.put("filePath", o[1]);
String fileName = (String) o[8];
if (fileName == null) fileName = QiniuCloud.parseFileName((String) o[1]);
item.put("fileName", fileName);
item.put("fileType", o[2]);
item.put("fileSize", FileUtils.byteCountToDisplaySize(ObjectUtils.toLong(o[3])));
item.put("uploadBy", new Object[]{o[4], UserHelper.getName((ID) o[4])});

View file

@ -7,6 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.web.files;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
@ -14,6 +15,7 @@ import com.alibaba.fastjson.JSONArray;
import com.rebuild.api.RespBody;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.service.feeds.FeedsHelper;
import com.rebuild.core.service.files.BatchDownload;
import com.rebuild.core.service.files.FilesHelper;
@ -76,7 +78,7 @@ public class FileManagerController extends BaseController {
final ID user = getRequestUser(request);
String[] files = getParameter(request, "ids", "").split(",");
Set<ID> willDeletes = new HashSet<>();
Set<ID> willDeleteIds = new HashSet<>();
for (String file : files) {
if (!ID.isId(file)) continue;
@ -84,11 +86,10 @@ public class FileManagerController extends BaseController {
if (!FilesHelper.isFileManageable(user, fileId)) {
return RespBody.errorl("无权删除他人文件");
}
willDeletes.add(fileId);
willDeleteIds.add(fileId);
}
Application.getCommonsService().delete(willDeletes.toArray(new ID[0]));
Application.getCommonsService().delete(willDeleteIds.toArray(new ID[0]));
return RespBody.ok();
}
@ -108,11 +109,8 @@ public class FileManagerController extends BaseController {
}
Record r = EntityHelper.forUpdate(fileId, user);
if (inFolder == null) {
r.setNull("inFolder");
} else {
r.setID("inFolder", inFolder);
}
if (inFolder == null) r.setNull("inFolder");
else r.setID("inFolder", inFolder);
fileRecords.add(r);
}
@ -120,38 +118,48 @@ public class FileManagerController extends BaseController {
return RespBody.ok();
}
// TODO 更严格的文件访问权限检查
@RequestMapping("check-readable")
public RespBody checkReadable(@IdParam ID recordOrFileId, HttpServletRequest request) {
final ID user = getRequestUser(request);
final int entityCode = recordOrFileId.getEntityCode();
public RespBody checkReadable(@IdParam ID fileId, HttpServletRequest request) {
String filePath = checkFileReadable(fileId, getRequestUser(request));
return filePath == null ? RespBody.error() : RespBody.ok(filePath);
}
// 是否可读取文件
static String checkFileReadable(ID fileId, ID user) {
Object[] file = Application.getQueryFactory().uniqueNoFilter(fileId, "filePath,relatedRecord,belongEntity");
if (file == null) return null;
if (UserHelper.isAdmin(user)) return (String) file[0];
boolean readable;
// 文件
if (entityCode == EntityHelper.Attachment) {
readable = FilesHelper.isFileAccessable(user, recordOrFileId);
} else {
// 附件
if (entityCode == EntityHelper.Feeds || entityCode == EntityHelper.FeedsComment) {
readable = FeedsHelper.checkReadable(recordOrFileId, user);
} else if (entityCode == EntityHelper.ProjectTask || entityCode == EntityHelper.ProjectTaskComment) {
readable = ProjectHelper.checkReadable(recordOrFileId, user);
} else {
readable = Application.getPrivilegesManager().allowRead(user, recordOrFileId);
}
if ((int) file[2] <= 0) {
if (FilesHelper.isFileAccessable(user, fileId)) return (String) file[0];
else return null;
}
return RespBody.ok(readable);
// 附件
final ID recordId = (ID) file[1];
if (recordId == null) return null;
int entityCode = recordId.getEntityCode();
boolean readable;
if (entityCode == EntityHelper.Feeds || entityCode == EntityHelper.FeedsComment) {
readable = FeedsHelper.checkReadable(recordId, user);
} else if (entityCode == EntityHelper.ProjectTask || entityCode == EntityHelper.ProjectTaskComment) {
readable = ProjectHelper.checkReadable(recordId, user);
} else {
readable = Application.getPrivilegesManager().allowRead(user, recordId);
}
return readable ? (String) file[0] : null;
}
@PostMapping("batch-download")
public void batchDownload(HttpServletRequest req, HttpServletResponse resp) throws IOException {
public void downloadBatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
final String files = req.getParameter("files");
List<String> filePaths = new ArrayList<>();
Collections.addAll(filePaths, files.split(","));
List<String> filesList = new ArrayList<>();
Collections.addAll(filesList, files.split(","));
BatchDownload bd = new BatchDownload(filePaths);
BatchDownload bd = new BatchDownload(filesList);
TaskExecutors.run(bd);
File zipName = bd.getDestZip();
@ -162,6 +170,20 @@ public class FileManagerController extends BaseController {
}
}
@RequestMapping("download")
public void download(@IdParam ID fileId, HttpServletRequest req, HttpServletResponse resp) throws IOException {
String filePath = checkFileReadable(fileId, getRequestUser(req));
if (filePath == null) {
resp.sendError(HttpStatus.FORBIDDEN.value(), Language.L("你没有查看此文件的权限"));
} else {
String fileUrl = CodecUtils.urlEncode(filePath);
fileUrl = fileUrl.replace("%2F", "/");
fileUrl = String.format("../filex/download/%s?attname=%s",
fileUrl, CodecUtils.urlEncode(QiniuCloud.parseFileName(filePath)));
resp.sendRedirect(fileUrl);
}
}
@PostMapping("file-edit")
public RespBody fileEdit(HttpServletRequest req) throws IOException {
final ID user = getRequestUser(req);

View file

@ -177,6 +177,12 @@ public class CommonOperatingController extends BaseController {
return StringUtils.join(fs, ",");
}
@RequestMapping("check-readable")
public RespBody checkReadable(@IdParam ID recordId, HttpServletRequest request) {
boolean r = Application.getPrivilegesManager().allowRead(getRequestUser(request), recordId);
return RespBody.ok(r);
}
/**
* 保存记录
*

View file

@ -28,6 +28,7 @@ import com.rebuild.core.service.dataimport.DataExporter;
import com.rebuild.core.service.datareport.DataReportManager;
import com.rebuild.core.service.datareport.EasyExcelGenerator;
import com.rebuild.core.service.datareport.EasyExcelGenerator33;
import com.rebuild.core.service.datareport.ReportsFile;
import com.rebuild.core.service.datareport.TemplateFile;
import com.rebuild.core.support.CommonsLog;
import com.rebuild.core.support.KVStorage;
@ -95,6 +96,10 @@ public class ReportsController extends BaseController {
final ID recordId = recordIds[0];
final TemplateFile tt = DataReportManager.instance.buildTemplateFile(reportId);
String typeOutput = getParameter(request, "output");
boolean isHtml = "HTML".equalsIgnoreCase(typeOutput);
boolean isPdf = "PDF".equalsIgnoreCase(typeOutput);
File output = null;
try {
EasyExcelGenerator reportGenerator;
@ -109,20 +114,23 @@ public class ReportsController extends BaseController {
reportGenerator = EasyExcelGenerator.create(reportId, Arrays.asList(recordIds));
}
if (reportGenerator != null) {
// vars in URL
String vars = getParameter(request, "vars");
if (JSONUtils.wellFormat(vars) && reportGenerator instanceof EasyExcelGenerator33) {
JSONObject varsJson = JSON.parseObject(vars);
if (varsJson != null) {
((EasyExcelGenerator33) reportGenerator).setTempVars(varsJson.getInnerMap());
}
// vars in URL
String vars = getParameter(request, "vars");
if (JSONUtils.wellFormat(vars) && reportGenerator instanceof EasyExcelGenerator33) {
JSONObject varsJson = JSON.parseObject(vars);
if (varsJson != null) {
((EasyExcelGenerator33) reportGenerator).setTempVars(varsJson.getInnerMap());
}
reportGenerator.setReportId(reportId);
output = reportGenerator.generate();
}
// 4.1-b5 压缩包
if (isPdf && recordIds.length > 1 && reportGenerator instanceof EasyExcelGenerator33) {
((EasyExcelGenerator33) reportGenerator).setRecordIdMultiMerge2Sheets(false);
}
reportGenerator.setReportId(reportId);
output = reportGenerator.generate();
CommonsLog.createLog(CommonsLog.TYPE_REPORT,
getRequestUser(request), reportId, StringUtils.join(recordIds, ";"));
// PH__EXPORTTIMES
@ -138,9 +146,10 @@ public class ReportsController extends BaseController {
RbAssert.is(output != null, Language.L("无法输出报表,请检查报表模板是否有误"));
String typeOutput = getParameter(request, "output");
boolean isHtml = "HTML".equalsIgnoreCase(typeOutput);
boolean isPdf = "PDF".equalsIgnoreCase(typeOutput);
if (output instanceof ReportsFile) {
output = ((ReportsFile) output).toZip(isPdf);
}
// 请求预览
boolean forcePreview = isHtml || getBoolParameter(request, "preview");
String fileName = DataReportManager.getPrettyReportName(reportId, recordId, output.getName());
@ -170,8 +179,7 @@ public class ReportsController extends BaseController {
writeSuccess(response, data);
} else if ("preview".equalsIgnoreCase(typeOutput)) {
String fileUrl = String.format(
"/filex/download/%s?temp=yes&_onceToken=%s&attname=%s",
String fileUrl = String.format("/filex/download/%s?temp=yes&_onceToken=%s&attname=%s",
CodecUtils.urlEncode(output.getName()), AuthTokenManager.generateOnceToken(null), CodecUtils.urlEncode(fileName));
fileUrl = RebuildConfiguration.getHomeUrl(fileUrl);
@ -187,10 +195,10 @@ public class ReportsController extends BaseController {
}
return null;
}
// 列表数据导出
@RequestMapping({ "export/submit", "report/export-list" })
@RequestMapping({"export/submit", "report/export-list"})
public RespBody export(@PathVariable String entity, HttpServletRequest request) {
final ID user = getRequestUser(request);
RbAssert.isAllow(
@ -222,7 +230,7 @@ public class ReportsController extends BaseController {
}
RbAssert.is(output != null, Language.L("无法输出报表,请检查报表模板是否有误"));
String fileName;
if (useReport == null) {
fileName = String.format("%s-%s.%s",
@ -237,7 +245,7 @@ public class ReportsController extends BaseController {
String.format("%s:%d", entity, exporter.getExportCount()));
JSONObject data = JSONUtils.toJSONObject(
new String[] { "fileKey", "fileName" }, new Object[] { output.getName(), fileName });
new String[]{"fileKey", "fileName"}, new Object[]{output.getName(), fileName});
if (AppUtils.isMobile(request)) putFileUrl(data);
return RespBody.ok(data);

View file

@ -3488,5 +3488,25 @@
"AI 助手会话附加数据":"AI 助手会话附加数据",
"AI 助手":"AI 助手",
"直接转换":"直接转换",
"(必填)":"(必填)"
"(必填)":"(必填)",
"隐藏明细":"隐藏明细",
"修改文件":"修改文件",
"指定明细实体布局":"指定明细实体布局",
"由于%s此触发器不会执行":"由于%s此触发器不会执行",
"无任何触发动作":"无任何触发动作",
"提交后提示":"提交后提示",
"清空配置":"清空配置",
"无 (全部权限)":"无 (全部权限)",
"无法修改外部文件":"无法修改外部文件",
"在线编辑":"在线编辑",
"显示明细":"显示明细",
"放在桌面":"放在桌面",
"API 密钥":"API 密钥",
"脱敏读取字段":"脱敏读取字段",
"前置校验模式":"前置校验模式",
"无权编辑此记录":"无权编辑此记录",
"确定要清空配置吗?":"确定要清空配置吗?",
"启用列表页单字段编辑":"启用列表页单字段编辑",
"无 (不限)":"无 (不限)",
"引用记录不存在":"引用记录不存在"
}

View file

@ -336,7 +336,7 @@
<index type="unique" field-list="appId"/>
</entity>
<entity name="RebuildApiRequest" type-code="031" description="API 调用日志" queryable="false" parent="false">
<entity name="RebuildApiRequest" type-code="031" description="OpenAPI 调用日志" queryable="false" parent="false">
<field name="requestId" type="primary"/>
<field name="appId" type="string" max-length="20" nullable="false" updatable="false" description="APPID"/>
<field name="remoteIp" type="string" max-length="100" nullable="false" updatable="false" description="来源 IP"/>

View file

@ -4,6 +4,7 @@
<meta name="renderer" content="webkit" />
<meta http-equiv="x-ua-compatible" content="IE=edge" />
<link rel="manifest" th:href="@{/assets/manifest.json}" />
<meta name="theme-color" th:content="${themeColor ?:'#4285f4'}" />
<link rel="shortcut icon" th:href="@{/assets/img/favicon.png}" />
<link rel="apple-touch-icon" th:href="@{/assets/img/favicon.png}" />
<th:block th:if="${css != 'none'}">

View file

@ -342,6 +342,7 @@ form.field-attr label > span {
font-size: 15px;
color: #999;
display: none;
transform: translateY(1px);
}
.table.table-p tr:hover a.easy-control {

View file

@ -137,7 +137,7 @@ div.dataTables_wrapper div.dataTables_oper.compact .btn-space {
.form-layout .type-NTEXT a.text-common {
position: absolute;
right: 15px;
right: 10px;
bottom: 5px;
background-color: #eee;
border: 0 none;

View file

@ -5038,7 +5038,7 @@ pre.unstyle {
font-weight: normal;
border: 0 none;
background-color: rgb(245, 247, 249);
max-width: 100%;
max-width: 99%;
font-size: 13px;
}

View file

@ -247,6 +247,7 @@ const CTs = {
Y: $L('按年'),
Q: $L('按季'),
M: $L('按月'),
W: $L('按周'),
D: $L('按日'),
H: $L('按时'),
I: $L('按时分'),

View file

@ -451,14 +451,27 @@ class FileShare extends RbModalHandler {
componentDidMount() {
$(this._dlg._rbmodal).css({ zIndex: 1099 })
this._changeTime()
this._filePath = this.props.file
if ($regex.isId(this.props.file)) {
$.get(`/files/check-readable?id=${this.props.file}`, (res) => {
if (res.data) {
this._filePath = res.data
this._changeTime()
} else {
RbHighbar.create($L('你没有查看此文件的权限'))
}
})
} else {
this._changeTime()
}
}
_changeTime = (e) => {
const t = e ? ~~e.target.dataset.time : EXPIRES_TIME[0][0]
if (this.state.time === t) return
this.setState({ time: t }, () => {
$.get(`/filex/make-share?url=${$encode(this.props.file)}&time=${t}&shareUrl=${$encode(this.__shareUrl)}`, (res) => {
$.get(`/filex/make-share?url=${$encode(this._filePath)}&time=${t}&shareUrl=${$encode(this.__shareUrl)}`, (res) => {
this.__shareUrl = (res.data || {}).shareUrl
this.setState({ shareUrl: this.__shareUrl })

View file

@ -423,15 +423,13 @@ class FileEditDlg extends RbFormHandler {
render() {
const file = this.props.file
let fileName = file.filePath
fileName = fileName ? $fileCutName(fileName) : null
return (
<RbModal title={$L('修改文件')} ref={(c) => (this._dlg = c)} disposeOnHide>
<div className="form">
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('文件名称')}</label>
<div className="col-sm-7">
<input className="form-control form-control-sm" defaultValue={fileName} ref={(c) => (this._$fileName = c)} />
<input className="form-control form-control-sm" defaultValue={file.fileName} ref={(c) => (this._$fileName = c)} />
<p className="form-text bosskey-show">
<a href={`${rb.baseUrl}/commons/file-editor?src=${file.id}`} target="_blank">
{$L('在线编辑')} (LAB)
@ -485,7 +483,7 @@ class FilesList4Docs extends FilesList {
<a title={$L('修改')} onClick={(e) => this._handleEdit(item, e)}>
<i className="icon zmdi zmdi-edit up-1" />
</a>
<a title={$L('下载')} onClick={(e) => $stopEvent(e)} href={`${rb.baseUrl}/filex/download/${item.filePath}?attname=${$encode(item.fileName)}`} target="_blank">
<a title={$L('下载')} onClick={(e) => $stopEvent(e)} href={`${rb.baseUrl}/files/download?id=${item.id}`} target="_blank">
<i className="icon zmdi zmdi-download fs-17" />
</a>
{rb.fileSharable && (
@ -506,7 +504,7 @@ class FilesList4Docs extends FilesList {
_handleShare(item, e) {
$stopEvent(e)
// eslint-disable-next-line react/jsx-no-undef
renderRbcomp(<FileShare file={item.filePath} />)
renderRbcomp(<FileShare file={item.id} />)
}
}

View file

@ -25,7 +25,7 @@ class FilesList extends React.Component {
{(this.state.files || []).map((item) => {
const checked = currentActive.includes(item.id)
return (
<div key={`file-${item.id}`} className={`file-list-item ${checked ? 'active' : ''}`} onClick={(e) => this._handleClick(e, item.id)}>
<div key={item.id} className={`file-list-item ${checked ? 'active' : ''}`} onClick={(e) => this._handleClick(e, item.id)}>
<div className="check">
<div className="custom-control custom-checkbox m-0">
<input className="custom-control-input" type="checkbox" checked={checked === true} readOnly />
@ -36,8 +36,15 @@ class FilesList extends React.Component {
<i className="file-icon" data-type={item.fileType || '?'} />
</div>
<div className="detail">
<a onClick={(e) => previewFile(e, item.filePath, item.relatedRecord ? item.relatedRecord[0] : null)} title={$L('预览')}>
{$fileCutName(item.filePath)}
<a
onClick={() => {
$.get(`/files/check-readable?id=${item.id}`, (res) => {
if (res.data) RbPreview.create(res.data)
else RbHighbar.create($L('你没有查看此文件的权限'))
})
}}
title={$L('预览')}>
{item.fileName}
</a>
<div className="extras">
<span className="fsize">{item.fileSize}</span>
@ -118,19 +125,6 @@ class FilesList extends React.Component {
}
}
// 文件预览
const previewFile = function (e, path, checkId) {
$stopEvent(e)
if (checkId) {
$.get(`/files/check-readable?id=${checkId}`, (res) => {
if (res.data) RbPreview.create(path)
else RbHighbar.error($L('你没有查看此文件的权限'))
})
} else {
RbPreview.create(path)
}
}
// ~~ 共享列表
class SharedFiles extends RbModalHandler {
render() {

View file

@ -1020,7 +1020,7 @@ class EditableFieldForms extends React.Component {
item.isFull = true
delete item.referenceQuickNew // v35
// eslint-disable-next-line no-undef
return detectElement(item, entity.entity)
return detectElement(item)
})}
</LiteForm>
)

View file

@ -130,9 +130,7 @@ class RbFormModal extends React.Component {
readonly={!!formModel.readonlyMessage}
ref={(c) => (that._formComponentRef = c)}
_disableAutoFillin={that.props._disableAutoFillin}>
{formModel.elements.map((item) => {
return detectElement(item, entity)
})}
{formModel.elements.map((item) => detectElement(item))}
</RbForm>
)
@ -1185,7 +1183,7 @@ class RbFormElement extends React.Component {
setReadonly(readonly) {
this.setState({ readonly: readonly === true }, () => {
// fix 4.0.6 只读变为非只读富附件需初始化
this.onEditModeChanged(readonly === true)
this.onEditModeChanged(readonly === true, true)
})
}
// TIP 仅表单有效
@ -1243,6 +1241,12 @@ class RbFormText extends RbFormElement {
</div>
</div>
)
// fix:4.1-b5 禁用时不触发
$(this._fieldValue).on('click', (e) => {
const $t = e.target || {}
if ($t.disabled || $t.readOnly) $stopEvent(e, true)
})
}
}
}
@ -1431,7 +1435,7 @@ class RbFormNText extends RbFormElement {
/>
{props.useMdedit && !_readonly37 && <input type="file" className="hide" accept="image/*" data-noname="true" ref={(c) => (this._fieldValue__upload = c)} />}
{this._textCommonMenuId && (
<a className="badge text-common" data-toggle="dropdown" data-target={`#${this._textCommonMenuId}`}>
<a className={`badge text-common ${_readonly37 && 'hide'}`} data-toggle="dropdown" data-target={`#${this._textCommonMenuId}`}>
{$L('常用值')}
</a>
)}
@ -1509,21 +1513,21 @@ class RbFormNText extends RbFormElement {
<div id={this._textCommonMenuId}>
<div className="dropdown-menu dropdown-menu-right common-texts">
{this.props.textCommon.split(',').map((c) => {
let cLN = c.replace(/\\n/g, '\n') // 换行符
return (
<a
key={c}
title={c}
title={cLN}
className="badge text-ellipsis"
onClick={() => {
c = c.replace(/\\n/g, '\n')
if (this._EasyMDE) {
this._mdeInsert(c)
this._mdeInsert(cLN)
} else {
const ps = this._fieldValue.selectionStart,
pe = this._fieldValue.selectionEnd
let val = this.state.value
if ($empty(val)) val = c
else val = val.substring(0, ps) + c + val.substring(pe)
if ($empty(val)) val = cLN
else val = val.substring(0, ps) + cLN + val.substring(pe)
this.handleChange({ target: { value: val } }, true)
// $focus2End(this._fieldValue)
}
@ -1559,6 +1563,9 @@ class RbFormNText extends RbFormElement {
_initMde() {
const _readonly37 = this.state.readonly
// fix:4.1-b5
this._EasyMDE && this._EasyMDE.toTextArea()
const mde = new EasyMDE({
element: this._fieldValue,
status: false,
@ -2109,12 +2116,16 @@ class RbFormPickList extends RbFormElement {
return super.renderViewElement(__findOptionText(this.state.options, this.state.value, true))
}
onEditModeChanged(destroy) {
onEditModeChanged(destroy, fromReadonly41) {
if (destroy) {
super.onEditModeChanged(destroy)
if (fromReadonly41) {
this.__select2 && $(this._fieldValue).attr('disabled', true)
} else {
super.onEditModeChanged(destroy)
}
} else {
if (this._isShowRadio39) {
// TODO
// Nothings
} else {
this.__select2 = $(this._fieldValue).select2({
placeholder: $L('选择%s', this.props.label),
@ -2142,7 +2153,7 @@ class RbFormPickList extends RbFormElement {
if (this._isShowRadio39) {
this.handleChange({ target: { value: val } }, true)
} else {
this.__select2.val(val).trigger('change')
this.__select2 && this.__select2.val(val).trigger('change')
}
}
}
@ -2232,9 +2243,13 @@ class RbFormReference extends RbFormElement {
return false
}
onEditModeChanged(destroy) {
onEditModeChanged(destroy, fromReadonly41) {
if (destroy) {
super.onEditModeChanged(destroy)
if (fromReadonly41) {
this.__select2 && $(this._fieldValue).attr('disabled', true)
} else {
super.onEditModeChanged(destroy)
}
} else {
this.__select2 = $initReferenceSelect2(this._fieldValue, {
name: this.props.field,
@ -2242,18 +2257,22 @@ class RbFormReference extends RbFormElement {
entity: this.props.entity,
wrapQuery: (query) => {
// v4.1 附加过滤条件支持从表单动态取值
const varRecord = this.props.referenceDataFilter ? this.props.$$$parent.getFormData() : null
if (varRecord) {
// FIXME 太长的值过滤以免 URL 超长
for (let k in varRecord) {
if (varRecord[k] && (varRecord[k] + '').length > 100) {
delete varRecord[k]
console.log('Ignore large value of field :', k, varRecord[k])
const $$$parent = this.props.$$$parent
if (this.props.referenceDataFilter && $$$parent) {
let varRecord = $$$parent.getFormData ? $$$parent.getFormData() : $$$parent.__ViewData
if (varRecord) {
// FIXME 太长的值过滤以免 URL 超长
for (let k in varRecord) {
if (varRecord[k] && (varRecord[k] + '').length > 100) {
delete varRecord[k]
console.log('Ignore large value of field :', k, varRecord[k])
}
}
varRecord['metadata.entity'] = $$$parent.props.entity
query.varRecord = $encode(JSON.stringify(varRecord))
}
varRecord['metadata.entity'] = this.props.$$$parent.props.entity
query.varRecord = $encode(JSON.stringify(varRecord))
}
const cascadingValue = this._getCascadingFieldValue()
if (cascadingValue) query.cascadingValue = cascadingValue
return query
@ -2546,6 +2565,7 @@ class RbFormN2NReference extends RbFormReference {
onEditModeChanged(destroy) {
super.onEditModeChanged(destroy)
if (!destroy && this.__select2) {
this.__select2.on('select2:select', (e) => __addRecentlyUse(e.params.data.id))
}
@ -2764,13 +2784,17 @@ class RbFormClassification extends RbFormElement {
return super.renderViewElement(text)
}
onEditModeChanged(destroy) {
onEditModeChanged(destroy, fromReadonly41) {
if (destroy) {
super.onEditModeChanged(destroy)
this.__cached = null
if (this.__selector) {
this.__selector.hide(true)
this.__selector = null
if (fromReadonly41) {
this.__select2 && $(this._fieldValue).attr('disabled', true)
} else {
super.onEditModeChanged(destroy)
this.__cached = null
if (this.__selector) {
this.__selector.hide(true)
this.__selector = null
}
}
} else {
this.__select2 = $initReferenceSelect2(this._fieldValue, {
@ -2897,10 +2921,14 @@ class RbFormMultiSelect extends RbFormElement {
return <div className="form-control-plaintext multi-values">{__findMultiTexts(this.props.options, maskValue, true)}</div>
}
onEditModeChanged(destroy) {
onEditModeChanged(destroy, fromReadonly41) {
if (this._isShowSelect41) {
if (destroy) {
super.onEditModeChanged(destroy)
if (fromReadonly41) {
this.__select2 && $(this._fieldValue).attr('disabled', true)
} else {
super.onEditModeChanged(destroy)
}
} else {
this.__select2 = $(this._fieldValue).select2({
placeholder: $L('选择%s', this.props.label),
@ -2945,7 +2973,16 @@ class RbFormMultiSelect extends RbFormElement {
setValue(val) {
// eg. {id:3, text:["A", "B"]}
if (typeof val === 'object') val = val.id || val
super.setValue(val)
if (this._isShowSelect41) {
let s = []
this.props.options &&
this.props.options.forEach((o) => {
if ((val & o.mask) !== 0) s.push(o.mask)
})
this.__select2 && this.__select2.val(s).trigger('change')
} else {
super.setValue(val)
}
}
}
@ -3345,10 +3382,14 @@ class RbFormTag extends RbFormElement {
this._selected = selected
}
onEditModeChanged(destroy) {
onEditModeChanged(destroy, fromReadonly41) {
if (destroy) {
super.onEditModeChanged(destroy)
this._initOptions()
if (fromReadonly41) {
this.__select2 && $(this._fieldValue).attr('disabled', true)
} else {
super.onEditModeChanged(destroy)
this._initOptions()
}
} else {
this.__select2 = $(this._fieldValue).select2({
placeholder: this.props.readonlyw > 0 ? this._placeholderw : $L('输入%s', this.props.label),
@ -3515,9 +3556,10 @@ var detectElement = function (item, entity) {
if (!item.key) {
item.key = `field-${item.field === TYPE_DIVIDER || item.field === TYPE_REFFORM ? $random() : item.field}`
}
// v4.1
item.entity = item.entity || entity
// v4.1-b5
if (entity) {
item.entity = entity
}
// 复写的字段组件
if (entity && window._CustomizedForms) {
const c = window._CustomizedForms.useFormElement(entity, item)

View file

@ -302,7 +302,7 @@ class ProTable extends React.Component {
const FORM = (
<InlineForm entity={entityName} id={model.id} rawModel={model} $$$parent={this} $$$main={this.props.$$$main} key={lineKey} ref={ref} _componentDidUpdate={() => this._componentDidUpdate()}>
{model.elements.map((item) => {
return detectElement({ ...item, colspan: 4, _disableAutoFillin: _disableAutoFillin === true }, entityName)
return detectElement({ ...item, colspan: 4, _disableAutoFillin: _disableAutoFillin === true })
})}
</InlineForm>
)

View file

@ -371,8 +371,15 @@ class LightAttachmentList extends RelatedList {
<i className="file-icon" data-type={item.fileType} />
</div>
<div className="detail">
<a onClick={() => (parent || window).RbPreview.create(item.filePath)} title={$L('预览')}>
{$fileCutName(item.filePath)}
<a
onClick={() => {
$.get(`/files/check-readable?id=${item.id}`, (res) => {
if (res.data) (parent || window).RbPreview.create(res.data)
else RbHighbar.create($L('你没有查看此文件的权限'))
})
}}
title={$L('预览')}>
{item.fileName}
</a>
<div className="extras">
<span className="fsize">{item.fileSize}</span>
@ -380,7 +387,7 @@ class LightAttachmentList extends RelatedList {
</div>
<div className="info position-relative">
<span className="fop-action">
<a title={$L('下载')} href={`${rb.baseUrl}/filex/download/${item.filePath}?attname=${$fileCutName(item.filePath)}`} target="_blank">
<a title={$L('下载')} href={`${rb.baseUrl}/files/download?id=${item.id}`} target="_blank">
<i className="icon zmdi zmdi-download fs-17" />
</a>
{rb.fileSharable && (
@ -389,7 +396,7 @@ class LightAttachmentList extends RelatedList {
onClick={(e) => {
$stopEvent(e)
// eslint-disable-next-line react/jsx-no-undef
renderRbcomp(<FileShare file={item.filePath} />)
renderRbcomp(<FileShare file={item.id} />)
}}>
<i className="icon zmdi zmdi-share up-1" />
</a>
@ -410,32 +417,27 @@ class LightAttachmentList extends RelatedList {
const pageSize = 20
const relatedId = this.props.mainid
$.get(
`/files/list-file?entry=${relatedId.substr(0, 3)}&sort=${this.__searchSort || ''}&q=${$encode(this.__searchKey)}&pageNo=${this.__pageNo}&pageSize=${pageSize}&related=${relatedId}`,
(res) => {
if (res.error_code !== 0) return RbHighbar.error(res.error_msg)
let url = `/files/list-file?entry=${relatedId.substr(0, 3)}&sort=${this.__searchSort || ''}&q=${$encode(this.__searchKey)}&pageNo=${this.__pageNo}&pageSize=${pageSize}&related=${relatedId}`
$.get(url, (res) => {
if (res.error_code !== 0) return RbHighbar.error(res.error_msg)
const data = res.data || []
const list = append ? (this.state.dataList || []).concat(data) : data
this.setState({ dataList: list, showMore: data.length >= pageSize })
const data = res.data || []
const list = append ? (this.state.dataList || []).concat(data) : data
this.setState({ dataList: list, showMore: data.length >= pageSize })
const files = list.map((item) => item.filePath)
if (files.length > 0) {
$(this._$downloadForm).find('input').val(files.join(','))
$(this._$downloadForm).find('button').attr('disabled', false)
}
const files = list.map((item) => item.id)
if (files.length > 0) {
$(this._$downloadForm).find('input').val(files.join(','))
$(this._$downloadForm).find('button').attr('disabled', false)
}
)
})
}
componentDidMount() {
// v3.1 有权限才加载
$.get(`/files/check-readable?id=${this.props.mainid}`, (res) => {
if (res.data === true) {
this.fetchData()
} else {
this.setState({ dataList: [] }, () => {})
}
// v3.1 有读取权限才加载
$.get(`/app/entity/check-readable?id=${this.props.mainid}`, (res) => {
if (res.data === true) this.fetchData()
else this.setState({ dataList: [] }, () => {})
})
}
}

View file

@ -665,7 +665,7 @@ const RbViewPage = {
$('.J_assign').on('click', () => DlgAssign.create({ entity: entity[0], ids: [id] }))
$('.J_share').on('click', () => DlgShare.create({ entity: entity[0], ids: [id] }))
$('.J_report').on('click', () => SelectReport.create(entity[0], id))
$('.J_add-detail-memu>a').on('click', function () {
$('.J_add-detail-menu>a').on('click', function () {
const iv = { $MAINID$: id }
const $this = $(this)
RbFormModal.create({ title: $L('添加%s', $this.data('label')), entity: $this.data('entity'), icon: $this.data('icon'), initialValue: iv, _nextAddDetail: true })
@ -681,7 +681,7 @@ const RbViewPage = {
// Privileges
if (ep) {
if (ep.D === false) $('.J_delete').remove()
if (ep.U === false) $('.J_edit, .J_add-detail, .J_add-detail-memu').remove()
if (ep.U === false) $('.J_edit, .J_add-detail, .J_add-detail-menu').remove()
if (ep.A !== true) $('.J_assign').remove()
if (ep.S !== true) $('.J_share').remove()
}

View file

@ -18,15 +18,19 @@ RbList.queryBefore = function (query) {
query = _RbList_queryBefore(query)
}
if (window.__PageConfig.protocolFilter && parent && parent.referenceSearch__dlg && parent.RbFormModal && parent.RbFormModal.__CURRENT35) {
const formComp = parent.RbFormModal.__CURRENT35.getFormComp()
let varRecord = formComp ? formComp.getFormData() : null
const formComp = parent.RbFormModal && parent.RbFormModal.__CURRENT35 ? parent.RbFormModal.__CURRENT35.getFormComp() : null
const viewComp = parent.RbViewPage ? parent.RbViewPage._RbViewForm : null
if (window.__PageConfig.protocolFilter && parent && parent.referenceSearch__dlg && (formComp || viewComp)) {
let varRecord = formComp ? formComp.getFormData() : viewComp ? viewComp.__ViewData : null
if (varRecord) {
// FIXME 太长的值过滤
for (let k in varRecord) {
if (varRecord[k] && (varRecord[k] + '').length > 200) delete varRecord[k]
if (varRecord[k] && (varRecord[k] + '').length > 100) {
console.log('Ignore large value of field :', k, varRecord[k])
delete varRecord[k]
}
}
query.protocolFilter__varRecord = { 'metadata.entity': formComp.props.entity, ...varRecord }
query.protocolFilter__varRecord = { 'metadata.entity': (formComp || viewComp).props.entity, ...varRecord }
}
}
return query

View file

@ -24,7 +24,7 @@ $(document).ready(() => {
setTimeout(function () {
if ($.browser.mobile) {
const $a = $('.h5-mobile>a')
const $a = $('.h5-mobile>a:eq(0)')
$a.parent().html('<a href="' + $a.attr('href') + '">' + $a.html() + '</a>')
} else {
$('.h5-mobile img').attr('src', `${rb.baseUrl}/commons/barcode/render-qr?w=296&t=${$encode($('.h5-mobile a').attr('href'))}`)
@ -97,9 +97,27 @@ $(document).ready(() => {
RbAlert.create($L('是否需要切换到手机版访问'), {
onConfirm: function () {
this.hide()
location.href = $('.h5-mobile a').attr('href')
location.href = $('.h5-mobile>a:eq(0)').attr('href')
},
})
}, 500)
}
})
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
let deferredPrompt = e
$('.h5-mobile.pwa')
.removeClass('hide')
.find('>a')
.on('click', () => {
if (deferredPrompt) {
deferredPrompt.prompt()
deferredPrompt.userChoice.then((choiceRes) => {
console.log('', choiceRes.outcome)
deferredPrompt = null
})
}
})
})

View file

@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "REBUILD",
"short_name": "REBUILD",
"description": "Made by getrebuild.com",
"description": "Powered by getrebuild.com",
"start_url": "../",
"background_color": "#ffffff",
"theme_color": "#4285f4",

View file

@ -55,6 +55,9 @@
text-align: center;
overflow: hidden;
}
.h5-mobile.pwa .icon {
line-height: 26px;
}
.h5-mobile span {
float: right;
color: rgb(64, 64, 64);
@ -163,7 +166,7 @@
</a>
</div>
<div class="row mb-2">
<div class="col">
<div class="col-8">
<div class="btn-group dropup h5-mobile fs-0">
<a class="dropdown-toggle" data-toggle="dropdown" th:href="${mobileUrl}">
<i class="icon zmdi zmdi-smartphone-iphone"></i>
@ -175,8 +178,14 @@
</div>
</div>
</div>
<div class="btn-group dropup h5-mobile pwa fs-0 ml-2 hide">
<a>
<i class="icon mdi mdi-monitor-arrow-down"></i>
<span class="up-1">[[${bundle.L('放在桌面')}]]</span>
</a>
</div>
</div>
<div class="col text-right">
<div class="col-4 text-right pl-0">
<div class="btn-group">
<a class="select-lang dropdown-toggle" data-toggle="dropdown">
<i class="icon zmdi zmdi-globe-alt"></i>