.navbar-context-favorite {
- float: left;
- padding: 7px 10px 0 0;
+ display: inline-block;
+ vertical-align: top;
+ padding-top: var(--gridSize);
+ padding-left: calc(1.5 * var(--gridSize));
}
.navbar-context-title-qualifier {
}
.navbar-context-branches {
- float: left;
- padding: 8px 0 6px;
- margin-left: 16px;
+ display: inline-block;
+ vertical-align: top;
+ padding: var(--gridSize) 0;
+ margin-left: calc(2 * var(--gridSize));
line-height: 16px;
}
import ComponentNavMenu from './ComponentNavMenu';
import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif';
import RecentHistory from '../../RecentHistory';
+import * as theme from '../../../theme';
import { Branch, Component } from '../../../types';
import ContextNavBar from '../../../../components/nav/ContextNavBar';
import { getTasksForComponent, PendingTask, Task } from '../../../../api/ce';
return (
<ContextNavBar
id="context-navigation"
- height={notifComponent ? 95 : 65}
+ height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
notif={notifComponent}>
- <ComponentNavFavorite
- component={this.props.component.key}
- favorite={this.props.component.isFavorite}
- />
<ComponentNavBreadcrumbs
component={this.props.component}
breadcrumbs={this.props.component.breadcrumbs}
/>
+ <ComponentNavFavorite
+ component={this.props.component.key}
+ favorite={this.props.component.isFavorite}
+ />
{this.props.currentBranch && (
<ComponentNavBranch
branches={this.props.branches}
import { Link } from 'react-router';
import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
+import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
import OrganizationHelmet from '../../../../components/common/OrganizationHelmet';
import OrganizationLink from '../../../../components/ui/OrganizationLink';
import PrivateBadge from '../../../../components/common/PrivateBadge';
import { collapsePath, limitComponentName } from '../../../../helpers/path';
+import { getProjectUrl } from '../../../../helpers/urls';
class ComponentNavBreadcrumbs extends React.PureComponent {
static propTypes = {
const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name);
return (
<span key={item.key}>
- {!displayOrganization &&
- index === 0 && (
- <span className="navbar-context-title-qualifier little-spacer-right">
- <QualifierIcon qualifier={lastItem.qualifier} />
- </span>
- )}
+ {index === 0 && (
+ <span className="navbar-context-title-qualifier spacer-right">
+ <QualifierIcon qualifier={lastItem.qualifier} />
+ </span>
+ )}
<Link
+ className="link-base-color link-no-underline"
title={item.name}
- to={{ pathname: '/dashboard', query: { id: item.key } }}
- className="link-base-color link-no-underline">
- {index === breadcrumbs.length - 1 ? (
- <strong>{itemName}</strong>
- ) : (
- <span>{itemName}</span>
- )}
+ to={getProjectUrl(item.key)}>
+ {itemName}
</Link>
{index < breadcrumbs.length - 1 && <span className="slash-separator" />}
</span>
/>
{displayOrganization && (
<span>
- <span className="navbar-context-title-qualifier little-spacer-right">
- <QualifierIcon qualifier={lastItem.qualifier} />
- </span>
+ <OrganizationAvatar organization={organization} />
<OrganizationLink
organization={organization}
- className="link-base-color link-no-underline">
+ className="link-base-color link-no-underline spacer-left">
{organization.name}
</OrganizationLink>
<span className="slash-separator" />
exports[`renders 1`] = `
<ContextNavBar
- height={95}
+ height={92}
id="context-navigation"
notif={
<ComponentNavBgTaskNotif
/>
}
>
- <ComponentNavFavorite
- component="component"
- />
<ComponentNavBreadcrumbs
breadcrumbs={
Array [
}
}
/>
+ <ComponentNavFavorite
+ component="component"
+ />
<ComponentNavMeta
component={
Object {
key="my-project"
>
<span
- className="navbar-context-title-qualifier little-spacer-right"
+ className="navbar-context-title-qualifier spacer-right"
>
<QualifierIcon
qualifier="TRK"
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "my-project",
},
}
}
>
- <strong>
- My Project
- </strong>
+ My Project
</Link>
</span>
</h1>
title="My Project"
/>
<span>
- <span
- className="navbar-context-title-qualifier little-spacer-right"
- >
- <QualifierIcon
- qualifier="TRK"
- />
- </span>
+ <OrganizationAvatar
+ organization={
+ Object {
+ "key": "foo",
+ "name": "The Foo Organization",
+ }
+ }
+ />
<OrganizationLink
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline spacer-left"
organization={
Object {
"key": "foo",
<span
key="my-project"
>
+ <span
+ className="navbar-context-title-qualifier spacer-right"
+ >
+ <QualifierIcon
+ qualifier="TRK"
+ />
+ </span>
<Link
className="link-base-color link-no-underline"
onlyActiveOnIndex={false}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "my-project",
},
}
}
>
- <strong>
- My Project
- </strong>
+ My Project
</Link>
</span>
</h1>
+++ /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 classNames from 'classnames';
-import { sortBy } from 'lodash';
-import { Link } from 'react-router';
-import * as theme from '../../../theme';
-import Avatar from '../../../../components/ui/Avatar';
-import OrganizationIcon from '../../../../components/icons-components/OrganizationIcon';
-import OrganizationLink from '../../../../components/ui/OrganizationLink';
-import { translate } from '../../../../helpers/l10n';
-
-/*::
-type CurrentUser = {
- avatar?: string,
- email?: string,
- isLoggedIn: boolean,
- name: string
-};
-*/
-
-/*::
-type Props = {
- appState: {
- organizationsEnabled: boolean
- },
- currentUser: CurrentUser,
- fetchMyOrganizations: () => Promise<*>,
- location: Object,
- organizations: Array<{ isAdmin: bool, key: string, name: string }>,
- router: { push: string => void }
-};
-*/
-
-/*::
-type State = {
- open: boolean
-};
-*/
-
-export default class GlobalNavUser extends React.PureComponent {
- /*:: node: HTMLElement; */
- /*:: props: Props; */
- state /*: State */ = { open: false };
-
- componentWillUnmount() {
- window.removeEventListener('click', this.handleClickOutside);
- }
-
- handleClickOutside = (event /*: { target: HTMLElement } */) => {
- if (!this.node || !this.node.contains(event.target)) {
- this.closeDropdown();
- }
- };
-
- handleLogin = (e /*: Event */) => {
- e.preventDefault();
- const shouldReturnToCurrentPage = window.location.pathname !== `${window.baseUrl}/about`;
- if (shouldReturnToCurrentPage) {
- const returnTo = encodeURIComponent(window.location.pathname + window.location.search);
- window.location =
- window.baseUrl + `/sessions/new?return_to=${returnTo}${window.location.hash}`;
- } else {
- window.location = `${window.baseUrl}/sessions/new`;
- }
- };
-
- handleLogout = (e /*: Event */) => {
- e.preventDefault();
- this.closeDropdown();
- this.props.router.push('/sessions/logout');
- };
-
- toggleDropdown = (evt /*: Event */) => {
- evt.preventDefault();
- if (this.state.open) {
- this.closeDropdown();
- } else {
- this.openDropdown();
- }
- };
-
- openDropdown = () => {
- this.fetchMyOrganizations().then(() => {
- window.addEventListener('click', this.handleClickOutside, true);
- this.setState({ open: true });
- });
- };
-
- closeDropdown = () => {
- window.removeEventListener('click', this.handleClickOutside);
- this.setState({ open: false });
- };
-
- fetchMyOrganizations = () => {
- if (this.props.appState.organizationsEnabled) {
- return this.props.fetchMyOrganizations();
- }
- return Promise.resolve();
- };
-
- renderAuthenticated() {
- const { currentUser, organizations } = this.props;
- const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0;
- return (
- <li
- className={classNames('dropdown js-user-authenticated', { open: this.state.open })}
- ref={node => (this.node = node)}>
- <a className="dropdown-toggle navbar-avatar" href="#" onClick={this.toggleDropdown}>
- <Avatar
- hash={currentUser.avatar}
- name={currentUser.name}
- size={theme.globalNavContentHeightRaw}
- />
- </a>
- {this.state.open && (
- <ul className="dropdown-menu dropdown-menu-right">
- <li className="dropdown-item">
- <div className="text-ellipsis text-muted" title={currentUser.name}>
- <strong>{currentUser.name}</strong>
- </div>
- {currentUser.email != null && (
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title={currentUser.email}>
- {currentUser.email}
- </div>
- )}
- </li>
- <li className="divider" />
- <li>
- <Link to="/account" onClick={this.closeDropdown}>
- {translate('my_account.page')}
- </Link>
- </li>
- {hasOrganizations && <li role="separator" className="divider" />}
- {hasOrganizations && (
- <li>
- <Link to="/account/organizations" onClick={this.closeDropdown}>
- {translate('my_organizations')}
- </Link>
- </li>
- )}
- {hasOrganizations &&
- sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
- <li key={organization.key}>
- <OrganizationLink
- className="dropdown-item-flex"
- organization={organization}
- onClick={this.closeDropdown}>
- <div>
- <OrganizationIcon />
- <span className="spacer-left">{organization.name}</span>
- </div>
- {organization.isAdmin && (
- <span className="outline-badge spacer-left">{translate('admin')}</span>
- )}
- </OrganizationLink>
- </li>
- ))}
- {hasOrganizations && <li role="separator" className="divider" />}
- <li>
- <a onClick={this.handleLogout} href="#">
- {translate('layout.logout')}
- </a>
- </li>
- </ul>
- )}
- </li>
- );
- }
-
- renderAnonymous() {
- return (
- <li>
- <a className="navbar-login" onClick={this.handleLogin} href="#">
- {translate('layout.login')}
- </a>
- </li>
- );
- }
-
- render() {
- return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous();
- }
-}
--- /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 * as React from 'react';
+import * as classNames from 'classnames';
+import { sortBy } from 'lodash';
+import * as PropTypes from 'prop-types';
+import { Link } from 'react-router';
+import * as theme from '../../../theme';
+import { CurrentUser, LoggedInUser, isLoggedIn, Organization } from '../../../types';
+import Avatar from '../../../../components/ui/Avatar';
+import OrganizationLink from '../../../../components/ui/OrganizationLink';
+import { translate } from '../../../../helpers/l10n';
+import { getBaseUrl } from '../../../../helpers/urls';
+import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
+
+interface Props {
+ appState: { organizationsEnabled: boolean };
+ currentUser: CurrentUser;
+ fetchMyOrganizations: () => Promise<void>;
+ organizations: Organization[];
+}
+
+interface State {
+ open: boolean;
+}
+
+export default class GlobalNavUser extends React.PureComponent<Props, State> {
+ node?: HTMLElement | null;
+
+ static contextTypes = {
+ router: PropTypes.object
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { open: false };
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('click', this.handleClickOutside);
+ }
+
+ handleClickOutside = (event: MouseEvent) => {
+ if (!this.node || !this.node.contains(event.target as Node)) {
+ this.closeDropdown();
+ }
+ };
+
+ handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`;
+ if (shouldReturnToCurrentPage) {
+ const returnTo = encodeURIComponent(window.location.pathname + window.location.search);
+ window.location.href =
+ getBaseUrl() + `/sessions/new?return_to=${returnTo}${window.location.hash}`;
+ } else {
+ window.location.href = `${getBaseUrl()}/sessions/new`;
+ }
+ };
+
+ handleLogout = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.closeDropdown();
+ this.context.router.push('/sessions/logout');
+ };
+
+ toggleDropdown = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ if (this.state.open) {
+ this.closeDropdown();
+ } else {
+ this.openDropdown();
+ }
+ };
+
+ openDropdown = () => {
+ this.fetchMyOrganizations().then(() => {
+ window.addEventListener('click', this.handleClickOutside, true);
+ this.setState({ open: true });
+ });
+ };
+
+ closeDropdown = () => {
+ window.removeEventListener('click', this.handleClickOutside);
+ this.setState({ open: false });
+ };
+
+ fetchMyOrganizations = () => {
+ if (this.props.appState.organizationsEnabled) {
+ return this.props.fetchMyOrganizations();
+ }
+ return Promise.resolve();
+ };
+
+ renderAuthenticated() {
+ const { organizations } = this.props;
+ const currentUser = this.props.currentUser as LoggedInUser;
+ const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0;
+ return (
+ <li
+ className={classNames('dropdown js-user-authenticated', { open: this.state.open })}
+ ref={node => (this.node = node)}>
+ <a className="dropdown-toggle navbar-avatar" href="#" onClick={this.toggleDropdown}>
+ <Avatar
+ hash={currentUser.avatar}
+ name={currentUser.name}
+ size={theme.globalNavContentHeightRaw}
+ />
+ </a>
+ {this.state.open && (
+ <ul className="dropdown-menu dropdown-menu-right">
+ <li className="dropdown-item">
+ <div className="text-ellipsis text-muted" title={currentUser.name}>
+ <strong>{currentUser.name}</strong>
+ </div>
+ {currentUser.email != null && (
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title={currentUser.email}>
+ {currentUser.email}
+ </div>
+ )}
+ </li>
+ <li className="divider" />
+ <li>
+ <Link to="/account" onClick={this.closeDropdown}>
+ {translate('my_account.page')}
+ </Link>
+ </li>
+ {hasOrganizations && <li role="separator" className="divider" />}
+ {hasOrganizations && (
+ <li>
+ <Link to="/account/organizations" onClick={this.closeDropdown}>
+ {translate('my_organizations')}
+ </Link>
+ </li>
+ )}
+ {hasOrganizations &&
+ sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
+ <li key={organization.key}>
+ <OrganizationLink
+ className="dropdown-item-flex"
+ organization={organization}
+ onClick={this.closeDropdown}>
+ <div>
+ <OrganizationAvatar organization={organization} small={true} />
+ <span className="spacer-left">{organization.name}</span>
+ </div>
+ {organization.canAdmin && (
+ <span className="outline-badge spacer-left">{translate('admin')}</span>
+ )}
+ </OrganizationLink>
+ </li>
+ ))}
+ {hasOrganizations && <li role="separator" className="divider" />}
+ <li>
+ <a onClick={this.handleLogout} href="#">
+ {translate('layout.logout')}
+ </a>
+ </li>
+ </ul>
+ )}
+ </li>
+ );
+ }
+
+ renderAnonymous() {
+ return (
+ <li>
+ <a className="navbar-login" onClick={this.handleLogin} href="#">
+ {translate('layout.login')}
+ </a>
+ </li>
+ );
+ }
+
+ render() {
+ return isLoggedIn(this.props.currentUser) ? this.renderAuthenticated() : this.renderAnonymous();
+ }
+}
+++ /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 { withRouter } from 'react-router';
-import { connect } from 'react-redux';
-import GlobalNavUser from './GlobalNavUser';
-import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions';
-import { getMyOrganizations } from '../../../../store/rootReducer';
-
-const mapStateToProps = state => ({
- organizations: getMyOrganizations(state)
-});
-
-const mapDispatchToProps = {
- fetchMyOrganizations
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(withRouter(GlobalNavUser));
--- /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 GlobalNavUser from './GlobalNavUser';
+import { Organization } from '../../../types';
+import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions';
+import { getMyOrganizations } from '../../../../store/rootReducer';
+
+interface StateProps {
+ organizations: Organization[];
+}
+
+const mapStateToProps = (state: any): StateProps => ({
+ organizations: getMyOrganizations(state)
+});
+
+interface DispatchProps {
+ fetchMyOrganizations: () => Promise<void>;
+}
+
+const mapDispatchToProps = {
+ fetchMyOrganizations: fetchMyOrganizations as any
+} as DispatchProps;
+
+export default connect(mapStateToProps, mapDispatchToProps)(GlobalNavUser);
+++ /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 GlobalNavUser from '../GlobalNavUser';
-
-const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' };
-const organizations = [
- { key: 'myorg', name: 'MyOrg' },
- { key: 'foo', name: 'Foo' },
- { key: 'bar', name: 'bar' }
-];
-const appState = { organizationsEnabled: true };
-
-it('should render the right interface for anonymous user', () => {
- const currentUser = { isLoggedIn: false };
- const wrapper = shallow(
- <GlobalNavUser
- appState={appState}
- currentUser={currentUser}
- fetchMyOrganizations={() => {}}
- organizations={[]}
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render the right interface for logged in user', () => {
- const wrapper = shallow(
- <GlobalNavUser
- appState={appState}
- currentUser={currentUser}
- fetchMyOrganizations={() => {}}
- organizations={[]}
- />
- );
- wrapper.setState({ open: true });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render the users organizations', () => {
- const wrapper = shallow(
- <GlobalNavUser
- appState={appState}
- currentUser={currentUser}
- fetchMyOrganizations={() => {}}
- organizations={organizations}
- />
- );
- wrapper.setState({ open: true });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should not render the users organizations when they are not activated', () => {
- const wrapper = shallow(
- <GlobalNavUser
- appState={{ organizationsEnabled: false }}
- currentUser={currentUser}
- fetchMyOrganizations={() => {}}
- organizations={organizations}
- />
- );
- wrapper.setState({ open: true });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should update the component correctly when the user changes to anonymous', () => {
- const fetchMyOrganizations = jest.fn();
- const wrapper = shallow(
- <GlobalNavUser
- appState={appState}
- currentUser={currentUser}
- fetchMyOrganizations={fetchMyOrganizations}
- organizations={[]}
- />
- );
- wrapper.setState({ open: true });
- expect(wrapper).toMatchSnapshot();
- wrapper.setProps({ currentUser: { isLoggedIn: false } });
- expect(fetchMyOrganizations.mock.calls.length).toBe(0);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should lazyload the organizations when opening the dropdown', () => {
- const fetchMyOrganizations = jest.fn(() => Promise.resolve());
- const wrapper = shallow(
- <GlobalNavUser
- appState={appState}
- currentUser={currentUser}
- fetchMyOrganizations={fetchMyOrganizations}
- organizations={organizations}
- />
- );
- expect(fetchMyOrganizations.mock.calls.length).toBe(0);
- wrapper.instance().openDropdown();
- expect(fetchMyOrganizations.mock.calls.length).toBe(1);
- wrapper.instance().openDropdown();
- expect(fetchMyOrganizations.mock.calls.length).toBe(2);
-});
-
-it('should update the organizations when the user changes', () => {
- const fetchMyOrganizations = jest.fn(() => Promise.resolve());
- const wrapper = shallow(
- <GlobalNavUser
- appState={appState}
- currentUser={currentUser}
- fetchMyOrganizations={fetchMyOrganizations}
- organizations={organizations}
- />
- );
- wrapper.instance().openDropdown();
- expect(fetchMyOrganizations.mock.calls.length).toBe(1);
- wrapper.setProps({
- currentUser: { isLoggedIn: true, name: 'test', email: 'test@sonarsource.com' }
- });
- wrapper.instance().openDropdown();
- expect(fetchMyOrganizations.mock.calls.length).toBe(2);
-});
--- /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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GlobalNavUser from '../GlobalNavUser';
+
+const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' };
+const organizations = [
+ { key: 'myorg', name: 'MyOrg', projectVisibility: 'public' },
+ { key: 'foo', name: 'Foo', projectVisibility: 'public' },
+ { key: 'bar', name: 'bar', projectVisibility: 'public' }
+];
+const appState = { organizationsEnabled: true };
+
+it('should render the right interface for anonymous user', () => {
+ const currentUser = { isLoggedIn: false };
+ const wrapper = shallow(
+ <GlobalNavUser
+ appState={appState}
+ currentUser={currentUser}
+ fetchMyOrganizations={jest.fn()}
+ organizations={[]}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the right interface for logged in user', () => {
+ const wrapper = shallow(
+ <GlobalNavUser
+ appState={appState}
+ currentUser={currentUser}
+ fetchMyOrganizations={jest.fn()}
+ organizations={[]}
+ />
+ );
+ wrapper.setState({ open: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the users organizations', () => {
+ const wrapper = shallow(
+ <GlobalNavUser
+ appState={appState}
+ currentUser={currentUser}
+ fetchMyOrganizations={jest.fn()}
+ organizations={organizations}
+ />
+ );
+ wrapper.setState({ open: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should not render the users organizations when they are not activated', () => {
+ const wrapper = shallow(
+ <GlobalNavUser
+ appState={{ organizationsEnabled: false }}
+ currentUser={currentUser}
+ fetchMyOrganizations={jest.fn()}
+ organizations={organizations}
+ />
+ );
+ wrapper.setState({ open: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should update the component correctly when the user changes to anonymous', () => {
+ const fetchMyOrganizations = jest.fn();
+ const wrapper = shallow(
+ <GlobalNavUser
+ appState={appState}
+ currentUser={currentUser}
+ fetchMyOrganizations={fetchMyOrganizations}
+ organizations={[]}
+ />
+ );
+ wrapper.setState({ open: true });
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ currentUser: { isLoggedIn: false } });
+ expect(fetchMyOrganizations.mock.calls.length).toBe(0);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should lazyload the organizations when opening the dropdown', () => {
+ const fetchMyOrganizations = jest.fn(() => Promise.resolve());
+ const wrapper = shallow(
+ <GlobalNavUser
+ appState={appState}
+ currentUser={currentUser}
+ fetchMyOrganizations={fetchMyOrganizations}
+ organizations={organizations}
+ />
+ );
+ expect(fetchMyOrganizations.mock.calls.length).toBe(0);
+ (wrapper.instance() as GlobalNavUser).openDropdown();
+ expect(fetchMyOrganizations.mock.calls.length).toBe(1);
+ (wrapper.instance() as GlobalNavUser).openDropdown();
+ expect(fetchMyOrganizations.mock.calls.length).toBe(2);
+});
+
+it('should update the organizations when the user changes', () => {
+ const fetchMyOrganizations = jest.fn(() => Promise.resolve());
+ const wrapper = shallow(
+ <GlobalNavUser
+ appState={appState}
+ currentUser={currentUser}
+ fetchMyOrganizations={fetchMyOrganizations}
+ organizations={organizations}
+ />
+ );
+ (wrapper.instance() as GlobalNavUser).openDropdown();
+ expect(fetchMyOrganizations.mock.calls.length).toBe(1);
+ wrapper.setProps({
+ currentUser: { isLoggedIn: true, name: 'test', email: 'test@sonarsource.com' }
+ });
+ (wrapper.instance() as GlobalNavUser).openDropdown();
+ expect(fetchMyOrganizations.mock.calls.length).toBe(2);
+});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not render the users organizations when they are not activated 1`] = `
-<li
- className="dropdown js-user-authenticated open"
->
- <a
- className="dropdown-toggle navbar-avatar"
- href="#"
- onClick={[Function]}
- >
- <Connect(Avatar)
- hash="abcd1234"
- name="foo"
- size={32}
- />
- </a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li
- className="dropdown-item"
- >
- <div
- className="text-ellipsis text-muted"
- title="foo"
- >
- <strong>
- foo
- </strong>
- </div>
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title="foo@bar.baz"
- >
- foo@bar.baz
- </div>
- </li>
- <li
- className="divider"
- />
- <li>
- <Link
- onClick={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account"
- >
- my_account.page
- </Link>
- </li>
- <li>
- <a
- href="#"
- onClick={[Function]}
- >
- layout.logout
- </a>
- </li>
- </ul>
-</li>
-`;
-
-exports[`should render the right interface for anonymous user 1`] = `
-<li>
- <a
- className="navbar-login"
- href="#"
- onClick={[Function]}
- >
- layout.login
- </a>
-</li>
-`;
-
-exports[`should render the right interface for logged in user 1`] = `
-<li
- className="dropdown js-user-authenticated open"
->
- <a
- className="dropdown-toggle navbar-avatar"
- href="#"
- onClick={[Function]}
- >
- <Connect(Avatar)
- hash="abcd1234"
- name="foo"
- size={32}
- />
- </a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li
- className="dropdown-item"
- >
- <div
- className="text-ellipsis text-muted"
- title="foo"
- >
- <strong>
- foo
- </strong>
- </div>
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title="foo@bar.baz"
- >
- foo@bar.baz
- </div>
- </li>
- <li
- className="divider"
- />
- <li>
- <Link
- onClick={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account"
- >
- my_account.page
- </Link>
- </li>
- <li>
- <a
- href="#"
- onClick={[Function]}
- >
- layout.logout
- </a>
- </li>
- </ul>
-</li>
-`;
-
-exports[`should render the users organizations 1`] = `
-<li
- className="dropdown js-user-authenticated open"
->
- <a
- className="dropdown-toggle navbar-avatar"
- href="#"
- onClick={[Function]}
- >
- <Connect(Avatar)
- hash="abcd1234"
- name="foo"
- size={32}
- />
- </a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li
- className="dropdown-item"
- >
- <div
- className="text-ellipsis text-muted"
- title="foo"
- >
- <strong>
- foo
- </strong>
- </div>
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title="foo@bar.baz"
- >
- foo@bar.baz
- </div>
- </li>
- <li
- className="divider"
- />
- <li>
- <Link
- onClick={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account"
- >
- my_account.page
- </Link>
- </li>
- <li
- className="divider"
- role="separator"
- />
- <li>
- <Link
- onClick={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account/organizations"
- >
- my_organizations
- </Link>
- </li>
- <li
- key="bar"
- >
- <OrganizationLink
- className="dropdown-item-flex"
- onClick={[Function]}
- organization={
- Object {
- "key": "bar",
- "name": "bar",
- }
- }
- >
- <div>
- <OrganizationIcon />
- <span
- className="spacer-left"
- >
- bar
- </span>
- </div>
- </OrganizationLink>
- </li>
- <li
- key="foo"
- >
- <OrganizationLink
- className="dropdown-item-flex"
- onClick={[Function]}
- organization={
- Object {
- "key": "foo",
- "name": "Foo",
- }
- }
- >
- <div>
- <OrganizationIcon />
- <span
- className="spacer-left"
- >
- Foo
- </span>
- </div>
- </OrganizationLink>
- </li>
- <li
- key="myorg"
- >
- <OrganizationLink
- className="dropdown-item-flex"
- onClick={[Function]}
- organization={
- Object {
- "key": "myorg",
- "name": "MyOrg",
- }
- }
- >
- <div>
- <OrganizationIcon />
- <span
- className="spacer-left"
- >
- MyOrg
- </span>
- </div>
- </OrganizationLink>
- </li>
- <li
- className="divider"
- role="separator"
- />
- <li>
- <a
- href="#"
- onClick={[Function]}
- >
- layout.logout
- </a>
- </li>
- </ul>
-</li>
-`;
-
-exports[`should update the component correctly when the user changes to anonymous 1`] = `
-<li
- className="dropdown js-user-authenticated open"
->
- <a
- className="dropdown-toggle navbar-avatar"
- href="#"
- onClick={[Function]}
- >
- <Connect(Avatar)
- hash="abcd1234"
- name="foo"
- size={32}
- />
- </a>
- <ul
- className="dropdown-menu dropdown-menu-right"
- >
- <li
- className="dropdown-item"
- >
- <div
- className="text-ellipsis text-muted"
- title="foo"
- >
- <strong>
- foo
- </strong>
- </div>
- <div
- className="little-spacer-top text-ellipsis text-muted"
- title="foo@bar.baz"
- >
- foo@bar.baz
- </div>
- </li>
- <li
- className="divider"
- />
- <li>
- <Link
- onClick={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/account"
- >
- my_account.page
- </Link>
- </li>
- <li>
- <a
- href="#"
- onClick={[Function]}
- >
- layout.logout
- </a>
- </li>
- </ul>
-</li>
-`;
-
-exports[`should update the component correctly when the user changes to anonymous 2`] = `
-<li>
- <a
- className="navbar-login"
- href="#"
- onClick={[Function]}
- >
- layout.login
- </a>
-</li>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render the users organizations when they are not activated 1`] = `
+<li
+ className="dropdown js-user-authenticated open"
+>
+ <a
+ className="dropdown-toggle navbar-avatar"
+ href="#"
+ onClick={[Function]}
+ >
+ <Connect(Avatar)
+ hash="abcd1234"
+ name="foo"
+ size={32}
+ />
+ </a>
+ <ul
+ className="dropdown-menu dropdown-menu-right"
+ >
+ <li
+ className="dropdown-item"
+ >
+ <div
+ className="text-ellipsis text-muted"
+ title="foo"
+ >
+ <strong>
+ foo
+ </strong>
+ </div>
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title="foo@bar.baz"
+ >
+ foo@bar.baz
+ </div>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <Link
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account"
+ >
+ my_account.page
+ </Link>
+ </li>
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ layout.logout
+ </a>
+ </li>
+ </ul>
+</li>
+`;
+
+exports[`should render the right interface for anonymous user 1`] = `
+<li>
+ <a
+ className="navbar-login"
+ href="#"
+ onClick={[Function]}
+ >
+ layout.login
+ </a>
+</li>
+`;
+
+exports[`should render the right interface for logged in user 1`] = `
+<li
+ className="dropdown js-user-authenticated open"
+>
+ <a
+ className="dropdown-toggle navbar-avatar"
+ href="#"
+ onClick={[Function]}
+ >
+ <Connect(Avatar)
+ hash="abcd1234"
+ name="foo"
+ size={32}
+ />
+ </a>
+ <ul
+ className="dropdown-menu dropdown-menu-right"
+ >
+ <li
+ className="dropdown-item"
+ >
+ <div
+ className="text-ellipsis text-muted"
+ title="foo"
+ >
+ <strong>
+ foo
+ </strong>
+ </div>
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title="foo@bar.baz"
+ >
+ foo@bar.baz
+ </div>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <Link
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account"
+ >
+ my_account.page
+ </Link>
+ </li>
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ layout.logout
+ </a>
+ </li>
+ </ul>
+</li>
+`;
+
+exports[`should render the users organizations 1`] = `
+<li
+ className="dropdown js-user-authenticated open"
+>
+ <a
+ className="dropdown-toggle navbar-avatar"
+ href="#"
+ onClick={[Function]}
+ >
+ <Connect(Avatar)
+ hash="abcd1234"
+ name="foo"
+ size={32}
+ />
+ </a>
+ <ul
+ className="dropdown-menu dropdown-menu-right"
+ >
+ <li
+ className="dropdown-item"
+ >
+ <div
+ className="text-ellipsis text-muted"
+ title="foo"
+ >
+ <strong>
+ foo
+ </strong>
+ </div>
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title="foo@bar.baz"
+ >
+ foo@bar.baz
+ </div>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <Link
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account"
+ >
+ my_account.page
+ </Link>
+ </li>
+ <li
+ className="divider"
+ role="separator"
+ />
+ <li>
+ <Link
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account/organizations"
+ >
+ my_organizations
+ </Link>
+ </li>
+ <li
+ key="bar"
+ >
+ <OrganizationLink
+ className="dropdown-item-flex"
+ onClick={[Function]}
+ organization={
+ Object {
+ "key": "bar",
+ "name": "bar",
+ "projectVisibility": "public",
+ }
+ }
+ >
+ <div>
+ <OrganizationAvatar
+ organization={
+ Object {
+ "key": "bar",
+ "name": "bar",
+ "projectVisibility": "public",
+ }
+ }
+ small={true}
+ />
+ <span
+ className="spacer-left"
+ >
+ bar
+ </span>
+ </div>
+ </OrganizationLink>
+ </li>
+ <li
+ key="foo"
+ >
+ <OrganizationLink
+ className="dropdown-item-flex"
+ onClick={[Function]}
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "projectVisibility": "public",
+ }
+ }
+ >
+ <div>
+ <OrganizationAvatar
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "projectVisibility": "public",
+ }
+ }
+ small={true}
+ />
+ <span
+ className="spacer-left"
+ >
+ Foo
+ </span>
+ </div>
+ </OrganizationLink>
+ </li>
+ <li
+ key="myorg"
+ >
+ <OrganizationLink
+ className="dropdown-item-flex"
+ onClick={[Function]}
+ organization={
+ Object {
+ "key": "myorg",
+ "name": "MyOrg",
+ "projectVisibility": "public",
+ }
+ }
+ >
+ <div>
+ <OrganizationAvatar
+ organization={
+ Object {
+ "key": "myorg",
+ "name": "MyOrg",
+ "projectVisibility": "public",
+ }
+ }
+ small={true}
+ />
+ <span
+ className="spacer-left"
+ >
+ MyOrg
+ </span>
+ </div>
+ </OrganizationLink>
+ </li>
+ <li
+ className="divider"
+ role="separator"
+ />
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ layout.logout
+ </a>
+ </li>
+ </ul>
+</li>
+`;
+
+exports[`should update the component correctly when the user changes to anonymous 1`] = `
+<li
+ className="dropdown js-user-authenticated open"
+>
+ <a
+ className="dropdown-toggle navbar-avatar"
+ href="#"
+ onClick={[Function]}
+ >
+ <Connect(Avatar)
+ hash="abcd1234"
+ name="foo"
+ size={32}
+ />
+ </a>
+ <ul
+ className="dropdown-menu dropdown-menu-right"
+ >
+ <li
+ className="dropdown-item"
+ >
+ <div
+ className="text-ellipsis text-muted"
+ title="foo"
+ >
+ <strong>
+ foo
+ </strong>
+ </div>
+ <div
+ className="little-spacer-top text-ellipsis text-muted"
+ title="foo@bar.baz"
+ >
+ foo@bar.baz
+ </div>
+ </li>
+ <li
+ className="divider"
+ />
+ <li>
+ <Link
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/account"
+ >
+ my_account.page
+ </Link>
+ </li>
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ layout.logout
+ </a>
+ </li>
+ </ul>
+</li>
+`;
+
+exports[`should update the component correctly when the user changes to anonymous 2`] = `
+<li>
+ <a
+ className="navbar-login"
+ href="#"
+ onClick={[Function]}
+ >
+ layout.login
+ </a>
+</li>
+`;
import * as React from 'react';
import * as classNames from 'classnames';
import { IndexLink, Link } from 'react-router';
+import * as theme from '../../../../app/theme';
import ContextNavBar from '../../../../components/nav/ContextNavBar';
import SettingsEditionsNotifContainer from './SettingsEditionsNotifContainer';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
return (
<ContextNavBar
id="context-navigation"
- height={notifComponent ? 95 : 65}
+ height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
notif={notifComponent}>
- <h1 className="navbar-context-header">
- <strong>{translate('layout.settings')}</strong>
- </h1>
+ <h1 className="navbar-context-header">{translate('layout.settings')}</h1>
<NavBarTabs>
{this.renderConfigurationTab()}
exports[`should work with extensions 1`] = `
<ContextNavBar
- height={65}
+ height={72}
id="context-navigation"
>
<h1
className="navbar-context-header"
>
- <strong>
- layout.settings
- </strong>
+ layout.settings
</h1>
<NavBarTabs>
<li
// IMPORTANT: any change in this file requires restart of the dev server
+const grid = 8;
+
module.exports = {
// colors
blue: '#4b9fd5',
leakBorderColor: '#eae3c7',
// sizes
+ gridSize: `${grid}px`,
+
baseFontSize: '13px',
smallFontSize: '12px',
mediumFontSize: '14px',
bigFontSize: '16px',
- controlHeight: '24px',
- smallControlHeight: '20px',
- tinyControlHeight: '16px',
+ controlHeight: `${3 * grid}px`,
+ smallControlHeight: `${2.5 * grid}px`,
+ tinyControlHeight: `${2 * grid}px`,
+
+ globalNavHeight: `${6 * grid}px`,
+ globalNavHeightRaw: 6 * grid,
+ globalNavContentHeight: `${4 * grid}px`,
+ globalNavContentHeightRaw: 4 * grid,
- globalNavHeight: '48px',
- globalNavHeightRaw: 48,
- globalNavContentHeight: '32px',
- globalNavContentHeightRaw: 32,
+ contextNavHeightRaw: 9 * grid,
// different
defaultShadow: '0 6px 12px rgba(0, 0, 0, 0.175)',
+++ /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 Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import { Link } from 'react-router';
-import OrganizationsList from './OrganizationsList';
-import { translate } from '../../../helpers/l10n';
-import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions';
-import { getAppState, getMyOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-
-class UserOrganizations extends React.PureComponent {
- /*:: mounted: boolean; */
-
- /*:: props: {
- anyoneCanCreate?: { value: string },
- canAdmin: boolean,
- children?: React.Element<*>,
- organizations: Array<Organization>,
- fetchIfAnyoneCanCreateOrganizations: () => Promise<*>,
- fetchMyOrganizations: () => Promise<*>
- };
-*/
-
- state /*: { loading: boolean } */ = {
- loading: true
- };
-
- componentDidMount() {
- this.mounted = true;
- Promise.all([
- this.props.fetchMyOrganizations(),
- this.props.fetchIfAnyoneCanCreateOrganizations()
- ]).then(() => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- });
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- render() {
- const anyoneCanCreate =
- this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true';
-
- const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin);
-
- return (
- <div className="account-body account-container">
- <Helmet title={translate('my_account.organizations')} />
-
- <header className="page-header">
- <h2 className="page-title">{translate('my_account.organizations')}</h2>
- {canCreateOrganizations && (
- <div className="page-actions">
- <Link to="/account/organizations/create" className="button">
- {translate('create')}
- </Link>
- </div>
- )}
- {this.props.organizations.length > 0 ? (
- <div className="page-description">
- {translate('my_account.organizations.description')}
- </div>
- ) : (
- <div className="page-description">
- {translate('my_account.organizations.no_results')}
- </div>
- )}
- </header>
-
- {this.state.loading ? (
- <i className="spinner" />
- ) : (
- <OrganizationsList organizations={this.props.organizations} />
- )}
-
- {this.props.children}
- </div>
- );
- }
-}
-
-const mapStateToProps = state => ({
- anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'),
- canAdmin: getAppState(state).canAdmin,
- organizations: getMyOrganizations(state)
-});
-
-const mapDispatchToProps = {
- fetchMyOrganizations,
- fetchIfAnyoneCanCreateOrganizations
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations);
--- /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 * as React from 'react';
+import Helmet from 'react-helmet';
+import { connect } from 'react-redux';
+import { Link } from 'react-router';
+import OrganizationsList from './OrganizationsList';
+import { translate } from '../../../helpers/l10n';
+import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions';
+import { getAppState, getMyOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
+import { Organization } from '../../../app/types';
+
+interface StateProps {
+ anyoneCanCreate?: { value: string };
+ canAdmin: boolean;
+ organizations: Array<Organization>;
+}
+
+interface DispatchProps {
+ fetchIfAnyoneCanCreateOrganizations: () => Promise<void>;
+ fetchMyOrganizations: () => Promise<void>;
+}
+
+interface Props extends StateProps, DispatchProps {
+ children?: React.ReactNode;
+}
+
+interface State {
+ loading: boolean;
+}
+
+class UserOrganizations extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ Promise.all([
+ this.props.fetchMyOrganizations(),
+ this.props.fetchIfAnyoneCanCreateOrganizations()
+ ]).then(this.stopLoading, this.stopLoading);
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ render() {
+ const anyoneCanCreate =
+ this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true';
+
+ const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin);
+
+ return (
+ <div className="account-body account-container">
+ <Helmet title={translate('my_account.organizations')} />
+
+ <header className="page-header">
+ <h2 className="page-title">{translate('my_account.organizations')}</h2>
+ {canCreateOrganizations && (
+ <div className="page-actions">
+ <Link to="/account/organizations/create" className="button">
+ {translate('create')}
+ </Link>
+ </div>
+ )}
+ {this.props.organizations.length > 0 ? (
+ <div className="page-description">
+ {translate('my_account.organizations.description')}
+ </div>
+ ) : (
+ <div className="page-description">
+ {translate('my_account.organizations.no_results')}
+ </div>
+ )}
+ </header>
+
+ {this.state.loading ? (
+ <i className="spinner" />
+ ) : (
+ <OrganizationsList organizations={this.props.organizations} />
+ )}
+
+ {this.props.children}
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state: any): StateProps => ({
+ anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'),
+ canAdmin: getAppState(state).canAdmin,
+ organizations: getMyOrganizations(state)
+});
+
+const mapDispatchToProps = {
+ fetchMyOrganizations: fetchMyOrganizations as any,
+ fetchIfAnyoneCanCreateOrganizations: fetchIfAnyoneCanCreateOrganizations as any
+} as DispatchProps;
+
+export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations);
+++ /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 { getOrganizations } from '../../../api/organizations';
-import { receiveMyOrganizations } from '../../../store/organizations/duck';
-import { getValues } from '../../../api/settings';
-import { receiveValues } from '../../settings/store/values/actions';
-
-export const fetchMyOrganizations = () => dispatch => {
- return getOrganizations({ member: true }).then(({ organizations }) => {
- return dispatch(receiveMyOrganizations(organizations));
- });
-};
-
-export const fetchIfAnyoneCanCreateOrganizations = () => dispatch => {
- return getValues('sonar.organizations.anyoneCanCreate').then(values => {
- dispatch(receiveValues(values));
- });
-};
--- /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 { Dispatch } from 'redux';
+import { getOrganizations } from '../../../api/organizations';
+import { receiveMyOrganizations } from '../../../store/organizations/duck';
+import { getValues } from '../../../api/settings';
+import { receiveValues } from '../../settings/store/values/actions';
+
+export const fetchMyOrganizations = () => (dispatch: Dispatch<any>) => {
+ return getOrganizations({ member: true }).then(({ organizations }) => {
+ return dispatch(receiveMyOrganizations(organizations));
+ });
+};
+
+export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch<any>) => {
+ return getValues('sonar.organizations.anyoneCanCreate').then(values => {
+ dispatch(receiveValues(values, undefined));
+ });
+};
*/
import * as React from 'react';
import { Link } from 'react-router';
+import * as theme from '../../app/theme';
import ContextNavBar from '../../components/nav/ContextNavBar';
import NavBarTabs from '../../components/nav/NavBarTabs';
import { translate } from '../../helpers/l10n';
export default function Explore(props: Props) {
return (
<div id="explore">
- <ContextNavBar id="explore-navigation" height={65}>
+ <ContextNavBar id="explore-navigation" height={theme.contextNavHeightRaw}>
<div className="navbar-context-header">
<h1 className="display-inline-block">{translate('explore')}</h1>
</div>
import React from 'react';
import { Link } from 'react-router';
import classNames from 'classnames';
+import * as theme from '../../../app/theme';
import { translate } from '../../../helpers/l10n';
import ContextNavBar from '../../../components/nav/ContextNavBar';
import NavBarTabs from '../../../components/nav/NavBarTabs';
-import OrganizationIcon from '../../../components/icons-components/OrganizationIcon';
+import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import { getQualityGatesUrl } from '../../../helpers/urls';
/*:: import type { Organization } from '../../../store/organizations/duck'; */
const moreActive = !adminActive && location.pathname.includes('/extension/');
return (
- <ContextNavBar id="context-navigation" height={65}>
+ <ContextNavBar id="context-navigation" height={theme.contextNavHeightRaw}>
<div className="navbar-context-header">
<h1 className="display-inline-block">
- <OrganizationIcon className="little-spacer-right" />
+ <OrganizationAvatar organization={organization} />
<Link
to={`/organizations/${organization.key}`}
- className="link-base-color link-no-underline">
- <strong>{organization.name}</strong>
+ className="link-base-color link-no-underline spacer-left">
+ {organization.name}
</Link>
</h1>
{organization.description != null && (
</div>
<div className="navbar-context-meta">
- {!!organization.avatar && (
- <img src={organization.avatar} height={30} alt={organization.name} />
- )}
+ <div className="text-muted">
+ <strong>{translate('organization.key')}:</strong> {organization.key}
+ </div>
{organization.url != null && (
<div>
<p className="text-limited text-top">
)}
</div>
- <NavBarTabs>
+ <NavBarTabs className="navbar-context-tabs">
<li>
<Link
to={`/organizations/${organization.key}/projects`}
exports[`admin 1`] = `
<ContextNavBar
- height={65}
+ height={72}
id="context-navigation"
>
<div
<h1
className="display-inline-block"
>
- <OrganizationIcon
- className="little-spacer-right"
+ <OrganizationAvatar
+ organization={
+ Object {
+ "canAdmin": true,
+ "canDelete": true,
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
/>
<Link
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to="/organizations/foo"
>
- <strong>
- Foo
- </strong>
+ Foo
</Link>
</h1>
</div>
<div
className="navbar-context-meta"
- />
- <NavBarTabs>
+ >
+ <div
+ className="text-muted"
+ >
+ <strong>
+ organization.key
+ :
+ </strong>
+
+ foo
+ </div>
+ </div>
+ <NavBarTabs
+ className="navbar-context-tabs"
+ >
<li>
<Link
className=""
exports[`regular user 1`] = `
<ContextNavBar
- height={65}
+ height={72}
id="context-navigation"
>
<div
<h1
className="display-inline-block"
>
- <OrganizationIcon
- className="little-spacer-right"
+ <OrganizationAvatar
+ organization={
+ Object {
+ "canAdmin": false,
+ "canDelete": false,
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
/>
<Link
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to="/organizations/foo"
>
- <strong>
- Foo
- </strong>
+ Foo
</Link>
</h1>
</div>
<div
className="navbar-context-meta"
- />
- <NavBarTabs>
+ >
+ <div
+ className="text-muted"
+ >
+ <strong>
+ organization.key
+ :
+ </strong>
+
+ foo
+ </div>
+ </div>
+ <NavBarTabs
+ className="navbar-context-tabs"
+ >
<li>
<Link
className=""
exports[`undeletable org 1`] = `
<ContextNavBar
- height={65}
+ height={72}
id="context-navigation"
>
<div
<h1
className="display-inline-block"
>
- <OrganizationIcon
- className="little-spacer-right"
+ <OrganizationAvatar
+ organization={
+ Object {
+ "canAdmin": true,
+ "canDelete": false,
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
/>
<Link
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to="/organizations/foo"
>
- <strong>
- Foo
- </strong>
+ Foo
</Link>
</h1>
</div>
<div
className="navbar-context-meta"
- />
- <NavBarTabs>
+ >
+ <div
+ className="text-muted"
+ >
+ <strong>
+ organization.key
+ :
+ </strong>
+
+ foo
+ </div>
+ </div>
+ <NavBarTabs
+ className="navbar-context-tabs"
+ >
<li>
<Link
className=""
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact 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.
+ */
+.navbar-context-avatar {
+ display: inline-flex;
+ vertical-align: top;
+ justify-content: center;
+ align-items: center;
+ width: calc(4 * var(--gridSize));
+ height: calc(4 * var(--gridSize));
+ border: 1px solid var(--barBorderColor);
+}
+
+.navbar-context-avatar.is-empty {
+ border: none;
+}
+
+.navbar-context-avatar.is-small {
+ width: calc(2 * var(--gridSize));
+ height: calc(2 * var(--gridSize));
+}
+
+.navbar-context-avatar img {
+ vertical-align: top;
+ max-width: 100%;
+ max-height: 100%;
+}
+
+.navbar-context-avatar img,
+.navbar-context-avatar svg {
+ transform: none;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact 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 * as React from 'react';
+import * as classNames from 'classnames';
+import GenericAvatar from '../ui/GenericAvatar';
+import './OrganizationAvatar.css';
+
+interface Props {
+ organization: {
+ avatar?: string;
+ name: string;
+ };
+ small?: boolean;
+}
+
+export default function OrganizationAvatar({ organization, small }: Props) {
+ return (
+ <div
+ className={classNames('navbar-context-avatar', 'rounded', {
+ 'is-empty': !organization.avatar,
+ 'is-small': small
+ })}>
+ {organization.avatar ? (
+ <img className="rounded" src={organization.avatar} alt={organization.name} />
+ ) : (
+ <GenericAvatar name={organization.name} size={small ? 15 : 30} />
+ )}
+ </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 * as React from 'react';
-import * as theme from '../../app/theme';
-import { IconProps } from './types';
-
-export default function OrganizationIcon({ className, fill = theme.blue, size = 16 }: IconProps) {
- return (
- <svg
- className={className}
- width={size}
- height={size}
- viewBox="0 0 16 16"
- version="1.1"
- xmlnsXlink="http://www.w3.org/1999/xlink"
- xmlSpace="preserve">
- <path
- style={{ fill }}
- d="M13.5 6c-.4 0-.7.1-1.1.2L11 4.8v-.3C11 3.1 9.9 2 8.5 2S6 3.1 6 4.5v.2L4.5 6.2c-.3-.1-.7-.2-1-.2C2.1 6 1 7.1 1 8.5S2.1 11 3.5 11 6 9.9 6 8.5c0-.7-.3-1.3-.7-1.7l1-1c.4.6 1 1 1.7 1.1V9c-1.1.2-2 1.2-2 2.4C6 12.9 7.1 14 8.5 14s2.5-1.1 2.5-2.5c0-1.2-.9-2.2-2-2.4V6.9c.7-.1 1.2-.5 1.6-1.1l1 1c-.4.4-.6 1-.6 1.6 0 1.4 1.1 2.5 2.5 2.5s2.5-1 2.5-2.4S14.9 6 13.5 6zm-10 4C2.7 10 2 9.3 2 8.5S2.7 7 3.5 7 5 7.7 5 8.5 4.3 10 3.5 10zm6.5 1.5c0 .8-.7 1.5-1.5 1.5S7 12.3 7 11.5 7.7 10 8.5 10s1.5.7 1.5 1.5zM8.5 6C7.7 6 7 5.3 7 4.5S7.7 3 8.5 3s1.5.7 1.5 1.5S9.3 6 8.5 6zm5 4c-.8 0-1.5-.7-1.5-1.5S12.7 7 13.5 7s1.5.7 1.5 1.5-.7 1.5-1.5 1.5z"
- />
- </svg>
- );
-}
export { default as ListIcon } from './ListIcon';
export { default as LongLivingBranchIcon } from './LongLivingBranchIcon';
export { default as OpenCloseIcon } from './OpenCloseIcon';
-export { default as OrganizationIcon } from './OrganizationIcon';
export { default as PendingIcon } from './PendingIcon';
export { default as ProjectEventIcon } from './ProjectEventIcon';
export { default as PullRequestIcon } from './PullRequestIcon';
}
.navbar-context .navbar-inner {
- padding-top: 5px;
+ padding-top: var(--gridSize);
border-bottom: 1px solid var(--barBorderColor);
}
}
.navbar-context-header {
- float: left;
- line-height: 30px;
- font-size: 15px;
+ display: inline-block;
+ height: calc(4 * var(--gridSize));
+ line-height: calc(4 * var(--gridSize));
+ font-size: var(--bigFontSize);
+}
+
+.navbar-context-header h1 {
+ vertical-align: top;
+ line-height: calc(4 * var(--gridSize));
+}
+
+.navbar-context-header .slash-separator {
+ display: inline-block;
+ vertical-align: top;
+ height: calc(4 * var(--gridSize));
+ margin-left: var(--gridSize);
+ margin-right: var(--gridSize);
+ font-size: 24px;
+}
+
+.navbar-context-header .slash-separator::after {
+ color: rgba(68, 68, 68, 0.2);
}
.navbar-context-meta {
position: absolute;
top: 0;
right: 0;
- line-height: 30px;
+ line-height: calc(4 * var(--gridSize));
padding: 0 10px;
color: var(--secondFontColor);
font-size: var(--smallFontSize);
display: flex;
align-items: center;
clear: left;
+ height: var(--controlHeight);
+ margin-top: var(--gridSize);
}
.navbar-tabs > li + li {
.navbar-tabs > li > a {
display: block;
- padding: 7px 0 4px;
+ height: var(--controlHeight);
+ line-height: calc(var(--controlHeight) - 6px);
border-bottom: 3px solid transparent;
+ box-sizing: border-box;
color: var(--baseFontColor);
transition: none;
}
import { connect } from 'react-redux';
import * as classNames from 'classnames';
import { getGlobalSettingValue } from '../../store/rootReducer';
+import GenericAvatar from './GenericAvatar';
interface Props {
className?: string;
size: number;
}
-class Avatar extends React.PureComponent<Props> {
- renderFallback() {
- const className = classNames(this.props.className, 'rounded');
- const color = stringToColor(this.props.name);
-
- let text = '';
- const words = this.props.name.split(/\s+/).filter(word => word.length > 0);
- if (words.length >= 2) {
- text = words[0][0] + words[1][0];
- } else if (this.props.name.length > 0) {
- text = this.props.name[0];
- }
-
- return (
- <div
- className={className}
- style={{
- backgroundColor: color,
- color: getTextColor(color),
- display: 'inline-block',
- fontSize: Math.min(this.props.size / 2, 14),
- fontWeight: 'normal',
- height: this.props.size,
- lineHeight: `${this.props.size}px`,
- textAlign: 'center',
- verticalAlign: 'top',
- width: this.props.size
- }}>
- {text.toUpperCase()}
- </div>
- );
+function Avatar(props: Props) {
+ if (!props.enableGravatar || !props.hash) {
+ return <GenericAvatar className={props.className} name={props.name} size={props.size} />;
}
- render() {
- if (!this.props.enableGravatar || !this.props.hash) {
- return this.renderFallback();
- }
-
- const url = this.props.gravatarServerUrl
- .replace('{EMAIL_MD5}', this.props.hash)
- .replace('{SIZE}', String(this.props.size * 2));
-
- return (
- <img
- className={classNames(this.props.className, 'rounded')}
- src={url}
- width={this.props.size}
- height={this.props.size}
- alt={this.props.name}
- />
- );
- }
+ const url = props.gravatarServerUrl
+ .replace('{EMAIL_MD5}', props.hash)
+ .replace('{SIZE}', String(props.size * 2));
+
+ return (
+ <img
+ className={classNames(props.className, 'rounded')}
+ src={url}
+ width={props.size}
+ height={props.size}
+ alt={props.name}
+ />
+ );
}
const mapStateToProps = (state: any) => ({
export default connect(mapStateToProps)(Avatar);
export const unconnectedAvatar = Avatar;
-
-/* eslint-disable no-bitwise, no-mixed-operators */
-function stringToColor(str: string) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
- }
- let color = '#';
- for (let i = 0; i < 3; i++) {
- const value = (hash >> (i * 8)) & 0xff;
- color += ('00' + value.toString(16)).substr(-2);
- }
- return color;
-}
-
-function getTextColor(background: string) {
- const rgb = parseInt(background.substr(1), 16);
- const r = (rgb >> 16) & 0xff;
- const g = (rgb >> 8) & 0xff;
- const b = (rgb >> 0) & 0xff;
- const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
- return luma > 140 ? '#222' : '#fff';
-}
--- /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 * as React from 'react';
+import * as classNames from 'classnames';
+
+interface Props {
+ className?: string;
+ name: string;
+ size: number;
+}
+
+export default function GenericAvatar({ className, name, size }: Props) {
+ const color = stringToColor(name);
+
+ let text = '';
+ const words = name.split(/\s+/).filter(word => word.length > 0);
+ if (words.length >= 2) {
+ text = words[0][0] + words[1][0];
+ } else if (name.length > 0) {
+ text = name[0];
+ }
+
+ return (
+ <div
+ className={classNames(className, 'rounded')}
+ style={{
+ backgroundColor: color,
+ color: getTextColor(color),
+ display: 'inline-block',
+ fontSize: Math.min(size / 2, 14),
+ fontWeight: 'normal',
+ height: size,
+ lineHeight: `${size}px`,
+ textAlign: 'center',
+ verticalAlign: 'top',
+ width: size
+ }}>
+ {text.toUpperCase()}
+ </div>
+ );
+}
+
+/* eslint-disable no-bitwise, no-mixed-operators */
+function stringToColor(str: string) {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ let color = '#';
+ for (let i = 0; i < 3; i++) {
+ const value = (hash >> (i * 8)) & 0xff;
+ color += ('00' + value.toString(16)).substr(-2);
+ }
+ return color;
+}
+
+function getTextColor(background: string) {
+ const rgb = parseInt(background.substr(1), 16);
+ const r = (rgb >> 16) & 0xff;
+ const g = (rgb >> 8) & 0xff;
+ const b = (rgb >> 0) & 0xff;
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ return luma > 140 ? '#222' : '#fff';
+}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`falls back to dummy avatar 1`] = `
-<div
- className="rounded"
- style={
- Object {
- "backgroundColor": "#79e189",
- "color": "#222",
- "display": "inline-block",
- "fontSize": 14,
- "fontWeight": "normal",
- "height": 30,
- "lineHeight": "30px",
- "textAlign": "center",
- "verticalAlign": "top",
- "width": 30,
- }
- }
->
- FB
-</div>
+<GenericAvatar
+ name="Foo Bar"
+ size={30}
+/>
`;
exports[`should be able to render with hash only 1`] = `
my_account.projects.analyzed_x=Analyzed {0}
my_account.projects.never_analyzed=Never analyzed
my_account.organizations=Organizations
-my_account.organizations.description=Those organizations are the ones you are administering.
-my_account.organizations.no_results=You are not administering any organizations yet.
+my_account.organizations.description=Those organizations are the ones you are member of.
+my_account.organizations.no_results=You are not a member of any organizations yet.
my_account.create_organization=Create Organization
my_account.search_project=Search Project
my_account.set_notifications_for=Set notifications for