Merge pull request #629 from 2betop/master
Transfer 中的 chained、tree 模式,选项支持延时加载
This commit is contained in:
commit
a0b2370d09
|
@ -386,6 +386,46 @@ export default {
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: '延时加载',
|
||||
type: 'transfer',
|
||||
name: 'transfer7',
|
||||
selectMode: 'tree',
|
||||
deferApi: '/api/mock2/form/deferOptions?label=${label}',
|
||||
options: [
|
||||
{
|
||||
label: '法师',
|
||||
children: [
|
||||
{
|
||||
label: '诸葛亮',
|
||||
value: 'zhugeliang'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '战士',
|
||||
defer: true
|
||||
},
|
||||
{
|
||||
label: '打野',
|
||||
children: [
|
||||
{
|
||||
label: '李白',
|
||||
value: 'libai'
|
||||
},
|
||||
{
|
||||
label: '韩信',
|
||||
value: 'hanxin'
|
||||
},
|
||||
{
|
||||
label: '云中君',
|
||||
value: 'yunzhongjun'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
module.exports = function (req, res) {
|
||||
let repeat = 2 + Math.round(Math.random() * 5);
|
||||
const options = [];
|
||||
|
||||
while (repeat--) {
|
||||
const value = Math.round(Math.random() * 1000000);
|
||||
const label = value + '';
|
||||
|
||||
options.push({
|
||||
label: label,
|
||||
value: value,
|
||||
defer: Math.random() > 0.7
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 0,
|
||||
msg: '',
|
||||
data: {
|
||||
options: options
|
||||
}
|
||||
});
|
||||
};
|
|
@ -404,6 +404,7 @@
|
|||
&-sublist {
|
||||
position: relative;
|
||||
margin: 0 0 0 px2rem(35px);
|
||||
display: none;
|
||||
|
||||
&:before {
|
||||
width: 1px;
|
||||
|
@ -420,8 +421,8 @@
|
|||
&-item {
|
||||
position: relative;
|
||||
|
||||
&.is-collapsed > .#{$ns}TreeCheckboxes-sublist {
|
||||
display: none;
|
||||
&.is-expanded > .#{$ns}TreeCheckboxes-sublist {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -442,6 +443,7 @@
|
|||
|
||||
&-itemInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $Form-input-height;
|
||||
line-height: $Form-input-lineHeight;
|
||||
font-size: $Form-input-fontSize;
|
||||
|
@ -452,6 +454,7 @@
|
|||
|
||||
> .#{$ns}Checkbox {
|
||||
margin-right: 0;
|
||||
margin-left: $gap-sm;
|
||||
}
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
@ -481,7 +484,7 @@
|
|||
|
||||
&-col {
|
||||
flex-grow: 1;
|
||||
min-width: 120px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&-col:not(:last-child) {
|
||||
|
|
|
@ -6,9 +6,10 @@ import Checkbox from './Checkbox';
|
|||
import {Option} from './Select';
|
||||
import {getTreeDepth} from '../utils/helper';
|
||||
import times from 'lodash/times';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
export interface ChainedCheckboxesState {
|
||||
selected: Array<Option>;
|
||||
selected: Array<string>;
|
||||
}
|
||||
|
||||
export class ChainedCheckboxes extends Checkboxes<
|
||||
|
@ -20,17 +21,22 @@ export class ChainedCheckboxes extends Checkboxes<
|
|||
selected: []
|
||||
};
|
||||
|
||||
selectOption(option: Option, depth: number) {
|
||||
selectOption(option: Option, depth: number, id: string) {
|
||||
const {onDeferLoad} = this.props;
|
||||
|
||||
const selected = this.state.selected.concat();
|
||||
selected.splice(depth, selected.length - depth);
|
||||
selected.push(option);
|
||||
selected.push(id);
|
||||
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
selected
|
||||
});
|
||||
},
|
||||
option.defer && onDeferLoad ? () => onDeferLoad(option) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
renderOption(option: Option, index: number, depth: number) {
|
||||
renderOption(option: Option, index: number, depth: number, id: string) {
|
||||
const {
|
||||
labelClassName,
|
||||
disabled,
|
||||
|
@ -40,7 +46,7 @@ export class ChainedCheckboxes extends Checkboxes<
|
|||
} = this.props;
|
||||
const valueArray = this.valueArray;
|
||||
|
||||
if (Array.isArray(option.children)) {
|
||||
if (Array.isArray(option.children) || option.defer) {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
|
@ -49,13 +55,15 @@ export class ChainedCheckboxes extends Checkboxes<
|
|||
itemClassName,
|
||||
option.className,
|
||||
disabled || option.disabled ? 'is-disabled' : '',
|
||||
~this.state.selected.indexOf(option) ? 'is-active' : ''
|
||||
~this.state.selected.indexOf(id) ? 'is-active' : ''
|
||||
)}
|
||||
onClick={() => this.selectOption(option, depth)}
|
||||
onClick={() => this.selectOption(option, depth, id)}
|
||||
>
|
||||
<div className={cx('ChainedCheckboxes-itemLabel')}>
|
||||
{itemRender(option)}
|
||||
</div>
|
||||
|
||||
{option.defer && option.loading ? <Spinner size="sm" show /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -101,26 +109,29 @@ export class ChainedCheckboxes extends Checkboxes<
|
|||
let body: Array<React.ReactNode> = [];
|
||||
|
||||
if (Array.isArray(options) && options.length) {
|
||||
const selected: Array<Option | null> = this.state.selected.concat();
|
||||
const depth = getTreeDepth(options);
|
||||
times(depth - selected.length, () => selected.push(null));
|
||||
const selected: Array<string | null> = this.state.selected.concat();
|
||||
const depth = Math.min(getTreeDepth(options), 3);
|
||||
times(Math.max(depth - selected.length, 1), () => selected.push(null));
|
||||
|
||||
selected.reduce(
|
||||
(
|
||||
{
|
||||
body,
|
||||
options,
|
||||
subTitle
|
||||
subTitle,
|
||||
indexes
|
||||
}: {
|
||||
body: Array<React.ReactNode>;
|
||||
options: Array<Option> | null;
|
||||
subTitle?: string;
|
||||
indexes: Array<number>;
|
||||
},
|
||||
selected,
|
||||
depth
|
||||
) => {
|
||||
let nextOptions: Array<Option> = [];
|
||||
let nextSubTitle: string = '';
|
||||
let nextIndexes = indexes;
|
||||
|
||||
body.push(
|
||||
<div key={depth} className={cx('ChainedCheckboxes-col')}>
|
||||
|
@ -131,12 +142,15 @@ export class ChainedCheckboxes extends Checkboxes<
|
|||
) : null}
|
||||
{Array.isArray(options) && options.length
|
||||
? options.map((option, index) => {
|
||||
if (option === selected) {
|
||||
const id = indexes.concat(index).join('-');
|
||||
|
||||
if (id === selected) {
|
||||
nextSubTitle = option.subTitle;
|
||||
nextOptions = option.children!;
|
||||
nextIndexes = indexes.concat(index);
|
||||
}
|
||||
|
||||
return this.renderOption(option, index, depth);
|
||||
return this.renderOption(option, index, depth, id);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
|
@ -145,12 +159,14 @@ export class ChainedCheckboxes extends Checkboxes<
|
|||
return {
|
||||
options: nextOptions,
|
||||
subTitle: nextSubTitle,
|
||||
indexes: nextIndexes,
|
||||
body: body
|
||||
};
|
||||
},
|
||||
{
|
||||
options,
|
||||
body
|
||||
body,
|
||||
indexes: []
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface CheckboxesProps extends ThemeProps {
|
|||
placeholder?: string;
|
||||
value?: Array<any>;
|
||||
onChange?: (value: Array<Option>) => void;
|
||||
onDeferLoad?: (option: Option) => void;
|
||||
inline?: boolean;
|
||||
labelClassName?: string;
|
||||
option2value?: (option: Option) => any;
|
||||
|
|
|
@ -18,6 +18,7 @@ export class ListCheckboxes extends Checkboxes {
|
|||
} = this.props;
|
||||
const valueArray = this.valueArray;
|
||||
|
||||
// todo 支持 option.defer 延时加载
|
||||
if (Array.isArray(option.children)) {
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -22,15 +22,45 @@ import {findDOMNode} from 'react-dom';
|
|||
import {ClassNamesFn, themeable} from '../theme';
|
||||
import Checkbox from './Checkbox';
|
||||
import Input from './Input';
|
||||
import {Api} from '../types';
|
||||
|
||||
export interface Option {
|
||||
label?: string;
|
||||
|
||||
// 可以用来给 Option 标记个范围,让数据展示更清晰。
|
||||
// 这个只有在数值展示的时候显示。
|
||||
scopeLabel?: string;
|
||||
|
||||
// 请保证数值唯一,多个选项值一致会认为是同一个选项。
|
||||
value?: any;
|
||||
|
||||
// 是否禁用
|
||||
disabled?: boolean;
|
||||
|
||||
// 支持嵌套
|
||||
children?: Options;
|
||||
|
||||
// 是否可见
|
||||
visible?: boolean;
|
||||
|
||||
// 最好不要用!因为有 visible 就够了。
|
||||
hidden?: boolean;
|
||||
|
||||
// 描述
|
||||
description?: string;
|
||||
|
||||
// 标记后数据延时加载
|
||||
defer?: boolean;
|
||||
|
||||
// 如果设置了,优先级更高,不设置走 source 接口加载。
|
||||
deferApi?: Api;
|
||||
|
||||
// 标记正在加载。只有 defer 为 true 时有意义。内部字段不可以外部设置
|
||||
loading?: boolean;
|
||||
|
||||
// 只有设置了 defer 才有意义,内部字段不可以外部设置
|
||||
loaded?: boolean;
|
||||
|
||||
[propName: string]: any;
|
||||
}
|
||||
export interface Options extends Array<Option> {}
|
||||
|
|
|
@ -85,7 +85,8 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
value,
|
||||
onChange,
|
||||
onSearch: searchable,
|
||||
option2value
|
||||
option2value,
|
||||
onDeferLoad
|
||||
} = this.props;
|
||||
|
||||
if (!Array.isArray(options) || !options.length) {
|
||||
|
@ -130,6 +131,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
) : option.selectMode === 'tree' ? (
|
||||
<TreeCheckboxes
|
||||
|
@ -138,6 +140,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
) : option.selectMode === 'chained' ? (
|
||||
<ChainedCheckboxes
|
||||
|
@ -146,6 +149,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
) : (
|
||||
<ListCheckboxes
|
||||
|
@ -154,6 +158,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
|
|
|
@ -299,7 +299,8 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
value,
|
||||
onChange,
|
||||
option2value,
|
||||
classnames: cx
|
||||
classnames: cx,
|
||||
onDeferLoad
|
||||
} = this.props;
|
||||
|
||||
return selectMode === 'table' ? (
|
||||
|
@ -310,6 +311,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
) : selectMode === 'tree' ? (
|
||||
<TreeCheckboxes
|
||||
|
@ -318,6 +320,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
) : selectMode === 'chained' ? (
|
||||
<ChainedCheckboxes
|
||||
|
@ -326,6 +329,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
) : (
|
||||
<ListCheckboxes
|
||||
|
@ -334,6 +338,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -368,7 +373,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
>
|
||||
<div className={cx('Transfer-select')}>{this.renderSelect()}</div>
|
||||
<div className={cx('Transfer-mid')}>
|
||||
{showArrow ? (
|
||||
{showArrow /*todo 需要改成确认模式,即:点了按钮才到右边 */ ? (
|
||||
<div className={cx('Transfer-arrow')}>
|
||||
<Icon icon="right-arrow" />
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,15 @@ import React from 'react';
|
|||
import uncontrollable from 'uncontrollable';
|
||||
import Checkbox from './Checkbox';
|
||||
import {Option} from './Select';
|
||||
import {autobind} from '../utils/helper';
|
||||
import {autobind, eachTree, everyTree} from '../utils/helper';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
export interface TreeCheckboxesProps extends CheckboxesProps {}
|
||||
export interface TreeCheckboxesProps extends CheckboxesProps {
|
||||
expand?: 'all' | 'first' | 'root' | 'none';
|
||||
}
|
||||
|
||||
export interface TreeCheckboxesState {
|
||||
collapsed: Array<Option>;
|
||||
expanded: Array<string>;
|
||||
}
|
||||
|
||||
export class TreeCheckboxes extends Checkboxes<
|
||||
|
@ -18,11 +21,60 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
> {
|
||||
valueArray: Array<Option>;
|
||||
state: TreeCheckboxesState = {
|
||||
collapsed: []
|
||||
expanded: []
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
...Checkboxes.defaultProps,
|
||||
expand: 'first' as 'first'
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.syncExpanded();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TreeCheckboxesProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (
|
||||
!this.state.expanded.length &&
|
||||
(props.expand !== prevProps.expand || props.options !== prevProps.options)
|
||||
) {
|
||||
this.syncExpanded();
|
||||
}
|
||||
}
|
||||
|
||||
syncExpanded() {
|
||||
const options = this.props.options;
|
||||
const mode = this.props.expand;
|
||||
const expanded: Array<string> = [];
|
||||
|
||||
if (!Array.isArray(options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'first' || mode === 'root') {
|
||||
options.every((option, index) => {
|
||||
if (Array.isArray(option.children)) {
|
||||
expanded.push(`${index}`);
|
||||
return mode === 'root';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else if (mode === 'all') {
|
||||
everyTree(options, (option, index, level, paths, indexes) => {
|
||||
if (Array.isArray(option.children)) {
|
||||
expanded.push(`${indexes.concat(index).join('-')}`);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({expanded});
|
||||
}
|
||||
|
||||
toggleOption(option: Option) {
|
||||
const {value, onChange, option2value, options} = this.props;
|
||||
const {value, onChange, option2value, options, onDeferLoad} = this.props;
|
||||
|
||||
if (option.disabled) {
|
||||
return;
|
||||
|
@ -74,22 +126,26 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
onChange && onChange(newValue);
|
||||
}
|
||||
|
||||
toggleCollapsed(option: Option) {
|
||||
const collapsed = this.state.collapsed.concat();
|
||||
const idx = collapsed.indexOf(option);
|
||||
toggleCollapsed(option: Option, index: string) {
|
||||
const onDeferLoad = this.props.onDeferLoad;
|
||||
const expanded = this.state.expanded.concat();
|
||||
const idx = expanded.indexOf(index);
|
||||
|
||||
if (~idx) {
|
||||
collapsed.splice(idx, 1);
|
||||
expanded.splice(idx, 1);
|
||||
} else {
|
||||
collapsed.push(option);
|
||||
expanded.push(index);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
collapsed
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
expanded: expanded
|
||||
},
|
||||
option.defer && onDeferLoad ? () => onDeferLoad(option) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
renderItem(option: Option, index: number) {
|
||||
renderItem(option: Option, index: number, indexes: Array<number> = []) {
|
||||
const {
|
||||
labelClassName,
|
||||
disabled,
|
||||
|
@ -97,6 +153,7 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
itemClassName,
|
||||
itemRender
|
||||
} = this.props;
|
||||
const id = indexes.join('-');
|
||||
const valueArray = this.valueArray;
|
||||
let partial = false;
|
||||
let checked = false;
|
||||
|
@ -129,15 +186,17 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
checked = !!~valueArray.indexOf(option);
|
||||
}
|
||||
|
||||
const collapsed = !!~this.state.collapsed.indexOf(option);
|
||||
const expaned = !!~this.state.expanded.indexOf(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cx(
|
||||
'TreeCheckboxes-item',
|
||||
disabled || option.disabled ? 'is-disabled' : '',
|
||||
collapsed ? 'is-collapsed' : ''
|
||||
disabled || option.disabled || (option.defer && option.loading)
|
||||
? 'is-disabled'
|
||||
: '',
|
||||
expaned ? 'is-expanded' : ''
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
@ -148,13 +207,13 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
)}
|
||||
onClick={() => this.toggleOption(option)}
|
||||
>
|
||||
{hasChildren ? (
|
||||
{hasChildren || option.defer ? (
|
||||
<a
|
||||
onClick={(e: React.MouseEvent<any>) => {
|
||||
e.stopPropagation();
|
||||
this.toggleCollapsed(option);
|
||||
this.toggleCollapsed(option, id);
|
||||
}}
|
||||
className={cx('Table-expandBtn', !collapsed ? 'is-active' : '')}
|
||||
className={cx('Table-expandBtn', expaned ? 'is-active' : '')}
|
||||
>
|
||||
<i />
|
||||
</a>
|
||||
|
@ -164,6 +223,9 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
{itemRender(option)}
|
||||
</div>
|
||||
|
||||
{option.defer && option.loading ? <Spinner show size="sm" /> : null}
|
||||
|
||||
{!option.defer || option.loaded ? (
|
||||
<Checkbox
|
||||
size="sm"
|
||||
checked={checked}
|
||||
|
@ -172,10 +234,13 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
labelClassName={labelClassName}
|
||||
description={option.description}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{Array.isArray(option.children) && option.children.length ? (
|
||||
{hasChildren ? (
|
||||
<div className={cx('TreeCheckboxes-sublist')}>
|
||||
{option.children.map((option, key) => this.renderItem(option, key))}
|
||||
{option.children!.map((option, key) =>
|
||||
this.renderItem(option, key, indexes.concat(key))
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -196,7 +261,7 @@ export class TreeCheckboxes extends Checkboxes<
|
|||
let body: Array<React.ReactNode> = [];
|
||||
|
||||
if (Array.isArray(options) && options.length) {
|
||||
body = options.map((option, key) => this.renderItem(option, key));
|
||||
body = options.map((option, key) => this.renderItem(option, key, [key]));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -7,11 +7,7 @@ import Downshift, {StateChangeOptions} from 'downshift';
|
|||
import {autobind} from '../../utils/helper';
|
||||
import {ICONS} from './IconPickerIcons';
|
||||
import {FormItem, FormControlProps} from './Item';
|
||||
|
||||
export interface Option {
|
||||
label?: string;
|
||||
value?: any;
|
||||
}
|
||||
import {Option} from '../../components/Select';
|
||||
|
||||
export interface IconPickerProps extends FormControlProps {
|
||||
placeholder?: string;
|
||||
|
|
|
@ -62,6 +62,7 @@ export interface OptionsControlProps extends FormControlProps, OptionProps {
|
|||
setOptions: (value: Array<any>, skipNormalize?: boolean) => void;
|
||||
setLoading: (value: boolean) => void;
|
||||
reloadOptions: () => void;
|
||||
deferLoad: (option: Option) => void;
|
||||
creatable?: boolean;
|
||||
onAdd?: (
|
||||
idx?: number | Array<number>,
|
||||
|
@ -79,6 +80,7 @@ export interface OptionsControlProps extends FormControlProps, OptionProps {
|
|||
// 自己接收的属性。
|
||||
export interface OptionsProps extends FormControlProps, OptionProps {
|
||||
source?: Api;
|
||||
deferApi?: Api;
|
||||
creatable?: boolean;
|
||||
addApi?: Api;
|
||||
addControls?: Array<any>;
|
||||
|
@ -449,6 +451,27 @@ export function registerOptionsControl(config: OptionsConfig) {
|
|||
return formItem.loadOptions(source, data, undefined, false, onChange);
|
||||
}
|
||||
|
||||
@autobind
|
||||
deferLoad(option: Option) {
|
||||
const {deferApi, source, env, formItem, data} = this.props;
|
||||
|
||||
if (option.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = option.deferApi || deferApi || source;
|
||||
|
||||
if (!api) {
|
||||
env.notify(
|
||||
'error',
|
||||
'请在选项中设置 `deferApi` 或者表单项中设置 `deferApi`,用来加载子选项。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
formItem?.deferLoadOptions(option, api, createObject(data, option));
|
||||
}
|
||||
|
||||
@autobind
|
||||
async initOptions(data: any) {
|
||||
await this.reload();
|
||||
|
@ -777,6 +800,7 @@ export function registerOptionsControl(config: OptionsConfig) {
|
|||
setOptions={this.setOptions}
|
||||
syncOptions={this.syncOptions}
|
||||
reloadOptions={this.reload}
|
||||
deferLoad={this.deferLoad}
|
||||
creatable={
|
||||
creatable || (creatable !== false && isEffectiveApi(addApi))
|
||||
}
|
||||
|
|
|
@ -149,7 +149,8 @@ export class TransferRenderer<
|
|||
columns,
|
||||
loading,
|
||||
searchable,
|
||||
searchResultMode
|
||||
searchResultMode,
|
||||
deferLoad
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -165,6 +166,7 @@ export class TransferRenderer<
|
|||
searchResultMode={searchResultMode}
|
||||
columns={columns}
|
||||
onSearch={searchable ? this.handleSearch : undefined}
|
||||
onDeferLoad={deferLoad}
|
||||
/>
|
||||
|
||||
<Spinner overlay key="info" show={loading} />
|
||||
|
|
|
@ -17,7 +17,9 @@ import {
|
|||
isObject,
|
||||
createObject,
|
||||
isObjectShallowModified,
|
||||
findTree
|
||||
findTree,
|
||||
findTreeIndex,
|
||||
spliceTree
|
||||
} from '../utils/helper';
|
||||
import {flattenTree} from '../utils/helper';
|
||||
import {IRendererStore} from '.';
|
||||
|
@ -170,13 +172,13 @@ export const FormItemStore = types
|
|||
unMatched = {
|
||||
[self.valueField || 'value']: item,
|
||||
[self.labelField || 'label']: item,
|
||||
'__unmatched': true
|
||||
__unmatched: true
|
||||
};
|
||||
} else if (unMatched && self.extractValue) {
|
||||
unMatched = {
|
||||
[self.valueField || 'value']: item,
|
||||
[self.labelField || 'label']: 'UnKnown',
|
||||
'__unmatched': true
|
||||
__unmatched: true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -349,22 +351,14 @@ export const FormItemStore = types
|
|||
}
|
||||
|
||||
let loadCancel: Function | null = null;
|
||||
const loadOptions: (
|
||||
const fetchOptions: (
|
||||
api: Api,
|
||||
data?: object,
|
||||
options?: fetchOptions,
|
||||
clearValue?: boolean,
|
||||
onChange?: (value: any) => void
|
||||
config?: fetchOptions
|
||||
) => Promise<Payload | null> = flow(function* getInitData(
|
||||
api: string,
|
||||
data: object,
|
||||
options?: fetchOptions,
|
||||
clearValue?: any,
|
||||
onChange?: (
|
||||
value: any,
|
||||
submitOnChange: boolean,
|
||||
changeImmediately: boolean
|
||||
) => void
|
||||
config?: fetchOptions
|
||||
) {
|
||||
try {
|
||||
if (loadCancel) {
|
||||
|
@ -381,15 +375,15 @@ export const FormItemStore = types
|
|||
{
|
||||
autoAppend: false,
|
||||
cancelExecutor: (executor: Function) => (loadCancel = executor),
|
||||
...options
|
||||
...config
|
||||
}
|
||||
);
|
||||
loadCancel = null;
|
||||
let result: any = null;
|
||||
|
||||
if (!json.ok) {
|
||||
setError(
|
||||
`加载选项失败,原因:${json.msg ||
|
||||
(options && options.errorMessage)}`
|
||||
`加载选项失败,原因:${json.msg || (config && config.errorMessage)}`
|
||||
);
|
||||
(getRoot(self) as IRendererStore).notify(
|
||||
'error',
|
||||
|
@ -402,29 +396,11 @@ export const FormItemStore = types
|
|||
: undefined
|
||||
);
|
||||
} else {
|
||||
clearError();
|
||||
self.validated = false; // 拉完数据应该需要再校验一下
|
||||
|
||||
let options: Array<IOption> =
|
||||
json.data?.options ||
|
||||
json.data.items ||
|
||||
json.data.rows ||
|
||||
json.data ||
|
||||
[];
|
||||
options = normalizeOptions(options as any);
|
||||
setOptions(options);
|
||||
|
||||
if (json.data && typeof (json.data as any).value !== 'undefined') {
|
||||
onChange && onChange((json.data as any).value, false, true);
|
||||
} else if (clearValue) {
|
||||
self.selectedOptions.some((item: any) => item.__unmatched) &&
|
||||
onChange &&
|
||||
onChange('', false, true);
|
||||
}
|
||||
result = json;
|
||||
}
|
||||
|
||||
self.loading = false;
|
||||
return json;
|
||||
return result;
|
||||
} catch (e) {
|
||||
const root = getRoot(self) as IRendererStore;
|
||||
if (root.storeType !== 'RendererStore') {
|
||||
|
@ -441,10 +417,103 @@ export const FormItemStore = types
|
|||
console.error(e.stack);
|
||||
getRoot(self) &&
|
||||
(getRoot(self) as IRendererStore).notify('error', e.message);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
} as any);
|
||||
|
||||
const loadOptions: (
|
||||
api: Api,
|
||||
data?: object,
|
||||
config?: fetchOptions,
|
||||
clearValue?: boolean,
|
||||
onChange?: (value: any) => void
|
||||
) => Promise<Payload | null> = flow(function* getInitData(
|
||||
api: string,
|
||||
data: object,
|
||||
config?: fetchOptions,
|
||||
clearValue?: any,
|
||||
onChange?: (
|
||||
value: any,
|
||||
submitOnChange: boolean,
|
||||
changeImmediately: boolean
|
||||
) => void
|
||||
) {
|
||||
let json = yield fetchOptions(api, data, config);
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearError();
|
||||
self.validated = false; // 拉完数据应该需要再校验一下
|
||||
|
||||
let options: Array<IOption> =
|
||||
json.data?.options ||
|
||||
json.data.items ||
|
||||
json.data.rows ||
|
||||
json.data ||
|
||||
[];
|
||||
|
||||
options = normalizeOptions(options as any);
|
||||
setOptions(options);
|
||||
|
||||
if (json.data && typeof (json.data as any).value !== 'undefined') {
|
||||
onChange && onChange((json.data as any).value, false, true);
|
||||
} else if (clearValue) {
|
||||
self.selectedOptions.some((item: any) => item.__unmatched) &&
|
||||
onChange &&
|
||||
onChange('', false, true);
|
||||
}
|
||||
|
||||
return json;
|
||||
});
|
||||
|
||||
const deferLoadOptions: (
|
||||
option: any,
|
||||
api: Api,
|
||||
data?: object,
|
||||
config?: fetchOptions
|
||||
) => Promise<Payload | null> = flow(function* getInitData(
|
||||
option: any,
|
||||
api: string,
|
||||
data: object,
|
||||
config?: fetchOptions
|
||||
) {
|
||||
const indexes = findTreeIndex(self.options, item => item === option);
|
||||
if (!indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(
|
||||
spliceTree(self.options, indexes, 1, {
|
||||
...option,
|
||||
loading: true
|
||||
})
|
||||
);
|
||||
|
||||
let json = yield fetchOptions(api, data, config);
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
|
||||
let options: Array<IOption> =
|
||||
json.data?.options ||
|
||||
json.data.items ||
|
||||
json.data.rows ||
|
||||
json.data ||
|
||||
[];
|
||||
|
||||
setOptions(
|
||||
spliceTree(self.options, indexes, 1, {
|
||||
...option,
|
||||
loading: false,
|
||||
loaded: true,
|
||||
children: options
|
||||
})
|
||||
);
|
||||
|
||||
return json;
|
||||
});
|
||||
|
||||
function syncOptions(originOptions?: Array<any>) {
|
||||
if (!self.options.length && typeof self.value === 'undefined') {
|
||||
self.selectedOptions = [];
|
||||
|
@ -527,7 +596,7 @@ export const FormItemStore = types
|
|||
unMatched = {
|
||||
[self.valueField || 'value']: item,
|
||||
[self.labelField || 'label']: item,
|
||||
'__unmatched': true
|
||||
__unmatched: true
|
||||
};
|
||||
|
||||
const orgin: any =
|
||||
|
@ -545,7 +614,7 @@ export const FormItemStore = types
|
|||
unMatched = {
|
||||
[self.valueField || 'value']: item,
|
||||
[self.labelField || 'label']: 'UnKnown',
|
||||
'__unmatched': true
|
||||
__unmatched: true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -631,6 +700,7 @@ export const FormItemStore = types
|
|||
clearError,
|
||||
setOptions,
|
||||
loadOptions,
|
||||
deferLoadOptions,
|
||||
syncOptions,
|
||||
setLoading,
|
||||
setSubStore,
|
||||
|
|
|
@ -892,15 +892,28 @@ export function filterTree<T extends TreeItem>(
|
|||
*/
|
||||
export function everyTree<T extends TreeItem>(
|
||||
tree: Array<T>,
|
||||
iterator: (item: T, key: number, level: number, paths: Array<T>) => boolean,
|
||||
iterator: (
|
||||
item: T,
|
||||
key: number,
|
||||
level: number,
|
||||
paths: Array<T>,
|
||||
indexes: Array<number>
|
||||
) => boolean,
|
||||
level: number = 1,
|
||||
paths: Array<T> = []
|
||||
paths: Array<T> = [],
|
||||
indexes: Array<number> = []
|
||||
): boolean {
|
||||
return tree.every((item, index) => {
|
||||
const value: any = iterator(item, index, level, paths);
|
||||
const value: any = iterator(item, index, level, paths, indexes);
|
||||
|
||||
if (value && item.children && item.children.splice) {
|
||||
return everyTree(item.children, iterator, level + 1, paths.concat(item));
|
||||
return everyTree(
|
||||
item.children,
|
||||
iterator,
|
||||
level + 1,
|
||||
paths.concat(item),
|
||||
indexes.concat(index)
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
@ -975,6 +988,7 @@ export function spliceTree<T extends TreeItem>(
|
|||
if (typeof idx === 'number') {
|
||||
list.splice(idx, deleteCount, ...items);
|
||||
} else if (Array.isArray(idx) && idx.length) {
|
||||
idx = idx.concat();
|
||||
const lastIdx = idx.pop()!;
|
||||
let host = idx.reduce((list: Array<T>, idx) => {
|
||||
const child = {
|
||||
|
|
Loading…
Reference in New Issue