Browse Source

wip: Upgrade react-select #13425

Johannes Schill 7 years ago
parent
commit
d9434ba1b1

+ 2 - 1
package.json

@@ -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",

+ 9 - 14
public/app/core/components/PermissionList/AddPermission.tsx

@@ -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}
 

+ 1 - 1
public/app/core/components/PermissionList/PermissionListItem.tsx

@@ -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>

+ 19 - 48
public/app/core/components/Picker/DescriptionOption.tsx

@@ -1,56 +1,27 @@
-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;

+ 23 - 19
public/app/core/components/Picker/DescriptionPicker.tsx

@@ -1,44 +1,48 @@
 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>
     );

+ 13 - 0
public/app/core/components/Picker/IndicatorsContainer.tsx

@@ -0,0 +1,13 @@
+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;

+ 18 - 0
public/app/core/components/Picker/NoOptionsMessage.tsx

@@ -0,0 +1,18 @@
+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;

+ 19 - 48
public/app/core/components/Picker/PickerOption.tsx

@@ -1,54 +1,25 @@
-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;

+ 23 - 0
public/app/core/components/Picker/ResetStyles.tsx

@@ -0,0 +1,23 @@
+export default {
+  clearIndicator: () => ({}),
+  container: () => ({}),
+  control: () => ({}),
+  dropdownIndicator: () => ({}),
+  group: () => ({}),
+  groupHeading: () => ({}),
+  indicatorsContainer: () => ({}),
+  indicatorSeparator: () => ({}),
+  input: () => ({}),
+  loadingIndicator: () => ({}),
+  loadingMessage: () => ({}),
+  menu: () => ({}),
+  menuList: () => ({}),
+  multiValue: () => ({}),
+  multiValueLabel: () => ({}),
+  multiValueRemove: () => ({}),
+  noOptionsMessage: () => ({}),
+  option: () => ({}),
+  placeholder: () => ({}),
+  singleValue: () => ({}),
+  valueContainer: () => ({}),
+};

+ 30 - 24
public/app/core/components/Picker/TeamPicker.tsx

@@ -1,24 +1,26 @@
 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>
     );

+ 28 - 24
public/app/core/components/Picker/UserPicker.tsx

@@ -1,13 +1,15 @@
 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>
     );

+ 21 - 0
public/app/core/components/Picker/ValueContainer.tsx

@@ -0,0 +1,21 @@
+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;

+ 3 - 4
public/app/features/teams/TeamMembers.tsx

@@ -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

+ 91 - 1
public/sass/components/_form_select_box.scss

@@ -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;

File diff suppressed because it is too large
+ 249 - 0
yarn.lock


Some files were not shown because too many files changed in this diff