feat: Type cast (#702)

* bcc

* feat: field-type-cast

* feat: aviator DateCompare

* tf.nullable
This commit is contained in:
REBUILD 企业管理系统 2023-12-28 14:33:30 +08:00 committed by GitHub
parent 30ddd5528a
commit b6530c7938
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 305 additions and 26 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 791d6f665c9e937b4503459cc68958f4c266b4cd
Subproject commit 031039036ba44f47b61a2b81bcaa70922ef3656a

View file

@ -411,4 +411,54 @@ public class Field2Schema extends SetUser {
return identifier;
}
/**
* 类型转换
*
* @param field
* @param toType
* @param force
* @return
*/
public boolean castType(Field field, DisplayType toType, boolean force) {
EasyField easyMeta = EasyMetaFactory.valueOf(field);
ID metaRecordId = easyMeta.getMetaId();
if (easyMeta.isBuiltin() || metaRecordId == null) {
throw new MetadataModificationException(Language.L("系统内置,不允许转换"));
}
if (!force) {
long count;
if ((count = checkRecordCount(field.getOwnEntity())) > 100000) {
throw new MetadataModificationException(Language.L("实体记录过多 (%d),转换字段可能导致表损坏", count));
}
}
Record meta = EntityHelper.forUpdate(metaRecordId, getUser(), false);
meta.setString("displayType", toType.name());
Application.getCommonsService().update(meta, false);
Dialect dialect = Application.getPersistManagerFactory().getDialect();
final Table table = new Table(field.getOwnEntity(), dialect);
StringBuilder ddl = new StringBuilder();
table.generateFieldDDL(field, ddl);
String alterSql = String.format("alter table `%s` change column `%s` ",
field.getOwnEntity().getPhysicalName(), field.getPhysicalName());
alterSql += ddl.toString().trim();
try {
Application.getSqlExecutor().executeBatch(new String[]{alterSql}, DDL_TIMEOUT);
} catch (Throwable ex) {
// 还原
meta.setString("displayType", EasyMetaFactory.getDisplayType(field).name());
Application.getCommonsService().update(meta, false);
log.error("DDL ERROR : \n" + alterSql, ex);
throw new MetadataModificationException(ThrowableUtils.getRootCause(ex).getLocalizedMessage());
}
MetadataHelper.getMetadataFactory().refresh();
return true;
}
}

View file

@ -46,6 +46,12 @@ public class AviatorUtils {
AVIATOR.addOpFunction(OperatorType.ADD, new OverOperatorType.DateAdd());
AVIATOR.addOpFunction(OperatorType.SUB, new OverOperatorType.DateSub());
AVIATOR.addOpFunction(OperatorType.LE, new OverOperatorType.DateCompareLE());
AVIATOR.addOpFunction(OperatorType.LT, new OverOperatorType.DateCompareLT());
AVIATOR.addOpFunction(OperatorType.GE, new OverOperatorType.DateCompareGE());
AVIATOR.addOpFunction(OperatorType.GT, new OverOperatorType.DateCompareGT());
AVIATOR.addOpFunction(OperatorType.EQ, new OverOperatorType.DateCompareEQ());
AVIATOR.addOpFunction(OperatorType.NEQ, new OverOperatorType.DateCompareNEQ());
addCustomFunction(new DateDiffFunction());
addCustomFunction(new DateAddFunction());

View file

@ -10,6 +10,7 @@ package com.rebuild.core.service.trigger.aviator;
import cn.devezhao.commons.CalendarUtils;
import com.googlecode.aviator.lexer.token.OperatorType;
import com.googlecode.aviator.runtime.function.AbstractFunction;
import com.googlecode.aviator.runtime.type.AviatorBoolean;
import com.googlecode.aviator.runtime.type.AviatorLong;
import com.googlecode.aviator.runtime.type.AviatorObject;
@ -26,6 +27,8 @@ public class OverOperatorType {
private OverOperatorType() {}
// -- 计算
/**
* 日期加
*/
@ -83,4 +86,102 @@ public class OverOperatorType {
Date d = CalendarUtils.addDay(date, num);
return new AviatorDate(d);
}
// -- 比较
/**
* 日期比较
*/
static abstract class DateCompare extends AbstractFunction {
private static final long serialVersionUID = -4160230503581309424L;
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
Object $argv1 = arg1.getValue(env);
Object $argv2 = arg2.getValue(env);
if ($argv1 instanceof Date && $argv2 instanceof Date) {
long v1 = ((Date) $argv1).getTime();
long v2 = ((Date) $argv2).getTime();
boolean b = compare(v1, v2);
return AviatorBoolean.valueOf(b);
} else {
return arg1.add(arg2, env); // Use default
}
}
abstract protected boolean compare(long v1, long v2);
}
// LE `<=`
static class DateCompareLE extends DateCompare {
private static final long serialVersionUID = 1321662048697121893L;
@Override
public String getName() {
return OperatorType.LE.getToken();
}
@Override
protected boolean compare(long v1, long v2) {
return v1 <= v2;
}
}
// LT `<`
static class DateCompareLT extends DateCompare {
private static final long serialVersionUID = 8197857653882782806L;
@Override
public String getName() {
return OperatorType.LT.getToken();
}
@Override
protected boolean compare(long v1, long v2) {
return v1 < v2;
}
}
// GE `>=`
static class DateCompareGE extends DateCompare {
private static final long serialVersionUID = -7966630104916265372L;
@Override
public String getName() {
return OperatorType.GE.getToken();
}
@Override
protected boolean compare(long v1, long v2) {
return v1 >= v2;
}
}
// GT `>`
static class DateCompareGT extends DateCompare {
private static final long serialVersionUID = 5214573679573440753L;
@Override
public String getName() {
return OperatorType.GT.getToken();
}
@Override
protected boolean compare(long v1, long v2) {
return v1 > v2;
}
}
// EQ `==`
static class DateCompareEQ extends DateCompare {
private static final long serialVersionUID = -6142749075506832977L;
@Override
public String getName() {
return OperatorType.EQ.getToken();
}
@Override
protected boolean compare(long v1, long v2) {
return v1 == v2;
}
}
// NEQ `!=`
static class DateCompareNEQ extends DateCompare {
private static final long serialVersionUID = -838391653977975466L;
@Override
public String getName() {
return OperatorType.NEQ.getToken();
}
@Override
protected boolean compare(long v1, long v2) {
return v1 != v2;
}
}
}

View file

@ -34,7 +34,7 @@ public enum ConfigurationItem {
StorageURL, StorageApiKey, StorageApiSecret, StorageBucket,
// 邮件
MailUser, MailPassword, MailAddr, MailName(AppName), MailCc,
MailUser, MailPassword, MailAddr, MailName(AppName), MailCc, MailBcc,
MailSmtpServer,
// 短信

View file

@ -115,7 +115,7 @@ public class RebuildConfiguration extends KVStorage {
/**
* 邮件账号
*
* @return returns [MailUser, MailPassword, MailAddr, MailName, MailCc, MailSmtpServer]
* @return returns [MailUser, MailPassword, MailAddr, MailName, MailCc, MailBcc, MailSmtpServer]
*/
public static String[] getMailAccount() {
String[] set = getsNoUnset(false,
@ -123,11 +123,13 @@ public class RebuildConfiguration extends KVStorage {
if (set == null) return null;
String cc = get(ConfigurationItem.MailCc);
String bcc = get(ConfigurationItem.MailBcc);
String smtpServer = get(ConfigurationItem.MailSmtpServer);
return new String[] {
set[0], set[1], set[2], set[3],
StringUtils.defaultIfBlank(cc, null),
StringUtils.defaultIfBlank(bcc, null),
StringUtils.defaultIfBlank(smtpServer, null)
};
}

View file

@ -89,16 +89,16 @@ public class SMSender {
* @throws ConfigurationException If mail-account unset
*/
public static String sendMail(String to, String subject, String content, boolean useTemplate, String[] specAccount) throws ConfigurationException {
if (specAccount == null || specAccount.length < 5
if (specAccount == null || specAccount.length < 6
|| StringUtils.isBlank(specAccount[0]) || StringUtils.isBlank(specAccount[1])
|| StringUtils.isBlank(specAccount[2]) || StringUtils.isBlank(specAccount[3])) {
throw new ConfigurationException(Language.L("邮件账户未配置或配置错误"));
}
if (Application.devMode()) {
log.info("[dev] Fake send email to : {} / {} / {}", to, subject, content);
return null;
}
// if (Application.devMode()) {
// log.info("[dev] Fake send email to : {} / {} / {}", to, subject, content);
// return null;
// }
// 使用邮件模板
if (useTemplate) {
@ -133,7 +133,7 @@ public class SMSender {
final String logContent = "" + subject + "" + content;
// Use SMTP
if (specAccount.length >= 6 && StringUtils.isNotBlank(specAccount[5])) {
if (specAccount.length >= 7 && StringUtils.isNotBlank(specAccount[6])) {
try {
String emailId = sendMailViaSmtp(to, subject, content, specAccount);
createLog(to, logContent, TYPE_EMAIL, emailId, null);
@ -150,6 +150,7 @@ public class SMSender {
params.put("signature", specAccount[1]);
params.put("to", to);
if (StringUtils.isNotBlank(specAccount[4])) params.put("cc", specAccount[4]);
if (StringUtils.isNotBlank(specAccount[5])) params.put("bcc", specAccount[5]);
params.put("from", specAccount[2]);
params.put("from_name", specAccount[3]);
params.put("subject", subject);
@ -196,6 +197,7 @@ public class SMSender {
HtmlEmail email = new HtmlEmail();
email.addTo(to);
if (StringUtils.isNotBlank(specAccount[4])) email.addCc(specAccount[4]);
if (StringUtils.isNotBlank(specAccount[5])) email.addBcc(specAccount[5]);
email.setSubject(subject);
email.setHtmlMsg(htmlContent);
@ -203,7 +205,7 @@ public class SMSender {
email.setAuthentication(specAccount[0], specAccount[1]);
// HOST[:PORT:SSL|TLS]
String[] hostPortSsl = specAccount[5].split(":");
String[] hostPortSsl = specAccount[6].split(":");
email.setHostName(hostPortSsl[0]);
if (hostPortSsl.length > 1) email.setSmtpPort(Integer.parseInt(hostPortSsl[1]));
if (hostPortSsl.length > 2) {

View file

@ -245,6 +245,7 @@ public class ConfigurationController extends BaseController {
data.getString("MailUser"), data.getString("MailPassword"),
data.getString("MailAddr"), data.getString("MailName"),
data.getString("MailCc"),
data.getString("MailBcc"),
data.getString("MailSmtpServer")
};
if (specAccount[1].contains("*")) {
@ -277,7 +278,7 @@ public class ConfigurationController extends BaseController {
Object[] smsCount = Application.createQueryNoFilter(sqlCount)
.setParameter(1, 1)
.setParameter(1, xday)
.setParameter(2, xday)
.unique();
Object[][] email = Application.createQueryNoFilter(sql)

View file

@ -43,8 +43,8 @@
<tbody>
<tr class="smtp-show">
<td width="40%">[[${bundle.L('SMTP 服务器地址')}]]</td>
<td data-id="MailSmtpServer" data-ignore="true" th:data-value="${mailAccount == null ? '' : mailAccount[5]}">
[[${mailAccount == null ? bundle.L('未设置') : mailAccount[5]}]]
<td data-id="MailSmtpServer" data-ignore="true" th:data-value="${mailAccount == null ? '' : mailAccount[6]}">
[[${mailAccount == null ? bundle.L('未设置') : mailAccount[6]}]]
</td>
</tr>
<tr>
@ -78,6 +78,12 @@
[[${mailAccount == null || mailAccount[4] == null ? bundle.L('无') : mailAccount[4]}]]
</td>
</tr>
<tr>
<td>[[${bundle.L('密送地址')}]]</td>
<td data-id="MailBcc" th:data-value="${mailAccount == null || mailAccount[5] == null ? '' : mailAccount[5]}" data-optional="true">
[[${mailAccount == null || mailAccount[5] == null ? bundle.L('无') : mailAccount[5]}]]
</td>
</tr>
<tr class="show-on-edit">
<td></td>
<td>

View file

@ -66,7 +66,12 @@
<div class="form-group row">
<label class="col-md-12 col-xl-3 col-lg-4 col-form-label text-lg-right">[[${bundle.L('类型')}]]</label>
<div class="col-md-12 col-xl-6 col-lg-8">
<input class="form-control form-control-sm" type="text" readonly th:value="${fieldTypeLabel}" />
<div class="input-group">
<input class="form-control form-control-sm" type="text" readonly th:value="${fieldTypeLabel}" />
<div class="input-group-append hide">
<button type="button" class="btn btn-secondary J_cast-type" th:title="${bundle.L('转换字段类型')}"><i class="zmdi zmdi-swap icon"></i></button>
</div>
</div>
</div>
</div>
<th:block th:if="${fieldType == 'DECIMAL'}">
@ -531,6 +536,7 @@
<script th:src="@{/assets/js/general/rb-forms.append.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/field-formula.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/field-edit.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/field-type.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/entity-switch.js}" type="text/babel"></script>
</body>
</html>

View file

@ -321,6 +321,9 @@ class DlgAddChart extends RbFormHandler {
<button className="btn btn-primary" type="button" onClick={() => this.next()}>
{$L('下一步')}
</button>
<button className="btn btn-link" type="button" onClick={() => this.hide()}>
{$L('取消')}
</button>
</div>
</div>
</div>
@ -340,12 +343,6 @@ class DlgAddChart extends RbFormHandler {
this.__select2 = $(this._$entity).select2({
allowClear: false,
placeholder: $L('选择数据来源'),
// templateResult: function (res) {
// const $span = $('<span class="icon-append"></span>').attr('title', res.text).text(res.text)
// const found = _data.find((x) => x.entity === res.id)
// if (found) $(`<i class="icon zmdi zmdi-${found.icon}"></i>`).appendTo($span)
// return $span
// },
})
})
}

View file

@ -4,7 +4,7 @@ Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights re
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
/* global FormulaDate, FormulaCalc */
/* global FormulaDate, FormulaCalc, FIELD_TYPES */
const wpc = window.__PageConfig
const __gExtConfig = {}
@ -25,6 +25,8 @@ $(document).ready(function () {
if (wpc.fieldBuildin) {
$('.J_fieldAttrs, .J_advOpt, .J_for-STATE').remove()
$('#referenceCascadingField, #referenceQuickNew, #referenceDataFilter').parents('.form-group').remove()
} else {
$('.J_cast-type').parent().removeClass('hide')
}
// 显示重复值选项
if (SHOW_REPEATABLE.includes(dt) && wpc.fieldName !== 'approvalId') {
@ -299,6 +301,14 @@ $(document).ready(function () {
},
})
})
$('.J_cast-type').on('click', () => {
if (rb.commercial < 10) {
RbHighbar.error(WrapHtml($L('免费版不支持此功能 [(查看详情)](https://getrebuild.com/docs/rbv-features)')))
return
}
renderRbcomp(<FieldTypeCast entity={wpc.entityName} field={wpc.fieldName} fromType={wpc.fieldType} />)
})
})
// Check incorrect?
@ -732,3 +742,87 @@ const _handleAnyReference = function (es) {
if (es) $s2.val(es.split(',')).trigger('change')
})
}
// 字段类型转换
const __TYPE2TYPE = {
'NUMBER': ['DECIMAL'],
'DECIMAL': ['NUMBER'],
'DATE': ['DATETIME'],
'DATETIME': ['DATE'],
'TEXT': ['NTEXT', 'PHONE', 'EMAIL', 'URL'],
'PHONE': ['TEXT'],
'EMAIL': ['TEXT'],
'URL': ['TEXT'],
'NTEXT': ['TEXT'],
'IMAGE': ['FILE'],
'FILE': ['IMAGE'],
}
class FieldTypeCast extends RbFormHandler {
render() {
const toTypes = __TYPE2TYPE[this.props.fromType] || []
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">
<div className="form-control-plaintext text-bold">{FIELD_TYPES[this.props.fromType][0]}</div>
</div>
</div>
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('新字段类型')}</label>
<div className="col-sm-7">
<select className="form-control form-control-sm" ref={(c) => (this._$toType = c)}>
{toTypes.map((t) => {
return (
<option value={t} key={t}>
{(FIELD_TYPES[t] || [t])[0]}
</option>
)
})}
</select>
<p className="form-text">{$L('转换可能导致一定的精度损失请谨慎进行')}</p>
</div>
</div>
<div className="form-group row footer" ref={(c) => (this._$btns = c)}>
<div className="col-sm-7 offset-sm-3">
<button className="btn btn-primary" type="button" onClick={() => this.post()}>
{$L('确定')}
</button>
<button className="btn btn-link" type="button" onClick={() => this.hide()}>
{$L('取消')}
</button>
</div>
</div>
</div>
</RbModal>
)
}
componentDidMount() {
$(this._$toType).select2({
placeholder: $L('不可转换'),
templateResult: function (res) {
const $span = $('<span class="icon-append"></span>').attr('title', res.text).text(res.text)
$(`<i class="icon mdi ${(FIELD_TYPES[res.id] || [])[1]}"></i>`).appendTo($span)
return $span
},
})
}
post() {
const toType = $(this._$toType).val()
if (!toType) return RbHighbar.create($L('不可转换'))
const $btn = $(this._$btns).find('.btn').button('loading')
$.post(`/admin/entity/field-type-cast?entity=${this.props.entity}&field=${this.props.field}&toType=${toType}`, (res) => {
if (res.error_code === 0) {
location.reload()
} else {
$btn.button('reset')
RbHighbar.error(res.error_msg)
}
})
}
}

View file

@ -181,12 +181,12 @@ class RbModalHandler extends React.Component {
this.state = { ...props }
}
show = (state, call) => {
show = (state, cb) => {
const callback = () => {
// eslint-disable-next-line react/no-string-refs
const dlg = this._dlg || this.refs['dlg']
if (dlg) dlg.show()
typeof call === 'function' && call(this)
typeof cb === 'function' && cb(this)
}
if (state && $.type(state) === 'object') this.setState(state, callback)
else callback()
@ -1243,7 +1243,7 @@ const renderRbcomp = function (jsx, target, callback) {
target = target[0]
}
// ReactDOM.render(<React.StrictMode>{jsx}</React.StrictMode>, target, call)
// ReactDOM.render(<React.StrictMode>{jsx}</React.StrictMode>, target, callback)
ReactDOM.render(jsx, target, callback)
return target
}

View file

@ -339,8 +339,9 @@ class ContentFieldWriteback extends ActionContentSpec {
sourceField = this._$sourceValue.val()
if (!sourceField) return
} else if (mode === 'VNULL') {
const tf = this.state.targetFields.find((x) => x.name === targetField)
if (!tf.nullable) return RbHighbar.create($L('目标字段 %s 不能为空', `[ ${tf.label} ]`))
// v3.6 不校验
// const tf = this.state.targetFields.find((x) => x.name === targetField)
// if (!tf.nullable) return RbHighbar.create($L('目标字段 %s 不能为空', tf.label))
}
const items = this.state.items || []

View file

@ -92,4 +92,17 @@ class AviatorUtilsTest {
Assertions.assertThrows(ExpressionRuntimeException.class,
() -> AviatorUtils.eval("date1 + date1", env, false));
}
@Test
void testDateCompare() {
Map<String, Object> env = new HashMap<>();
env.put("date1", CalendarUtils.now());
env.put("date2", CalendarUtils.addDay(1));
AviatorUtils.eval("p(date1 == date1)", env, true);
AviatorUtils.eval("p(date1 != date2)", env, true);
AviatorUtils.eval("p(date1 > date2)", env, true);
AviatorUtils.eval("p(date1 < date2)", env, true);
AviatorUtils.eval("p(date1 <= date1)", env, true);
AviatorUtils.eval("p(date2 <= date2)", env, true);
}
}