Merge branch 'baidu:master' into fix-transfer

This commit is contained in:
zhou999 2022-10-11 14:56:50 +08:00 committed by GitHub
commit f57de85e67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1731 additions and 712 deletions

View File

@ -63,6 +63,9 @@ npm run build
# 执行测试用例
npm test --workspaces
# 测试某个用例
npm test --workspace amis inputImage
# 查看测试用例覆盖率
npm run coverage

View File

@ -179,6 +179,56 @@ fieldSet 的另一种标题展现样式,不同的是展开的时候收起文
}
```
## 嵌套使用
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "fieldSet",
"title": "基本配置",
"collapsable": true,
"body": [
{
"name": "text1",
"type": "input-text",
"label": "文本1"
},
{
"name": "text2",
"type": "input-text",
"label": "文本2"
},
{
"type": "fieldSet",
"title": "基本配置",
"collapsable": true,
"collapsed": true,
size: 'base',
"body": [
{
"name": "text1",
"type": "input-text",
"label": "文本1"
},
{
"name": "text2",
"type": "input-text",
"label": "文本2"
}
]
}
]
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
@ -192,3 +242,4 @@ fieldSet 的另一种标题展现样式,不同的是展开的时候收起文
| collapsable | `boolean` | `false` | 是否可折叠 |
| collapsed | `booelan` | `false` | 默认是否折叠 |
| collapseTitle | [SchemaNode](../../../docs/types/schemanode) | `收起` | 收起的标题 |
| size | string | `` | 大小,支持 xs、sm、base、lg、xl |

View File

@ -237,7 +237,7 @@ app.listen(8080, function () {});
}
```
**多选模式**
### 多选模式
当表单项为多选模式时,不能再直接取选项中的值了,而是通过 `items` 变量来取,通过它可以获取当前选中的选项集合。
@ -257,12 +257,75 @@ app.listen(8080, function () {});
"myUrl": "${items|pick:url}",
"lastUrl": "${items|last|pick:url}"
}
},
{
"type": "tpl",
"label": false,
"inline": false,
"tpl": "<strong>myUrl集合</strong>"
},
{
"type": "each",
"name": "myUrl",
"className": "mb-1",
"items": {
"type": "tpl",
"tpl": "<span class='label label-info m-l-sm inline-block mb-1'><%= data.item %></span>"
}
},
{
"type": "tpl",
"label": false,
"inline": false,
"tpl": "<strong>lastUrl</strong>"
},
{
"type": "text",
"name": "lastUrl",
"label": "lastUrl",
"inline": false
}
]
}
```
**initAutoFill**
### 其他表单项填充
```schema: scope="body"
{
"type": "form",
"title": "表单",
"body": [
{
"type": "select",
"label": "选项",
"name": "imageUrl",
"delimiter": "|",
"autoFill": {
"inputImage": "${value}"
},
"options": [
{
"label": "imageURL",
"value": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80"
},
{
"label": "空链接",
"value": ""
}
]
},
{
"type": "input-image",
"label": "图片上传",
"name": "inputImage",
"imageClassName": "r w-full"
}
]
}
```
### initAutoFill 初始化时自动同步
当表单反显时,可通过`initAutoFill`控制`autoFill`在数据反显时是否执行。

View File

@ -115,6 +115,28 @@ order: 32
}
```
## 是否是大数
> 2.3.0 及以上版本
默认情况下使用 JavaScript 原生数字类型,但如果要支持输入超过 JavaScript 支持范围的整数或浮点数,可以通过 `"big": true` 开启大数支持,开启之后输入输出都将是字符串
```schema: scope="body"
{
"type": "form",
"debug": true,
"api": "/api/mock2/form/saveForm",
"body": [
{
"type": "input-number",
"name": "number",
"label": "数字",
"big": "true"
}
]
}
```
## 原生数字组件
原生数字组件将直接使用浏览器的实现,最终展现效果和浏览器有关,而且只支持 `min`、`max`、`step` 这几个属性设置。
@ -148,6 +170,7 @@ order: 32
| suffix | `string` | | 后缀 |
| kilobitSeparator | `boolean` | | 千分分隔 |
| keyboard | `boolean` | | 键盘事件(方向上下) |
| big | `boolean` | | 是否使用大数 |
| displayMode | `string` | | 样式类型 |
## 事件表

View File

@ -1457,14 +1457,17 @@ order: 2
"type": "form",
"api": "/api/mock2/form/saveForm",
"debug": true,
"debugConfig": {
"levelExpand": 2
},
"body": [
{
"type": "select",
"label": "选项",
"label": "autoFill触发器",
"name": "select",
"autoFill": {
"option.instantValidate": "${label}",
"option.submitValidate": "${label}",
"option.submitValidate": "${label}"
},
"clearable": true,
"options": [
@ -1481,7 +1484,7 @@ order: 2
{
"type": "input-text",
"name": "option.instantValidate",
"label": "选中项",
"label": "目标1",
"description": "填充后立即校验",
"required": true,
"validateOnChange": true,
@ -1495,7 +1498,7 @@ order: 2
{
"type": "input-text",
"name": "option.submitValidate",
"label": "选中项1",
"label": "目标2",
"description": "填充后提交表单时才校验",
"required": true,
"validations": {
@ -1504,6 +1507,13 @@ order: 2
"validationErrors": {
"equals": "校验失败数据必须为Option B"
}
},
{
"type": "input-text",
"name": "option.c",
"label": "表单项3",
"description":'不受autoFill影响的表单项',
"value": "abc",
}
]
}

View File

@ -460,6 +460,12 @@ order: 67
## 可展开
支持点击按钮展开/关闭当前行的自定义内容,展开按钮可放在表格的最左侧、最右侧或通过事件动作来触发展开。
### 默认展开
默认模式 展开按钮在表格最左侧
```schema: scope="body"
{
"type": "service",
@ -490,7 +496,7 @@ order: 67
}
],
"expandable": {
"expandableOn": "this.record.id === 1 || this.record.id === 3",
"expandableOn": "this.record && (this.record.id === 1 || this.record.id === 3)",
"keyField": "id",
"expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>",
"expandedRowKeys": ["3"],
@ -507,7 +513,7 @@ order: 67
}
```
##展开 - 正则表达式
### 默认展开 - 正则表达式
```schema: scope="body"
{
@ -539,7 +545,7 @@ order: 67
}
],
"expandable": {
"expandableOn": "this.record.id === 1 || this.record.id === 3",
"expandableOn": "this.record && (this.record.id === 1 || this.record.id === 3)",
"keyField": "id",
"expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>",
"expandedRowKeysExpr": "data.record.id == '3'",
@ -556,6 +562,207 @@ order: 67
}
```
### 右侧展开按钮
通过设置`expandable.position`属性为`right`控制,支持 不设置、`left`、`right`、`none`四种情况。
```schema: scope="body"
{
"type": "service",
"api": "/api/sample?perPage=5",
"body": [
{
"type": "table2",
"source": "$rows",
"columns": [
{
"title": "Engine",
"name": "engine"
},
{
"title": "Version",
"name": "version"
},
{
"title": "Browser",
"name": "browser"
},
{
"title": "Operation",
"name": "operation",
"type": "button",
"label": "删除",
"size": "sm"
}
],
"expandable": {
"expandableOn": "this.record && (this.record.id === 1 || this.record.id === 3)",
"keyField": "id",
"expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>",
"expandedRowKeys": ["3"],
"type": "container",
"position": "right",
"body": [
{
"type": "tpl",
"html": "<div class=\"test\">测试测试</div>"
}
]
},
"footSummary": [
{
"type": "text",
"text": "总计"
},
{
"type": "tpl",
"tpl": "测试测试",
"colSpan": 2
},
{
"type": "tpl",
"tpl": "最后一列"
}
]
}
]
}
```
### 无展开按钮
可设置无展开按钮,通过事件动作展开关闭,可单独行控制
```schema: scope="body"
{
"type": "service",
"api": "/api/sample?perPage=5",
"body": [
{
"type": "table2",
"source": "$rows",
"id": "table-select",
"columns": [
{
"title": "Engine",
"name": "engine"
},
{
"title": "Version",
"name": "version"
},
{
"title": "Browser",
"name": "browser"
},
{
"title": "Operation",
"name": "operation",
"type": "button",
"label": "展开",
"size": "sm",
"onEvent": {
"click": {
"actions": [
{
"actionType": "expand",
"componentId": "table-select",
"description": "展开行",
"args": {
"value": "${id}"
}
}
]
}
}
}
],
"expandable": {
"keyField": "id",
"expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>",
"type": "container",
"position": "none",
"body": [
{
"type": "tpl",
"html": "<div class=\"test\">测试测试</div>"
}
]
}
}
]
}
```
也可以通过正则表达式一次控制多行展开关闭
```schema: scope="body"
{
"type": "service",
"api": "/api/sample?perPage=5",
"body": [
{
"type": "container",
"style": {
"marginBottom": "5px"
},
"body": [
{
"type": "button",
"label": "展开",
"size": "sm",
"onEvent": {
"click": {
"actions": [
{
"actionType": "expand",
"componentId": "table-select2",
"description": "展开行",
"args": {
"expandedRowsExpr": "data.record?.id === 1 || data.record?.id === 3"
}
}
]
}
}
}
]
},
{
"type": "table2",
"source": "$rows",
"id": "table-select2",
"columns": [
{
"title": "Engine",
"name": "engine"
},
{
"title": "Version",
"name": "version"
},
{
"title": "Browser",
"name": "browser"
}
],
"expandable": {
"keyField": "id",
"expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>",
"type": "container",
"position": "none",
"body": [
{
"type": "tpl",
"html": "<div class=\"test\">测试测试</div>"
}
]
}
}
]
}
```
## 表格行/列合并
```schema: scope="body"

View File

@ -89,7 +89,8 @@ export default {
{
title: '超级小',
type: 'fieldSet',
className: 'fieldset-xs',
collapsable: true,
size: 'xs',
body: [
{
type: 'plain',
@ -100,7 +101,8 @@ export default {
{
title: '小尺寸',
type: 'fieldSet',
className: 'fieldset-sm',
collapsable: true,
size: 'sm',
body: [
{
type: 'plain',
@ -111,7 +113,8 @@ export default {
{
title: '正常尺寸',
type: 'fieldSet',
className: 'fieldset',
collapsable: true,
size: 'base',
body: [
{
type: 'plain',
@ -122,7 +125,8 @@ export default {
{
title: '中大尺寸',
type: 'fieldSet',
className: 'fieldset-md',
collapsable: true,
size: 'md',
body: [
{
type: 'plain',
@ -133,7 +137,8 @@ export default {
{
title: '超大尺寸',
type: 'fieldSet',
className: 'fieldset-lg',
collapsable: true,
size: 'lg',
body: [
{
type: 'plain',

View File

@ -2,5 +2,5 @@
"packages": [
"packages/*"
],
"version": "2.2.0"
"version": "2.3.0"
}

View File

@ -85,6 +85,10 @@
"testPathIgnorePatterns": [
"/node_modules/",
"/.rollup.cache/"
]
],
"snapshotFormat": {
"escapeString": false,
"printBasicPrototype": false
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "amis-core",
"version": "2.2.0",
"version": "2.3.0",
"description": "amis-core",
"main": "lib/index.js",
"module": "esm/index.js",
@ -43,7 +43,7 @@
"esm"
],
"dependencies": {
"amis-formula": "^2.1.0",
"amis-formula": "^2.3.0",
"classnames": "2.3.1",
"file-saver": "^2.0.2",
"hoist-non-react-statics": "^3.3.2",

View File

@ -25,7 +25,7 @@ export interface ScopedComponentType extends React.Component<RendererProps> {
data: RendererData,
throwErrors?: boolean
) => void;
receive?: (values: RendererData, subPath?: string) => void;
receive?: (values: RendererData, subPath?: string, replace?: boolean) => void;
reload?: (
subPath?: string,
query?: RendererData | null,

View File

@ -21,7 +21,8 @@ export interface ListenerAction {
actionType: string; // 动作类型 逻辑动作|自定义(脚本支撑)|reload|url|ajax|dialog|drawer 其他扩充的组件动作
description?: string; // 事件描述actionType: broadcast
componentId?: string; // 组件ID用于直接执行指定组件的动作
args?: Record<string, any>; // 参数,可以配置数据映射
args?: Record<string, any> | null; // 参数,可以配置数据映射
dataMergeMode?: 'merge' | 'override'; // 参数模式,合并或者覆盖
outputVar?: string; // 输出数据变量名
preventDefault?: boolean; // 阻止原有组件的动作行为
stopPropagation?: boolean; // 阻止后续的事件处理器执行
@ -145,14 +146,13 @@ export const runAction = async (
actionConfig.stopPropagation &&
evalExpression(String(actionConfig.stopPropagation), mergeData);
// 修正参数,处理数据映射
let args = event.data;
if (actionConfig.args) {
args = dataMapping(actionConfig.args, mergeData, key =>
['adaptor', 'responseAdaptor', 'requestAdaptor'].includes(key)
);
}
// 处理数据映射,默认参数为事件数据
let args =
actionConfig.args !== undefined
? dataMapping(actionConfig.args, mergeData, key =>
['adaptor', 'responseAdaptor', 'requestAdaptor'].includes(key)
)
: event.data;
await actionInstrance.run(
{

View File

@ -32,7 +32,7 @@ export class BroadcastAction implements RendererAction {
}
// 作为一个新的事件需要把广播动作的args参数追加到事件数据中
event.setData(createObject(event.data, action.args));
event.setData(createObject(event.data, action.args ?? {}));
// 直接触发对应的动作
return await dispatchEvent(

View File

@ -41,6 +41,7 @@ export class CmptAction implements RendererAction {
action.componentId && renderer.props.$schema.id !== action.componentId
? event.context.scoped?.getComponentById(action.componentId)
: renderer;
const dataMergeMode = action.dataMergeMode || 'merge';
// 显隐&状态控制
if (['show', 'hidden'].includes(action.actionType)) {
@ -58,7 +59,11 @@ export class CmptAction implements RendererAction {
// 数据更新
if (action.actionType === 'setValue') {
if (component?.setData) {
return component?.setData(action.args?.value, action.args?.index);
return component?.setData(
action.args?.value,
dataMergeMode === 'override',
action.args?.index
);
} else {
return component?.props.onChange?.(action.args?.value);
}
@ -66,7 +71,13 @@ export class CmptAction implements RendererAction {
// 刷新
if (action.actionType === 'reload') {
return component?.reload?.(undefined, action.args);
return component?.reload?.(
undefined,
action.args,
undefined,
undefined,
dataMergeMode === 'override'
);
}
// 执行组件动作

View File

@ -755,10 +755,10 @@ export default class Form extends React.Component<FormProps, object> {
: store.reset(undefined, false);
}
receive(values: object) {
receive(values: object, name?: string, replace?: boolean) {
const {store} = this.props;
store.updateData(values);
store.updateData(values, undefined, replace);
this.reload();
}
@ -812,10 +812,10 @@ export default class Form extends React.Component<FormProps, object> {
return store.data;
}
setValues(value: any) {
setValues(value: any, replace?: boolean) {
const {store} = this.props;
this.flush();
store.setValues(value);
store.setValues(value, undefined, replace);
}
submit(fn?: (values: object) => Promise<any>): Promise<any> {
@ -1872,9 +1872,15 @@ export class FormRenderer extends Form {
scoped.close(target);
}
reload(target?: string, query?: any, ctx?: any, silent?: boolean) {
reload(
target?: string,
query?: any,
ctx?: any,
silent?: boolean,
replace?: boolean
) {
if (query) {
return this.receive(query);
return this.receive(query, undefined, replace);
}
const scoped = this.context as IScopedContext;
@ -1913,7 +1919,7 @@ export class FormRenderer extends Form {
}
}
receive(values: object, name?: string) {
receive(values: object, name?: string, replace?: boolean) {
if (name) {
const scoped = this.context as IScopedContext;
const idx = name.indexOf('.');
@ -1929,10 +1935,10 @@ export class FormRenderer extends Form {
return;
}
return super.receive(values);
return super.receive(values, undefined, replace);
}
setData(values: object) {
setData(values: object, replace?: boolean) {
return super.setValues(values);
}
}

View File

@ -26,8 +26,7 @@ import {
getTreeDepth,
flattenTree,
keyToPath,
getVariable,
isObject
getVariable
} from '../utils/helper';
import {reaction} from 'mobx';
import {
@ -543,6 +542,8 @@ export function registerOptionsControl(config: OptionsConfig) {
selectedOptions[0]
)
);
const tmpData = {...data};
const result = {...toSync};
Object.keys(autoFill).forEach(key => {
const keys = keyToPath(key);
@ -550,15 +551,16 @@ export function registerOptionsControl(config: OptionsConfig) {
// 如果左边的 key 是一个路径
// 这里不希望直接把原始对象都给覆盖没了
// 而是保留原始的对象,只修改指定的属性
if (keys.length > 1 && isPlainObject(data[keys[0]])) {
const obj = {...data[keys[0]]};
if (keys.length > 1 && isPlainObject(tmpData[keys[0]])) {
const value = getVariable(toSync, key);
toSync[keys[0]] = obj;
setVariable(toSync, key, value);
// 存在情况依次更新同一子路径的多个keyeg: a.b.c1 和 a.b.c2所以需要同步更新data
setVariable(tmpData, key, value);
result[keys[0]] = tmpData[keys[0]];
}
});
onBulkChange(toSync);
onBulkChange(result);
}
}

View File

@ -1,7 +1,7 @@
// 主要用于解决 0.1+0.2 结果的精度问题导致太长
export function stripNumber(number: number) {
if (typeof number === 'number' && !Number.isInteger(number)) {
return parseFloat(number.toPrecision(12));
return parseFloat(number.toPrecision(16));
} else {
return number;
}

View File

@ -1,4 +1,5 @@
import {parse} from '../src/index';
test('parser:simple', () => {
expect(
parse('expression result is ${a + b}', {

View File

@ -1,6 +1,6 @@
{
"name": "amis-formula",
"version": "2.2.0",
"version": "2.3.0",
"description": "负责 amis 里面的表达式实现,内置公式,编辑器等",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -94,7 +94,11 @@
},
"setupFilesAfterEnv": [
"<rootDir>/__tests__/jest.setup.js"
]
],
"snapshotFormat": {
"escapeString": false,
"printBasicPrototype": false
}
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}

View File

@ -1915,7 +1915,7 @@ function parseJson(str: string, defaultValue?: any) {
function stripNumber(number: number) {
if (typeof number === 'number' && !Number.isInteger(number)) {
return parseFloat(number.toPrecision(12));
return parseFloat(number.toPrecision(16));
} else {
return number;
}

View File

@ -3,7 +3,7 @@
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"version": "2.2.0",
"version": "2.3.0",
"description": "",
"scripts": {
"build": "npm run clean-dist && NODE_ENV=production rollup -c ",
@ -33,8 +33,8 @@
}
},
"dependencies": {
"amis-core": "^2.2.0",
"amis-formula": "^2.1.0",
"amis-core": "^2.3.0",
"amis-formula": "^2.3.0",
"classnames": "2.3.1",
"codemirror": "^5.63.0",
"downshift": "6.1.12",

View File

@ -61,15 +61,15 @@
display: inline-block;
}
&.is-active &-arrow:before {
&.is-active > * > &-arrow:before {
transform: rotate(135deg);
transform-origin: 50% 30%;
}
&.is-active &-icon-tranform {
&.is-active > * > &-icon-tranform {
transform: rotate(90deg);
}
&--disabled &-header {
&--disabled > &-header {
cursor: not-allowed;
user-select: none;
color: var(--text--muted-color);
@ -78,7 +78,7 @@
}
}
&--disabled &-arrow:before {
&--disabled > * > &-arrow:before {
border-color: var(--text--muted-color);
}

View File

@ -174,3 +174,9 @@
transform: translate3d(-50%, -50%, 0);
}
}
.#{$ns}Spinner-mark {
position: absolute;
z-index: -999;
opacity: 0;
}

View File

@ -61,6 +61,10 @@ fieldset.#{$ns}Collapse {
font-size: var(--fontSizeXs);
padding: 0 3px;
margin: 0 0 0 -3px;
&:hover {
background-color: var(--white);
}
}
&:after {
@ -78,6 +82,10 @@ fieldset.#{$ns}Collapse {
font-size: var(--fontSizeSm);
padding: 0 5px;
margin: 0 0 0 -5px;
&:hover {
background-color: var(--white);
}
}
&:after {
@ -95,6 +103,10 @@ fieldset.#{$ns}Collapse {
font-size: var(--fontSizeBase);
padding: 0 8px;
margin: 0 0 0 -8px;
&:hover {
background-color: var(--white);
}
}
&:after {
@ -111,6 +123,10 @@ fieldset.#{$ns}Collapse {
font-size: var(--fontSizeMd);
padding: 0 10px;
margin: 0 0 0 -10px;
&:hover {
background-color: var(--white);
}
}
&:after {
@ -127,6 +143,10 @@ fieldset.#{$ns}Collapse {
font-size: var(--fontSizeLg);
padding: 0 var(--gap-md);
margin: 0 0 0 calc(var(--gap-md) * -1);
&:hover {
background-color: var(--white);
}
}
&:after {

View File

@ -51,6 +51,11 @@ export interface NumberProps extends ThemeProps {
*/
displayMode?: 'base' | 'enhance';
keyboard?: Boolean;
/**
*
*/
big?: boolean;
}
export class NumberInput extends React.Component<NumberProps, any> {
@ -60,6 +65,19 @@ export class NumberInput extends React.Component<NumberProps, any> {
borderMode: 'full'
};
/**
* bigNumber
*/
isBig: boolean = false;
constructor(props: NumberProps) {
super(props);
const value = props.value;
if (typeof value === 'string' || props.big) {
this.isBig = true;
}
}
@autobind
handleChange(value: any) {
const {min, max, onChange} = this.props;
@ -74,6 +92,22 @@ export class NumberInput extends React.Component<NumberProps, any> {
}
}
if (typeof value === 'string') {
let val = getMiniDecimal(value);
if (typeof min !== 'undefined') {
let minValue = getMiniDecimal(min);
if (val.lessEquals(minValue)) {
value = min;
}
}
if (typeof max !== 'undefined') {
let maxValue = getMiniDecimal(max);
if (maxValue.lessEquals(val)) {
value = max;
}
}
}
onChange?.(value);
}
@autobind
@ -87,18 +121,19 @@ export class NumberInput extends React.Component<NumberProps, any> {
const {onBlur} = this.props;
onBlur && onBlur(e);
}
@autobind
handleEnhanceModeChange(action: 'add' | 'subtract'): void {
const {value, step, disabled, readOnly, precision} = this.props;
const {value, step = 1, disabled, readOnly, precision} = this.props;
// value为undefined会导致溢出错误
let val = Number(value) || 0;
let val = value || 0;
if (disabled || readOnly) {
return;
}
if (isNaN(Number(step)) || !Number(step)) {
return;
}
let stepDecimal = getMiniDecimal(Number(step));
let stepDecimal = getMiniDecimal(step);
if (action !== 'add') {
stepDecimal = stepDecimal.negate();
}
@ -126,9 +161,14 @@ export class NumberInput extends React.Component<NumberProps, any> {
return updateValue;
};
const updatedValue = triggerValueUpdate(target, false);
val = Number(updatedValue.toString());
this.handleChange(val);
if (this.isBig) {
this.handleChange(updatedValue.toString());
} else {
val = Number(updatedValue.toString());
this.handleChange(val);
}
}
@autobind
renderBase() {
const {
@ -182,6 +222,7 @@ export class NumberInput extends React.Component<NumberProps, any> {
placeholder={placeholder}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
stringMode={this.isBig ? true : false}
keyboard={keyboard}
{...precisionProps}
/>

View File

@ -133,7 +133,8 @@ class HandleItem extends React.Component<HandleItemProps, HandleItemState> {
this.setState({
isDrag: false
});
this.props.onAfterChange();
const {onAfterChange} = this.props;
onAfterChange && onAfterChange();
window.removeEventListener('mousemove', this.onMouseMove);
window.removeEventListener('mouseup', this.onMouseUp);
}
@ -291,7 +292,8 @@ export class Range extends React.Component<RangeItemProps, any> {
*/
@autobind
updateValue(value: FormatValue) {
this.props.updateValue(value);
const {onChange} = this.props;
onChange && onChange(value);
}
/**

View File

@ -105,7 +105,8 @@ export class Spinner extends React.Component<SpinnerProps> {
};
state = {
spinning: false
spinning: false,
showMark: true
};
parent: HTMLElement | null = null;
@ -113,15 +114,13 @@ export class Spinner extends React.Component<SpinnerProps> {
spinnerRef = (dom: HTMLElement) => {
if (dom) {
this.parent = dom.parentNode as HTMLElement;
this.setState({
showMark: false
});
}
};
componentDidUpdate(prev: SpinnerProps) {
if (!prev.show && this.props.show) {
// 先根据 props.show 触发一次 loading否则元素没有渲染无法找到 parent
this.setState({spinning: true});
}
componentDidUpdate() {
if (this.parent) {
if (this.props.show) {
store.push(this.parent);
@ -131,18 +130,6 @@ export class Spinner extends React.Component<SpinnerProps> {
}
}
componentDidMount(): void {
// 对于 通过 条件语句控制 Spinner 是否展示的情况,需要在这里处理 show && <Spinner show overlay />
if (this.props.show) {
// 先根据 props.show 触发一次 loading否则元素没有渲染无法找到 parent
this.setState({spinning: true});
if (this.parent) {
store.push(this.parent);
}
}
}
componentWillUnmount() {
// 卸载 reaction
this.loadingChecker();
@ -150,6 +137,9 @@ export class Spinner extends React.Component<SpinnerProps> {
store.remove(this.parent!);
}
/**
* spinnerContainers
*/
loadingChecker = reaction(
() => store.spinnerContainers.size,
() => {
@ -175,67 +165,71 @@ export class Spinner extends React.Component<SpinnerProps> {
const timeout = {enter: delay, exit: 0};
return (
<Transition
mountOnEnter
unmountOnExit
in={this.state.spinning}
timeout={timeout}
>
{(status: string) => {
return (
<>
{/* 遮罩层 */}
{overlay ? (
<div className={cx(`Spinner-overlay`, fadeStyles[status])} />
) : null}
<>
{this.state.showMark && (
<span className={cx('Spinner-mark')} ref={this.spinnerRef as any} />
)}
<Transition
mountOnEnter
unmountOnExit
in={this.state.spinning}
timeout={timeout}
>
{(status: string) => {
return (
<>
{/* 遮罩层 */}
{overlay ? (
<div className={cx(`Spinner-overlay`, fadeStyles[status])} />
) : null}
{/* spinner图标和文案 */}
<div
ref={this.spinnerRef as any}
data-testid="spinner"
className={cx(
`Spinner`,
tip && {
[`Spinner-tip--${tipPlacement}`]: [
'top',
'right',
'bottom',
'left'
].includes(tipPlacement)
},
{[`Spinner--overlay`]: overlay},
fadeStyles[status],
className
)}
>
{/* spinner图标和文案 */}
<div
data-testid="spinner"
className={cx(
`Spinner-icon`,
{
[`Spinner-icon--${size}`]: ['lg', 'sm'].includes(size),
[`Spinner-icon--default`]: !icon,
[`Spinner-icon--simple`]: !isCustomIcon && icon,
[`Spinner-icon--custom`]: isCustomIcon
`Spinner`,
tip && {
[`Spinner-tip--${tipPlacement}`]: [
'top',
'right',
'bottom',
'left'
].includes(tipPlacement)
},
spinnerClassName
{[`Spinner--overlay`]: overlay},
fadeStyles[status],
className
)}
>
{icon ? (
isCustomIcon ? (
icon
) : hasIcon(icon as string) ? (
<Icon icon={icon} className="icon" />
) : (
generateIcon(cx, icon as string, 'icon')
)
) : null}
<div
className={cx(
`Spinner-icon`,
{
[`Spinner-icon--${size}`]: ['lg', 'sm'].includes(size),
[`Spinner-icon--default`]: !icon,
[`Spinner-icon--simple`]: !isCustomIcon && icon,
[`Spinner-icon--custom`]: isCustomIcon
},
spinnerClassName
)}
>
{icon ? (
isCustomIcon ? (
icon
) : hasIcon(icon as string) ? (
<Icon icon={icon} className="icon" />
) : (
generateIcon(cx, icon as string, 'icon')
)
) : null}
</div>
{tip ? <span className={cx(`Spinner-tip`)}>{tip}</span> : ''}
</div>
{tip ? <span className={cx(`Spinner-tip`)}>{tip}</span> : ''}
</div>
</>
);
}}
</Transition>
</>
);
}}
</Transition>
</>
);
}
}

View File

@ -97,6 +97,7 @@ export interface ExpandableProps {
expandedRowClassName?: Function;
expandIcon?: Function;
fixed?: boolean;
position?: string; // 控制展开按钮的位置 不设置默认是在左侧 设置支持left、right、none
}
export interface SummaryProps {
@ -154,6 +155,7 @@ export interface TableProps extends ThemeProps, LocaleProps {
onSelect?: Function;
onSelectAll?: Function;
itemActions?: Function;
onRef?: (ref: any) => void;
}
export interface ScrollProps {
@ -463,6 +465,8 @@ export class Table extends React.PureComponent<TableProps, TableState> {
}
componentDidMount() {
this.props?.onRef?.(this);
if (this.props.loading) {
return;
}
@ -792,7 +796,20 @@ export class Table extends React.PureComponent<TableProps, TableState> {
const tdColumns = this.tdColumns;
const isExpandable = this.isExpandableTable();
const extraCount = this.getExtraColumnCount();
const extraCount =
this.getExtraColumnCount() - (this.isRightExpandable() ? 1 : 0);
const isLeftExpandable = this.isLeftExpandable();
const isRightExpandable = this.isRightExpandable();
const expandableCol =
!draggable && isExpandable ? (
<col
className={cx('Table-expand-col')}
style={{
width: (expandable?.columnWidth || DefaultCellWidth) + 'px'
}}
></col>
) : null;
return (
<colgroup>
@ -810,16 +827,11 @@ export class Table extends React.PureComponent<TableProps, TableState> {
}}
></col>
) : null}
{!draggable && isExpandable ? (
<col
className={cx('Table-expand-col')}
style={{
width: (expandable?.columnWidth || DefaultCellWidth) + 'px'
}}
></col>
) : null}
{isLeftExpandable ? expandableCol : null}
{tdColumns.map((data, index) => {
const width = colWidths ? colWidths[index + extraCount] : data.width;
const width = colWidths
? colWidths[index + extraCount - (isRightExpandable ? 1 : 0)]
: data.width;
return (
<col
@ -831,6 +843,7 @@ export class Table extends React.PureComponent<TableProps, TableState> {
></col>
);
})}
{isRightExpandable ? expandableCol : null}
</colgroup>
);
}
@ -932,6 +945,18 @@ export class Table extends React.PureComponent<TableProps, TableState> {
: this.state.dataSource;
const isExpandable = this.isExpandableTable();
const isLeftExpandable = this.isLeftExpandable();
const isRightExpandable = this.isRightExpandable();
const expandableCell =
!draggable && isExpandable ? (
<Cell
wrapperComponent="th"
rowSpan={thColumns.length}
fixed={expandable && expandable.fixed ? 'left' : ''}
className={cx('Table-row-expand-icon-cell')}
></Cell>
) : null;
let allRowKeys: Array<string> = [];
let allRows: Array<any> = [];
@ -1016,14 +1041,7 @@ export class Table extends React.PureComponent<TableProps, TableState> {
: null}
</Cell>
) : null}
{!draggable && isExpandable && index === 0 ? (
<Cell
wrapperComponent="th"
rowSpan={thColumns.length}
fixed={expandable && expandable.fixed ? 'left' : ''}
className={cx('Table-row-expand-icon-cell')}
></Cell>
) : null}
{isLeftExpandable && index === 0 ? expandableCell : null}
{data.map((item, i) => {
let sort = null;
if (item.sorter) {
@ -1108,6 +1126,7 @@ export class Table extends React.PureComponent<TableProps, TableState> {
</Cell>
);
})}
{isRightExpandable && index === 0 ? expandableCell : null}
</tr>
);
})}
@ -1205,21 +1224,24 @@ export class Table extends React.PureComponent<TableProps, TableState> {
this.setState({hoverRow: null});
}
onExpandRow(data: any) {
onExpandRows(data: Array<any>) {
const {expandedRowKeys} = this.state;
const {expandable} = this.props;
const key = data[this.getExpandableKeyField()];
this.setState({expandedRowKeys: [...expandedRowKeys, key]});
const keys = data.map((d: any) => d[this.getExpandableKeyField()]);
this.setState({expandedRowKeys: [...expandedRowKeys, ...keys]});
expandable?.onExpand && expandable?.onExpand(true, data);
}
onCollapseRow(data: any) {
onCollapseRows(data: Array<any>) {
const {expandedRowKeys} = this.state;
const {expandable} = this.props;
const key = data[this.getExpandableKeyField()];
// 还是得模糊匹配 否则'3'、3匹配不上
this.setState({expandedRowKeys: expandedRowKeys.filter(k => k != key)});
expandable?.onExpand && expandable?.onExpand(false, data);
const keys = data.map((d: any) => d[this.getExpandableKeyField()]);
this.setState({
expandedRowKeys: expandedRowKeys.filter(
(k: string | number) => !keys.find(v => v == k) // 模糊匹配 否则'3'、3匹配不上
)
});
expandable?.onExpand && expandable?.onExpand(true, data);
}
getChildrenColumnName() {
@ -1251,8 +1273,8 @@ export class Table extends React.PureComponent<TableProps, TableState> {
return (
expandable &&
expandable.rowExpandable &&
expandable.rowExpandable(data, rowIndex)
(!expandable.rowExpandable ||
(expandable.rowExpandable && expandable.rowExpandable(data, rowIndex)))
);
}
@ -1295,20 +1317,27 @@ export class Table extends React.PureComponent<TableProps, TableState> {
return length > 0;
}
getExpandedIcons(isExpanded: boolean, record: any) {
isExpanded(record: any) {
return !!find(
this.state.expandedRowKeys,
key => key == record[this.getExpandableKeyField()]
); // == 匹配 否则'3'、3匹配不上
}
getExpandedIcons(record: any) {
const {classnames: cx} = this.props;
return isExpanded ? (
return this.isExpanded(record) ? (
<i
className={cx('Table-expandBtn', 'is-active')}
onClick={this.onCollapseRow.bind(this, record)}
onClick={this.onCollapseRows.bind(this, [record])}
>
<Icon icon="right-arrow-bold" className="icon" />
</i>
) : (
<i
className={cx('Table-expandBtn')}
onClick={this.onExpandRow.bind(this, record)}
onClick={this.onExpandRows.bind(this, [record])}
>
<Icon icon="right-arrow-bold" className="icon" />
</i>
@ -1375,17 +1404,15 @@ export class Table extends React.PureComponent<TableProps, TableState> {
const isExpandable = this.isExpandableTable();
const defaultKey = this.getRowSelectionKeyField();
const colCount = this.getExtraColumnCount();
const isLeftExpandable = this.isLeftExpandable();
const isRightExpandable = this.isRightExpandable();
// 当前行是否可展开
const isExpandableRow = this.isExpandableRow(data, rowIndex);
// 当前行是否有children
const hasChildrenRow = this.hasChildrenRow(data);
const isExpanded = !!find(
this.state.expandedRowKeys,
key => key == data[this.getExpandableKeyField()]
); // == 匹配 否则'3'、3匹配不上
const isExpanded = this.isExpanded(data);
// 设置缩进效果
const indentDom =
levels.length > 0 ? (
@ -1439,9 +1466,7 @@ export class Table extends React.PureComponent<TableProps, TableState> {
})}
>
{i === 0 && levels.length > 0 ? indentDom : null}
{i === 0 && hasChildrenRow
? this.getExpandedIcons(isExpanded, data)
: null}
{i === 0 && hasChildrenRow ? this.getExpandedIcons(data) : null}
{render ? children : data[item.name]}
</div>
</Cell>
@ -1497,6 +1522,24 @@ export class Table extends React.PureComponent<TableProps, TableState> {
const hasChildrenChecked = this.hasCheckedChildrenRows(data);
const isRadio = rowSelection && rowSelection.type === 'radio';
const expandableCell =
!draggable && isExpandable ? (
<Cell
fixed={
expandable && expandable.fixed
? isRightExpandable
? 'right'
: 'left'
: ''
}
className={cx('Table-cell-expand-icon-cell')}
>
{isExpandableRow || hasChildrenRow
? this.getExpandedIcons(data)
: null}
</Cell>
) : null;
return [
<tr
key={`${data[keyField || 'key'] || rowIndex}`} // 可能会拖拽排序就不能用rowIndex作为key了否则显示会有问题
@ -1540,17 +1583,9 @@ export class Table extends React.PureComponent<TableProps, TableState> {
></CheckBox>
</Cell>
) : null}
{!draggable && isExpandable ? (
<Cell
fixed={expandable && expandable.fixed ? 'left' : ''}
className={cx('Table-cell-expand-icon-cell')}
>
{isExpandableRow || hasChildrenRow
? this.getExpandedIcons(isExpanded, data)
: null}
</Cell>
) : null}
{isLeftExpandable ? expandableCell : null}
{cells}
{isRightExpandable ? expandableCell : null}
</tr>,
children
];
@ -1600,19 +1635,35 @@ export class Table extends React.PureComponent<TableProps, TableState> {
return !!expandable;
}
// 展开列放到右侧 会影响之前的一些合并的规则
isRightExpandable() {
const {expandable} = this.props;
return expandable && expandable.position === 'right';
}
// 展开列放到左侧
isLeftExpandable() {
const {expandable} = this.props;
return (
expandable && (!expandable.position || expandable.position === 'left')
);
}
isNestedTable() {
const {dataSource} = this.props;
return !!find(dataSource, item => this.hasChildrenRow(item));
}
// 计算自动增加的列数
// 选择、拖拽、展开
getExtraColumnCount() {
const {draggable, rowSelection} = this.props;
const {draggable, rowSelection, expandable} = this.props;
let count = 0;
if (draggable) {
count++;
} else {
if (this.isExpandableTable()) {
if (this.isExpandableTable() && expandable?.position !== 'none') {
count++;
}
if (rowSelection) {
@ -1628,6 +1679,7 @@ export class Table extends React.PureComponent<TableProps, TableState> {
const cells: Array<React.ReactNode> = [];
const trs: Array<React.ReactNode> = [];
let colCount = this.getExtraColumnCount();
const isRightExpandable = this.isRightExpandable() ? 1 : 0;
Array.isArray(summary)
? summary.forEach((s, index) => {
@ -1642,7 +1694,11 @@ export class Table extends React.PureComponent<TableProps, TableState> {
{s.map((d, i) => {
// 将操作列自动添加到第一列用户的colSpan只需要关心实际的列数
const colSpan =
i === 0 ? (d.colSpan || 1) + colCount : d.colSpan;
i === 0
? (d.colSpan || 1) + colCount - isRightExpandable
: i === s.length - 1
? (d.colSpan || 1) + isRightExpandable
: d.colSpan;
return (
<Cell
key={'summary-tr-cell-' + i}
@ -1662,7 +1718,11 @@ export class Table extends React.PureComponent<TableProps, TableState> {
key={'summary-cell-' + index}
fixed={s.fixed}
colSpan={
cells.length === 0 ? (s.colSpan || 1) + colCount : s.colSpan
cells.length === 0
? (s.colSpan || 1) + colCount - isRightExpandable
: index === summary.length - 1
? (s.colSpan || 1) + isRightExpandable
: s.colSpan
}
>
{typeof s.render === 'function'
@ -1776,10 +1836,8 @@ export class Table extends React.PureComponent<TableProps, TableState> {
// 设置了横向滚动轴 则table的table-layout为fixed
const hasScrollX = scroll && scroll.x;
const hoverRow = this.state.hoverRow;
// 如果设置了列宽 那么table-layout为fixed才能生效
const columnWidth = this.tdColumns.some(item => item.width);
const tableLayout = hasScrollX || columnWidth ? 'fixed' : 'auto';
const tableLayout = hasScrollX ? 'fixed' : 'auto';
const tableStyle = hasScrollX ? {width: scroll.x + 'px'} : {};
return (

View File

@ -44,6 +44,7 @@ register('de-DE', {
'CRUD.invalidArray': '"data.items" muss ein Array sein',
'CRUD.invalidData': '"data" ist leer',
'CRUD.loadMore': 'Weitere laden',
'CRUD.loadMoreDisableTip': 'Keine Daten oder letzte Seite',
'CRUD.perPage': 'Pro Seite',
'CRUD.stat': '{{page}} von {{lastPage}} insgesamt: {{total}}.',
'CRUD.paginationGoText': 'Wechseln zu',

View File

@ -39,6 +39,7 @@ register('en-US', {
'CRUD.invalidArray': 'data.items must be an array',
'CRUD.invalidData': 'data is empty',
'CRUD.loadMore': 'Load more',
'CRUD.loadMoreDisableTip': 'No data or last page',
'CRUD.perPage': 'Per page',
'CRUD.stat': '{{page}} of {{lastPage}} total: {{total}}.',
'CRUD.paginationGoText': 'Go to',

View File

@ -42,6 +42,7 @@ register('zh-CN', {
'CRUD.invalidArray': 'data.items 必须是数组',
'CRUD.invalidData': '返回数据格式不正确data 没有数据',
'CRUD.loadMore': '加载更多',
'CRUD.loadMoreDisableTip': '无数据或最后一页',
'CRUD.perPage': '每页显示',
'CRUD.stat': '{{page}}/{{lastPage}} 总共:{{total}} 项',
'CRUD.paginationGoText': '前往',

View File

@ -29,6 +29,7 @@ import '../../src';
import {clearStoresCache, render as amisRender} from '../../src';
import {makeEnv as makeEnvRaw, wait} from '../helper';
import rows from '../mockData/rows';
import type {RenderOptions} from '../../src';
afterEach(() => {
cleanup();
@ -37,7 +38,8 @@ afterEach(() => {
});
/** 避免updateLocation里的console.error */
const makeEnv = args => makeEnvRaw({updateLocation: () => {}, ...args});
const makeEnv = (env?: Partial<RenderOptions>) =>
makeEnvRaw({updateLocation: () => {}, ...env});
async function fetcher(config: any) {
return {

View File

@ -1,5 +1,21 @@
import React = require('react');
import {render, cleanup, fireEvent, waitFor} from '@testing-library/react';
/**
* autoFill
*
* 01.
* 02. &
* 03. name为对象路径格式
* 04.
* 05. & validateOnChange正常触发
*/
import React from 'react';
import {
render,
cleanup,
screen,
fireEvent,
waitFor
} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {makeEnv, wait} from '../../helper';
@ -10,6 +26,11 @@ afterEach(() => {
clearStoresCache();
});
const optisons = [
{label: 'OptionA', value: 'a'},
{label: 'OptionB', value: 'b'}
];
test('Form:options:autoFill', async () => {
const {container, getByText} = render(
amisRender(
@ -151,6 +172,74 @@ test('Form:options:autoFill:data', async () => {
expect(onSubmit.mock.calls[1][0]).toMatchSnapshot();
});
test('Form:options:autoFill:keyWithPath', async () => {
const {container} = render(
amisRender(
{
type: 'form',
body: [
{
type: 'radios',
label: 'trigger',
name: 'trigger',
autoFill: {
'receiver.target1': '${value}',
'receiver.target2': '${value}'
},
options: [
{label: 'OptionA', value: 'a'},
{label: 'OptionB', value: 'b'}
]
},
{
type: 'input-text',
name: 'receiver.target1',
label: 'receiver.target1'
},
{
type: 'input-text',
name: 'receiver.target2',
label: 'receiver.target2'
},
{
type: 'input-text',
name: 'standalone',
label: 'standalone',
value: 'abc'
}
]
},
{},
makeEnv()
)
);
const triggerA = await screen.findByText(/OptionA/);
const triggerB = await screen.findByText(/OptionB/);
const target1 = container.querySelector(
'input[name=receiver\\.target1]'
) as HTMLInputElement;
const target2 = container.querySelector(
'input[name=receiver\\.target2]'
) as HTMLInputElement;
const standalone = container.querySelector(
'input[name=standalone]'
) as HTMLInputElement;
fireEvent.click(triggerA);
await wait(1000);
expect(target1.getAttribute('value')).toEqual('a');
expect(target2.getAttribute('value')).toEqual('a');
expect(standalone.getAttribute('value')).toEqual('abc');
fireEvent.click(triggerB);
await wait(1000);
expect(target1.getAttribute('value')).toEqual('b');
expect(target2.getAttribute('value')).toEqual('b');
expect(standalone.getAttribute('value')).toEqual('abc');
});
test('Form:options:autoFill:multiple:data', async () => {
const onSubmit = jest.fn();
const {container, getByText, findByText} = render(
@ -206,3 +295,107 @@ test('Form:options:autoFill:multiple:data', async () => {
expect(onSubmit).toBeCalledTimes(2);
expect(onSubmit.mock.calls[1][0]).toMatchSnapshot();
});
test('Form:options:autoFill:validation', async () => {
const onSubmitFn = jest.fn();
const submitText = 'Submit';
const validationMsg1 = '选项1校验失败数据必须为Option B';
const validationMsg2 = '选项2校验失败数据必须为Option B';
const {container} = render(
amisRender(
{
type: 'form',
submitText,
body: [
{
type: 'select',
label: '选项',
name: 'select',
placeholder: 'SelectCompt',
autoFill: {
instantValidate: '${label}',
submitValidate: '${label}'
},
clearable: true,
options: [
{label: 'OptionA', value: 'a'},
{label: 'OptionB', value: 'b'}
]
},
{
type: 'input-text',
name: 'instantValidate',
label: '选项1',
placeholder: '选项1',
description: '填充后立即校验',
required: true,
validateOnChange: true,
validations: {
equals: 'OptionB'
},
validationErrors: {
equals: validationMsg1
}
},
{
type: 'input-text',
name: 'submitValidate',
label: '选项2',
placeholder: '选项2',
description: '填充后提交表单时才校验',
required: true,
validations: {
equals: 'OptionB'
},
validationErrors: {
equals: validationMsg2
}
}
]
},
{onSubmit: onSubmitFn},
makeEnv({})
)
);
const select = container.querySelector(
'span[class*=Select-arrow]'
) as HTMLDivElement;
const option1 = container.querySelector(
'input[name=instantValidate]'
) as HTMLInputElement;
const option2 = container.querySelector(
'input[name=submitValidate]'
) as HTMLInputElement;
const submitBtn = screen.getByRole('button', {name: submitText});
// 自动填充触发后生成校验信息
fireEvent.click(select);
await wait(300);
fireEvent.click(screen.getByText(/OptionA/));
await wait(1000);
expect(option1.getAttribute('value')).toEqual('OptionA');
expect(option2.getAttribute('value')).toEqual('OptionA');
expect(screen.queryByText(validationMsg1)).toBeInTheDocument();
// 提交后校验选项2
fireEvent.click(submitBtn);
await wait(500);
expect(screen.queryByText(validationMsg2)).toBeInTheDocument();
// 自动填充再次触发后validateOnChange的选项消除校验信息
fireEvent.click(select);
await wait(300);
fireEvent.click(screen.getByText(/OptionB/));
await wait(1000);
expect(option1.getAttribute('value')).toEqual('OptionB');
expect(option1.getAttribute('value')).toEqual('OptionB');
expect(screen.queryByText(validationMsg1)).not.toBeInTheDocument();
expect(screen.queryByText(validationMsg2)).toBeInTheDocument();
// 提交后校验信息全部消除
fireEvent.click(submitBtn);
await wait(500);
expect(screen.queryByText(validationMsg1)).not.toBeInTheDocument();
expect(screen.queryByText(validationMsg2)).not.toBeInTheDocument();
});

View File

@ -0,0 +1,86 @@
import React = require('react');
import PageRenderer from '../../../../amis-core/src/renderers/Form';
import * as renderer from 'react-test-renderer';
import {
render,
fireEvent,
waitFor,
getByText,
prettyDOM
} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {makeEnv, wait} from '../../helper';
test('Renderer:input-image autoFill', async () => {
const fetcher = jest.fn().mockImplementation(() => {
return Promise.resolve({
data: {
status: 0,
msg: 'ok',
data: {
value: 'img.png',
filename: 'filename.png',
myUrl: 'http://amis.com/image.png'
}
}
});
});
global.URL.createObjectURL = jest.fn();
const {
debug,
container,
findByText,
getByLabelText,
findByPlaceholderText,
findByDisplayValue
} = render(
amisRender(
{
type: 'form',
api: '/api/xxx',
body: [
{
type: 'input-image',
name: 'img',
label: 'img',
receiver: '/api/upload/file',
autoFill: {
// 不知道为啥这里不能用 ${url},可能是有什么地方和真实浏览器不一致
myUrl: '${myUrl}'
}
},
{
type: 'input-text',
name: 'myUrl'
}
],
title: 'The form',
actions: []
},
{},
makeEnv({fetcher})
)
);
const fileInput = container.querySelector(
'input[type=file]'
)! as HTMLInputElement;
const file = new File(['file'], 'ping.png', {
type: 'image/png'
});
fireEvent.change(fileInput, {
target: {files: [file]}
});
await wait(500);
const textInput = container.querySelector(
'input[name=myUrl]'
)! as HTMLInputElement;
expect(textInput.value).toBe('http://amis.com/image.png');
});

View File

@ -324,319 +324,351 @@ exports[`Renderer:breadcrumb separator 1`] = `
`;
exports[`Renderer:breadcrumb tooltip labelMaxLength 1`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<div
className="cxd-Breadcrumb"
className="cxd-Page-body"
>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
className="cxd-Spinner-mark"
/>
<div
className="cxd-Breadcrumb"
>
<span
className="cxd-Breadcrumb-item-default"
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
>
<span
className="cxd-TplField"
className="cxd-Breadcrumb-item-default"
>
当前页面
<span
className="cxd-TplField"
>
当前页面
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:breadcrumb tooltip labelMaxLength 2`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<div
className="cxd-Breadcrumb"
className="cxd-Page-body"
>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
className="cxd-Spinner-mark"
/>
<div
className="cxd-Breadcrumb"
>
<span
className="cxd-Breadcrumb-item-default"
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
>
<span
className="cxd-TplField"
className="cxd-Breadcrumb-item-default"
>
当前页面
<span
className="cxd-TplField"
>
当前页面
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:breadcrumb tooltip labelMaxLength 3`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<div
className="cxd-Breadcrumb"
className="cxd-Page-body"
>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
className="cxd-Spinner-mark"
/>
<div
className="cxd-Breadcrumb"
>
<span
className="cxd-Breadcrumb-item-default"
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
>
<span
className="cxd-TplField"
className="cxd-Breadcrumb-item-default"
>
当前页面
<span
className="cxd-TplField"
>
当前页面
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:breadcrumb tooltip labelMaxLength 4`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<div
className="cxd-Breadcrumb"
className="cxd-Page-body"
>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
className="cxd-Spinner-mark"
/>
<div
className="cxd-Breadcrumb"
>
<span
className="cxd-Breadcrumb-item-default"
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
href="https://baidu.gitee.com/"
>
<i
className="cxd-Icon fa fa-home cxd-Breadcrumb-icon"
/>
<span
className="cxd-TplField"
/>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item"
>
<a
className="cxd-Breadcrumb-item-default"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-TplField"
>
上级页面上级页面上级页面上级页面...
</span>
</a>
</span>
<span
className="cxd-Breadcrumb-separator text-black"
>
&gt;
</span>
<span
className="cxd-Breadcrumb-item cxd-Breadcrumb-item-last"
>
<span
className="cxd-TplField"
className="cxd-Breadcrumb-item-default"
>
当前页面
<span
className="cxd-TplField"
>
当前页面
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:breadcrumb var 1`] = `

View File

@ -1,55 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renderer:Page 1`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-headerRow"
className="cxd-Page-main"
>
<div
className="cxd-Page-header"
className="cxd-Page-headerRow"
>
<h2
className="cxd-Page-title"
<div
className="cxd-Page-header"
>
<span
className="cxd-TplField"
<h2
className="cxd-Page-title"
>
<span
dangerouslySetInnerHTML={
{
"__html": "This is Title",
}
}
/>
</span>
<div
className="cxd-Remark cxd-Remark--warning"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-Remark-icon icon"
className="cxd-TplField"
>
<icon-mock
className=" icon-question-mark"
icon="question-mark"
<span
dangerouslySetInnerHTML={
{
"__html": "This is Title",
}
}
/>
</span>
</div>
</h2>
<small
className="cxd-Page-subTitle"
<div
className="cxd-Remark cxd-Remark--warning"
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
className="cxd-Remark-icon icon"
>
<icon-mock
className=" icon-question-mark"
icon="question-mark"
/>
</span>
</div>
</h2>
<small
className="cxd-Page-subTitle"
>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "This is SubTitle",
}
}
/>
</span>
</small>
</div>
<div
className="cxd-Page-toolbar"
>
<span
className="cxd-TplField"
@ -57,47 +74,38 @@ exports[`Renderer:Page 1`] = `
<span
dangerouslySetInnerHTML={
{
"__html": "This is SubTitle",
"__html": "This is toolbar",
}
}
/>
</span>
</small>
</div>
</div>
<div
className="cxd-Page-toolbar"
className="cxd-Page-body"
>
<span
className="cxd-Spinner-mark"
/>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "This is toolbar",
"__html": "This is body",
}
}
/>
</span>
</div>
</div>
<div
className="cxd-Page-body"
>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "This is body",
}
}
/>
</span>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:Page classNames 1`] = `
@ -979,87 +987,81 @@ exports[`Renderer:Page initApi error show Message 1`] = `
`;
exports[`Renderer:Page initApi reFetch when condition changes 1`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<div
className="cxd-Spinner-overlay in"
/>
<div
className="cxd-Spinner cxd-Spinner--overlay in"
data-testid="spinner"
>
<div
className="cxd-Spinner-icon cxd-Spinner-icon--lg cxd-Spinner-icon--default"
/>
</div>
<span
className="cxd-TplField"
className="cxd-Page-body"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 1",
}
}
className="cxd-Spinner-mark"
/>
</span>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 1",
}
}
/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:Page initApi reFetch when condition changes 2`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<div
className="cxd-Spinner-overlay in"
/>
<div
className="cxd-Spinner cxd-Spinner--overlay in"
data-testid="spinner"
>
<div
className="cxd-Spinner-icon cxd-Spinner-icon--lg cxd-Spinner-icon--default"
/>
</div>
<span
className="cxd-TplField"
className="cxd-Page-body"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 2",
}
}
className="cxd-Spinner-mark"
/>
</span>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 2",
}
}
/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:Page initApi reload by Dialog action 1`] = `
@ -1972,136 +1974,157 @@ exports[`Renderer:Page initApi silentPolling 2`] = `
`;
exports[`Renderer:Page initData 1`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<span
className="cxd-TplField"
<div
className="cxd-Page-body"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 1",
}
}
className="cxd-Spinner-mark"
/>
</span>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 1",
}
}
/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:Page initFetchOn trigger initApi fetch when condition becomes ture 1`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<div
className="cxd-Spinner-overlay in"
/>
<div
className="cxd-Spinner cxd-Spinner--overlay in"
data-testid="spinner"
>
<div
className="cxd-Spinner-icon cxd-Spinner-icon--lg cxd-Spinner-icon--default"
/>
</div>
<span
className="cxd-TplField"
className="cxd-Page-body"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 6",
}
}
className="cxd-Spinner-mark"
/>
</span>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 6",
}
}
/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:Page location query 1`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<span
className="cxd-TplField"
<div
className="cxd-Page-body"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 5",
}
}
className="cxd-Spinner-mark"
/>
</span>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 5",
}
}
/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;
exports[`Renderer:Page location query 2`] = `
<div
className="cxd-Page"
onClick={[Function]}
>
[
<div
className="cxd-Page-content"
className="cxd-Page"
onClick={[Function]}
>
<div
className="cxd-Page-main"
className="cxd-Page-content"
>
<div
className="cxd-Page-body"
className="cxd-Page-main"
>
<span
className="cxd-TplField"
<div
className="cxd-Page-body"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 6",
}
}
className="cxd-Spinner-mark"
/>
</span>
<span
className="cxd-TplField"
>
<span
dangerouslySetInnerHTML={
{
"__html": "The variable value is 6",
}
}
/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>,
<span
className="cxd-Spinner-mark"
/>,
]
`;

View File

@ -1,6 +1,6 @@
{
"name": "amis",
"version": "2.2.0",
"version": "2.3.0",
"description": "一种MIS页面生成工具",
"main": "lib/index.js",
"module": "esm/index.js",
@ -40,8 +40,8 @@
]
},
"dependencies": {
"amis-core": "^2.2.0",
"amis-ui": "^2.2.0",
"amis-core": "^2.3.0",
"amis-ui": "^2.3.0",
"attr-accept": "2.2.2",
"blueimp-canvastoblob": "2.1.0",
"classnames": "2.3.1",
@ -83,7 +83,7 @@
"@rollup/plugin-typescript": "^8.3.4",
"@svgr/rollup": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.0.0",
"@testing-library/react": "^13.4.0",
"@types/async": "^2.0.45",
"@types/codemirror": "^5.60.3",
"@types/echarts": "^4.9.2",
@ -215,7 +215,11 @@
"testPathIgnorePatterns": [
"/node_modules/",
"/.rollup.cache/"
]
],
"snapshotFormat": {
"escapeString": false,
"printBasicPrototype": false
}
},
"peerDependencies": {
"amis-core": "*",

View File

@ -215,9 +215,15 @@ export default class App extends React.Component<AppProps, object> {
this.unWatchRouteChange?.();
}
async reload(subpath?: any, query?: any, ctx?: any, silent?: boolean) {
async reload(
subpath?: any,
query?: any,
ctx?: any,
silent?: boolean,
replace?: boolean
) {
if (query) {
return this.receive(query);
return this.receive(query, undefined, replace);
}
const {
@ -246,10 +252,10 @@ export default class App extends React.Component<AppProps, object> {
}
}
receive(values: object) {
receive(values: object, subPath?: string, replace?: boolean) {
const {store} = this.props;
store.updateData(values);
store.updateData(values, undefined, replace);
this.reload();
}
@ -468,7 +474,7 @@ export class AppRenderer extends App {
super.componentWillUnmount();
}
setData(values: object) {
return this.props.store.updateData(values);
setData(values: object, replace?: boolean) {
return this.props.store.updateData(values, undefined, replace);
}
}

View File

@ -1501,9 +1501,8 @@ export default class CRUD extends React.Component<CRUDProps, any> {
}
}
handleQuery(values: object, forceReload: boolean = false) {
handleQuery(values: object, forceReload: boolean = false, replace?: boolean) {
const {store, syncLocation, env, pageField, perPageField} = this.props;
store.updateQuery(
{
...values,
@ -1513,21 +1512,22 @@ export default class CRUD extends React.Component<CRUDProps, any> {
? env.updateLocation
: undefined,
pageField,
perPageField
perPageField,
replace
);
this.search(undefined, undefined, undefined, forceReload);
}
reload(subpath?: string, query?: any) {
reload(subpath?: string, query?: any, replace?: boolean) {
if (query) {
return this.receive(query);
return this.receive(query, undefined, replace);
} else {
this.search(undefined, undefined, true, true);
}
}
receive(values: object) {
this.handleQuery(values, true);
receive(values: object, subPath?: string, replace?: boolean) {
this.handleQuery(values, true, replace);
}
reloadTarget(target: string, data: any) {
@ -1807,9 +1807,11 @@ export default class CRUD extends React.Component<CRUDProps, any> {
const {store, classPrefix: ns, classnames: cx, translate: __} = this.props;
const {page, lastPage} = store;
return page < lastPage ? (
return (
<div className={cx('Crud-loadMore')}>
<Button
disabled={page >= lastPage}
disabledTip={__('CRUD.loadMoreDisableTip')}
classPrefix={ns}
onClick={() =>
this.search({page: page + 1, loadDataMode: 'load-more'})
@ -1819,8 +1821,6 @@ export default class CRUD extends React.Component<CRUDProps, any> {
{__('CRUD.loadMore')}
</Button>
</div>
) : (
''
);
}
@ -2244,7 +2244,13 @@ export class CRUDRenderer extends CRUD {
scoped.unRegisterComponent(this);
}
reload(subpath?: string, query?: any, ctx?: any) {
reload(
subpath?: string,
query?: any,
ctx?: any,
silent?: boolean,
replace?: boolean
) {
const scoped = this.context as IScopedContext;
if (subpath) {
return scoped.reload(
@ -2253,16 +2259,16 @@ export class CRUDRenderer extends CRUD {
);
}
return super.reload(subpath, query);
return super.reload(subpath, query, replace);
}
receive(values: any, subPath?: string) {
receive(values: any, subPath?: string, replace?: boolean) {
const scoped = this.context as IScopedContext;
if (subPath) {
return scoped.send(subPath, values);
}
return super.receive(values);
return super.receive(values, undefined, replace);
}
reloadTarget(target: string, data: any) {

View File

@ -324,11 +324,17 @@ export class Chart extends React.Component<ChartProps> {
this.ref = ref;
}
reload(subpath?: string, query?: any) {
reload(
subpath?: string,
query?: any,
ctx?: any,
silent?: boolean,
replace?: boolean
) {
const {api, env, store, interval, translate: __} = this.props;
if (query) {
return this.receive(query);
return this.receive(query, undefined, replace);
} else if (!env || !env.fetcher || !isEffectiveApi(api, store.data)) {
return;
}
@ -389,10 +395,10 @@ export class Chart extends React.Component<ChartProps> {
});
}
receive(data: object) {
receive(data: object, subPath?: string, replace?: boolean) {
const store = this.props.store;
store.updateData(data);
store.updateData(data, undefined, replace);
this.reload();
}
@ -508,9 +514,9 @@ export class ChartRenderer extends Chart {
scoped.unRegisterComponent(this);
}
setData(values: object) {
setData(values: object, replace?: boolean) {
const {store} = this.props;
store.updateData(values);
store.updateData(values, undefined, replace);
// 重新渲染
this.renderChart(this.props.config, values);
}

View File

@ -100,6 +100,18 @@ export interface CollapseProps
}
export default class Collapse extends React.Component<CollapseProps, {}> {
static propsList: Array<string> = [
'collapsable',
'collapsed',
'collapseTitle',
'showArrow',
'headerPosition',
'bodyClassName',
'headingClassName',
'collapseHeader',
'size'
];
render() {
const {
id,

View File

@ -66,7 +66,7 @@ export default class Container<T> extends React.Component<
{children
? typeof children === 'function'
? ((children as any)(this.props) as JSX.Element)
: (children as unknown)
: (children as any)
: body
? (render('body', body as any, {disabled}) as JSX.Element)
: null}

View File

@ -120,7 +120,7 @@ export interface ComboControlSchema extends FormBaseControlSchema {
/**
* Add at top
*/
addattop?: boolean;
addattop?: boolean;
/**
*
@ -464,8 +464,15 @@ export default class ComboControl extends React.Component<ComboProps> {
}
addItemWith(condition: ComboCondition) {
const {flat, joinValues, addattop, delimiter, scaffold, disabled, submitOnChange} =
this.props;
const {
flat,
joinValues,
addattop,
delimiter,
scaffold,
disabled,
submitOnChange
} = this.props;
if (disabled) {
return;
@ -486,7 +493,7 @@ export default class ComboControl extends React.Component<ComboProps> {
value = value.join(delimiter || ',');
}
if (addattop === true){
if (addattop === true) {
value.unshift(value.pop());
}
@ -537,7 +544,7 @@ export default class ComboControl extends React.Component<ComboProps> {
value = value.join(delimiter || ',');
}
if (addattop === true){
if (addattop === true) {
value.unshift(value.pop());
}
@ -1623,7 +1630,7 @@ export default class ComboControl extends React.Component<ComboProps> {
})
export class ComboControlRenderer extends ComboControl {
// 支持更新指定索引的值
setData(value: any, index?: number) {
setData(value: any, replace?: boolean, index?: number) {
const {multiple, onChange, submitOnChange} = this.props;
if (multiple) {
if (index !== undefined && ~index) {

View File

@ -89,6 +89,14 @@ export default class FieldSetControl extends React.Component<
collapsable: false
};
static propsList: Array<string> = [
'collapsable',
'collapsed',
'collapseTitle',
'titlePosition',
'collapseTitle'
];
renderBody(): JSX.Element {
const {
body,
@ -116,7 +124,6 @@ export default class FieldSetControl extends React.Component<
formHorizontal: subFormHorizontal || formHorizontal
};
mode && (props.mode = mode);
typeof collapsable !== 'undefined' && (props.collapsable = collapsable);
horizontal && (props.horizontal = horizontal);
return (

View File

@ -836,7 +836,7 @@ export default class ImageControl extends React.Component<
// 排除自身的字段否则会无限更新state
const excludeSelfAutoFill = omit(autoFill, name || '');
if (!isEmpty(excludeSelfAutoFill) && onBulkChange && this.initAutoFill) {
if (!isEmpty(excludeSelfAutoFill) && onBulkChange) {
const files = this.state.files.filter(
file => ~['uploaded', 'init', 'ready'].indexOf(file.state as string)
);
@ -1341,7 +1341,6 @@ export default class ImageControl extends React.Component<
frameImageStyle.width = frameImageWidth;
}
const filterFrameImage = filter(frameImage, this.props.data, '| raw');
const hasPending = files.some(file => file.state == 'pending');
return (
<div className={cx(`ImageControl`, className)}>

View File

@ -63,18 +63,26 @@ export interface NumberControlSchema extends FormBaseControlSchema {
*/
unitOptions?: string | Array<Option> | string[] | PlainObject;
/**
*
*/
big?: boolean;
/**
*
*/
kilobitSeparator?: boolean;
/**
*
*/
readOnly?: boolean;
/**
*
*/
keyboard?: boolean;
/**
*
*/
@ -115,6 +123,10 @@ export interface NumberProps extends FormControlProps {
*
*/
displayMode?: 'base' | 'enhance';
/**
*
*/
big?: boolean;
}
interface NumberState {
@ -213,7 +225,11 @@ export default class NumberControl extends React.Component<
getValue(inputValue: any) {
const {resetValue, unitOptions} = this.props;
if (inputValue && typeof inputValue !== 'number') {
if (
inputValue &&
typeof inputValue !== 'number' &&
typeof inputValue !== 'string'
) {
return;
}
@ -253,6 +269,9 @@ export default class NumberControl extends React.Component<
}
filterNum(value: number | string | undefined) {
if (typeof value === 'undefined') {
return undefined;
}
if (typeof value !== 'number') {
value = filter(value, this.props.data);
value = /^[-]?\d+/.test(value) ? +value : undefined;
@ -315,7 +334,8 @@ export default class NumberControl extends React.Component<
unitOptions,
readOnly,
keyboard,
displayMode
displayMode,
big
} = this.props;
let precisionProps: any = {};
const finalPrecision = this.filterNum(precision);
@ -376,6 +396,7 @@ export default class NumberControl extends React.Component<
onBlur={() => this.dispatchEvent('blur')}
keyboard={keyboard}
displayMode={displayMode}
big={big}
/>
{unitOptions ? (
<Select

View File

@ -239,7 +239,7 @@ export interface DefaultProps {
export interface RangeItemProps extends RangeProps {
value: FormatValue;
updateValue: (value: Value) => void;
onChange: (value: Value) => void;
onAfterChange: () => void;
}
@ -298,7 +298,7 @@ export class Input extends React.Component<RangeItemProps, any> {
const {multiple, value: originValue, type, min} = this.props;
const _value = this.getValue(value, type);
this.props.updateValue(
this.props.onChange(
multiple
? {...(originValue as MultipleValue), [type]: _value}
: value ?? min
@ -314,7 +314,7 @@ export class Input extends React.Component<RangeItemProps, any> {
const {multiple, value: originValue, type} = this.props;
const _value = this.getValue(value, type);
this.props.updateValue(
this.props.onChange(
multiple ? {...(originValue as MultipleValue), [type]: _value} : value
);
}
@ -547,11 +547,11 @@ export default class RangeControl extends React.PureComponent<
}
/**
* value变换 -> updateValue
* value变换 -> onChange
* @param value
*/
@autobind
async updateValue(value: FormatValue) {
async onChange(value: FormatValue) {
this.setState({value: this.getValue(value)});
const {onChange, data, dispatchEvent} = this.props;
const result = this.getFormatValue(value);
@ -604,7 +604,7 @@ export default class RangeControl extends React.PureComponent<
const props: RangeItemProps = {
...this.props,
value,
updateValue: this.updateValue,
onChange: this.onChange,
onAfterChange: this.onAfterChange
};

View File

@ -1,5 +1,11 @@
import React from 'react';
import {FormItem, FormControlProps, FormBaseControl} from 'amis-core';
import {
FormItem,
FormControlProps,
FormBaseControl,
buildApi,
qsstringify
} from 'amis-core';
import cx from 'classnames';
import {LazyComponent} from 'amis-core';
import {tokenize} from 'amis-core';
@ -108,8 +114,23 @@ export default class RichTextControl extends React.Component<
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleChange = this.handleChange.bind(this);
const imageReceiver = normalizeApi(
props.receiver,
props.receiver.method || 'post'
);
imageReceiver.data = imageReceiver.data || {};
const imageApi = buildApi(imageReceiver, props.data, {
method: props.receiver.method || 'post'
});
if (finnalVendor === 'froala') {
const videoReceiver = normalizeApi(
props.videoReceiver,
props.videoReceiver.method || 'post'
);
videoReceiver.data = videoReceiver.data || {};
const videoApi = buildApi(videoReceiver, props.data, {
method: props.videoReceiver.method || 'post'
});
this.config = {
imageAllowedTypes: ['jpeg', 'jpg', 'png', 'gif'],
imageDefaultAlign: 'left',
@ -135,13 +156,15 @@ export default class RichTextControl extends React.Component<
...props.options,
editorClass: props.editorClass,
placeholderText: props.translate(props.placeholder),
imageUploadURL: tokenize(props.receiver, props.data),
imageUploadURL: imageApi.url,
imageUploadParams: {
from: 'rich-text'
from: 'rich-text',
...imageApi.data
},
videoUploadURL: tokenize(props.videoReceiver, props.data),
videoUploadURL: videoApi.url,
videoUploadParams: {
from: 'rich-text'
from: 'rich-text',
...videoApi.data
},
events: {
...(props.options && props.options.events),
@ -162,8 +185,19 @@ export default class RichTextControl extends React.Component<
images_upload_handler: (blobInfo: any, progress: any) =>
new Promise(async (resolve, reject) => {
const formData = new FormData();
if (imageApi.data) {
qsstringify(imageApi.data)
.split('&')
.filter(item => item !== '')
.forEach(item => {
let parts = item.split('=');
formData.append(parts[0], decodeURIComponent(parts[1]));
});
}
formData.append(
props.fileField,
props.fileField || 'file',
blobInfo.blob(),
blobInfo.filename()
);
@ -176,7 +210,7 @@ export default class RichTextControl extends React.Component<
data: payload
};
},
...normalizeApi(tokenize(props.receiver, props.data), 'post')
...imageApi
};
const response = await fetcher(receiver, formData, {

View File

@ -620,9 +620,15 @@ export default class Page extends React.Component<PageProps> {
});
}
reload(subpath?: any, query?: any, ctx?: any, silent?: boolean) {
reload(
subpath?: any,
query?: any,
ctx?: any,
silent?: boolean,
replace?: boolean
) {
if (query) {
return this.receive(query);
return this.receive(query, undefined, replace);
}
const {store, initApi} = this.props;
@ -636,10 +642,10 @@ export default class Page extends React.Component<PageProps> {
.then(this.initInterval);
}
receive(values: object) {
receive(values: object, subPath?: string, replace?: boolean) {
const {store} = this.props;
store.updateData(values);
store.updateData(values, undefined, replace);
this.reload();
}
@ -1024,7 +1030,7 @@ export class PageRenderer extends Page {
}, 300);
}
setData(values: object) {
return this.props.store.updateData(values);
setData(values: object, replace?: boolean) {
return this.props.store.updateData(values, undefined, replace);
}
}

View File

@ -535,9 +535,15 @@ export default class Service extends React.Component<ServiceProps> {
return value;
}
reload(subpath?: string, query?: any, ctx?: RendererData, silent?: boolean) {
reload(
subpath?: string,
query?: any,
ctx?: RendererData,
silent?: boolean,
replace?: boolean
) {
if (query) {
return this.receive(query);
return this.receive(query, undefined, replace);
}
const {
@ -587,10 +593,10 @@ export default class Service extends React.Component<ServiceProps> {
this.reload(target, query, undefined, true);
}
receive(values: object) {
receive(values: object, subPath?: string, replace?: boolean) {
const {store} = this.props;
store.updateData(values);
store.updateData(values, undefined, replace);
this.reload();
}
@ -772,7 +778,13 @@ export class ServiceRenderer extends Service {
scoped.registerComponent(this as ScopedComponentType);
}
reload(subpath?: string, query?: any, ctx?: any, silent?: boolean) {
reload(
subpath?: string,
query?: any,
ctx?: any,
silent?: boolean,
replace?: boolean
) {
const scoped = this.context as IScopedContext;
if (subpath) {
return scoped.reload(
@ -781,16 +793,16 @@ export class ServiceRenderer extends Service {
);
}
return super.reload(subpath, query, ctx, silent);
return super.reload(subpath, query, ctx, silent, replace);
}
receive(values: any, subPath?: string) {
receive(values: any, subPath?: string, replace?: boolean) {
const scoped = this.context as IScopedContext;
if (subPath) {
return scoped.send(subPath, values);
}
return super.receive(values);
return super.receive(values, subPath, replace);
}
componentWillUnmount() {
@ -804,7 +816,7 @@ export class ServiceRenderer extends Service {
scoped.reload(target, data);
}
setData(values: object) {
return this.props.store.updateData(values);
setData(values: object, replace?: boolean) {
return this.props.store.updateData(values, undefined, replace);
}
}

View File

@ -376,7 +376,11 @@ export type Table2RendererEvent =
| 'columnToggled'
| 'dragOver';
export type Table2RendererAction = 'selectAll' | 'clearAll' | 'select';
export type Table2RendererAction =
| 'selectAll'
| 'clearAll'
| 'select'
| 'expand';
export interface Table2Props extends RendererProps {
title?: string;
@ -408,7 +412,7 @@ export default class Table2 extends React.Component<Table2Props, object> {
static contextType = ScopedContext;
renderedToolbars: Array<string> = [];
control: any;
tableRef?: any;
constructor(props: Table2Props, context: IScopedContext) {
super(props);
@ -438,16 +442,6 @@ export default class Table2 extends React.Component<Table2Props, object> {
scoped.unRegisterComponent(this);
}
@autobind
controlRef(control: any) {
// 因为 control 有可能被 n 层 hoc 包裹。
while (control && control.getWrappedInstance) {
control = control.getWrappedInstance();
}
this.control = control;
}
syncSelected() {
const {store, onSelect} = this.props;
@ -934,7 +928,7 @@ export default class Table2 extends React.Component<Table2Props, object> {
reload && this.reloadTarget(reload, data);
})
.catch(() => {
options?.resetOnFailed && this.control.reset();
options?.resetOnFailed && this.reset();
});
}
}
@ -1199,10 +1193,11 @@ export default class Table2 extends React.Component<Table2Props, object> {
}
doAction(action: ActionObject, args: any, throwErrors: boolean): any {
const {store, rowSelection, data} = this.props;
const {store, rowSelection, data, keyField: key, expandable} = this.props;
const actionType = action?.actionType as string;
const keyField = rowSelection?.keyField;
const keyField = rowSelection?.keyField || key || 'key';
const dataSource = store.getData(data).items || [];
switch (actionType) {
case 'selectAll':
@ -1212,24 +1207,71 @@ export default class Table2 extends React.Component<Table2Props, object> {
store.updateSelected([], keyField);
break;
case 'select':
const dataSource = store.getData(data);
const selected: Array<any> = [];
dataSource.items.forEach((item: any, rowIndex: number) => {
dataSource.forEach((item: any, rowIndex: number) => {
const flag = evalExpression(args?.selectedRowKeysExpr, {
record: item,
rowIndex
});
if (flag && keyField) {
if (flag) {
selected.push(item[keyField]);
}
});
store.updateSelected(selected, keyField);
break;
case 'expand':
const expandableKey = expandable?.keyField || key || 'key';
const expanded: Array<any> = [];
const collapse: Array<any> = [];
// value值控制展开1个
if (args?.value) {
const rowIndex = dataSource.findIndex(
(d: any) => d[expandableKey] === args.value
);
const item = dataSource[rowIndex];
if (this.tableRef && this.tableRef.isExpandableRow(item, rowIndex)) {
if (this.tableRef.isExpanded(item)) {
collapse.push(item);
} else {
expanded.push(item);
}
}
} else if (args?.expandedRowsExpr) {
dataSource.forEach((item: any, rowIndex: number) => {
const flag = evalExpression(args?.expandedRowsExpr, {
record: item,
rowIndex
});
if (
flag &&
this.tableRef &&
this.tableRef.isExpandableRow(item, rowIndex)
) {
if (this.tableRef.isExpanded(item)) {
collapse.push(item);
} else {
expanded.push(item);
}
}
});
}
if (expanded.length > 0) {
this.tableRef && this.tableRef.onExpandRows(expanded);
}
if (collapse.length > 0) {
this.tableRef && this.tableRef.onCollapseRows(collapse);
}
break;
default:
break;
}
}
@autobind
getRef(ref: any) {
this.tableRef = ref;
}
renderTable() {
const {
render,
@ -1393,6 +1435,7 @@ export default class Table2 extends React.Component<Table2Props, object> {
return (
<Table
{...rest}
onRef={this.getRef}
title={this.renderSchema('title', title, {data: this.props.data})}
footer={this.renderSchema('footer', footer, {data: this.props.data})}
columns={this.buildColumns(store.filteredColumns)}

View File

@ -423,9 +423,15 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
throw new Error('Please implements this!');
}
reload(subPath?: string, query?: any, ctx?: any) {
reload(
subPath?: string,
query?: any,
ctx?: any,
silent?: boolean,
replace?: boolean
) {
if (query) {
return this.receive(query);
return this.receive(query, undefined, replace);
}
const {
@ -486,10 +492,10 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
}
}
receive(values: object) {
receive(values: object, subPath?: string, replace?: boolean) {
const {store} = this.props;
store.updateData(values);
store.updateData(values, undefined, replace);
this.reload();
}
@ -1243,7 +1249,7 @@ export class WizardRenderer extends Wizard {
}
}
setData(values: object) {
return this.props.store.updateData(values);
setData(values: object, replace?: boolean) {
return this.props.store.updateData(values, undefined, replace);
}
}

View File

@ -305,7 +305,7 @@ main().catch(e => {
const sourceFile = node.getSourceFile();
const position = sourceFile.getLineAndCharacterOfPosition(node.pos);
console.log(sourceFile, position);
console.log(
`\x1b[36m${sourceFile.fileName}:${position.line + 1}:${
position.character + 1