Merge branch 'baidu:master' into feat-fileName

This commit is contained in:
lengshengren 2022-05-19 15:18:38 +08:00 committed by GitHub
commit dd6188c50f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1382 additions and 53 deletions

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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",
}
`;

View File

@ -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

View File

@ -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>
`;

View File

@ -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();

View 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');
});
});

View File

@ -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

View File

@ -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}

View File

@ -1036,6 +1036,10 @@ body.dark {
color: #fff;
}
&-edit-btn {
font-size: 14px;
}
&-preview {
position: relative;
display: flex;

View File

@ -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,

View File

@ -94,7 +94,7 @@ const STYLE_ITEM: {
position: 'absolute' as ItemPosition,
top: 0,
left: 0,
width: '100%'
width: 'auto'
};
const STYLE_STICKY_ITEM = {

View File

@ -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)}
/>
);
}
}

View File

@ -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
);
}
}

View File

@ -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
View 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 }
* windowlocalStoragesessionStorage获取数据${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 + 2this.num1 + 2num1 + 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} = {};
/**
* tplformulajsvar
*
*/
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
View 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;
}

View File

@ -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)

View File

@ -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, {