Commit d9434ba1 by Johannes Schill

wip: Upgrade react-select #13425

parent cecf8757
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
"@types/react": "^16.4.14", "@types/react": "^16.4.14",
"@types/react-custom-scrollbars": "^4.0.5", "@types/react-custom-scrollbars": "^4.0.5",
"@types/react-dom": "^16.0.7", "@types/react-dom": "^16.0.7",
"@types/react-select": "^2.0.4",
"angular-mocks": "1.6.6", "angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0", "autoprefixer": "^6.4.0",
"axios": "^0.17.1", "axios": "^0.17.1",
...@@ -157,7 +158,7 @@ ...@@ -157,7 +158,7 @@
"react-highlight-words": "^0.10.0", "react-highlight-words": "^0.10.0",
"react-popper": "^0.7.5", "react-popper": "^0.7.5",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"react-select": "^1.1.0", "react-select": "^2.0.0",
"react-sizeme": "^2.3.6", "react-sizeme": "^2.3.6",
"react-transition-group": "^2.2.1", "react-transition-group": "^2.2.1",
"redux": "^4.0.0", "redux": "^4.0.0",
......
...@@ -17,6 +17,10 @@ export interface Props { ...@@ -17,6 +17,10 @@ export interface Props {
onCancel: () => void; onCancel: () => void;
} }
export interface TeamSelectedAction {
action: string;
}
class AddPermissions extends Component<Props, NewDashboardAclItem> { class AddPermissions extends Component<Props, NewDashboardAclItem> {
constructor(props) { constructor(props) {
super(props); super(props);
...@@ -50,11 +54,11 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> { ...@@ -50,11 +54,11 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
}; };
onUserSelected = (user: User) => { onUserSelected = (user: User) => {
this.setState({ userId: user ? user.id : 0 }); this.setState({ userId: user && !Array.isArray(user) ? user.id : 0 });
}; };
onTeamSelected = (team: Team) => { onTeamSelected = (team: Team, info: TeamSelectedAction) => {
this.setState({ teamId: team ? team.id : 0 }); this.setState({ teamId: team && !Array.isArray(team) ? team.id : 0 });
}; };
onPermissionChanged = (permission: OptionWithDescription) => { onPermissionChanged = (permission: OptionWithDescription) => {
...@@ -82,7 +86,6 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> { ...@@ -82,7 +86,6 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
const newItem = this.state; const newItem = this.state;
const pickerClassName = 'width-20'; const pickerClassName = 'width-20';
const isValid = this.isValid(); const isValid = this.isValid();
return ( return (
<div className="gf-form-inline cta-form"> <div className="gf-form-inline cta-form">
<button className="cta-form__close btn btn-transparent" onClick={onCancel}> <button className="cta-form__close btn btn-transparent" onClick={onCancel}>
...@@ -107,21 +110,13 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> { ...@@ -107,21 +110,13 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
{newItem.type === AclTarget.User ? ( {newItem.type === AclTarget.User ? (
<div className="gf-form"> <div className="gf-form">
<UserPicker <UserPicker onSelected={this.onUserSelected} value={newItem.userId} className={pickerClassName} />
onSelected={this.onUserSelected}
value={newItem.userId.toString()}
className={pickerClassName}
/>
</div> </div>
) : null} ) : null}
{newItem.type === AclTarget.Team ? ( {newItem.type === AclTarget.Team ? (
<div className="gf-form"> <div className="gf-form">
<TeamPicker <TeamPicker onSelected={this.onTeamSelected} value={newItem.teamId} className={pickerClassName} />
onSelected={this.onTeamSelected}
value={newItem.teamId.toString()}
className={pickerClassName}
/>
</div> </div>
) : null} ) : null}
......
...@@ -79,7 +79,7 @@ export default class PermissionsListItem extends PureComponent<Props> { ...@@ -79,7 +79,7 @@ export default class PermissionsListItem extends PureComponent<Props> {
onSelected={this.onPermissionChanged} onSelected={this.onPermissionChanged}
value={item.permission} value={item.permission}
disabled={item.inherited} disabled={item.inherited}
className={'gf-form-input--form-dropdown-right'} className={'gf-form-select2__control--menu-right'}
/> />
</div> </div>
</td> </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 { export interface Props {
onSelect: any; children: Element;
onFocus: any; isSelected: boolean;
option: any; data: any;
isFocused: any; getStyles: any;
className: any;
} }
class DescriptionOption extends Component<Props, any> { export const Option = (props: OptionProps<any>) => {
constructor(props) { const { children, isSelected, data } = 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 ( return (
<button <components.Option {...props}>
onMouseDown={this.handleMouseDown} <div className={`description-picker-option__button btn btn-link width-19`}>
onMouseEnter={this.handleMouseEnter} {isSelected && <i className="fa fa-check pull-right" aria-hidden="true" />}
onMouseMove={this.handleMouseMove}
title={option.title}
className={`description-picker-option__button btn btn-link ${className} width-19`}
>
<div className="gf-form">{children}</div> <div className="gf-form">{children}</div>
<div className="gf-form"> <div className="gf-form">
<div className="muted width-17">{option.description}</div> <div className="muted width-17">{data.description}</div>
{className.indexOf('is-selected') > -1 && <i className="fa fa-check" aria-hidden="true" />} </div>
</div> </div>
</button> </components.Option>
); );
} };
}
export default DescriptionOption; export default Option;
import React, { Component } from 'react'; import React, { Component } from 'react';
import Select from 'react-select'; import Select from 'react-select';
import DescriptionOption from './DescriptionOption'; 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 { export interface Props {
optionsWithDesc: OptionWithDescription[]; optionsWithDesc: OptionWithDescription[];
onSelected: (permission) => void; onSelected: (permission) => void;
value: number;
disabled: boolean; disabled: boolean;
className?: string; className?: string;
} }
export interface OptionWithDescription {
value: any;
label: string;
description: string;
}
class DescriptionPicker extends Component<Props, any> { class DescriptionPicker extends Component<Props, any> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {};
} }
render() { render() {
const { optionsWithDesc, onSelected, value, disabled, className } = this.props; const { optionsWithDesc, onSelected, disabled, className } = this.props;
return ( return (
<div className="permissions-picker"> <div className="permissions-picker">
<Select <Select
value={value} placeholder="Choose"
valueKey="value" classNamePrefix={`gf-form-select2`}
multi={false} className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`}
clearable={false}
labelKey="label"
options={optionsWithDesc} options={optionsWithDesc}
components={{
Option: DescriptionOption,
IndicatorsContainer,
NoOptionsMessage,
}}
styles={ResetStyles}
isDisabled={disabled}
onChange={onSelected} onChange={onSelected}
className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`} getOptionValue={i => i.value}
optionComponent={DescriptionOption} getOptionLabel={i => i.label}
placeholder="Choose" // menuIsOpen={true} // debug
disabled={disabled}
/> />
</div> </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 { export interface Props {
onSelect: any; children: Element;
onFocus: any; isSelected: boolean;
option: any; data: any;
isFocused: any; getStyles: any;
className: any; className?: string;
} }
class UserPickerOption extends Component<Props, any> { export const PickerOption = (props: OptionProps<any>) => {
constructor(props) { const { children, data } = 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 ( return (
<button <components.Option {...props}>
onMouseDown={this.handleMouseDown} <div className={`description-picker-option__button btn btn-link width-19`}>
onMouseEnter={this.handleMouseEnter} <img src={data.avatarUrl} alt={data.label} className="user-picker-option__avatar" />
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" />
{children} {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 React, { Component } from 'react';
import Select from 'react-select'; import AsyncSelect from 'react-select/lib/Async';
import PickerOption from './PickerOption'; import PickerOption from './PickerOption';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv'; 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 { export interface Props {
onSelected: (team: Team) => void; onSelected: (team: Team) => void;
value?: string;
className?: string; className?: string;
} }
export interface State { export interface State {
isLoading; isLoading: boolean;
}
export interface Team {
id: number;
label: string;
name: string;
avatarUrl: string;
} }
export class TeamPicker extends Component<Props, State> { export class TeamPicker extends Component<Props, State> {
...@@ -39,7 +41,7 @@ export class TeamPicker extends Component<Props, State> { ...@@ -39,7 +41,7 @@ export class TeamPicker extends Component<Props, State> {
const backendSrv = getBackendSrv(); const backendSrv = getBackendSrv();
this.setState({ isLoading: true }); 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 => { const teams = result.teams.map(team => {
return { return {
id: team.id, id: team.id,
...@@ -50,31 +52,35 @@ export class TeamPicker extends Component<Props, State> { ...@@ -50,31 +52,35 @@ export class TeamPicker extends Component<Props, State> {
}); });
this.setState({ isLoading: false }); this.setState({ isLoading: false });
return { options: teams }; return teams;
}); });
} }
render() { render() {
const { onSelected, value, className } = this.props; const { onSelected, className } = this.props;
const { isLoading } = this.state; const { isLoading } = this.state;
return ( return (
<div className="user-picker"> <div className="user-picker">
<Select.Async <AsyncSelect
valueKey="id" classNamePrefix={`gf-form-select2`}
multi={false} isMulti={false}
labelKey="label"
cache={false}
isLoading={isLoading} isLoading={isLoading}
defaultOptions={true}
loadOptions={this.debouncedSearch} loadOptions={this.debouncedSearch}
loadingPlaceholder="Loading..."
noResultsText="No teams found"
onChange={onSelected} onChange={onSelected}
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
optionComponent={PickerOption} styles={ResetStyles}
components={{
Option: PickerOption,
IndicatorsContainer,
NoOptionsMessage,
}}
placeholder="Select a team" placeholder="Select a team"
value={value} loadingMessage={() => 'Loading...'}
autosize={true} noOptionsMessage={() => 'No teams found'}
getOptionValue={i => i.id}
getOptionLabel={i => i.label}
// menuIsOpen={true}
/> />
</div> </div>
); );
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import Select from 'react-select'; import AsyncSelect from 'react-select/lib/Async';
import PickerOption from './PickerOption'; import PickerOption from './PickerOption';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { User } from 'app/types'; import { User } from 'app/types';
import ResetStyles from './ResetStyles';
import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage';
export interface Props { export interface Props {
onSelected: (user: User) => void; onSelected: (user: User) => void;
value?: string;
className?: string; className?: string;
} }
...@@ -31,20 +33,17 @@ export class UserPicker extends Component<Props, State> { ...@@ -31,20 +33,17 @@ export class UserPicker extends Component<Props, State> {
search(query?: string) { search(query?: string) {
const backendSrv = getBackendSrv(); const backendSrv = getBackendSrv();
this.setState({ isLoading: true }); this.setState({ isLoading: true });
return backendSrv return backendSrv
.get(`/api/org/users?query=${query}&limit=10`) .get(`/api/org/users?query=${query}&limit=1`)
.then(result => { .then(result => {
return { return result.map(user => ({
options: result.map(user => ({
id: user.userId, id: user.userId,
label: `${user.login} - ${user.email}`, label: `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
login: user.login, login: user.login,
})), }));
};
}) })
.finally(() => { .finally(() => {
this.setState({ isLoading: false }); this.setState({ isLoading: false });
...@@ -52,26 +51,31 @@ export class UserPicker extends Component<Props, State> { ...@@ -52,26 +51,31 @@ export class UserPicker extends Component<Props, State> {
} }
render() { render() {
const { value, className } = this.props; const { className, onSelected } = this.props;
const { isLoading } = this.state; const { isLoading } = this.state;
return ( return (
<div className="user-picker"> <div className="user-picker">
<Select.Async <AsyncSelect
valueKey="id" classNamePrefix={`gf-form-select2`}
multi={false} isMulti={false}
labelKey="label"
cache={false}
isLoading={isLoading} isLoading={isLoading}
defaultOptions={true}
loadOptions={this.debouncedSearch} loadOptions={this.debouncedSearch}
loadingPlaceholder="Loading..." onChange={onSelected}
noResultsText="No users found"
onChange={this.props.onSelected}
className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
optionComponent={PickerOption} styles={ResetStyles}
components={{
Option: PickerOption,
IndicatorsContainer,
NoOptionsMessage,
}}
placeholder="Select user" placeholder="Select user"
value={value} loadingMessage={() => 'Loading...'}
autosize={true} noOptionsMessage={() => 'No users found'}
getOptionValue={i => i.id}
getOptionLabel={i => i.label}
// menuIsOpen={true}
/> />
</div> </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> { ...@@ -85,8 +85,8 @@ export class TeamMembers extends PureComponent<Props, State> {
render() { render() {
const { newTeamMember, isAdding } = this.state; const { newTeamMember, isAdding } = this.state;
const { searchMemberQuery, members, syncEnabled } = this.props; const { searchMemberQuery, members, syncEnabled } = this.props;
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); const newTeamMemberValue = newTeamMember && newTeamMember;
console.log('newTeamMemberValue', newTeamMemberValue);
return ( return (
<div> <div>
<div className="page-action-bar"> <div className="page-action-bar">
...@@ -117,8 +117,7 @@ export class TeamMembers extends PureComponent<Props, State> { ...@@ -117,8 +117,7 @@ export class TeamMembers extends PureComponent<Props, State> {
</button> </button>
<h5>Add Team Member</h5> <h5>Add Team Member</h5>
<div className="gf-form-inline"> <div className="gf-form-inline">
<UserPicker onSelected={this.onUserSelected} className="width-30" value={newTeamMemberValue} /> <UserPicker onSelected={this.onUserSelected} className="width-30" />
{this.state.newTeamMember && ( {this.state.newTeamMember && (
<button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}> <button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
Add to team Add to team
......
...@@ -13,7 +13,7 @@ $select-text-color: $text-color; ...@@ -13,7 +13,7 @@ $select-text-color: $text-color;
$select-input-bg-disabled: $input-bg-disabled; $select-input-bg-disabled: $input-bg-disabled;
$select-option-selected-bg: $dropdownLinkBackgroundActive; $select-option-selected-bg: $dropdownLinkBackgroundActive;
@import '../../../node_modules/react-select/scss/default.scss'; // @import '../../../node_modules/react-select/scss/default.scss';
@mixin select-control() { @mixin select-control() {
width: 100%; width: 100%;
...@@ -29,6 +29,96 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive; ...@@ -29,6 +29,96 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive;
@include box-shadow($shadow); @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 // react-select tweaks
.gf-form-input--form-dropdown { .gf-form-input--form-dropdown {
padding: 0; 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