Merge branch 'baidu:master' into feat-unit-test-721

This commit is contained in:
sansiro 2022-07-25 19:14:14 +08:00 committed by GitHub
commit 950dfc012d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 3409 additions and 1633 deletions

View File

@ -73,6 +73,9 @@ npm run update-snapshot
# 先通过一下命令设置版本号
npm run version
# 如果是 beta 版本使用如下命令
# npm run version -- 2.0.1-beta.0 --no-git-tag-version
# 发布内部 registry
npm run publish-to-internal

View File

@ -26,6 +26,7 @@ order: 31
"type": "button",
"label": "编辑",
"actionType": "dialog",
"icon": "fa fa-pencil",
"dialog": {
"title": "编辑",
"body": "你正在编辑该卡片"
@ -34,6 +35,7 @@ order: 31
{
"type": "button",
"label": "删除",
"icon": "fa fa-trash",
"actionType": "dialog",
"dialog": {
"title": "提示",

View File

@ -18,6 +18,7 @@ order: 55
"type": "input-tag",
"name": "tag",
"label": "标签",
"placeholder": "请选择标签",
"options": [
"Aaron Rodgers",
"Tom Brady",

View File

@ -165,6 +165,7 @@ order: 60
{
"type": "page",
"data": {
"title": "title1",
"items": [
{
"label": "cpu",
@ -196,6 +197,7 @@ order: 60
},
"body": {
"type": "property",
"title": "${title}",
"source": "${items}"
}
}
@ -372,6 +374,8 @@ items 里的属性还支持 `visibleOn` 和 `hiddenOn` 表达式,能隐藏部
| column | `number` | 3 | 每行几列 |
| mode | `string` | 'table' | 显示模式,目前只有 'table' 和 'simple' |
| separator | `string` | ',' | 'simple' 模式下属性名和值之间的分隔符 |
| title | `string` | | 标题 |
| source | `string` | | 数据源 |
| items[].label | `SchemaTpl` | | 属性名 |
| items[].content | `SchemaTpl` | | 属性值 |
| items[].span | `SchemaTpl` | | 属性值跨几列 |

View File

@ -95,7 +95,12 @@ order: 9
### 发送 http 请求
通过配置`actionType: 'ajax'`和`api`实现 http 请求发送,该动作需实现 `env.fetcher` 请求器。请求结果的状态、数据、消息分别默认缓存在 `event.data.responseStatus`、`event.data.responseData`或`event.data.{{outputVar}}`、`event.data.responseMsg`。< 2.0.3 以以下版本请求返回数据默认缓存在 `event.data`。`outputVar` 配置用于解决串行或者并行发送多个 http 请求的场景
通过配置`actionType: 'ajax'`和`api`实现 http 请求发送,该动作需实现 `env.fetcher` 请求器。
- 请求结果缓存在`event.data.responseResult`或`event.data.{{outputVar}}`。
- 请求结果的状态、数据、消息分别默认缓存在:`event.data.{{outputVar}}.responseStatus`、`event.data.{{outputVar}}.responseData`、`event.data.{{outputVar}}.responseMsg`。
< 2.0.3 及以下版本请求返回数据默认缓存在 `event.data`。`outputVar` 配置用于解决串行或者并行发送多个 http 请求的场景
```schema
{
@ -129,7 +134,7 @@ order: 9
},
{
actionType: 'toast',
expression: '${event.data.responseStatus === 0}',
expression: '${event.data.responseResult.responseStatus === 0}',
args: {
msg: '${event.data|json}'
}
@ -167,7 +172,7 @@ order: 9
},
{
actionType: 'toast',
expression: '${event.data.responseStatus === 0}',
expression: '${event.data.responseResult.responseStatus === 0}',
args: {
msg: '${event.data|json}'
}
@ -2300,7 +2305,7 @@ registerAction('my-action', new MyAction());
**引用 http 请求动作返回的数据**
http 请求动作执行结束后,后面的动作可以通过 `event.data.responseStatus`、`event.data.responseData`或`event.data.{{outputVar}}`、`event.data.responseMsg`来获取请求结果的状态、数据、消息。
http 请求动作执行结束后,后面的动作可以通过 `event.data.responseResult.responseStatus`或`event.data.{{outputVar}}.responseStatus`、`event.data.responseResult.responseData`或`event.data.{{outputVar}}.responseData`、`event.data.responseResult.responseMsg`或`event.data.{{outputVar}}.responseMsg`来获取请求结果的状态、数据、消息。
```schema
{
@ -2322,7 +2327,7 @@ http 请求动作执行结束后,后面的动作可以通过 `event.data.respo
{
actionType: 'dialog',
args: {
id: '${event.data.responseData.id}'
id: '${event.data.responseResult.responseData.id}'
},
dialog: {
type: 'dialog',

View File

@ -58,7 +58,6 @@ export class AjaxAction implements RendererAction {
omit(action.args ?? {}, ['api', 'options', 'messages']),
action.args?.options ?? {}
);
const responseData =
!isEmpty(result.data) || result.ok
? normalizeApiResponseData(result.data)
@ -68,9 +67,13 @@ export class AjaxAction implements RendererAction {
event.setData(
createObject(event.data, {
...responseData, // 兼容历史配置
[action.outputVar || 'responseData']: responseData,
responseStatus: result.status,
responseMsg: result.msg
responseData: responseData,
[action.outputVar || 'responseResult']: {
...responseData,
responseData,
responseStatus: result.status,
responseMsg: result.msg
}
})
);

View File

@ -166,6 +166,10 @@
text-decoration: none;
}
}
& > .#{$ns}Button-icon:first-child:not(:last-child) {
margin-right: var(--Button-icon-rightMargin);
}
}
}

View File

@ -263,8 +263,11 @@ export class ResultBox extends React.Component<ResultBoxProps> {
value={value || ''}
onChange={this.handleChange}
placeholder={__(
Array.isArray(result) && result.length
? inputPlaceholder
/** 数组模式下输入内容后将不再展示placeholder */
Array.isArray(result)
? result.length > 0
? inputPlaceholder
: placeholder
: result
? ''
: placeholder

View File

@ -683,7 +683,11 @@ export class Select extends React.Component<SelectProps, SelectState> {
@autobind
handleKeyPress(e: React.KeyboardEvent) {
if (this.props.multiple && e.key === ' ') {
/**
* label/value中有空格的case
* 使 winshift + spacemacshift + space
*/
if (e.key === ' ' && e.shiftKey) {
this.toggle();
e.preventDefault();
}

View File

@ -628,46 +628,54 @@ export class Transfer<
const placeholder =
resultSearchPlaceholder || __('Transfer.selectFromLeft');
return resultSelectMode === 'table' ? (
<ResultTableList
classnames={cx}
columns={columns!}
options={options || []}
value={value}
disabled={disabled}
option2value={option2value}
cellRender={cellRender}
onChange={onChange}
multiple={false}
searchable={searchable}
placeholder={placeholder}
onSearch={onResultSearch}
/>
) : resultSelectMode === 'tree' ? (
<ResultTreeList
classnames={cx}
options={options}
valueField={'value'}
value={value || []}
onChange={onChange!}
itemRender={resultItemRender}
searchable={searchable}
placeholder={placeholder}
onSearch={onResultSearch}
/>
) : (
<ResultList
className={cx('Transfer-value')}
sortable={sortable}
disabled={disabled}
value={value}
onChange={onChange}
placeholder={placeholder}
itemRender={resultItemRender}
searchable={searchable}
onSearch={onResultSearch}
/>
);
switch (resultSelectMode) {
case 'table':
return (
<ResultTableList
classnames={cx}
columns={columns!}
options={options || []}
value={value}
disabled={disabled}
option2value={option2value}
cellRender={cellRender}
onChange={onChange}
multiple={false}
searchable={searchable}
placeholder={placeholder}
onSearch={onResultSearch}
/>
);
case 'tree':
return (
<ResultTreeList
classnames={cx}
className={cx('Transfer-value')}
options={options}
valueField={'value'}
value={value || []}
onChange={onChange!}
itemRender={resultItemRender}
searchable={searchable}
placeholder={placeholder}
onSearch={onResultSearch}
/>
);
default:
return (
<ResultList
className={cx('Transfer-value')}
sortable={sortable}
disabled={disabled}
value={value}
onChange={onChange}
placeholder={placeholder}
itemRender={resultItemRender}
searchable={searchable}
onSearch={onResultSearch}
/>
);
}
}
render() {

View File

@ -172,12 +172,10 @@ export class FormulaPlugin {
eachTree(variables, item => {
if (item.value) {
const key = `\${${item.value}}`;
varMap[key] = item.label;
varMap[item.value] = item.label;
}
});
const vars = Object.keys(varMap).sort((a, b) => b.length - a.length);
const editor = this.editor;
const lines = editor.lineCount();
for (let line = 0; line < lines; line++) {

View File

@ -231,7 +231,7 @@ register('zh-CN', {
'Table.index': '序号',
'Table.toggleColumn': '显示列',
'Table.searchFields': '设置查询字段',
'Tag.placeholder': '暂无标签',
'Tag.placeholder': '请输入/选择标签',
'Tag.tip': '最近使用的标签',
'Text.add': '新增:{{label}}',
'Time.placeholder': '请选择时间',

View File

@ -49,7 +49,7 @@ test('EventAction:ajax', async () => {
actionType: 'setValue',
componentId: 'page_001',
args: {
value: '${event.data.result}'
value: '${event.data.result.responseData}'
}
}
]
@ -93,7 +93,7 @@ test('EventAction:ajax', async () => {
},
{
type: 'tpl',
tpl: '${responseData.age}岁的天空status:${responseStatus}msg:${responseMsg}'
tpl: '${responseResult.responseData.age}岁的天空status:${responseResult.responseStatus}msg:${responseResult.responseMsg}'
}
]
},

View File

@ -40,7 +40,7 @@ test('EventAction:custom', async () => {
actionType: 'setValue',
componentId: 'page_001',
args: {
value: '${event.data.result}'
value: '${event.data.result.responseData}'
}
}
]
@ -63,7 +63,7 @@ test('EventAction:custom', async () => {
actionType: 'setValue',
componentId: 'page_001',
args: {
value: '${event.data.result}'
value: '${event.data.result.responseData}'
}
}
]

View File

@ -1,11 +1,17 @@
import React = require('react');
import {render, waitFor} from '@testing-library/react';
import {cleanup, fireEvent, render, waitFor} from '@testing-library/react';
import '../../src';
import {render as amisRender} from '../../src';
import {clearStoresCache, render as amisRender} from '../../src';
import {makeEnv, wait} from '../helper';
import rows from '../mockData/rows';
const fetcher = async (config: any) => {
afterEach(() => {
cleanup();
clearStoresCache();
jest.useRealTimers();
});
async function fetcher(config: any) {
return {
status: 200,
headers: {},
@ -18,10 +24,11 @@ const fetcher = async (config: any) => {
}
}
};
};
}
test('Renderer:crud', async () => {
const {container, getByText} = render(
test('Renderer:crud basic interval headerToolbar footerToolbar', async () => {
const mockFetcher = jest.fn(fetcher);
const {container} = render(
amisRender(
{
type: 'page',
@ -29,6 +36,10 @@ test('Renderer:crud', async () => {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
interval: 1000,
perPage: 2,
headerToolbar: ['export-excel', 'statistics'],
footerToolbar: ['pagination', 'export-excel'],
columns: [
{
name: '__id',
@ -41,34 +52,52 @@ test('Renderer:crud', async () => {
{
name: 'browser',
label: 'Browser'
},
{
name: 'platform',
label: 'Platform(s)'
},
{
name: 'version',
label: 'Engine version'
},
{
name: 'grade',
label: 'CSS grade'
}
]
}
},
{},
makeEnv({fetcher})
makeEnv({fetcher: mockFetcher})
)
);
await waitFor(() => {
expect(getByText('Internet Explorer 4.0')).toBeInTheDocument();
expect(
container.querySelector('[data-testid="spinner"]')
).not.toBeInTheDocument();
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
expect(container).toMatchSnapshot();
await wait(1001);
expect(mockFetcher).toHaveBeenCalledTimes(2);
});
test('Renderer:crud stopAutoRefreshWhen', async () => {
const mockFetcher = jest.fn(fetcher);
render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
interval: 1000,
stopAutoRefreshWhen: 'true',
columns: [
{
name: '__id',
label: 'ID'
}
]
}
},
{},
makeEnv({fetcher: mockFetcher})
)
);
await wait(1001);
expect(mockFetcher).toHaveBeenCalledTimes(1);
});
test('Renderer:crud loadDataOnce', async () => {
@ -114,7 +143,9 @@ test('Renderer:crud loadDataOnce', async () => {
)
);
await wait(300);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
expect(container.querySelector('.cxd-Crud-pager')).not.toBeInTheDocument();
});
@ -141,7 +172,9 @@ test('Renderer:crud list', async () => {
)
);
expect(container).toMatchSnapshot();
await wait(300);
await waitFor(() => {
expect(container.querySelectorAll('.cxd-ListItem').length > 5).toBeTruthy();
});
expect(container).toMatchSnapshot();
});
@ -182,12 +215,14 @@ test('Renderer:crud cards', async () => {
);
expect(container).toMatchSnapshot();
await wait(300);
await waitFor(() =>
expect(container.querySelector('.cxd-Card-title')).toBeInTheDocument()
);
expect(container).toMatchSnapshot();
});
test('Renderer:crud [source]', async () => {
const {container, getByText} = render(
test('Renderer:crud source & alwaysShowPagination', async () => {
const {container} = render(
amisRender(
{
type: 'page',
@ -197,6 +232,7 @@ test('Renderer:crud [source]', async () => {
body: {
type: 'crud',
source: 'fields',
alwaysShowPagination: true,
columns: [
{
name: '__id',
@ -221,6 +257,7 @@ test('Renderer:crud [source]', async () => {
});
test('Renderer:crud filter', async () => {
const mockFetcher = jest.fn(fetcher);
const {container} = render(
amisRender(
{
@ -228,6 +265,9 @@ test('Renderer:crud filter', async () => {
body: {
type: 'crud',
api: '/api/mock2/sample',
defaultParams: {defaultValue: 'defaultValue'},
pageField: 'customPageField',
perPageField: 'customPerPageField',
filter: {
title: '条件搜索',
body: [
@ -269,15 +309,392 @@ test('Renderer:crud filter', async () => {
}
},
{},
makeEnv({fetcher: mockFetcher})
)
);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
const {query} = mockFetcher.mock.calls[0][0];
expect(query.defaultValue).toBe('defaultValue');
expect(query.keywords).toBe('123');
expect(query.customPageField).toBe(1);
expect(query.customPerPageField).toBe(10);
});
test('Renderer:crud draggable & itemDraggableOn', async () => {
const {container} = render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
draggable: true,
itemDraggableOn: '${__id !== 1}',
columns: [
{
name: '__id',
label: 'ID'
},
{
name: 'engine',
label: 'Rendering engine'
},
{
name: 'browser',
label: 'Browser'
},
{
name: 'platform',
label: 'Platform(s)'
},
{
name: 'version',
label: 'Engine version'
},
{
name: 'grade',
label: 'CSS grade'
}
]
}
},
{},
makeEnv({fetcher})
)
);
await waitFor(() => {
expect(container.querySelector('[icon="exchange"]')).toBeInTheDocument();
});
fireEvent.click(container.querySelector('[icon="exchange"]')!);
await waitFor(() => {
expect(container.querySelector('[icon=drag]')).toBeInTheDocument();
});
expect(container.querySelectorAll('[icon=drag]').length).toBe(9);
});
test('Renderer:crud quickEdit quickSaveApi', async () => {
const mockFetcher = jest.fn(fetcher);
const {container, getAllByText} = render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
stopAutoRefreshWhen: 'true',
quickSaveApi: '/api/mock2/sample/bulkUpdate',
columns: [
{
name: '__id',
label: 'ID'
},
{
name: 'engine',
label: 'Rendering engine',
quickEdit: true
},
{
name: 'browser',
label: 'Browser',
quickEdit: {
saveImmediately: true
}
}
]
}
},
{},
makeEnv({fetcher: mockFetcher})
)
);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
fireEvent.click(container.querySelector('.cxd-Field-quickEditBtn')!);
await waitFor(() => {
expect(container.querySelector('input[name="engine"]')).toBeInTheDocument();
});
fireEvent.change(container.querySelector('input[name="engine"]')!, {
target: {value: 'xxx'}
});
fireEvent.click(container.querySelector('button[type="submit"]')!);
await waitFor(() => {
expect(
container.querySelector('input[name="engine"]')
).not.toBeInTheDocument();
});
fireEvent.click(getAllByText('提交')[0]!);
await wait(10);
// * 提交后会调用一次 quickSaveApi 和 api
expect(mockFetcher).toBeCalledTimes(3);
});
test('Renderer:crud quickSaveItemApi saveImmediately', async () => {
const mockFetcher = jest.fn(fetcher);
const {container, getAllByText} = render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
stopAutoRefreshWhen: 'true',
quickSaveItemApi: '/api/mock2/sample/$id',
hideQuickSaveBtn: true,
columns: [
{
name: '__id',
label: 'ID'
},
{
name: 'browser',
label: 'Browser',
quickEdit: {
saveImmediately: true
}
}
]
}
},
{},
makeEnv({fetcher: mockFetcher})
)
);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
fireEvent.click(container.querySelector('.cxd-Field-quickEditBtn')!);
await waitFor(() => {
expect(
container.querySelector('input[name="browser"]')
).toBeInTheDocument();
});
fireEvent.change(container.querySelector('input[name="browser"]')!, {
target: {value: 'xxx'}
});
fireEvent.click(container.querySelector('button[type="submit"]')!);
await waitFor(() => {
expect(
container.querySelector('input[name="browser"]')
).not.toBeInTheDocument();
});
// * 提交后会调用一次 quickSaveItemApi 和 api
expect(mockFetcher.mock.calls[1][0].url).toBe('/api/mock2/sample/');
expect(mockFetcher).toBeCalledTimes(3);
});
test('Renderer:crud bulkActions', async () => {
const {container} = render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
bulkActions: [
{
label: '批量删除',
actionType: 'ajax',
api: 'delete:/amis/api/mock2/sample/${ids|raw}',
confirmText: '确定要批量删除?'
}
],
columns: [
{
name: '__id',
label: 'ID'
},
{
name: 'engine',
label: 'Rendering engine',
quickEdit: true
},
{
name: 'browser',
label: 'Browser',
quickEdit: {
saveImmediately: true
}
}
]
}
},
{},
makeEnv({fetcher})
)
);
// await wait(300);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
expect(
container.querySelector('.cxd-Button.is-disabled')
).toBeInTheDocument();
fireEvent.click(
container.querySelector('.cxd-Table-checkCell input[type="checkbox"]')!
);
await waitFor(() => {
expect(
container.querySelector('[data-testid="spinner"]')
container.querySelector('.cxd-Button.is-disabled')
).not.toBeInTheDocument();
});
});
test('Renderer: crud sortable & orderBy & orderDir & orderField', async () => {
const mockFetcher = jest.fn(fetcher);
const {container} = render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
orderBy: 'id',
orderDir: 'desc',
syncLocation: false,
orderField: 'xxx',
columns: [
{
name: '__id',
label: 'ID',
sortable: true
},
{
name: 'engine',
label: 'Rendering engine',
quickEdit: true
}
]
}
},
{},
makeEnv({fetcher: mockFetcher})
)
);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
expect(mockFetcher.mock.calls[0][0].query).toEqual({
orderBy: 'id',
orderDir: 'desc',
page: 1,
perPage: 10
});
expect(container).toMatchSnapshot();
});
test('Renderer: crud keepItemSelectionOnPageChange & maxKeepItemSelectionLength & labelTpl', async () => {
const mockFetcher = jest.fn(fetcher);
const {container} = render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
keepItemSelectionOnPageChange: true,
maxKeepItemSelectionLength: 4,
labelTpl: '${id}${engine}',
bulkActions: [
{
label: '批量删除',
actionType: 'ajax',
api: ''
}
],
columns: [
{
name: '__id',
label: 'ID',
sortable: true
},
{
name: 'engine',
label: 'Rendering engine',
quickEdit: true
}
]
}
},
{},
makeEnv({fetcher: mockFetcher})
)
);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
// 点击全部
fireEvent.click(container.querySelector('th input[type="checkbox"]')!);
await waitFor(() => {
expect(
container.querySelectorAll('.cxd-Crud-selection>.cxd-Crud-value').length
).toBe(4);
});
expect(container).toMatchSnapshot();
});
test('Renderer: crud autoGenerateFilter', async () => {
const mockFetcher = jest.fn(fetcher);
const {container} = render(
amisRender(
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample',
syncLocation: false,
autoGenerateFilter: true,
bulkActions: [
{
label: '批量删除',
actionType: 'ajax',
api: ''
}
],
columns: [
{
name: '__id',
label: 'ID',
sortable: true,
searchable: {
type: 'input-text',
name: 'id',
label: '主键',
placeholder: '输入id'
}
},
{
name: 'engine',
label: 'Rendering engine',
quickEdit: true
}
]
}
},
{},
makeEnv({fetcher: mockFetcher})
)
);
await waitFor(() => {
expect(container.querySelectorAll('tbody>tr').length > 5).toBeTruthy();
});
expect(
container.querySelector('input[name="id"][placeholder="输入id"]')
).toBeInTheDocument();
});

View File

@ -1,123 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renderer:InputTag InputTag input with batch tag 1`] = `
<div>
<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>
标签
</span>
</span>
</span>
</label>
<div
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="downshift-2-label"
aria-owns="downshift-2-menu"
class="cxd-Form-control cxd-TagControl"
role="combobox"
>
<div
class="cxd-ResultBox cxd-TagControl-input is-focused is-group"
tabindex="-1"
>
<div
class="cxd-ResultBox-value-wrap"
>
<div
class="cxd-ResultBox-value"
>
<span
class="cxd-ResultBox-valueLabel"
>
Apple
</span>
<a
data-index="0"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<div
class="cxd-ResultBox-value"
>
<span
class="cxd-ResultBox-valueLabel"
>
Orange
</span>
<a
data-index="1"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<div
class="cxd-ResultBox-value"
>
<span
class="cxd-ResultBox-valueLabel"
>
Banana
</span>
<a
data-index="2"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<input
aria-autocomplete="list"
aria-controls="downshift-2-menu"
aria-labelledby="downshift-2-label"
autocomplete="off"
class="cxd-ResultBox-value-input"
id="downshift-2-input"
name="tag"
placeholder="请输入"
theme="cxd"
type="text"
value=""
/>
</div>
<div
class="cxd-ResultBox-actions"
/>
</div>
</div>
</div>
</form>
</div>
`;
exports[`Renderer:InputTag InputTag input with batch tag and separator "|" 1`] = `
<div>
<form
class="cxd-Form cxd-Form--normal"
@ -218,7 +101,7 @@ exports[`Renderer:InputTag InputTag input with batch tag and separator "|" 1`] =
class="cxd-ResultBox-value-input"
id="downshift-3-input"
name="tag"
placeholder="请输入"
placeholder=""
theme="cxd"
type="text"
value=""
@ -234,7 +117,7 @@ exports[`Renderer:InputTag InputTag input with batch tag and separator "|" 1`] =
</div>
`;
exports[`Renderer:InputTag InputTag input with max quantity 2 1`] = `
exports[`Renderer:InputTag InputTag input with batch tag and separator "|" 1`] = `
<div>
<form
class="cxd-Form cxd-Form--normal"
@ -262,9 +145,10 @@ exports[`Renderer:InputTag InputTag input with max quantity 2 1`] = `
</span>
</label>
<div
aria-expanded="false"
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="downshift-4-label"
aria-owns="downshift-4-menu"
class="cxd-Form-control cxd-TagControl"
role="combobox"
>
@ -309,17 +193,35 @@ exports[`Renderer:InputTag InputTag input with max quantity 2 1`] = `
/>
</a>
</div>
<div
class="cxd-ResultBox-value"
>
<span
class="cxd-ResultBox-valueLabel"
>
Banana
</span>
<a
data-index="2"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<input
aria-autocomplete="list"
aria-controls="downshift-4-menu"
aria-labelledby="downshift-4-label"
autocomplete="off"
class="cxd-ResultBox-value-input"
id="downshift-4-input"
name="tag"
placeholder="请输入"
placeholder=""
theme="cxd"
type="text"
value="Banana"
value=""
/>
</div>
<div
@ -332,7 +234,7 @@ exports[`Renderer:InputTag InputTag input with max quantity 2 1`] = `
</div>
`;
exports[`Renderer:InputTag InputTag input with maxTagLength 5 1`] = `
exports[`Renderer:InputTag InputTag input with max quantity 2 1`] = `
<div>
<form
class="cxd-Form cxd-Form--normal"
@ -390,6 +292,23 @@ exports[`Renderer:InputTag InputTag input with maxTagLength 5 1`] = `
/>
</a>
</div>
<div
class="cxd-ResultBox-value"
>
<span
class="cxd-ResultBox-valueLabel"
>
Orange
</span>
<a
data-index="1"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<input
aria-autocomplete="list"
aria-labelledby="downshift-5-label"
@ -397,7 +316,88 @@ exports[`Renderer:InputTag InputTag input with maxTagLength 5 1`] = `
class="cxd-ResultBox-value-input"
id="downshift-5-input"
name="tag"
placeholder="请输入"
placeholder=""
theme="cxd"
type="text"
value="Banana"
/>
</div>
<div
class="cxd-ResultBox-actions"
/>
</div>
</div>
</div>
</form>
</div>
`;
exports[`Renderer:InputTag InputTag input with maxTagLength 5 1`] = `
<div>
<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>
标签
</span>
</span>
</span>
</label>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="downshift-6-label"
class="cxd-Form-control cxd-TagControl"
role="combobox"
>
<div
class="cxd-ResultBox cxd-TagControl-input is-focused is-group"
tabindex="-1"
>
<div
class="cxd-ResultBox-value-wrap"
>
<div
class="cxd-ResultBox-value"
>
<span
class="cxd-ResultBox-valueLabel"
>
Apple
</span>
<a
data-index="0"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<input
aria-autocomplete="list"
aria-labelledby="downshift-6-label"
autocomplete="off"
class="cxd-ResultBox-value-input"
id="downshift-6-input"
name="tag"
placeholder=""
theme="cxd"
type="text"
value="Banana"
@ -443,8 +443,8 @@ exports[`Renderer:InputTag InputTag input with single tag 1`] = `
<div
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="downshift-1-label"
aria-owns="downshift-1-menu"
aria-labelledby="downshift-2-label"
aria-owns="downshift-2-menu"
class="cxd-Form-control cxd-TagControl"
role="combobox"
>
@ -474,13 +474,13 @@ exports[`Renderer:InputTag InputTag input with single tag 1`] = `
</div>
<input
aria-autocomplete="list"
aria-controls="downshift-1-menu"
aria-labelledby="downshift-1-label"
aria-controls="downshift-2-menu"
aria-labelledby="downshift-2-label"
autocomplete="off"
class="cxd-ResultBox-value-input"
id="downshift-1-input"
id="downshift-2-input"
name="tag"
placeholder="请输入"
placeholder=""
theme="cxd"
type="text"
value=""
@ -526,8 +526,8 @@ exports[`Renderer:InputTag InputTag with options 1`] = `
<div
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="downshift-0-label"
aria-owns="downshift-0-menu"
aria-labelledby="downshift-1-label"
aria-owns="downshift-1-menu"
class="cxd-Form-control cxd-TagControl has-popover"
role="combobox"
>
@ -541,13 +541,13 @@ exports[`Renderer:InputTag InputTag with options 1`] = `
>
<input
aria-autocomplete="list"
aria-controls="downshift-0-menu"
aria-labelledby="downshift-0-label"
aria-controls="downshift-1-menu"
aria-labelledby="downshift-1-label"
autocomplete="off"
class="cxd-ResultBox-value-input"
id="downshift-0-input"
id="downshift-1-input"
name="tag"
placeholder=""
placeholder="请输入/选择标签"
theme="cxd"
type="text"
value=""
@ -610,7 +610,7 @@ exports[`Renderer:InputTag InputTag with options 1`] = `
<div
aria-selected="false"
class="cxd-ListMenu-item"
id="downshift-0-item-0"
id="downshift-1-item-0"
role="option"
>
<div
@ -622,7 +622,7 @@ exports[`Renderer:InputTag InputTag with options 1`] = `
<div
aria-selected="false"
class="cxd-ListMenu-item"
id="downshift-0-item-1"
id="downshift-1-item-1"
role="option"
>
<div
@ -634,7 +634,7 @@ exports[`Renderer:InputTag InputTag with options 1`] = `
<div
aria-selected="false"
class="cxd-ListMenu-item"
id="downshift-0-item-2"
id="downshift-1-item-2"
role="option"
>
<div
@ -689,3 +689,67 @@ exports[`Renderer:InputTag InputTag with options 1`] = `
</form>
</div>
`;
exports[`Renderer:InputTag InputTag with placeholder 1`] = `
<div>
<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>
标签
</span>
</span>
</span>
</label>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="downshift-0-label"
class="cxd-Form-control cxd-TagControl"
role="combobox"
>
<div
class="cxd-ResultBox cxd-TagControl-input is-group"
tabindex="-1"
>
<div
class="cxd-ResultBox-value-wrap"
>
<input
aria-autocomplete="list"
aria-labelledby="downshift-0-label"
autocomplete="off"
class="cxd-ResultBox-value-input"
id="downshift-0-input"
name="tag"
placeholder="please input the tag"
theme="cxd"
type="text"
value=""
/>
</div>
<div
class="cxd-ResultBox-actions"
/>
</div>
</div>
</div>
</form>
</div>
`;

View File

@ -52,6 +52,14 @@ const setupInputTag = async (inputTagOptions: any = {}) => {
};
describe('Renderer:InputTag', () => {
test('InputTag with placeholder', async () => {
const placeholder = 'please input the tag';
const {container, input} = await setupInputTag({placeholder});
expect(input.placeholder).toBe(placeholder);
expect(container).toMatchSnapshot();
});
test('InputTag with options', async () => {
const {container, input} = await setupInputTag({
options: ['Apple', 'Orange', 'Banana']

View File

@ -927,11 +927,12 @@ test('Renderer:table list', () => {
expect(container).toMatchSnapshot();
});
test('Renderer:table selectable', async () => {
describe('Renderer:table selectable & itemCheckableOn', () => {
const schema: any = {
type: 'table',
title: '表格1',
selectable: true,
itemCheckableOn: '${__id != 1}',
data: {
items: rows
},
@ -947,19 +948,26 @@ test('Renderer:table selectable', async () => {
]
};
const {container, rerender} = render(amisRender(schema, {}, makeEnv({})));
test('radio style', async () => {
const {container, debug} = render(amisRender(schema, {}, makeEnv({})));
await waitFor(() => {
expect(container.querySelector('[type=radio]')).toBeInTheDocument();
});
await waitFor(() => {
expect(container.querySelector('[type=radio]')).toBeInTheDocument();
expect(
container.querySelector('[data-id="1"] [type=radio][disabled=""]')!
).toBeInTheDocument();
});
schema.multiple = true;
const {container: multipleContainer} = render(
amisRender(schema, {}, makeEnv({}))
);
await waitFor(() => {
test('checkbox style', async () => {
schema.multiple = true;
const {container} = render(amisRender(schema, {}, makeEnv({})));
await waitFor(() => {
expect(container.querySelector('[type=checkbox]')).toBeInTheDocument();
});
expect(
multipleContainer.querySelector('[type=checkbox]')
container.querySelector('[data-id="1"] [type=checkbox][disabled=""]')!
).toBeInTheDocument();
});
});

View File

@ -486,14 +486,13 @@ export default class FormTable extends React.Component<TableProps, TableState> {
typeof column.name === 'string'
) {
if (
[
'input-date',
'input-datetime',
'input-time',
'input-month',
'input-quarter',
'input-year'
].includes(column.type)
'type' in column &&
(column.type === 'input-date' ||
column.type === 'input-datetime' ||
column.type === 'input-time' ||
column.type === 'input-month' ||
column.type === 'input-quarter' ||
column.type === 'input-year')
) {
const date = filterDate(column.value, data, column.format || 'X');
setVariable(

View File

@ -254,6 +254,10 @@ export default class TagControl extends React.PureComponent<
}
async addItem(option: Option) {
if (this.isReachMax()) {
return;
}
const {selectedOptions, onChange} = this.props;
const newValue = selectedOptions.concat();
@ -393,7 +397,7 @@ export default class TagControl extends React.PureComponent<
@autobind
handleOptionChange(option: Option) {
if (this.state.inputValue || !option) {
if (this.isReachMax() || this.state.inputValue || !option) {
return;
}
@ -415,6 +419,12 @@ export default class TagControl extends React.PureComponent<
reload?.();
}
@autobind
isReachMax() {
const {max, selectedOptions} = this.props;
return max != null && isInteger(max) && selectedOptions.length >= max;
}
render() {
const {
className,
@ -445,6 +455,8 @@ export default class TagControl extends React.PureComponent<
)
: [];
const reachMax = this.isReachMax();
return (
<Downshift
selectedItem={selectedOptions}
@ -461,13 +473,14 @@ export default class TagControl extends React.PureComponent<
{...getInputProps({
name,
ref: this.input,
placeholder: __(placeholder || 'Tag.placeholder'),
placeholder: __(placeholder ?? 'Tag.placeholder'),
value: this.state.inputValue,
onKeyDown: this.handleKeyDown,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled
})}
inputPlaceholder={''}
onChange={this.handleInputChange}
className={cx('TagControl-input')}
result={selectedOptions}
@ -507,7 +520,10 @@ export default class TagControl extends React.PureComponent<
...getItemProps({
index,
item,
disabled: item.disabled
disabled: reachMax || item.disabled,
className: cx('ListMenu-item', {
'is-disabled': reachMax
}),
})
})}
/>
@ -524,7 +540,7 @@ export default class TagControl extends React.PureComponent<
{options.map((item, index) => (
<div
className={cx('TagControl-sugItem', {
'is-disabled': item.disabled || disabled
'is-disabled': item.disabled || disabled || reachMax
})}
key={index}
onClick={this.addItem.bind(this, item)}

View File

@ -889,6 +889,7 @@ export default class NestedSelectControl extends React.Component<
disabled={disabled}
ref={this.domRef}
placeholder={__(placeholder ?? 'placeholder.empty')}
inputPlaceholder={''}
className={cx(`NestedSelect`, {
'NestedSelect--inline': inline,
'NestedSelect--single': !multiple,
@ -918,7 +919,6 @@ export default class NestedSelectControl extends React.Component<
clearable={clearable}
hasDropDownArrow={true}
allowInput={searchable}
inputPlaceholder={''}
>
{loading ? <Spinner size="sm" /> : undefined}
</ResultBox>

View File

@ -629,7 +629,8 @@ export default class TreeSelectControl extends React.Component<
<ResultBox
disabled={disabled}
ref={this.targetRef}
placeholder={__(placeholder || 'placeholder.empty')}
placeholder={__(placeholder ?? 'placeholder.empty')}
inputPlaceholder={''}
className={cx(`TreeSelect`, {
'TreeSelect--inline': inline,
'TreeSelect--single': !multiple,
@ -657,7 +658,6 @@ export default class TreeSelectControl extends React.Component<
onKeyDown={this.handleInputKeyDown}
clearable={clearable}
allowInput={searchable || isEffectiveApi(autoComplete)}
inputPlaceholder={''}
>
{loading ? <Spinner size="sm" /> : undefined}
</ResultBox>

View File

@ -119,14 +119,7 @@ export default class Property extends React.Component<PropertyProps, object> {
*/
prepareRows() {
const {column = 3, items, source, data} = this.props;
const propertyItems =
(items
? items
: (resolveVariableAndFilter(
source,
data,
'| raw'
) as Array<PropertyItem>)) || [];
const propertyItems = items ? items : source || [];
const rows: PropertyContent[][] = [];
@ -246,6 +239,7 @@ export default class Property extends React.Component<PropertyProps, object> {
}
@Renderer({
type: 'property'
type: 'property',
autoVar: true
})
export class PropertyRenderer extends Property {}

View File

@ -659,6 +659,7 @@ export class ServiceRenderer extends Service {
}
componentWillUnmount() {
super.componentWillUnmount();
const scoped = this.context as IScopedContext;
scoped.unRegisterComponent(this as ScopedComponentType);
}

View File

@ -228,7 +228,7 @@ export default class TableView extends React.Component<TableViewProps, object> {
renderTrs(trs: TrObject[]) {
const {data} = this.props;
const tr = trs.map((tr, rowIndex) =>
this.renderTr(resolveMappingObject(tr, data), rowIndex)
this.renderTr(resolveMappingObject(tr, data) as TrObject, rowIndex)
);
return tr;
}

View File

@ -6,6 +6,10 @@ npm run build --workspaces
rm -rf npm
mkdir npm
# 如果有问题可以注释掉这两行,不知道为啥会导致 cp -rf 挂掉
# rm -rf packages/amis/node_modules/.bin
# rm -rf packages/amis-ui/node_modules/.bin
cp -rf packages npm
cp package.json npm