mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-14 17:01:14 +08:00
Merge branch 'baidu:master' into feat-fileName
This commit is contained in:
commit
dd6188c50f
@ -19,9 +19,7 @@ test('Renderer:bar-code', async () => {
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(container.querySelector('.cxd-BarCode')).toBeInTheDocument()
|
||||
);
|
||||
await waitFor(() => expect(container.querySelector('.cxd-BarCode')).toBeInTheDocument());
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
@ -197,3 +197,89 @@ test('Renderer:cards media', () => {
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Renderer:cards hightlight', () => {
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'page',
|
||||
body: {
|
||||
type: 'card',
|
||||
header: {
|
||||
title: '标题',
|
||||
highlight: true,
|
||||
highlightClassName: 'test-highlight-class'
|
||||
},
|
||||
media: {
|
||||
type: 'image',
|
||||
className: 'w-36 h-24',
|
||||
url: 'https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80',
|
||||
position: 'left'
|
||||
},
|
||||
body: '这里是内容',
|
||||
secondary: '次要说明',
|
||||
actions: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '操作',
|
||||
actionType: 'dialog',
|
||||
className: 'mr-4',
|
||||
dialog: {
|
||||
title: '操作',
|
||||
body: '你正在编辑该卡片'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
label: '操作',
|
||||
actionType: 'dialog',
|
||||
className: 'mr-2.5',
|
||||
dialog: {
|
||||
title: '操作',
|
||||
body: '你正在编辑该卡片'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'dropdown-button',
|
||||
level: 'link',
|
||||
icon: 'fa fa-ellipsis-h',
|
||||
className: 'pr-1 flex',
|
||||
hideCaret: true,
|
||||
buttons: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '编辑',
|
||||
actionType: 'dialog',
|
||||
dialog: {
|
||||
title: '编辑',
|
||||
body: '你正在编辑该卡片'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
label: '删除',
|
||||
actionType: 'dialog',
|
||||
dialog: {
|
||||
title: '提示',
|
||||
body: '你删掉了该卡片'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
toolbar: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '标签',
|
||||
className: 'label label-warning'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
@ -140,6 +140,41 @@ exports[`Form:initData:remote 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
Label
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="c"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
@ -211,9 +246,9 @@ Object {
|
||||
},
|
||||
"method": "get",
|
||||
"query": Object {
|
||||
"a": "123",
|
||||
"c": "123",
|
||||
},
|
||||
"url": "/api/xxx?a=123",
|
||||
"url": "/api/xxx?c=123",
|
||||
}
|
||||
`;
|
||||
|
||||
@ -221,6 +256,7 @@ exports[`Form:initData:remote 3`] = `
|
||||
Object {
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": "123",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -1599,7 +1599,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-0"
|
||||
role="option"
|
||||
style="position: absolute; top: 0px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 0px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option0
|
||||
@ -1610,7 +1610,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-1"
|
||||
role="option"
|
||||
style="position: absolute; top: 35px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 35px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option1
|
||||
@ -1621,7 +1621,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-2"
|
||||
role="option"
|
||||
style="position: absolute; top: 70px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 70px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option2
|
||||
@ -1632,7 +1632,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-3"
|
||||
role="option"
|
||||
style="position: absolute; top: 105px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 105px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option3
|
||||
@ -1643,7 +1643,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-4"
|
||||
role="option"
|
||||
style="position: absolute; top: 140px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 140px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option4
|
||||
@ -1654,7 +1654,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-5"
|
||||
role="option"
|
||||
style="position: absolute; top: 175px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 175px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option5
|
||||
@ -1665,7 +1665,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-6"
|
||||
role="option"
|
||||
style="position: absolute; top: 210px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 210px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option6
|
||||
@ -1676,7 +1676,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-7"
|
||||
role="option"
|
||||
style="position: absolute; top: 245px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 245px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option7
|
||||
@ -1687,7 +1687,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-8"
|
||||
role="option"
|
||||
style="position: absolute; top: 280px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 280px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option8
|
||||
@ -1698,7 +1698,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-9"
|
||||
role="option"
|
||||
style="position: absolute; top: 315px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 315px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option9
|
||||
@ -1709,7 +1709,7 @@ exports[`Renderer:select virtual 1`] = `
|
||||
class="cxd-Select-option"
|
||||
id="downshift-1-item-10"
|
||||
role="option"
|
||||
style="position: absolute; top: 350px; left: 0px; width: 100%; height: 35px;"
|
||||
style="position: absolute; top: 350px; left: 0px; width: auto; height: 35px;"
|
||||
>
|
||||
<span>
|
||||
option10
|
||||
|
@ -0,0 +1,387 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Renderer:inputDate 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="cxd-Panel-heading"
|
||||
>
|
||||
<h3
|
||||
class="cxd-Panel-title"
|
||||
>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
The form
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Panel-body"
|
||||
>
|
||||
<form
|
||||
class="cxd-Form cxd-Form--normal"
|
||||
novalidate=""
|
||||
>
|
||||
<input
|
||||
style="display: none;"
|
||||
type="submit"
|
||||
/>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test1
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test1"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test2(test)
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test3(test1)
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test1"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="email"
|
||||
value="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test4
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test4"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="$"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test5
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test5"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="\${test}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<div
|
||||
class="cxd-SwitchControl cxd-Form-control"
|
||||
>
|
||||
<label
|
||||
class="cxd-Switch"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
theme="cxd"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="text"
|
||||
/>
|
||||
<span
|
||||
class="slider"
|
||||
/>
|
||||
</label>
|
||||
<span
|
||||
class="cxd-Switch-option"
|
||||
>
|
||||
开关
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test6
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test6"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="test: 123, test1: 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test7(test6)
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test7"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="test: 123, test1: 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Form-item cxd-Form-item--normal"
|
||||
data-role="form-item"
|
||||
>
|
||||
<label
|
||||
class="cxd-Form-label"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
test8
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="cxd-Form-control cxd-TextControl"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class=""
|
||||
name="test8"
|
||||
placeholder=""
|
||||
size="10"
|
||||
type="text"
|
||||
value="test1: 1, switch: false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="resize-sensor"
|
||||
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-expand"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-shrink"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-appear"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -175,12 +175,17 @@ test('Form:initData:remote', async () => {
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
initApi: '/api/xxx?a=${a}',
|
||||
initApi: '/api/xxx?c=${c}',
|
||||
controls: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'a',
|
||||
label: 'Label',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'c',
|
||||
label: 'Label',
|
||||
value: '123'
|
||||
}
|
||||
],
|
||||
@ -204,6 +209,9 @@ test('Form:initData:remote', async () => {
|
||||
expect(
|
||||
container.querySelector('[name="a"][value="1"]')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('[name="c"][value="123"]')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('[data-testid="spinner"]')
|
||||
).not.toBeInTheDocument();
|
||||
|
303
__tests__/renderers/Form/valueFormula.test.tsx
Normal file
303
__tests__/renderers/Form/valueFormula.test.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import React = require('react');
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
cleanup,
|
||||
getByText,
|
||||
waitFor
|
||||
} from '@testing-library/react';
|
||||
import '../../../src/themes/default';
|
||||
import {render as amisRender} from '../../../src/index';
|
||||
import {makeEnv, wait} from '../../helper';
|
||||
|
||||
/**
|
||||
* 组件默认值 支持 公式运算
|
||||
* 测试用例:
|
||||
* 1. 普通文字带一个$,不应该启用公式,带转义的 \${abc} 也不启用 渲染器后值就是当前原始值;
|
||||
* 2. '文本 ${abc}' 这个默认值需要启动公式,且验证新值初始是否在上下文中,关联 abc 修改后,新值是否在上下文中;
|
||||
*/
|
||||
test('Renderer:inputDate', async () => {
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
api: '/api/xxx',
|
||||
data: {
|
||||
test: '123'
|
||||
},
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'test1',
|
||||
label: 'test1',
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'test',
|
||||
label: 'test2(test)'
|
||||
},
|
||||
{
|
||||
type: 'input-email',
|
||||
name: 'test1',
|
||||
label: 'test3(test1)'
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'test4',
|
||||
label: 'test4',
|
||||
value: '$'
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'test5',
|
||||
label: 'test5',
|
||||
value: '\\${test}'
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
option: '开关',
|
||||
name: 'switch',
|
||||
falseValue: false,
|
||||
trueValue: true,
|
||||
value: true
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'test6',
|
||||
label: 'test6',
|
||||
value: 'test: ${test}, test1: ${test1}'
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'test7',
|
||||
label: 'test7(test6)',
|
||||
value: '${test6}'
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'test8',
|
||||
label: 'test8',
|
||||
value: 'test1: ${test1}, switch: ${switch}'
|
||||
}
|
||||
],
|
||||
title: 'The form',
|
||||
actions: []
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const test2 = container.querySelector(
|
||||
'input[name="test"]'
|
||||
) as HTMLInputElement;
|
||||
expect(test2.value).toEqual('123');
|
||||
|
||||
const test3 = container.querySelector(
|
||||
'input[type="email"][name="test1"]'
|
||||
) as HTMLInputElement;
|
||||
expect(test3.value).toEqual('1');
|
||||
|
||||
const test4 = container.querySelector(
|
||||
'input[type="text"][name="test4"]'
|
||||
) as HTMLInputElement;
|
||||
expect(test4.value).toEqual('$');
|
||||
|
||||
const test5 = container.querySelector(
|
||||
'input[type="text"][name="test5"]'
|
||||
) as HTMLInputElement;
|
||||
expect(test5.value).toEqual('${test}');
|
||||
|
||||
const test6 = container.querySelector(
|
||||
'input[type="text"][name="test6"]'
|
||||
) as HTMLInputElement;
|
||||
expect(test6.value).toEqual('test: 123, test1: 1');
|
||||
|
||||
const test7 = container.querySelector(
|
||||
'input[type="text"][name="test7"]'
|
||||
) as HTMLInputElement;
|
||||
expect(test7.value).toEqual('test: 123, test1: 1');
|
||||
});
|
||||
|
||||
const Switch = container.querySelector(
|
||||
'input[type="checkbox"]'
|
||||
) as HTMLInputElement;
|
||||
fireEvent.click(Switch);
|
||||
|
||||
await waitFor(() => {
|
||||
const test8 = container.querySelector(
|
||||
'input[type="text"][name="test8"]'
|
||||
) as HTMLInputElement;
|
||||
expect(test8.value).toEqual('test1: 1, switch: false');
|
||||
});
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('ValueFormula: case2', async () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
const {container, getByText} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
onSubmit: onSubmit,
|
||||
data: {
|
||||
test: {a: 123, b: 234},
|
||||
vara: '123',
|
||||
varb: '234'
|
||||
},
|
||||
body: [
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'test1',
|
||||
label: 'test1',
|
||||
value: '${test.a} - ${test.b}'
|
||||
},
|
||||
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'test2',
|
||||
label: 'test2',
|
||||
value: '\\${test.a} - ${test.b}'
|
||||
},
|
||||
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'test3',
|
||||
label: 'test3',
|
||||
value: '<%= data.test.a %>'
|
||||
},
|
||||
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'test4',
|
||||
label: 'test4',
|
||||
value: '<%= data.test.a %> 不应该支持即便是有 \\${test.a} 混用'
|
||||
},
|
||||
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'test5',
|
||||
label: 'test5',
|
||||
value: '<%= data.test.a %> 不应该支持即便是有 ${test.a} 混用'
|
||||
},
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'test6',
|
||||
label: 'test6',
|
||||
value: '${test}'
|
||||
},
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'vara',
|
||||
label: 'vara',
|
||||
value: '\\${test}'
|
||||
},
|
||||
{
|
||||
type: 'combo',
|
||||
name: 'varb',
|
||||
label: 'varb',
|
||||
value: '345'
|
||||
}
|
||||
],
|
||||
title: 'The form',
|
||||
actions: [
|
||||
{
|
||||
type: 'submit',
|
||||
label: 'Submit'
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('Submit'));
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onSubmit.mock.calls[0][0].test1).toBe('123 - 234');
|
||||
expect(onSubmit.mock.calls[0][0].test2).toBe('${test.a} - 234');
|
||||
expect(onSubmit.mock.calls[0][0].test3).toBe('<%= data.test.a %>');
|
||||
expect(onSubmit.mock.calls[0][0].test4).toBe(
|
||||
'<%= data.test.a %> 不应该支持即便是有 ${test.a} 混用'
|
||||
);
|
||||
expect(onSubmit.mock.calls[0][0].test5).toBe(
|
||||
'<%= data.test.a %> 不应该支持即便是有 123 混用'
|
||||
);
|
||||
expect(onSubmit.mock.calls[0][0].test6).toMatchObject({a: 123});
|
||||
expect(onSubmit.mock.calls[0][0].vara).toBe('123');
|
||||
expect(onSubmit.mock.calls[0][0].varb).toBe('234');
|
||||
});
|
||||
|
||||
test('ValueFormula: case3', async () => {
|
||||
const {container, getByText} = render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
data: {
|
||||
a: 1,
|
||||
b: 2
|
||||
},
|
||||
body: [
|
||||
{
|
||||
name: 'b',
|
||||
component: (props: any) => {
|
||||
const [value, setValue] = React.useState(233);
|
||||
function onChange() {
|
||||
setValue(344);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render(
|
||||
'inner',
|
||||
{
|
||||
name: 'a',
|
||||
type: 'input-text'
|
||||
},
|
||||
{
|
||||
value: value
|
||||
}
|
||||
)}
|
||||
|
||||
<button onClick={onChange}>Change</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
],
|
||||
title: 'The form',
|
||||
actions: [
|
||||
{
|
||||
type: 'submit',
|
||||
label: 'Submit'
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('input[name="a"]')).toBeInTheDocument();
|
||||
expect(
|
||||
(container.querySelector('input[name="a"]') as HTMLInputElement).value
|
||||
).toBe('233');
|
||||
});
|
||||
|
||||
fireEvent.click(getByText('Change'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('input[name="a"]')).toBeInTheDocument();
|
||||
expect(
|
||||
(container.querySelector('input[name="a"]') as HTMLInputElement).value
|
||||
).toBe('344');
|
||||
});
|
||||
});
|
@ -388,6 +388,135 @@ exports[`Renderer:cards color 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Renderer:cards hightlight 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="cxd-Page"
|
||||
>
|
||||
<div
|
||||
class="cxd-Page-content"
|
||||
>
|
||||
<div
|
||||
class="cxd-Page-main"
|
||||
>
|
||||
<div
|
||||
class="cxd-Page-body"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card-multiMedia--left"
|
||||
>
|
||||
<img
|
||||
class="cxd-Card-multiMedia-img w-36 h-24"
|
||||
src="https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80"
|
||||
/>
|
||||
<div
|
||||
class="cxd-Card-multiMedia-flex"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card-heading"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card-meta"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card-title"
|
||||
>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
标题
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Card-toolbar"
|
||||
>
|
||||
<i
|
||||
class="cxd-Card-highlight test-highlight-class"
|
||||
/>
|
||||
<span
|
||||
class="cxd-TplField label label-warning"
|
||||
>
|
||||
<span>
|
||||
标签
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Card-body"
|
||||
>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
这里是内容
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Card-footer-wrapper"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card-secondary"
|
||||
>
|
||||
<span
|
||||
class="cxd-TplField"
|
||||
>
|
||||
<span>
|
||||
次要说明
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-Card-actions-wrapper"
|
||||
>
|
||||
<div
|
||||
class="cxd-Card-actions"
|
||||
>
|
||||
<a
|
||||
class="cxd-Card-action mr-4"
|
||||
>
|
||||
<span>
|
||||
操作
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
class="cxd-Card-action mr-2.5"
|
||||
>
|
||||
<span>
|
||||
操作
|
||||
</span>
|
||||
</a>
|
||||
<div
|
||||
class="cxd-DropDown cxd-Card-action pr-1 flex"
|
||||
>
|
||||
<button
|
||||
class="cxd-Button cxd-Button--link cxd-Button--sm"
|
||||
>
|
||||
<i
|
||||
class="m-r-xs fa fa-ellipsis-h"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Renderer:cards media 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -483,11 +483,12 @@ export default class PlayGround extends React.Component {
|
||||
if (mini) {
|
||||
return (
|
||||
<div className="Playgroud Playgroud--mini">
|
||||
<a onClick={this.toggleDrawer}>
|
||||
<a onClick={this.toggleDrawer} className="Playgroud-edit-btn">
|
||||
编辑代码 <i className="fa fa-code p-l-xs"></i>
|
||||
</a>
|
||||
<Drawer
|
||||
showCloseButton
|
||||
closeOnOutside
|
||||
resizable
|
||||
theme={theme}
|
||||
overlay={false}
|
||||
|
@ -1036,6 +1036,10 @@ body.dark {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&-edit-btn {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-preview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -17,7 +17,7 @@ import {ScopedContext} from './Scoped';
|
||||
import {Schema, SchemaNode} from './types';
|
||||
import {DebugWrapper} from './utils/debug';
|
||||
import getExprProperties from './utils/filter-schema';
|
||||
import {anyChanged, chainEvents, autobind} from './utils/helper';
|
||||
import {anyChanged, chainEvents, autobind, createObject} from './utils/helper';
|
||||
import {SimpleMap} from './utils/SimpleMap';
|
||||
|
||||
import {bindEvent, dispatchEvent, RendererEvent} from './utils/renderer-event';
|
||||
@ -302,7 +302,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
|
||||
const isSFC = !(schema.component.prototype instanceof React.Component);
|
||||
const {
|
||||
data: defaultData,
|
||||
value: defaultValue,
|
||||
value: defaultValue, // render时的value改放defaultValue中
|
||||
activeKey: defaultActiveKey,
|
||||
key: propKey,
|
||||
...restSchema
|
||||
@ -313,6 +313,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
|
||||
...rest,
|
||||
...restSchema,
|
||||
...exprProps,
|
||||
// value: defaultValue, // 备注: 此处并没有将value传递给渲染器
|
||||
defaultData,
|
||||
defaultValue,
|
||||
defaultActiveKey,
|
||||
@ -388,6 +389,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
|
||||
...restSchema,
|
||||
...chainEvents(rest, restSchema),
|
||||
...exprProps,
|
||||
// value: defaultValue, // 备注: 此处并没有将value传递给渲染器
|
||||
defaultData: restSchema.defaultData ?? defaultData,
|
||||
defaultValue: restSchema.defaultValue ?? defaultValue,
|
||||
defaultActiveKey: defaultActiveKey,
|
||||
|
@ -94,7 +94,7 @@ const STYLE_ITEM: {
|
||||
position: 'absolute' as ItemPosition,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%'
|
||||
width: 'auto'
|
||||
};
|
||||
|
||||
const STYLE_STICKY_ITEM = {
|
||||
|
@ -354,13 +354,18 @@ export class CardRenderer extends React.Component<CardProps> {
|
||||
} = this.props;
|
||||
|
||||
const toolbars: Array<JSX.Element> = [];
|
||||
|
||||
if (header) {
|
||||
const {highlightClassName, highlight: highlightTpl} = header;
|
||||
const highlight = !!evalExpression(highlightTpl!, data as object);
|
||||
if (highlight) {
|
||||
const {highlightClassName, highlight} = header;
|
||||
if (
|
||||
typeof highlight === 'string'
|
||||
? evalExpression(highlight, data)
|
||||
: highlight
|
||||
) {
|
||||
toolbars.push(
|
||||
<i className={cx('Card-highlight', highlightClassName)} />
|
||||
<i
|
||||
key="highlight"
|
||||
className={cx('Card-highlight', highlightClassName)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import Transition, {
|
||||
EXITING
|
||||
} from 'react-transition-group/Transition';
|
||||
import {Renderer, RendererProps} from '../factory';
|
||||
import {resolveVariable} from '../utils/tpl-builtin';
|
||||
import {resolveVariableAndFilter} from '../utils/tpl-builtin';
|
||||
import {
|
||||
autobind,
|
||||
createObject,
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '../utils/helper';
|
||||
import {Action} from '../types';
|
||||
import {Icon} from '../components/icons';
|
||||
import {BaseSchema, SchemaCollection, SchemaName, SchemaTpl} from '../Schema';
|
||||
import {BaseSchema, SchemaCollection, SchemaName} from '../Schema';
|
||||
import Html from '../components/Html';
|
||||
import Image from '../renderers/Image';
|
||||
import {ScopedContext, IScopedContext} from '../Scoped';
|
||||
@ -38,7 +38,7 @@ export interface CarouselSchema extends BaseSchema {
|
||||
/**
|
||||
* 轮播间隔时间
|
||||
*/
|
||||
interval?: number;
|
||||
interval?: number | string;
|
||||
|
||||
/**
|
||||
* 动画时长
|
||||
@ -211,7 +211,13 @@ export class Carousel extends React.Component<CarouselProps, CarouselState> {
|
||||
|
||||
this.clearAutoTimeout();
|
||||
if (this.props.auto) {
|
||||
this.intervalTimeout = setTimeout(this.autoSlide, this.props.interval);
|
||||
const interval = this.props.interval;
|
||||
this.intervalTimeout = setTimeout(
|
||||
this.autoSlide,
|
||||
typeof interval === 'string'
|
||||
? resolveVariableAndFilter(interval, this.props.data) || 5000
|
||||
: interval
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,25 @@
|
||||
import React from 'react';
|
||||
import {IFormStore, IFormItemStore} from '../../store/form';
|
||||
import debouce from 'lodash/debounce';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import {RendererProps, Renderer} from '../../factory';
|
||||
import {ComboStore, IComboStore, IUniqueGroup} from '../../store/combo';
|
||||
import {
|
||||
anyChanged,
|
||||
promisify,
|
||||
isObject,
|
||||
guid,
|
||||
isEmpty,
|
||||
autobind,
|
||||
getVariable,
|
||||
createObject
|
||||
} from '../../utils/helper';
|
||||
import {
|
||||
isNeedFormula,
|
||||
isExpression,
|
||||
FormulaExec,
|
||||
replaceExpression
|
||||
} from '../../utils/formula';
|
||||
import {IIRendererStore, IRendererStore} from '../../store';
|
||||
import {ScopedContext, IScopedContext} from '../../Scoped';
|
||||
import {reaction} from 'mobx';
|
||||
@ -193,12 +199,27 @@ export function wrapControl<
|
||||
combo.bindUniuqueItem(model);
|
||||
}
|
||||
|
||||
// 同步 value
|
||||
model.changeTmpValue(
|
||||
propValue ?? store?.getValueByName(model.name) ?? value
|
||||
);
|
||||
if (propValue !== undefined && propValue !== null) {
|
||||
// 同步 value: 优先使用 props 中的 value
|
||||
model.changeTmpValue(propValue);
|
||||
} else {
|
||||
// 备注: 此处的 value 是 schema 中的 value(和props.defaultValue相同)
|
||||
const curTmpValue = isExpression(value)
|
||||
? FormulaExec['formula'](value, data) // 对组件默认值进行运算
|
||||
: store?.getValueByName(model.name) ?? replaceExpression(value); // 优先使用公式表达式
|
||||
// 同步 value
|
||||
model.changeTmpValue(curTmpValue);
|
||||
|
||||
if (
|
||||
onChange &&
|
||||
value !== undefined &&
|
||||
curTmpValue !== undefined
|
||||
) {
|
||||
// 组件默认值支持表达式需要: 避免初始化时上下文中丢失组件默认值
|
||||
onChange(model.tmpValue, model.name, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有初始值,通过 onChange 设置过去
|
||||
if (
|
||||
onChange &&
|
||||
typeof propValue === 'undefined' &&
|
||||
@ -208,6 +229,7 @@ export function wrapControl<
|
||||
// 对应 issue 为 https://github.com/baidu/amis/issues/2674
|
||||
store?.storeType !== TableStore.name
|
||||
) {
|
||||
// 如果没有初始值,通过 onChange 设置过去
|
||||
onChange(model.tmpValue, model.name, false, true);
|
||||
}
|
||||
}
|
||||
@ -258,6 +280,7 @@ export function wrapControl<
|
||||
'validations',
|
||||
'validationErrors',
|
||||
'value',
|
||||
'defaultValue',
|
||||
'required',
|
||||
'unique',
|
||||
'multiple',
|
||||
@ -301,30 +324,75 @@ export function wrapControl<
|
||||
});
|
||||
}
|
||||
|
||||
// 此处需要同时考虑 defaultValue 和 value
|
||||
if (model && typeof props.value !== 'undefined') {
|
||||
// 自己控制的 value 优先
|
||||
if (
|
||||
props.value !== prevProps.value &&
|
||||
props.value !== model.tmpValue
|
||||
) {
|
||||
// 渲染器中的 value 优先
|
||||
if (props.value !== prevProps.value && props.value !== model.tmpValue) {
|
||||
// 外部直接传入的 value 无需执行运算器
|
||||
model.changeTmpValue(props.value);
|
||||
}
|
||||
} else if (
|
||||
// 然后才是查看关联的 name 属性值是否变化
|
||||
model &&
|
||||
props.data !== prevProps.data &&
|
||||
(!model.emitedValue || model.emitedValue === model.tmpValue)
|
||||
typeof props.defaultValue !== 'undefined' &&
|
||||
isExpression(props.defaultValue)
|
||||
) {
|
||||
model.changeEmitedValue(undefined);
|
||||
const value = getVariable(props.data, model.name);
|
||||
const prevValue = getVariable(prevProps.data, model.name);
|
||||
// 渲染器中的 defaultValue 优先(备注: SchemaRenderer中会将 value 改成 defaultValue)
|
||||
if (
|
||||
(value !== prevValue ||
|
||||
getVariable(props.data, model.name, false) !==
|
||||
getVariable(prevProps.data, model.name, false)) &&
|
||||
value !== model.tmpValue
|
||||
props.defaultValue !== prevProps.defaultValue ||
|
||||
(!isEqual(props.data, prevProps.data) &&
|
||||
isNeedFormula(props.defaultValue, props.data, prevProps.data))
|
||||
) {
|
||||
model.changeTmpValue(value);
|
||||
const curResult = FormulaExec['formula'](
|
||||
props.defaultValue,
|
||||
props.data
|
||||
);
|
||||
const prevResult = FormulaExec['formula'](
|
||||
prevProps.defaultValue,
|
||||
prevProps.data
|
||||
);
|
||||
if (curResult !== prevResult && curResult !== model.tmpValue) {
|
||||
// 识别上下文变动、自身数值变动、公式运算结果变动
|
||||
model.changeTmpValue(curResult);
|
||||
if (props.onChange) {
|
||||
props.onChange(curResult, model.name, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (model) {
|
||||
const valueByName = getVariable(props.data, model.name);
|
||||
|
||||
if (
|
||||
valueByName !== undefined &&
|
||||
props.defaultValue === prevProps.defaultValue
|
||||
) {
|
||||
// value 非公式表达式时,name 值优先,若 defaultValue 主动变动时,则使用 defaultValue
|
||||
if (
|
||||
// 然后才是查看关联的 name 属性值是否变化
|
||||
props.data !== prevProps.data &&
|
||||
(!model.emitedValue || model.emitedValue === model.tmpValue)
|
||||
) {
|
||||
model.changeEmitedValue(undefined);
|
||||
const prevValueByName = getVariable(props.data, model.name);
|
||||
if (
|
||||
(valueByName !== prevValueByName ||
|
||||
getVariable(props.data, model.name, false) !==
|
||||
getVariable(prevProps.data, model.name, false)) &&
|
||||
valueByName !== model.tmpValue
|
||||
) {
|
||||
model.changeTmpValue(valueByName);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
typeof props.defaultValue !== 'undefined' &&
|
||||
props.defaultValue !== prevProps.defaultValue &&
|
||||
props.defaultValue !== model.tmpValue
|
||||
) {
|
||||
// 组件默认值非公式
|
||||
const curValue = replaceExpression(props.defaultValue);
|
||||
model.changeTmpValue(curValue);
|
||||
if (props.onChange) {
|
||||
props.onChange(curValue, model.name, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
240
src/utils/formula.ts
Normal file
240
src/utils/formula.ts
Normal file
@ -0,0 +1,240 @@
|
||||
import isObjectByLodash from 'lodash/isObject';
|
||||
import isString from 'lodash/isString';
|
||||
import isBoolean from 'lodash/isBoolean';
|
||||
import {
|
||||
getVariable,
|
||||
isPureVariable,
|
||||
resolveVariable,
|
||||
resolveVariableAndFilter,
|
||||
evaluate
|
||||
} from 'amis-formula';
|
||||
|
||||
import {filter} from './tpl';
|
||||
import {getFilters} from './tpl-builtin';
|
||||
import {collectVariables} from './grammar';
|
||||
|
||||
/**
|
||||
* formulaExec 运算器:根据当前字符串类型执行对应运算,也可按指定执行模式执行运算
|
||||
*
|
||||
* 运算模式(execMode)支持以下取值:
|
||||
* 1. tpl: 按模板字符串执行(JavaScript 模板引擎),比如:Hello ${amisUser.email}、<h1>Hello</h1>, <span>${amisUser.email}</span>;
|
||||
* 备注: 在模板中可以自由访问变量,详细请见:https://www.lodashjs.com/docs/lodash.template;
|
||||
* 2. formula: 按新版公式表达式执行,用于执行 ${ xxx } 格式的表达式;
|
||||
* 支持从window、localStorage、sessionStorage获取数据,比如:${num1 + 2}、${ls:env}、${window:document}、${window:document.URL}、${amisUser.email};
|
||||
* 详细请见:https://aisuda.bce.baidu.com/amis/zh-CN/docs/concepts/data-mapping#namespace
|
||||
* 3. evalFormula: 按新版公式表达式执行,用于执行 ${ xxx } 和 非${ xxx } 格式的表达式(evalMode 为 true,不用 ${} 包裹也可以执行),功能同 formula 运算模式;
|
||||
* 4. js: 按Javascript执行,表达式中可以通过data.xxx来获取指定数据,并且支持简单运算;
|
||||
* 比如:data.num1 + 2、this.num1 + 2、num1 + 2;(备注:三个表达式是等价的,这里的 this 就是 data。)
|
||||
* 5. var: 以此字符串作为key值从当前数据域data中获取数值;性能最高(运行期间不会生成ast和表达式运算);
|
||||
* 6. true 或者 false: 当execMode设置为true时,不用 ${} 包裹也可以执行表达式;
|
||||
* 7. collect: 用于从表达式中获取所有变量;
|
||||
*
|
||||
* 备注1: 用户也可以使用 registerFormulaExec 注册一个自定义运算器;
|
||||
* 备注2: 模板字符串 和 Javascript 模板引擎 不可以交叉使用;
|
||||
* 备注3: amis 现有的 evalFormula 方法,可执行 ${} 格式类表达式,但不支持 filter 过滤器,所以这里用 resolveValueByName 实现;
|
||||
* 备注4: 后续可考虑将 amis现有的运算器都放这里管理,充当统一的运算器入口。
|
||||
*/
|
||||
|
||||
// 缓存,用于提升性能
|
||||
const FORMULA_EVAL_CACHE: {[key: string]: Function} = {};
|
||||
|
||||
/**
|
||||
* 用于存储当前可用运算器,默认支持 tpl、formula、js、var 四种类型运算器
|
||||
* 备注:在这里统一参数。
|
||||
*/
|
||||
export const FormulaExec: {
|
||||
[key: string]: Function;
|
||||
} = {
|
||||
tpl: (expression: string, data?: object) => {
|
||||
const curData = data || {};
|
||||
return filter(expression, curData);
|
||||
},
|
||||
formula: (expression: string, data?: object) => {
|
||||
// 邮箱格式直接返回,后续需要在 amis-formula 中处理
|
||||
if (
|
||||
/^\$\{([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})\}$/.test(
|
||||
expression
|
||||
)
|
||||
) {
|
||||
return expression.substring(2, expression.length - 1); // 剔除前后特殊字符
|
||||
}
|
||||
const curData = data || {};
|
||||
let result = undefined;
|
||||
try {
|
||||
// 执行 ${} 格式类表达式,且支持 filter 过滤器。(备注: isPureVariable 可用于判断是否有 过滤器。)
|
||||
result = resolveVariableAndFilter(expression, curData, '| raw');
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[formula]表达式执行异常,当前表达式: ',
|
||||
expression,
|
||||
',当前上下文数据: ',
|
||||
data
|
||||
);
|
||||
return expression;
|
||||
}
|
||||
// 备注: 此处不用 result ?? expression 是为了避免 没有对应结果时直接显示 expression: ${xxx}
|
||||
return result;
|
||||
},
|
||||
evalFormula: (expression: string, data?: object) => {
|
||||
const curData = data || {};
|
||||
let result = undefined;
|
||||
try {
|
||||
result = evaluate(expression, curData, {
|
||||
evalMode: true, // evalMode 为 true 时,不用 ${} 包裹也可以执行,
|
||||
allowFilter: false
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[evalFormula]表达式执行异常,当前表达式: ',
|
||||
expression,
|
||||
',当前上下文数据: ',
|
||||
data
|
||||
);
|
||||
return expression;
|
||||
}
|
||||
return result ?? expression;
|
||||
},
|
||||
js: (expression: string, data?: object) => {
|
||||
let debug = false;
|
||||
const idx = expression.indexOf('debugger');
|
||||
if (~idx) {
|
||||
debug = true;
|
||||
expression = expression.replace(/debugger;?/, '');
|
||||
}
|
||||
|
||||
let fn;
|
||||
if (expression in FORMULA_EVAL_CACHE) {
|
||||
fn = FORMULA_EVAL_CACHE[expression];
|
||||
} else {
|
||||
fn = new Function(
|
||||
'data',
|
||||
'utils',
|
||||
`with(data) {${debug ? 'debugger;' : ''}return (${expression});}`
|
||||
);
|
||||
FORMULA_EVAL_CACHE[expression] = fn;
|
||||
}
|
||||
|
||||
data = data || {};
|
||||
|
||||
let curResult = undefined;
|
||||
try {
|
||||
curResult = fn.call(data, data, getFilters());
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[formula:js]表达式执行异常,当前表达式: ',
|
||||
expression,
|
||||
',当前上下文数据: ',
|
||||
data
|
||||
);
|
||||
return expression;
|
||||
}
|
||||
return curResult;
|
||||
},
|
||||
var: (expression: string, data?: object) => {
|
||||
const curData = data || {};
|
||||
const result = getVariable(curData, expression); // 不支持过滤器
|
||||
return result ?? expression;
|
||||
},
|
||||
collect: (expression: any) => {
|
||||
let variables: Array<string> = [];
|
||||
if (isObjectByLodash(expression) || isString(expression)) {
|
||||
// 仅对 Object类型 和 String类型 进行变量提取
|
||||
variables = collectVariables(expression);
|
||||
} else {
|
||||
variables = [];
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
};
|
||||
|
||||
// 根据表达式类型自动匹配指定运算器,也可以通过设置 execMode 直接指定运算器
|
||||
export function formulaExec(
|
||||
value: any,
|
||||
data: any,
|
||||
execMode?: string | boolean
|
||||
) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
let OpenFormulaExecEvalMode = false;
|
||||
let curExecMode = '';
|
||||
if (isBoolean(execMode)) {
|
||||
// OpenFormulaExecEvalMode 设置为 true 后,非 ${ xxx } 格式也使用表达式运算器
|
||||
OpenFormulaExecEvalMode = execMode;
|
||||
} else if (isString(execMode)) {
|
||||
// 指定 execMode 可以直接选用对应的运算器
|
||||
curExecMode = execMode;
|
||||
}
|
||||
if (!isString(value)) {
|
||||
// 非字符串类型,直接返回,比如:boolean、number类型、Object、Array类型
|
||||
return value;
|
||||
} else if (curExecMode && FormulaExec[curExecMode]) {
|
||||
return FormulaExec[curExecMode];
|
||||
}
|
||||
|
||||
const curValue = value.trim(); // 剔除前后空格
|
||||
|
||||
// OpenFormulaExecEvalMode 为 true 时,非 ${ xxx } 格式也会尝试使用表达式运算器
|
||||
if (OpenFormulaExecEvalMode && /^[0-9a-zA-z_]+$/.test(curValue)) {
|
||||
// 普通字符串类型(非表达式),先试一下从上下文中获取数据
|
||||
const curValueTemp = FormulaExec['var'](curValue, data);
|
||||
// 备注: 其他特殊格式,比如邮箱、日期
|
||||
return curValueTemp ?? curValue;
|
||||
} else if (curValue.startsWith('${') && curValue.endsWith('}')) {
|
||||
// ${ xxx } 格式 使用 formula 运算器
|
||||
return FormulaExec['formula'](curValue, data);
|
||||
} else if (isExpression(test)) {
|
||||
// 表达式格式使用 formula 运算器
|
||||
return FormulaExec['formula'](curValue, data);
|
||||
} else if (/(\${).+(\})/.test(curValue)) {
|
||||
// 包含 ${ xxx } 则使用 tpl 运算器
|
||||
return FormulaExec['tpl'](curValue, data); // 可识别 <% %> 语法
|
||||
} else if (OpenFormulaExecEvalMode) {
|
||||
// 支持 ${} 和 非 ${} 表达式
|
||||
return FormulaExec['evalFormula'](curValue, data);
|
||||
} else {
|
||||
return curValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于注册自定义 formulaExec 运算器
|
||||
export function registerFormulaExec(execMode: string, formulaExec: Function) {
|
||||
if (FormulaExec[execMode]) {
|
||||
console.error(
|
||||
`registerFormulaExec: 运算器注册失败,存在同名运算器($(execMode))。`
|
||||
);
|
||||
} else {
|
||||
FormulaExec[execMode] = formulaExec;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于判断是否优先使用value。
|
||||
export function isExpression(expression: any): boolean {
|
||||
if (!isString(expression)) {
|
||||
// 非字符串类型,比如:Object、Array类型、boolean、number类型
|
||||
return false;
|
||||
}
|
||||
// 备注: "\\${xxx}"不作为表达式,至少含一个${xxx}才算是表达式
|
||||
return /(?<!\\)(\${).+(\})/.test(expression);
|
||||
}
|
||||
|
||||
// 用于判断是否需要执行表达式:
|
||||
export function isNeedFormula(
|
||||
expression: any,
|
||||
prevData: {[propName: string]: any},
|
||||
curData: {[propName: string]: any}
|
||||
): boolean {
|
||||
const variables = FormulaExec.collect(expression);
|
||||
return variables.some(
|
||||
(variable: string) =>
|
||||
FormulaExec.var(variable, prevData) !== FormulaExec.var(variable, curData)
|
||||
);
|
||||
}
|
||||
|
||||
// 将 \${xx} 替换成 ${xx}
|
||||
export function replaceExpression(expression: any): any {
|
||||
if (expression && isString(expression) && /(\\)(\${).+(\})/.test(expression)) {
|
||||
return expression.replace(/\\\$\{/g, '${');
|
||||
}
|
||||
return expression;
|
||||
}
|
53
src/utils/grammar.ts
Normal file
53
src/utils/grammar.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file 公式语法解析
|
||||
*/
|
||||
|
||||
import {parse} from 'amis-formula';
|
||||
|
||||
function traverseAst(ast: any, iterator: (ast: any) => void) {
|
||||
if (!ast || !ast.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
iterator(ast);
|
||||
|
||||
Object.keys(ast).forEach(key => {
|
||||
const value = ast[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(child => traverseAst(child, iterator));
|
||||
} else {
|
||||
traverseAst(value, iterator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 缓存,用于提升性能
|
||||
const COLLECT_EXPRESSION_CACHE: {[key: string]: Array<string>} = {};
|
||||
|
||||
// 提取表达式中有哪些变量
|
||||
export function collectVariables(strOrAst: string | Object, execMode?: boolean): Array<string> {
|
||||
const variables: Array<string> = [];
|
||||
|
||||
if (typeof strOrAst === 'string' && COLLECT_EXPRESSION_CACHE[strOrAst]) {
|
||||
return COLLECT_EXPRESSION_CACHE[strOrAst];
|
||||
}
|
||||
const ast =
|
||||
typeof strOrAst === 'string'
|
||||
? parse(strOrAst, {
|
||||
evalMode: execMode ?? false
|
||||
})
|
||||
: strOrAst;
|
||||
|
||||
traverseAst(ast, (item: any) => {
|
||||
if (item.type === 'variable') {
|
||||
variables.push(item.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof strOrAst === 'string') {
|
||||
COLLECT_EXPRESSION_CACHE[strOrAst] = variables;
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
@ -20,7 +20,7 @@ import {
|
||||
keyToPath,
|
||||
isPureVariable,
|
||||
resolveVariable,
|
||||
resolveVariableAndFilter
|
||||
resolveVariableAndFilter,
|
||||
} from 'amis-formula';
|
||||
import {isObservable} from 'mobx';
|
||||
|
||||
@ -1424,6 +1424,7 @@ export function getScrollbarWidth() {
|
||||
return scrollbarWidth;
|
||||
}
|
||||
|
||||
// 后续改用 FormulaExec['formula']
|
||||
function resolveValueByName(data: any, name?: string) {
|
||||
return isPureVariable(name)
|
||||
? resolveVariableAndFilter(name, data)
|
||||
|
@ -71,6 +71,7 @@ export function evalExpression(expression: string, data?: object): boolean {
|
||||
return evalFormula(expression, data);
|
||||
}
|
||||
|
||||
// 后续改用 FormulaExec['js']
|
||||
let debug = false;
|
||||
const idx = expression.indexOf('debugger');
|
||||
if (~idx) {
|
||||
@ -149,6 +150,7 @@ export function evalJS(js: string, data: object): any {
|
||||
}
|
||||
|
||||
[registerBulitin, registerLodash].forEach(fn => {
|
||||
if (!fn) return;
|
||||
const info = fn();
|
||||
|
||||
registerTplEnginer(info.name, {
|
||||
|
Loading…
Reference in New Issue
Block a user