mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-30 02:48:55 +08:00
人员选择组件支持部门、角色、岗位选择 (#4285)
* 人员选择组件 * 修复ts报错 Co-authored-by: zhangxulong <zhangxulong@baidu.com>
This commit is contained in:
parent
f4e29ad18f
commit
dd5200ce18
33
__tests__/renderers/Form/usersSelect.test.tsx
Normal file
33
__tests__/renderers/Form/usersSelect.test.tsx
Normal 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();
|
||||
});
|
@ -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)};
|
||||
|
422
scss/components/form/_user-select.scss
Normal file
422
scss/components/form/_user-select.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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
|
||||
|
850
src/components/UserSelect.tsx
Normal file
850
src/components/UserSelect.tsx
Normal 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));
|
261
src/components/UserTabSelect.tsx
Normal file
261
src/components/UserTabSelect.tsx
Normal 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));
|
@ -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
17
src/icons/department.svg
Normal 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
2
src/icons/menu.svg
Normal 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
15
src/icons/post.svg
Normal 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
14
src/icons/role.svg
Normal 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
12
src/icons/user-remove.svg
Normal 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 |
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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': '整数',
|
||||
|
263
src/renderers/Form/UserSelect.tsx
Normal file
263
src/renderers/Form/UserSelect.tsx
Normal 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 {}
|
Loading…
Reference in New Issue
Block a user