Merge pull request #348 from getrebuild/text-expr-686

trigger : All fields support formula
This commit is contained in:
devezhao 2021-06-11 22:54:54 +08:00 committed by GitHub
commit 6394ec7184
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 307 additions and 114 deletions

2
@rbv

@ -1 +1 @@
Subproject commit d6df9e9bc54aedb4d1627a6cdb0b1c28b857a96c
Subproject commit fff7b6eb61ad97fd79d876a229de681d4c64af62

View file

@ -30,7 +30,7 @@ public enum DisplayType {
SERIES(EasySeries.class, "自动编号", FieldType.STRING, 40, "{YYYYMMDD}-{0000}"),
IMAGE(EasyImage.class, "图片", FieldType.STRING, 700, null),
FILE(EasyFile.class, "附件", FieldType.STRING, 700, null),
PICKLIST(EasyPickList.class, "选项", FieldType.REFERENCE, -1, null),
PICKLIST(EasyPickList.class, "下拉列表", FieldType.REFERENCE, -1, null),
CLASSIFICATION(EasyClassification.class, "分类", FieldType.REFERENCE, -1, null),
REFERENCE(EasyReference.class, "引用", FieldType.REFERENCE, -1, null),
AVATAR(EasyAvatar.class, "头像", FieldType.STRING, 300, null),

View file

@ -13,6 +13,7 @@ import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import cn.devezhao.persist4j.engine.StandardRecord;
import cn.devezhao.persist4j.metadata.MissingMetaExcetion;
import cn.devezhao.persist4j.record.RecordVisitor;
import com.alibaba.fastjson.JSONArray;
@ -35,8 +36,8 @@ import com.rebuild.core.service.trigger.TriggerException;
import com.rebuild.core.support.general.ContentWithFieldVars;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
import java.util.*;
@ -51,6 +52,7 @@ import java.util.*;
public class FieldWriteback extends FieldAggregation {
private static final String DATE_EXPR = "#";
private static final String CODE_PREFIX = "{{{{"; // ends with }}}}
private Set<ID> targetRecordIds;
private Record targetRecordData;
@ -133,7 +135,7 @@ public class FieldWriteback extends FieldAggregation {
final Record record = EntityHelper.forNew(targetEntity.getEntityCode(), UserService.SYSTEM_USER, false);
final JSONArray items = ((JSONObject) context.getActionContent()).getJSONArray("items");
Set<String> fieldVars = new HashSet<>();
final Set<String> fieldVars = new HashSet<>();
for (Object o : items) {
JSONObject item = (JSONObject) o;
String sourceField = item.getString("sourceField");
@ -146,7 +148,7 @@ public class FieldWriteback extends FieldAggregation {
if ("FIELD".equalsIgnoreCase(updateMode)) {
fieldVars.add(sourceField);
} else if ("FORMULA".equalsIgnoreCase(updateMode)) {
if (sourceField.contains(DATE_EXPR)) {
if (sourceField.contains(DATE_EXPR) && !sourceField.startsWith(CODE_PREFIX)) {
fieldVars.add(sourceField.split(DATE_EXPR)[0]);
} else {
Set<String> matchsVars = ContentWithFieldVars.matchsVars(sourceField);
@ -179,7 +181,7 @@ public class FieldWriteback extends FieldAggregation {
EasyField targetFieldEasy = EasyMetaFactory.valueOf(targetEntity.getField(targetField));
String updateMode = item.getString("updateMode");
String sourceField = item.getString("sourceField");
String sourceAny = item.getString("sourceField");
// 置空
if ("VNULL".equalsIgnoreCase(updateMode)) {
@ -188,15 +190,15 @@ public class FieldWriteback extends FieldAggregation {
// 固定值
else if ("VFIXED".equalsIgnoreCase(updateMode)) {
RecordVisitor.setValueByLiteral(targetField, sourceField, record);
RecordVisitor.setValueByLiteral(targetField, sourceAny, record);
}
// 字段
else if ("FIELD".equalsIgnoreCase(updateMode)) {
Field sourceField2 = MetadataHelper.getLastJoinField(sourceEntity, sourceField);
Field sourceField2 = MetadataHelper.getLastJoinField(sourceEntity, sourceAny);
if (sourceField2 == null) continue;
Object value = Objects.requireNonNull(useSourceData).getObjectValue(sourceField);
Object value = Objects.requireNonNull(useSourceData).getObjectValue(sourceAny);
Object newValue = value == null ? null : EasyMetaFactory.valueOf(sourceField2)
.convertCompatibleValue(value, targetFieldEasy);
if (newValue != null) {
@ -206,17 +208,23 @@ public class FieldWriteback extends FieldAggregation {
// 公式
else if ("FORMULA".equalsIgnoreCase(updateMode)) {
Assert.notNull(useSourceData, "[useSourceData] not be null");
if (useSourceData == null) {
log.warn("[useSourceData] is null, Set to empty");
useSourceData = new StandardRecord(sourceEntity, null);
}
// 高级公式代码
final boolean useCode = sourceAny.startsWith(CODE_PREFIX);
// 日期兼容 fix: v2.2
if (sourceField.contains(DATE_EXPR)) {
String fieldName = sourceField.split(DATE_EXPR)[0];
if (sourceAny.contains(DATE_EXPR) && !useCode) {
String fieldName = sourceAny.split(DATE_EXPR)[0];
Field sourceField2 = MetadataHelper.getLastJoinField(sourceEntity, fieldName);
if (sourceField2 == null) continue;
Object value = useSourceData.getObjectValue(fieldName);
Object newValue = value == null ? null : ((EasyDateTime) EasyMetaFactory.valueOf(sourceField2))
.convertCompatibleValue(value, targetFieldEasy, sourceField);
.convertCompatibleValue(value, targetFieldEasy, sourceAny);
if (newValue != null) {
record.setObjectValue(targetField, newValue);
}
@ -224,21 +232,25 @@ public class FieldWriteback extends FieldAggregation {
// 公式
else {
String clearFormual = sourceField.toUpperCase()
String clearFormual = useCode
? sourceAny.substring(4, sourceAny.length() - 4)
: sourceAny
.replace("×", "*")
.replace("÷", "/")
.replace("`", "'");
.replace("`", "\""); // fix: 2.4 改为 "
for (String fieldName : useSourceData.getAvailableFields()) {
String replace = "{" + fieldName.toUpperCase() + "}";
for (String fieldName : fieldVars) {
String replace = "{" + fieldName + "}";
if (clearFormual.contains(replace)) {
Object value = useSourceData.getObjectValue(fieldName);
if (value instanceof Date) {
value = CalendarUtils.getUTCDateTimeFormat().format(value);
} else {
value = value == null ? "0" : value.toString();
value = value == null ? StringUtils.EMPTY : value.toString();
}
clearFormual = clearFormual.replace(replace, (String) value);
} else {
log.warn("No replace of field found : {}", replace);
}
}
@ -251,6 +263,11 @@ public class FieldWriteback extends FieldAggregation {
record.setDouble(targetField, ObjectUtils.toDouble(newValue));
} else if (dt == DisplayType.DATE || dt == DisplayType.DATETIME) {
record.setDate(targetField, (Date) newValue);
} else {
newValue = checkoutFieldValue(newValue, targetFieldEasy);
if (newValue != null) {
record.setObjectValue(targetField, newValue);
}
}
}
}
@ -258,4 +275,64 @@ public class FieldWriteback extends FieldAggregation {
}
return record;
}
/**
* @see DisplayType
* @see com.rebuild.core.metadata.EntityRecordCreator
*/
private Object checkoutFieldValue(Object value, EasyField field) {
DisplayType dt = field.getDisplayType();
Object newValue = null;
if (dt == DisplayType.PICKLIST || dt == DisplayType.CLASSIFICATION
|| dt == DisplayType.REFERENCE || dt == DisplayType.ANYREFERENCE) {
ID id = ID.isId(value) ? ID.valueOf(value.toString()) : null;
if (id != null) {
int idCode = id.getEntityCode();
if (dt == DisplayType.PICKLIST) {
if (idCode == EntityHelper.PickList) newValue = id;
} else if (dt == DisplayType.CLASSIFICATION) {
if (idCode == EntityHelper.ClassificationData) newValue = id;
} else if (dt == DisplayType.REFERENCE) {
if (field.getRawMeta().getReferenceEntity().getEntityCode() == idCode) newValue = id;
} else {
newValue = id;
}
}
} else if (dt == DisplayType.N2NREFERENCE) {
String[] ids = value.toString().split(",");
List<String> idsList = new ArrayList<>();
for (String id : ids) {
if (ID.isId(id)) idsList.add(id);
}
if (ids.length == idsList.size()) newValue = value.toString();
} else if (dt == DisplayType.BOOL) {
if (value instanceof Boolean) {
newValue = value;
} else {
newValue = BooleanUtils.toBooleanObject(value.toString());
}
} else if (dt == DisplayType.MULTISELECT || dt == DisplayType.STATE) {
if (value instanceof Integer || value instanceof Long) {
newValue = value;
}
} else {
// TODO 验证字段格式
newValue = value.toString();
}
if (newValue == null) {
log.warn("Value `{}` cannot be convert to field (value) : {}", value, field.getRawMeta());
}
return newValue;
}
}

View file

@ -9,6 +9,7 @@ package com.rebuild.web.robot.trigger;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.MetadataSorter;
@ -71,12 +72,19 @@ public class TriggerAdminController extends BaseController {
mv.getModel().put("when", config[2]);
mv.getModel().put("whenTimer", config[7] == null ? StringUtils.EMPTY : config[7]);
mv.getModel().put("whenFilter", StringUtils.defaultIfBlank((String) config[3], JSONUtils.EMPTY_OBJECT_STR));
mv.getModel().put("actionContent", StringUtils.defaultIfBlank((String) config[4], JSONUtils.EMPTY_OBJECT_STR));
mv.getModel().put("actionContent", JSONUtils.EMPTY_OBJECT_STR);
mv.getModel().put("priority", config[5]);
mv.getModel().put("name", config[6]);
return mv;
}
// 单独加载否则会有转义问题
@GetMapping("trigger/{id}/actionContent")
public JSON pageEditor(@PathVariable String id) throws IOException {
Object[] x = Application.getQueryFactory().unique(ID.valueOf(id), "actionContent");
return (JSON) JSON.parse(x[0] == null ? "{}" : (String) x[0]);
}
@GetMapping("trigger/available-actions")
public List<String[]> getAvailableActions() {
List<String[]> alist = new ArrayList<>();

View file

@ -12,7 +12,7 @@
"自动编号":"自动编号",
"图片":"图片",
"附件":"附件",
"选项":"选项",
"下拉列表":"下拉列表",
"分类":"分类",
"引用":"引用",
"头像":"头像",
@ -68,7 +68,6 @@
"所属主实体":"所属主实体",
"字段":"字段",
"显示类型":"显示类型",
"下拉列表":"下拉列表",
"排序":"排序",
"布局":"布局",
"共享给谁":"共享给谁",

View file

@ -59,7 +59,7 @@
<a th:href="@{/admin/projects}"><i class="icon zmdi zmdi-shape"></i><span>[[${bundle.L('项目')}]]</span></a>
</li>
<li th:class="${active == 'frontjs-code'} ? 'active'">
<a th:href="@{/admin/frontjs-code}"><i class="icon zmdi zmdi-code-setting"></i><span>FrontJS</span> <sup class="rbv"></sup></a>
<a th:href="@{/admin/frontjs-code}"><i class="icon zmdi zmdi-code"></i><span>FrontJS</span> <sup class="rbv"></sup></a>
</li>
<li class="divider">[[${bundle.L('用户')}]]</li>
<li th:class="${active == 'users'} ? 'active'">

View file

@ -26,7 +26,7 @@
<option value="DECIMAL">[[${bundle.L('货币')}]]</option>
<option value="DATE">[[${bundle.L('日期')}]]</option>
<option value="DATETIME">[[${bundle.L('日期时间')}]]</option>
<option value="PICKLIST">[[${bundle.L('选项')}]]</option>
<option value="PICKLIST">[[${bundle.L('下拉列表')}]]</option>
<option value="CLASSIFICATION">[[${bundle.L('分类')}]]</option>
<option value="MULTISELECT">[[${bundle.L('多选')}]]</option>
<option value="REFERENCE">[[${bundle.L('引用')}]]</option>

View file

@ -37,7 +37,7 @@
<div class="tab-container">
<ul class="nav nav-tabs bg-transparent">
<li class="nav-item"><a class="nav-link active" href="#FIELDLIST" data-toggle="tab">[[${bundle.L('字段列表')}]]</a></li>
<li class="nav-item"><a class="nav-link" href="#FIELDNEW" data-toggle="tab">[[${bundle.L('添加字段')}]]</a></li>
<li class="nav-item"><a class="nav-link" href="#FIELDNEW" data-toggle="tab">+ [[${bundle.L('添加字段')}]]</a></li>
</ul>
<div class="tab-content bg-transparent p-0">
<div class="tab-pane active" id="FIELDLIST">

View file

@ -130,6 +130,10 @@ See LICENSE and COMMERCIAL in the project root for license information.
display: inline-block;
}
.formula-calc {
position: relative;
}
.formula-calc .fields ul:empty::after {
content: attr(_title);
color: #999;
@ -264,6 +268,45 @@ See LICENSE and COMMERCIAL in the project root for license information.
margin: 3px 0;
}
.form-control-plaintext.formula.switch-code {
padding-right: 40px;
}
.form-control-plaintext.formula.switch-code + .switch-code-btn {
position: absolute;
right: 2px;
top: 2px;
display: inline-block;
width: 33px;
height: 33px;
line-height: 33px;
text-align: center;
font-size: 1.35rem;
color: #777;
border-radius: 2px;
transition: background-color 0.2s;
}
.form-control-plaintext.formula.switch-code + .switch-code-btn:hover {
background-color: #212121;
color: #fff;
}
textarea.formula-code {
font-family: Consolas, Menlo, Monaco, 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace, serif;
font-size: 13px;
background-color: #212121;
color: #f07178;
border: 0 none;
height: 223px;
width: 100%;
resize: none;
outline: none;
border-radius: 0;
line-height: 1.6;
padding: 4px 10px;
}
.auto-assign .select2-selection__rendered,
.auto-share .select2-selection__rendered {
display: block !important;

View file

@ -18,12 +18,6 @@ class FormulaCalc extends RbAlert {
return (
<div className="formula-calc">
<div className="form-control-plaintext formula mb-2" _title={$L('计算公式')} ref={(c) => (this._$formula = c)}></div>
<div className="bosskey-show mb-2">
<textarea className="form-control form-control-sm row3x mb-1" ref={(c) => (this._$formulaInput = c)} />
<a href="https://www.yuque.com/boyan-avfmj/aviatorscript" target="_blank" className="link">
EXPRESSION ENGINE : AVIATORSCRIPT
</a>
</div>
<div className="row unselect">
<div className="col-6">
<div className="fields rb-scroller" ref={(c) => (this._$fields = c)}>
@ -84,17 +78,14 @@ class FormulaCalc extends RbAlert {
}
confirm() {
let expr = []
const expr = []
$(this._$formula)
.find('i')
.each(function () {
expr.push($(this).data('v'))
})
expr = expr.join('')
if ($(this._$formulaInput).val()) expr = $(this._$formulaInput).val()
typeof this.props.onConfirm === 'function' && this.props.onConfirm(expr)
typeof this.props.onConfirm === 'function' && this.props.onConfirm(expr.join(''))
this.hide()
}
@ -124,7 +115,7 @@ class FormulaDate extends RbAlert {
<div className="form-group">
<label className="text-bold">{$L('设置日期公式')}</label>
<div className="input-group">
<select className="form-control form-control-sm" ref={(c) => (this._base = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$base = c)}>
{base.map((item) => {
return (
<option key={item[0]} value={item[0]}>
@ -161,7 +152,7 @@ class FormulaDate extends RbAlert {
</div>
</div>
<div className="form-group mb-1">
<button type="button" className="btn btn-space btn-primary" onClick={() => this.confirm()}>
<button type="button" className="btn btn-primary" onClick={() => this.confirm()}>
{$L('确定')}
</button>
</div>
@ -170,7 +161,7 @@ class FormulaDate extends RbAlert {
}
confirm() {
let expr = $(this._base).val()
let expr = $(this._$base).val()
if (!expr) return
if (this.state.calcOp) {

View file

@ -75,7 +75,7 @@ $(document).ready(function () {
'DECIMAL': $L('货币'),
'DATE': $L('日期'),
'DATETIME': $L('日期时间'),
'PICKLIST': $L('选项'),
'PICKLIST': $L('下拉列表'),
'CLASSIFICATION': $L('分类'),
'MULTISELECT': $L('多选'),
'REFERENCE': $L('引用'),

View file

@ -735,11 +735,17 @@ var $expired = function (date, offset) {
*/
var _$unthy = function (text) {
if (!text) return null
if (rb.env === 'dev') console.log(text)
text = text.replace(/&quot;/g, '"')
text = text.replace(/\n/g, '\\n')
try {
var s = $.parseJSON(text)
if (rb.env === 'dev') console.log(s)
if (rb.env === 'dev') console.log(text, s)
return s
} catch (err) {
console.log(text, err)
return null
}
}
/**
* 获取语言PH_KEY

View file

@ -38,7 +38,9 @@ $(document).ready(() => {
})
saveFilter(wpc.whenFilter)
renderContentComp({ sourceEntity: wpc.sourceEntity, content: wpc.actionContent })
$.get(`/admin/robot/trigger/${wpc.configId}/actionContent`, (res) => {
renderContentComp({ sourceEntity: wpc.sourceEntity, content: res.data })
})
const $btn = $('.J_save').click(() => {
if (!contentComp) return

View file

@ -31,7 +31,7 @@ class ContentFieldAggregation extends ActionContentSpec {
<div className="col-md-12 col-lg-9">
<div className="row">
<div className="col-5">
<select className="form-control form-control-sm" ref={(c) => (this._targetEntity = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$targetEntity = c)}>
{(this.state.targetEntities || []).map((item) => {
const val = `${item[2]}.${item[0]}`
return (
@ -82,7 +82,7 @@ class ContentFieldAggregation extends ActionContentSpec {
</div>
<div className="row">
<div className="col-5">
<select className="form-control form-control-sm" ref={(c) => (this._targetField = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$targetField = c)}>
{(this.state.targetFields || []).map((item) => {
return (
<option key={item[0]} value={item[0]}>
@ -95,7 +95,7 @@ class ContentFieldAggregation extends ActionContentSpec {
</div>
<div className="col-2 pr-0">
<span className="zmdi zmdi-forward zmdi-hc-rotate-180"></span>
<select className="form-control form-control-sm" ref={(c) => (this._calcMode = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$calcMode = c)}>
{Object.keys(CALC_MODES).map((item) => {
return (
<option key={item} value={item}>
@ -112,7 +112,7 @@ class ContentFieldAggregation extends ActionContentSpec {
<p>{$L('计算公式')}</p>
</div>
<div className={this.state.calcMode === 'FORMULA' ? 'hide' : ''}>
<select className="form-control form-control-sm" ref={(c) => (this._sourceField = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$sourceField = c)}>
{(this.state.sourceFields || []).map((item) => {
return (
<option key={item[0]} value={item[0]}>
@ -136,7 +136,7 @@ class ContentFieldAggregation extends ActionContentSpec {
<label className="col-md-12 col-lg-3 col-form-label text-lg-right"></label>
<div className="col-md-12 col-lg-9">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mb-0">
<input className="custom-control-input" type="checkbox" ref={(c) => (this._readonlyFields = c)} />
<input className="custom-control-input" type="checkbox" ref={(c) => (this._$readonlyFields = c)} />
<span className="custom-control-label">
{$L('自动设置目标字段为只读')}
<i className="zmdi zmdi-help zicon down-1" data-toggle="tooltip" title={$L('本选项仅针对表单有效')} />
@ -163,7 +163,7 @@ class ContentFieldAggregation extends ActionContentSpec {
this.__select2 = []
$.get(`/admin/robot/trigger/field-aggregation-entities?source=${this.props.sourceEntity}`, (res) => {
this.setState({ targetEntities: res.data }, () => {
const $s2te = $(this._targetEntity)
const $s2te = $(this._$targetEntity)
.select2({ placeholder: $L('选择目标实体') })
.on('change', () => this._changeTargetEntity())
@ -178,13 +178,13 @@ class ContentFieldAggregation extends ActionContentSpec {
})
if (content) {
$(this._readonlyFields).attr('checked', content.readonlyFields === true)
$(this._$readonlyFields).attr('checked', content.readonlyFields === true)
this._saveAdvFilter(content.dataFilter)
}
}
_changeTargetEntity() {
const te = ($(this._targetEntity).val() || '').split('.')[1]
const te = ($(this._$targetEntity).val() || '').split('.')[1]
if (!te) return
// 清空现有规则
this.setState({ items: [] })
@ -195,12 +195,12 @@ class ContentFieldAggregation extends ActionContentSpec {
if (this.state.targetFields) {
this.setState({ targetFields: res.data.target }, () => {
$(this._calcMode).trigger('change')
$(this._$calcMode).trigger('change')
})
} else {
this.setState({ sourceFields: res.data.source, targetFields: res.data.target }, () => {
const $s2sf = $(this._sourceField).select2({ placeholder: $L('选择源字段') })
const $s2cm = $(this._calcMode)
const $s2sf = $(this._$sourceField).select2({ placeholder: $L('选择源字段') })
const $s2cm = $(this._$calcMode)
.select2({ placeholder: $L('选择聚合方式') })
.on('change', (e) => {
this.setState({ calcMode: e.target.value })
@ -213,7 +213,7 @@ class ContentFieldAggregation extends ActionContentSpec {
this.setState({ sourceFields: fs })
}
})
const $s2tf = $(this._targetField).select2({ placeholder: $L('选择目标字段') })
const $s2tf = $(this._$targetField).select2({ placeholder: $L('选择目标字段') })
$s2cm.trigger('change')
@ -257,9 +257,9 @@ class ContentFieldAggregation extends ActionContentSpec {
}
addItem() {
const tf = $(this._targetField).val()
const calc = $(this._calcMode).val()
const sf = calc === 'FORMULA' ? null : $(this._sourceField).val()
const tf = $(this._$targetField).val()
const calc = $(this._$calcMode).val()
const sf = calc === 'FORMULA' ? null : $(this._$sourceField).val()
const formula = calc === 'FORMULA' ? $(this._$formula).attr('data-v') : null
if (!tf) return RbHighbar.create($L('请选择目标字段'))
@ -270,7 +270,7 @@ class ContentFieldAggregation extends ActionContentSpec {
}
// 目标字段=源字段
const tfFull = `${$(this._targetEntity).val().split('.')[0]}.${tf}`.replace('$PRIMARY$.', '')
const tfFull = `${$(this._$targetEntity).val().split('.')[0]}.${tf}`.replace('$PRIMARY$.', '')
if (sf === tfFull) return RbHighbar.create($L('目标字段与源字段不能为同一字段'))
const items = this.state.items || []
@ -290,9 +290,9 @@ class ContentFieldAggregation extends ActionContentSpec {
buildContent() {
const content = {
targetEntity: $(this._targetEntity).val(),
targetEntity: $(this._$targetEntity).val(),
items: this.state.items,
readonlyFields: $(this._readonlyFields).prop('checked'),
readonlyFields: $(this._$readonlyFields).prop('checked'),
dataFilter: this._advFilter__data,
}

View file

@ -30,7 +30,7 @@ class ContentFieldWriteback extends ActionContentSpec {
<div className="col-md-12 col-lg-9">
<div className="row">
<div className="col-5">
<select className="form-control form-control-sm" ref={(c) => (this._targetEntity = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$targetEntity = c)}>
{(this.state.targetEntities || []).map((item) => {
const val = `${item[2]}.${item[0]}`
return (
@ -85,7 +85,7 @@ class ContentFieldWriteback extends ActionContentSpec {
</div>
<div className="row">
<div className="col-5">
<select className="form-control form-control-sm" ref={(c) => (this._targetField = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$targetField = c)}>
{(this.state.targetFields || []).map((item) => {
return (
<option key={item.name} value={item.name}>
@ -98,7 +98,7 @@ class ContentFieldWriteback extends ActionContentSpec {
</div>
<div className="col-2 pr-0">
<span className="zmdi zmdi-forward zmdi-hc-rotate-180"></span>
<select className="form-control form-control-sm" ref={(c) => (this._updateMode = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$updateMode = c)}>
{Object.keys(UPDATE_MODES).map((item) => {
return (
<option key={item} value={item}>
@ -111,7 +111,7 @@ class ContentFieldWriteback extends ActionContentSpec {
</div>
<div className={`col-5 ${this.state.targetField ? '' : 'hide'}`}>
<div className={this.state.updateMode === 'FIELD' ? '' : 'hide'}>
<select className="form-control form-control-sm" ref={(c) => (this._sourceField = c)}>
<select className="form-control form-control-sm" ref={(c) => (this._$sourceField = c)}>
{(this.state.sourceFields || []).map((item) => {
return (
<option key={item.name} value={item.name}>
@ -124,13 +124,13 @@ class ContentFieldWriteback extends ActionContentSpec {
</div>
<div className={this.state.updateMode === 'VFIXED' ? '' : 'hide'}>
{this.state.updateMode === 'VFIXED' && this.state.targetField && (
<FieldValueSet entity={this.state.targetEntity} field={this.state.targetField} placeholder={$L('固定值')} ref={(c) => (this._sourceValue = c)} />
<FieldValueSet entity={this.state.targetEntity} field={this.state.targetField} placeholder={$L('固定值')} ref={(c) => (this._$sourceValue = c)} />
)}
<p>{$L('固定值')}</p>
</div>
<div className={this.state.updateMode === 'FORMULA' ? '' : 'hide'}>
{this.state.updateMode === 'FORMULA' && this.state.targetField && (
<FieldFormula fields={this.__sourceFieldsCache} field={this.state.targetField} ref={(c) => (this._sourceFormula = c)} />
<FieldFormula fields={this.__sourceFieldsCache} field={this.state.targetField} ref={(c) => (this._$sourceFormula = c)} />
)}
<p>{$L('计算公式')}</p>
</div>
@ -147,7 +147,7 @@ class ContentFieldWriteback extends ActionContentSpec {
<label className="col-md-12 col-lg-3 col-form-label text-lg-right"></label>
<div className="col-md-12 col-lg-9">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mb-0">
<input className="custom-control-input" type="checkbox" ref={(c) => (this._readonlyFields = c)} />
<input className="custom-control-input" type="checkbox" ref={(c) => (this._$readonlyFields = c)} />
<span className="custom-control-label">
{$L('自动设置目标字段为只读')}
<i className="zmdi zmdi-help zicon down-1" data-toggle="tooltip" title={$L('本选项仅针对表单有效')} />
@ -165,7 +165,7 @@ class ContentFieldWriteback extends ActionContentSpec {
this.__select2 = []
$.get(`/admin/robot/trigger/field-writeback-entities?source=${this.props.sourceEntity}`, (res) => {
this.setState({ targetEntities: res.data }, () => {
const $s2te = $(this._targetEntity)
const $s2te = $(this._$targetEntity)
.select2({ placeholder: $L('选择目标实体') })
.on('change', () => this._changeTargetEntity())
@ -180,12 +180,12 @@ class ContentFieldWriteback extends ActionContentSpec {
})
if (content) {
$(this._readonlyFields).attr('checked', content.readonlyFields === true)
$(this._$readonlyFields).attr('checked', content.readonlyFields === true)
}
}
_changeTargetEntity() {
const te = ($(this._targetEntity).val() || '').split('.')[1]
const te = ($(this._$targetEntity).val() || '').split('.')[1]
if (!te) return
// 清空现有规则
this.setState({ targetEntity: te, items: [] })
@ -196,19 +196,19 @@ class ContentFieldWriteback extends ActionContentSpec {
if (this.state.targetFields) {
this.setState({ targetFields: res.data.target }, () => {
$(this._targetField).trigger('change')
$(this._$targetField).trigger('change')
})
} else {
this.setState({ sourceFields: res.data.source, targetFields: res.data.target }, () => {
const $s2tf = $(this._targetField)
const $s2tf = $(this._$targetField)
.select2({ placeholder: $L('选择目标字段') })
.on('change', () => this._changeTargetField())
const $s2um = $(this._updateMode)
const $s2um = $(this._$updateMode)
.select2({ placeholder: $L('选择更新方式') })
.on('change', (e) => {
this.setState({ updateMode: e.target.value })
})
const $s2sf = $(this._sourceField).select2({ placeholder: $L('选择源字段') })
const $s2sf = $(this._$sourceField).select2({ placeholder: $L('选择源字段') })
$s2tf.trigger('change')
this.__select2.push($s2tf)
@ -224,7 +224,7 @@ class ContentFieldWriteback extends ActionContentSpec {
}
_changeTargetField() {
const tf = $(this._targetField).val()
const tf = $(this._$targetField).val()
if (!tf) return
const targetField = this.state.targetFields.find((x) => x.name === tf)
@ -237,29 +237,29 @@ class ContentFieldWriteback extends ActionContentSpec {
})
this.setState({ targetField: null, sourceFields: sourceFields }, () => {
if (sourceFields.length > 0) $(this._sourceField).val(sourceFields[0].name)
if (sourceFields.length > 0) $(this._$sourceField).val(sourceFields[0].name)
// 强制销毁后再渲染
this.setState({ targetField: targetField })
})
}
addItem() {
const tf = $(this._targetField).val()
const mode = $(this._updateMode).val()
const tf = $(this._$targetField).val()
const mode = $(this._$updateMode).val()
if (!tf) return RbHighbar.create($L('请选择目标字段'))
let sourceField = null
if (mode === 'FIELD') {
sourceField = $(this._sourceField).val()
sourceField = $(this._$sourceField).val()
// 目标字段=源字段
const tfFull = `${$(this._targetEntity).val().split('.')[0]}.${tf}`.replace('$PRIMARY$.', '')
const tfFull = `${$(this._$targetEntity).val().split('.')[0]}.${tf}`.replace('$PRIMARY$.', '')
if (tfFull === sourceField) return RbHighbar.create($L('目标字段与源字段不能为同一字段'))
} else if (mode === 'FORMULA') {
sourceField = this._sourceFormula.val()
sourceField = this._$sourceFormula.val()
if (!sourceField) return RbHighbar.create($L('请输入计算公式'))
} else if (mode === 'VFIXED') {
sourceField = this._sourceValue.val()
sourceField = this._$sourceValue.val()
if (!sourceField) return
} else if (mode === 'VNULL') {
const tf2 = this.state.targetFields.find((x) => x.name === tf)
@ -271,7 +271,7 @@ class ContentFieldWriteback extends ActionContentSpec {
if (exists) return RbHighbar.create($L('目标字段重复'))
items.push({ targetField: tf, updateMode: mode, sourceField: sourceField })
this.setState({ items: items })
this.setState({ items: items }, () => this._$sourceFormula.clear())
}
delItem(targetField) {
@ -283,9 +283,9 @@ class ContentFieldWriteback extends ActionContentSpec {
buildContent() {
const content = {
targetEntity: $(this._targetEntity).val(),
targetEntity: $(this._$targetEntity).val(),
items: this.state.items,
readonlyFields: $(this._readonlyFields).prop('checked'),
readonlyFields: $(this._$readonlyFields).prop('checked'),
}
if (!content.targetEntity) {
RbHighbar.create($L('请选择目标实体'))
@ -314,37 +314,53 @@ class FieldFormula extends React.Component {
render() {
const fieldType = this.state.field.type
if (fieldType === 'DATE' || fieldType === 'DATETIME' || fieldType === 'NUMBER' || fieldType === 'DECIMAL') {
return <div className="form-control-plaintext formula" _title={$L('计算公式')} ref={(c) => (this._formula = c)} onClick={() => this.showFormula()}></div>
} else {
// @see DisplayType.java
if (fieldType === 'AVATAR' || fieldType === 'IMAGE' || fieldType === 'FILE') {
return <div className="form-control-plaintext text-danger">{$L('暂不支持')}</div>
} else {
return (
<div className="form-control-plaintext formula" _title={$L('计算公式')} onClick={() => this.show(this.state.field.type)}>
{this.state.valueText}
</div>
)
}
}
showFormula() {
const fieldVars = []
show(ft) {
const ndVars = []
this.props.fields.forEach((item) => {
if (item.name !== this.state.field.name && ['NUMBER', 'DECIMAL', 'DATE', 'DATETIME'].includes(item.type)) {
fieldVars.push([item.name, item.label, item.type])
if (['NUMBER', 'DECIMAL', 'DATE', 'DATETIME'].includes(item.type)) {
ndVars.push([item.name, item.label, item.type])
}
})
renderRbcomp(<FormulaCalc2 fields={fieldVars} onConfirm={(expr) => this._confirm(expr)} />)
// 数字日期支持计算器模式
const ndType = ['NUMBER', 'DECIMAL', 'DATE', 'DATETIME'].includes(ft)
renderRbcomp(<FormulaCalcWithCode fields={ndVars} forceCode={!ndType} onConfirm={(expr) => this.onConfirm(expr)} />)
}
_confirm(expr) {
onConfirm(expr) {
this._value = expr
$(this._formula).text(FieldFormula.formatText(expr, this.props.fields))
this.setState({ valueText: FieldFormula.formatText(expr, this.props.fields) })
}
val() {
return this._value
}
clear() {
this._value = null
this.setState({ valueText: null })
}
}
FieldFormula.formatText = function (formula, fields) {
if (!formula) return
// CODE
if (formula.startsWith('{{{{')) {
return FormulaCode.textCode(formula)
}
// DATE
if (formula.includes('#')) {
const fs = formula.split('#')
@ -355,15 +371,27 @@ FieldFormula.formatText = function (formula, fields) {
else {
const fs = []
fields.forEach((item) => fs.push([item.name, item.label]))
return FormulaCalc2.textFormula(formula, fs)
return FormulaCalcWithCode.textFormula(formula, fs)
}
}
// ~ 公式编辑器
// eslint-disable-next-line no-undef
class FormulaCalc2 extends FormulaCalc {
constructor(props) {
super(props)
class FormulaCalcWithCode extends FormulaCalc {
renderContent() {
if (this.props.forceCode || this.state.useCode) {
return (
<FormulaCode
initCode={this.props.initCode}
onConfirm={(code) => {
this.props.onConfirm(`{{{{${code}}}}}`)
this.hide()
}}
/>
)
} else {
return super.renderContent()
}
}
renderExtraKeys() {
@ -382,7 +410,8 @@ class FormulaCalc2 extends FormulaCalc {
DATESUB
</a>
<div className="dropdown-divider"></div>
<a className="dropdown-item" target="_blank" href="https://getrebuild.com/docs/admin/triggers#%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0%20(%E6%95%B0%E6%8D%AE%E8%BD%AC%E5%86%99)%20~~v2.3">
<a className="dropdown-item" target="_blank" href="https://getrebuild.com/docs/admin/triggers#%E5%85%AC%E5%BC%8F%E7%BC%96%E8%BE%91%E5%99%A8">
<i className="zmdi zmdi-help icon"></i>
{$L('如何使用函数')}
</a>
</div>
@ -405,7 +434,7 @@ class FormulaCalc2 extends FormulaCalc {
</div>
</li>
<li className="list-inline-item">
<a onClick={() => this.handleInput('`')}>`</a>
<a onClick={() => this.handleInput('"')}>&#34;</a>
</li>
<li className="list-inline-item">
<a onClick={() => this.handleInput(',')}>,</a>
@ -415,17 +444,24 @@ class FormulaCalc2 extends FormulaCalc {
}
componentDidMount() {
if (this._$fields) {
$(this._$fields).css('max-height', 221)
const $btn = $(`<a class="switch-code-btn" title="${$L('使用高级计算公式')}"><i class="icon zmdi zmdi-edit"></i></a>`)
$(this._$formula).addClass('switch-code').after($btn)
$btn.click(() => this.setState({ useCode: true }))
}
super.componentDidMount()
}
handleInput(v) {
if (['DATEDIFF', 'DATEADD', 'DATESUB', ',', '`'].includes(v)) {
$(`<i class="v oper" data-v="${v}">${v}</em>`).appendTo(this._$formula)
if (['DATEDIFF', 'DATEADD', 'DATESUB', ',', '"'].includes(v)) {
$(`<i class="v oper">${v}</em>`).appendTo(this._$formula).attr('data-v', v)
if (['DATEDIFF', 'DATEADD', 'DATESUB'].includes(v)) {
setTimeout(() => this.handleInput('('), 400)
setTimeout(() => this.handleInput('`'), 600)
setTimeout(() => this.handleInput('('), 300)
setTimeout(() => this.handleInput('"'), 400)
}
} else {
super.handleInput(v)
@ -433,13 +469,44 @@ class FormulaCalc2 extends FormulaCalc {
}
}
class FormulaCode extends React.Component {
render() {
return (
<div>
<textarea className="formula-code" ref={(c) => (this._$formulaInput = c)} defaultValue={this.props.initCode || ''} maxLength="2000" placeholder="// Support AviatorScript" autoFocus />
<div className="row mt-1">
<div className="col pt-2">
<span className="d-inline-block">
<a href="https://getrebuild.com/docs/admin/triggers#%E9%AB%98%E7%BA%A7%E8%AE%A1%E7%AE%97%E5%85%AC%E5%BC%8F" target="_blank" className="link">
{$L('如何使用高级计算公式')}
</a>
<i className="zmdi zmdi-help zicon"></i>
</span>
</div>
<div className="col text-right">
<button type="button" className="btn btn-primary" onClick={() => this.confirm()}>
{$L('确定')}
</button>
</div>
</div>
</div>
)
}
confirm() {
typeof this.props.onConfirm === 'function' && this.props.onConfirm($val(this._$formulaInput))
}
// 格式化显示
static textCode(code) {
code = code.substr(4, code.length - 8) // Remove {{{{ xxx }}}}
code = code.replace(/\n/gi, '<br/>').replace(/( )/gi, '&nbsp;')
return <code style={{ lineHeight: 1.2 }} dangerouslySetInnerHTML={{ __html: code }} />
}
}
// eslint-disable-next-line no-undef
renderContentComp = function (props) {
// // 禁用`删除`
// $('.J_when .custom-control-input').each(function () {
// if (~~$(this).val() === 2) $(this).attr('disabled', true)
// })
renderRbcomp(<ContentFieldWriteback {...props} />, 'react-content', function () {
// eslint-disable-next-line no-undef
contentComp = this