Merge remote-tracking branch 'origin/10289_user_picker'

This commit is contained in:
Torkel Ödegaard
2018-01-10 13:14:43 +01:00
17 changed files with 456 additions and 35 deletions

View File

@@ -1,14 +1,16 @@
import { react2AngularDirective } from "app/core/utils/react2angular";
import { PasswordStrength } from "./components/PasswordStrength";
import PageHeader from "./components/PageHeader/PageHeader";
import EmptyListCTA from "./components/EmptyListCTA/EmptyListCTA";
import LoginBackground from "./components/Login/LoginBackground";
import { SearchResult } from "./components/search/SearchResult";
import { react2AngularDirective } from 'app/core/utils/react2angular';
import { PasswordStrength } from './components/PasswordStrength';
import PageHeader from './components/PageHeader/PageHeader';
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
import LoginBackground from './components/Login/LoginBackground';
import { SearchResult } from './components/search/SearchResult';
import UserPicker from './components/UserPicker/UserPicker';
export function registerAngularDirectives() {
react2AngularDirective("passwordStrength", PasswordStrength, ["password"]);
react2AngularDirective("pageHeader", PageHeader, ["model", "noTabs"]);
react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]);
react2AngularDirective("loginBackground", LoginBackground, []);
react2AngularDirective("searchResult", SearchResult, []);
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
react2AngularDirective('loginBackground', LoginBackground, []);
react2AngularDirective('searchResult', SearchResult, []);
react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'teamId', 'refreshList']);
}

View File

@@ -3,19 +3,18 @@ import renderer from 'react-test-renderer';
import EmptyListCTA from './EmptyListCTA';
const model = {
title: 'Title',
buttonIcon: 'ga css class',
buttonLink: 'http://url/to/destination',
buttonTitle: 'Click me',
proTip: 'This is a tip',
proTipLink: 'http://url/to/tip/destination',
proTipLinkTitle: 'Learn more',
proTipTarget: '_blank'
title: 'Title',
buttonIcon: 'ga css class',
buttonLink: 'http://url/to/destination',
buttonTitle: 'Click me',
proTip: 'This is a tip',
proTipLink: 'http://url/to/tip/destination',
proTipLinkTitle: 'Learn more',
proTipTarget: '_blank',
};
describe('CollorPalette', () => {
it('renders correctly', () => {
describe('EmptyListCTA', () => {
it('renders correctly', () => {
const tree = renderer.create(<EmptyListCTA model={model} />).toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollorPalette renders correctly 1`] = `
exports[`EmptyListCTA renders correctly 1`] = `
<div
className="empty-list-cta"
>

View File

@@ -0,0 +1,20 @@
import React from 'react';
import renderer from 'react-test-renderer';
import UserPicker from './UserPicker';
const model = {
backendSrv: {
get: () => {
return new Promise((resolve, reject) => {});
},
},
refreshList: () => {},
teamId: '1',
};
describe('UserPicker', () => {
it('renders correctly', () => {
const tree = renderer.create(<UserPicker {...model} />).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,108 @@
import React, { Component } from 'react';
import { debounce } from 'lodash';
import Select from 'react-select';
import UserPickerOption from './UserPickerOption';
export interface IProps {
backendSrv: any;
teamId: string;
refreshList: any;
}
export interface User {
id: number;
name: string;
login: string;
email: string;
}
class UserPicker extends Component<IProps, any> {
debouncedSearchUsers: any;
backendSrv: any;
teamId: string;
refreshList: any;
constructor(props) {
super(props);
this.backendSrv = this.props.backendSrv;
this.teamId = this.props.teamId;
this.refreshList = this.props.refreshList;
this.searchUsers = this.searchUsers.bind(this);
this.handleChange = this.handleChange.bind(this);
this.addUser = this.addUser.bind(this);
this.toggleLoading = this.toggleLoading.bind(this);
this.debouncedSearchUsers = debounce(this.searchUsers, 300, {
leading: true,
trailing: false,
});
this.state = {
multi: false,
isLoading: false,
};
}
handleChange(user) {
this.addUser(user.id);
}
toggleLoading(isLoading) {
this.setState(prevState => {
return {
...prevState,
isLoading: isLoading,
};
});
}
addUser(userId) {
this.toggleLoading(true);
this.backendSrv.post(`/api/teams/${this.teamId}/members`, { userId: userId }).then(() => {
this.refreshList();
this.toggleLoading(false);
});
}
searchUsers(query) {
this.toggleLoading(true);
return this.backendSrv.get(`/api/users/search?perpage=10&page=1&query=${query}`).then(result => {
const users = result.users.map(user => {
return {
id: user.id,
label: `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl,
};
});
this.toggleLoading(false);
return { options: users };
});
}
render() {
const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async;
return (
<div className="user-picker">
<AsyncComponent
valueKey="id"
multi={this.state.multi}
labelKey="label"
cache={false}
isLoading={this.state.isLoading}
loadOptions={this.debouncedSearchUsers}
loadingPlaceholder="Loading..."
noResultsText="No users found"
onChange={this.handleChange}
className="width-8 gf-form-input gf-form-input--form-dropdown"
optionComponent={UserPickerOption}
placeholder="Choose"
/>
</div>
);
}
}
export default UserPicker;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import renderer from 'react-test-renderer';
import UserPickerOption from './UserPickerOption';
const model = {
onSelect: () => {},
onFocus: () => {},
isFocused: () => {},
option: {
title: 'Model title',
avatarUrl: 'url/to/avatar',
label: 'User picker label',
},
className: 'class-for-user-picker',
};
describe('UserPickerOption', () => {
it('renders correctly', () => {
const tree = renderer.create(<UserPickerOption {...model} />).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,52 @@
import React, { Component } from 'react';
export interface IProps {
onSelect: any;
onFocus: any;
option: any;
isFocused: any;
className: any;
}
class UserPickerOption extends Component<IProps, 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" />
{children}
</button>
);
}
}
export default UserPickerOption;

View File

@@ -0,0 +1,98 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserPicker renders correctly 1`] = `
<div
className="user-picker"
>
<div
className="Select width-8 gf-form-input is-clearable is-loading is-searchable Select--single"
style={undefined}
>
<div
className="Select-control"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
style={undefined}
>
<span
className="Select-multi-value-wrapper"
id="react-select-2--value"
>
<div
className="Select-placeholder"
>
Loading...
</div>
<div
className="Select-input"
style={
Object {
"display": "inline-block",
}
}
>
<input
aria-activedescendant="react-select-2--value"
aria-describedby={undefined}
aria-expanded="false"
aria-haspopup="false"
aria-label={undefined}
aria-labelledby={undefined}
aria-owns=""
className={undefined}
id={undefined}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
required={false}
role="combobox"
style={
Object {
"boxSizing": "content-box",
"width": "5px",
}
}
tabIndex={undefined}
value=""
/>
<div
style={
Object {
"height": 0,
"left": 0,
"overflow": "scroll",
"position": "absolute",
"top": 0,
"visibility": "hidden",
"whiteSpace": "pre",
}
}
>
</div>
</div>
</span>
<span
aria-hidden="true"
className="Select-loading-zone"
>
<span
className="Select-loading"
/>
</span>
<span
className="Select-arrow-zone"
onMouseDown={[Function]}
>
<span
className="Select-arrow"
onMouseDown={[Function]}
/>
</span>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserPickerOption renders correctly 1`] = `
<button
className="user-picker-option__button btn btn-link class-for-user-picker"
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseMove={[Function]}
title="Model title"
>
<img
alt="User picker label"
className="user-picker-option__avatar"
src="url/to/avatar"
/>
</button>
`;

View File

@@ -26,11 +26,10 @@
<div class="gf-form-group">
<h3 class="page-heading">Team Members</h3>
<form name="ctrl.addMemberForm" class="gf-form-group">
<form name="ctrl.addMemberForm" class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-10">Add member</span>
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
<select-user-picker backendSrv="ctrl.backendSrv" teamId="ctrl.$routeParams.id" refreshList="ctrl.get" teamMembers="ctrl.teamMembers"></select-user-picker>
</div>
</form>

View File

@@ -8,6 +8,7 @@ export default class TeamDetailsCtrl {
/** @ngInject **/
constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
this.get = this.get.bind(this);
this.get();
}
@@ -35,7 +36,7 @@ export default class TeamDetailsCtrl {
}
removeMemberConfirmed(teamMember: TeamMember) {
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get.bind(this));
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get);
}
update() {