Commit d9434ba1 by Johannes Schill

wip: Upgrade react-select #13425

parent cecf8757
......@@ -17,6 +17,7 @@
"@types/react": "^16.4.14",
"@types/react-custom-scrollbars": "^4.0.5",
"@types/react-dom": "^16.0.7",
"@types/react-select": "^2.0.4",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
......@@ -157,7 +158,7 @@
"react-highlight-words": "^0.10.0",
"react-popper": "^0.7.5",
"react-redux": "^5.0.7",
"react-select": "^1.1.0",
"react-select": "^2.0.0",
"react-sizeme": "^2.3.6",
"react-transition-group": "^2.2.1",
"redux": "^4.0.0",
......
......@@ -17,6 +17,10 @@ export interface Props {
onCancel: () => void;
}
export interface TeamSelectedAction {
action: string;
}
class AddPermissions extends Component<Props, NewDashboardAclItem> {
constructor(props) {
super(props);
......@@ -50,11 +54,11 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
};
onUserSelected = (user: User) => {
this.setState({ userId: user ? user.id : 0 });
this.setState({ userId: user && !Array.isArray(user) ? user.id : 0 });
};
onTeamSelected = (team: Team) => {
this.setState({ teamId: team ? team.id : 0 });
onTeamSelected = (team: Team, info: TeamSelectedAction) => {
this.setState({ teamId: team && !Array.isArray(team) ? team.id : 0 });
};
onPermissionChanged = (permission: OptionWithDescription) => {
......@@ -82,7 +86,6 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
const newItem = this.state;
const pickerClassName = 'width-20';
const isValid = this.isValid();
return (
<div className="gf-form-inline cta-form">
<button className="cta-form__close btn btn-transparent" onClick={onCancel}>
......@@ -107,21 +110,13 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
{newItem.type === AclTarget.User ? (
<div className="gf-form">
<UserPicker
onSelected={this.onUserSelected}
value={newItem.userId.toString()}
className={pickerClassName}
/>
<UserPicker onSelected={this.onUserSelected} value={newItem.userId} className={pickerClassName} />
</div>
) : null}
{newItem.type === AclTarget.Team ? (
<div className="gf-form">
<TeamPicker
onSelected={this.onTeamSelected}
value={newItem.teamId.toString()}
className={pickerClassName}
/>
<TeamPicker onSelected={this.onTeamSelected} value={newItem.teamId} className={pickerClassName} />
</div>
) : null}
......
......@@ -79,7 +79,7 @@ export default class PermissionsListItem extends PureComponent<Props> {
onSelected={this.onPermissionChanged}
value={item.permission}
disabled={item.inherited}
className={'gf-form-input--form-dropdown-right'}
className={'gf-form-select2__control--menu-right'}
/>
</div>
</td>
......
import React, { Component } from 'react';
import React from 'react';
import { components } from 'react-select';
import { OptionProps } from 'react-select/lib/components/Option';
export interface Props {
onSelect: any;
onFocus: any;
option: any;
isFocused: any;
className: any;
children: Element;
isSelected: boolean;
data: any;
getStyles: any;
}
class DescriptionOption extends Component<Props, any> {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
}
handleMouseDown(event) {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
}
handleMouseEnter(event) {
this.props.onFocus(this.props.option, event);
}
handleMouseMove(event) {
if (this.props.isFocused) {
return;
}
this.props.onFocus(this.props.option, event);
}
render() {
const { option, children, className } = this.props;
return (
<button
onMouseDown={this.handleMouseDown}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
title={option.title}
className={`description-picker-option__button btn btn-link ${className} width-19`}
>
export const Option = (props: OptionProps<any>) => {
const { children, isSelected, data } = props;
return (
<components.Option {...props}>
<div className={`description-picker-option__button btn btn-link width-19`}>
{isSelected && <i className="fa fa-check pull-right" aria-hidden="true" />}
<div className="gf-form">{children}</div>
<div className="gf-form">
<div className="muted width-17">{option.description}</div>
{className.indexOf('is-selected') > -1 && <i className="fa fa-check" aria-hidden="true" />}
<div className="muted width-17">{data.description}</div>
</div>
</button>
);
}
}
</div>
</components.Option>
);
};
export default DescriptionOption;
export default Option;
import React, { Component } from 'react';
import Select from 'react-select';
import DescriptionOption from './DescriptionOption';
import IndicatorsContainer from './IndicatorsContainer';
import ResetStyles from './ResetStyles';
import NoOptionsMessage from './NoOptionsMessage';
export interface OptionWithDescription {
value: any;
label: string;
description: string;
}
export interface Props {
optionsWithDesc: OptionWithDescription[];
onSelected: (permission) => void;
value: number;
disabled: boolean;
className?: string;
}
export interface OptionWithDescription {
value: any;
label: string;
description: string;
}
class DescriptionPicker extends Component<Props, any> {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { optionsWithDesc, onSelected, value, disabled, className } = this.props;
const { optionsWithDesc, onSelected, disabled, className } = this.props;
return (
<div className="permissions-picker">
<Select
value={value}
valueKey="value"
multi={false}
clearable={false}
labelKey="label"
placeholder="Choose"
classNamePrefix={`gf-form-select2`}
className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`}
options={optionsWithDesc}
components={{
Option: DescriptionOption,
IndicatorsContainer,
NoOptionsMessage,
}}
styles={ResetStyles}
isDisabled={disabled}
onChange={onSelected}
className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`}
optionComponent={DescriptionOption}
placeholder="Choose"
disabled={disabled}
getOptionValue={i => i.value}
getOptionLabel={i => i.label}
// menuIsOpen={true} // debug
/>
</div>
);
......
import React from 'react';
import { components } from 'react-select';
export const IndicatorsContainer = props => {
const isOpen = props.selectProps.menuIsOpen;
return (
<components.IndicatorsContainer {...props}>
<span className={`gf-form-select2__select-arrow ${isOpen ? `gf-form-select2__select-arrow--reversed` : ''}`} />
</components.IndicatorsContainer>
);
};
export default IndicatorsContainer;
import React from 'react';
import { components } from 'react-select';
import { OptionProps } from 'react-select/lib/components/Option';
export interface Props {
children: Element;
}
export const PickerOption = (props: OptionProps<any>) => {
const { children } = props;
return (
<components.Option {...props}>
<div className={`description-picker-option__button btn btn-link width-19`}>{children}</div>
</components.Option>
);
};
export default PickerOption;
import React, { Component } from 'react';
import React from 'react';
import { components } from 'react-select';
import { OptionProps } from 'react-select/lib/components/Option';
export interface Props {
onSelect: any;
onFocus: any;
option: any;
isFocused: any;
className: any;
children: Element;
isSelected: boolean;
data: any;
getStyles: any;
className?: string;
}
class UserPickerOption extends Component<Props, any> {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
}
handleMouseDown(event) {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
}
handleMouseEnter(event) {
this.props.onFocus(this.props.option, event);
}
handleMouseMove(event) {
if (this.props.isFocused) {
return;
}
this.props.onFocus(this.props.option, event);
}
render() {
const { option, children, className } = this.props;
return (
<button
onMouseDown={this.handleMouseDown}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
title={option.title}
className={`user-picker-option__button btn btn-link ${className}`}
>
<img src={option.avatarUrl} alt={option.label} className="user-picker-option__avatar" />
export const PickerOption = (props: OptionProps<any>) => {
const { children, data } = props;
return (
<components.Option {...props}>
<div className={`description-picker-option__button btn btn-link width-19`}>
<img src={data.avatarUrl} alt={data.label} className="user-picker-option__avatar" />
{children}
</button>
);
}
}
</div>
</components.Option>
);
};
export default UserPickerOption;
export default PickerOption;
export default {
clearIndicator: () => ({}),
container: () => ({}),
control: () => ({}),
dropdownIndicator: () => ({}),
group: () => ({}),
groupHeading: () => ({}),
indicatorsContainer: () => ({}),
indicatorSeparator: () => ({}),
input: () => ({}),
loadingIndicator: () => ({}),
loadingMessage: () => ({}),
menu: () => ({}),
menuList: () => ({}),
multiValue: () => ({}),
multiValueLabel: () => ({}),
multiValueRemove: () => ({}),
noOptionsMessage: () => ({}),
option: () => ({}),
placeholder: () => ({}),
singleValue: () => ({}),
valueContainer: () => ({}),
};
import React, { Component } from 'react';
import Select from 'react-select';
import AsyncSelect from 'react-select/lib/Async';
import PickerOption from './PickerOption';
import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv';
import ResetStyles from './ResetStyles';
import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage';
export interface Team {
id: number;
label: string;
name: string;
avatarUrl: string;
}
export interface Props {
onSelected: (team: Team) => void;
value?: string;
className?: string;
}
export interface State {
isLoading;
}
export interface Team {
id: number;
label: string;
name: string;
avatarUrl: string;
isLoading: boolean;
}
export class TeamPicker extends Component<Props, State> {
......@@ -39,7 +41,7 @@ export class TeamPicker extends Component<Props, State> {
const backendSrv = getBackendSrv();
this.setState({ isLoading: true });
return backendSrv.get(`/api/teams/search?perpage=50&page=1&query=${query}`).then(result => {
return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => {
const teams = result.teams.map(team => {
return {
id: team.id,
......@@ -50,31 +52,35 @@ export class TeamPicker extends Component<Props, State> {
});
this.setState({ isLoading: false });
return { options: teams };
return teams;
});
}
render() {
const { onSelected, value, className } = this.props;
const { onSelected, className } = this.props;
const { isLoading } = this.state;
return (
<div className="user-picker">
<Select.Async
valueKey="id"
multi={false}
labelKey="label"
cache={false}
<AsyncSelect
classNamePrefix={`gf-form-select2`}
isMulti={false}
isLoading={isLoading}
defaultOptions={true}
loadOptions={this.debouncedSearch}
loadingPlaceholder="Loading..."
noResultsText="No teams found"
onChange={onSelected}
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
optionComponent={PickerOption}
styles={ResetStyles}
components={{
Option: PickerOption,
IndicatorsContainer,
NoOptionsMessage,
}}
placeholder="Select a team"
value={value}
autosize={true}
loadingMessage={() => 'Loading...'}
noOptionsMessage={() => 'No teams found'}
getOptionValue={i => i.id}
getOptionLabel={i => i.label}
// menuIsOpen={true}
/>
</div>
);
......
import React, { Component } from 'react';
import Select from 'react-select';
import AsyncSelect from 'react-select/lib/Async';
import PickerOption from './PickerOption';
import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { User } from 'app/types';
import ResetStyles from './ResetStyles';
import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage';
export interface Props {
onSelected: (user: User) => void;
value?: string;
className?: string;
}
......@@ -31,20 +33,17 @@ export class UserPicker extends Component<Props, State> {
search(query?: string) {
const backendSrv = getBackendSrv();
this.setState({ isLoading: true });
return backendSrv
.get(`/api/org/users?query=${query}&limit=10`)
.get(`/api/org/users?query=${query}&limit=1`)
.then(result => {
return {
options: result.map(user => ({
id: user.userId,
label: `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl,
login: user.login,
})),
};
return result.map(user => ({
id: user.userId,
label: `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl,
login: user.login,
}));
})
.finally(() => {
this.setState({ isLoading: false });
......@@ -52,26 +51,31 @@ export class UserPicker extends Component<Props, State> {
}
render() {
const { value, className } = this.props;
const { className, onSelected } = this.props;
const { isLoading } = this.state;
return (
<div className="user-picker">
<Select.Async
valueKey="id"
multi={false}
labelKey="label"
cache={false}
<AsyncSelect
classNamePrefix={`gf-form-select2`}
isMulti={false}
isLoading={isLoading}
defaultOptions={true}
loadOptions={this.debouncedSearch}
loadingPlaceholder="Loading..."
noResultsText="No users found"
onChange={this.props.onSelected}
onChange={onSelected}
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
optionComponent={PickerOption}
styles={ResetStyles}
components={{
Option: PickerOption,
IndicatorsContainer,
NoOptionsMessage,
}}
placeholder="Select user"
value={value}
autosize={true}
loadingMessage={() => 'Loading...'}
noOptionsMessage={() => 'No users found'}
getOptionValue={i => i.id}
getOptionLabel={i => i.label}
// menuIsOpen={true}
/>
</div>
);
......
import React from 'react';
import { components } from 'react-select';
export const ValueContainer = props => {
const { children, getValue, options } = props;
console.log('getValue', getValue());
console.log('options', options);
const existingValue = getValue();
const selectedOption = options.find(i => (existingValue[0] ? i.id === existingValue[0].id : undefined));
console.log('selectedOption', selectedOption);
return (
<components.ValueContainer {...props}>
{children}
{/* {selectedOption ?
<span>{selectedOption.label}</span>
: children} */}
</components.ValueContainer>
);
};
export default ValueContainer;
......@@ -85,8 +85,8 @@ export class TeamMembers extends PureComponent<Props, State> {
render() {
const { newTeamMember, isAdding } = this.state;
const { searchMemberQuery, members, syncEnabled } = this.props;
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
const newTeamMemberValue = newTeamMember && newTeamMember;
console.log('newTeamMemberValue', newTeamMemberValue);
return (
<div>
<div className="page-action-bar">
......@@ -117,8 +117,7 @@ export class TeamMembers extends PureComponent<Props, State> {
</button>
<h5>Add Team Member</h5>
<div className="gf-form-inline">
<UserPicker onSelected={this.onUserSelected} className="width-30" value={newTeamMemberValue} />
<UserPicker onSelected={this.onUserSelected} className="width-30" />
{this.state.newTeamMember && (
<button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
Add to team
......
......@@ -13,7 +13,7 @@ $select-text-color: $text-color;
$select-input-bg-disabled: $input-bg-disabled;
$select-option-selected-bg: $dropdownLinkBackgroundActive;
@import '../../../node_modules/react-select/scss/default.scss';
// @import '../../../node_modules/react-select/scss/default.scss';
@mixin select-control() {
width: 100%;
......@@ -29,6 +29,96 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive;
@include box-shadow($shadow);
}
// new react-select WIP
.gf-form-select2__control {
@include select-control();
border-color: $dark-3;
border: 1px solid #262628;
color: #d8d9da;
cursor: default;
display: table;
border-spacing: 0;
border-collapse: separate;
height: $select-input-height;
outline: none;
overflow: hidden;
position: relative;
}
.gf-form-select2__control--is-focused {
background-color: $input-bg;
@include select-control-focus();
}
.gf-form-select2__control--is-disabled {
background-color: $select-input-bg-disabled;
}
.gf-form-select2__control--menu-right {
.gf-form-select2__menu {
right: 0;
left: unset;
}
}
.gf-form-select2__input {
position: absolute;
z-index: 1;
top: 0;
left: 0;
right: 0;
padding: 8px 10px;
}
.gf-form-select2__menu {
background: $select-input-bg-disabled;
position: absolute;
z-index: 2;
}
.gf-form-select2__option {
border-left: 2px solid transparent;
&.gf-form-select2__option--is-focused,
&.gf-form-select2__option--is-selected {
background-color: $dropdownLinkBackgroundHover;
color: $dropdownLinkColorHover;
@include left-brand-border-gradient();
.fa {
color: white;
}
}
}
.gf-form-select2__value-container {
display: table-cell;
padding: 8px 10px;
}
.gf-form-select2__indicators {
display: table-cell;
vertical-align: middle;
padding-right: 5px;
width: 25px;
}
.gf-form-select2__select-arrow {
border-color: #999 transparent transparent;
border-style: solid;
border-width: 5px 5px 2.5px;
display: inline-block;
height: 0;
width: 0;
position: relative;
&.gf-form-select2__select-arrow--reversed {
border-color: transparent transparent #999;
top: -2px;
border-width: 0 5px 5px;
}
}
// react-select tweaks
.gf-form-input--form-dropdown {
padding: 0;
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment