Merge branch 'baidu:master' into fix-transfer

This commit is contained in:
zhou999 2022-09-29 19:20:05 +08:00 committed by GitHub
commit a33f9e7779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 2653 additions and 1264 deletions

10
.vscode/settings.json vendored
View File

@ -7,6 +7,7 @@
"files.exclude": {
"**/lib": true,
"**/esm": true,
"**/sdk": true,
"**/tsconfig.tsbuildinfo": true,
"**/schema.json": true,
"**/lerna-debug.log": true,
@ -15,6 +16,15 @@
"public/**": true
},
"search.exclude": {
"**/lib": true,
"**/esm": true,
"**/sdk": true,
"**/tsconfig.tsbuildinfo": true,
"**/schema.json": true,
"**/lerna-debug.log": true,
"**/package-lock.json": true,
"**/.rollup.cache": true,
"public/**": true,
"examples/docs.json": true,
"examples/components/EChartsEditor/option-parts/**/*": true
}

View File

@ -882,6 +882,7 @@ combo 还有一个作用是增加层级,比如返回的数据是一个深层
| joinValues | `boolean` | `true` | 默认为 `true` 当扁平化开启的时候,是否用分隔符的形式发送给后端,否则采用 array 的方式。 |
| delimiter | `string` | `false` | 当扁平化开启并且 joinValues 为 true 时,用什么分隔符。 |
| addable | `boolean` | `false` | 是否可新增 |
| addattop | `boolean` | `false` | 在顶部添加 |
| removable | `boolean` | `false` | 是否可删除 |
| deleteApi | [API](../../../docs/types/api) | | 如果配置了,则删除前会发送一个 api请求成功才完成删除 |
| deleteConfirmText | `string` | `"确认要删除?"` | 当配置 `deleteApi` 才生效!删除时用来做用户确认 |

View File

@ -218,8 +218,34 @@ order: 1
"label": "禁用",
"name": "text2",
"disabled": true
},
{
"type": "grid",
"columns": [
{
"body": [
{
"type": "input-text",
"label": "姓名",
"name": "name",
"value": "amis",
"disabled": true
}
]
},
{
"body": [
{
"type": "input-email",
"label": "邮箱",
"name": "email",
"disabled": true
}
]
}
]
},
]
}
```

View File

@ -250,6 +250,8 @@ order: 56
## 前缀和后缀
`prefix``suffix` 属性支持数据映射。
```schema: scope="body"
{
"type": "form",
@ -281,10 +283,10 @@ order: 56
}
```
支持数据映射
## 显示计数器
配置`"showCounter": true`后输入框将显示计数器,一般会配合`maxLength`属性以限制输入长度,如果不设置`maxLength`,则仅展示计数器,并不会限制用户的输入长度。
```schema: scope="body"
{
"type": "form",
@ -294,14 +296,33 @@ order: 56
"type": "input-text",
"label": "A",
"showCounter": true,
"placeholder": "请输入"
"placeholder": "请输入",
"showCounter": true,
"options": [
{
"label": "aa",
"value": "aa"
},
{
"label": "bb",
"value": "bb"
},
{
"label": "cc",
"value": "cc"
},
{
"label": "dd",
"value": "dd"
}
]
},
{
"name": "b",
"type": "input-text",
"label": "B",
"showCounter": true,
"maxLength": 100,
"maxLength": 20,
"placeholder": "请输入"
}
]
@ -382,7 +403,7 @@ order: 56
当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`event.data.xxx`事件参数变量来获取事件产生的数据,详细请查看[事件动作](../../docs/concepts/event-action)。
| 事件名称 | 事件参数 | 说明 |
| -------- | --------------------------------- | --------------------------------------------- |
| -------- | --------------------------------- | ---------------------------------------------- |
| click | `event.data.value: string` 输入值 | 点击输入框时触发,只针对选择器模式的输入框有效 |
| enter | `event.data.value: string` 输入值 | 回车时触发,只针对选择器模式的输入框有效 |
| focus | `event.data.value: string` 输入值 | 输入框获取焦点时触发 |

View File

@ -518,3 +518,4 @@ order: 35
| 事件名称 | 事件参数 | 说明 |
| -------- | ------------------------------------------------------------------ | ---------------- |
| change | `event.data.value: string`<br/> `event.data.option: Option` 选中值 | 选中值变化时触发 |
| itemclick | `event.data.label: string`<br/> `event.data.id: string` | 点击时触发 |

View File

@ -37,7 +37,8 @@ order: 57
"name": "textarea",
"type": "textarea",
"label": "多行文本",
"clearable": true
"clearable": true,
"value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmodtion tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
]
}
@ -63,6 +64,8 @@ order: 57
## 显示计数器
配置`"showCounter": true`后输入框将显示计数器,一般会配合`maxLength`属性以限制输入长度,如果不设置`maxLength`,则仅展示计数器,并不会限制用户的输入长度。
```schema: scope="body"
{
"type": "form",
@ -80,7 +83,7 @@ order: 57
"type": "textarea",
"label": "B",
"showCounter": true,
"maxLength": 100,
"maxLength": 30,
"placeholder": "请输入"
}
]

View File

@ -322,10 +322,28 @@ List 的内容、Card 卡片的内容配置同上
}
```
## 工具栏
> 2.2.0 及以上版本
配置`"showToolbar": true`使图片在放大模式下开启图片工具栏。配置`"toolbarActions"`属性可以自定义工具栏的展示方式,具体配置参考[ImageAction](./image#imageaction)
```schema
{
"type": "page",
"body": {
"type": "image",
"src": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"enlargeAble": true,
"showToolbar": true
}
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| -------------- | ------------------------------------ | --------- | -------------------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| -------------- | ------------------------------------ | --------- | -------------------------------------------------------------------------------------- | ------- |
| type | `string` | | 如果在 Table、Card 和 List 中,为`"image"`;在 Form 中用作静态展示,为`"static-image"` |
| className | `string` | | 外层 CSS 类名 |
| innerClassName | `string` | | 组件内层 CSS 类名 |
@ -346,3 +364,22 @@ List 的内容、Card 卡片的内容配置同上
| thumbMode | `string` | `contain` | 预览图模式,可选:`'w-full'`, `'h-full'`, `'contain'`, `'cover'` |
| thumbRatio | `string` | `1:1` | 预览图比例,可选:`'1:1'`, `'4:3'`, `'16:9'` |
| imageMode | `string` | `thumb` | 图片展示模式,可选:`'thumb'`, `'original'` 即:缩略图模式 或者 原图模式 |
| showToolbar | `boolean` | `false` | 放大模式下是否展示图片的工具栏 | `2.2.0` |
| toolbarActions | `ImageAction[]` | | 图片工具栏,支持旋转,缩放,默认操作全部开启 | `2.2.0` |
#### ImageAction
```typescript
interface ImageAction {
/* 操作key */
key: 'rotateRight' | 'rotateLeft' | 'zoomIn' | 'zoomOut' | 'scaleOrigin';
/* 动作名称 */
label?: string;
/* 动作icon */
icon?: string;
/* 动作自定义CSS类 */
iconClassName?: string;
/* 动作是否禁用 */
disabled?: boolean;
}
```

View File

@ -71,7 +71,7 @@ Array<{
title?: string; // 标题
description?: string; // 描述
[propName: string]: any; // 还可以有其他数据
}>
}>;
```
### 配置预览图地址
@ -454,10 +454,57 @@ List 的内容、Card 卡片的内容配置同上
}
```
## 工具栏
> 2.2.0 及以上版本
配置`"showToolbar": true`使图片在放大模式下开启图片工具栏。配置`"toolbarActions"`属性可以自定义工具栏的展示方式,具体配置参考[ImageAction](./image#imageaction)
```schema
{
"type": "page",
"data": {
"images": [
{
"image": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692722/4f3cb4202335.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"a": "aaa1",
"b": "bbb1"
},
{
"image": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395692942/d8e4992057f9.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"a": "aaa2",
"b": "bbb2"
},
{
"image": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693148/1314a2a3d3f6.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"a": "aaa3",
"b": "bbb3"
},
{
"image": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693379/8f2e79f82be0.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"a": "aaa4",
"b": "bbb4"
},
{
"image": "https://internal-amis-res.cdn.bcebos.com/images/2020-1/1578395693566/552b175ef11d.jpeg@s_0,w_216,l_1,f_jpg,q_80",
"a": "aaa5",
"b": "bbb5"
}
]
},
"body": {
"type": "images",
"source": "${images}",
"enlargeAble": true,
"showToolbar": true
}
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ------------ | ------------------------------------------ | --------- | ---------------------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| -------------- | ------------------------------------------ | --------- | ---------------------------------------------------------------------------------------- | ------- |
| type | `string` | `images` | 如果在 Table、Card 和 List 中,为`"images"`;在 Form 中用作静态展示,为`"static-images"` |
| className | `string` | | 外层 CSS 类名 |
| defaultImage | `string` | | 默认展示图片 |
@ -469,3 +516,5 @@ List 的内容、Card 卡片的内容配置同上
| enlargeAble | `boolean` | | 支持放大预览 |
| thumbMode | `string` | `contain` | 预览图模式,可选:`'w-full'`, `'h-full'`, `'contain'`, `'cover'` |
| thumbRatio | `string` | `1:1` | 预览图比例,可选:`'1:1'`, `'4:3'`, `'16:9'` |
| showToolbar | `boolean` | `false` | 放大模式下是否展示图片的工具栏 | `2.2.0` |
| toolbarActions | `ImageAction[]` | | 图片工具栏,支持旋转,缩放,默认操作全部开启 | `2.2.0` |

View File

@ -161,7 +161,7 @@ public class StreamingResponseBodyController {
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ------------ | --------- | ------ | ----------------------------------------------- |
| ----------- | --------- | ------ | -------------------------------------------------------- |
| height | `number` | 500 | 展示区域高度 |
| className | `string` | | 外层 CSS 类名 |
| autoScroll | `boolean` | true | 是否自动滚动 |
@ -170,5 +170,4 @@ public class StreamingResponseBodyController {
| source | `string` | | 接口 |
| rowHeight | `number` | | 设置每行高度,将会开启虚拟渲染 |
| maxLength | `number` | | 最大显示行数 |
| disableColor | `boolean` | | 关闭 ANSI 颜色支持 |
| operation | `Array` | | 可选日志操作:['stop','clear','showLineNumber','filter'] |

View File

@ -125,7 +125,6 @@ order: 57
"name": "id",
"label": "Id"
},
{
"name": "type",
"label": "映射",

View File

@ -638,18 +638,57 @@ ws.on('connection', function connection(ws) {
}
```
### 函数触发事件
> 2.3.0 及以上版本
```schema: scope="body"
{
"type": "service",
"api": "/api/mock2/page/initData",
"dataProvider": {
"inited": "setData({ addedNumber: data.number + 1 })",
"onApiFetched": "setData({ year: new Date(data.date).getFullYear(), })"
},
"data": {
"number": 8887
},
"body": {
"type": "panel",
"title": "$title",
"body": [
{
"type": "tpl",
"wrapperComponent": "p",
"tpl": "静态数字为:<strong>${addedNumber}</strong>"
},
{
"type": "tpl",
"wrapperComponent": "p",
"tpl": "接口返回值的日期为:<strong>${date}</strong>"
},
{
"type": "tpl",
"wrapperComponent": "p",
"tpl": "接口返回值的年份为:<strong>${year}</strong>"
},
]
}
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------------------- | ----------------------------------------- | -------------- | ----------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| --------------------- | ----------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| type | `string` | `"service"` | 指定为 service 渲染器 |
| className | `string` | | 外层 Dom 的类名 |
| body | [SchemaNode](../../docs/types/schemanode) | | 内容容器 |
| api | [api](../../docs/types/api) | | 初始化数据域接口地址 |
| api | [API](../../docs/types/api) | | 初始化数据域接口地址 |
| ws | `string` | | WebScocket 地址 |
| dataProvider | `string` | | 数据获取函数 |
| dataProvider | `string \| Record<"inited" \| "onApiFetched" \| "onSchemaApiFetched" \| "onWsFetched", string>` | | 数据获取函数 | <ul><li>`1.4.0`</li><li>`1.8.0`支持`env`参数</li><li>`2.3.0` 支持基于事件触发</li></ul> |
| initFetch | `boolean` | | 是否默认拉取 |
| schemaApi | [api](../../docs/types/api) | | 用来获取远程 Schema 接口地址 |
| schemaApi | [API](../../docs/types/api) | | 用来获取远程 Schema 接口地址 |
| initFetchSchema | `boolean` | | 是否默认拉取 Schema |
| messages | `Object` | | 消息提示覆写,默认消息读取的是接口返回的 toast 提示文字,但是在此可以覆写它。 |
| messages.fetchSuccess | `string` | | 接口请求成功时的 toast 提示文字 |

View File

@ -10,6 +10,8 @@ order: 65
## 基本用法
它最适合的用法是放在列表类组件CRUDTableList 等)的列中,用来表示状态。
```schema: scope="body"
{
"type": "status",
@ -17,45 +19,81 @@ order: 65
}
```
它最适合的用法是放在 crud 的列中,用来表示状态
## 默认状态列表
```schema
下表是默认支持的几种状态:
```schema: scope="body"
{
"type": "page",
"body": [
"type": "table",
"data": {
"items": [
{
"type": "status",
"value": 0
"label": "-",
"value": "0",
"icon": "fail",
"status": 0
},
{
"type": "status",
"value": 1
"label": "-",
"value": "1",
"icon": "success",
"status": 1
},
{
"type": "status",
"value": "success"
"label": "成功",
"value": "success",
"icon": "success",
"status": "success"
},
{
"type": "status",
"value": "pending"
"label": "运行中",
"value": "pending",
"icon": "rolling",
"status": "pending"
},
{
"type": "status",
"value": "fail"
"label": "排队中",
"value": "queue",
"icon": "warning",
"status": "queue"
},
{
"type": "status",
"value": "fail"
"label": "调度中",
"value": "schedule",
"icon": "schedule",
"status": "schedule"
},
{
"type": "status",
"value": "queue"
"label": "失败",
"value": "fail",
"icon": "fail",
"status": "fail"
}
]
},
"columns": [
{
"name": "value",
"label": "默认value值"
},
{
"type": "status",
"value": "schedule"
"name": "label",
"label": "默认label"
},
{
"name": "icon",
"label": "默认icon值"
},
{
"name": "status",
"label": "状态",
"type": "mapping",
"map": {
"*": {
"type": "status"
}
}
}
]
}
@ -63,7 +101,7 @@ order: 65
## 自定义状态图标和文本
通过 `map``mapLabel`
如果默认提供的状态无法满足业务需求,可以使用`map` 和 `labelMap`属性分别配置状态组件的**图标**和**展示文案**。用户自定义的`map` 和 `labelMap`会和默认属性进行 merge如果只需要修改某一项配置时无需全量覆盖。
```schema
{
@ -79,7 +117,8 @@ order: 65
"0": "正常",
"1": "异常"
},
"value": 0
"value": 0,
"className": "mr-3"
},
{
"type": "status",
@ -97,12 +136,97 @@ order: 65
}
```
## 动态数据
`map``labelMap`支持配置变量,通过数据映射获取上下文中的变量。
> 2.3.0 及以上版本
```schema
{
"type": "page",
"data": {
"statusLabel": {
"success": "任务成功",
"warning": "已停机"
},
"statusIcon": {
"waiting": "far fa-clock",
"warning": "fas fa-exclamation"
}
},
"body": {
"type": "table",
"data": {
"items": [
{
"id": "1",
"name": "Task1",
"status": "success"
},
{
"id": "2",
"name": "Task2",
"status": "processing",
},
{
"id": "3",
"name": "Task3",
"status": "waiting",
},
{
"id": "4",
"name": "Task4",
"status": "warning",
},
{
"id": "5",
"name": "Task5",
"status": "fail",
}
]
},
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "name",
"label": "Name"
},
{
"name": "status",
"label": "状态",
"type": "mapping",
"map": {
"*": {
"type": "status",
"map": {
"processing": "rolling",
"waiting": "${statusIcon.waiting}",
"warning": "${statusIcon.warning}"
},
"labelMap": {
"success": "${statusLabel.success}",
"processing": "处理中",
"waiting": "等待中",
"warning": "${statusLabel.warning}"
}
}
}
}
]
}
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | -------- | ------ | ------------------------------- |
| type | `string` | | `"status"` 指定为 Status 渲染器 |
| className | `string` | | 外层 Dom 的类名 |
| className | `string` | | 外层 Dom 的 CSS 类名 |
| placeholder | `string` | `-` | 占位文本 |
| map | `object` | | 映射图标 |
| labelMap | `object` | | 映射文本 |

View File

@ -650,7 +650,11 @@ order: 67
{
"type": "service",
"api": "/api/mock2/sample?perPage=5",
"className": "w-xxl",
"className": "flex justify-center",
"body": [
{
"type": "wrapper",
"className": "w-xxl border-2 border-solid border-indigo-400",
"body": [
{
"type": "table",
@ -659,40 +663,42 @@ order: 67
"columnsTogglable": false,
"columns": [
{
"name": "engine",
"label": "Engine",
"name": "id",
"label": "ID",
"fixed": "left"
},
{
"name": "engine",
"label": "Engine",
"groupName": 'Group-1',
"fixed": "left"
},
{
"name": "grade",
"label": "Grade"
"label": "Grade",
},
{
"name": "version",
"label": "Version"
},
{
"name": "browser",
"label": "Browser"
"label": "Browser",
"groupName": 'Group-2',
"fixed": "right"
},
{
"name": "id",
"label": "ID"
},
{
"name": "platform",
"label": "Platform",
"groupName": 'Group-2',
"fixed": "right"
}
]
}
]
}
]
}
```
### 可复制
@ -1415,7 +1421,7 @@ popOver 的其它配置请参考 [popover](./popover)
{
"type": "table",
"source": "$rows",
"rowClassNameExpr": "<%= data.id % 2 ? 'bg-success' : '' %>",
"rowClassNameExpr": "<%= data.id % 2 ? 'bg-success' : 'bg-blue-50' %>",
"columns": [
{
"name": "engine",
@ -1876,7 +1882,6 @@ popOver 的其它配置请参考 [popover](./popover)
| selectable | `boolean` | `false` | 支持勾选 |
| multiple | `boolean` | `false` | 勾选 icon 是否为多选样式`checkbox` 默认为`radio` |
## 列配置属性表
| 属性名 | 类型 | 默认值 | 说明 |

View File

@ -18,10 +18,12 @@ order: 12
"data": {
"name": "rick"
},
"body": {
"body": [
{
"type": "tpl",
"tpl": "my name is ${name}" // 输出: my name is rick
}
]
}
```
@ -812,7 +814,7 @@ ${xxx | url_encode}
### url_decode
效果同 [decodeURIComponent() - JavaScript | MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent)
效果同 [decodeURIComponent() - JavaScript | MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent),注意从`2.3.0`版本开始,不合法的输入会被转化为`undefined`。
##### 基本用法

View File

@ -8,42 +8,124 @@ icon:
order: 13
---
一般来说,属性名类似于`xxxOn` 或者 `className` 的配置项,都可以使用表达式进行配置,表达式具有如下的语法:
## 使用场景
amis 中有很多场景会用到表达式。
- 模板中变量取值。 如:`my name is ${xxx}`
- api 地址参数取值 如 `http://mydomain.com/api/xxx?id=${id}`
- api 发送&接收数据映射
```
{
"type": "crud",
"api": {
method: "post"
url: "http://mydomain.com/api/xxx",
data: {
skip: "${(page - 1) * perPage}",
take: "${perPage}"
}
},
...
}
```
- 组件显示与隐藏条件
```
{
"name": "xxxText",
"type": "input-text",
"visibleOn": "${ xxxFeature.on }"
}
```
- 表单默认值
```
{
"name": "xxxText",
"type": "input-text",
"value": "${ TODAY() }"
}
```
- 等等
amis 中表达式有两种语法:
- 一种是纯 js 表达式,如 `data.xxx === 1`
- 另一种是用 `${``}` 包裹的表达式。如:`${ xxx === 1}`。
```json
{
"type": "tpl",
"tpl": "当前作用域中变量 show 是 1 的时候才可以看得到我哦~",
"visibleOn": "this.show === 1"
"visibleOn": "${show === 1}"
}
```
其中:`this.show === 1` 就是表达式。
第一种是早期的版本,偷懒直接用的 [Javascript](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript),灵活性虽高,但是安全性欠佳。建议使用新版本规则,新规则跟 tpl 模板取值规则完全一样,不用来回切换语法
## 表达式语法
> 表达式语法实际上是 JavaScript 代码,更多 JavaScript 知识查看 [这里](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript)。
表达式主要由三部分组成:开始字符、表达式内容和结束字符。其中开始字符固定是 `${` 结束字符固定是 `}`,中间内容才是表达式正文。这里说的语法主要还是表达式正文的语法
在 amis 的实现过程中,当正则匹配到某个组件存在`xxxOn`语法的属性名时,会尝试进行下面步骤(以上面配置为例):
规则主要包含
1. 提取`visibleOn`配置项配置的 JavaScript 语句`this.show === 1`,并以当前组件的数据域为这段代码的数据作用域,执行这段 js 代码;
2. 之后将执行结果赋值给`visible`并添加到组件属性中
3. 执行渲染。当前示例中:`visible`代表着是否显示当前组件;
- 变量名: `xxx变量`、`xxx变量.xxx属性`、`xxx变量[xxx属性]`
- 布尔值: `true` 或者 `false`
- null`null`
- undefined: `undefined`
- 数字: `123` 或者 `123.23`
- 字符串: `"string"` 或者 `'string'`
- 字符模板
组件不同的配置项会有不同的效果,请大家在组件文档中多留意。
```
`my name is ${name}`
```
> 表达式的执行结果预期应该是`boolean`类型值如果不是amis 会根据 JavaScript 的规则将结果视作`boolean`类型进行判断
- 数组: `[1, 2, 3]`
- 对象: `{a: 1, b: 2}`
- 组合使用如: `{a: 1: b: [1, 2, 3], [key]: yyy变量}`
- 三元表达式: `xx变量 == 1 ? 2 : 3`
- 二元表达式: `xx变量 && yy 变量``xx变量 || yy 变量``xx变量 == 123`
## 新表达式语法
- `+` 相加
- `-` 相减
- `*`
- `/`
- `**` pow 运算
- `||` 或者
- `&&` 并且
- `|` 或运算
- `^` 异或运算
- `&` 与运算
- `==` 等于比较
- `!=` 不等于
- `===` 恒等于
- `!==` 不恒等于
- `<` 小于
- `<=` 小于或等于
- `>` 大于
- `>=` 大于或等于
- `<<` 左移
- `>>` 右移
- `>>>` 带符号位的右移运算符
> 1.5.0 及以上版本
- 一元表达式: `!xx变量`、`~xx变量`
原来的表达式用的就是原生 js灵活性虽大但是安全性不佳为了与后端公式保持统一故引入了新的规则`${这里是表达式}`,也就是说如果开始字符是 `${``}` 结尾则认为是新版本的表达式。这个规则与模板中的语法保持一致。
* `+` 一元加法
* `-` 一元减法
* `~` 否运算符,加 1 取反
* `!` 取反
- `${a == 1}` 变量 a 是否和 1 相等
- `${a % 2}` 变量 a 是否为偶数。
- 函数调用:`SUM(1, 2, 3)`
- 箭头函数:`() => abc` 注意这个箭头函数只支持单表达式,不支持多条语句。主要配置其他函数使用如:`ARRAY_MAP(arr, item => item.abc)`
- 括号包裹,修改运算优先级:`(10 - 2) * 3`
表达式中的语法与默认模板中的语法保持一致,所以以下示例直接用模板来方便呈现结果。
示例:
```schema
{

View File

@ -40,13 +40,15 @@ export default {
name: 'id',
label: 'ID',
remark: 'ID',
groupName: 'A'
groupName: 'A',
fixed: 'left'
},
{
name: 'grade',
label: 'CSS grade',
remark: 'CSS grade',
groupName: 'A'
groupName: 'A',
fixed: 'left'
},
{
name: 'engine',
@ -69,7 +71,8 @@ export default {
name: 'version',
label: 'Engine version',
remark: 'Engine version',
groupName: 'B'
groupName: 'B',
fixed: 'right'
}
],
data: {

View File

@ -131,7 +131,13 @@ export default {
align: 'right'
}
],
footerToolbar: ['statistics', 'switch-per-page', 'pagination'],
footerToolbar: [
'statistics',
{
type: 'pagination',
layout: 'perPage,pager,go'
}
],
// rowClassNameExpr: '<%= data.id == 1 ? "bg-success" : "" %>',
columns: [
{

View File

@ -118,6 +118,7 @@ import DynamicTabSchema from './Tabs/Dynamic';
import Tab1Schema from './Tabs/Tab1';
import Tab2Schema from './Tabs/Tab2';
import Tab3Schema from './Tabs/Tab3';
import Loading from './Loading';
import {Switch} from 'react-router-dom';
import {navigations2route} from './App';
@ -822,6 +823,13 @@ export const examples = [
component: SdkTest
},
{
label: '多 loading',
icon: 'fa fa-spinner',
path: '/examples/loading',
component: makeSchemaRenderer(Loading)
},
{
label: 'APP 多页应用',
icon: 'fa fa-cubes',

View File

@ -39,7 +39,19 @@ export default {
label: '选项4',
value: 4
}
],
"onEvent": {
"itemclick": {
"actions": [
{
"actionType": "alert",
"args": {
"msg": "${event.data.label}~${event.data.id}</a>"
}
}
]
}
}
},
{
@ -285,6 +297,18 @@ export default {
toggled: true
}
]
},
"onEvent": {
"itemclick": {
"actions": [
{
"actionType": "alert",
"args": {
"msg": "${ecent.data.label}~${event.data.id}</a>"
}
}
]
}
}
},

View File

@ -0,0 +1,181 @@
const loadingBody = {
type: 'service',
api: '/api/mock2/sample?orderBy=id&orderDir=desc&perPage=10&waitSeconds=10',
body: {
type: 'page',
initApi:
'/api/mock2/sample?orderBy=id&orderDir=desc&perPage=10&waitSeconds=10',
body: [
{
loading: false,
type: 'nav',
stacked: true,
className: 'w-md',
draggable: true,
saveOrderApi: '/api/options/nav',
source:
'/api/mock2/sample?orderBy=id&orderDir=desc&perPage=10&waitSeconds=30',
itemActions: [
{
type: 'icon',
icon: 'cloud',
visibleOn: "this.to === '?cat=1'"
},
{
type: 'dropdown-button',
level: 'link',
icon: 'fa fa-ellipsis-h',
hideCaret: true,
buttons: [
{
type: 'button',
label: '编辑'
},
{
type: 'button',
label: '删除'
}
]
}
]
},
{
type: 'button',
label: 'OpenDialog',
actionType: 'drawer',
reload: 'thepage',
drawer: {
body: {
type: 'form',
controls: [
{
type: 'text',
name: 'a',
value: '3'
}
]
}
}
},
{
type: 'page',
body: {
type: 'crud',
syncLocation: false,
api: '/api/mock2/sample?waitSeconds=30',
headerToolbar: ['bulkActions'],
bulkActions: [
{
label: '批量删除',
actionType: 'ajax',
api: 'delete:/api/mock2/sample/${ids|raw}?waitSeconds=30',
confirmText: '确定要批量删除?'
},
{
label: '批量修改',
actionType: 'dialog',
dialog: {
title: '批量编辑',
body: {
type: 'form',
api: '/api/mock2/sample/bulkUpdate2?waitSeconds=30',
body: [
{
type: 'hidden',
name: 'ids'
},
{
type: 'input-text',
name: 'engine',
label: 'Engine'
}
]
}
}
}
],
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'
}
]
}
},
{
type: 'page',
body: {
type: 'crud',
api: '/api/mock2/sample?orderBy=id&orderDir=desc&waitSeconds=30',
syncLocation: false,
columns: [
{
name: 'id',
label: 'ID'
},
{
name: 'engine',
label: 'Rendering engine'
},
{
name: 'browser',
label: 'Browser'
},
{
type: 'operation',
label: '操作',
buttons: [
{
label: '删除',
type: 'button',
actionType: 'ajax',
level: 'danger',
confirmText: '确认要删除?',
api: 'delete:/api/mock2/sample/${id}?waitSeconds=30'
}
]
}
]
}
}
]
}
};
export default {
type: 'page',
body: [
{
type: 'button',
label: 'dialog内带 loading',
actionType: 'dialog',
level: 'primary',
dialog: {
size: 'lg',
title: '提示',
body: loadingBody
}
},
loadingBody
]
};

View File

@ -6,7 +6,7 @@ import {toast} from 'amis';
import {normalizeLink} from 'amis-core';
import {withRouter} from 'react-router';
import copy from 'copy-to-clipboard';
import {qsparse} from 'amis-core';
import {qsparse, parseQuery} from 'amis-core';
function loadEditor() {
return new Promise(resolve =>
@ -86,7 +86,7 @@ export default function (schema, showCode, envOverrides) {
if (pathname !== location.pathname || !location.search) {
return false;
}
const currentQuery = qsparse(location.search.substring(1));
const currentQuery = parseQuery(location);
const query = qsparse(search.substring(1));
return Object.keys(query).every(

View File

@ -223,7 +223,7 @@ fis.match('*.html:jsx', {
// 这些用了 esm
fis.match(
'{echarts/extension/**.js,zrender/**.js,ansi-to-react/lib/index.js,markdown-it-html5-media/**.js}',
'{echarts/extension/**.js,zrender/**.js,markdown-it-html5-media/**.js}',
{
parser: fis.plugin('typescript', {
sourceMap: false,

View File

@ -30,7 +30,7 @@
},
"devDependencies": {
"@types/jest": "^28.1.0",
"echarts": "5.3.3",
"echarts": "5.4.0",
"fis-optimizer-terser": "^1.0.1",
"fis-parser-sass": "^1.2.0",
"fis-parser-svgr": "^1.0.0",
@ -46,12 +46,12 @@
"fis3-preprocessor-js-require-css": "^0.1.3",
"fis3-preprocessor-js-require-file": "^0.1.3",
"husky": "^8.0.0",
"jest": "^28.1.0",
"jest-environment-jsdom": "^28.1.0",
"lerna": "^5.0.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"lerna": "^5.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ts-jest": "^28.0.3",
"ts-jest": "^29.0.2",
"zrender": "^5.3.2"
},
"jest": {
@ -85,11 +85,6 @@
"testPathIgnorePatterns": [
"/node_modules/",
"/.rollup.cache/"
],
"globals": {
"ts-jest": {
"diagnostics": false
}
}
]
}
}

View File

@ -156,7 +156,8 @@ test(`filter:url_encode`, () => {
).toBe('%3D');
});
test(`filter:url_encode`, () => {
describe('filter:url_decode', () => {
test(`filter:url_decode:normal`, () => {
expect(
resolveVariableAndFilter('${a|url_decode}', {
a: '%3D'
@ -164,7 +165,16 @@ test(`filter:url_encode`, () => {
).toBe('=');
});
test(`filter:url_encode`, () => {
test(`filter:url_decode:error`, () => {
expect(
resolveVariableAndFilter('${a|url_decode}', {
a: '%'
})
).toBe(undefined);
});
});
test(`filter:default`, () => {
expect(
resolveVariableAndFilter('${a|default:-}', {
a: ''

View File

@ -10,7 +10,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-typescript": "^8.3.4",
"@testing-library/jest-dom": "^5.16.4",
"@types/file-saver": "^2.0.1",
@ -18,16 +18,16 @@
"@types/jest": "^28.1.0",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"jest": "^28.1.0",
"jest-environment-jsdom": "^28.1.0",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"moment-timezone": "^0.5.34",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"rollup": "^2.73.0",
"rollup": "^2.79.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-license": "^2.7.0",
"ts-jest": "^28.0.3",
"ts-jest": "^29.0.2",
"typescript": "^4.6.4"
},
"scripts": {
@ -76,7 +76,12 @@
"js"
],
"transform": {
"\\.(ts|tsx)$": "ts-jest"
"\\.(ts|tsx)$": [
"ts-jest",
{
"diagnostics": false
}
]
},
"setupFiles": [
"jest-canvas-mock"
@ -92,12 +97,7 @@
"testPathIgnorePatterns": [
"/node_modules/",
"/.rollup.cache/"
],
"globals": {
"ts-jest": {
"diagnostics": false
}
}
]
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}

View File

@ -225,7 +225,7 @@ export class RootRenderer extends React.Component<RootRendererProps> {
const api = normalizeApi((action as any).api);
if (typeof api.url === 'string') {
let fileName = action.fileName || 'data.txt';
if (api.url.indexOf('.') !== -1) {
if (!action.fileName && api.url.indexOf('.') !== -1) {
fileName = api.url.split('/').pop();
}
saveAs(api.url, fileName);

View File

@ -16,7 +16,7 @@ import {ScopedContext} from './Scoped';
import {Schema, SchemaNode} from './types';
import {DebugWrapper} from './utils/debug';
import getExprProperties from './utils/filter-schema';
import {anyChanged, chainEvents, autobind, createObject} from './utils/helper';
import {anyChanged, chainEvents, autobind} from './utils/helper';
import {SimpleMap} from './utils/SimpleMap';
import {bindEvent, dispatchEvent, RendererEvent} from './utils/renderer-event';

View File

@ -9,13 +9,12 @@ import hoistNonReactStatic from 'hoist-non-react-statics';
import {dataMapping} from './utils/tpl-builtin';
import {RendererEnv, RendererProps} from './factory';
import {
noop,
autobind,
qsstringify,
qsparse,
createObject,
findTree,
TreeItem
TreeItem,
parseQuery
} from './utils/helper';
import {RendererData, ActionObject} from './types';
@ -212,7 +211,7 @@ function createScopedTools(
component.receive(values, subPath);
} else if (name === 'window' && env && env.updateLocation) {
const query = {
...(location.search ? qsparse(location.search.substring(1)) : {}),
...parseQuery(location),
...values
};
const link = location.pathname + '?' + qsstringify(query);

View File

@ -3,7 +3,13 @@ import {RendererStore, IRendererStore, IIRendererStore} from './store/index';
import {getEnv, destroy} from 'mobx-state-tree';
import {wrapFetcher} from './utils/api';
import {normalizeLink} from './utils/normalizeLink';
import {findIndex, promisify, qsparse, string2regExp} from './utils/helper';
import {
findIndex,
promisify,
qsparse,
string2regExp,
parseQuery
} from './utils/helper';
import {
fetcherResult,
SchemaNode,
@ -302,7 +308,7 @@ export const defaultOptions: RenderOptions = {
return false;
}
const query = qsparse(search.substring(1));
const currentQuery = qsparse(location.search.substring(1));
const currentQuery = parseQuery(location);
return Object.keys(query).every(key => query[key] === currentQuery[key]);
} else if (pathname === location.pathname) {
return true;

View File

@ -1522,7 +1522,14 @@ export default class Form extends React.Component<FormProps, object> {
formLabelAlign: labelAlign !== 'left' ? 'right' : labelAlign,
formLabelWidth: labelWidth,
controlWidth,
disabled: disabled || (control as Schema).disabled || form.loading,
/**
* form.loading有为true时才下发disabled属性disbaled为false
* Form中包含容器类组件时disbaled继续下发至子组件SchemaRenderer中props.disabled覆盖schema.disabled
*/
disabled:
disabled ||
(control as Schema).disabled ||
(form.loading ? true : undefined),
btnDisabled: disabled || form.loading || form.validating,
onAction: this.handleAction,
onQuery: this.handleQuery,

View File

@ -1,5 +1,5 @@
import {Instance, types} from 'mobx-state-tree';
import {createObject, qsparse} from '../utils/helper';
import {createObject, parseQuery} from '../utils/helper';
import {ServiceStore} from './service';
export const RootStore = ServiceStore.named('RootStore')
@ -30,15 +30,7 @@ export const RootStore = ServiceStore.named('RootStore')
self.runtimeErrorStack = errorStack;
},
updateLocation(location?: any) {
const query =
(location && location.query) ||
(location &&
location.search &&
qsparse(location.search.substring(1))) ||
(window.location.search &&
qsparse(window.location.search.substring(1)));
self.query = query;
self.query = parseQuery(location);
},
setVisible(id: string, value: boolean) {
const state = {

View File

@ -1016,6 +1016,9 @@ export const TableStore = iRendererStore
self.selectedRows.clear();
// self.expandedRows.clear();
/* 避免输入内容为非数组挂掉 */
rows = !Array.isArray(rows) ? [] : rows;
let arr: Array<SRow> = rows.map((item, index) => {
if (!isObject(item)) {
item = {

View File

@ -190,7 +190,18 @@ extendsFilters({
}
return encodeURIComponent(input);
},
url_decode: input => decodeURIComponent(input),
url_decode: (input: string) => {
let result;
try {
result = decodeURIComponent(input);
} catch (e) {
console.warn(
`[amis] ${e?.name ?? 'URIError'}: input string is not valid.`
);
}
return result;
},
default: (input, defaultValue, strict = false) =>
(strict ? input : input ? input : undefined) ??
(() => {

View File

@ -3,6 +3,7 @@ import isEqual from 'lodash/isEqual';
import isNaN from 'lodash/isNaN';
import uniq from 'lodash/uniq';
import last from 'lodash/last';
import merge from 'lodash/merge';
import {Schema, PlainObject, FunctionPropertyNames} from '../types';
import {evalExpression} from './tpl';
import qs from 'qs';
@ -1705,3 +1706,25 @@ export function isNumeric(value: any): boolean {
}
return /^[-+]?(?:\d*[.])?\d+$/.test(value);
}
/**
* URL链接中的query参数hash mode
*
* @param location Location对象Location结构的对象
*/
export function parseQuery(
location?: Location | {query?: any; search?: any; [propName: string]: any}
): Record<string, any> {
const query =
(location && !(location instanceof Location) && location?.query) ||
(location && location?.search && qsparse(location.search.substring(1))) ||
(window.location.search && qsparse(window.location.search.substring(1)));
/* 处理hash中的query */
const hashQuery =
window.location?.hash && typeof window.location?.hash === 'string'
? qsparse(window.location.hash.replace(/^#.*\?/gi, ''))
: {};
const normalizedQuery = isPlainObject(query) ? query : {};
return merge(normalizedQuery, hashQuery);
}

View File

@ -68,7 +68,7 @@ export function evalExpression(expression: string, data?: object): boolean {
expression[expression.length - 1] === '}'
) {
// 启用新版本的公式表达式
return evalFormula(expression, data);
return !!evalFormula(expression, data);
}
// 后续改用 FormulaExec['js']

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lexer:filter 1`] = `
Array [
[
"<Raw> $abc is ",
"<OpenScript> \${",
"<Identifier> abc",
@ -19,7 +19,7 @@ Array [
`;
exports[`lexer:simple 1`] = `
Array [
[
"<Raw> expression result is ",
"<OpenScript> \${",
"<Identifier> a",

View File

@ -59,9 +59,12 @@ test('formula:expression2', () => {
});
test('formula:expression3', () => {
expect(evalFormual('${a} === "b"', {a: 'b'})).toBe(true);
// expect(evalFormual('${a} === "b"', {a: 'b'})).toBe(true);
expect(evalFormual('b === "b"')).toBe(false);
expect(evalFormual('${a}', {a: 'b'})).toBe('b');
// expect(evalFormual('${a}', {a: 'b'})).toBe('b');
expect(evalFormual('obj.x.a', {obj: {x: {a: 1}}})).toBe(1);
expect(evalFormual('obj.y.a', {obj: {x: {a: 1}}})).toBe(undefined);
});
test('formula:if', () => {
@ -84,10 +87,13 @@ test('formula:or', () => {
});
test('formula:xor', () => {
expect(evalFormual('XOR(0, 1)')).toBe(false);
expect(evalFormual('XOR(1, 0)')).toBe(false);
expect(evalFormual('XOR(1, 1)')).toBe(true);
expect(evalFormual('XOR(0, 0)')).toBe(true);
expect(evalFormual('XOR(0, 1)')).toBe(true);
expect(evalFormual('XOR(1, 0)')).toBe(true);
expect(evalFormual('XOR(1, 1)')).toBe(false);
expect(evalFormual('XOR(0, 0)')).toBe(false);
expect(evalFormual('XOR(0, 0, 1)')).toBe(true);
expect(evalFormual('XOR(0, 1, 1)')).toBe(false);
});
test('formula:ifs', () => {

View File

@ -41,26 +41,26 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-typescript": "^8.3.4",
"@types/doctrine": "0.0.5",
"@types/jest": "^28.1.0",
"@types/lodash": "^4.14.175",
"doctrine": "^3.0.0",
"jest": "^28.1.0",
"jest": "^29.0.3",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^28.1.0",
"jest-environment-jsdom": "^29.0.3",
"mini-css-extract-plugin": "^2.4.5",
"moment-timezone": "^0.5.33",
"rimraf": "^3.0.2",
"rollup": "^2.60.2",
"rollup-plugin-license": "^2.6.0",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.54.0",
"sass": "^1.54.9",
"sass-loader": "^12.1.0",
"style-loader": "^3.2.1",
"stylelint": "^13.0.0",
"ts-jest": "^28.0.3",
"ts-jest": "^29.0.2",
"ts-loader": "^9.2.3",
"ts-node": "^10.4.0",
"typescript": "^4.3.5"
@ -77,7 +77,12 @@
"js"
],
"transform": {
"\\.(ts|tsx)$": "ts-jest"
"\\.(ts|tsx)$": [
"ts-jest",
{
"diagnostics": false
}
]
},
"setupFiles": [
"jest-canvas-mock"
@ -89,12 +94,7 @@
},
"setupFilesAfterEnv": [
"<rootDir>/__tests__/jest.setup.js"
],
"globals": {
"ts-jest": {
"diagnostics": false
}
}
]
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}

View File

@ -106,7 +106,7 @@ export class Evaluator {
const filter = filters.shift()!;
const fn = this.filters[filter.name];
if (!fn) {
throw new Error(`filter \`${filter.name}\` not exits`);
throw new Error(`filter \`${filter.name}\` not exists.`);
}
context.filter = filter;
input = fn.apply(
@ -542,7 +542,7 @@ export class Evaluator {
}
/**
*
*
*
* @example XOR(condition1, condition2)
* @param {expression} condition1 - 1
@ -551,8 +551,8 @@ export class Evaluator {
*
* @returns {boolean}
*/
fnXOR(c1: () => any, c2: () => any) {
return !!c1() === !!c2();
fnXOR(...condtions: Array<() => any>) {
return !!(condtions.filter(c => c()).length % 2);
}
/**
@ -896,10 +896,15 @@ export class Evaluator {
*/
fnUPPERMONEY(n: number) {
n = this.formatNumber(n);
const maxLen = 14;
if (n.toString().split('.')[0]?.length > maxLen) {
return `最大数额只支持到兆(既小数点前${maxLen}位)`;
}
const fraction = ['角', '分'];
const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
const unit = [
['元', '万', '亿'],
['元', '万', '亿', '兆'],
['', '拾', '佰', '仟']
];
const head = n < 0 ? '欠' : '';

View File

@ -294,7 +294,10 @@ export function lexer(input: string, options?: LexerOptions) {
}
function openScript() {
if (mainState === mainStates.Template) {
if (
mainState === mainStates.Template ||
mainState === mainStates.EXPRESSION
) {
return null;
}

View File

@ -37,8 +37,8 @@
"amis-formula": "^2.1.0",
"classnames": "2.3.1",
"codemirror": "^5.63.0",
"downshift": "6.1.7",
"echarts": "5.3.3",
"downshift": "6.1.12",
"echarts": "5.4.0",
"froala-editor": "3.1.1",
"hoist-non-react-statics": "^3.3.2",
"jsbarcode": "^3.11.5",
@ -53,7 +53,7 @@
"moment": "^2.19.4",
"monaco-editor": "0.30.1",
"prop-types": "^15.6.1",
"rc-input-number": "^7.3.4",
"rc-input-number": "^7.3.9",
"rc-progress": "^3.1.4",
"react-color": "^2.19.3",
"react-hook-form": "7.30.0",
@ -62,7 +62,7 @@
"react-textarea-autosize": "8.3.3",
"react-transition-group": "4.4.2",
"react-visibility-sensor": "5.1.1",
"sortablejs": "1.14.0",
"sortablejs": "1.15.0",
"tinymce": "^6.1.2",
"tslib": "^2.3.1",
"uncontrollable": "7.2.1"
@ -70,7 +70,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-typescript": "^8.3.4",
"@svgr/rollup": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4",
@ -78,21 +78,21 @@
"@types/jest": "^28.1.0",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"autoprefixer": "^10.4.7",
"jest": "^28.1.0",
"jest-environment-jsdom": "^28.1.0",
"autoprefixer": "^10.4.12",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",
"moment-timezone": "^0.5.34",
"postcss-import": "^14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"rollup": "^2.73.0",
"rollup": "^2.79.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-license": "^2.7.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-scss": "^3.0.0",
"sass": "^1.54.0",
"ts-jest": "^28.0.3",
"sass": "^1.54.9",
"ts-jest": "^29.0.2",
"typescript": "^4.6.4"
},
"peerDependencies": {
@ -112,7 +112,12 @@
"js"
],
"transform": {
"\\.(ts|tsx)$": "ts-jest"
"\\.(ts|tsx)$": [
"ts-jest",
{
"diagnostics": false
}
]
},
"setupFiles": [
"jest-canvas-mock"
@ -129,12 +134,7 @@
"testPathIgnorePatterns": [
"/node_modules/",
"/.rollup.cache/"
],
"globals": {
"ts-jest": {
"diagnostics": false
}
}
]
},
"gitHead": "37d23b4a8eb1c663bc38e8dd9040889ea1526ec4"
}

View File

@ -409,6 +409,10 @@
background: var(--Form-input-onDisabled-bg);
border-color: var(--Form-input-onDisabled-borderColor);
transition: all var(--animation-duration);
& > input {
color: var(--text--muted-color);
}
}
&-spinner {
@ -471,3 +475,9 @@
color: var(--icon-onHover-color);
}
}
@mixin truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -30,6 +30,8 @@
margin-left: px2rem(-2px);
height: px2rem(32px);
line-height: px2rem(32px);
@include truncate();
max-width: var(--Tabs--vertical-width);
&:hover {
color: var(--primary);
@ -46,8 +48,8 @@
border-color: var(--primary);
}
> a:hover {
color: #528EFF;
border-color: #528EFF;
color: #528eff;
border-color: #528eff;
}
> a:active {
color: #144bcc;
@ -72,6 +74,11 @@
display: inline-block;
position: relative;
> a {
@include truncate();
max-width: var(--Tabs--vertical-width);
}
> a:first-child {
font-size: var(--Tabs-linkFontSize);
outline: 0;

View File

@ -420,7 +420,7 @@
}
}
@if var(--Table-strip-bg) !=transparent {
@if $Table-strip-bg != transparent {
background: transparent;
&.#{$ns}Table-tr--odd {

View File

@ -16,6 +16,8 @@
border-radius: var(--Form-input-borderRadius);
background: var(--Form-input-bg);
padding: var(--Form-input-paddingY) var(--Form-input-paddingX);
/* 避免和clear btn 重叠 */
padding-right: calc(var(--Form-input-paddingX) + var(--Form-fontSize));
font-size: var(--Form-input-fontSize);
display: block;
width: 100%;
@ -54,13 +56,13 @@
}
.has-error--maxLength &-counter {
background: var(--danger);
color: var(--danger);
}
&-clear {
@include input-clear();
position: absolute;
right: var(--Form-input-paddingX);
right: var(--Form-input-paddingY);
top: var(--Form-input-paddingY);
}
}

View File

@ -13,10 +13,11 @@
&-searchBox {
height: px2rem(52px);
margin: 0 px2rem(16px);
padding: 0 px2rem(16px);
flex: none;
display: flex;
align-items: center;
background: var(--white);
}
&-search {
@ -30,6 +31,7 @@
overflow-x: hidden;
overflow-y: auto;
background: var(--UserSelect--content-bg);
margin-bottom: px2rem(16px);
}
&-wrap {
@ -38,6 +40,17 @@
display: flex;
flex-direction: column;
text-align: left;
margin-bottom: px2rem(16px);
background: var(--UserSelect--content-bg);
}
&-footer {
background: var(--white);
padding: px2rem(10px) px2rem(16px) 0;
.#{$ns}Button {
width: 100%;
}
}
&-navbar {
@ -73,6 +86,7 @@
flex: none;
white-space: nowrap;
overflow-x: auto;
background: var(--white);
&-item {
cursor: pointer;
@ -95,6 +109,8 @@
position: relative;
flex: 1;
background: var(--UserSelect--content-bg);
margin-top: px2rem(16px);
margin-bottom: px2rem(16px);
}
&-scroll {
@ -108,7 +124,6 @@
&-memberList-box {
width: 100vw;
margin-top: px2rem(16px);
}
&-memberList,
@ -238,6 +253,7 @@
flex: none;
overflow: hidden;
box-sizing: border-box;
background: var(--white);
}
&-selectNum {
@ -396,11 +412,28 @@
flex-direction: column;
}
.#{$ns}UserSelect-wrap {
height: calc(100% - 16px);
}
&-footer {
padding: px2rem(16px) px2rem(16px) 0;
background: var(--white);
.#{$ns}Button {
width: 100%;
}
}
&-tabs {
flex: 1;
display: flex;
flex-direction: column;
.#{$ns}Tabs-content {
background-color: var(--UserSelect--content-bg);
}
> div {
&:first-child {
flex: none;

View File

@ -36,6 +36,9 @@ $link-color: $info;
@import '../variables';
@import '../properties';
/* 此处放置需要override的变量因为部分变量已经在variables.scss中定义 */
$Table-strip-bg: transparent;
:root {
--fontFamilyBase: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
@ -158,8 +161,7 @@ $link-color: $info;
--Tabs--vertical-onActive-container-bg: #fff;
--Tabs--vertical-onActive-container-borderRight: 1px solid #f0f0f0;
$Table-strip-bg: transparent;
--Table-strip-bg: transparent;
--Table-strip-bg: #{$Table-strip-bg};
--Table-thead-bg: #fafafa;
--Table-onHover-bg: rgb(250, 250, 250);
--Table-onHover-borderColor: var(--Table-borderColor);

View File

@ -28,6 +28,9 @@ $Wizard-steps-liAfterBorder: none !important;
@import '../variables';
@import '../properties';
/* 此处放置需要override的变量因为部分变量已经在variables.scss中定义 */
$Table-strip-bg: transparent;
// yunshe4.0 font-size
$T1: 10px;
$T2: 12px;
@ -186,6 +189,7 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Form-select-valueIcon-color--dark: #{$G8};
--Form-select-multiple-bgColor: #{$G10};
--Form-select-value-bgColor--dark: #{$G4};
--Form-selectValue-onDisabled-color: #{$G6};
--InputGroup-select-borderWidth: #{px2rem(1px)};
--InputGroup-select-onFocused-bg: #eaf6fe;
@ -391,7 +395,7 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Table-thead-bg: #{$G10};
--Table-thead-borderColor: #fff;
--Table-thead-iconColor: #999;
--Table-strip-bg: transparent;
--Table-strip-bg: #{$Table-strip-bg};
--Table-onHover-bg: #{$B1};
--Table-onHover-bg-rgb: 245, 251, 255;
--Table-onHover-borderColor: #eceff8;

View File

@ -35,6 +35,9 @@ $link-color: $info;
@import '../variables';
@import '../properties';
/* 此处放置需要override的变量因为部分变量已经在variables.scss中定义 */
$Table-strip-bg: $Panel-bg;
:root {
--Panel-bg: #{$Panel-bg};
--background-head: #191c22;

View File

@ -219,7 +219,7 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
key={index}
onClick={() => this.handleSelect(name)}
>
<a>{title}</a>
<a title={title}>{title}</a>
</li>
);
}

View File

@ -8,6 +8,7 @@ import TooltipWrapper, {TooltipObject, Trigger} from './TooltipWrapper';
import {pickEventsProps} from 'amis-core';
import {ClassNamesFn, themeable} from 'amis-core';
import {Icon} from './icons';
import Spinner from './Spinner';
interface ButtonProps extends React.DOMAttributes<HTMLButtonElement> {
id?: string;
className?: string;
@ -103,18 +104,19 @@ export class Button extends React.Component<ButtonProps> {
title={title}
disabled={disabled}
>
{loading && !disabled ? (
<span
{loading && !disabled && (
<Spinner
size="sm"
show
icon="loading-outline"
className={cx(
overrideClassName
? ''
: {[`Button--loading Button--loading--${level}`]: level},
loadingClassName
)}
>
<Icon icon="loading-outline" className="icon" />
</span>
) : null}
/>
)}
{children}
</Comp>
);

View File

@ -1178,8 +1178,11 @@ export class DateRangePicker extends React.Component<
? minDate
: startDate
: minDate || startDate;
if (minDate && currentDate.isBefore(minDate, precision)) {
// 在 dateTimeRange 的场景下,如果选择了开始时间的时间点不为 0比如 2020-10-1 10:10这时 currentDate 传入的当天值是 2020-10-1 00:00这个值在起始时间后面导致没法再选这一天了所以在这时需要先通过将时间都转成 00 再比较
if (
minDate &&
currentDate.startOf('day').isBefore(minDate.startOf('day'), precision)
) {
return false;
} else if (maxDate && currentDate.isAfter(maxDate, precision)) {
return false;

View File

@ -10,7 +10,11 @@ import {localeable} from 'amis-core';
export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
valueArray: Array<Option>;
renderOption(option: Option, index: number) {
renderOption(
option: Option,
index: number,
key: string = `${index}`
): JSX.Element {
const {
labelClassName,
disabled,
@ -24,6 +28,16 @@ export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
const valueArray = this.valueArray;
if (Array.isArray(option.children)) {
if (!option.label) {
return (
<>
{option.children.map((child: Option, index: number) =>
this.renderOption(child, index)
)}
</>
);
}
return (
<div
key={index}

View File

@ -31,6 +31,7 @@ export interface ImageAction {
export interface ImageGalleryProps extends ThemeProps, LocaleProps {
children: React.ReactNode;
modalContainer?: () => HTMLElement;
/** 操作栏 */
actions?: ImageAction[];
}
@ -47,6 +48,10 @@ export interface ImageGalleryState {
scale: number;
/** 图片旋转角度 */
rotate: number;
/** 是否开启操作栏 */
showToolbar?: boolean;
/** 工具栏配置 */
actions?: ImageAction[];
}
export class ImageGallery extends React.Component<
@ -80,7 +85,9 @@ export class ImageGallery extends React.Component<
index: -1,
items: [],
scale: 1,
rotate: 0
rotate: 0,
showToolbar: false,
actions: ImageGallery.defaultProps.actions
};
@autobind
@ -96,11 +103,24 @@ export class ImageGallery extends React.Component<
title?: string;
caption?: string;
index?: number;
showToolbar?: boolean;
toolbarActions?: ImageAction[];
}) {
const {actions} = this.props;
const validActionKeys = Object.values(ImageActionKey);
this.setState({
isOpened: true,
items: info.list ? info.list : [info],
index: info.index || 0
index: info.index || 0,
/* children组件可以控制工具栏的展示 */
showToolbar: !!info.showToolbar,
/** 外部传入合法key值的actions才会生效 */
actions: Array.isArray(info.toolbarActions)
? info.toolbarActions.filter(action =>
validActionKeys.includes(action?.key)
)
: actions
});
}
@ -212,8 +232,8 @@ export class ImageGallery extends React.Component<
}
render() {
const {children, classnames: cx, modalContainer, actions} = this.props;
const {index, items, rotate, scale} = this.state;
const {children, classnames: cx, modalContainer} = this.props;
const {index, items, rotate, scale, showToolbar, actions} = this.state;
const __ = this.props.translate;
return (
@ -249,7 +269,7 @@ export class ImageGallery extends React.Component<
style={{transform: `scale(${scale}) rotate(${rotate}deg)`}}
/>
{Array.isArray(actions) && actions.length > 0
{showToolbar && Array.isArray(actions) && actions.length > 0
? this.renderToolbar(actions)
: null}

View File

@ -478,7 +478,6 @@ export class Pagination extends React.Component<
<Select
key="perpage"
className={cx('Pagination-perpage', 'Pagination-item')}
overlayPlacement="right-bottom-right-top"
clearable={false}
disabled={disabled}
value={perPage}

View File

@ -10,6 +10,8 @@ import {themeable, ThemeProps} from 'amis-core';
import Transition, {ENTERED, ENTERING} from 'react-transition-group/Transition';
import {Icon, hasIcon} from './icons';
import {generateIcon} from 'amis-core';
import {types} from 'mobx-state-tree';
import {observable, reaction} from 'mobx';
const fadeStyles: {
[propName: string]: string;
@ -35,6 +37,60 @@ export interface SpinnerProps extends ThemeProps {
overlay?: boolean; // 是否显示遮罩层有children属性才生效
}
const SpinnerSharedStore = types
.model('SpinnerSharedStore', {})
.volatile(self => {
return {
// 保存所有可以进入 loading 状态props.show = true的 Spinner 的父级容器
spinnerContainers: observable.set([] as HTMLElement[], {
deep: false
})
};
})
.actions(self => {
return {
push: (spinnerContainer: HTMLElement) => {
if (self.spinnerContainers.has(spinnerContainer)) {
return;
}
self.spinnerContainers.add(spinnerContainer);
},
remove: (spinnerContainer: HTMLElement) => {
self.spinnerContainers.delete(spinnerContainer);
},
/**
* Spinner loading
* @param spinnerContainerWillCheck Spinner
* @returns {boolean} loading
*/
checkLoading: (spinnerContainerWillCheck: HTMLElement | null) => {
if (self.spinnerContainers.has(spinnerContainerWillCheck)) {
if (!self.spinnerContainers.size) {
return false;
}
let loading = true;
// 检查缓存的容器中是否有当前容器的父级元素
self.spinnerContainers.forEach(container => {
if (
container.contains(spinnerContainerWillCheck) &&
container !== spinnerContainerWillCheck
) {
loading = false;
}
});
return loading;
}
return false;
}
};
});
const store = SpinnerSharedStore.create({});
export class Spinner extends React.Component<SpinnerProps> {
static defaultProps = {
show: true,
@ -48,10 +104,64 @@ export class Spinner extends React.Component<SpinnerProps> {
overlay: false
};
state = {
spinning: false
};
parent: HTMLElement | null = null;
spinnerRef = (dom: HTMLElement) => {
if (dom) {
this.parent = dom.parentNode as HTMLElement;
}
};
componentDidUpdate(prev: SpinnerProps) {
if (!prev.show && this.props.show) {
// 先根据 props.show 触发一次 loading否则元素没有渲染无法找到 parent
this.setState({spinning: true});
}
if (this.parent) {
if (this.props.show) {
store.push(this.parent);
} else if (this.state.spinning) {
store.remove(this.parent);
}
}
}
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();
// 删除 当前 parent 元素
store.remove(this.parent!);
}
loadingChecker = reaction(
() => store.spinnerContainers.size,
() => {
this.setState({
spinning: store.checkLoading(this.parent)
});
}
);
render() {
const {
classnames: cx,
show,
className,
spinnerClassName,
size = '',
@ -65,7 +175,12 @@ export class Spinner extends React.Component<SpinnerProps> {
const timeout = {enter: delay, exit: 0};
return (
<Transition mountOnEnter unmountOnExit in={show} timeout={timeout}>
<Transition
mountOnEnter
unmountOnExit
in={this.state.spinning}
timeout={timeout}
>
{(status: string) => {
return (
<>
@ -76,6 +191,7 @@ export class Spinner extends React.Component<SpinnerProps> {
{/* spinner图标和文案 */}
<div
ref={this.spinnerRef as any}
data-testid="spinner"
className={cx(
`Spinner`,

View File

@ -202,6 +202,7 @@ export class Textarea extends React.Component<TextAreaProps, TextAreaState> {
placeholder={placeholder}
autoCorrect="off"
spellCheck="false"
maxLength={maxLength}
readOnly={readOnly}
minRows={minRows || undefined}
maxRows={maxRows || undefined}

View File

@ -133,6 +133,7 @@ export default class TinymceEditor extends React.Component<TinymceEditorProps> {
...this.props.config,
target: this.elementRef.current,
readOnly: this.props.disabled,
promotion: false,
setup: (editor: any) => {
this.editor = editor;

View File

@ -4,7 +4,7 @@
*/
import React from 'react';
import {Payload, themeable, ThemeProps} from 'amis-core';
import {eachTree, Payload, themeable, ThemeProps} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core';
import {ResultBox} from '.';
import type {Option} from 'amis-core';
@ -53,7 +53,11 @@ export interface UserSelectProps extends ThemeProps, LocaleProps {
isRef?: boolean,
param?: PlainObject
) => Promise<Option[]>;
onChange: (value: Array<Option> | Option, isReplace?: boolean) => void;
onChange: (
value: Array<Option> | Option,
isReplace?: boolean,
isDelete?: boolean
) => void;
}
export interface UserSelectState {
@ -108,7 +112,7 @@ export class UserSelect extends React.Component<
if (prevProps.options !== options) {
if (
options &&
options.length === 1 &&
options.length &&
options[0].leftOptions &&
Array.isArray(options[0].children)
) {
@ -214,6 +218,7 @@ export class UserSelect extends React.Component<
swapSelectPosition(oldIndex: number, newIndex: number) {
const tempSelection = this.state.tempSelection;
tempSelection.splice(newIndex, 0, tempSelection.splice(oldIndex, 1)[0]);
this.setState({tempSelection});
}
@ -255,10 +260,10 @@ export class UserSelect extends React.Component<
@autobind
onOpen() {
const {selection} = this.state;
const {selection} = this.props;
this.setState({
isOpened: true,
tempSelection: selection.slice()
selection: selection || []
});
}
@ -269,6 +274,7 @@ export class UserSelect extends React.Component<
inputValue: '',
isSearch: false,
searchList: [],
selection: [],
breadList: []
});
}
@ -307,10 +313,13 @@ export class UserSelect extends React.Component<
onChange(option);
return;
}
let selection = this.state.selection.slice();
// 直接替换的option 肯定是数组
if (isReplace) {
selection = option as Option[];
// ResultBox 删除场景
onChange(selection);
} else {
let selectionVals = selection.map((option: Option) => option[valueField]);
let pos = selectionVals.indexOf(option[valueField]);
@ -325,16 +334,25 @@ export class UserSelect extends React.Component<
}
}
onChange(multiple ? selection : selection?.[0]);
// onChange(multiple ? selection : selection?.[0]);
this.setState({
selection
});
return false;
}
@autobind
handleSubmit() {
const {onChange, multiple} = this.props;
const {selection} = this.state;
const value = multiple ? selection : selection?.[0];
onChange(value);
this.handleBack();
}
@autobind
onDelete(option: Option, isTemp: boolean = false) {
const {valueField = 'value'} = this.props;
const {valueField = 'value', controlled, onChange} = this.props;
const {tempSelection, selection} = this.state;
let _selection = isTemp ? tempSelection : selection;
_selection = _selection.filter(
@ -342,10 +360,14 @@ export class UserSelect extends React.Component<
);
if (isTemp) {
this.setState({tempSelection: _selection});
} else {
if (controlled) {
onChange(option, false, true);
} else {
this.setState({selection: _selection});
}
}
}
@autobind
handleBreadChange(option: Option, index: number) {
@ -355,6 +377,17 @@ export class UserSelect extends React.Component<
});
}
@autobind
handleSort() {
const {controlled} = this.props;
this.setState({
isSelectOpened: true,
tempSelection: controlled
? this.props.selection?.slice() || []
: this.state.selection.slice()
});
}
@autobind
handleEdit() {
const {multiple, onChange, controlled} = this.props;
@ -381,6 +414,31 @@ export class UserSelect extends React.Component<
}
}
@autobind
handleClear() {
this.setState({tempSelection: []});
}
@autobind
getResult() {
const {
valueField = 'value',
labelField = 'label',
options = []
} = this.props;
const _selection = this.props.selection?.slice() || [];
eachTree(options, (item: Option) => {
const res = _selection.find(
(item2: Option) => item2[valueField] === item[valueField]
);
if (res) {
res[labelField] = item[labelField];
}
});
return _selection;
}
renderIcon(option: Option, isSelect?: boolean) {
const {labelField = 'label', classnames: cx, isRef} = this.props;
const {isSearch} = this.state;
@ -389,7 +447,7 @@ export class UserSelect extends React.Component<
if (option.isRef || ((isSearch || isSelect) && isRef)) {
return (
<span className={cx('UserSelect-text-userPic')}>
{option[labelField].slice(0, 1)}
{option[labelField]?.slice(0, 1)}
</span>
);
} else {
@ -602,9 +660,7 @@ export class UserSelect extends React.Component<
const {breadList, options, isSearch, searchList, searchLoading} =
this.state;
let selection = controlled
? this.props.selection || []
: this.state.selection;
const selection = controlled ? this.props.selection : this.state.selection;
return (
<div className={cx(`UserSelect-wrap`)}>
@ -690,12 +746,7 @@ export class UserSelect extends React.Component<
</ul>
<span
className={cx('UserSelect-selectSort-box')}
onClick={() =>
this.setState({
isSelectOpened: true,
tempSelection: selection.slice()
})
}
onClick={this.handleSort}
>
<Icon
icon="menu"
@ -745,6 +796,18 @@ export class UserSelect extends React.Component<
</div>
</div>
)}
{!controlled ? (
<div className={cx('UserSelect-footer')}>
<button
type="button"
className={cx('Button Button--md Button--primary')}
onClick={this.handleSubmit}
>
{__('UserSelect.sure')}
</button>
</div>
) : null}
</div>
);
}
@ -754,13 +817,10 @@ export class UserSelect extends React.Component<
classnames: cx,
translate: __,
placeholder = '请选择',
showResultBox,
controlled,
onChange
showResultBox
} = this.props;
const {isOpened, tempSelection, isSelectOpened, isEdit} = this.state;
let selection = controlled ? this.props.selection : this.state.selection;
const {isOpened, isEdit, isSelectOpened} = this.state;
return (
<div className={cx('UserSelect')}>
@ -768,7 +828,7 @@ export class UserSelect extends React.Component<
<ResultBox
className={cx('UserSelect-input', isOpened ? 'is-active' : '')}
allowInput={false}
result={selection}
result={this.getResult()}
onResultChange={value => this.handleSelectChange(value, true)}
onResultClick={this.onOpen}
placeholder={placeholder}
@ -832,13 +892,13 @@ export class UserSelect extends React.Component<
{isEdit ? (
<span
className={cx('UserSelect-select-head-btnClear')}
onClick={() => this.setState({tempSelection: []})}
onClick={this.handleClear}
>
{__('UserSelect.clear')}
</span>
) : null}
</div>
{this.renderselectList(tempSelection)}
{this.renderselectList(this.state.tempSelection)}
</div>
</div>
</PopUp>

View File

@ -54,7 +54,6 @@ export interface UserTabSelectState {
inputValue: string;
breadList: Array<any>;
options: Array<Option>;
tempSelection: Array<Option>;
selection: Array<Option>;
searchList: Array<Option>;
searchLoading: boolean;
@ -79,7 +78,6 @@ export class UserTabSelect extends React.Component<
options: [],
breadList: [],
searchList: [],
tempSelection: [],
selection: props.selection ? props.selection : [],
isSearch: false,
searchLoading: false,
@ -106,37 +104,46 @@ export class UserTabSelect extends React.Component<
inputValue: '',
searchList: [],
searchLoading: false,
activeKey: 0
activeKey: 0,
selection: []
});
}
@autobind
onOpen() {
const {selection} = this.state;
const {selection = []} = this.props;
this.setState({
isOpened: true,
tempSelection: selection.slice()
selection: selection.slice()
});
}
@autobind
handleBack() {
this.onClose();
handleSubmit() {
const {onChange} = this.props;
onChange(this.state.selection);
this.onClose();
}
@autobind
handleSelectChange(option: Option | Array<Option>, isReplace?: boolean) {
handleSelectChange(
option: Option | Array<Option>,
isReplace?: boolean,
isDelete?: boolean
) {
const {multiple, valueField = 'value'} = this.props;
let selection = this.state.selection.slice();
let selectionVals = selection.map((option: Option) => option[valueField]);
if (isReplace && Array.isArray(option)) {
if (isDelete) {
selection = selection.filter(
(item: Option) => item[valueField] !== (option as Option)[valueField]
);
} else if (isReplace && Array.isArray(option)) {
selection = option.slice();
} else if (!Array.isArray(option)) {
let pos = selectionVals.indexOf(option[valueField]);
if (pos !== -1) {
selection.splice(selection.indexOf(option), 1);
selection.splice(pos, 1);
} else {
if (multiple) {
selection.push(option);
@ -152,6 +159,17 @@ export class UserTabSelect extends React.Component<
return false;
}
@autobind
handleImmediateChange(option: Array<Option>) {
const {onChange} = this.props;
if (Array.isArray(option)) {
this.setState({
selection: option
});
onChange(option);
}
}
@autobind
handleTabChange(key: number) {
this.setState({
@ -159,26 +177,50 @@ export class UserTabSelect extends React.Component<
});
}
@autobind
getResult() {
const {
selection,
tabOptions,
valueField = 'value',
labelField = 'label'
} = this.props;
const _selection = selection?.slice() || [];
if (tabOptions) {
for (let item of tabOptions) {
for (let item2 of item.options) {
const res = _selection.find(
item => item[valueField] === item2[valueField]
);
if (res) {
res[labelField] = item2[labelField];
}
}
}
}
return _selection;
}
render() {
let {
classnames: cx,
translate: __,
onChange,
placeholder = '请选择',
tabOptions,
onSearch,
deferLoad,
data
} = this.props;
const {activeKey, isOpened, selection} = this.state;
const {activeKey, isOpened} = this.state;
return (
<div className={cx('UserTabSelect')}>
<ResultBox
className={cx('UserTabSelect-input', isOpened ? 'is-active' : '')}
allowInput={false}
result={selection}
onResultChange={value => this.handleSelectChange(value, true)}
result={this.getResult()}
onResultChange={this.handleImmediateChange}
onResultClick={this.onOpen}
placeholder={placeholder}
useMobileUI
@ -191,7 +233,7 @@ export class UserTabSelect extends React.Component<
>
<div className={cx('UserTabSelect-wrap')}>
<div className={cx('UserSelect-navbar')}>
<span className="left-arrow-box" onClick={this.handleBack}>
<span className="left-arrow-box" onClick={this.onClose}>
<Icon icon="left-arrow" className="icon" />
</span>
<div className={cx('UserSelect-navbar-title')}></div>
@ -212,7 +254,7 @@ export class UserTabSelect extends React.Component<
className="TabsTransfer-tab"
>
<UserSelect
selection={selection}
selection={this.state.selection}
showResultBox={false}
{...item}
options={
@ -251,6 +293,16 @@ export class UserTabSelect extends React.Component<
);
})}
</Tabs>
<div className={cx('UserTabSelect-footer')}>
<button
type="button"
className={cx('Button Button--md Button--primary')}
onClick={this.handleSubmit}
>
{__('UserSelect.sure')}
</button>
</div>
</div>
</PopUp>
</div>

View File

@ -83,7 +83,7 @@ export class ConditionField extends React.Component<
}
// 选了值还原options
onPopClose(e: React.MouseEvent, onClose: () => void) {
onPopClose(onClose: () => void) {
this.setState({searchText: ''});
onClose();
}
@ -117,14 +117,14 @@ export class ConditionField extends React.Component<
options={this.filterOptions(this.props.options)}
value={value}
onChange={(value: any) => {
this.onPopClose(null, onClose);
this.onPopClose(onClose);
onChange(value.name);
}}
/>
) : (
<ListSelection
multiple={false}
onClick={(e: any) => this.onPopClose(e, onClose)}
onClick={() => this.onPopClose(onClose)}
options={this.filterOptions(this.props.options)}
value={[value]}
option2value={option2value}

View File

@ -138,6 +138,24 @@ export class FormulaEditor extends React.Component<
evalMode: true
};
static replaceStrByIndex(
str: string,
idx: number,
key: string,
replaceKey: string
) {
const from = str.slice(0, idx);
const left = str.slice(idx);
return from + left.replace(key, replaceKey);
}
static getRegExpByMode(evalMode: boolean, key: string) {
const reg = evalMode
? `\\b${key}\\b`
: `\\$\\{[^\\{\\}]*\\b${key}\\b[^\\{\\}]*\\}`;
return new RegExp(reg);
}
static highlightValue(
value: string,
variables: Array<VariableItem>,
@ -174,14 +192,18 @@ export class FormulaEditor extends React.Component<
let from = 0;
let idx = -1;
while (~(idx = content.indexOf(v, from))) {
// 处理一下 \b 匹配不到的字符,比如 中文、[] 等
const encodeHtml = html.replace(v, REPLACE_KEY);
const curNameEg = new RegExp(`\\b${REPLACE_KEY}\\b`, 'g'); // 避免变量识别冲突比如name、me 被识别成 na「me」
const encodeHtml = FormulaEditor.replaceStrByIndex(
html,
idx,
v,
REPLACE_KEY
);
const reg = FormulaEditor.getRegExpByMode(evalMode, REPLACE_KEY);
// 如果匹配到则高亮,没有匹配到替换成原值
if (curNameEg.test(encodeHtml)) {
if (reg.test(encodeHtml)) {
html = encodeHtml.replace(
curNameEg,
REPLACE_KEY,
`<span class="c-field">${varMap[v]}</span>`
);
} else {

View File

@ -4,7 +4,7 @@
import type CodeMirror from 'codemirror';
import {eachTree} from 'amis-core';
import type {FormulaEditorProps, VariableItem} from './Editor';
import {FormulaEditorProps, VariableItem, FormulaEditor} from './Editor';
export function editorFactory(
dom: HTMLElement,
@ -179,6 +179,7 @@ export class FormulaPlugin {
const vars = Object.keys(varMap).sort((a, b) => b.length - a.length);
const editor = this.editor;
const lines = editor.lineCount();
const {evalMode = true} = this.getProps();
for (let line = 0; line < lines; line++) {
const content = editor.getLine(line);
@ -205,10 +206,15 @@ export class FormulaPlugin {
let from = 0;
let idx = -1;
while (~(idx = content.indexOf(v, from))) {
const encode = content.replace(v, REPLACE_KEY);
const curNameEg = new RegExp(`\\b${REPLACE_KEY}\\b`, 'g');
const encode = FormulaEditor.replaceStrByIndex(
content,
idx,
v,
REPLACE_KEY
);
const reg = FormulaEditor.getRegExpByMode(evalMode, REPLACE_KEY);
if (curNameEg.test(encode)) {
if (reg.test(encode)) {
this.markText(
{
line: line,

View File

@ -152,6 +152,9 @@ register('de-DE', {
'Form.unique': 'Aktueller Wert ist nicht eindeutig',
'Form.validateFailed': 'Fehler bei der Überprüfung der Formulareingabe',
'Form.nestedError': 'Form kann nicht als Nachkomme von Form erscheinen',
'Iframe.invalid': 'Ungültige Iframe-URL',
'Iframe.invalidProtocol':
'HTTP-URL-Iframe kann nicht in https verwendet werden',
'Image.configError':
'Es können nur eine Beschneidung oder mehrere festgelegt werden',
'Image.crop': 'Bild beschneiden',
@ -196,6 +199,7 @@ register('de-DE', {
'Quarter.placeholder': 'Quartal auswählen',
'Repeat.pre': 'Pro',
'reset': 'Zurücksetzen',
'save': 'Konservierung',
'saveFailed': 'Fehler beim Speichern',
'saveSuccess': 'Erfolgreich gespeichert',
'search': 'Suchen',
@ -375,6 +379,7 @@ register('de-DE', {
'UserSelect.resultSort': 'Ergebnissortierung auswählen',
'UserSelect.selected': 'Ausgewählt',
'UserSelect.clear': 'leer',
'UserSelect.sure': 'Submit',
'SchemaType.string': 'String',
'SchemaType.number': 'Number',
'SchemaType.integer': 'integer',

View File

@ -148,6 +148,8 @@ register('en-US', {
'Form.unique': 'Current value is not unique',
'Form.validateFailed': 'Form input validation failed',
'Form.nestedError': 'Form cannot appear as a descendant of form',
'Iframe.invalid': 'Invalid iframe url',
'Iframe.invalidProtocol': 'Can not use http url iframe in https',
'Image.configError': 'Can only set one of crop or multiple',
'Image.crop': 'Crop image',
'Image.dragDrop': `Drag 'n' drop some photos here`,
@ -188,6 +190,7 @@ register('en-US', {
'Quarter.placeholder': 'Select a quarter',
'Repeat.pre': 'Per',
'reset': 'Reset',
'save': 'Save',
'saveFailed': 'Save failed',
'saveSuccess': 'Saved successfully',
'search': 'Search',
@ -363,6 +366,7 @@ register('en-US', {
'UserSelect.resultSort': 'Select result sort',
'UserSelect.selected': 'Selected',
'UserSelect.clear': 'empty',
'UserSelect.sure': 'submit',
'SchemaType.string': 'String',
'SchemaType.number': 'Number',
'SchemaType.integer': 'integer',

View File

@ -152,6 +152,8 @@ register('zh-CN', {
'Form.unique': '当前值不唯一',
'Form.validateFailed': '依赖的部分字段没有通过验证',
'Form.nestedError': '表单不要直接嵌套在表单下面',
'Iframe.invalid': 'iframe 地址不合法',
'Iframe.invalidProtocol': '无法加载 http 协议的 iframe',
'Image.configError': '图片多选配置和裁剪配置只能设置一个',
'Image.crop': '裁剪图片',
'Image.dragDrop': '将图片拖拽到此处',
@ -193,6 +195,7 @@ register('zh-CN', {
'Quarter.placeholder': '请选择季度',
'Repeat.pre': '每',
'reset': '重置',
'save': '保存',
'saveFailed': '保存失败',
'saveSuccess': '保存成功',
'search': '搜索',
@ -358,6 +361,7 @@ register('zh-CN', {
'UserSelect.resultSort': '选择结果排序',
'UserSelect.selected': '已选',
'UserSelect.clear': '清空',
'UserSelect.sure': '确定',
'SchemaType.string': '文本',
'SchemaType.number': '数字',
'SchemaType.integer': '整数',

View File

@ -1,8 +1,33 @@
import React = require('react');
import {cleanup, fireEvent, render, waitFor} from '@testing-library/react';
/**
* CRUD
*
* 01. interval & headerToolbar & footerToolbar
* 02. stopAutoRefreshWhen
* 03. loadDataOnce
* 04. list模式
* 05. card模式
* 06. source & alwaysShowPagination
* 07. filter
* 08. draggable & itemDraggableOn
* 09. quickEdit & quickSaveApi
* 10. quickSaveItemApi
* 11. bulkActions
* 12. sortable & orderBy & orderDir & orderField
* 13. keepItemSelectionOnPageChange & maxKeepItemSelectionLength & labelTpl
* 14. autoGenerateFilter
* 15. group
*/
import {
cleanup,
fireEvent,
render,
waitFor,
waitForElementToBeRemoved
} from '@testing-library/react';
import '../../src';
import {clearStoresCache, render as amisRender} from '../../src';
import {makeEnv, wait} from '../helper';
import {makeEnv as makeEnvRaw, wait} from '../helper';
import rows from '../mockData/rows';
afterEach(() => {
@ -11,6 +36,9 @@ afterEach(() => {
jest.useRealTimers();
});
/** 避免updateLocation里的console.error */
const makeEnv = args => makeEnvRaw({updateLocation: () => {}, ...args});
async function fetcher(config: any) {
return {
status: 200,
@ -171,16 +199,8 @@ test('Renderer:crud list', async () => {
makeEnv({fetcher})
)
);
await waitFor(() => {
expect(
container.querySelector('[data-testid="spinner"]')
).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
await waitFor(() => {
expect(
container.querySelector('[data-testid="spinner"]')
).not.toBeInTheDocument();
expect(container.querySelectorAll('.cxd-ListItem').length > 5).toBeTruthy();
});
expect(container).toMatchSnapshot();
@ -223,15 +243,6 @@ test('Renderer:crud cards', async () => {
);
await waitFor(() => {
expect(
container.querySelector('[data-testid="spinner"]')
).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
await waitFor(() => {
expect(
container.querySelector('[data-testid="spinner"]')
).not.toBeInTheDocument();
expect(container.querySelector('.cxd-Card-title')).toBeInTheDocument();
});
expect(container).toMatchSnapshot();

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Form:options:autoFill:data 1`] = `
Object {
{
"a": "a",
"aId": 233,
"aLabel": "OptionA",
@ -10,7 +10,7 @@ Object {
`;
exports[`Form:options:autoFill:data 2`] = `
Object {
{
"a": "b",
"aId": "",
"aLabel": "OptionB",
@ -19,30 +19,30 @@ Object {
`;
exports[`Form:options:autoFill:multiple:data 1`] = `
Object {
{
"a": "a",
"aIds": Array [
"aIds": [
233,
],
"aLabels": Array [
"aLabels": [
"OptionA",
],
"aValues": Array [
"aValues": [
"a",
],
}
`;
exports[`Form:options:autoFill:multiple:data 2`] = `
Object {
{
"a": "b",
"aIds": Array [
"aIds": [
undefined,
],
"aLabels": Array [
"aLabels": [
"OptionB",
],
"aValues": Array [
"aValues": [
"b",
],
}

View File

@ -421,7 +421,7 @@ exports[`Renderer:FormItem:validateApi:success 1`] = `
`;
exports[`Renderer:FormItem:validateApi:success 2`] = `
Object {
{
"a": "123",
}
`;

View File

@ -129,18 +129,18 @@ exports[`Renderer:Form 1`] = `
`;
exports[`Renderer:Form 2`] = `
Object {
"body": Object {
{
"body": {
"a": "123",
},
"config": Object {
"config": {
"errorMessage": "saveFailed",
"method": "post",
"onFailed": [Function],
"onSuccess": [Function],
"successMessage": "saveSuccess",
},
"data": Object {
"data": {
"a": "123",
},
"method": "post",
@ -837,7 +837,7 @@ exports[`Renderer:Form:onValidate 1`] = `
`;
exports[`Renderer:Form:onValidate 2`] = `
Object {
{
"a": 1,
"b": 2,
}
@ -1007,7 +1007,7 @@ exports[`Renderer:Form:onValidate 3`] = `
`;
exports[`Renderer:Form:onValidate 4`] = `
Object {
{
"a": 1,
"b": 2,
}
@ -1429,7 +1429,7 @@ exports[`Renderer:Form:valdiate 2`] = `
`;
exports[`Renderer:Form:valdiate 3`] = `
Object {
{
"a": "123",
}
`;

View File

@ -67,7 +67,7 @@ exports[`Form:initData 1`] = `
`;
exports[`Form:initData 2`] = `
Object {
{
"a": "1",
"b": "2",
}
@ -237,15 +237,15 @@ exports[`Form:initData:remote 1`] = `
`;
exports[`Form:initData:remote 2`] = `
Object {
"config": Object {
{
"config": {
"cancelExecutor": [Function],
"errorMessage": "fetchFailed",
"onSuccess": [Function],
"successMessage": undefined,
},
"method": "get",
"query": Object {
"query": {
"c": "123",
},
"url": "/api/xxx?c=123",
@ -253,7 +253,7 @@ Object {
`;
exports[`Form:initData:remote 3`] = `
Object {
{
"a": 1,
"b": 2,
"c": "123",
@ -424,4 +424,4 @@ exports[`Form:initData:without-super 1`] = `
</div>
`;
exports[`Form:initData:without-super 2`] = `Object {}`;
exports[`Form:initData:without-super 2`] = `{}`;

View File

@ -1418,7 +1418,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
`;
exports[`Renderer:select table with labelField & valueField 2`] = `
Object {
{
"a": "zhugeliang,libai",
}
`;

View File

@ -1654,6 +1654,7 @@ exports[`Renderer:text with counter and maxLength 1`] = `
<input
autocomplete="off"
class=""
maxlength="10"
name="text"
placeholder=""
size="10"
@ -1787,6 +1788,7 @@ exports[`Renderer:text with counter and maxLength 2`] = `
<input
autocomplete="off"
class=""
maxlength="10"
name="text"
placeholder=""
size="10"
@ -1920,6 +1922,8 @@ exports[`Renderer:text with minLength 1`] = `
<input
autocomplete="off"
class="test-text-class-two"
maxlength="8"
minlength="5"
name="text"
placeholder=""
size="10"

View File

@ -164,6 +164,7 @@ exports[`Renderer:textarea with maxLength & clearable & resetValue 1`] = `
autocomplete="off"
autocorrect="off"
class="cxd-TextareaControl-input cxd-TextareaControl-input--counter"
maxlength="9"
name="text"
spellcheck="false"
>

View File

@ -99,14 +99,12 @@ test('Form:initData:super', async () => {
await waitFor(() => {
expect(onSubmit).toBeCalled();
expect(onSubmit.mock.calls[0][0]).toMatchInlineSnapshot(
`
Object {
expect(onSubmit.mock.calls[0][0]).toMatchInlineSnapshot(`
{
"a": 1,
"b": 2,
}
`
);
`);
});
});

View File

@ -1,10 +1,17 @@
import React = require('react');
/**
* Image/Images /
*
* 1. Image图片
* 2. Images图片集
*/
import {render} from '@testing-library/react';
import '../../src';
import {render as amisRender} from '../../src';
import {makeEnv} from '../helper';
test('Renderer:image', async () => {
describe('Renderer:image', () => {
test('image:basic', async () => {
const {container} = render(
amisRender(
{
@ -22,8 +29,10 @@ test('Renderer:image', async () => {
expect(container).toMatchSnapshot();
});
});
test('Renderer:images', async () => {
describe('Renderer:images', () => {
test('images:basic', async () => {
const {container} = render(
amisRender(
{
@ -58,3 +67,4 @@ test('Renderer:images', async () => {
expect(container).toMatchSnapshot();
});
});

View File

@ -24,14 +24,18 @@ exports[`Renderer:anchorNav 1`] = `
<li
class="cxd-AnchorNav-link is-active"
>
<a>
<a
title="基本信息"
>
基本信息
</a>
</li>
<li
class="cxd-AnchorNav-link"
>
<a>
<a
title="工作信息"
>
工作信息
</a>
</li>
@ -396,7 +400,9 @@ exports[`Renderer:anchorNav horizontal 1`] = `
<li
class="cxd-AnchorNav-link is-active"
>
<a>
<a
title="基本信息"
>
基本信息
</a>
</li>

View File

@ -2933,60 +2933,6 @@ exports[`Renderer:crud basic interval headerToolbar footerToolbar 1`] = `
`;
exports[`Renderer:crud cards 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
>
<div
class="cxd-Crud is-loading"
>
<div
class="cxd-Cards cxd-Crud-body"
>
<div
class="cxd-Cards-fixedTop"
/>
<div
class="cxd-Cards-placeholder"
>
<span
class="cxd-TplField"
>
<span>
没有用户信息
</span>
</span>
</div>
<div
class="cxd-Spinner-overlay in"
/>
<div
class="cxd-Spinner cxd-Spinner--overlay in"
data-testid="spinner"
>
<div
class="cxd-Spinner-icon cxd-Spinner-icon--default"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:crud cards 2`] = `
<div>
<div
class="cxd-Page"
@ -3846,56 +3792,11 @@ exports[`Renderer:crud cards 2`] = `
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:crud list 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
>
<div
class="cxd-Crud is-loading"
>
<div
class="cxd-List cxd-Crud-body"
>
<div
class="cxd-List-heading"
>
list title
</div>
<div
class="cxd-List-placeholder"
>
<span
class="cxd-TplField"
>
<span>
当前组内, 还没有配置任何权限.
</span>
</span>
</div>
<div
class="cxd-Spinner-overlay in"
class="cxd-Spinner-overlay"
/>
<div
class="cxd-Spinner cxd-Spinner--overlay in"
class="cxd-Spinner cxd-Spinner--overlay"
data-testid="spinner"
>
<div
@ -3911,7 +3812,7 @@ exports[`Renderer:crud list 1`] = `
</div>
`;
exports[`Renderer:crud list 2`] = `
exports[`Renderer:crud list 1`] = `
<div>
<div
class="cxd-Page"
@ -4293,6 +4194,17 @@ exports[`Renderer:crud list 2`] = `
</div>
</div>
</div>
<div
class="cxd-Spinner-overlay"
/>
<div
class="cxd-Spinner cxd-Spinner--overlay"
data-testid="spinner"
>
<div
class="cxd-Spinner-icon cxd-Spinner-icon--default"
/>
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renderer:image 1`] = `
exports[`Renderer:image image:basic 1`] = `
<div>
<div
class="cxd-ImageField cxd-ImageField--thumb show"
@ -35,7 +35,7 @@ exports[`Renderer:image 1`] = `
</div>
`;
exports[`Renderer:images 1`] = `
exports[`Renderer:images images:basic 1`] = `
<div>
<div
class="cxd-Page"

View File

@ -25,7 +25,7 @@ exports[`Renderer:Page 1`] = `
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "This is Title",
}
}
@ -56,7 +56,7 @@ exports[`Renderer:Page 1`] = `
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "This is SubTitle",
}
}
@ -72,7 +72,7 @@ exports[`Renderer:Page 1`] = `
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "This is toolbar",
}
}
@ -88,7 +88,7 @@ exports[`Renderer:Page 1`] = `
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "This is body",
}
}
@ -992,12 +992,23 @@ exports[`Renderer:Page initApi reFetch when condition changes 1`] = `
<div
className="cxd-Page-body"
>
<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"
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "The variable value is 1",
}
}
@ -1023,12 +1034,23 @@ exports[`Renderer:Page initApi reFetch when condition changes 2`] = `
<div
className="cxd-Page-body"
>
<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"
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "The variable value is 2",
}
}
@ -1968,7 +1990,7 @@ exports[`Renderer:Page initData 1`] = `
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "The variable value is 1",
}
}
@ -1994,12 +2016,23 @@ exports[`Renderer:Page initFetchOn trigger initApi fetch when condition becomes
<div
className="cxd-Page-body"
>
<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"
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "The variable value is 6",
}
}
@ -2030,7 +2063,7 @@ exports[`Renderer:Page location query 1`] = `
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "The variable value is 5",
}
}
@ -2061,7 +2094,7 @@ exports[`Renderer:Page location query 2`] = `
>
<span
dangerouslySetInnerHTML={
Object {
{
"__html": "The variable value is 6",
}
}

View File

@ -1,25 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`store:index 1`] = `
Object {
{
"storeType": "RendererStore",
}
`;
exports[`store:index 2`] = `
Object {
{
"storeType": "RendererStore",
}
`;
exports[`store:index 3`] = `
Object {
{
"storeType": "RendererStore",
}
`;
exports[`store:index 4`] = `
Object {
{
"storeType": "RendererStore",
}
`;

View File

@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`store:ServiceStore 1`] = `
Object {
{
"action": undefined,
"busying": false,
"checking": false,
"childrenIds": Array [],
"data": Object {},
"childrenIds": [],
"data": {},
"dialogData": undefined,
"dialogOpen": false,
"disposed": false,
@ -21,7 +21,7 @@ Object {
"msg": "",
"parentId": "",
"path": "",
"pristine": Object {},
"pristine": {},
"saving": false,
"schema": null,
"schemaKey": "",
@ -31,13 +31,13 @@ Object {
`;
exports[`store:ServiceStore fetchInitData failed 1`] = `
Array [
Object {
[
{
"action": undefined,
"busying": false,
"checking": false,
"childrenIds": Array [],
"data": Object {},
"childrenIds": [],
"data": {},
"dialogData": undefined,
"dialogOpen": false,
"disposed": false,
@ -52,19 +52,19 @@ Array [
"msg": "",
"parentId": "",
"path": "",
"pristine": Object {},
"pristine": {},
"saving": false,
"schema": null,
"schemaKey": "",
"storeType": "ServiceStore",
"updatedAt": 0,
},
Object {
{
"action": undefined,
"busying": false,
"checking": false,
"childrenIds": Array [],
"data": Object {},
"childrenIds": [],
"data": {},
"dialogData": undefined,
"dialogOpen": false,
"disposed": false,
@ -79,7 +79,7 @@ Array [
"msg": "",
"parentId": "",
"path": "",
"pristine": Object {},
"pristine": {},
"saving": false,
"schema": null,
"schemaKey": "",
@ -90,13 +90,13 @@ Array [
`;
exports[`store:ServiceStore fetchInitData success 1`] = `
Array [
Object {
[
{
"action": undefined,
"busying": false,
"checking": false,
"childrenIds": Array [],
"data": Object {},
"childrenIds": [],
"data": {},
"dialogData": undefined,
"dialogOpen": false,
"disposed": false,
@ -111,18 +111,18 @@ Array [
"msg": "",
"parentId": "",
"path": "",
"pristine": Object {},
"pristine": {},
"saving": false,
"schema": null,
"schemaKey": "",
"storeType": "ServiceStore",
},
Object {
{
"action": undefined,
"busying": false,
"checking": false,
"childrenIds": Array [],
"data": Object {
"childrenIds": [],
"data": {
"a": 1,
"b": 2,
},
@ -140,7 +140,7 @@ Array [
"msg": "",
"parentId": "",
"path": "",
"pristine": Object {
"pristine": {
"a": 1,
"b": 2,
},

View File

@ -42,12 +42,11 @@
"dependencies": {
"amis-core": "^2.2.0",
"amis-ui": "^2.2.0",
"ansi-to-react": "^6.1.6",
"attr-accept": "2.2.2",
"blueimp-canvastoblob": "2.1.0",
"classnames": "2.3.1",
"downshift": "6.1.7",
"echarts": "5.3.3",
"downshift": "6.1.12",
"echarts": "5.4.0",
"echarts-stat": "^1.2.0",
"exceljs": "^4.3.0",
"file-saver": "^2.0.2",
@ -72,7 +71,7 @@
"react-dropzone": "^11.4.2",
"react-json-view": "1.21.3",
"react-transition-group": "4.4.2",
"sortablejs": "1.14.0",
"sortablejs": "1.15.0",
"tslib": "^2.3.1",
"video-react": "0.15.0"
},
@ -80,7 +79,7 @@
"@fortawesome/fontawesome-free": "^6.1.1",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-node-resolve": "^14.1.0",
"@rollup/plugin-typescript": "^8.3.4",
"@svgr/rollup": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4",
@ -119,9 +118,9 @@
"glob": "^7.2.0",
"history": "^4.7.2",
"husky": "^7.0.4",
"jest": "^28.1.0",
"jest": "^29.0.3",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^28.1.0",
"jest-environment-jsdom": "^29.0.3",
"js-yaml": "^4.1.0",
"json5": "^2.2.1",
"lint-staged": "^12.3.3",
@ -141,10 +140,10 @@
"react-router-dom": "5.3.0",
"react-test-renderer": "^18.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.73.0",
"rollup": "^2.79.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-license": "^2.7.0",
"ts-jest": "^28.0.3",
"ts-jest": "^29.0.2",
"ts-json-schema-generator": "0.96.0",
"ts-node": "^10.5.0",
"typescript": "^4.6.4"
@ -193,7 +192,12 @@
"js"
],
"transform": {
"\\.(ts|tsx)$": "ts-jest"
"\\.(ts|tsx)$": [
"ts-jest",
{
"diagnostics": false
}
]
},
"setupFiles": [
"jest-canvas-mock"
@ -211,12 +215,7 @@
"testPathIgnorePatterns": [
"/node_modules/",
"/.rollup.cache/"
],
"globals": {
"ts-jest": {
"diagnostics": false
}
}
]
},
"peerDependencies": {
"amis-core": "*",

View File

@ -2,7 +2,9 @@ import {
addRootWrapper,
extendDefaultEnv,
LazyComponent,
render
render,
themeable,
ThemeProps
} from 'amis-core';
import {
@ -55,4 +57,30 @@ addRootWrapper((props: any) => {
);
});
LazyComponent.defaultProps.placeholder = <Spinner />;
const SimpleSpinner = themeable(
(
props: {
className?: string;
spinnerClassName?: string;
} & ThemeProps
) => {
const cx = props.classnames;
return (
<div
data-testid="spinner"
className={cx(`Spinner`, 'in', props.className)}
>
<div
className={cx(
`Spinner-icon`,
'Spinner-icon--default',
props.spinnerClassName
)}
></div>
</div>
);
}
);
LazyComponent.defaultProps.placeholder = <SimpleSpinner />;

View File

@ -44,7 +44,7 @@ import {ActionSchema} from './Action';
import {CardsSchema} from './Cards';
import {ListSchema} from './List';
import {TableSchema} from './Table';
import {isPureVariable, resolveVariableAndFilter} from 'amis-core';
import {isPureVariable, resolveVariableAndFilter, parseQuery} from 'amis-core';
import type {PaginationProps} from './Pagination';
@ -448,14 +448,14 @@ export default class CRUD extends React.Component<CRUDProps, any> {
if (syncLocation && location && (location.query || location.search)) {
store.updateQuery(
qsparse(location.search.substring(1)),
parseQuery(location),
undefined,
pageField,
perPageField
);
} else if (syncLocation && !location && window.location.search) {
store.updateQuery(
qsparse(window.location.search.substring(1)) as object,
parseQuery(window.location),
undefined,
pageField,
perPageField
@ -545,7 +545,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
) {
// 同步地址栏,那么直接检测 query 是否变了,变了就重新拉数据
store.updateQuery(
qsparse(props.location.search.substring(1)),
parseQuery(props.location),
undefined,
props.pageField,
props.perPageField

View File

@ -12,7 +12,8 @@ import {
qsstringify,
qsparse,
isArrayChildrenModified,
autobind
autobind,
parseQuery
} from 'amis-core';
import {ScopedContext, IScopedContext} from 'amis-core';
import Button from 'amis-ui';
@ -254,14 +255,14 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
if (syncLocation && location && (location.query || location.search)) {
store.updateQuery(
qsparse(location.search.substring(1)),
parseQuery(location),
undefined,
pageField,
perPageField
);
} else if (syncLocation && !location && window.location.search) {
store.updateQuery(
qsparse(window.location.search.substring(1)) as object,
parseQuery(window.location),
undefined,
pageField,
perPageField
@ -331,7 +332,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
) {
// 同步地址栏,那么直接检测 query 是否变了,变了就重新拉数据
store.updateQuery(
qsparse(props.location.search.substring(1)),
parseQuery(props.location),
undefined,
props.pageField,
props.perPageField

View File

@ -579,9 +579,7 @@ export default class Drawer extends React.Component<DrawerProps> {
onExited={this.handleExited}
closeOnEsc={closeOnEsc}
closeOnOutside={
!store.drawerOpen &&
!store.dialogOpen &&
(closeOnOutside || !showCloseButton)
!store.drawerOpen && !store.dialogOpen && closeOnOutside
}
container={
drawerContainer

View File

@ -117,6 +117,11 @@ export interface ComboControlSchema extends FormBaseControlSchema {
*/
addable?: boolean;
/**
* Add at top
*/
addattop?: boolean;
/**
*
*/
@ -459,7 +464,7 @@ export default class ComboControl extends React.Component<ComboProps> {
}
addItemWith(condition: ComboCondition) {
const {flat, joinValues, delimiter, scaffold, disabled, submitOnChange} =
const {flat, joinValues, addattop, delimiter, scaffold, disabled, submitOnChange} =
this.props;
if (disabled) {
@ -481,6 +486,10 @@ export default class ComboControl extends React.Component<ComboProps> {
value = value.join(delimiter || ',');
}
if (addattop === true){
value.unshift(value.pop());
}
this.props.onChange(value, submitOnChange, true);
}
@ -488,6 +497,7 @@ export default class ComboControl extends React.Component<ComboProps> {
const {
flat,
joinValues,
addattop,
delimiter,
scaffold,
disabled,
@ -527,6 +537,10 @@ export default class ComboControl extends React.Component<ComboProps> {
value = value.join(delimiter || ',');
}
if (addattop === true){
value.unshift(value.pop());
}
this.props.onChange(value, submitOnChange, true);
}

View File

@ -1,8 +1,9 @@
import React, {Suspense} from 'react';
import React from 'react';
import Dropzone from 'react-dropzone';
import {autobind, createObject} from 'amis-core';
import {FormItem, FormControlProps, FormBaseControl} from 'amis-core';
import {autobind, createObject, isObject} from 'amis-core';
import {FormItem, FormControlProps} from 'amis-core';
import {FormBaseControlSchema} from '../../Schema';
import type {CellValue, CellRichTextValue} from 'exceljs';
/**
* Excel
@ -115,6 +116,69 @@ export default class ExcelControl extends React.PureComponent<
})
);
}
/**
*
*
* @reference https://github.com/exceljs/exceljs#rich-text
*/
isRichTextValue(value: any) {
return !!(
value &&
isObject(value) &&
value.hasOwnProperty('richText') &&
Array.isArray(value?.richText)
);
}
/**
* Plain Text
*
* @param {CellRichTextValue} cellValue
* @param {Boolean} html html格式
*/
richText2PlainString(cellValue: CellRichTextValue, html = false) {
const result = cellValue.richText.map(({text, font = {}}) => {
let outputStr = text;
/* 如果以HTML格式输出简单处理一下样式 */
if (html) {
let styles = '';
const htmlTag = font?.bold
? 'strong'
: font?.italic
? 'em'
: font?.vertAlign === 'superscript'
? 'sup'
: font?.vertAlign === 'subscript'
? 'sub'
: 'span';
if (font?.strike) {
styles += 'text-decoration: line-through;';
} else if (font?.underline) {
styles += 'text-decoration: underline;';
}
if (font?.outline) {
styles += 'outline: solid;';
}
if (font?.size) {
styles += `font-size: ${font.size}px;`;
}
outputStr = `<${htmlTag} ${
styles ? `style=${styles}` : ''
}>${text}</${htmlTag}>`;
}
return outputStr;
});
return result.join('');
}
/**
* sheet
*/
@ -134,7 +198,11 @@ export default class ExcelControl extends React.PureComponent<
worksheet.eachRow((row: any, rowNumber: number) => {
// 将第一列作为字段名
if (rowNumber == 1) {
firstRowValues = row.values;
firstRowValues = (row.values ?? []).map((item: CellValue) =>
this.isRichTextValue(item)
? this.richText2PlainString(item as CellRichTextValue)
: item
);
} else {
const data: any = {};
if (includeEmpty) {

View File

@ -468,7 +468,7 @@ export default class ImageControl extends React.Component<
componentDidUpdate(prevProps: ImageProps) {
const props = this.props;
if (prevProps.value !== props.value && this.emitValue !== props.value) {
if (prevProps.value !== props.value) {
const value: string | Array<string | FileValue> | FileValue = props.value;
const multiple = props.multiple;
const joinValues = props.joinValues;

View File

@ -64,7 +64,12 @@ export interface TextControlSchema extends FormOptionsSchema {
borderMode?: 'full' | 'half' | 'none';
/**
*
*
*/
minLength?: number;
/**
*
*/
maxLength?: number;
@ -581,6 +586,8 @@ export default class TextControl extends React.PureComponent<
? ''
: typeof value === 'string'
? value
: value instanceof Date
? value.toISOString()
: JSON.stringify(value);
}
@ -608,6 +615,7 @@ export default class TextControl extends React.PureComponent<
borderMode,
showCounter,
maxLength,
minLength,
translate: __
} = this.props;
let type = this.props.type?.replace(/^(?:native|input)\-/, '');
@ -711,7 +719,9 @@ export default class TextControl extends React.PureComponent<
onFocus: this.handleFocus,
onBlur: this.handleBlur,
onChange: this.handleInputChange,
onKeyDown: this.handleKeyDown
onKeyDown: this.handleKeyDown,
maxLength,
minLength
})}
autoComplete="off"
size={10}
@ -818,7 +828,8 @@ export default class TextControl extends React.PureComponent<
suffix,
data,
showCounter,
maxLength
maxLength,
minLength
} = this.props;
const type = this.props.type?.replace(/^(?:native|input)\-/, '');
@ -850,6 +861,8 @@ export default class TextControl extends React.PureComponent<
onBlur={this.handleBlur}
max={max}
min={min}
maxLength={maxLength}
minLength={minLength}
autoComplete="off"
size={10}
step={step}

View File

@ -282,6 +282,7 @@ export default class PickerControl extends React.PureComponent<
}
});
additionalOptions.length && setOptions(options.concat(additionalOptions));
const rendererEvent = await dispatchEvent(
'change',
@ -294,6 +295,25 @@ export default class PickerControl extends React.PureComponent<
onChange(value);
}
@autobind
async handleItemClick(itemlabel: string, itemid: string) {
const {
data,
dispatchEvent,
setOptions
} = this.props;
const rendererEvent = await dispatchEvent(
'itemclick',
createObject(data, {'label': itemlabel, 'id': itemid})
);
if (rendererEvent?.prevented) {
return;
}
}
removeItem(index: number) {
const {
selectedOptions,
@ -393,7 +413,13 @@ export default class PickerControl extends React.PureComponent<
>
×
</span>
<span className={`${ns}Picker-valueLabel`}>
<span
className={`${ns}Picker-valueLabel`}
onClick={e => {
e.stopPropagation();
this.handleItemClick(getVariable(item, labelField || 'label'), getVariable(item, 'id')|| '');
}}
>
{labelTpl ? (
<Html html={filter(labelTpl, item)} />
) : (

View File

@ -201,11 +201,13 @@ export default class SelectControl extends React.Component<SelectProps, any> {
this.input && this.input.focus();
}
getValue(value: Option | Array<Option> | string | void) {
getValue(
value: Option | Array<Option> | string | void,
additonalOptions: Array<any> = []
) {
const {joinValues, extractValue, delimiter, multiple, valueField, options} =
this.props;
let newValue: string | Option | Array<Option> | void = value;
let additonalOptions: Array<any> = [];
(Array.isArray(value) ? value : value ? [value] : []).forEach(
(option: any) => {
@ -268,8 +270,12 @@ export default class SelectControl extends React.Component<SelectProps, any> {
async changeValue(value: Option | Array<Option> | string | void) {
const {onChange, setOptions, options, data, dispatchEvent} = this.props;
let newValue: string | Option | Array<Option> | void = this.getValue(value);
let additonalOptions: Array<any> = [];
let newValue: string | Option | Array<Option> | void = this.getValue(
value,
additonalOptions
);
// 不设置没法回显
additonalOptions.length && setOptions(options.concat(additonalOptions));
@ -531,7 +537,7 @@ class TransferDropdownRenderer extends BaseTransferRenderer<TransferDropDownProp
if (
selectMode === 'associated' &&
options &&
options.length === 1 &&
options.length &&
options[0].leftOptions &&
Array.isArray(options[0].children)
) {

View File

@ -207,6 +207,23 @@ export class BaseTransferRenderer<
joinValues || extractValue
? value[(valueField as string) || 'value']
: value;
const indexes = findTreeIndex(
options,
optionValueCompare(
value[(valueField as string) || 'value'],
(valueField as string) || 'value'
)
);
if (!indexes) {
newOptions.push(value);
} else if (optionModified) {
const origin = getTree(newOptions, indexes);
newOptions = spliceTree(newOptions, indexes, 1, {
...origin,
...value
});
}
}
(newOptions.length > options.length || optionModified) &&
@ -258,7 +275,7 @@ export class BaseTransferRenderer<
const result =
payload.data.options || payload.data.items || payload.data;
if (!Array.isArray(result)) {
throw new Error('CRUD.invalidArray');
throw new Error(__('CRUD.invalidArray'));
}
return result.map(item => {

View File

@ -86,7 +86,7 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
if (
selectMode === 'associated' &&
options &&
options.length === 1 &&
options.length &&
options[0].leftOptions &&
Array.isArray(options[0].children)
) {

View File

@ -655,6 +655,7 @@ export default class TreeSelectControl extends React.Component<
onKeyDown={this.handleInputKeyDown}
clearable={clearable}
allowInput={searchable || isEffectiveApi(autoComplete)}
hasDropDownArrow
>
{loading ? <Spinner size="sm" /> : undefined}
</ResultBox>

View File

@ -1,13 +1,7 @@
import React from 'react';
import {FormHorizontal, Renderer, RendererProps} from 'amis-core';
import {Schema} from 'amis-core';
import pick from 'lodash/pick';
import {
BaseSchema,
SchemaClassName,
SchemaCollection,
SchemaObject
} from '../Schema';
import {BaseSchema, SchemaClassName, SchemaCollection} from '../Schema';
import {ucFirst} from 'amis-core';
import {Spinner} from 'amis-ui';

View File

@ -170,7 +170,9 @@ export default class IFrame extends React.Component<IFrameProps, object> {
style,
allow,
sandbox,
referrerpolicy
referrerpolicy,
translate: __,
env
} = this.props;
let tempStyle: any = {};
@ -192,7 +194,15 @@ export default class IFrame extends React.Component<IFrameProps, object> {
finalSrc &&
!/^(\.\/|\.\.\/|\/|https?\:\/\/|\/\/)/.test(finalSrc)
) {
return <p> iframe </p>;
return <p>{__('Iframe.invalid')}</p>;
}
if (
location.protocol === 'https:' &&
finalSrc &&
finalSrc.startsWith('http://')
) {
env.notify('error', __('Iframe.invalidProtocol'));
}
return (

View File

@ -1,13 +1,24 @@
import React from 'react';
import {Renderer, RendererProps} from 'amis-core';
import {filter} from 'amis-core';
import {ClassNamesFn, themeable, ThemeProps} from 'amis-core';
import {themeable, ThemeProps} from 'amis-core';
import {autobind, getPropValue} from 'amis-core';
import {Icon} from 'amis-ui';
import {LocaleProps, localeable} from 'amis-core';
import {BaseSchema, SchemaClassName, SchemaTpl, SchemaUrlPath} from '../Schema';
import {resolveVariable} from 'amis-core';
import {handleAction} from 'amis-core';
import type {
ImageAction,
ImageActionKey
} from 'amis-ui/lib/components/ImageGallery';
export interface ImageToolbarAction {
key: keyof typeof ImageActionKey;
label?: string;
icon?: string;
iconClassName?: string;
disabled?: boolean;
}
/**
*
@ -124,6 +135,16 @@ export interface ImageSchema extends BaseSchema {
* target
*/
htmlTarget?: string;
/**
*
*/
showToolbar?: boolean;
/**
*
*/
toolbarActions?: ImageToolbarAction[];
}
export interface ImageThumbProps
@ -284,6 +305,8 @@ export interface ImageFieldProps extends RendererProps {
thumbRatio: '1:1' | '4:3' | '16:9';
originalSrc?: string; // 原图
enlargeAble?: boolean;
showToolbar?: boolean;
toolbarActions?: ImageAction[];
onImageEnlarge?: (
info: {
src: string;
@ -292,6 +315,8 @@ export interface ImageFieldProps extends RendererProps {
caption?: string;
thumbMode?: 'w-full' | 'h-full' | 'contain' | 'cover';
thumbRatio?: '1:1' | '4:3' | '16:9';
showToolbar?: boolean;
toolbarActions?: ImageAction[];
},
target: any
) => void;
@ -317,7 +342,13 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
thumbMode,
thumbRatio
}: ImageThumbProps) {
const {onImageEnlarge, enlargeTitle, enlargeCaption} = this.props;
const {
onImageEnlarge,
enlargeTitle,
enlargeCaption,
showToolbar,
toolbarActions
} = this.props;
onImageEnlarge &&
onImageEnlarge(
@ -327,7 +358,9 @@ export class ImageField extends React.Component<ImageFieldProps, object> {
title: enlargeTitle || title,
caption: enlargeCaption || caption,
thumbMode,
thumbRatio
thumbRatio,
showToolbar,
toolbarActions
},
this.props
);

Some files were not shown because too many files have changed in this diff Show More