--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+//@flow
+import React from 'react';
+import MembersListItem from './MembersListItem';
+import type { Member } from '../../../store/organizationsMembers/actions';
+import type { Organization } from '../../../store/organizations/duck';
+
+type Props = {
+ members: Array<Member>,
+ organization?: Organization
+};
+
+export default class MembersList extends React.PureComponent {
+ props: Props;
+
+ render() {
+ return (
+ <table className="data zebra">
+ <tbody>
+ {this.props.members.map(member => (
+ <MembersListItem key={member.login} member={member} organization={this.props.organization} />
+ ))}
+ </tbody>
+ </table>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+//@flow
+import React from 'react';
+import Avatar from '../../../components/ui/Avatar';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import type { Member } from '../../../store/organizationsMembers/actions';
+import type { Organization } from '../../../store/organizations/duck';
+
+type Props = {
+ member: Member,
+ organization: Organization
+};
+
+const AVATAR_SIZE: number = 36;
+
+export default class MembersListItem extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { member, organization } = this.props;
+ return (
+ <tr>
+ <td className="thin nowrap">
+ <Avatar hash={member.avatar} size={AVATAR_SIZE} />
+ </td>
+ <td className="nowrap text-middle"><strong>{member.name}</strong></td>
+ {organization.canAdmin &&
+ <td className="text-right text-middle">
+ {translate('organization.members.x_group(s)', formatMeasure(member.groupCount, 'INT'))}
+ </td>
+ }
+ </tr>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import PageHeader from './PageHeader';
+import MembersList from './MembersList';
+import UsersSearch from '../../users/components/UsersSearch';
+import ListFooter from '../../../components/controls/ListFooter';
+import type { Organization } from '../../../store/organizations/duck';
+import type { Member } from '../../../store/organizationsMembers/actions';
+
+type Props = {
+ members: Array<Member>,
+ status: { loading?: boolean, total?: number, pageIndex?: number, query?: string },
+ organization: Organization,
+ fetchOrganizationMembers: (organizationKey: string, query?: string) => void,
+ fetchMoreOrganizationMembers: (organizationKey: string, query?: string) => void,
+};
+
+export default class OrganizationMembers extends React.PureComponent {
+ props: Props;
+
+ componentDidMount() {
+ const notLoadedYet = this.props.members.length < 1 || this.props.status.query != null;
+ if (!this.props.loading && notLoadedYet) {
+ this.handleSearchMembers();
+ }
+ }
+
+ handleSearchMembers = (query?: string) => {
+ this.props.fetchOrganizationMembers(this.props.organization.key, query);
+ };
+
+ handleLoadMoreMembers = () => {
+ this.props.fetchMoreOrganizationMembers(this.props.organization.key, this.props.status.query);
+ };
+
+ addMember() {
+ // TODO Not yet implemented
+ }
+
+ render() {
+ const { organization, status, members } = this.props;
+ return (
+ <div className="page page-limited">
+ <PageHeader loading={status.loading} total={status.total} />
+ <UsersSearch onSearch={this.handleSearchMembers} />
+ <MembersList members={members} organization={organization} />
+ {status.total != null &&
+ <ListFooter
+ count={members.length}
+ total={status.total}
+ ready={!status.loading}
+ loadMore={this.handleLoadMoreMembers}
+ />}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+import OrganizationMembers from './OrganizationMembers';
+import {
+ getOrganizationByKey,
+ getOrganizationMembersLogins,
+ getUsersByLogins,
+ getOrganizationMembersState
+} from '../../../store/rootReducer';
+import { fetchOrganizationMembers, fetchMoreOrganizationMembers } from '../actions';
+
+const mapStateToProps = (state, ownProps) => {
+ const { organizationKey } = ownProps.params;
+ const memberLogins = getOrganizationMembersLogins(state, organizationKey);
+ return {
+ members: getUsersByLogins(state, memberLogins),
+ organization: getOrganizationByKey(state, organizationKey),
+ status: getOrganizationMembersState(state, organizationKey)
+ };
+};
+
+export default connect(mapStateToProps, { fetchOrganizationMembers, fetchMoreOrganizationMembers })(
+ OrganizationMembers
+);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+//@flow
+import React from 'react';
+import { formatMeasure } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {
+ loading: boolean,
+ total?: number,
+ children?: {}
+};
+
+export default class PageHeader extends React.PureComponent {
+ props: Props;
+
+ render() {
+ return (
+ <header className="page-header">
+ {this.props.loading && <i className="spinner" />}
+ {this.props.children}
+ {this.props.total != null &&
+ <span className="page-totalcount">
+ <strong>{formatMeasure(this.props.total, 'INT')}</strong>
+ {' '}
+ {translate('organization.members.member(s)')}
+ </span>}
+ </header>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import MembersList from '../MembersList';
+
+const organization = { key: 'foo', name: 'Foo' };
+const members = [
+ { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
+ { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }
+];
+
+it('should render a list of members of an organization', () => {
+ const wrapper = shallow(
+ <MembersList
+ organization={organization}
+ members={members}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import MembersListItem from '../MembersListItem';
+
+const organization = { key: 'foo', name: 'Foo' };
+const admin = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
+const john = { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 };
+
+it('should not render actions and groups for non admin', () => {
+ const wrapper = shallow(
+ <MembersListItem
+ organization={organization}
+ member={john}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render actions and groups for admin', () => {
+ const wrapper = shallow(
+ <MembersListItem
+ organization={{ ...organization, canAdmin: true }}
+ member={admin}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import OrganizationMembers from '../OrganizationMembers';
+
+const organization = { key: 'foo', name: 'Foo' };
+const members = [
+ { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
+ { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }
+];
+const status = { total: members.length };
+const fetchOrganizationMembers = jest.fn();
+const fetchMoreOrganizationMembers = jest.fn();
+
+it('should not render actions for non admin', () => {
+ const wrapper = shallow(
+ <OrganizationMembers
+ organization={organization}
+ members={members}
+ status={status}
+ fetchOrganizationMembers={fetchOrganizationMembers}
+ fetchMoreOrganizationMembers={fetchMoreOrganizationMembers}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render actions for admin', () => {
+ const wrapper = shallow(
+ <OrganizationMembers
+ organization={{ ...organization, canAdmin: true }}
+ members={members}
+ status={{ ...status, loading: true }}
+ fetchOrganizationMembers={fetchOrganizationMembers}
+ fetchMoreOrganizationMembers={fetchMoreOrganizationMembers}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import PageHeader from '../PageHeader';
+
+it('should render the members page header', () => {
+ const wrapper = shallow(
+ <PageHeader />
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ loading: true });
+ expect(wrapper.find('.spinner')).toMatchSnapshot();
+});
+
+it('should render the members page header with the total', () => {
+ const wrapper = shallow(<PageHeader total="5" />);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render its children', () => {
+ const wrapper = shallow(
+ <PageHeader loading={true} total="5">
+ <span>children test</span>
+ </PageHeader>
+ );
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+exports[`test should render a list of members of an organization 1`] = `
+<table
+ className="data zebra">
+ <tbody>
+ <MembersListItem
+ member={
+ Object {
+ "avatar": "",
+ "groupCount": 3,
+ "login": "admin",
+ "name": "Admin Istrator",
+ }
+ }
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ } />
+ <MembersListItem
+ member={
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "groupCount": 1,
+ "login": "john",
+ "name": "John Doe",
+ }
+ }
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ } />
+ </tbody>
+</table>
+`;
--- /dev/null
+exports[`test should not render actions and groups for non admin 1`] = `
+<tr>
+ <td
+ className="thin nowrap">
+ <Connect(Avatar)
+ hash="7daf6c79d4802916d83f6266e24850af"
+ size={36} />
+ </td>
+ <td
+ className="nowrap text-middle">
+ <strong>
+ John Doe
+ </strong>
+ </td>
+</tr>
+`;
+
+exports[`test should render actions and groups for admin 1`] = `
+<tr>
+ <td
+ className="thin nowrap">
+ <Connect(Avatar)
+ hash=""
+ size={36} />
+ </td>
+ <td
+ className="nowrap text-middle">
+ <strong>
+ Admin Istrator
+ </strong>
+ </td>
+ <td
+ className="text-right text-middle">
+ organization.members.x_group(s).3
+ </td>
+</tr>
+`;
--- /dev/null
+exports[`test should not render actions for non admin 1`] = `
+<div
+ className="page page-limited">
+ <PageHeader
+ total={2} />
+ <UsersSearch
+ onSearch={[Function]} />
+ <MembersList
+ members={
+ Array [
+ Object {
+ "avatar": "",
+ "groupCount": 3,
+ "login": "admin",
+ "name": "Admin Istrator",
+ },
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "groupCount": 1,
+ "login": "john",
+ "name": "John Doe",
+ },
+ ]
+ }
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ } />
+ <ListFooter
+ count={2}
+ loadMore={[Function]}
+ ready={true}
+ total={2} />
+</div>
+`;
+
+exports[`test should render actions for admin 1`] = `
+<div
+ className="page page-limited">
+ <PageHeader
+ loading={true}
+ total={2} />
+ <UsersSearch
+ onSearch={[Function]} />
+ <MembersList
+ members={
+ Array [
+ Object {
+ "avatar": "",
+ "groupCount": 3,
+ "login": "admin",
+ "name": "Admin Istrator",
+ },
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "groupCount": 1,
+ "login": "john",
+ "name": "John Doe",
+ },
+ ]
+ }
+ organization={
+ Object {
+ "canAdmin": true,
+ "key": "foo",
+ "name": "Foo",
+ }
+ } />
+ <ListFooter
+ count={2}
+ loadMore={[Function]}
+ ready={false}
+ total={2} />
+</div>
+`;
--- /dev/null
+exports[`test should render its children 1`] = `
+<header
+ className="page-header">
+ <i
+ className="spinner" />
+ <span>
+ children test
+ </span>
+ <span
+ className="page-totalcount">
+ <strong>
+ 5
+ </strong>
+
+ organization.members.member(s)
+ </span>
+</header>
+`;
+
+exports[`test should render the members page header 1`] = `
+<header
+ className="page-header" />
+`;
+
+exports[`test should render the members page header 2`] = `
+<i
+ className="spinner" />
+`;
+
+exports[`test should render the members page header with the total 1`] = `
+<header
+ className="page-header">
+ <span
+ className="page-totalcount">
+ <strong>
+ 5
+ </strong>
+
+ organization.members.member(s)
+ </span>
+</header>
+`;
{translate('projects.page')}
</Link>
</li>
+ <li>
+ <Link to={`/organizations/${organization.key}/members`} activeClassName="active">
+ {translate('organization.members.page')}
+ </Link>
+ </li>
{organization.canAdmin && this.renderAdministration()}
</ul>
</div>
projects.page
</Link>
</li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/members">
+ organization.members.page
+ </Link>
+ </li>
<li
className="">
<a
projects.page
</Link>
</li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/members">
+ organization.members.page
+ </Link>
+ </li>
</ul>
</div>
</div>
projects.page
</Link>
</li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/members">
+ organization.members.page
+ </Link>
+ </li>
<li
className="">
<a
import OrganizationAdmin from './components/OrganizationAdmin';
import OrganizationEdit from './components/OrganizationEdit';
import OrganizationGroups from './components/OrganizationGroups';
+import OrganizationMembersContainer from './components/OrganizationMembersContainer';
import OrganizationPermissions from './components/OrganizationPermissions';
import OrganizationPermissionTemplates from './components/OrganizationPermissionTemplates';
import OrganizationProjectsManagement from './components/OrganizationProjectsManagement';
path: 'projects/favorite',
component: OrganizationFavoriteProjects
},
+ {
+ path: 'members',
+ component: OrganizationMembersContainer
+ },
{
component: OrganizationAdmin,
childRoutes: [
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+//@flow
+import React from 'react';
+import { debounce } from 'lodash';
+import classNames from 'classnames';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+type Props = {
+ onSearch: (query?: string) => void
+};
+
+type State = {
+ query?: string
+};
+
+export default class UsersSearch extends React.PureComponent {
+ props: Props;
+ state: State = {
+ query: ''
+ }
+
+ constructor(props: Props) {
+ super(props);
+ this.handleSearch = debounce(this.handleSearch, 250);
+ }
+
+ handleSearch = (query: string) => {
+ this.props.onSearch(query);
+ }
+
+ handleInputChange = ({ target }: { target: HTMLInputElement }) => {
+ this.setState({ query: target.value });
+ if (!target.value || target.value.length >= 2) {
+ this.handleSearch(target.value);
+ }
+ };
+
+ render() {
+ const { query } = this.state;
+ const inputClassName = classNames('search-box-input', {
+ touched: query != null && query !== '' && query.length < 2
+ });
+ return (
+ <div className="panel panel-vertical bordered-bottom spacer-bottom">
+ <div className="search-box">
+ <button className="search-box-submit button-clean">
+ <i className="icon-search" />
+ </button>
+ <input
+ type="search"
+ value={query}
+ className={inputClassName}
+ placeholder={translate('search_verb')}
+ onChange={this.handleInputChange}
+ autoComplete="off"
+ />
+ <span className="note spacer-left text-middle">
+ {translateWithParameters('select2.tooShort', 2)}
+ </span>
+ </div>
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import UsersSearch from '../UsersSearch';
+
+const onSearch = jest.fn();
+
+it('should render correctly without any search query', () => {
+ const wrapper = shallow(<UsersSearch onSearch={onSearch} query={null} />);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render with a search query', () => {
+ const wrapper = shallow(<UsersSearch onSearch={onSearch} query={'foo'} />);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should display a help message when there is less than 2 characters', () => {
+ const wrapper = shallow(<UsersSearch onSearch={onSearch} query={'a'} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ query: 'foo' });
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+exports[`test should display a help message when there is less than 2 characters 1`] = `
+<div
+ className="panel panel-vertical bordered-bottom spacer-bottom">
+ <div
+ className="search-box">
+ <button
+ className="search-box-submit button-clean">
+ <i
+ className="icon-search" />
+ </button>
+ <input
+ autoComplete="off"
+ className="search-box-input"
+ onChange={[Function]}
+ placeholder="search_verb"
+ type="search"
+ value="" />
+ <span
+ className="note spacer-left text-middle">
+ select2.tooShort.2
+ </span>
+ </div>
+</div>
+`;
+
+exports[`test should display a help message when there is less than 2 characters 2`] = `
+<div
+ className="panel panel-vertical bordered-bottom spacer-bottom">
+ <div
+ className="search-box">
+ <button
+ className="search-box-submit button-clean">
+ <i
+ className="icon-search" />
+ </button>
+ <input
+ autoComplete="off"
+ className="search-box-input"
+ onChange={[Function]}
+ placeholder="search_verb"
+ type="search"
+ value="foo" />
+ <span
+ className="note spacer-left text-middle">
+ select2.tooShort.2
+ </span>
+ </div>
+</div>
+`;
+
+exports[`test should render correctly without any search query 1`] = `
+<div
+ className="panel panel-vertical bordered-bottom spacer-bottom">
+ <div
+ className="search-box">
+ <button
+ className="search-box-submit button-clean">
+ <i
+ className="icon-search" />
+ </button>
+ <input
+ autoComplete="off"
+ className="search-box-input"
+ onChange={[Function]}
+ placeholder="search_verb"
+ type="search"
+ value="" />
+ <span
+ className="note spacer-left text-middle">
+ select2.tooShort.2
+ </span>
+ </div>
+</div>
+`;
+
+exports[`test should render with a search query 1`] = `
+<div
+ className="panel panel-vertical bordered-bottom spacer-bottom">
+ <div
+ className="search-box">
+ <button
+ className="search-box-submit button-clean">
+ <i
+ className="icon-search" />
+ </button>
+ <input
+ autoComplete="off"
+ className="search-box-input"
+ onChange={[Function]}
+ placeholder="search_verb"
+ type="search"
+ value="" />
+ <span
+ className="note spacer-left text-middle">
+ select2.tooShort.2
+ </span>
+ </div>
+</div>
+`;
import ListFooter from '../ListFooter';
import { click } from '../../../helpers/testUtils';
+const loadMore = jest.fn();
+
it('should render "3 of 5 shown"', () => {
const listFooter = shallow(<ListFooter count={3} total={5} />);
expect(listFooter.text()).toContain('x_of_y_shown.3.5');
expect(listFooter.find('a').length).toBe(0);
});
+it('should not render "show more"', () => {
+ const listFooter = shallow(<ListFooter count={5} total={5} loadMore={loadMore} />);
+ expect(listFooter.find('a').length).toBe(0);
+});
+
it('should "show more"', () => {
- const loadMore = jest.fn();
const listFooter = shallow(<ListFooter count={3} total={5} loadMore={loadMore} />);
const link = listFooter.find('a');
expect(link.length).toBe(1);
enableGravatar: React.PropTypes.bool.isRequired,
gravatarServerUrl: React.PropTypes.string.isRequired,
email: React.PropTypes.string,
+ hash: React.PropTypes.string,
size: React.PropTypes.number.isRequired,
className: React.PropTypes.string
};
return null;
}
- const emailHash = md5.md5((this.props.email || '').toLowerCase()).trim();
+ const emailHash = this.props.hash || md5.md5((this.props.email || '').toLowerCase()).trim();
const url = this.props.gravatarServerUrl
.replace('{EMAIL_MD5}', emailHash)
.replace('{SIZE}', this.props.size * 2);
);
expect(avatar.is('img')).toBe(false);
});
+
+it('should be able to render with hash only', () => {
+ const avatar = shallow(
+ <Avatar
+ enableGravatar={true}
+ gravatarServerUrl={gravatarServerUrl}
+ hash="7daf6c79d4802916d83f6266e24850af"
+ size={30}
+ />
+ );
+ expect(avatar.is('img')).toBe(true);
+ expect(avatar.prop('width')).toBe(30);
+ expect(avatar.prop('height')).toBe(30);
+ expect(avatar.prop('alt')).toBeUndefined();
+ expect(avatar.prop('src')).toBe('http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=60');
+});
}
.page-header {
+ position: relative;
+
.clearfix;
margin-bottom: 20px;
top: 3px;
margin-left: 8px;
}
+
+ .page-totalcount {
+ position: absolute;
+ bottom: -50px;
+ right: 0;
+ }
}
.page-title {
width: 260px;
}
}
-}
\ No newline at end of file
+}
width: 250px;
border: none !important;
font-size: @baseFontSize;
+
+ & ~ .note {
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &.touched ~ .note {
+ opacity: 1;
+ }
}
.search-box-submit {
user.login_or_email_used_as_scm_account=Login and email are automatically considered as SCM accounts
user.password_cant_be_changed_on_external_auth=Password cannot be changed when external authentication is used
+#------------------------------------------------------------------------------
+#
+# USERS PAGE
+#
+#------------------------------------------------------------------------------
+users.create=Create User
#------------------------------------------------------------------------------
#
organization.updated=Organization details have been updated.
organization.url=Url
organization.url.description=Url of the homepage of the organization.
+organization.members.page=Members
+organization.members.add=Add member
+organization.members.x_group(s)={0} group(s)
+organization.members.member(s)=member(s)