diff --git a/docs/zh-CN/components/form/transfer.md b/docs/zh-CN/components/form/transfer.md index a499906a0..c2c05e5f3 100644 --- a/docs/zh-CN/components/form/transfer.md +++ b/docs/zh-CN/components/form/transfer.md @@ -929,5 +929,6 @@ icon: | --------- | -------------------------------------- | --------------------------------------------------------------------------------------- | | clear | - | 清空 | | reset | - | 将值重置为`resetValue`,若没有配置`resetValue`,则清空 | +| clearSearch | `left: boolean` 左侧搜索、`right:boolean`右侧搜索 | 默认清除所有搜索态,可以单独配置`left`、`right`为`true`来清空对应左侧或者右侧面板(`3.4.0`及以上版本支持) | | selectAll | - | 全选 | | setValue | `value: string` \| `string[]` 更新的值 | 更新数据,开启`multiple`支持设置多项,开启`joinValues`时,多值用`,`分隔,否则多值用数组 | diff --git a/examples/components/EventAction/cmpt-event-action/TransferEvent.jsx b/examples/components/EventAction/cmpt-event-action/TransferEvent.jsx index ea4fbf976..8b5424201 100644 --- a/examples/components/EventAction/cmpt-event-action/TransferEvent.jsx +++ b/examples/components/EventAction/cmpt-event-action/TransferEvent.jsx @@ -52,6 +52,29 @@ export default { } } }, + { + name: 'transferEvent2', + id: 'transferEvent2', + type: 'action', + label: '清空搜索', + level: 'primary', + className: 'mr-3 mb-3', + debugger: true, + onEvent: { + click: { + actions: [ + { + actionType: 'clearSearch', + componentId: 'transfer-receiver', + args: { + left: true, + right: true + } + } + ] + } + } + }, { name: 'transferEvent2', id: 'transferEvent2', @@ -84,6 +107,8 @@ export default { id: 'transfer-receiver', type: 'transfer', name: 'transfer', + searchable: true, + resultSearchable: true, debugger: true, resetValue: 'c', source: '/api/mock2/form/getTreeOptions', diff --git a/packages/amis-core/src/types.ts b/packages/amis-core/src/types.ts index 159200ca4..37874785c 100644 --- a/packages/amis-core/src/types.ts +++ b/packages/amis-core/src/types.ts @@ -339,7 +339,8 @@ export interface ActionObject extends ButtonObject { | 'collapse' | 'step-submit' | 'selectAll' - | 'changeTabKey'; + | 'changeTabKey' + | 'clearSearch'; api?: BaseApiObject | string; asyncApi?: BaseApiObject | string; payload?: any; diff --git a/packages/amis-ui/src/components/ResultList.tsx b/packages/amis-ui/src/components/ResultList.tsx index 77de17f99..a331cca1a 100644 --- a/packages/amis-ui/src/components/ResultList.tsx +++ b/packages/amis-ui/src/components/ResultList.tsx @@ -81,6 +81,7 @@ export class ResultList extends React.Component< id = guid(); sortable?: Sortable; unmounted = false; + searchRef?: any; componentDidMount() { this.props.sortable && this.initSortable(); @@ -99,6 +100,14 @@ export class ResultList extends React.Component< this.unmounted = true; } + @autobind + domSearchRef(ref: any) { + while (ref && ref.getWrappedInstance) { + ref = ref.getWrappedInstance(); + } + this.searchRef = ref; + } + initSortable() { const ns = this.props.classPrefix; const dom = findDOMNode(this) as HTMLElement; @@ -181,6 +190,14 @@ export class ResultList extends React.Component< this.setState({searchResult: null}); } + @autobind + clearInput() { + if (this.props.searchable) { + this.searchRef?.clearInput?.(); + } + this.clearSearch(); + } + // 删除项 @autobind handleCloseItem(e: React.MouseEvent, option: Option) { @@ -359,6 +376,7 @@ export class ResultList extends React.Component< {title ?
{title}
: null} {searchable ? ( {title} : null} {searchable ? ( {title} : null} {searchable ? ( void; treeRef: any; + resultRef: any; componentDidMount() { this.props?.onRef?.(this); @@ -196,9 +197,20 @@ export class Transfer< @autobind domRef(ref: any) { + while (ref && ref.getWrappedInstance) { + ref = ref.getWrappedInstance(); + } this.treeRef = ref; } + @autobind + domResultRef(ref: any) { + while (ref && ref.getWrappedInstance) { + ref = ref.getWrappedInstance(); + } + this.resultRef = ref; + } + @autobind toggleAll() { const { @@ -207,7 +219,8 @@ export class Transfer< onChange, value, onSelectAll, - valueField = 'value' + valueField = 'value', + selectMode } = this.props; let valueArray = BaseSelection.value2array( value, @@ -217,6 +230,11 @@ export class Transfer< ); const availableOptions = this.availableOptions; + if (selectMode === 'tree') { + this.treeRef?.handleToggle(); + return; + } + // availableOptions 中选项是否都被选中了 // to do intersectionWith 需要优化,大数据会卡死 const isAvailableOptionsAllSelected = @@ -247,7 +265,17 @@ export class Transfer< // 全选,给予动作全选使用 selectAll() { - const {options, option2value, onChange, valueField = 'value'} = this.props; + const { + options, + option2value, + onChange, + valueField = 'value', + selectMode + } = this.props; + if (selectMode === 'tree') { + this.treeRef?.handleToggle(true); + return; + } const availableOptions = flattenTree(options).filter( (option, index, list) => !option.disabled && @@ -260,6 +288,20 @@ export class Transfer< onChange?.(newValue); } + // 清空搜索 + clearSearch(target?: {left?: boolean; right?: boolean}) { + if (!target) { + this.handleSeachCancel(); + this.resultRef?.clearInput(); + } + if (target?.left) { + this.handleSeachCancel(); + } + if (target?.right) { + this.resultRef?.clearInput(); + } + } + @autobind clearAll() { const {onChange} = this.props; @@ -369,6 +411,22 @@ export class Transfer< onChange && onChange(newArr); } + @autobind + optionItemRender(option: Option, states: ItemRenderStates) { + const {optionItemRender, labelField = 'label'} = this.props; + return optionItemRender + ? optionItemRender(option, states) + : BaseSelection.itemRender(option, {labelField, ...states}); + } + + @autobind + resultItemRender(option: Option, states: ItemRenderStates) { + const {resultItemRender} = this.props; + return resultItemRender + ? resultItemRender(option, states) + : ResultList.itemRender(option, states); + } + renderSelect( props: TransferProps & { onToggleAll?: () => void; @@ -509,11 +567,15 @@ export class Transfer< checkAllLabel, onlyChildren } = props; - const {isTreeDeferLoad, searchResult} = this.state; + const {isTreeDeferLoad, searchResult, inputValue} = this.state; const options = searchResult ?? []; const mode = searchResultMode || selectMode; const resultColumns = searchResultColumns || columns; + const treeItemRender = + !searchResult || optionItemRender ? this.optionItemRender : undefined; + const highlightTxt = searchResult ? inputValue : undefined; + return mode === 'table' ? ( ) : mode === 'tree' ? ( ) : selectMode === 'tree' ? ( + this.isItemChecked(option) + ); + this.handleCheckAll(availableOptions, checkedAll); + return; + } + this.handleCheckAll(availableOptions, bool); + } + renderCheckAll() { const { multiple, diff --git a/packages/amis/__tests__/renderers/Form/__snapshots__/transfer.test.tsx.snap b/packages/amis/__tests__/renderers/Form/__snapshots__/transfer.test.tsx.snap index 1819c39f3..74e3c115c 100644 --- a/packages/amis/__tests__/renderers/Form/__snapshots__/transfer.test.tsx.snap +++ b/packages/amis/__tests__/renderers/Form/__snapshots__/transfer.test.tsx.snap @@ -1775,11 +1775,7 @@ exports[`Renderer:transfer follow left mode 1`] = ` class="cxd-Tree-itemText" title="法师" > - - 法师 - + 法师
- - 诸葛亮 - + 诸葛亮
- - 战士 - + 战士
- - 曹操 - + 曹操
- - 钟无艳 - + 钟无艳
- - 打野 - + 打野
- - 李白 - + 李白
- - 韩信 - + 韩信
- - 云中君 - + 云中君
- - 法师 - + 法师
- - 诸葛亮 - + 诸葛亮
- - 战士 - + 战士
- - 曹操 - + 曹操
- - 钟无艳 - + 钟无艳
- - 打野 - + 打野
- - 李白 - + 李白
- - 韩信 - + 韩信
- - 云中君 - + 云中君
- - 法师 - + 法师
- - 诸葛亮 - + 诸葛亮
- - 战士 - + 战士
- - 曹操 - + 曹操
- - 钟无艳 - + 钟无艳
- - 打野 - + 打野
- - 李白 - + 李白
- - 韩信 - + 韩信
- - 云中君 - + 云中君
- - 诸葛亮 - + 诸葛亮
- - 曹操 - + 曹操
- - 钟无艳 - + 钟无艳
{ const caocao = container.querySelector('span[title=曹操]'); expect(caocao).toBeNull(); +}); + +test('Renderer:transfer tree onlyChildren true', async () => { + const onSubmit = jest.fn(); + const schema = { + "type": "form", + "api": "/api/mock2/form/saveForm", + "debug": true, + "body": [ + { + "label": "默认", + "type": "transfer", + "name": "transfer", + "value": "libai", + "selectMode": "tree", + "searchable": true, + "onlyChildren": true, + "options": [ + { + "label": "法师", + "children": [ + { + "label": "诸葛亮", + "value": "zhugeliang" + } + ] + }, + { + "label": "战士", + "value": "战士", + "children": [ + { + "label": "曹操", + "disabled": true, + "value": "caocao" + }, + { + "label": "钟无艳", + "value": "zhongwuyan" + } + ] + }, + { + "label": "打野", + "children": [ + { + "label": "李白", + "value": "libai" + }, + { + "label": "韩信", + "value": "hanxin" + }, + { + "label": "云中君", + "value": "yunzhongjun" + } + ] + } + ] + } + ] + }; + + const {getByText, container} = render( + amisRender(schema, {onSubmit}, makeEnv({})) + ); + + const checkbox = container.querySelector('.cxd-Checkbox'); + expect(checkbox).not.toBeNull(); + + checkbox && fireEvent.click(checkbox); + + fireEvent.click(getByText('提交')); + + await wait(100); + expect(onSubmit).toBeCalledTimes(1); + + expect(onSubmit.mock.calls[0][0]).toEqual({ + transfer: "zhugeliang,zhongwuyan,libai,hanxin,yunzhongjun" + }); }); \ No newline at end of file diff --git a/packages/amis/src/renderers/Form/Transfer.tsx b/packages/amis/src/renderers/Form/Transfer.tsx index 97547b264..7f28542e0 100644 --- a/packages/amis/src/renderers/Form/Transfer.tsx +++ b/packages/amis/src/renderers/Form/Transfer.tsx @@ -428,29 +428,21 @@ export class BaseTransferRenderer< @autobind optionItemRender(option: Option, states: ItemRenderStates) { - const {menuTpl, render, data, labelField = 'label'} = this.props; + const {menuTpl, render, data} = this.props; - if (menuTpl) { - return render(`item/${states.index}`, menuTpl, { - data: createObject(createObject(data, states), option) - }); - } - - return BaseSelection.itemRender(option, {labelField, ...states}); + return render(`item/${states.index}`, menuTpl, { + data: createObject(createObject(data, states), option) + }); } @autobind resultItemRender(option: Option, states: ItemRenderStates) { const {valueTpl, render, data} = this.props; - if (valueTpl) { - return render(`value/${states.index}`, valueTpl, { - onChange: states.onChange, - data: createObject(createObject(data, states), option) - }); - } - - return ResultList.itemRender(option, states); + return render(`value/${states.index}`, valueTpl, { + onChange: states.onChange, + data: createObject(createObject(data, states), option) + }); } @autobind @@ -508,6 +500,10 @@ export class BaseTransferRenderer< case 'selectAll': this.tranferRef?.selectAll(); break; + case 'clearSearch': { + this.tranferRef?.clearSearch(data); + break; + } } } @@ -533,6 +529,7 @@ export class BaseTransferRenderer< selectTitle, resultTitle, menuTpl, + valueTpl, searchPlaceholder, resultListModeFollowSelect = false, resultSearchPlaceholder, @@ -597,8 +594,8 @@ export class BaseTransferRenderer< statistics={statistics} labelField={labelField} valueField={valueField} - optionItemRender={this.optionItemRender} - resultItemRender={this.resultItemRender} + optionItemRender={menuTpl ? this.optionItemRender : undefined} + resultItemRender={valueTpl ? this.resultItemRender : undefined} onSelectAll={this.onSelectAll} onRef={this.getRef} virtualThreshold={virtualThreshold}