人员选择组件支持部门、角色、岗位选择 (#4285)

* 人员选择组件

* 修复ts报错

Co-authored-by: zhangxulong <zhangxulong@baidu.com>
This commit is contained in:
龙少 2022-05-10 17:54:56 +08:00 committed by GitHub
parent f4e29ad18f
commit dd5200ce18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1932 additions and 2 deletions

View File

@ -0,0 +1,33 @@
import React = require('react');
import {render, cleanup} from '@testing-library/react';
import '../../../src/themes/default';
import {render as amisRender} from '../../../src/index';
import {makeEnv} from '../../helper';
test('Renderer:usersselect', async () => {
const {container} = render(
amisRender(
{
type: 'form',
api: '/api/mock2/form/saveForm',
controls: [
{
type: 'users-select',
name: 'usersselect',
label: '人员选择',
source: '/amis/api/mock2/form/departUser',
deferApi: '/amis/api/mock2/form/departUser?ref=${ref}&dep=${value}',
searchApi: '',
isRef: true,
isDep: false
}
],
title: 'The form',
actions: []
},
{},
makeEnv()
)
);
expect(container).toMatchSnapshot();
});

View File

@ -1649,6 +1649,12 @@
--Timeline--warning-bg: var(--warning);
--Timeline--danger-bg: var(--danger);
--UserSelect--post-bg: #528eff;
--UserSelect--department-bg: #ffab52;
--UserSelect--role-bg: #0bc286;
--UserSelect--border-color: #f7f7f9;
--UserSelect--content-bg: #f5f7f8;
--UserSelect--bread-color: #2468f2;
// tag
--Tag-content-fontSize: var(--fontSizeSm);
--Tag-height: #{px2rem(24px)};

View File

@ -0,0 +1,422 @@
.#{$ns}UserSelect {
position: relative;
&-popup {
height: 100vh;
}
&-selectPopup {
width: 100vw;
height: 100vh;
z-index: var($zindex-top) + 1;
}
&-searchBox {
height: px2rem(52px);
margin: 0 px2rem(16px);
flex: none;
display: flex;
align-items: center;
}
&-search {
background: var(--UserSelect--content-bg);
flex: 1;
}
&-searchResult {
width: 100vw;
flex: 1;
overflow-x: hidden;
overflow-y: auto;
background: var(--UserSelect--content-bg);
}
&-wrap {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
text-align: left;
}
&-navbar {
position: relative;
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--white);
padding-left: px2rem(12px);
padding-right: px2rem(16px);
flex: none;
&-title {
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
line-height: 44px;
text-align: center;
}
&-btnEdit {
color: var(--UserSelect--bread-color);
font-size: px2rem(16px);
}
}
&-breadcrumb {
width: 100%;
line-height: px2rem(44px);
padding-left: px2rem(16px);
flex: none;
white-space: nowrap;
overflow-x: auto;
&-item {
cursor: pointer;
color: var(--UserSelect--bread-color);
&:last-child {
color: inherit;
}
}
&-separator {
margin: 0 px2rem(8px);
transform: rotate(-90deg) scale(0.5);
}
}
&-contentBox {
width: 100vw;
overflow: hidden;
position: relative;
flex: 1;
background: var(--UserSelect--content-bg);
}
&-scroll {
height: 100%;
display: flex;
position: absolute;
left: 0;
top: 0;
transition: left var(--animation-duration) ease-in-out;
}
&-memberList-box {
width: 100vw;
margin-top: px2rem(16px);
}
&-memberList,
&-selection {
height: 100%;
list-style: none;
margin: 0 px2rem(16px);
padding: px2rem(8px) px2rem(16px);
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
background: var(--white);
li {
display: flex;
justify-content: space-between;
align-items: center;
height: px2rem(42px);
line-height: px2rem(42px);
cursor: pointer;
user-select: none;
padding: 0 px2rem(16px);
border-bottom: px2rem(1px) solid var(--UserSelect--border-color);
> span {
flex: 1;
}
}
}
&-selection {
margin: 0;
padding: 0;
li {
padding: 0;
}
}
&-memberName {
font-size: 14px;
flex: 2 !important;
text-align: left;
user-select: none;
display: flex;
align-items: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&-label {
flex: 1;
}
&-icon-box {
width: px2rem(28px);
height: px2rem(28px);
border-radius: 100%;
overflow: hidden;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
> svg {
position: static;
}
&.role {
background-color: var(--UserSelect--role-bg);
}
&.department {
background-color: var(--UserSelect--department-bg);
}
&.post {
background-color: var(--UserSelect--post-bg);
}
}
&-userPic {
width: px2rem(28px);
height: px2rem(28px);
border-radius: 100%;
overflow: hidden;
}
&-userPic-box {
width: px2rem(28px);
margin-right: px2rem(10px);
flex: none !important;
display: flex;
align-items: center;
}
&-text-userPic {
display: flex;
justify-content: center;
align-items: center;
background-color: var(--UserSelect--post-bg);
text-align: center;
line-height: px2rem(28px);
width: px2rem(28px);
height: px2rem(28px);
border-radius: 50%;
overflow: hidden;
color: var(--white);
margin: 0;
}
&-more {
text-align: right;
svg {
width: 10px;
height: 10px;
transform: rotateZ(-90deg);
}
}
&-resultBox {
width: 100vw;
height: px2rem(48px);
display: flex;
align-items: center;
padding: 0 px2rem(16px);
flex: none;
overflow: hidden;
box-sizing: border-box;
}
&-selectNum {
flex: none;
}
&-selectList {
width: 100%;
flex: 1;
padding: 0;
overflow-x: scroll;
white-space: nowrap;
cursor: pointer;
display: flex;
align-items: center;
&-item {
list-style: none;
margin-right: px2rem(8px);
display: flex;
align-items: center;
background: var(--UserSelect--border-color);
border-radius: 4px;
padding: 0 px2rem(8px);
padding-right: 0;
&-closeBox {
height: 100%;
margin-left: px2rem(4px);
padding: 0 px2rem(6px);
display: flex;
align-items: center;
.icon {
font-size: 8px;
}
}
}
}
&-selectSort-box {
margin-left: px2rem(10px);
padding: px2rem(4px) px2rem(10px);
}
&-noRecord {
width: 100vw;
height: 100%;
margin: 0 px2rem(16px);
margin-top: px2rem(16px);
display: flex;
align-items: center;
justify-content: center;
background: var(--white);
padding: px2rem(100px) 0;
box-sizing: border-box;
}
&-selectList-pop {
margin: 0;
padding: 0 10px;
li {
height: 40px;
display: flex;
align-items: center;
}
}
&-btnSure {
flex: none;
}
&-del {
text-align: right;
flex: none !important;
padding: 0 10px;
}
&-dragBar {
flex: none;
margin-right: 10px;
}
&-checkContent {
li {
> label {
flex: 1 !important;
}
}
}
&-selectBody {
width: 100%;
background: var(--UserSelect--content-bg);
display: flex;
flex-direction: column;
}
&-searchLoadingBox {
flex: 1;
width: 100vw;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
&-spinnerBox {
width: 100vw;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
&-selectList-box {
margin-top: px2rem(16px);
background: var(--white);
border-radius: 4px;
padding: 0 px2rem(16px);
margin: px2rem(16px);
flex: 1;
overflow-y: auto;
.#{$ns}UserSelect-noRecord {
width: auto;
}
}
&-select-head {
height: px2rem(48px);
display: flex;
align-items: center;
justify-content: space-between;
&-text {
font-size: px2rem(16px);
color: #151b26;
}
&-btnClear {
color: var(--UserSelect--bread-color);
font-size: px2rem(16px);
cursor: pointer;
}
}
}
.#{$ns}UserTabSelect {
&-popup {
width: 100vw;
height: 100vh;
}
&-wrap {
display: flex;
flex-direction: column;
}
&-tabs {
flex: 1;
display: flex;
flex-direction: column;
> div {
&:first-child {
flex: none;
}
&:last-child {
flex: 1;
> div {
height: 100%;
> div {
height: 100%;
}
}
}
}
}
}

View File

@ -121,6 +121,7 @@
@import '../components/cascader';
@import '../components/form/icon-picker';
@import '../components/form/form';
@import '../components/form/user-select';
@import '../components/anchor-nav';
@import '../components/markdown';
@import '../components/link';

View File

@ -117,6 +117,7 @@ import {UUIDControlSchema} from './renderers/Form/UUID';
import {FormControlSchema} from './renderers/Form/Control';
import {TransferPickerControlSchema} from './renderers/Form/TransferPicker';
import {TabsTransferPickerControlSchema} from './renderers/Form/TabsTransferPicker';
import {UserSelectControlSchema} from './renderers/Form/UserSelect';
import {JSONSchemaEditorControlSchema} from './renderers/Form/JSONSchemaEditor';
import {TableSchemaV2} from './renderers/Table-v2';
@ -334,6 +335,7 @@ export type SchemaType =
| 'table-view'
| 'portlet'
| 'grid-nav'
| 'users-select'
| 'tag'
// 原生 input 类型
@ -465,7 +467,8 @@ export type SchemaObject =
| TransferPickerControlSchema
| TabsTransferPickerControlSchema
| TreeControlSchema
| TreeSelectControlSchema;
| TreeSelectControlSchema
| UserSelectControlSchema;
export type SchemaCollection =
| SchemaObject

View File

@ -0,0 +1,850 @@
/**
* @file
* @author fex
*/
import React from 'react';
import {themeable, ThemeProps} from '../theme';
import {LocaleProps, localeable} from '../locale';
import {ResultBox} from '../components';
import {Option} from '../renderers/Form/Options';
import Sortable from 'sortablejs';
import PopUp from '../components/PopUp';
import InputBox from '../components/InputBox';
import {Icon} from '../components/icons';
import debounce from 'lodash/debounce';
import {autobind, findTree} from '../utils/helper';
import Checkbox from '../components/Checkbox';
import {optionValueCompare, value2array} from './Select';
import Spinner from '../components/Spinner';
import flatten from 'lodash/flatten';
import {findDOMNode} from 'react-dom';
import {Api, PlainObject} from '../types';
import {Payload} from 'echarts';
export interface UserSelectProps extends ThemeProps, LocaleProps {
showNav?: boolean;
navTitle?: string;
options: Array<any>;
value?: Array<Option> | Option | string;
selection?: Array<Option>;
valueField?: string;
labelField?: string;
multi?: boolean;
multiple?: boolean;
isDep?: boolean;
isRef?: boolean;
searchable?: boolean;
// 选项卡模式开关
showResultBox?: boolean;
placeholder?: string;
searchPlaceholder?: string;
controlled?: boolean;
fetcher?: (
api: Api,
data?: any,
options?: PlainObject | undefined
) => Promise<Payload>;
onSearch?: (
term: string,
cancelExecutor: Function
) => Promise<any[]> | undefined;
deferLoad: (
data?: PlainObject,
isRef?: boolean,
param?: PlainObject
) => Promise<Option[]>;
onChange: (value: Array<Option> | Option, isReplace?: boolean) => void;
}
export interface UserSelectState {
isOpened: boolean;
isSearch: boolean;
isSelectOpened: boolean;
inputValue: string;
breadList: Array<any>;
options: Array<Option>;
tempSelection: Array<Option>;
selection: Array<Option>;
searchList: Array<Option>;
searchLoading: boolean;
isEdit: boolean;
}
export class UserSelect extends React.Component<
UserSelectProps,
UserSelectState
> {
cancelSearch?: Function;
sortable?: Sortable;
unmounted = false;
constructor(props: UserSelectProps) {
super(props);
this.state = {
isOpened: false,
isSelectOpened: false,
inputValue: '',
options: this.props.options || [],
breadList: [],
searchList: [],
tempSelection: [],
selection: props.selection || [],
isSearch: false,
searchLoading: false,
isEdit: false
};
}
static defaultProps = {
showResultBox: true,
labelField: 'label',
valueField: 'value'
};
componentDidMount() {}
componentDidUpdate(prevProps: UserSelectProps) {
let {options, value} = this.props;
if (prevProps.options !== options) {
if (
options &&
options.length === 1 &&
options[0].leftOptions &&
Array.isArray(options[0].children)
) {
let leftOptions = options[0].leftOptions as Option[];
this.setState({
options: leftOptions
});
} else {
// 部门选择
this.setState({
options: options
});
}
}
if (
JSON.stringify(value) !== JSON.stringify(prevProps.value) ||
JSON.stringify(options) !== JSON.stringify(prevProps.options)
) {
const selection: Array<Option> = value2array(value, this.props);
this.setState({
selection
});
}
}
componentWillUnmount() {
this.unmounted = true;
}
@autobind
onClose() {
this.setState({
isOpened: false,
isSearch: false,
inputValue: '',
searchList: [],
searchLoading: false
});
}
@autobind
handleSearch(text: string) {
if (text) {
this.setState(
{
isSearch: true,
searchLoading: true,
inputValue: text
},
() => {
// 如果有取消搜索,先取消掉。
this.cancelSearch && this.cancelSearch();
this.lazySearch(text);
}
);
} else {
this.handleSeachCancel();
}
}
@autobind
handleSeachCancel() {
this.setState({
isSearch: false,
searchLoading: false,
inputValue: ''
});
}
lazySearch = debounce(
(text: string) => {
(async (text: string) => {
const onSearch = this.props.onSearch!;
let result = await onSearch(
text,
(cancelExecutor: () => void) => (this.cancelSearch = cancelExecutor)
);
if (this.unmounted) {
return;
}
if (!Array.isArray(result)) {
throw new Error('onSearch 需要返回数组');
}
this.setState({
searchList: result,
searchLoading: false
});
})(text).catch(e => {
this.setState({searchLoading: false});
console.error(e);
});
},
250,
{
trailing: true,
leading: false
}
);
swapSelectPosition(oldIndex: number, newIndex: number) {
const tempSelection = this.state.tempSelection;
tempSelection.splice(newIndex, 0, tempSelection.splice(oldIndex, 1)[0]);
this.setState({tempSelection});
}
@autobind
dragRef(ref: any) {
if (ref) {
this.initDragging();
}
}
initDragging() {
const ns = this.props.classPrefix;
this.sortable = new Sortable(
document.querySelector(`.${ns}UserSelect-checkContent`) as HTMLElement,
{
group: `UserSelect-checkContent`,
animation: 150,
handle: `.${ns}UserSelect-dragBar`,
ghostClass: `${ns}UserSelect--dragging`,
onEnd: (e: any) => {
if (!this.state.isEdit || e.newIndex === e.oldIndex) {
return;
}
const parent = e.to as HTMLElement;
if (e.oldIndex < parent.childNodes.length - 1) {
parent.insertBefore(e.item, parent.childNodes[e.oldIndex]);
} else {
parent.appendChild(e.item);
}
this.swapSelectPosition(e.oldIndex, e.newIndex);
}
}
);
}
destroyDragging() {
this.sortable && this.sortable.destroy();
}
@autobind
onOpen() {
const {selection} = this.state;
this.setState({
isOpened: true,
tempSelection: selection.slice()
});
}
@autobind
handleBack() {
this.setState({
isOpened: false,
inputValue: '',
isSearch: false,
searchList: [],
breadList: []
});
}
@autobind
async handleExpand(option: Option) {
const {deferLoad, isRef, isDep} = this.props;
if (!option.isLoaded || (!isRef && isDep && !option.children?.length)) {
option.isLoaded = true;
let deferParam = option.deferApi ? {deferApi: option.deferApi} : {};
if (isRef) {
// 部门、人员一起加载
const res = await Promise.all([
deferLoad(option, false, deferParam),
deferLoad({...option, ref: option.value}, true, deferParam)
]);
option.children = flatten(res);
} else {
// 只加载部门
const res = await deferLoad(option, false, deferParam);
option.children = res || [];
}
}
const breadList = this.state.breadList;
breadList.push(option);
this.setState({
breadList
});
}
@autobind
handleSelectChange(option: Option, isReplace?: boolean) {
const {multiple, onChange, valueField = 'value', controlled} = this.props;
if (controlled) {
onChange(option);
return;
}
let selection = this.state.selection.slice();
// 直接替换的option 肯定是数组
if (isReplace) {
selection = option as Option[];
} else {
let selectionVals = selection.map((option: Option) => option[valueField]);
let pos = selectionVals.indexOf(option[valueField]);
if (pos !== -1) {
selection.splice(selection.indexOf(option), 1);
} else {
if (multiple) {
selection.push(option);
} else {
selection = [option];
}
}
}
onChange(multiple ? selection : selection?.[0]);
this.setState({
selection
});
return false;
}
@autobind
onDelete(option: Option, isTemp: boolean = false) {
const {valueField = 'value'} = this.props;
const {tempSelection, selection} = this.state;
let _selection = isTemp ? tempSelection : selection;
_selection = _selection.filter(
(item: Option) => item[valueField] !== option[valueField]
);
if (isTemp) {
this.setState({tempSelection: _selection});
} else {
this.setState({selection: _selection});
}
}
@autobind
handleBreadChange(option: Option, index: number) {
const breadList = this.state.breadList.slice(0, index);
this.setState({
breadList
});
}
@autobind
handleEdit() {
const {multiple, onChange, controlled} = this.props;
const {isEdit, tempSelection} = this.state;
if (isEdit) {
if (controlled) {
onChange(multiple ? tempSelection : tempSelection?.[0], true);
this.setState({
isSelectOpened: false,
isEdit: false
});
return;
} else {
this.setState({
isSelectOpened: false,
isEdit: false,
selection: tempSelection
});
}
} else {
this.setState({
isEdit: true
});
}
}
renderIcon(option: Option, isSelect?: boolean) {
const {labelField = 'label', classnames: cx, isRef} = this.props;
const {isSearch} = this.state;
if (!option.icon) {
if (option.isRef || ((isSearch || isSelect) && isRef)) {
return (
<span className={cx('UserSelect-text-userPic')}>
{option[labelField].slice(0, 1)}
</span>
);
} else {
// 没有icon默认返回部门图标
return (
<span className={cx('icon', 'UserSelect-icon-box', 'department')}>
<Icon icon="department" className="icon" />
</span>
);
}
}
// 支持角色、岗位等图标配置
let IconHtml;
switch (option.icon) {
case 'user-default-department':
IconHtml = (
<span className={cx('icon', 'UserSelect-icon-box', 'department')}>
<Icon icon="department" className="icon" />
</span>
);
break;
case 'user-default-role':
IconHtml = (
<span className={cx('icon', 'UserSelect-icon-box', 'role')}>
<Icon icon="role" className="icon" />
</span>
);
break;
case 'user-default-post':
IconHtml = (
<span className={cx('icon', 'UserSelect-icon-box', 'post')}>
<Icon icon="post" className="icon" />
</span>
);
break;
case '':
IconHtml = (
<span className={cx('UserSelect-text-userPic')}>
{option[labelField].slice(0, 1)}
</span>
);
break;
default:
IconHtml = (
<img src={option.icon} className={cx('UserSelect-userPic')} />
);
}
return IconHtml;
}
renderList(
options: Array<object> = [],
key?: number | string,
isSearch?: boolean
) {
const {
classnames: cx,
valueField = 'value',
labelField = 'label',
isDep,
isRef,
translate: __,
controlled
} = this.props;
let selection = controlled
? this.props.selection || []
: this.state.selection;
const checkValues = selection.map((item: Option) => item[valueField]);
return options.length ? (
<div className={cx('UserSelect-memberList-box')} key={key}>
<ul className={cx(`UserSelect-memberList`)} key={key}>
{options.map((option: Option, index: number) => {
const hasChildren =
(isRef && !option.isRef) ||
(isDep && (option.defer || option.children?.length));
const checkVisible =
(isDep && isRef) ||
(isRef && option.isRef) ||
(isDep && !isRef) ||
isSearch;
const userIcon = this.renderIcon(option);
return (
<li key={index}>
{checkVisible ? (
<Checkbox
size="sm"
checked={checkValues.includes(option[valueField])}
label={''}
onChange={() => this.handleSelectChange(option)}
/>
) : null}
<span
className={cx('UserSelect-memberName')}
onClick={() =>
checkVisible
? this.handleSelectChange(option)
: hasChildren && this.handleExpand(option)
}
>
{userIcon ? (
<span className={cx('UserSelect-userPic-box')}>
{userIcon}
</span>
) : null}
<span className={cx('UserSelect-label')}>
{option[labelField]}
</span>
</span>
{!isSearch && hasChildren ? (
<span
className={cx(`UserSelect-more`)}
onClick={() => this.handleExpand(option)}
>
<Icon icon="caret" className="icon" />
</span>
) : null}
</li>
);
})}
</ul>
</div>
) : (
<div className={cx(`UserSelect-noRecord`)}>
{__('placeholder.noOption')}~
</div>
);
}
renderselectList(options: Array<object> = []) {
const {
classnames: cx,
labelField = 'label',
valueField = 'value',
translate: __
} = this.props;
const {isEdit} = this.state;
return options.length ? (
<div className={cx('UserSelect-selection-wrap')}>
<ul
className={cx(`UserSelect-selection`, `UserSelect-checkContent`)}
ref={this.dragRef}
>
{options.map((option: Option, index: number) => {
const userIcon = this.renderIcon(option, true);
const options = this.state.options;
const originOption = findTree(
options,
(item: Option) => item[valueField] === option[valueField]
);
return (
<li key={index}>
{isEdit ? (
<span
className={cx(`UserSelect-del`)}
onClick={() => this.onDelete(option, true)}
>
<Icon icon="user-remove" className="icon" />
</span>
) : null}
<span className={cx(`UserSelect-memberName`)}>
{userIcon ? (
<span className={cx('UserSelect-userPic-box')}>
{userIcon}
</span>
) : null}
<span className={cx('UserSelect-label')}>
{originOption
? originOption[labelField]
: option[labelField]}
</span>
</span>
{isEdit ? (
<a className={cx('UserSelect-dragBar')}>
<Icon icon="drag-bar" className={cx('icon')} />
</a>
) : null}
</li>
);
})}
</ul>
</div>
) : (
<div className={cx(`UserSelect-noRecord`)}>
{__('placeholder.noOption')}~
</div>
);
}
renderContent() {
let {
navTitle,
showNav,
searchable,
searchPlaceholder,
controlled,
labelField = 'label',
valueField = 'value',
classnames: cx,
translate: __
} = this.props;
const {breadList, options, isSearch, searchList, searchLoading} =
this.state;
let selection = controlled
? this.props.selection || []
: this.state.selection;
return (
<div className={cx(`UserSelect-wrap`)}>
{showNav ? (
<div className={cx('UserSelect-navbar')}>
<span className="left-arrow-box" onClick={this.handleBack}>
<Icon icon="left-arrow" className="icon" />
</span>
<div className={cx('UserSelect-navbar-title')}>{navTitle}</div>
</div>
) : null}
{/* 搜索 */}
{searchable ? (
<div className={cx('UserSelect-searchBox')}>
<InputBox
className={cx(`UserSelect-search`)}
value={this.state.inputValue}
onChange={this.handleSearch}
placeholder={searchPlaceholder}
clearable={false}
>
{this.state.isSearch ? (
<a onClick={this.handleSeachCancel}>
<Icon icon="close" className="icon" />
</a>
) : (
<Icon icon="search" className="icon" />
)}
</InputBox>
</div>
) : null}
{/* 面包屑 */}
{breadList.length ? (
<div className={cx('UserSelect-breadcrumb')}>
{breadList
.map<React.ReactNode>((item, index) => (
<span
className={cx('UserSelect-breadcrumb-item')}
key={index}
onClick={() => this.handleBreadChange(item, index)}
>
{item[labelField]}
</span>
))
.reduce((prev, curr, index) => [
prev,
<Icon
icon="caret"
className={cx('UserSelect-breadcrumb-separator', 'icon')}
key={`separator-${index}`}
/>,
curr
])}
</div>
) : null}
{selection?.length ? (
<div className={cx(`UserSelect-resultBox`)}>
<ul className={cx(`UserSelect-selectList`)}>
{selection.map((item: Option, index) => {
const originOption = findTree(
options,
(op: Option) => op[valueField] === item[valueField]
);
return (
<li key={index} className={cx('UserSelect-selectList-item')}>
<span>
{originOption
? originOption[labelField]
: item[labelField]}
</span>
<span
className={cx('UserSelect-selectList-item-closeBox')}
onClick={() => this.onDelete(item)}
>
<Icon icon="close" className="icon" />
</span>
</li>
);
})}
</ul>
<span
className={cx('UserSelect-selectSort-box')}
onClick={() =>
this.setState({
isSelectOpened: true,
tempSelection: selection.slice()
})
}
>
<Icon
icon="menu"
className={cx('UserSelect-selectSort', 'icon')}
/>
</span>
</div>
) : null}
{isSearch ? (
searchLoading ? (
<div className={cx(`UserSelect-searchLoadingBox`)}>
<Spinner />
</div>
) : (
<div className={cx('UserSelect-searchResult')}>
{this.renderList(searchList, -1, true)}
</div>
)
) : (
<div className={cx(`UserSelect-contentBox`)}>
<div
className={cx(`UserSelect-scroll`)}
style={{
width: 100 * (breadList.length + 1) + 'vw',
left: -breadList.length * 100 + 'vw'
}}
>
{this.renderList(options)}
{breadList.map((option: Option, index: number) => {
const treeOption = findTree(
options,
optionValueCompare(option[valueField], valueField || 'value')
) as Option;
const children = treeOption.children;
const hasChildren = Array.isArray(children) && children;
return hasChildren ? (
this.renderList(children, option[valueField])
) : (
<div className={cx(`UserSelect-spinnerBox`)} key={index}>
<Spinner />
</div>
);
})}
</div>
</div>
)}
</div>
);
}
render() {
let {
classnames: cx,
translate: __,
placeholder = '请选择',
showResultBox,
controlled,
onChange
} = this.props;
const {isOpened, tempSelection, isSelectOpened, isEdit} = this.state;
let selection = controlled ? this.props.selection : this.state.selection;
return (
<div className={cx('UserSelect')}>
{showResultBox ? (
<ResultBox
className={cx('UserSelect-input', isOpened ? 'is-active' : '')}
allowInput={false}
result={selection}
onResultChange={value => this.handleSelectChange(value, true)}
onResultClick={this.onOpen}
placeholder={placeholder}
useMobileUI
/>
) : null}
{showResultBox ? (
<PopUp
isShow={isOpened}
className={cx(`UserSelect-popup`)}
onHide={this.onClose}
showClose={false}
>
{this.renderContent()}
</PopUp>
) : (
this.renderContent()
)}
<PopUp
isShow={isSelectOpened}
className={cx(`UserSelect-selectPopup`)}
onHide={() =>
this.setState({
isSelectOpened: false,
isEdit: false
})
}
showClose={false}
>
<div className={cx('UserSelect-selectBody')}>
<div className={cx('UserSelect-navbar')}>
<span
className="left-arrow-box"
onClick={() =>
this.setState({
isSelectOpened: false,
isEdit: false
})
}
>
<Icon icon="left-arrow" className="icon" />
</span>
<div className={cx('UserSelect-navbar-title')}>
{__('UserSelect.resultSort')}
</div>
<span
className={cx('UserSelect-navbar-btnEdit')}
onClick={this.handleEdit}
>
{isEdit ? __('UserSelect.save') : __('UserSelect.edit')}
</span>
</div>
<div className={cx('UserSelect-selectList-box')}>
<div className={cx('UserSelect-select-head')}>
<span className={cx('UserSelect-select-head-text')}>
{__('UserSelect.selected')}
</span>
{isEdit ? (
<span
className={cx('UserSelect-select-head-btnClear')}
onClick={() => this.setState({tempSelection: []})}
>
{__('UserSelect.clear')}
</span>
) : null}
</div>
{this.renderselectList(tempSelection)}
</div>
</div>
</PopUp>
</div>
);
}
}
export default themeable(localeable(UserSelect));

View File

@ -0,0 +1,261 @@
/**
* @file
* @author fex
*/
import React from 'react';
import {themeable, ThemeProps} from '../theme';
import {LocaleProps, localeable} from '../locale';
import {ResultBox} from '../components';
import UserSelect from './UserSelect';
import {Option} from '../renderers/Form/Options';
import Sortable from 'sortablejs';
import PopUp from '../components/PopUp';
import {Icon} from '../components/icons';
import {autobind, findTree} from '../utils/helper';
import {default as Tabs, Tab} from './Tabs';
import {UserSelectProps} from './UserSelect';
import {PlainObject} from '../types';
import {resolveVariableAndFilter} from '../utils/tpl-builtin';
export interface UserSelectTop extends UserSelectProps {
title: string;
deferApi?: string;
searchApi?: string;
searchable?: boolean;
searchParam?: PlainObject;
searchTerm?: string;
}
export interface UserTabSelectProps extends ThemeProps, LocaleProps {
tabOptions?: Array<UserSelectTop>;
multiple?: boolean;
placeholder?: string;
valueField?: string;
labelField?: string;
selection?: Array<Option>;
data?: PlainObject;
onChange: (value: Array<Option> | Option) => void;
onSearch?: (
term: string,
cancelExecutor: Function,
paramObj?: PlainObject
) => Promise<any[]> | undefined;
deferLoad: (
data?: Object,
isRef?: boolean,
param?: PlainObject
) => Promise<Option[]>;
}
export interface UserTabSelectState {
isOpened: boolean;
isSearch: boolean;
isSelectOpened: boolean;
inputValue: string;
breadList: Array<any>;
options: Array<Option>;
tempSelection: Array<Option>;
selection: Array<Option>;
searchList: Array<Option>;
searchLoading: boolean;
isEdit: boolean;
activeKey: number;
}
export class UserTabSelect extends React.Component<
UserTabSelectProps,
UserTabSelectState
> {
cancelSearch?: Function;
sortable?: Sortable;
unmounted = false;
constructor(props: UserTabSelectProps) {
super(props);
this.state = {
isOpened: false,
isSelectOpened: false,
inputValue: '',
options: [],
breadList: [],
searchList: [],
tempSelection: [],
selection: props.selection ? props.selection : [],
isSearch: false,
searchLoading: false,
isEdit: false,
activeKey: 0
};
}
static defaultProps = {};
componentDidMount() {}
componentDidUpdate(prevProps: UserTabSelectProps) {}
componentWillUnmount() {
this.unmounted = true;
}
@autobind
onClose() {
this.setState({
isOpened: false,
isSearch: false,
inputValue: '',
searchList: [],
searchLoading: false,
activeKey: 0
});
}
@autobind
onOpen() {
const {selection} = this.state;
this.setState({
isOpened: true,
tempSelection: selection.slice()
});
}
@autobind
handleBack() {
this.onClose();
const {onChange} = this.props;
onChange(this.state.selection);
}
@autobind
handleSelectChange(option: Option | Array<Option>, isReplace?: boolean) {
const {multiple, valueField = 'value'} = this.props;
let selection = this.state.selection.slice();
let selectionVals = selection.map((option: Option) => option[valueField]);
if (isReplace && Array.isArray(option)) {
selection = option.slice();
} else if (!Array.isArray(option)) {
let pos = selectionVals.indexOf(option[valueField]);
if (pos !== -1) {
selection.splice(selection.indexOf(option), 1);
} else {
if (multiple) {
selection.push(option);
} else {
selection = [option];
}
}
}
this.setState({
selection: selection
});
return false;
}
@autobind
handleTabChange(key: number) {
this.setState({
activeKey: key
});
}
render() {
let {
classnames: cx,
translate: __,
onChange,
placeholder = '请选择',
tabOptions,
onSearch,
deferLoad,
data
} = this.props;
const {activeKey, isOpened, selection} = this.state;
return (
<div className={cx('UserTabSelect')}>
<ResultBox
className={cx('UserTabSelect-input', isOpened ? 'is-active' : '')}
allowInput={false}
result={selection}
onResultChange={value => this.handleSelectChange(value, true)}
onResultClick={this.onOpen}
placeholder={placeholder}
useMobileUI
/>
<PopUp
isShow={isOpened}
className={cx(`UserTabSelect-popup`)}
onHide={this.onClose}
showClose={false}
>
<div className={cx('UserTabSelect-wrap')}>
<div className={cx('UserSelect-navbar')}>
<span className="left-arrow-box" onClick={this.handleBack}>
<Icon icon="left-arrow" className="icon" />
</span>
<div className={cx('UserSelect-navbar-title')}></div>
</div>
<Tabs
mode="tiled"
className={cx('UserTabSelect-tabs')}
onSelect={this.handleTabChange}
activeKey={activeKey}
>
{tabOptions?.map((item: UserSelectTop, index: number) => {
return (
<Tab
{...this.props}
eventKey={index}
key={index}
title={item.title}
className="TabsTransfer-tab"
>
<UserSelect
selection={selection}
showResultBox={false}
{...item}
options={
typeof item.options === 'string' && data
? resolveVariableAndFilter(
item.options,
data,
'| raw'
)
: item.options
}
multiple
controlled
onChange={this.handleSelectChange}
onSearch={(input: string, cancelExecutor: Function) =>
item.searchable && onSearch
? onSearch(input, cancelExecutor, {
searchApi: item.searchApi,
searchParam: item.searchParam,
searchTerm: item.searchTerm
})
: undefined
}
deferLoad={(
data?: PlainObject,
isRef?: boolean,
param?: PlainObject
) =>
deferLoad(data, isRef, {
deferApi: item.deferApi,
...(param || {})
})
}
/>
</Tab>
);
})}
</Tabs>
</div>
</PopUp>
</div>
);
}
}
export default themeable(localeable(UserTabSelect));

View File

@ -84,6 +84,11 @@ import FunctionIcon from '../icons/function.svg';
import InputClearIcon from '../icons/input-clear.svg';
import SliderHandleIcon from '../icons/slider-handle-icon.svg';
import TrashIcon from '../icons/trash.svg';
import MenuIcon from '../icons/menu.svg';
import UserRemove from '../icons/user-remove.svg';
import Role from '../icons/role.svg';
import Department from '../icons/department.svg';
import Post from '../icons/post.svg';
import DotIcon from '../icons/dot.svg';
// 兼容原来的用法,后续不直接试用。
@ -195,6 +200,11 @@ registerIcon('cloud-upload', CloudUploadIcon);
registerIcon('image', ImageIcon);
registerIcon('refresh', RefreshIcon);
registerIcon('trash', TrashIcon);
registerIcon('menu', MenuIcon);
registerIcon('user-remove', UserRemove);
registerIcon('role', Role);
registerIcon('department', Department);
registerIcon('post', Post);
registerIcon('dot', DotIcon);
export function Icon({
@ -234,5 +244,10 @@ export {
PlusIcon,
MinusIcon,
PencilIcon,
FunctionIcon
FunctionIcon,
MenuIcon,
UserRemove,
Role,
Department,
Post
};

17
src/icons/department.svg Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>部门</title>
<g id="PC-流程属性、找人找部门+公式化编辑器+字段权限" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-3271.000000, -1246.000000)" id="部门">
<g transform="translate(3271.000000, 1246.000000)">
<rect id="矩形" fill-opacity="0.01" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="16" height="16"></rect>
<circle id="椭圆形" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" cx="4.66666667" cy="9.66666667" r="1.66666667"></circle>
<circle id="椭圆形" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" cx="11.3333333" cy="9.66666667" r="1.66666667"></circle>
<circle id="椭圆形" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" cx="8" cy="3" r="1.66666667"></circle>
<path d="M8,14.6666667 C8,12.8257333 6.5076,11.3333333 4.66666667,11.3333333 C2.82571667,11.3333333 1.33333333,12.8257333 1.33333333,14.6666667" id="路径" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M14.6666667,14.6666667 C14.6666667,12.8257333 13.1742667,11.3333333 11.3333333,11.3333333 C9.4924,11.3333333 8,12.8257333 8,14.6666667" id="路径" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M11.3333333,8 C11.3333333,6.15906667 9.84093333,4.66666667 8,4.66666667 C6.15906667,4.66666667 4.66666667,6.15906667 4.66666667,8" id="路径" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

2
src/icons/menu.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1649759681558" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1191" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M549.624242 214.626263H113.648485c-18.10101 0-32.840404-14.739394-32.840404-32.840404 0-18.10101 14.739394-32.840404 32.840404-32.840404h436.10505c18.10101 0 32.840404 14.739394 32.840404 32.840404-0.129293 18.10101-14.868687 32.840404-32.969697 32.840404z m0 0M549.624242 545.616162H113.648485c-18.10101 0-32.840404-14.739394-32.840404-32.840404 0-18.10101 14.739394-32.840404 32.840404-32.840404h436.10505c18.10101 0 32.840404 14.739394 32.840404 32.840404-0.129293 18.10101-14.868687 32.840404-32.969697 32.840404z m0 0M549.624242 876.088889H113.648485c-18.10101 0-32.840404-14.739394-32.840404-32.840404s14.739394-32.840404 32.840404-32.840404h436.10505c18.10101 0 32.840404 14.739394 32.840404 32.840404s-14.868687 32.840404-32.969697 32.840404z m0 0M932.589899 659.006061c-12.8-12.8-33.616162-12.8-46.416162 0l-104.727272 104.727272V182.044444c0-18.10101-14.739394-32.840404-32.840404-32.840404-18.10101 0-32.840404 14.739394-32.840404 32.840404v661.462627c0 18.10101 14.739394 32.840404 32.840404 32.840404 8.016162 0 15.385859-2.844444 21.074747-7.628283 0.905051-0.775758 162.779798-163.167677 162.779798-163.167677 12.929293-12.8 12.929293-33.745455 0.129293-46.545454z m0 0" fill="" p-id="1192"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

15
src/icons/post.svg Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>岗位</title>
<g id="选人选部门" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="补充icon及颜色" transform="translate(-95.000000, -12.000000)">
<g id="岗位" transform="translate(95.000000, 12.000000)">
<rect id="矩形" fill-opacity="0.01" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="16" height="16"></rect>
<path d="M6.33333333,6.66666667 C7.622,6.66666667 8.66666667,5.622 8.66666667,4.33333333 C8.66666667,3.04467 7.622,2 6.33333333,2 C5.04466667,2 4,3.04467 4,4.33333333 C4,5.622 5.04466667,6.66666667 6.33333333,6.66666667 Z" id="路径" stroke="#FFFFFF" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M10.8692333,2.33333333 C11.5468333,2.74163333 12.0000333,3.48456667 12.0000333,4.33333333 C12.0000333,5.1821 11.5468333,5.92503333 10.8692333,6.33333333" id="路径" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M1.33333333,13.6 L1.33333333,14 L11.3333333,14 L11.3333333,13.6 C11.3333333,12.1065333 11.3333333,11.3598 11.0427,10.7893667 C10.7870333,10.2876 10.3790667,9.87963333 9.8773,9.62396667 C9.30686667,9.33333333 8.56013333,9.33333333 7.06666667,9.33333333 L5.6,9.33333333 C4.10653333,9.33333333 3.3598,9.33333333 2.78936,9.62396667 C2.28759333,9.87963333 1.87964333,10.2876 1.62398333,10.7893667 C1.33333333,11.3598 1.33333333,12.1065333 1.33333333,13.6 Z" id="路径" stroke="#FFFFFF" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M14.6666333,14 L14.6666333,13.6 C14.6666333,12.1065333 14.6666333,11.3598 14.376,10.7893667 C14.1203333,10.2876 13.7123667,9.87963333 13.2106,9.62396667" id="路径" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

14
src/icons/role.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>角色</title>
<g id="PC-流程属性、找人找部门+公式化编辑器+字段权限" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-3426.000000, -1246.000000)" id="角色">
<g transform="translate(3426.000000, 1246.000000)">
<rect id="矩形" fill-opacity="0.01" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="16" height="16"></rect>
<circle id="椭圆形" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" cx="8" cy="4" r="2.66666667"></circle>
<path d="M14,14.6666667 C14,11.3529667 11.3137,8.66666667 8,8.66666667 C4.6863,8.66666667 2,11.3529667 2,14.6666667" id="路径" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round"></path>
<polygon id="路径" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" points="8 14.6666667 9.33333333 13 8 8.66666667 6.66666667 13"></polygon>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

12
src/icons/user-remove.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>应用中心</title>
<g id="控件" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="排序删除部门" transform="translate(-16.000000, -16.000000)">
<g id="编组-39" transform="translate(16.000000, 16.000000)">
<circle id="椭圆形" fill="#F6654D" cx="8" cy="8" r="8"></circle>
<rect id="矩形" fill="#FFFFFF" x="4" y="7" width="8" height="2" rx="0.5"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@ -128,6 +128,7 @@ import './renderers/Form/TabsTransfer';
import './renderers/Form/TabsTransferPicker';
import './renderers/Form/Group';
import './renderers/Form/InputGroup';
import './renderers/Form/UserSelect';
import './renderers/Grid';
import './renderers/Grid2D';
import './renderers/HBox';

View File

@ -311,6 +311,11 @@ register('de-DE', {
'pullRefresh.loadingText': 'Laden...',
'pullRefresh.successText': 'Laden erfolgreich',
'Picker.placeholder': 'Klicken Sie rechts auf das Symbol',
'UserSelect.edit': 'bearbeiten',
'UserSelect.save': 'Konservierung',
'UserSelect.resultSort': 'Ergebnissortierung auswählen',
'UserSelect.selected': 'Ausgewählt',
'UserSelect.clear': 'leer',
'SchemaType.string': 'String',
'SchemaType.number': 'Number',
'SchemaType.interger': 'Interger',

View File

@ -314,6 +314,11 @@ register('en-US', {
'pullRefresh.loadingText': 'Loading...',
'pullRefresh.successText': 'Loading success',
'Picker.placeholder': 'Click icon on the right',
'UserSelect.edit': 'edit',
'UserSelect.save': 'preservation',
'UserSelect.resultSort': 'Select result sort',
'UserSelect.selected': 'Selected',
'UserSelect.clear': 'empty',
'SchemaType.string': 'String',
'SchemaType.number': 'Number',
'SchemaType.interger': 'Interger',

View File

@ -321,6 +321,11 @@ register('zh-CN', {
'pullRefresh.loadingText': '加载中...',
'pullRefresh.successText': '加载成功',
'Picker.placeholder': '请点击右侧的图标',
'UserSelect.edit': '编辑',
'UserSelect.save': '保存',
'UserSelect.resultSort': '选择结果排序',
'UserSelect.selected': '已选',
'UserSelect.clear': '清空',
'SchemaType.string': '文本',
'SchemaType.number': '数字',
'SchemaType.interger': '整数',

View File

@ -0,0 +1,263 @@
import React from 'react';
import cx from 'classnames';
import {
OptionsControl,
OptionsControlProps,
Option,
FormOptionsControl
} from './Options';
import UserSelect from '../../components/UserSelect';
import UserTabSelect from '../../components/UserTabSelect';
import {isEffectiveApi} from '../../utils/api';
import find from 'lodash/find';
import {createObject, autobind} from '../../utils/helper';
import {PlainObject} from '../../types';
/**
* UserSelect
*/
export interface UserSelectControlSchema extends FormOptionsControl {
type: 'users-select';
}
export interface UserSelectProps extends OptionsControlProps {
/**
*
*/
isDep?: boolean;
/**
*
*/
isRef?: boolean;
/**
*
*/
showNav?: boolean;
/**
*
*/
navTitle?: string;
/**
*
*/
tabMode?: boolean;
tabOptions?: Array<any>;
/**
*
*/
searchTerm?: string;
/**
*
*/
searchParam?: PlainObject;
}
export default class UserSelectControl extends React.Component<
UserSelectProps,
any
> {
static defaultProps: Partial<UserSelectProps> = {
showNav: true
};
input?: HTMLInputElement;
unHook: Function;
lazyloadRemote: Function;
constructor(props: UserSelectProps) {
super(props);
}
componentWillUnmount() {
this.unHook && this.unHook();
}
@autobind
async onSearch(input: string, cancelExecutor: Function, param?: PlainObject) {
let {searchApi, setLoading, env} = this.props;
searchApi = param?.searchApi || searchApi;
let searchTerm = param?.searchTerm || this.props.searchTerm || 'term';
let searchObj = param?.searchParam || this.props.searchParam || {};
const ctx = {
[searchTerm]: input,
...searchObj
};
if (!isEffectiveApi(searchApi, ctx)) {
return Promise.resolve([]);
}
setLoading(true);
try {
const ret = await env.fetcher(searchApi, ctx, {
cancelExecutor,
autoAppend: true
});
let options = (ret.data && (ret.data as any).options) || ret.data || [];
return options;
} finally {
setLoading(false);
}
}
@autobind
async deferLoad(data?: Object, isRef?: boolean, param?: PlainObject) {
let {env, deferApi, setLoading, formInited, addHook} = this.props;
deferApi = param?.deferApi || deferApi;
if (!env || !env.fetcher) {
throw new Error('fetcher is required');
}
const ctx = createObject(data, {});
if (!isEffectiveApi(deferApi, ctx)) {
return Promise.resolve([]);
}
try {
const ret = await env.fetcher(deferApi, ctx);
let options = (ret.data && (ret.data as any).options) || ret.data || [];
if (isRef) {
options.forEach((option: Option) => {
option.isRef = true;
});
}
return options;
} finally {
setLoading(false);
}
}
@autobind
async changeValue(value: Option | Array<Option> | string | void) {
const {
joinValues,
extractValue,
delimiter,
multiple,
valueField,
onChange,
options,
setOptions,
data,
dispatchEvent
} = this.props;
let newValue: string | Option | Array<Option> | void = value;
let additonalOptions: Array<any> = [];
(Array.isArray(value) ? value : value ? [value] : []).forEach(
(option: any) => {
let resolved = find(
options,
(item: any) =>
item[valueField || 'value'] == option[valueField || 'value']
);
resolved || additonalOptions.push(option);
}
);
if (joinValues) {
if (multiple) {
newValue = Array.isArray(value)
? (value
.map(item => item[valueField || 'value'])
.join(delimiter) as string)
: value
? (value as Option)[valueField || 'value']
: '';
} else {
newValue = newValue ? (newValue as Option)[valueField || 'value'] : '';
}
} else if (extractValue) {
if (multiple) {
newValue = Array.isArray(value)
? value.map(item => item[valueField || 'value'])
: value
? [(value as Option)[valueField || 'value']]
: [];
} else {
newValue = newValue ? (newValue as Option)[valueField || 'value'] : '';
}
}
const rendererEvent = await dispatchEvent(
'change',
createObject(data, {
value: newValue,
options
})
);
if (rendererEvent?.prevented) {
return;
}
onChange(newValue);
}
render() {
let {
showNav,
navTitle,
searchable,
options,
className,
selectedOptions,
tabOptions,
multi,
multiple,
isDep,
isRef,
placeholder,
searchPlaceholder,
tabMode,
data
} = this.props;
tabOptions?.forEach((item: any) => {
item.deferLoad = this.deferLoad;
item.onChange = this.changeValue;
item.onSearch = this.onSearch;
});
return (
<div className={cx(`UserSelectControl`, className)}>
{tabMode ? (
<UserTabSelect
selection={selectedOptions}
tabOptions={tabOptions}
multiple={multiple}
onChange={this.changeValue}
onSearch={this.onSearch}
deferLoad={this.deferLoad}
data={data}
/>
) : (
<UserSelect
showNav={showNav}
navTitle={navTitle}
selection={selectedOptions}
options={options}
multi={multi}
multiple={multiple}
searchable={searchable}
placeholder={placeholder}
searchPlaceholder={searchPlaceholder}
deferLoad={this.deferLoad}
onChange={this.changeValue}
onSearch={this.onSearch}
isDep={isDep}
isRef={isRef}
/>
)}
</div>
);
}
}
@OptionsControl({
type: 'users-select'
})
export class UserSelectControlRenderer extends UserSelectControl {}