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