mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:48:13 +08:00
feat:transfer组件功能完善 (#3696)
* feat:transfer组件功能完善 * update-snapshot * feat:transfer组件功能完善 * update-snapshot * feat:transfer组件功能完善 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * update-snapshot * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * feat:transfer组件云舍4.0功能 * update-snapshot Co-authored-by: sqzhou <zhoushengqiang01@baidu.com>
This commit is contained in:
parent
8829f6346f
commit
e22e2f80ce
@ -959,40 +959,11 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
|||||||
style="position: relative;"
|
style="position: relative;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<span
|
||||||
class="cxd-ResultBox-value"
|
class="cxd-ResultBox-placeholder"
|
||||||
>
|
>
|
||||||
<span
|
请选择
|
||||||
class="cxd-ResultBox-valueLabel"
|
</span>
|
||||||
>
|
|
||||||
诸葛亮
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
data-index="0"
|
|
||||||
>
|
|
||||||
<icon-mock
|
|
||||||
classname="icon icon-close"
|
|
||||||
icon="close"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="cxd-ResultBox-value"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="cxd-ResultBox-valueLabel"
|
|
||||||
>
|
|
||||||
李白
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
data-index="1"
|
|
||||||
>
|
|
||||||
<icon-mock
|
|
||||||
classname="icon icon-close"
|
|
||||||
icon="close"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<span
|
<span
|
||||||
class="cxd-TransferDropDown-icon"
|
class="cxd-TransferDropDown-icon"
|
||||||
>
|
>
|
||||||
@ -1067,7 +1038,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
|||||||
class="cxd-Table-checkCell"
|
class="cxd-Table-checkCell"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--sm"
|
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -1088,7 +1059,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
class="is-active"
|
class=""
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
class="cxd-Table-checkCell"
|
class="cxd-Table-checkCell"
|
||||||
@ -1175,7 +1146,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
class="is-active"
|
class=""
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
class="cxd-Table-checkCell"
|
class="cxd-Table-checkCell"
|
||||||
@ -1369,11 +1340,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Renderer:select table with labelField & valueField 2`] = `
|
exports[`Renderer:select table with labelField & valueField 2`] = `Object {}`;
|
||||||
Object {
|
|
||||||
"a": "zhugeliang,libai",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Renderer:select tree 1`] = `
|
exports[`Renderer:select tree 1`] = `
|
||||||
<div>
|
<div>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -179,6 +179,7 @@ icon:
|
|||||||
"type": "transfer",
|
"type": "transfer",
|
||||||
"name": "transfer4",
|
"name": "transfer4",
|
||||||
"selectMode": "tree",
|
"selectMode": "tree",
|
||||||
|
"searchable": true,
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"label": "法师",
|
"label": "法师",
|
||||||
@ -283,116 +284,6 @@ icon:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 支持搜索
|
|
||||||
|
|
||||||
```schema: scope="body"
|
|
||||||
{
|
|
||||||
"type": "form",
|
|
||||||
"api": "/api/mock2/form/saveForm",
|
|
||||||
"body": [
|
|
||||||
{
|
|
||||||
"label": "带搜索",
|
|
||||||
"type": "transfer",
|
|
||||||
"name": "transfer6",
|
|
||||||
"selectMode": "chained",
|
|
||||||
"searchable": true,
|
|
||||||
"sortable": true,
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"label": "法师",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "诸葛亮",
|
|
||||||
"value": "zhugeliang"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "战士",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "曹操",
|
|
||||||
"value": "caocao"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "钟无艳",
|
|
||||||
"value": "zhongwuyan"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "打野",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "李白",
|
|
||||||
"value": "libai"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "韩信",
|
|
||||||
"value": "hanxin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "云中君",
|
|
||||||
"value": "yunzhongjun"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 延时加载
|
|
||||||
|
|
||||||
```schema: scope="body"
|
|
||||||
{
|
|
||||||
"type": "form",
|
|
||||||
"api": "/api/mock2/form/saveForm",
|
|
||||||
"body": [
|
|
||||||
{
|
|
||||||
"label": "延时加载",
|
|
||||||
"type": "transfer",
|
|
||||||
"name": "transfer7",
|
|
||||||
"selectMode": "tree",
|
|
||||||
"deferApi": "/api/mock2/form/deferOptions?label=${label}&waitSeconds=2",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"label": "法师",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "诸葛亮",
|
|
||||||
"value": "zhugeliang"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "战士",
|
|
||||||
"defer": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "打野",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"label": "李白",
|
|
||||||
"value": "libai"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "韩信",
|
|
||||||
"value": "hanxin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "云中君",
|
|
||||||
"value": "yunzhongjun"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 关联选择模式
|
### 关联选择模式
|
||||||
|
|
||||||
```schema: scope="body"
|
```schema: scope="body"
|
||||||
@ -506,7 +397,7 @@ icon:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
leftOptions 动态加载,默认 source 接口是返回 options 部分,而 leftOptions 是没有对应的接口可以动态返回了。为了方便,目前如果 source 接口返回的选中中,第一个 option 是以下这种格式则也会把 options[0].leftOptions 当成 leftOptions, options[0].children 当 options。同时 options[0].leftDefaultValue 可以用来配置左侧选项的默认值。
|
`leftOptions` 动态加载,默认 `source` 接口是返回 options 部分,而 `leftOptions` 是没有对应的接口可以动态返回了。为了方便,目前如果 `source` 接口返回的选中中,第一个 option 是以下这种格式则也会把 options[0].leftOptions 当成 `leftOptions`, options[0].children 当 options。同时 options[0].leftDefaultValue 可以用来配置左侧选项的默认值。
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@ -524,13 +415,200 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## searchApi
|
|
||||||
|
|
||||||
**发送**
|
### 延时加载
|
||||||
|
|
||||||
|
```schema: scope="body"
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"api": "/api/mock2/form/saveForm",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"label": "延时加载",
|
||||||
|
"type": "transfer",
|
||||||
|
"name": "transfer7",
|
||||||
|
"selectMode": "tree",
|
||||||
|
"deferApi": "/api/mock2/form/deferOptions?label=${label}&waitSeconds=2",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "法师",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "诸葛亮",
|
||||||
|
"value": "zhugeliang"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "战士",
|
||||||
|
"defer": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "打野",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "李白",
|
||||||
|
"value": "libai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "韩信",
|
||||||
|
"value": "hanxin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "云中君",
|
||||||
|
"value": "yunzhongjun"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 支持搜索
|
||||||
|
#### 左侧搜索功能
|
||||||
|
通过 `searchable` 字段来控制左侧选项栏的搜索功能。
|
||||||
|
|
||||||
|
在不设置 `searchApi` 情况下,对输入框内容和对应列表项的value、label进行匹配,匹配成功就会左侧面板中显示。
|
||||||
|
|
||||||
|
```schema: scope="body"
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"api": "/api/mock2/form/saveForm",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"label": "带搜索",
|
||||||
|
"type": "transfer",
|
||||||
|
"name": "transfer6",
|
||||||
|
"selectMode": "chained",
|
||||||
|
"searchable": true,
|
||||||
|
"sortable": true,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "法师",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "诸葛亮",
|
||||||
|
"value": "zhugeliang"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "战士",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "曹操",
|
||||||
|
"value": "caocao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "钟无艳",
|
||||||
|
"value": "zhongwuyan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "打野",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "李白",
|
||||||
|
"value": "libai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "韩信",
|
||||||
|
"value": "hanxin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "云中君",
|
||||||
|
"value": "yunzhongjun"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 右侧结果搜索功能
|
||||||
|
|
||||||
|
右侧结果搜索是通过`resultSearchable`字段开启,设置该字段为true时开启。
|
||||||
|
|
||||||
|
开启结果搜索后,目前默认通过value、label对输入内容进行模糊匹配。
|
||||||
|
|
||||||
|
目前树的延时加载不支持结果搜索功能。
|
||||||
|
|
||||||
|
```schema: scope="body"
|
||||||
|
{
|
||||||
|
"type": "page",
|
||||||
|
"body": {
|
||||||
|
"type": "form",
|
||||||
|
"api": "/api/mock2/form/saveForm",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"label": "树型展示",
|
||||||
|
"type": "transfer",
|
||||||
|
"name": "transfer4",
|
||||||
|
"selectMode": "tree",
|
||||||
|
"searchable": true,
|
||||||
|
"resultListModeFollowSelect": true,
|
||||||
|
"resultSearchable": true,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "法师",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "诸葛亮",
|
||||||
|
"value": "zhugeliang"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "战士",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "曹操",
|
||||||
|
"value": "caocao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "钟无艳",
|
||||||
|
"value": "zhongwuyan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "打野",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "李白",
|
||||||
|
"value": "libai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "韩信",
|
||||||
|
"value": "hanxin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "云中君",
|
||||||
|
"value": "yunzhongjun"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### searchApi
|
||||||
|
|
||||||
|
设置这个api,可以实现左侧选项搜索结果的检索。
|
||||||
|
|
||||||
|
##### 发送
|
||||||
|
|
||||||
默认 GET,携带 term 变量,值为搜索框输入的文字,可从上下文中取数据设置进去。
|
默认 GET,携带 term 变量,值为搜索框输入的文字,可从上下文中取数据设置进去。
|
||||||
|
|
||||||
**响应**
|
##### 响应
|
||||||
|
|
||||||
格式要求如下:
|
格式要求如下:
|
||||||
|
|
||||||
@ -559,7 +637,132 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
|
|||||||
|
|
||||||
适用于需选择的数据/信息源较多时,用户可直观的知道自己所选择的数据/信息的场景,一般左侧框为数据/信息源,右侧为已选数据/信息,被选中信息同时存在于 2 个框内。
|
适用于需选择的数据/信息源较多时,用户可直观的知道自己所选择的数据/信息的场景,一般左侧框为数据/信息源,右侧为已选数据/信息,被选中信息同时存在于 2 个框内。
|
||||||
|
|
||||||
## 自定义选项展示
|
### 结果面板跟随模式
|
||||||
|
|
||||||
|
`resultListModeFollowSelect` 开启结果面板跟随模式。
|
||||||
|
|
||||||
|
#### 表格跟随模式
|
||||||
|
```schema: scope="body"
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"api": "/api/mock2/form/saveForm",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"label": "表格形式",
|
||||||
|
"type": "transfer",
|
||||||
|
"name": "transfer",
|
||||||
|
"selectMode": "table",
|
||||||
|
"resultListModeFollowSelect": true,
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "label",
|
||||||
|
"label": "英雄"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "position",
|
||||||
|
"label": "位置"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "诸葛亮",
|
||||||
|
"value": "zhugeliang",
|
||||||
|
"position": "中单"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "曹操",
|
||||||
|
"value": "caocao",
|
||||||
|
"position": "上单"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "钟无艳",
|
||||||
|
"value": "zhongwuyan",
|
||||||
|
"position": "上单"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "李白",
|
||||||
|
"value": "libai",
|
||||||
|
"position": "打野"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "韩信",
|
||||||
|
"value": "hanxin",
|
||||||
|
"position": "打野"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "云中君",
|
||||||
|
"value": "yunzhongjun",
|
||||||
|
"position": "打野"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 树形跟随模式
|
||||||
|
```schema: scope="body"
|
||||||
|
{
|
||||||
|
"type": "page",
|
||||||
|
"body": {
|
||||||
|
"type": "form",
|
||||||
|
"api": "/api/mock2/form/saveForm",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"label": "树型展示",
|
||||||
|
"type": "transfer",
|
||||||
|
"name": "transfer4",
|
||||||
|
"selectMode": "tree",
|
||||||
|
"searchable": true,
|
||||||
|
"resultListModeFollowSelect": true,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "法师",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "诸葛亮",
|
||||||
|
"value": "zhugeliang"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "战士",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "曹操",
|
||||||
|
"value": "caocao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "钟无艳",
|
||||||
|
"value": "zhongwuyan"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "打野",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "李白",
|
||||||
|
"value": "libai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "韩信",
|
||||||
|
"value": "hanxin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "云中君",
|
||||||
|
"value": "yunzhongjun"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义选项展示
|
||||||
|
|
||||||
```schema: scope="body"
|
```schema: scope="body"
|
||||||
{
|
{
|
||||||
@ -612,27 +815,31 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
|
|||||||
|
|
||||||
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
|
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
|
||||||
|
|
||||||
| 属性名 | 类型 | 默认值 | 说明 |
|
| 属性名 | 类型 | 默认值 | 说明 |
|
||||||
| ---------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) |
|
| options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) |
|
||||||
| source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) |
|
| source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) |
|
||||||
| delimiter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
|
| delimeter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
|
||||||
| joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) |
|
| joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) |
|
||||||
| extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) |
|
| extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) |
|
||||||
| searchable | `boolean` | `false` | 当设置为 `true` 时表示可以通过输入部分内容检索出选项。 |
|
| searchApi | [API](../../../docs/types/api) | | 如果想通过接口检索,可以设置这个api。|
|
||||||
| searchApi | [API](../../../docs/types/api) | | 如果想通过接口检索,可以设置个 api。 |
|
| resultListModeFollowSelect | `boolean` | `false` | 结果面板跟随模式,目前只支持`list`、`table`、`tree`(tree目前只支持非延时加载的`tree`) |
|
||||||
| statistics | `boolean` | `true` | 是否显示统计数据 |
|
| statistics | `boolean` | `true` | 是否显示统计数据|
|
||||||
| selectTitle | `string` | `"请选择"` | 左侧的标题文字 |
|
| selectTitle | `string` | `"请选择"` | 左侧的标题文字 |
|
||||||
| resultTitle | `string` | `"当前选择"` | 右侧结果的标题文字 |
|
| resultTitle | `string` | `"当前选择"` | 右侧结果的标题文字|
|
||||||
| sortable | `boolean` | `false` | 结果可以进行拖拽排序 |
|
| sortable | `boolean` | `false` | 结果可以进行拖拽排序(结果列表为树时,不支持排序)|
|
||||||
| selectMode | `string` | `list` | 可选:`list`、`table`、`tree`、`chained`、`associated`。分别为:列表形式、表格形式、树形选择形式、级联选择形式,关联选择形式(与级联选择的区别在于,级联是无限极,而关联只有一级,关联左边可以是个 tree)。 |
|
| selectMode | `string` | `list` | 可选:`list`、`table`、`tree`、`chained`、`associated`。分别为:列表形式、表格形式、树形选择形式、级联选择形式,关联选择形式(与级联选择的区别在于,级联是无限极,而关联只有一级,关联左边可以是个 tree)。 |
|
||||||
| searchResultMode | `string` | | 如果不设置将采用 `selectMode` 的值,可以单独配置,参考 `selectMode`,决定搜索结果的展示形式。 |
|
| searchResultMode | `string` | | 如果不设置将采用 `selectMode` 的值,可以单独配置,参考 `selectMode`,决定搜索结果的展示形式。 |
|
||||||
| columns | `Array<Object>` | | 当展示形式为 `table` 可以用来配置展示哪些列,跟 table 中的 columns 配置相似,只是只有展示功能。 |
|
| searchable | `boolean` | `false` | 左侧列表搜索功能,当设置为 true 时表示可以通过输入部分内容检索出选项项。 |
|
||||||
| leftOptions | `Array<Object>` | | 当展示形式为 `associated` 时用来配置左边的选项集。 |
|
| searchPlaceholder | `string` | | 左侧列表搜索框提示 |
|
||||||
| leftMode | `string` | | 当展示形式为 `associated` 时用来配置左边的选择形式,支持 `list` 或者 `tree`。默认为 `list`。 |
|
| columns | `Array<Object>` | | 当展示形式为 `table` 可以用来配置展示哪些列,跟 table 中的 columns 配置相似,只是只有展示功能。 |
|
||||||
| rightMode | `string` | | 当展示形式为 `associated` 时用来配置右边的选择形式,可选:`list`、`table`、`tree`、`chained`。 |
|
| leftOptions | `Array<Object>` | | 当展示形式为 `associated` 时用来配置左边的选项集。|
|
||||||
| menuTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义选项展示 |
|
| leftMode | `string` | | 当展示形式为 `associated` 时用来配置左边的选择形式,支持 `list` 或者 `tree`。默认为 `list`。|
|
||||||
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
|
| rightMode | `string` | | 当展示形式为 `associated` 时用来配置右边的选择形式,可选:`list`、`table`、`tree`、`chained`。 |
|
||||||
|
| resultSearchable | `boolean` | `false` | 结果(右则)列表的检索功能,当设置为true时,可以通过输入检索模糊匹配检索内容(目前树的延时加载不支持结果搜索功能) |
|
||||||
|
| resultSearchPlaceholder | `string` | | 右侧列表搜索框提示 |
|
||||||
|
| menuTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义选项展示 |
|
||||||
|
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
|
||||||
|
|
||||||
## 事件表
|
## 事件表
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
color: var(--Form-input-placeholderColor);
|
color: var(--Form-input-placeholderColor);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
flex-basis: var(--Form-input-height);
|
flex-basis: var(--Form-input-height);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
> .#{$ns}Checkbox {
|
> .#{$ns}Checkbox {
|
||||||
margin-right: px2rem(8px);
|
margin-right: px2rem(10px);
|
||||||
}
|
}
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -104,6 +104,10 @@
|
|||||||
|
|
||||||
&-placeholder {
|
&-placeholder {
|
||||||
@include checkboxes-placeholder();
|
@include checkboxes-placeholder();
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +139,6 @@
|
|||||||
padding-right: var(--gap-md);
|
padding-right: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$ns}Table-table > tbody > tr {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$ns}Table-table > tbody > tr.is-active {
|
.#{$ns}Table-table > tbody > tr.is-active {
|
||||||
color: var(--Form-select-menu-onActive-color);
|
color: var(--Form-select-menu-onActive-color);
|
||||||
background: var(--Form-select-menu-onActive-bg);
|
background: var(--Form-select-menu-onActive-bg);
|
||||||
@ -148,24 +148,13 @@
|
|||||||
.#{$ns}TreeSelection {
|
.#{$ns}TreeSelection {
|
||||||
.#{$ns}Table-expandBtn {
|
.#{$ns}Table-expandBtn {
|
||||||
color: var(--icon-color);
|
color: var(--icon-color);
|
||||||
margin-right: 5px;
|
margin-right: var(--gap-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-sublist {
|
&-sublist {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 0 0 px2rem(35px);
|
margin: 0 0 0 px2rem(35px);
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
&:before {
|
|
||||||
width: 1px;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--gap-xs) * -1);
|
|
||||||
bottom: calc(var(--Form-input-height) / 2);
|
|
||||||
left: -19px;
|
|
||||||
border-left: dashed 1px var(--icon-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
@ -183,35 +172,17 @@
|
|||||||
color: var(--text--muted-color);
|
color: var(--text--muted-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-sublist &-item:before {
|
|
||||||
height: 1px;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--Form-input-height) / 2);
|
|
||||||
width: 19px;
|
|
||||||
left: -19px;
|
|
||||||
border-top: dashed 1px var(--icon-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-itemInner {
|
&-itemInner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// height: var(--Form-input-height);
|
line-height: var(--Tree-itemHeight);
|
||||||
line-height: var(--Form-input-lineHeight);
|
position: relative;
|
||||||
font-size: var(--Form-input-fontSize);
|
font-size: var(--Form-input-fontSize);
|
||||||
padding: calc(
|
padding: 0 var(--gap-sm);
|
||||||
(
|
|
||||||
var(--Form-input-height) - var(--Form-input-lineHeight) *
|
|
||||||
var(--Form-input-fontSize)
|
|
||||||
) / 2
|
|
||||||
)
|
|
||||||
var(--gap-sm);
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
> .#{$ns}Checkbox {
|
> .#{$ns}Checkbox {
|
||||||
margin-right: 0;
|
margin-right: var(--gap-sm);
|
||||||
margin-left: var(--gap-sm);
|
|
||||||
}
|
}
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -234,7 +205,6 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-left: var(--gap-xs);
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,7 +273,7 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-left: var(--gap-xs);
|
margin-left: px2rem(10px);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -358,3 +328,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$ns}ResultTreeList {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$ns}ResultTableList {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&-close-btn {
|
||||||
|
float: right;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,10 @@
|
|||||||
min-height: px2rem(300px);
|
min-height: px2rem(300px);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&-searchbox {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&--inline {
|
&--inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@ -70,6 +74,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$ns}-ResultTreeList {
|
||||||
|
border-top: 1px solid var(--borderColor);
|
||||||
|
}
|
||||||
|
|
||||||
.#{$ns}AssociatedSelection {
|
.#{$ns}AssociatedSelection {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -79,6 +87,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-select {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
&-search + &-selection {
|
&-search + &-selection {
|
||||||
border-top: 1px solid var(--borderColor);
|
border-top: 1px solid var(--borderColor);
|
||||||
}
|
}
|
||||||
@ -129,6 +141,14 @@
|
|||||||
color: var(--text--muted-color);
|
color: var(--text--muted-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$ns}Tree {
|
||||||
|
padding: px2rem(2px) px2rem(10px);
|
||||||
|
|
||||||
|
&-itemLabel:hover::after {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$ns}TabsTransfer {
|
.#{$ns}TabsTransfer {
|
||||||
@ -230,6 +250,7 @@
|
|||||||
|
|
||||||
.#{$ns}TransferControl {
|
.#{$ns}TransferControl {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&.is-inline {
|
&.is-inline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -155,16 +155,65 @@
|
|||||||
|
|
||||||
.#{$ns}Transfer {
|
.#{$ns}Transfer {
|
||||||
&-title {
|
&-title {
|
||||||
height: var(--Form-input-height);
|
flex: 0 0 #{px2rem(38px)};
|
||||||
padding-left: #{px2rem(16px)};
|
height: #{px2rem(38px)};
|
||||||
padding-right: #{px2rem(16px)};
|
padding-left: var(--gap-base);
|
||||||
|
padding-right: var(--gap-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-result {
|
||||||
|
|
||||||
|
>.#{$ns}Selections {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-table-title {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.#{$ns}ListCheckboxes {
|
.#{$ns}ListCheckboxes {
|
||||||
.#{$ns}ListCheckboxes-item {
|
.#{$ns}ListCheckboxes-item {
|
||||||
padding-left: #{px2rem(16px)};
|
padding-left: #{px2rem(16px)};
|
||||||
padding-right: #{px2rem(16px)};
|
padding-right: #{px2rem(16px)};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$ns}TableSelection {
|
||||||
|
.#{$ns}Table-table {
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
&.is-active {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$ns}GroupedSelection {
|
||||||
|
&-item {
|
||||||
|
padding-left: var(--gap-base);
|
||||||
|
padding-right: var(--gap-base);
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$ns}Selections {
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
padding-left: var(--gap-base);
|
||||||
|
padding-right: var(--gap-base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$ns}Modal {
|
.#{$ns}Modal {
|
||||||
|
@ -12,11 +12,11 @@ import {themeable} from '../theme';
|
|||||||
import {uncontrollable} from 'uncontrollable';
|
import {uncontrollable} from 'uncontrollable';
|
||||||
import GroupedSelection from './GroupedSelection';
|
import GroupedSelection from './GroupedSelection';
|
||||||
import TableSelection from './TableSelection';
|
import TableSelection from './TableSelection';
|
||||||
import TreeSelection from './TreeSelection';
|
|
||||||
import GroupedSelecton from './GroupedSelection';
|
import GroupedSelecton from './GroupedSelection';
|
||||||
import ChainedSelection from './ChainedSelection';
|
import ChainedSelection from './ChainedSelection';
|
||||||
import {Icon} from './icons';
|
import {Icon} from './icons';
|
||||||
import {localeable} from '../locale';
|
import {localeable} from '../locale';
|
||||||
|
import Tree from './Tree';
|
||||||
|
|
||||||
export interface AssociatedSelectionProps extends BaseSelectionProps {
|
export interface AssociatedSelectionProps extends BaseSelectionProps {
|
||||||
leftOptions: Options;
|
leftOptions: Options;
|
||||||
@ -44,6 +44,7 @@ export class AssociatedSelection extends BaseSelection<
|
|||||||
AssociatedSelectionProps,
|
AssociatedSelectionProps,
|
||||||
AssociatedSelectionState
|
AssociatedSelectionState
|
||||||
> {
|
> {
|
||||||
|
|
||||||
state: AssociatedSelectionState = {
|
state: AssociatedSelectionState = {
|
||||||
leftValue: this.props.leftDefaultValue
|
leftValue: this.props.leftDefaultValue
|
||||||
};
|
};
|
||||||
@ -133,14 +134,12 @@ export class AssociatedSelection extends BaseSelection<
|
|||||||
<div className={cx('AssociatedSelection', className)}>
|
<div className={cx('AssociatedSelection', className)}>
|
||||||
<div className={cx('AssociatedSelection-left')}>
|
<div className={cx('AssociatedSelection-left')}>
|
||||||
{leftMode === 'tree' ? (
|
{leftMode === 'tree' ? (
|
||||||
<TreeSelection
|
<Tree
|
||||||
option2value={this.leftOption2Value}
|
|
||||||
options={leftOptions}
|
|
||||||
value={this.state.leftValue}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={this.handleLeftSelect}
|
|
||||||
multiple={false}
|
multiple={false}
|
||||||
clearable={false}
|
disabled={disabled}
|
||||||
|
value={this.state.leftValue}
|
||||||
|
options={leftOptions}
|
||||||
|
onChange={this.handleLeftSelect}
|
||||||
onDeferLoad={this.handleLeftDeferLoad}
|
onDeferLoad={this.handleLeftDeferLoad}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -192,14 +191,12 @@ export class AssociatedSelection extends BaseSelection<
|
|||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
/>
|
/>
|
||||||
) : rightMode === 'tree' ? (
|
) : rightMode === 'tree' ? (
|
||||||
<TreeSelection
|
<Tree
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
options={selectdOption.children || []}
|
options={selectdOption.children || []}
|
||||||
onChange={onChange}
|
onChange={onChange!}
|
||||||
option2value={option2value}
|
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
itemRender={itemRender}
|
|
||||||
/>
|
/>
|
||||||
) : rightMode === 'chained' ? (
|
) : rightMode === 'chained' ? (
|
||||||
<ChainedSelection
|
<ChainedSelection
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import {BaseSelection} from './Selection';
|
|
||||||
import {themeable} from '../theme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {uncontrollable} from 'uncontrollable';
|
import {uncontrollable} from 'uncontrollable';
|
||||||
|
|
||||||
|
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||||
|
import {themeable} from '../theme';
|
||||||
import Checkbox from './Checkbox';
|
import Checkbox from './Checkbox';
|
||||||
import {Option} from './Select';
|
import {Option} from './Select';
|
||||||
import {localeable} from '../locale';
|
import {localeable} from '../locale';
|
||||||
|
|
||||||
export class GroupedSelection extends BaseSelection {
|
export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
|
||||||
valueArray: Array<Option>;
|
valueArray: Array<Option>;
|
||||||
|
|
||||||
renderOption(option: Option, index: number) {
|
renderOption(option: Option, index: number) {
|
||||||
@ -18,6 +19,7 @@ export class GroupedSelection extends BaseSelection {
|
|||||||
itemRender,
|
itemRender,
|
||||||
multiple
|
multiple
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const valueArray = this.valueArray;
|
const valueArray = this.valueArray;
|
||||||
|
|
||||||
if (Array.isArray(option.children)) {
|
if (Array.isArray(option.children)) {
|
||||||
|
@ -2,15 +2,21 @@
|
|||||||
* 用来显示选择结果,垂直显示。支持移出、排序等操作。
|
* 用来显示选择结果,垂直显示。支持移出、排序等操作。
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Option} from './Select';
|
import Sortable from 'sortablejs';
|
||||||
|
import {findDOMNode} from 'react-dom';
|
||||||
|
|
||||||
|
import {Option, Options} from './Select';
|
||||||
import {ThemeProps, themeable} from '../theme';
|
import {ThemeProps, themeable} from '../theme';
|
||||||
import {Icon} from './icons';
|
import {Icon} from './icons';
|
||||||
import {autobind, guid} from '../utils/helper';
|
import {autobind, guid} from '../utils/helper';
|
||||||
import Sortable from 'sortablejs';
|
|
||||||
import {findDOMNode} from 'react-dom';
|
|
||||||
import {LocaleProps, localeable} from '../locale';
|
import {LocaleProps, localeable} from '../locale';
|
||||||
|
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||||
|
import TransferSearch from './TransferSearch';
|
||||||
|
|
||||||
export interface ResultListProps extends ThemeProps, LocaleProps {
|
export interface ResultListProps
|
||||||
|
extends ThemeProps,
|
||||||
|
LocaleProps,
|
||||||
|
BaseSelectionProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
value?: Array<Option>;
|
value?: Array<Option>;
|
||||||
onChange?: (value: Array<Option>, optionModified?: boolean) => void;
|
onChange?: (value: Array<Option>, optionModified?: boolean) => void;
|
||||||
@ -20,6 +26,23 @@ export interface ResultListProps extends ThemeProps, LocaleProps {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
|
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||||
itemClassName?: string;
|
itemClassName?: string;
|
||||||
|
columns: Array<{
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
}>;
|
||||||
|
cellRender?: (
|
||||||
|
column: {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
},
|
||||||
|
option: Option,
|
||||||
|
colIndex: number,
|
||||||
|
rowIndex: number
|
||||||
|
) => JSX.Element;
|
||||||
|
searchable?: boolean;
|
||||||
|
onSearch?: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ItemRenderStates {
|
export interface ItemRenderStates {
|
||||||
@ -28,17 +51,35 @@ export interface ItemRenderStates {
|
|||||||
onChange: (value: any, name: string) => void;
|
onChange: (value: any, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResultList extends React.Component<ResultListProps> {
|
interface ResultListState {
|
||||||
|
searchResult: Options | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResultList extends React.Component<
|
||||||
|
ResultListProps,
|
||||||
|
ResultListState
|
||||||
|
> {
|
||||||
|
|
||||||
static itemRender(option: any) {
|
static itemRender(option: any) {
|
||||||
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
|
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
|
||||||
}
|
}
|
||||||
static defaultProps: Pick<ResultListProps, 'placeholder' | 'itemRender'> = {
|
|
||||||
|
static defaultProps: Pick<
|
||||||
|
ResultListProps,
|
||||||
|
'placeholder' | 'itemRender'
|
||||||
|
> = {
|
||||||
placeholder: 'placeholder.selectData',
|
placeholder: 'placeholder.selectData',
|
||||||
itemRender: ResultList.itemRender
|
itemRender: ResultList.itemRender
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state: ResultListState = {
|
||||||
|
searchResult: null
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelSearch?: () => void;
|
||||||
id = guid();
|
id = guid();
|
||||||
sortable?: Sortable;
|
sortable?: Sortable;
|
||||||
|
unmounted = false;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.sortable && this.initSortable();
|
this.props.sortable && this.initSortable();
|
||||||
@ -54,20 +95,7 @@ export class ResultList extends React.Component<ResultListProps> {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.desposeSortable();
|
this.desposeSortable();
|
||||||
}
|
this.unmounted = true;
|
||||||
|
|
||||||
@autobind
|
|
||||||
handleRemove(e: React.MouseEvent<HTMLElement>) {
|
|
||||||
const index = parseInt(e.currentTarget.getAttribute('data-index')!, 10);
|
|
||||||
const {value, onChange} = this.props;
|
|
||||||
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newValue = value.concat();
|
|
||||||
newValue.splice(index, 1);
|
|
||||||
onChange?.(newValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initSortable() {
|
initSortable() {
|
||||||
@ -138,24 +166,65 @@ export class ResultList extends React.Component<ResultListProps> {
|
|||||||
onChange?.(result, true);
|
onChange?.(result, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
@autobind
|
||||||
|
search(inputValue: string) {
|
||||||
|
const {onSearch, value} = this.props;
|
||||||
|
const searchResult = (value || []).filter(
|
||||||
|
item => onSearch && onSearch(inputValue, item)
|
||||||
|
);
|
||||||
|
this.setState({searchResult})
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
clearSearch() {
|
||||||
|
this.setState({searchResult: null})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭表格最后一项
|
||||||
|
@autobind
|
||||||
|
handleCloseItem(option: Option) {
|
||||||
|
const {value, onChange, option2value, options, disabled} = this.props;
|
||||||
|
|
||||||
|
if (disabled || option.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除普通值
|
||||||
|
let valueArray = BaseSelection.value2array(value, options, option2value);
|
||||||
|
|
||||||
|
let idx = valueArray.indexOf(option);
|
||||||
|
valueArray.splice(idx, 1);
|
||||||
|
let newValue: string | Array<Option> = option2value
|
||||||
|
? valueArray.map(item => option2value(item))
|
||||||
|
: valueArray;
|
||||||
|
onChange && onChange(newValue);
|
||||||
|
|
||||||
|
const {searchResult} = this.state;
|
||||||
|
if (searchResult) {
|
||||||
|
const searchArray = BaseSelection.value2array(
|
||||||
|
searchResult,
|
||||||
|
options,
|
||||||
|
option2value
|
||||||
|
);
|
||||||
|
const searchIdx = searchArray.indexOf(option);
|
||||||
|
searchResult.splice(searchIdx, 1);
|
||||||
|
this.setState({searchResult});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNormalList(value?: Options) {
|
||||||
const {
|
const {
|
||||||
classnames: cx,
|
classnames: cx,
|
||||||
className,
|
|
||||||
value,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
itemRender,
|
itemRender,
|
||||||
disabled,
|
disabled,
|
||||||
title,
|
|
||||||
itemClassName,
|
itemClassName,
|
||||||
sortable,
|
sortable,
|
||||||
translate: __
|
translate: __
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('Selections', className)}>
|
<>
|
||||||
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
|
||||||
|
|
||||||
{Array.isArray(value) && value.length ? (
|
{Array.isArray(value) && value.length ? (
|
||||||
<div className={cx('Selections-items')}>
|
<div className={cx('Selections-items')}>
|
||||||
{value.map((option, index) => (
|
{value.map((option, index) => (
|
||||||
@ -186,7 +255,9 @@ export class ResultList extends React.Component<ResultListProps> {
|
|||||||
<a
|
<a
|
||||||
className={cx('Selections-delBtn')}
|
className={cx('Selections-delBtn')}
|
||||||
data-index={index}
|
data-index={index}
|
||||||
onClick={this.handleRemove}
|
onClick={(e: React.MouseEvent<HTMLElement>) =>
|
||||||
|
this.handleCloseItem(option)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon icon="close" className="icon" />
|
<Icon icon="close" className="icon" />
|
||||||
</a>
|
</a>
|
||||||
@ -194,9 +265,36 @@ export class ResultList extends React.Component<ResultListProps> {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) :
|
||||||
<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>
|
(<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)}
|
||||||
)}
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
classnames: cx,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
searchable,
|
||||||
|
value,
|
||||||
|
translate: __,
|
||||||
|
placeholder = __('Transfer.searchKeyword')
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {searchResult} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('Selections', className)}>
|
||||||
|
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
||||||
|
{searchable ? (
|
||||||
|
<TransferSearch
|
||||||
|
placeholder={placeholder}
|
||||||
|
onSearch={this.search}
|
||||||
|
onCancelSearch={this.clearSearch}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{this.renderNormalList(searchResult !== null ? searchResult : value)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
231
src/components/ResultTableList.tsx
Normal file
231
src/components/ResultTableList.tsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* 结果表格(暂时不支持结果排序)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||||
|
import {themeable} from '../theme';
|
||||||
|
import {Option, Options} from './Select';
|
||||||
|
import {resolveVariable} from '../utils/tpl-builtin';
|
||||||
|
import {localeable} from '../locale';
|
||||||
|
import {autobind} from '../utils/helper';
|
||||||
|
import TransferSearch from './TransferSearch';
|
||||||
|
|
||||||
|
import {CloseIcon} from './icons';
|
||||||
|
import TableSelection from './TableSelection';
|
||||||
|
|
||||||
|
export interface ResultTableSelectionProps extends BaseSelectionProps {
|
||||||
|
title?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
searchable?: boolean;
|
||||||
|
onSearch?: Function;
|
||||||
|
columns: Array<{
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
}>;
|
||||||
|
cellRender: (
|
||||||
|
column: {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
},
|
||||||
|
option: Option,
|
||||||
|
colIndex: number,
|
||||||
|
rowIndex: number
|
||||||
|
) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResultTableSelectionState {
|
||||||
|
tableOptions: Options;
|
||||||
|
searching: Boolean;
|
||||||
|
searchTableOptions: Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseResultTableSelection extends BaseSelection<ResultTableSelectionProps, ResultTableSelectionState> {
|
||||||
|
static defaultProps = {
|
||||||
|
...BaseSelection.defaultProps,
|
||||||
|
cellRender: (
|
||||||
|
column: {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
},
|
||||||
|
option: Option,
|
||||||
|
colIndex: number,
|
||||||
|
rowIndex: number
|
||||||
|
) => <span>{resolveVariable(column.name, option)}</span>,
|
||||||
|
};
|
||||||
|
|
||||||
|
state: ResultTableSelectionState = {
|
||||||
|
tableOptions: [],
|
||||||
|
searching: false,
|
||||||
|
searchTableOptions: []
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props: ResultTableSelectionProps) {
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
option2value,
|
||||||
|
} = props;
|
||||||
|
const valueArray = BaseSelection.value2array(value, options, option2value);
|
||||||
|
return {
|
||||||
|
tableOptions: valueArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleCloseItem(option: Option) {
|
||||||
|
const {value, onChange, option2value, options, disabled} = this.props;
|
||||||
|
|
||||||
|
const {searching, searchTableOptions} = this.state;
|
||||||
|
|
||||||
|
if (disabled || option.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除普通值
|
||||||
|
let valueArray = BaseSelection.value2array(value, options, option2value);
|
||||||
|
|
||||||
|
let idx = valueArray.indexOf(option);
|
||||||
|
valueArray.splice(idx, 1);
|
||||||
|
let newValue: string | Array<Option> = option2value
|
||||||
|
? valueArray.map(item => option2value(item))
|
||||||
|
: valueArray;
|
||||||
|
onChange && onChange(newValue);
|
||||||
|
|
||||||
|
if (searching) {
|
||||||
|
const searchArray = BaseSelection.value2array(
|
||||||
|
searchTableOptions,
|
||||||
|
options,
|
||||||
|
option2value
|
||||||
|
);
|
||||||
|
const searchIdx = searchArray.indexOf(option);
|
||||||
|
searchTableOptions.splice(searchIdx, 1);
|
||||||
|
this.setState({searchTableOptions});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
search(inputValue: string) {
|
||||||
|
// 结果为空,直接清空
|
||||||
|
if (!inputValue) {
|
||||||
|
this.clearSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {value, onSearch} = this.props;
|
||||||
|
const searchOptions = ((value || []) as Options)
|
||||||
|
.filter(item => onSearch?.(inputValue, item));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
searching: true,
|
||||||
|
searchTableOptions: searchOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
clearSearch() {
|
||||||
|
this.setState({
|
||||||
|
searching: false,
|
||||||
|
searchTableOptions: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTable() {
|
||||||
|
const {
|
||||||
|
classnames: cx,
|
||||||
|
className,
|
||||||
|
columns,
|
||||||
|
cellRender,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
option2value,
|
||||||
|
onChange,
|
||||||
|
translate: __,
|
||||||
|
placeholder
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {searching, tableOptions, searchTableOptions} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('ResultTableList', className)}>
|
||||||
|
{
|
||||||
|
Array.isArray(value) && value.length ?
|
||||||
|
(
|
||||||
|
<TableSelection
|
||||||
|
columns={columns}
|
||||||
|
options={!searching ? tableOptions : searchTableOptions}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
option2value={option2value}
|
||||||
|
onChange={onChange}
|
||||||
|
multiple={false}
|
||||||
|
cellRender={(
|
||||||
|
column: {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
},
|
||||||
|
option: Option,
|
||||||
|
colIndex: number,
|
||||||
|
rowIndex: number
|
||||||
|
) => {
|
||||||
|
const raw = cellRender(column, option, colIndex, rowIndex);
|
||||||
|
if (colIndex === columns.length - 1) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{raw}
|
||||||
|
{
|
||||||
|
<span
|
||||||
|
className={cx('ResultTableList-close-btn')}
|
||||||
|
onClick={(e: React.SyntheticEvent<HTMLElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.handleCloseItem(option);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: (<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
classnames: cx,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
searchable,
|
||||||
|
translate: __,
|
||||||
|
placeholder = __('Transfer.searchKeyword')
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('Selections', className)}>
|
||||||
|
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
||||||
|
{searchable ? (
|
||||||
|
<TransferSearch
|
||||||
|
placeholder={placeholder}
|
||||||
|
onSearch={this.search}
|
||||||
|
onCancelSearch={this.clearSearch}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{this.renderTable()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(localeable(BaseResultTableSelection));
|
298
src/components/ResultTreeList.tsx
Normal file
298
src/components/ResultTreeList.tsx
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* 结果树(暂时不支持结果排序)
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import {cloneDeep, isEqual, omit} from 'lodash';
|
||||||
|
|
||||||
|
import {Option, Options} from './Select';
|
||||||
|
import {ThemeProps, themeable} from '../theme';
|
||||||
|
import {autobind, noop} from '../utils/helper';
|
||||||
|
import {LocaleProps, localeable} from '../locale';
|
||||||
|
import {BaseSelectionProps} from './Selection';
|
||||||
|
import Tree from './Tree';
|
||||||
|
import TransferSearch from './TransferSearch';
|
||||||
|
|
||||||
|
export interface ResultTreeListProps extends ThemeProps, LocaleProps, BaseSelectionProps {
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
searchable?: boolean;
|
||||||
|
value: Array<Option>;
|
||||||
|
valueField: string;
|
||||||
|
onSearch?: Function;
|
||||||
|
onChange: (value: Array<Option>, optionModified?: boolean) => void;
|
||||||
|
placeholder: string;
|
||||||
|
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||||
|
itemClassName?: string;
|
||||||
|
cellRender?: (
|
||||||
|
column: {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
[propName: string]: any;
|
||||||
|
},
|
||||||
|
option: Option,
|
||||||
|
colIndex: number,
|
||||||
|
rowIndex: number
|
||||||
|
) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemRenderStates {
|
||||||
|
index: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: any, name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultTreeListState {
|
||||||
|
searching: Boolean;
|
||||||
|
treeOptions: Options;
|
||||||
|
searchTreeOptions: Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归找到对应选中节点(dfs)
|
||||||
|
function getDeep(
|
||||||
|
node: Option,
|
||||||
|
cb: (node: Option) => boolean,
|
||||||
|
pathNodes: Array<Option>,
|
||||||
|
valueField: string
|
||||||
|
) {
|
||||||
|
if (node[valueField] && cb(node)) {
|
||||||
|
node.isChecked = true;
|
||||||
|
for (let i = pathNodes.length - 2; i >= 0; i--) {
|
||||||
|
if (!pathNodes[i].isChecked) {
|
||||||
|
pathNodes[i].isChecked = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (node.children && Array.isArray(node.children)) {
|
||||||
|
node.children.forEach(n => {
|
||||||
|
pathNodes.push(n);
|
||||||
|
getDeep(n, cb, pathNodes, valueField);
|
||||||
|
pathNodes.pop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归删除树多余的节点
|
||||||
|
function deepCheckedTreeNode(nodes: Array<Option>) {
|
||||||
|
let arr: Array<Option> = [];
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
if (node.isChecked) {
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
node.children = deepCheckedTreeNode(node.children);
|
||||||
|
}
|
||||||
|
arr.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据选项获取到结果
|
||||||
|
function getResultOptions(value: Options = [], options: Options, valueField: string) {
|
||||||
|
const newOptions = cloneDeep(options) as Array<Option>;
|
||||||
|
const callBack = (node: Option) =>
|
||||||
|
!!(value || []).find(target => target[valueField] === node[valueField]);
|
||||||
|
newOptions &&
|
||||||
|
newOptions.forEach(op => {
|
||||||
|
getDeep(op, callBack, [op], valueField);
|
||||||
|
});
|
||||||
|
return deepCheckedTreeNode(newOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在包含回调函数情况下,遍历树
|
||||||
|
function deepTree(nodes: Options, cb: (node: Option) => void) {
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
cb(node);
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
deepTree(node.children, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树的节点删除
|
||||||
|
function deepDeleteTree(nodes: Options, option: Option, valueField: string) {
|
||||||
|
let arr: Array<Option> = [];
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
if (isEqual(node, option)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
node.children = deepDeleteTree(node.children, option, valueField);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(node.children && node.children.length > 0) ||
|
||||||
|
node[valueField] !== undefined
|
||||||
|
) {
|
||||||
|
arr.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseResultTreeList extends React.Component<ResultTreeListProps, ResultTreeListState> {
|
||||||
|
|
||||||
|
static itemRender(option: any) {
|
||||||
|
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps: Pick<ResultTreeListProps, 'placeholder' | 'itemRender'> = {
|
||||||
|
placeholder: 'placeholder.selectData',
|
||||||
|
itemRender: BaseResultTreeList.itemRender
|
||||||
|
};
|
||||||
|
|
||||||
|
state: ResultTreeListState = {
|
||||||
|
searching: false,
|
||||||
|
treeOptions: [],
|
||||||
|
searchTreeOptions: []
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props: ResultTreeListProps) {
|
||||||
|
const newOptions = getResultOptions(props.value, props.options, props.valueField);
|
||||||
|
return {
|
||||||
|
treeOptions: cloneDeep(newOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除非选中节点
|
||||||
|
@autobind
|
||||||
|
deleteTreeChecked(option: Option) {
|
||||||
|
const {value = [], onChange, valueField} = this.props;
|
||||||
|
const {searching, treeOptions} = this.state;
|
||||||
|
let temNode: Options = [];
|
||||||
|
const cb = (node: Option) => {
|
||||||
|
if (isEqual(node, option)) {
|
||||||
|
temNode = [node];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deepTree(treeOptions || [], cb);
|
||||||
|
let arr: Options = [];
|
||||||
|
const cb2 = (node: Option) => {
|
||||||
|
if (node.isChecked && node[valueField]) {
|
||||||
|
arr.push(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deepTree(temNode, cb2);
|
||||||
|
onChange &&
|
||||||
|
onChange(
|
||||||
|
value.filter(
|
||||||
|
item =>
|
||||||
|
!arr.find(arrItem =>
|
||||||
|
isEqual(omit(arrItem, ['isChecked', 'childrens']), item)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 搜索时,重新生成树
|
||||||
|
searching && this.deleteResultTreeNode(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索树点击删除时,删除对应节点
|
||||||
|
deleteResultTreeNode(option: Option) {
|
||||||
|
const arr = deepDeleteTree(
|
||||||
|
cloneDeep(this.state.searchTreeOptions) || [],
|
||||||
|
option,
|
||||||
|
this.props.valueField
|
||||||
|
);
|
||||||
|
this.setState({searchTreeOptions: arr});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
search(inputValue: string) {
|
||||||
|
// 结果为空,直接清空
|
||||||
|
if (!inputValue) {
|
||||||
|
this.clearSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {valueField, onSearch} = this.props;
|
||||||
|
let temOptions: Array<Option> = this.state.treeOptions || [];
|
||||||
|
const cb = (node: Option) => {
|
||||||
|
node.isChecked = false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
deepTree(temOptions, cb);
|
||||||
|
|
||||||
|
const callBack = (node: Option) => onSearch?.(inputValue, node);
|
||||||
|
|
||||||
|
temOptions &&
|
||||||
|
temOptions.forEach(op => {
|
||||||
|
getDeep(op, callBack, [op], valueField);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
searching: true,
|
||||||
|
searchTreeOptions: deepCheckedTreeNode(temOptions)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
clearSearch() {
|
||||||
|
this.setState({
|
||||||
|
searching: false,
|
||||||
|
searchTreeOptions: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTree() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
classnames: cx,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
valueField,
|
||||||
|
itemRender,
|
||||||
|
translate: __
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {treeOptions, searching, searchTreeOptions} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('ResultTreeList', className)}>
|
||||||
|
{
|
||||||
|
Array.isArray(value) && value.length ?
|
||||||
|
(<Tree
|
||||||
|
className={cx('Transfer-tree')}
|
||||||
|
options={!searching ? treeOptions : searchTreeOptions}
|
||||||
|
valueField={valueField}
|
||||||
|
value={[]}
|
||||||
|
onChange={noop}
|
||||||
|
showIcon={false}
|
||||||
|
itemRender={itemRender}
|
||||||
|
removable
|
||||||
|
onDelete={(option: Option) => this.deleteTreeChecked(option)}
|
||||||
|
/>) :
|
||||||
|
(<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
classnames: cx,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
searchable,
|
||||||
|
translate: __,
|
||||||
|
placeholder = __('Transfer.searchKeyword')
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('Selections', className)}>
|
||||||
|
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
|
||||||
|
{searchable ? (
|
||||||
|
<TransferSearch
|
||||||
|
placeholder={placeholder}
|
||||||
|
onSearch={this.search}
|
||||||
|
onCancelSearch={this.clearSearch}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{this.renderTree()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(localeable(BaseResultTreeList));
|
@ -122,7 +122,6 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
onClick={e => e.defaultPrevented || this.toggleOption(option)}
|
|
||||||
className={cx(
|
className={cx(
|
||||||
itemClassName,
|
itemClassName,
|
||||||
option.className,
|
option.className,
|
||||||
@ -131,8 +130,18 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{multiple ? (
|
{multiple ? (
|
||||||
<td className={cx('Table-checkCell')} key="checkbox">
|
<td className={cx('Table-checkCell')}
|
||||||
<Checkbox size="sm" checked={checked} disabled={disabled} />
|
key="checkbox"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleOption(option);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
size="sm"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
) : null}
|
) : null}
|
||||||
{columns.map((column, colIndex) => (
|
{columns.map((column, colIndex) => (
|
||||||
@ -174,4 +183,4 @@ export default themeable(
|
|||||||
value: 'onChange'
|
value: 'onChange'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
@ -1,20 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {intersectionWith, differenceWith, includes, debounce, result} from 'lodash';
|
||||||
|
|
||||||
import {ThemeProps, themeable} from '../theme';
|
import {ThemeProps, themeable} from '../theme';
|
||||||
import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection';
|
import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection';
|
||||||
import {Options, Option} from './Select';
|
import {Options, Option} from './Select';
|
||||||
import {uncontrollable} from 'uncontrollable';
|
import {uncontrollable} from 'uncontrollable';
|
||||||
import ResultList from './ResultList';
|
import ResultList from './ResultList';
|
||||||
import TableSelection from './TableSelection';
|
import TableSelection from './TableSelection';
|
||||||
import TreeSelection from './TreeSelection';
|
|
||||||
import {autobind, flattenTree} from '../utils/helper';
|
import {autobind, flattenTree} from '../utils/helper';
|
||||||
import InputBox from './InputBox';
|
import InputBox from './InputBox';
|
||||||
|
import Checkbox from './Checkbox';
|
||||||
|
import Tree from './Tree';
|
||||||
import {Icon} from './icons';
|
import {Icon} from './icons';
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import AssociatedSelection from './AssociatedSelection';
|
import AssociatedSelection from './AssociatedSelection';
|
||||||
import {LocaleProps, localeable} from '../locale';
|
import {LocaleProps, localeable} from '../locale';
|
||||||
import GroupedSelection from './GroupedSelection';
|
import GroupedSelection from './GroupedSelection';
|
||||||
import ChainedSelection from './ChainedSelection';
|
import ChainedSelection from './ChainedSelection';
|
||||||
import {ItemRenderStates as ResultItemRenderStates} from './ResultList';
|
import {ItemRenderStates as ResultItemRenderStates} from './ResultList';
|
||||||
|
import ResultTableList from './ResultTableList';
|
||||||
|
import ResultTreeList from './ResultTreeList';
|
||||||
|
|
||||||
|
export type SelectMode =
|
||||||
|
'table'
|
||||||
|
| 'group'
|
||||||
|
| 'list'
|
||||||
|
| 'tree'
|
||||||
|
| 'chained'
|
||||||
|
| 'associated';
|
||||||
|
|
||||||
export interface TransferProps
|
export interface TransferProps
|
||||||
extends ThemeProps,
|
extends ThemeProps,
|
||||||
@ -26,7 +38,7 @@ export interface TransferProps
|
|||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
|
||||||
selectTitle?: string;
|
selectTitle?: string;
|
||||||
selectMode?: 'table' | 'group' | 'list' | 'tree' | 'chained' | 'associated';
|
selectMode?: SelectMode;
|
||||||
columns?: Array<{
|
columns?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -72,11 +84,16 @@ export interface TransferProps
|
|||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
|
|
||||||
resultTitle?: string;
|
resultTitle?: string;
|
||||||
|
// 结果提示语
|
||||||
|
resultListModeFollowSelect?: boolean;
|
||||||
|
resultSearchPlaceholder?: string;
|
||||||
optionItemRender?: (option: Option, states: ItemRenderStates) => JSX.Element;
|
optionItemRender?: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||||
resultItemRender?: (
|
resultItemRender?: (
|
||||||
option: Option,
|
option: Option,
|
||||||
states: ResultItemRenderStates
|
states: ResultItemRenderStates
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
|
resultSearchable?: boolean;
|
||||||
|
onResultSearch?: (text: string, item: Option) => boolean;
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
onRef?: (ref: Transfer) => void;
|
onRef?: (ref: Transfer) => void;
|
||||||
onSelectAll?: (options: Options) => void;
|
onSelectAll?: (options: Options) => void;
|
||||||
@ -85,34 +102,76 @@ export interface TransferProps
|
|||||||
export interface TransferState {
|
export interface TransferState {
|
||||||
inputValue: string;
|
inputValue: string;
|
||||||
searchResult: Options | null;
|
searchResult: Options | null;
|
||||||
|
isTreeDeferLoad: boolean;
|
||||||
|
resultSelectMode: 'list' | 'tree' | 'table'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Transfer<
|
export class Transfer<
|
||||||
T extends TransferProps = TransferProps
|
T extends TransferProps = TransferProps
|
||||||
> extends React.Component<T, TransferState> {
|
> extends React.Component<T, TransferState> {
|
||||||
static defaultProps: Pick<TransferProps, 'multiple'> = {
|
|
||||||
multiple: true
|
static defaultProps: Pick<
|
||||||
|
TransferProps,
|
||||||
|
'multiple' | 'resultListModeFollowSelect' | 'selectMode'
|
||||||
|
> = {
|
||||||
|
multiple: true,
|
||||||
|
resultListModeFollowSelect: false,
|
||||||
|
selectMode: 'list'
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state: TransferState = {
|
||||||
inputValue: '',
|
inputValue: '',
|
||||||
searchResult: null
|
searchResult: null,
|
||||||
|
isTreeDeferLoad: false,
|
||||||
|
resultSelectMode: 'list'
|
||||||
};
|
};
|
||||||
|
|
||||||
valueArray: Options;
|
valueArray: Options;
|
||||||
availableOptions: Options;
|
availableOptions: Options;
|
||||||
unmounted = false;
|
unmounted = false;
|
||||||
cancelSearch?: () => void;
|
cancelSearch?: () => void;
|
||||||
|
treeRef: any;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props?.onRef?.(this);
|
this.props?.onRef?.(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props: TransferProps) {
|
||||||
|
// 计算是否是懒加载模式
|
||||||
|
let isTreeDeferLoad: boolean = false;
|
||||||
|
props.selectMode === 'tree' &&
|
||||||
|
props.options.forEach(item => {
|
||||||
|
if (item.defer) {
|
||||||
|
isTreeDeferLoad = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算结果的selectMode
|
||||||
|
let resultSelectMode = 'list';
|
||||||
|
if (props.selectMode === 'tree' && props.resultListModeFollowSelect && !isTreeDeferLoad) {
|
||||||
|
resultSelectMode = 'tree';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.selectMode === 'table' && props.resultListModeFollowSelect) {
|
||||||
|
resultSelectMode = 'table';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isTreeDeferLoad,
|
||||||
|
resultSelectMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.lazySearch.cancel();
|
this.lazySearch.cancel();
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
domRef(ref: any) {
|
||||||
|
this.treeRef = ref;
|
||||||
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
toggleAll() {
|
toggleAll() {
|
||||||
const {options, option2value, onChange, value, onSelectAll} = this.props;
|
const {options, option2value, onChange, value, onSelectAll} = this.props;
|
||||||
@ -142,7 +201,7 @@ export class Transfer<
|
|||||||
|
|
||||||
// 全选,给予动作全选使用
|
// 全选,给予动作全选使用
|
||||||
selectAll() {
|
selectAll() {
|
||||||
const {options, option2value, onChange} = this.props;;
|
const {options, option2value, onChange} = this.props;
|
||||||
const availableOptions = flattenTree(options).filter(
|
const availableOptions = flattenTree(options).filter(
|
||||||
(option, index, list) =>
|
(option, index, list) =>
|
||||||
!option.disabled &&
|
!option.disabled &&
|
||||||
@ -169,22 +228,17 @@ export class Transfer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleSearch(text: string) {
|
handleSearch(inputValue: string) {
|
||||||
// text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮
|
// text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮
|
||||||
if (text) {
|
this.setState({inputValue}, () => {
|
||||||
this.setState(
|
if (inputValue) {
|
||||||
{
|
// 如果有取消搜索,先取消掉。
|
||||||
inputValue: text
|
this.cancelSearch && this.cancelSearch();
|
||||||
},
|
this.lazySearch();
|
||||||
() => {
|
} else {
|
||||||
// 如果有取消搜索,先取消掉。
|
this.handleSeachCancel();
|
||||||
this.cancelSearch && this.cancelSearch();
|
}
|
||||||
this.lazySearch(text);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.handleSeachCancel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
@ -196,34 +250,75 @@ export class Transfer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
lazySearch = debounce(
|
lazySearch = debounce(
|
||||||
(text: string) => {
|
async () => {
|
||||||
(async (text: string) => {
|
const {inputValue} = this.state;
|
||||||
const onSearch = this.props.onSearch!;
|
if (!inputValue) {
|
||||||
let result = await onSearch(
|
return;
|
||||||
text,
|
}
|
||||||
(cancelExecutor: () => void) => (this.cancelSearch = cancelExecutor)
|
const onSearch = this.props.onSearch!;
|
||||||
);
|
let result = await onSearch(
|
||||||
|
inputValue,
|
||||||
|
(cancelExecutor: () => void) => (this.cancelSearch = cancelExecutor)
|
||||||
|
);
|
||||||
|
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(result)) {
|
if (!Array.isArray(result)) {
|
||||||
throw new Error('onSearch 需要返回数组');
|
throw new Error('onSearch 需要返回数组');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
searchResult: result
|
searchResult: result
|
||||||
});
|
});
|
||||||
})(text).catch(e => console.error(e));
|
|
||||||
},
|
},
|
||||||
250,
|
250,
|
||||||
{
|
{trailing: true, leading: false}
|
||||||
trailing: true,
|
|
||||||
leading: false
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getFlattenArr(options: Array<Option>) {
|
||||||
|
return flattenTree(options).filter(
|
||||||
|
(option, index, list) =>
|
||||||
|
!option.disabled &&
|
||||||
|
option.value !== void 0 &&
|
||||||
|
list.indexOf(option) === index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 树搜索处理
|
||||||
|
@autobind
|
||||||
|
handleSearchTreeChange(values: Array<Option>, searchOptions: Array<Option>) {
|
||||||
|
const {onChange, value} = this.props;
|
||||||
|
const searchAvailableOptions = this.getFlattenArr(searchOptions);
|
||||||
|
|
||||||
|
const useArr = intersectionWith(
|
||||||
|
searchAvailableOptions,
|
||||||
|
values,
|
||||||
|
(a, b) => a.value === b.value
|
||||||
|
);
|
||||||
|
const unuseArr = differenceWith(
|
||||||
|
searchAvailableOptions,
|
||||||
|
values,
|
||||||
|
(a, b) => a.value === b.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const newArr: Array<Option> = [];
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.forEach(item => {
|
||||||
|
if (!unuseArr.find(v => v.value === item.value)) {
|
||||||
|
newArr.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
useArr.forEach(item => {
|
||||||
|
if (!newArr.find(v => v.value === item.value)) {
|
||||||
|
newArr.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onChange && onChange(newArr);
|
||||||
|
}
|
||||||
|
|
||||||
renderSelect(
|
renderSelect(
|
||||||
props: TransferProps & {
|
props: TransferProps & {
|
||||||
onToggleAll?: () => void;
|
onToggleAll?: () => void;
|
||||||
@ -238,7 +333,8 @@ export class Transfer<
|
|||||||
disabled,
|
disabled,
|
||||||
options,
|
options,
|
||||||
statistics,
|
statistics,
|
||||||
translate: __
|
translate: __,
|
||||||
|
searchPlaceholder = __('Transfer.searchKeyword')
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (selectRender) {
|
if (selectRender) {
|
||||||
@ -250,6 +346,16 @@ export class Transfer<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let checkedPartial = false;
|
||||||
|
let checkedAll = false;
|
||||||
|
|
||||||
|
checkedAll = this.availableOptions.every(
|
||||||
|
option => this.valueArray.indexOf(option) > -1
|
||||||
|
);
|
||||||
|
checkedPartial = this.availableOptions.some(
|
||||||
|
option => this.valueArray.indexOf(option) > -1
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -259,14 +365,23 @@ export class Transfer<
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
{includes(['list', 'tree'], selectMode) ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={checkedPartial}
|
||||||
|
partial={checkedPartial && !checkedAll}
|
||||||
|
onChange={props.onToggleAll || this.toggleAll}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{__(selectTitle || 'Transfer.available')}
|
{__(selectTitle || 'Transfer.available')}
|
||||||
{statistics !== false ? (
|
{statistics !== false ? (
|
||||||
<span>
|
<span>
|
||||||
({this.valueArray.length}/{this.availableOptions.length})
|
({this.availableOptions.length - this.valueArray.length}/
|
||||||
|
{this.availableOptions.length})
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
{selectMode !== 'table' ? (
|
{includes(['chained', 'associated'], selectMode) ? (
|
||||||
<a
|
<a
|
||||||
onClick={props.onToggleAll || this.toggleAll}
|
onClick={props.onToggleAll || this.toggleAll}
|
||||||
className={cx(
|
className={cx(
|
||||||
@ -284,9 +399,9 @@ export class Transfer<
|
|||||||
<InputBox
|
<InputBox
|
||||||
value={this.state.inputValue}
|
value={this.state.inputValue}
|
||||||
onChange={this.handleSearch}
|
onChange={this.handleSearch}
|
||||||
placeholder={__('Transfer.searchKeyword')}
|
|
||||||
clearable={false}
|
clearable={false}
|
||||||
onKeyDown={this.handleSearchKeyDown}
|
onKeyDown={this.handleSearchKeyDown}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
>
|
>
|
||||||
{this.state.searchResult !== null ? (
|
{this.state.searchResult !== null ? (
|
||||||
<a onClick={this.handleSeachCancel}>
|
<a onClick={this.handleSeachCancel}>
|
||||||
@ -322,7 +437,8 @@ export class Transfer<
|
|||||||
cellRender,
|
cellRender,
|
||||||
multiple
|
multiple
|
||||||
} = props;
|
} = props;
|
||||||
const options = this.state.searchResult || [];
|
const {isTreeDeferLoad, searchResult} = this.state;
|
||||||
|
const options = searchResult ?? [];
|
||||||
const mode = searchResultMode || selectMode;
|
const mode = searchResultMode || selectMode;
|
||||||
const resultColumns = searchResultColumns || columns;
|
const resultColumns = searchResultColumns || columns;
|
||||||
|
|
||||||
@ -341,16 +457,22 @@ export class Transfer<
|
|||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
/>
|
/>
|
||||||
) : mode === 'tree' ? (
|
) : mode === 'tree' ? (
|
||||||
<TreeSelection
|
<Tree
|
||||||
|
onRef={this.domRef}
|
||||||
placeholder={noResultsText}
|
placeholder={noResultsText}
|
||||||
className={cx('Transfer-selection')}
|
className={cx('Transfer-selection')}
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={(value: Array<any>) =>
|
||||||
option2value={option2value}
|
this.handleSearchTreeChange(value, options)
|
||||||
|
}
|
||||||
|
joinValues={false}
|
||||||
|
showIcon={false}
|
||||||
|
multiple={true}
|
||||||
|
cascade={true}
|
||||||
|
onlyChildren={!isTreeDeferLoad}
|
||||||
itemRender={optionItemRender}
|
itemRender={optionItemRender}
|
||||||
multiple={multiple}
|
|
||||||
/>
|
/>
|
||||||
) : mode === 'chained' ? (
|
) : mode === 'chained' ? (
|
||||||
<ChainedSelection
|
<ChainedSelection
|
||||||
@ -396,7 +518,8 @@ export class Transfer<
|
|||||||
cellRender,
|
cellRender,
|
||||||
leftDefaultValue,
|
leftDefaultValue,
|
||||||
optionItemRender,
|
optionItemRender,
|
||||||
multiple
|
multiple,
|
||||||
|
noResultsText
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return selectMode === 'table' ? (
|
return selectMode === 'table' ? (
|
||||||
@ -413,16 +536,20 @@ export class Transfer<
|
|||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
/>
|
/>
|
||||||
) : selectMode === 'tree' ? (
|
) : selectMode === 'tree' ? (
|
||||||
<TreeSelection
|
<Tree
|
||||||
|
onRef={this.domRef}
|
||||||
|
placeholder={noResultsText}
|
||||||
className={cx('Transfer-selection')}
|
className={cx('Transfer-selection')}
|
||||||
options={options || []}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
onChange={onChange!}
|
||||||
onChange={onChange}
|
onlyChildren={!this.state.isTreeDeferLoad}
|
||||||
option2value={option2value}
|
|
||||||
onDeferLoad={onDeferLoad}
|
|
||||||
itemRender={optionItemRender}
|
itemRender={optionItemRender}
|
||||||
multiple={multiple}
|
onDeferLoad={onDeferLoad}
|
||||||
|
joinValues={false}
|
||||||
|
showIcon={false}
|
||||||
|
multiple={true}
|
||||||
|
cascade={true}
|
||||||
/>
|
/>
|
||||||
) : selectMode === 'chained' ? (
|
) : selectMode === 'chained' ? (
|
||||||
<ChainedSelection
|
<ChainedSelection
|
||||||
@ -468,21 +595,90 @@ export class Transfer<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderResult() {
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
options,
|
||||||
|
disabled,
|
||||||
|
option2value,
|
||||||
|
classnames: cx,
|
||||||
|
cellRender,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
resultItemRender,
|
||||||
|
resultSearchable,
|
||||||
|
resultSearchPlaceholder,
|
||||||
|
onResultSearch,
|
||||||
|
sortable,
|
||||||
|
translate: __
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {resultSelectMode, isTreeDeferLoad} = this.state;
|
||||||
|
const searchable = !isTreeDeferLoad && resultSearchable;
|
||||||
|
|
||||||
|
const placeholder = resultSearchPlaceholder || __('Transfer.selectFromLeft');
|
||||||
|
|
||||||
|
return resultSelectMode === 'table'
|
||||||
|
? (
|
||||||
|
<ResultTableList
|
||||||
|
classnames={cx}
|
||||||
|
columns={columns!}
|
||||||
|
options={options || []}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
option2value={option2value}
|
||||||
|
cellRender={cellRender}
|
||||||
|
onChange={onChange}
|
||||||
|
multiple={false}
|
||||||
|
searchable={searchable}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onSearch={onResultSearch}
|
||||||
|
/>)
|
||||||
|
: resultSelectMode === 'tree'
|
||||||
|
? (
|
||||||
|
<ResultTreeList
|
||||||
|
classnames={cx}
|
||||||
|
options={options}
|
||||||
|
valueField={'value'}
|
||||||
|
value={value || []}
|
||||||
|
onChange={onChange!}
|
||||||
|
itemRender={resultItemRender}
|
||||||
|
searchable={searchable}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onSearch={onResultSearch}
|
||||||
|
/>
|
||||||
|
): (
|
||||||
|
<ResultList
|
||||||
|
className={cx('Transfer-value')}
|
||||||
|
sortable={sortable}
|
||||||
|
disabled={disabled}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
itemRender={resultItemRender}
|
||||||
|
columns={columns!}
|
||||||
|
options={options || []}
|
||||||
|
option2value={option2value}
|
||||||
|
cellRender={cellRender}
|
||||||
|
searchable={searchable}
|
||||||
|
onSearch={onResultSearch}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
inline,
|
inline,
|
||||||
classnames: cx,
|
classnames: cx,
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
onChange,
|
|
||||||
resultTitle,
|
resultTitle,
|
||||||
sortable,
|
|
||||||
options,
|
options,
|
||||||
option2value,
|
option2value,
|
||||||
disabled,
|
disabled,
|
||||||
statistics,
|
statistics,
|
||||||
showArrow,
|
showArrow,
|
||||||
resultItemRender,
|
resultListModeFollowSelect,
|
||||||
|
selectMode = 'list',
|
||||||
translate: __
|
translate: __
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -494,6 +690,8 @@ export class Transfer<
|
|||||||
list.indexOf(option) === index
|
list.indexOf(option) === index
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tableType = resultListModeFollowSelect && selectMode === 'table';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx('Transfer', className, inline ? 'Transfer--inline' : '')}
|
className={cx('Transfer', className, inline ? 'Transfer--inline' : '')}
|
||||||
@ -509,13 +707,16 @@ export class Transfer<
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={cx('Transfer-result')}>
|
<div className={cx('Transfer-result')}>
|
||||||
<div className={cx('Transfer-title')}>
|
<div
|
||||||
|
className={cx(
|
||||||
|
'Transfer-title',
|
||||||
|
tableType ? 'Transfer-table-title' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{__(resultTitle || 'Transfer.selectd')}
|
{__(resultTitle || 'Transfer.selectd')}
|
||||||
{statistics !== false ? (
|
{statistics !== false ? (
|
||||||
<span>
|
<span>({this.valueArray.length})</span>
|
||||||
({this.valueArray.length}/{this.availableOptions.length})
|
|
||||||
</span>
|
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
@ -528,15 +729,7 @@ export class Transfer<
|
|||||||
{__('clear')}
|
{__('clear')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ResultList
|
{this.renderResult()}
|
||||||
className={cx('Transfer-value')}
|
|
||||||
sortable={sortable}
|
|
||||||
disabled={disabled}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
placeholder={__('Transfer.selectFromLeft')}
|
|
||||||
itemRender={resultItemRender}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
129
src/components/TransferSearch.tsx
Normal file
129
src/components/TransferSearch.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Tranasfer搜索
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import {debounce} from 'lodash';
|
||||||
|
|
||||||
|
import {ThemeProps, themeable} from '../theme';
|
||||||
|
import {Icon} from './icons';
|
||||||
|
import {autobind} from '../utils/helper';
|
||||||
|
import {LocaleProps, localeable} from '../locale';
|
||||||
|
import InputBox from './InputBox';
|
||||||
|
|
||||||
|
export interface TransferSearchProps
|
||||||
|
extends ThemeProps,
|
||||||
|
LocaleProps {
|
||||||
|
className?: string;
|
||||||
|
placeholder: string;
|
||||||
|
onSearch: Function;
|
||||||
|
onCancelSearch: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemRenderStates {
|
||||||
|
index: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: any, name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransferSearchState {
|
||||||
|
inputValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TransferSearch extends React.Component<
|
||||||
|
TransferSearchProps,
|
||||||
|
TransferSearchState
|
||||||
|
> {
|
||||||
|
|
||||||
|
static itemRender(option: any) {
|
||||||
|
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps: Pick<TransferSearchProps, 'placeholder'> = {
|
||||||
|
placeholder: 'placeholder.selectData'
|
||||||
|
};
|
||||||
|
|
||||||
|
state: TransferSearchState = {
|
||||||
|
inputValue: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelSearch?: () => void;
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.lazySearch.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleSearch(inputValue: string) {
|
||||||
|
// text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮
|
||||||
|
this.setState({inputValue}, () => {
|
||||||
|
if (inputValue) {
|
||||||
|
// 如果有取消搜索,先取消掉。
|
||||||
|
this.cancelSearch && this.cancelSearch();
|
||||||
|
this.lazySearch();
|
||||||
|
} else {
|
||||||
|
this.handleSeachCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lazySearch = debounce(
|
||||||
|
() => {
|
||||||
|
const {inputValue} = this.state;
|
||||||
|
// 防止由于防抖导致空值问题
|
||||||
|
if (!inputValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {onSearch} = this.props;
|
||||||
|
onSearch!(inputValue);
|
||||||
|
},
|
||||||
|
250,
|
||||||
|
{trailing: true, leading: false}
|
||||||
|
);
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleSearchKeyDown(e: React.KeyboardEvent<any>) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleSeachCancel() {
|
||||||
|
this.props.onCancelSearch?.();
|
||||||
|
this.setState({
|
||||||
|
inputValue: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
classnames: cx,
|
||||||
|
translate: __,
|
||||||
|
placeholder = __('Transfer.searchKeyword')
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {inputValue} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('Transfer-search')}>
|
||||||
|
<InputBox
|
||||||
|
value={inputValue}
|
||||||
|
onChange={this.handleSearch}
|
||||||
|
clearable={false}
|
||||||
|
onKeyDown={this.handleSearchKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
>
|
||||||
|
{!!inputValue ? (
|
||||||
|
<a onClick={this.handleSeachCancel}>
|
||||||
|
<Icon icon="close" className="icon" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Icon icon="search" className="icon" />
|
||||||
|
)}
|
||||||
|
</InputBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(localeable(TransferSearch));
|
@ -23,6 +23,7 @@ import {Icon, getIcon} from './icons';
|
|||||||
import Checkbox from './Checkbox';
|
import Checkbox from './Checkbox';
|
||||||
import {LocaleProps, localeable} from '../locale';
|
import {LocaleProps, localeable} from '../locale';
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
|
import {ItemRenderStates} from './Selection';
|
||||||
|
|
||||||
interface IDropIndicator {
|
interface IDropIndicator {
|
||||||
left: number;
|
left: number;
|
||||||
@ -41,7 +42,7 @@ export interface IDropInfo {
|
|||||||
interface TreeSelectorProps extends ThemeProps, LocaleProps {
|
interface TreeSelectorProps extends ThemeProps, LocaleProps {
|
||||||
highlightTxt?: string;
|
highlightTxt?: string;
|
||||||
|
|
||||||
onRef: any;
|
onRef?: any;
|
||||||
|
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
// 是否默认都展开
|
// 是否默认都展开
|
||||||
@ -124,6 +125,7 @@ interface TreeSelectorProps extends ThemeProps, LocaleProps {
|
|||||||
onExpandTree?: (nodePathArr: any[]) => void;
|
onExpandTree?: (nodePathArr: any[]) => void;
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
onMove?: (dropInfo: IDropInfo) => void;
|
onMove?: (dropInfo: IDropInfo) => void;
|
||||||
|
itemRender?: (option: Option, states: ItemRenderStates) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TreeSelectorState {
|
interface TreeSelectorState {
|
||||||
@ -806,6 +808,7 @@ export class TreeSelector extends React.Component<
|
|||||||
editTip,
|
editTip,
|
||||||
removeTip,
|
removeTip,
|
||||||
translate: __,
|
translate: __,
|
||||||
|
itemRender,
|
||||||
draggable
|
draggable
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
@ -967,7 +970,15 @@ export class TreeSelector extends React.Component<
|
|||||||
>
|
>
|
||||||
{highlightTxt
|
{highlightTxt
|
||||||
? highlight(`${item[labelField]}`, highlightTxt)
|
? highlight(`${item[labelField]}`, highlightTxt)
|
||||||
: `${item[labelField]}`}
|
: itemRender ? (
|
||||||
|
itemRender(item, {
|
||||||
|
index: key,
|
||||||
|
multiple: multiple,
|
||||||
|
checked: checked,
|
||||||
|
onChange: () => this.handleCheck(item, !selfChecked),
|
||||||
|
disabled: disabled || item.disabled
|
||||||
|
}))
|
||||||
|
: `${item[labelField]}`}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{!nodeDisabled &&
|
{!nodeDisabled &&
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import find from 'lodash/find';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OptionsControlProps,
|
OptionsControlProps,
|
||||||
OptionsControl,
|
OptionsControl,
|
||||||
FormOptionsControl
|
FormOptionsControl
|
||||||
} from './Options';
|
} from './Options';
|
||||||
import React from 'react';
|
|
||||||
import Transfer, {Transfer as BaseTransfer} from '../../components/Transfer';
|
import Transfer, {Transfer as BaseTransfer} from '../../components/Transfer';
|
||||||
import type {Option} from './Options';
|
import type {Option} from './Options';
|
||||||
import {
|
import {
|
||||||
@ -16,9 +18,7 @@ import {
|
|||||||
getTree,
|
getTree,
|
||||||
spliceTree
|
spliceTree
|
||||||
} from '../../utils/helper';
|
} from '../../utils/helper';
|
||||||
import {Api} from '../../types';
|
|
||||||
import Spinner from '../../components/Spinner';
|
import Spinner from '../../components/Spinner';
|
||||||
import find from 'lodash/find';
|
|
||||||
import {optionValueCompare} from '../../components/Select';
|
import {optionValueCompare} from '../../components/Select';
|
||||||
import {resolveVariable} from '../../utils/tpl-builtin';
|
import {resolveVariable} from '../../utils/tpl-builtin';
|
||||||
import {SchemaApi, SchemaObject} from '../../Schema';
|
import {SchemaApi, SchemaObject} from '../../Schema';
|
||||||
@ -51,6 +51,11 @@ export interface TransferControlSchema extends FormOptionsControl {
|
|||||||
*/
|
*/
|
||||||
selectMode?: 'table' | 'list' | 'tree' | 'chained' | 'associated';
|
selectMode?: 'table' | 'list' | 'tree' | 'chained' | 'associated';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果面板是否追踪显示
|
||||||
|
*/
|
||||||
|
resultListModeFollowSelect?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当 selectMode 为 associated 时用来定义左侧的选项
|
* 当 selectMode 为 associated 时用来定义左侧的选项
|
||||||
*/
|
*/
|
||||||
@ -86,6 +91,11 @@ export interface TransferControlSchema extends FormOptionsControl {
|
|||||||
*/
|
*/
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果(右则)列表的检索功能,当设置为true时,可以通过输入检索模糊匹配检索内容
|
||||||
|
*/
|
||||||
|
resultSearchable?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 搜索 API
|
* 搜索 API
|
||||||
*/
|
*/
|
||||||
@ -110,6 +120,16 @@ export interface TransferControlSchema extends FormOptionsControl {
|
|||||||
* 用来丰富值的展示
|
* 用来丰富值的展示
|
||||||
*/
|
*/
|
||||||
valueTpl?: SchemaObject;
|
valueTpl?: SchemaObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 左侧列表搜索框提示
|
||||||
|
*/
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右侧列表搜索框提示
|
||||||
|
*/
|
||||||
|
resultSearchPlaceholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseTransferProps
|
export interface BaseTransferProps
|
||||||
@ -271,6 +291,13 @@ export class BaseTransferRenderer<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleResultSearch(term: string, item: Option) {
|
||||||
|
const {valueField} = this.props;
|
||||||
|
const regexp = string2regExp(term);
|
||||||
|
return regexp.test(item[(valueField as string) || 'value']);
|
||||||
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
optionItemRender(option: Option, states: ItemRenderStates) {
|
optionItemRender(option: Option, states: ItemRenderStates) {
|
||||||
const {menuTpl, render, data} = this.props;
|
const {menuTpl, render, data} = this.props;
|
||||||
@ -351,7 +378,7 @@ export class BaseTransferRenderer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
let {
|
||||||
className,
|
className,
|
||||||
classnames: cx,
|
classnames: cx,
|
||||||
selectedOptions,
|
selectedOptions,
|
||||||
@ -370,7 +397,10 @@ export class BaseTransferRenderer<
|
|||||||
selectTitle,
|
selectTitle,
|
||||||
resultTitle,
|
resultTitle,
|
||||||
menuTpl,
|
menuTpl,
|
||||||
resultItemRender
|
searchPlaceholder,
|
||||||
|
resultListModeFollowSelect = false,
|
||||||
|
resultSearchPlaceholder,
|
||||||
|
resultSearchable = false
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// 目前 LeftOptions 没有接口可以动态加载
|
// 目前 LeftOptions 没有接口可以动态加载
|
||||||
@ -412,6 +442,11 @@ export class BaseTransferRenderer<
|
|||||||
cellRender={this.renderCell}
|
cellRender={this.renderCell}
|
||||||
selectTitle={selectTitle}
|
selectTitle={selectTitle}
|
||||||
resultTitle={resultTitle}
|
resultTitle={resultTitle}
|
||||||
|
resultListModeFollowSelect={resultListModeFollowSelect}
|
||||||
|
onResultSearch={this.handleResultSearch}
|
||||||
|
searchPlaceholder={searchPlaceholder}
|
||||||
|
resultSearchable={resultSearchable}
|
||||||
|
resultSearchPlaceholder={resultSearchPlaceholder}
|
||||||
optionItemRender={this.optionItemRender}
|
optionItemRender={this.optionItemRender}
|
||||||
resultItemRender={this.resultItemRender}
|
resultItemRender={this.resultItemRender}
|
||||||
onSelectAll={this.onSelectAll}
|
onSelectAll={this.onSelectAll}
|
||||||
|
Loading…
Reference in New Issue
Block a user