]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10182 Users should be able to choose their homepage
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 13 Dec 2017 12:44:12 +0000 (13:44 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 2 Jan 2018 09:38:10 +0000 (10:38 +0100)
58 files changed:
server/sonar-web/src/main/js/api/users.ts
server/sonar-web/src/main/js/app/components/Landing.tsx
server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/Search.css
server/sonar-web/src/main/js/app/styles/init/forms.css
server/sonar-web/src/main/js/app/styles/init/icons.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/explore/Explore.tsx
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/issues/components/PageActions.js
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap
server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx
server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap
server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx
server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/nav/ContextNavBar.css
server/sonar-web/src/main/js/helpers/urls.ts
server/sonar-web/src/main/js/store/users/actions.js [deleted file]
server/sonar-web/src/main/js/store/users/actions.ts [new file with mode: 0644]
server/sonar-web/src/main/js/store/users/reducer.js [deleted file]
server/sonar-web/src/main/js/store/users/reducer.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index fa1b73f80fa343b3ed793233192168457bacf3ac..7b3f4de1b773f438fe3c8729b16ce7f8e1837319 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { getJSON, post, postJSON, RequestData } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { CurrentUser, Paging } from '../app/types';
+import { Paging, HomePage, CurrentUser } from '../app/types';
 
 export interface IdentityProvider {
   backgroundColor: string;
@@ -102,3 +102,7 @@ export function deactivateUser(data: { login: string }): Promise<User> {
 export function skipOnboarding(): Promise<void | Response> {
   return post('/api/users/skip_onboarding_tutorial').catch(throwGlobalError);
 }
+
+export function setHomePage(homepage: HomePage): Promise<void | Response> {
+  return post('/api/users/set_homepage', homepage).catch(throwGlobalError);
+}
index 792826f10911bd6f53d14e8b80045951ac72e022..cdf7a236ad267deff81d030c1cf4306cbe820c07 100644 (file)
@@ -20,8 +20,9 @@
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
 import { connect } from 'react-redux';
-import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer';
 import { CurrentUser, isLoggedIn } from '../types';
+import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer';
+import { getHomePageUrl } from '../../helpers/urls';
 
 interface Props {
   currentUser: CurrentUser;
@@ -36,7 +37,12 @@ class Landing extends React.PureComponent<Props> {
   componentDidMount() {
     const { currentUser, onSonarCloud } = this.props;
     if (isLoggedIn(currentUser)) {
-      this.context.router.replace('/projects');
+      if (onSonarCloud && currentUser.homepage) {
+        const homepage = getHomePageUrl(currentUser.homepage);
+        this.context.router.replace(homepage);
+      } else {
+        this.context.router.replace('/projects');
+      }
     } else if (onSonarCloud) {
       window.location.href = 'https://about.sonarcloud.io';
     } else {
index a4ec0a4c324875d1fa47b8d508c3d685c2abd20e..a20b7e80dd2400564c8a77d95ce260c9aaddbc8e 100644 (file)
@@ -88,7 +88,7 @@ export default class GlobalHelp extends React.PureComponent {
 
   renderMenu = () => (
     <ul className="side-tabs-menu">
-      {(this.props.currentUser.isLoggedIn
+      {(this.props.currentUser.isLoggedIn && !this.props.onSonarCloud
         ? ['shortcuts', 'tutorials', 'links']
         : ['shortcuts', 'links']
       ).map(this.renderMenuItem)}
index ccfd5d9c63e497e7e61e06dc5d04de81e19a9a4b..fd0f3a46df1fe9d08f36e4cb5329158a99697f0d 100644 (file)
@@ -1,17 +1,3 @@
-.navbar-context-favorite {
-  display: inline-block;
-  vertical-align: top;
-  padding-top: var(--gridSize);
-  padding-left: calc(1.5 * var(--gridSize));
-}
-
-.navbar-context-title-qualifier {
-  display: inline-block;
-  line-height: 16px;
-  padding-top: 5px;
-  box-sizing: border-box;
-}
-
 .navbar-context-branches {
   display: inline-block;
   vertical-align: top;
@@ -20,11 +6,6 @@
   line-height: 16px;
 }
 
-.navbar-context-meta-branch {
-  margin-top: 3px;
-  line-height: 16px;
-}
-
 .navbar-context-meta-branch-menu-item {
   display: flex !important;
   justify-content: space-between;
index 2dbc6fe9ea480ffaf5fcbf84c7ded8ee8796c2a4..ff2205adbb6347ce3b8ba583c0a3b6daada86852 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import ComponentNavFavorite from './ComponentNavFavorite';
 import ComponentNavBranch from './ComponentNavBranch';
 import ComponentNavBreadcrumbs from './ComponentNavBreadcrumbs';
 import ComponentNavMeta from './ComponentNavMeta';
@@ -112,14 +111,7 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
         id="context-navigation"
         height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
         notif={notifComponent}>
-        <ComponentNavBreadcrumbs
-          component={this.props.component}
-          breadcrumbs={this.props.component.breadcrumbs}
-        />
-        <ComponentNavFavorite
-          component={this.props.component.key}
-          favorite={this.props.component.isFavorite}
-        />
+        <ComponentNavBreadcrumbs component={this.props.component} />
         {this.props.currentBranch && (
           <ComponentNavBranch
             branches={this.props.branches}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
deleted file mode 100644 (file)
index 23138af..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * 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 PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-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 = {
-    breadcrumbs: PropTypes.array,
-    component: PropTypes.shape({
-      visibility: PropTypes.string
-    }).isRequired
-  };
-
-  render() {
-    const { breadcrumbs, component, organization, shouldOrganizationBeDisplayed } = this.props;
-
-    if (!breadcrumbs) {
-      return null;
-    }
-
-    const displayOrganization = organization != null && shouldOrganizationBeDisplayed;
-
-    const lastItem = breadcrumbs[breadcrumbs.length - 1];
-
-    const items = breadcrumbs.map((item, index) => {
-      const isPath = item.qualifier === 'DIR';
-      const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name);
-      return (
-        <span key={item.key}>
-          {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={getProjectUrl(item.key)}>
-            {itemName}
-          </Link>
-          {index < breadcrumbs.length - 1 && <span className="slash-separator" />}
-        </span>
-      );
-    });
-
-    return (
-      <h1 className="navbar-context-header">
-        <OrganizationHelmet
-          title={component.name}
-          organization={displayOrganization ? organization : null}
-        />
-        {displayOrganization && (
-          <span>
-            <OrganizationAvatar organization={organization} />
-            <OrganizationLink
-              organization={organization}
-              className="link-base-color link-no-underline spacer-left">
-              {organization.name}
-            </OrganizationLink>
-            <span className="slash-separator" />
-          </span>
-        )}
-        {items}
-        {component.visibility === 'private' && (
-          <PrivateBadge className="spacer-left" qualifier={component.qualifier} />
-        )}
-      </h1>
-    );
-  }
-}
-
-const mapStateToProps = (state, ownProps) => ({
-  organization:
-    ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization),
-  shouldOrganizationBeDisplayed: areThereCustomOrganizations(state)
-});
-
-export default connect(mapStateToProps)(ComponentNavBreadcrumbs);
-
-export const Unconnected = ComponentNavBreadcrumbs;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx
new file mode 100644 (file)
index 0000000..c8a75e8
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { Link } from 'react-router';
+import { Component, Organization } from '../../../types';
+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';
+
+interface StateProps {
+  organization?: Organization;
+  shouldOrganizationBeDisplayed: boolean;
+}
+
+interface OwnProps {
+  component: Component;
+}
+
+interface Props extends StateProps, OwnProps {}
+
+export function ComponentNavBreadcrumbs(props: Props) {
+  const { component, organization, shouldOrganizationBeDisplayed } = props;
+  const { breadcrumbs } = component;
+
+  const lastItem = breadcrumbs[breadcrumbs.length - 1];
+
+  const items: JSX.Element[] = [];
+  breadcrumbs.forEach((item, index) => {
+    const isPath = item.qualifier === 'DIR';
+    const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name);
+
+    if (index === 0) {
+      items.push(
+        <QualifierIcon
+          className="spacer-right"
+          key={`qualifier-${item.key}`}
+          qualifier={lastItem.qualifier}
+        />
+      );
+    }
+
+    items.push(
+      <Link
+        className="link-base-color link-no-underline"
+        key={`name-${item.key}`}
+        title={item.name}
+        to={getProjectUrl(item.key)}>
+        {itemName}
+      </Link>
+    );
+
+    if (index < breadcrumbs.length - 1) {
+      items.push(<span className="slash-separator" key={`separator-${item.key}`} />);
+    }
+  });
+
+  return (
+    <header className="navbar-context-header">
+      <OrganizationHelmet
+        title={component.name}
+        organization={organization && shouldOrganizationBeDisplayed ? organization : undefined}
+      />
+      {organization &&
+        shouldOrganizationBeDisplayed && <OrganizationAvatar organization={organization} />}
+      {organization &&
+        shouldOrganizationBeDisplayed && (
+          <OrganizationLink
+            organization={organization}
+            className="link-base-color link-no-underline spacer-left">
+            {organization.name}
+          </OrganizationLink>
+        )}
+      {organization && shouldOrganizationBeDisplayed && <span className="slash-separator" />}
+      {items}
+      {component.visibility === 'private' && (
+        <PrivateBadge className="spacer-left" qualifier={component.qualifier} />
+      )}
+    </header>
+  );
+}
+
+const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => ({
+  organization:
+    ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization),
+  shouldOrganizationBeDisplayed: areThereCustomOrganizations(state)
+});
+
+export default connect(mapStateToProps)(ComponentNavBreadcrumbs);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js
deleted file mode 100644 (file)
index 9d6c59c..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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 PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import Favorite from '../../../../components/controls/Favorite';
-import { getCurrentUser } from '../../../../store/rootReducer';
-
-class ComponentNavFavorite extends React.PureComponent {
-  static propTypes = {
-    currentUser: PropTypes.object.isRequired
-  };
-
-  render() {
-    if (!this.props.currentUser.isLoggedIn) {
-      return null;
-    }
-
-    return (
-      <div className="navbar-context-favorite">
-        <Favorite component={this.props.component} favorite={this.props.favorite} />
-      </div>
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(ComponentNavFavorite);
index 0e157ebe2194c2f9cb02291671c49c668431d17d..01be74777a6bf4048e3eaf7851cf279e2e257228 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { connect } from 'react-redux';
+import { Branch, Component, CurrentUser, isLoggedIn } from '../../../types';
 import BranchStatus from '../../../../components/common/BranchStatus';
-import { Branch, Component } from '../../../types';
 import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
+import Favorite from '../../../../components/controls/Favorite';
+import HomePageSelect from '../../../../components/controls/HomePageSelect';
 import Tooltip from '../../../../components/controls/Tooltip';
 import { isShortLivingBranch } from '../../../../helpers/branches';
 import { translate } from '../../../../helpers/l10n';
+import { getCurrentUser } from '../../../../store/rootReducer';
 
-interface Props {
+interface StateProps {
+  currentUser: CurrentUser;
+}
+
+interface Props extends StateProps {
   branch?: Branch;
   component: Component;
 }
 
-export default function ComponentNavMeta(props: Props) {
-  const shortBranch = props.branch && isShortLivingBranch(props.branch);
-  const showVersion = props.component.version && !shortBranch;
+export function ComponentNavMeta({ branch, component, currentUser }: Props) {
+  const shortBranch = branch && isShortLivingBranch(branch);
+  const mainBranch = !branch || branch.isMain;
 
   return (
     <div className="navbar-context-meta">
-      <ul className="list-inline">
-        {props.component.analysisDate && (
-          <li>
-            <DateTimeFormatter date={props.component.analysisDate} />
-          </li>
-        )}
-        {showVersion && (
-          <li>
-            <Tooltip
-              overlay={`${translate('version')} ${props.component.version}`}
-              mouseEnterDelay={0.5}>
-              <span className="text-limited">
-                {translate('version')} {props.component.version}
-              </span>
-            </Tooltip>
-          </li>
-        )}
-      </ul>
-      {shortBranch && (
-        <div className="navbar-context-meta-branch">
-          <BranchStatus branch={props.branch!} />
+      {component.analysisDate && (
+        <div className="spacer-left">
+          <DateTimeFormatter date={component.analysisDate} />
         </div>
       )}
+      {component.version &&
+        !shortBranch && (
+          <Tooltip overlay={`${translate('version')} ${component.version}`} mouseEnterDelay={0.5}>
+            <div className="spacer-left text-limited">
+              {translate('version')} {component.version}
+            </div>
+          </Tooltip>
+        )}
+      {isLoggedIn(currentUser) &&
+        mainBranch && (
+          <div className="navbar-context-meta-secondary">
+            <Favorite component={component.key} favorite={Boolean(component.isFavorite)} />
+            <HomePageSelect
+              className="spacer-left"
+              currentPage={{ type: 'project', key: component.key }}
+            />
+          </div>
+        )}
+      {shortBranch && <BranchStatus branch={branch!} />}
     </div>
   );
 }
+
+const mapStateToProps = (state: any): StateProps => ({
+  currentUser: getCurrentUser(state)
+});
+
+export default connect(mapStateToProps)(ComponentNavMeta);
index 170bce7eb271d44b011ab6e9ae2f8800face22c8..26f876d34f06e8e642f8dff38c436f79342e4f44 100644 (file)
@@ -22,9 +22,9 @@ import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import ComponentNav from '../ComponentNav';
 
-jest.mock('../ComponentNavFavorite', () => ({
+jest.mock('../ComponentNavMeta', () => ({
   // eslint-disable-next-line
-  default: function ComponentNavFavorite() {
+  default: function ComponentNavMeta() {
     return null;
   }
 }));
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js
deleted file mode 100644 (file)
index 3a524a9..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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 { Unconnected } from '../ComponentNavBreadcrumbs';
-
-it('should not render breadcrumbs with one element', () => {
-  const component = {
-    key: 'my-project',
-    name: 'My Project',
-    qualifier: 'TRK',
-    visibility: 'public'
-  };
-  const breadcrumbs = [component];
-  const result = shallow(<Unconnected breadcrumbs={breadcrumbs} component={component} />);
-  expect(result).toMatchSnapshot();
-});
-
-it('should render organization', () => {
-  const component = {
-    key: 'my-project',
-    name: 'My Project',
-    organization: 'foo',
-    qualifier: 'TRK',
-    visibility: 'public'
-  };
-  const breadcrumbs = [component];
-  const organization = { key: 'foo', name: 'The Foo Organization' };
-  const result = shallow(
-    <Unconnected
-      breadcrumbs={breadcrumbs}
-      component={component}
-      organization={organization}
-      shouldOrganizationBeDisplayed={true}
-    />
-  );
-  expect(result).toMatchSnapshot();
-});
-
-it('renders private badge', () => {
-  const component = {
-    key: 'my-project',
-    name: 'My Project',
-    qualifier: 'TRK',
-    visibility: 'private'
-  };
-  const breadcrumbs = [component];
-  const result = shallow(<Unconnected breadcrumbs={breadcrumbs} component={component} />);
-  expect(result.find('PrivateBadge')).toHaveLength(1);
-});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx
new file mode 100644 (file)
index 0000000..78a2eed
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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 { ComponentNavBreadcrumbs } from '../ComponentNavBreadcrumbs';
+import { Visibility } from '../../../../types';
+
+it('should not render breadcrumbs with one element', () => {
+  const component = {
+    breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
+    key: 'my-project',
+    name: 'My Project',
+    organization: 'org',
+    qualifier: 'TRK',
+    visibility: 'public'
+  };
+  const result = shallow(
+    <ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} />
+  );
+  expect(result).toMatchSnapshot();
+});
+
+it('should render organization', () => {
+  const component = {
+    breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
+    key: 'my-project',
+    name: 'My Project',
+    organization: 'foo',
+    qualifier: 'TRK',
+    visibility: 'public'
+  };
+  const organization = {
+    key: 'foo',
+    name: 'The Foo Organization',
+    projectVisibility: Visibility.Public
+  };
+  const result = shallow(
+    <ComponentNavBreadcrumbs
+      component={component}
+      organization={organization}
+      shouldOrganizationBeDisplayed={true}
+    />
+  );
+  expect(result).toMatchSnapshot();
+});
+
+it('renders private badge', () => {
+  const component = {
+    breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
+    key: 'my-project',
+    name: 'My Project',
+    organization: 'org',
+    qualifier: 'TRK',
+    visibility: 'private'
+  };
+  const result = shallow(
+    <ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} />
+  );
+  expect(result.find('PrivateBadge')).toHaveLength(1);
+});
index 331c7c034e73041fd1ecc2873d64085f9f06cb77..b989df1214264fc32d91ed7a2c8d0a19e1a04743 100644 (file)
@@ -19,7 +19,7 @@
  */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import ComponentNavMeta from '../ComponentNavMeta';
+import { ComponentNavMeta } from '../ComponentNavMeta';
 import { BranchType, ShortLivingBranch, LongLivingBranch } from '../../../../types';
 
 const component = {
@@ -40,7 +40,11 @@ it('renders status of short-living branch', () => {
     status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 },
     type: BranchType.SHORT
   };
-  expect(shallow(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot();
+  expect(
+    shallow(
+      <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} />
+    )
+  ).toMatchSnapshot();
 });
 
 it('renders meta for long-living branch', () => {
@@ -50,5 +54,9 @@ it('renders meta for long-living branch', () => {
     status: { qualityGateStatus: 'OK' },
     type: BranchType.LONG
   };
-  expect(shallow(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot();
+  expect(
+    shallow(
+      <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} />
+    )
+  ).toMatchSnapshot();
 });
index 1baaad7dc1fdb98820f2214b38695f71a38a6ea1..6fea2eff38eb4846c9737c232168397378889af2 100644 (file)
@@ -28,15 +28,6 @@ exports[`renders 1`] = `
   }
 >
   <ComponentNavBreadcrumbs
-    breadcrumbs={
-      Array [
-        Object {
-          "key": "component",
-          "name": "component",
-          "qualifier": "TRK",
-        },
-      ]
-    }
     component={
       Object {
         "breadcrumbs": Array [
@@ -53,9 +44,6 @@ exports[`renders 1`] = `
       }
     }
   />
-  <ComponentNavFavorite
-    component="component"
-  />
   <ComponentNavMeta
     component={
       Object {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
deleted file mode 100644 (file)
index 99cf4b7..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not render breadcrumbs with one element 1`] = `
-<h1
-  className="navbar-context-header"
->
-  <OrganizationHelmet
-    organization={null}
-    title="My Project"
-  />
-  <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}
-      style={Object {}}
-      title="My Project"
-      to={
-        Object {
-          "pathname": "/dashboard",
-          "query": Object {
-            "branch": undefined,
-            "id": "my-project",
-          },
-        }
-      }
-    >
-      My Project
-    </Link>
-  </span>
-</h1>
-`;
-
-exports[`should render organization 1`] = `
-<h1
-  className="navbar-context-header"
->
-  <OrganizationHelmet
-    organization={
-      Object {
-        "key": "foo",
-        "name": "The Foo Organization",
-      }
-    }
-    title="My Project"
-  />
-  <span>
-    <OrganizationAvatar
-      organization={
-        Object {
-          "key": "foo",
-          "name": "The Foo Organization",
-        }
-      }
-    />
-    <OrganizationLink
-      className="link-base-color link-no-underline spacer-left"
-      organization={
-        Object {
-          "key": "foo",
-          "name": "The Foo Organization",
-        }
-      }
-    >
-      The Foo Organization
-    </OrganizationLink>
-    <span
-      className="slash-separator"
-    />
-  </span>
-  <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}
-      style={Object {}}
-      title="My Project"
-      to={
-        Object {
-          "pathname": "/dashboard",
-          "query": Object {
-            "branch": undefined,
-            "id": "my-project",
-          },
-        }
-      }
-    >
-      My Project
-    </Link>
-  </span>
-</h1>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap
new file mode 100644 (file)
index 0000000..e907a97
--- /dev/null
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render breadcrumbs with one element 1`] = `
+<header
+  className="navbar-context-header"
+>
+  <OrganizationHelmet
+    title="My Project"
+  />
+  <QualifierIcon
+    className="spacer-right"
+    key="qualifier-my-project"
+    qualifier="TRK"
+  />
+  <Link
+    className="link-base-color link-no-underline"
+    key="name-my-project"
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    title="My Project"
+    to={
+      Object {
+        "pathname": "/dashboard",
+        "query": Object {
+          "branch": undefined,
+          "id": "my-project",
+        },
+      }
+    }
+  >
+    My Project
+  </Link>
+</header>
+`;
+
+exports[`should render organization 1`] = `
+<header
+  className="navbar-context-header"
+>
+  <OrganizationHelmet
+    organization={
+      Object {
+        "key": "foo",
+        "name": "The Foo Organization",
+        "projectVisibility": "public",
+      }
+    }
+    title="My Project"
+  />
+  <OrganizationAvatar
+    organization={
+      Object {
+        "key": "foo",
+        "name": "The Foo Organization",
+        "projectVisibility": "public",
+      }
+    }
+  />
+  <OrganizationLink
+    className="link-base-color link-no-underline spacer-left"
+    organization={
+      Object {
+        "key": "foo",
+        "name": "The Foo Organization",
+        "projectVisibility": "public",
+      }
+    }
+  >
+    The Foo Organization
+  </OrganizationLink>
+  <span
+    className="slash-separator"
+  />
+  <QualifierIcon
+    className="spacer-right"
+    key="qualifier-my-project"
+    qualifier="TRK"
+  />
+  <Link
+    className="link-base-color link-no-underline"
+    key="name-my-project"
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    title="My Project"
+    to={
+      Object {
+        "pathname": "/dashboard",
+        "query": Object {
+          "branch": undefined,
+          "id": "my-project",
+        },
+      }
+    }
+  >
+    My Project
+  </Link>
+</header>
+`;
index a1dd339dfce14c0256dc20b675311057231e92b7..726f3168f9ff9cca949581bf0d0090a3e15db10b 100644 (file)
@@ -4,30 +4,26 @@ exports[`renders meta for long-living branch 1`] = `
 <div
   className="navbar-context-meta"
 >
-  <ul
-    className="list-inline"
+  <div
+    className="spacer-left"
+  >
+    <DateTimeFormatter
+      date="2017-01-02T00:00:00.000Z"
+    />
+  </div>
+  <Tooltip
+    mouseEnterDelay={0.5}
+    overlay="version 0.0.1"
+    placement="bottom"
   >
-    <li>
-      <DateTimeFormatter
-        date="2017-01-02T00:00:00.000Z"
-      />
-    </li>
-    <li>
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 0.0.1"
-        placement="bottom"
-      >
-        <span
-          className="text-limited"
-        >
-          version
-           
-          0.0.1
-        </span>
-      </Tooltip>
-    </li>
-  </ul>
+    <div
+      className="spacer-left text-limited"
+    >
+      version
+       
+      0.0.1
+    </div>
+  </Tooltip>
 </div>
 `;
 
@@ -35,33 +31,27 @@ exports[`renders status of short-living branch 1`] = `
 <div
   className="navbar-context-meta"
 >
-  <ul
-    className="list-inline"
-  >
-    <li>
-      <DateTimeFormatter
-        date="2017-01-02T00:00:00.000Z"
-      />
-    </li>
-  </ul>
   <div
-    className="navbar-context-meta-branch"
+    className="spacer-left"
   >
-    <BranchStatus
-      branch={
-        Object {
-          "isMain": false,
-          "mergeBranch": "master",
-          "name": "feature",
-          "status": Object {
-            "bugs": 0,
-            "codeSmells": 2,
-            "vulnerabilities": 3,
-          },
-          "type": "SHORT",
-        }
-      }
+    <DateTimeFormatter
+      date="2017-01-02T00:00:00.000Z"
     />
   </div>
+  <BranchStatus
+    branch={
+      Object {
+        "isMain": false,
+        "mergeBranch": "master",
+        "name": "feature",
+        "status": Object {
+          "bugs": 0,
+          "codeSmells": 2,
+          "vulnerabilities": 3,
+        },
+        "type": "SHORT",
+      }
+    }
+  />
 </div>
 `;
index 04404a1fe1e3a58b18dbea231537256b9daf8208..107251d362974a72216e373e9a0445ad3061a0f2 100644 (file)
@@ -110,6 +110,15 @@ class GlobalNav extends React.PureComponent<Props, State> {
     }, 3000);
   };
 
+  withTutorialTooltip = (element: React.ReactNode) =>
+    this.state.onboardingTutorialTooltip ? (
+      <Tooltip defaultVisible={true} overlay={translate('tutorials.follow_later')} trigger="manual">
+        {element}
+      </Tooltip>
+    ) : (
+      element
+    );
+
   render() {
     return (
       <NavBar className="navbar-global" id="global-navigation" height={theme.globalNavHeightRaw}>
@@ -121,21 +130,13 @@ class GlobalNav extends React.PureComponent<Props, State> {
           <GlobalNavExplore location={this.props.location} onSonarCloud={this.props.onSonarCloud} />
           <li>
             <a className="navbar-help" onClick={this.handleHelpClick} href="#">
-              {this.state.onboardingTutorialTooltip ? (
-                <Tooltip
-                  defaultVisible={true}
-                  overlay={translate('tutorials.follow_later')}
-                  trigger="manual">
-                  <HelpIcon />
-                </Tooltip>
-              ) : (
-                <HelpIcon />
-              )}
+              {this.props.onSonarCloud ? <HelpIcon /> : this.withTutorialTooltip(<HelpIcon />)}
             </a>
           </li>
           <Search appState={this.props.appState} currentUser={this.props.currentUser} />
           {isLoggedIn(this.props.currentUser) &&
-            this.props.onSonarCloud && (
+            this.props.onSonarCloud &&
+            this.withTutorialTooltip(
               <GlobalNavPlus openOnboardingTutorial={this.openOnboardingTutorial} />
             )}
           <GlobalNavUserContainer {...this.props} />
index 50277e52187f6cdda0a5ff41b316bb6e03560f83..ba7dfcdbf64b841b1773fb95ca63fa8f0c352ecf 100644 (file)
@@ -28,7 +28,7 @@ interface Props {
   appState: AppState;
   currentUser: CurrentUser;
   location: { pathname: string };
-  onSonarCloud: boolean;
+  onSonarCloud?: boolean;
 }
 
 export default class GlobalNavMenu extends React.PureComponent<Props> {
index fb19ba06d40435a20e41363dbfa3cb41b07f7aca..b2e8119ec2a23b94cb984abfb19872951171a646 100644 (file)
@@ -31,7 +31,7 @@ import { getBaseUrl } from '../../../../helpers/urls';
 import Dropdown from '../../../../components/controls/Dropdown';
 
 interface Props {
-  appState: { organizationsEnabled: boolean };
+  appState: { organizationsEnabled?: boolean };
   currentUser: CurrentUser;
   organizations: Organization[];
 }
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js
deleted file mode 100644 (file)
index 33546be..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 GlobalNavMenu from '../GlobalNavMenu';
-
-it('should work with extensions', () => {
-  const appState = {
-    globalPages: [{ key: 'foo', name: 'Foo' }],
-    qualifiers: ['TRK']
-  };
-  const currentUser = {
-    isLoggedIn: false
-  };
-  const wrapper = shallow(
-    <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should show administration menu if the user has the rights', () => {
-  const appState = {
-    canAdmin: true,
-    globalPages: [],
-    qualifiers: ['TRK']
-  };
-  const currentUser = {
-    isLoggedIn: false
-  };
-  const wrapper = shallow(
-    <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
new file mode 100644 (file)
index 0000000..7b3afe8
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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 GlobalNavMenu from '../GlobalNavMenu';
+
+it('should work with extensions', () => {
+  const appState = {
+    globalPages: [{ key: 'foo', name: 'Foo' }],
+    qualifiers: ['TRK']
+  };
+  const currentUser = {
+    isLoggedIn: false
+  };
+  const wrapper = shallow(
+    <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should show administration menu if the user has the rights', () => {
+  const appState = {
+    canAdmin: true,
+    globalPages: [],
+    qualifiers: ['TRK']
+  };
+  const currentUser = {
+    isLoggedIn: false
+  };
+  const wrapper = shallow(
+    <GlobalNavMenu appState={appState} currentUser={currentUser} location={{ pathname: '' }} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
deleted file mode 100644 (file)
index 34dd8f9..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should show administration menu if the user has the rights 1`] = `
-<ul
-  className="global-navbar-menu pull-left"
->
-  <li>
-    <Link
-      activeClassName="active"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/projects"
-    >
-      projects.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to={
-        Object {
-          "pathname": "/issues",
-          "query": Object {
-            "resolved": "false",
-          },
-        }
-      }
-    >
-      issues.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/coding_rules"
-    >
-      coding_rules.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      activeClassName="active"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/profiles"
-    >
-      quality_profiles.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      activeClassName="active"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to={
-        Object {
-          "pathname": "/quality_gates",
-        }
-      }
-    >
-      quality_gates.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      activeClassName="active"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/admin"
-    >
-      layout.settings
-    </Link>
-  </li>
-</ul>
-`;
-
-exports[`should work with extensions 1`] = `
-<ul
-  className="global-navbar-menu pull-left"
->
-  <li>
-    <Link
-      activeClassName="active"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/projects"
-    >
-      projects.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to={
-        Object {
-          "pathname": "/issues",
-          "query": Object {
-            "resolved": "false",
-          },
-        }
-      }
-    >
-      issues.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/coding_rules"
-    >
-      coding_rules.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      activeClassName="active"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/profiles"
-    >
-      quality_profiles.page
-    </Link>
-  </li>
-  <li>
-    <Link
-      activeClassName="active"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to={
-        Object {
-          "pathname": "/quality_gates",
-        }
-      }
-    >
-      quality_gates.page
-    </Link>
-  </li>
-  <li
-    className="dropdown"
-  >
-    <a
-      className="dropdown-toggle"
-      data-toggle="dropdown"
-      href="#"
-      id="global-navigation-more"
-    >
-      more
-      Â 
-      <span
-        className="icon-dropdown"
-      />
-    </a>
-    <ul
-      className="dropdown-menu"
-    >
-      <li
-        key="foo"
-      >
-        <Link
-          onlyActiveOnIndex={false}
-          style={Object {}}
-          to="/extension/foo"
-        >
-          Foo
-        </Link>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap
new file mode 100644 (file)
index 0000000..34dd8f9
--- /dev/null
@@ -0,0 +1,174 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should show administration menu if the user has the rights 1`] = `
+<ul
+  className="global-navbar-menu pull-left"
+>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/projects"
+    >
+      projects.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/issues",
+          "query": Object {
+            "resolved": "false",
+          },
+        }
+      }
+    >
+      issues.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/coding_rules"
+    >
+      coding_rules.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/profiles"
+    >
+      quality_profiles.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/quality_gates",
+        }
+      }
+    >
+      quality_gates.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/admin"
+    >
+      layout.settings
+    </Link>
+  </li>
+</ul>
+`;
+
+exports[`should work with extensions 1`] = `
+<ul
+  className="global-navbar-menu pull-left"
+>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/projects"
+    >
+      projects.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/issues",
+          "query": Object {
+            "resolved": "false",
+          },
+        }
+      }
+    >
+      issues.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/coding_rules"
+    >
+      coding_rules.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/profiles"
+    >
+      quality_profiles.page
+    </Link>
+  </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/quality_gates",
+        }
+      }
+    >
+      quality_gates.page
+    </Link>
+  </li>
+  <li
+    className="dropdown"
+  >
+    <a
+      className="dropdown-toggle"
+      data-toggle="dropdown"
+      href="#"
+      id="global-navigation-more"
+    >
+      more
+      Â 
+      <span
+        className="icon-dropdown"
+      />
+    </a>
+    <ul
+      className="dropdown-menu"
+    >
+      <li
+        key="foo"
+      >
+        <Link
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to="/extension/foo"
+        >
+          Foo
+        </Link>
+      </li>
+    </ul>
+  </li>
+</ul>
+`;
index 54d91d77c75ea0f13551e3b7d6814c960c9feb77..f45d95441dcc44326d60ffa6759489bf7b41d369 100644 (file)
@@ -206,7 +206,9 @@ export default class SettingsNav extends React.PureComponent<Props> {
         id="context-navigation"
         height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
         notif={notifComponent}>
-        <h1 className="navbar-context-header">{translate('layout.settings')}</h1>
+        <header className="navbar-context-header">
+          <h1>{translate('layout.settings')}</h1>
+        </header>
 
         <NavBarTabs>
           {this.renderConfigurationTab()}
index 3208552f512b664ef8ef5e5ef94d345be1bf8312..64822b179f66e14d2685a907a4f74be9318b1de7 100644 (file)
@@ -5,11 +5,13 @@ exports[`should work with extensions 1`] = `
   height={72}
   id="context-navigation"
 >
-  <h1
+  <header
     className="navbar-context-header"
   >
-    layout.settings
-  </h1>
+    <h1>
+      layout.settings
+    </h1>
+  </header>
   <NavBarTabs>
     <li
       className="dropdown"
index 028707705cff7e90c0fe1d249e5fc54a13bfd86f..033eda0c07a4076b1762800634db86e2e2e0949e 100644 (file)
@@ -81,7 +81,7 @@
   left: 0;
 }
 
-.navbar-search-item-icons > .icon-star,
+.navbar-search-item-icons > .icon-outline,
 .navbar-search-item-icons > .icon-clock {
   z-index: 6;
   top: -4px;
index 97287e21ac06157dd654b13c2fe44861d6a7d51f..d63fe1e3b1558676c9954f1049750d0db7b1bdf2 100644 (file)
@@ -304,7 +304,7 @@ input[type='submit'].button-grey.button-active {
 }
 
 .button-small > svg {
-  margin-top: 2px;
+  padding-top: 2px;
 }
 
 .button-group {
index 315803eb185966993195ff53916ee44a871220aa..2033873f7842097d0940b0a334ceca8f68cd1e85 100644 (file)
@@ -476,11 +476,11 @@ a:hover > .icon-radio {
   background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M15.428%205.777c0%20.13-.078.274-.233.428l-3.24%203.16.767%204.465c.006.042.01.102.01.18%200%20.124-.032.23-.095.316-.062.086-.153.13-.272.13-.113%200-.232-.036-.357-.108l-4.01-2.107L3.99%2014.35c-.13.072-.25.107-.357.107-.125%200-.22-.043-.28-.13-.064-.085-.095-.19-.095-.316%200-.037.006-.096.018-.18l.768-4.464-3.25-3.16C.644%206.045.57%205.9.57%205.775c0-.22.167-.356.5-.41l4.482-.652L7.562.652c.112-.244.258-.366.437-.366.177%200%20.323.122.436.366l2.01%204.062%204.48.652c.335.054.5.19.5.41h.002z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E');
 }
 
-.icon-star {
+.icon-outline {
   transition: all 0.2s ease !important;
 }
 
-.icon-star path {
+.icon-outline path {
   stroke: var(--secondFontColor);
   stroke-width: 1.41421356;
   stroke-opacity: 1;
@@ -488,9 +488,9 @@ a:hover > .icon-radio {
   transition: all 0.2s ease;
 }
 
-.icon-star-favorite path {
-  fill: #ff9900;
-  stroke-opacity: 0;
+.icon-outline.is-filled path {
+  fill: currentColor;
+  stroke: currentColor;
   fill-opacity: 1;
 }
 
index 2b2bc7a64a8802dfd1a3837bd3756cb5ec87e6e6..70df316dea2d038bd94db8058534ee6571a95912 100644 (file)
@@ -80,6 +80,7 @@ export interface Component {
   qualifier: string;
   refKey?: string;
   version?: string;
+  visibility?: string;
 }
 
 interface ComponentConfiguration {
@@ -140,9 +141,19 @@ export interface CurrentUser {
   showOnboardingTutorial?: boolean;
 }
 
+export interface HomePage {
+  key?: string;
+  type: string;
+}
+
+export function isSameHomePage(a: HomePage, b: HomePage) {
+  return a.type === b.type && a.key === b.key;
+}
+
 export interface LoggedInUser extends CurrentUser {
   avatar?: string;
   email?: string;
+  homepage?: HomePage;
   isLoggedIn: true;
   name: string;
 }
@@ -153,10 +164,10 @@ export function isLoggedIn(user: CurrentUser): user is LoggedInUser {
 
 export interface AppState {
   adminPages?: Extension[];
-  authenticationError: boolean;
-  authorizationError: boolean;
+  authenticationError?: boolean;
+  authorizationError?: boolean;
   canAdmin?: boolean;
   globalPages?: Extension[];
-  organizationsEnabled: boolean;
+  organizationsEnabled?: boolean;
   qualifiers: string[];
 }
index a1ed154d36af16d54184e463e79bc7b97d829648..62510555da883b9570c2cb13afdaee26f96509ba 100644 (file)
@@ -32,9 +32,9 @@ export default function Explore(props: Props) {
   return (
     <div id="explore">
       <ContextNavBar id="explore-navigation" height={theme.contextNavHeightRaw}>
-        <div className="navbar-context-header">
-          <h1 className="display-inline-block">{translate('explore')}</h1>
-        </div>
+        <header className="navbar-context-header">
+          <h1>{translate('explore')}</h1>
+        </header>
 
         <NavBarTabs>
           <li>
index a7121ced22de51a1a78b95682ea995197e10d86e..cc68fbb41c9edbc0d721ecb74f7bfc068b07d9f9 100644 (file)
@@ -56,6 +56,7 @@ import {
   CurrentUser
 } from '../utils'; */
 import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import { isLoggedIn } from '../../../app/types';
 import ListFooter from '../../../components/controls/ListFooter';
 import EmptySearch from '../../../components/common/EmptySearch';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
@@ -923,6 +924,13 @@ export default class App extends React.PureComponent {
                   </div>
                 ) : (
                   <PageActions
+                    canSetHome={
+                      this.props.onSonarCloud &&
+                      isLoggedIn(this.props.currentUser) &&
+                      this.props.myIssues &&
+                      !this.props.organization &&
+                      !this.props.component
+                    }
                     loading={this.state.loading}
                     onReload={this.handleReload}
                     paging={paging}
index e939e60869fa5bee38d2b34560437ccc4f7da522..2aaca8517e887153b78078a1d9b6aa5b74b317bb 100644 (file)
  */
 // @flow
 import React from 'react';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
 import IssuesCounter from './IssuesCounter';
 import ReloadButton from './ReloadButton';
 /*:: import type { Paging } from '../utils'; */
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import HomePageSelect from '../../../components/controls/HomePageSelect';
 import { translate } from '../../../helpers/l10n';
 
 /*::
 type Props = {|
+  canSetHome: bool,
   loading: boolean,
   onReload: () => void,
   paging: ?Paging,
@@ -70,6 +72,10 @@ export default class PageActions extends React.PureComponent {
             <IssuesCounter className="spacer-left" current={selectedIndex} total={paging.total} />
           )}
         </div>
+
+        {this.props.canSetHome && (
+          <HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-issues' }} />
+        )}
       </div>
     );
   }
index f61bbd33bd2988498f9a654d357a4abe3e3facbe..05af0bd0bbe518d0e9d0ef2516345da30f36bfd4 100644 (file)
@@ -22,9 +22,9 @@
 }
 
 .organization-switch .dropdown-toggle {
-  display: block;
+  display: flex;
+  align-items: center;
   height: calc(4 * var(--gridSize));
-  line-height: calc(4 * var(--gridSize) - 2px);
   padding: 0 var(--gridSize);
   border: 1px solid transparent;
   border-radius: 2px;
index 84f0256e579342a1efa351a8ba4aa0cb75e8a01a..22e7d9e480d52f68947cdd915a8cf039e59358d4 100644 (file)
@@ -35,29 +35,27 @@ export default function OrganizationNavigationHeader({ organization, organizatio
   const other = organizations.filter(o => o.key !== organization.key);
 
   return (
-    <div className="navbar-context-header">
-      <h1 className="display-inline-block">
-        <OrganizationAvatar organization={organization} />
-        {other.length ? (
-          <Dropdown>
-            {({ onToggleClick, open }) => (
-              <div className={classNames('organization-switch', 'dropdown', { open })}>
-                <a className="dropdown-toggle" href="#" onClick={onToggleClick}>
-                  {organization.name}
-                  <DropdownIcon className="little-spacer-left" />
-                </a>
-                <ul className="dropdown-menu">
-                  {sortBy(other, org => org.name.toLowerCase()).map(organization => (
-                    <OrganizationListItem key={organization.key} organization={organization} />
-                  ))}
-                </ul>
-              </div>
-            )}
-          </Dropdown>
-        ) : (
-          <span className="spacer-left">{organization.name}</span>
-        )}
-      </h1>
+    <header className="navbar-context-header">
+      <OrganizationAvatar organization={organization} />
+      {other.length ? (
+        <Dropdown>
+          {({ onToggleClick, open }) => (
+            <div className={classNames('organization-switch', 'dropdown', { open })}>
+              <a className="dropdown-toggle" href="#" onClick={onToggleClick}>
+                {organization.name}
+                <DropdownIcon className="little-spacer-left" />
+              </a>
+              <ul className="dropdown-menu">
+                {sortBy(other, org => org.name.toLowerCase()).map(organization => (
+                  <OrganizationListItem key={organization.key} organization={organization} />
+                ))}
+              </ul>
+            </div>
+          )}
+        </Dropdown>
+      ) : (
+        <span className="spacer-left">{organization.name}</span>
+      )}
       {organization.description != null && (
         <div className="navbar-context-description">
           <p className="text-limited text-top" title={organization.description}>
@@ -65,6 +63,6 @@ export default function OrganizationNavigationHeader({ organization, organizatio
           </p>
         </div>
       )}
-    </div>
+    </header>
   );
 }
index b720223abe1a524b0b122b84a37884ce5a66fb40..077cc4a67ee67c89b3928ff3ede73b0864c6fd2c 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import { Organization } from '../../../app/types';
+import HomePageSelect from '../../../components/controls/HomePageSelect';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
@@ -28,22 +29,21 @@ interface Props {
 export default function OrganizationNavigationMeta({ organization }: Props) {
   return (
     <div className="navbar-context-meta">
+      {organization.url != null && (
+        <a
+          className="spacer-right text-limited"
+          href={organization.url}
+          title={organization.url}
+          rel="nofollow">
+          {organization.url}
+        </a>
+      )}
       <div className="text-muted">
         <strong>{translate('organization.key')}:</strong> {organization.key}
       </div>
-      {organization.url != null && (
-        <div>
-          <p className="text-limited text-top">
-            <a
-              className="link-underline"
-              href={organization.url}
-              title={organization.url}
-              rel="nofollow">
-              {organization.url}
-            </a>
-          </p>
-        </div>
-      )}
+      <div className="navbar-context-meta-secondary">
+        <HomePageSelect currentPage={{ type: 'organization', key: organization.key }} />
+      </div>
     </div>
   );
 }
index 78f6ea2ae7c4c938c07b4e4f1765819114376be3..949bb39ff041dba9cdbc5678c2039fcc76c6a009 100644 (file)
@@ -1,28 +1,24 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`renders 1`] = `
-<div
+<header
   className="navbar-context-header"
 >
-  <h1
-    className="display-inline-block"
-  >
-    <OrganizationAvatar
-      organization={
-        Object {
-          "key": "foo",
-          "name": "Foo",
-          "projectVisibility": "public",
-        }
+  <OrganizationAvatar
+    organization={
+      Object {
+        "key": "foo",
+        "name": "Foo",
+        "projectVisibility": "public",
       }
-    />
-    <span
-      className="spacer-left"
-    >
-      Foo
-    </span>
-  </h1>
-</div>
+    }
+  />
+  <span
+    className="spacer-left"
+  >
+    Foo
+  </span>
+</header>
 `;
 
 exports[`renders dropdown 1`] = `
index 3da71c2593067e7bd4317e5e9c14f9ae23ba119e..bdd7594127b7b75002f114847d2032dd1081d4bf 100644 (file)
@@ -14,5 +14,17 @@ exports[`renders 1`] = `
      
     foo
   </div>
+  <div
+    className="navbar-context-meta-secondary"
+  >
+    <Connect(HomePageSelect)
+      currentPage={
+        Object {
+          "key": "foo",
+          "type": "organization",
+        }
+      }
+    />
+  </div>
 </div>
 `;
index daa3ffa6fa4c798777fce3eb1ce5f5101cec20bf..15dc8f4e7caf6aced553a6275660c0df6e141ff6 100644 (file)
@@ -263,9 +263,11 @@ export default class AllProjects extends React.PureComponent<Props, State> {
         <div className="layout-page-main-inner">
           <PageHeader
             currentUser={this.props.currentUser}
+            isFavorite={this.props.isFavorite}
             loading={this.state.loading}
             onPerspectiveChange={this.handlePerspectiveChange}
             onQueryChange={this.updateLocationQuery}
+            onSonarCloud={this.props.onSonarCloud}
             onSortChange={this.handleSortChange}
             organization={this.props.organization}
             projects={this.state.projects}
index d8de26017fea1dfdf8294412d0d60fa7fadde7a1..3f347339ad02c1ec3557e923f0d4a93c6e1e748e 100644 (file)
@@ -24,15 +24,18 @@ import Tooltip from '../../../components/controls/Tooltip';
 import PerspectiveSelect from './PerspectiveSelect';
 import ProjectsSortingSelect from './ProjectsSortingSelect';
 import { CurrentUser, isLoggedIn } from '../../../app/types';
+import HomePageSelect from '../../../components/controls/HomePageSelect';
 import { translate } from '../../../helpers/l10n';
 import { RawQuery } from '../../../helpers/query';
 import { Project } from '../types';
 
 interface Props {
   currentUser: CurrentUser;
+  isFavorite: boolean;
   loading: boolean;
   onPerspectiveChange: (x: { view: string; visualization?: string }) => void;
   onQueryChange: (change: RawQuery) => void;
+  onSonarCloud: boolean;
   onSortChange: (sort: string, desc: boolean) => void;
   organization?: { key: string };
   projects?: Project[];
@@ -97,6 +100,13 @@ export default function PageHeader(props: Props) {
           </span>
         )}
       </div>
+
+      {props.onSonarCloud &&
+        isLoggedIn(currentUser) &&
+        props.isFavorite &&
+        !props.organization && (
+          <HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-projects' }} />
+        )}
     </header>
   );
 }
index ec11f7414104aeed5a159166009732ff6fe10382..2c2f8ab5333709032afb0683053925147bd27b85 100644 (file)
@@ -71,9 +71,11 @@ function shallowRender(props?: {}) {
   return shallow(
     <PageHeader
       currentUser={{ isLoggedIn: false }}
+      isFavorite={false}
       loading={false}
       onPerspectiveChange={jest.fn()}
       onQueryChange={jest.fn()}
+      onSonarCloud={false}
       onSortChange={jest.fn()}
       projects={[]}
       query={{ search: 'test' }}
index 40a5ea5b64724f783d0560e5cdaf915ea39abd35..3dd798e4094c993436d1cdb861451f7ca2f58901 100644 (file)
@@ -31,9 +31,11 @@ exports[`renders 1`] = `
                 "isLoggedIn": true,
               }
             }
+            isFavorite={false}
             loading={false}
             onPerspectiveChange={[Function]}
             onQueryChange={[Function]}
+            onSonarCloud={false}
             onSortChange={[Function]}
             projects={
               Array [
@@ -158,9 +160,11 @@ exports[`renders 2`] = `
                 "isLoggedIn": true,
               }
             }
+            isFavorite={false}
             loading={false}
             onPerspectiveChange={[Function]}
             onQueryChange={[Function]}
+            onSonarCloud={false}
             onSortChange={[Function]}
             projects={
               Array [
index 5a1ea7dcaa6178a02fd490ab4665d1956acb6217..c3c52bb74cb04d88acce1833e540aa29026eb2a1 100644 (file)
@@ -165,7 +165,11 @@ export default class Onboarding extends React.PureComponent {
                   {translate('tutorials.skip')}
                 </a>
               )}
-              <p className="note">{translate('tutorials.find_it_back_in_help')}</p>
+              <p className="note">
+                {translate(
+                  sonarCloud ? 'tutorials.find_it_back_in_plus' : 'tutorials.find_it_back_in_help'
+                )}
+              </p>
             </div>
             <div className="page-description">
               {translateWithParameters(
index 4db0fec476c57548aa887987ae5c2a6b8cff8dc6..3ff62c4f7d8221365f1deec365d61d54862dcb2c 100644 (file)
@@ -71,7 +71,7 @@ export default class OrganizationStep extends React.PureComponent {
     getOrganizations({ member: true }).then(
       ({ organizations }) => {
         if (this.mounted) {
-          const organizationKeys = organizations.map(o => o.key);
+          const organizationKeys = organizations.filter(o => o.isAdmin).map(o => o.key);
           // best guess: if there is only one organization, then it is personal
           // otherwise, we can't guess, let's display them all as just "existing organizations"
           const personalOrganization =
index be9c2542a870b349283a7cb8a10797815d41fbdd..dda967a28bfe1f86f059a74d562c7bd10ed2d1c7 100644 (file)
@@ -26,7 +26,9 @@ import { getOrganizations } from '../../../../api/organizations';
 
 jest.mock('../../../../api/organizations', () => ({
   getOrganizations: jest.fn(() =>
-    Promise.resolve({ organizations: [{ key: 'user' }, { key: 'another' }] })
+    Promise.resolve({
+      organizations: [{ isAdmin: true, key: 'user' }, { isAdmin: true, key: 'another' }]
+    })
   )
 }));
 
index 9c24b5b42a499bbe452f98a88bc9da9ff5705a20..57a706a0d5d90a34e993c2b3979d0a7f4a06219e 100644 (file)
@@ -169,7 +169,7 @@ exports[`guides for sonarcloud 1`] = `
         <p
           className="note"
         >
-          tutorials.find_it_back_in_help
+          tutorials.find_it_back_in_plus
         </p>
       </div>
       <div
@@ -249,7 +249,7 @@ exports[`guides for sonarcloud 2`] = `
         <p
           className="note"
         >
-          tutorials.find_it_back_in_help
+          tutorials.find_it_back_in_plus
         </p>
       </div>
       <div
@@ -330,7 +330,7 @@ exports[`guides for sonarcloud 3`] = `
         <p
           className="note"
         >
-          tutorials.find_it_back_in_help
+          tutorials.find_it_back_in_plus
         </p>
       </div>
       <div
index fc76186117b3cc8db6fd3ec0feae398285c54f19..f802e0885f7f3c1dce507a1f6250ad72f6144ed8 100644 (file)
@@ -19,7 +19,9 @@
  */
 import * as React from 'react';
 import * as classNames from 'classnames';
+import Tooltip from './Tooltip';
 import FavoriteIcon from '../icons-components/FavoriteIcon';
+import { translate } from '../../helpers/l10n';
 
 interface Props {
   addFavorite: () => Promise<void>;
@@ -80,13 +82,18 @@ export default class FavoriteBase extends React.PureComponent<Props, State> {
   }
 
   render() {
+    const tooltip = this.state.favorite
+      ? translate('favorite.current')
+      : translate('favorite.check');
     return (
-      <a
-        className={classNames('link-no-underline', this.props.className)}
-        href="#"
-        onClick={this.toggleFavorite}>
-        <FavoriteIcon favorite={this.state.favorite} />
-      </a>
+      <Tooltip overlay={tooltip}>
+        <a
+          className={classNames('display-inline-block', 'link-no-underline', this.props.className)}
+          href="#"
+          onClick={this.toggleFavorite}>
+          <FavoriteIcon favorite={this.state.favorite} />
+        </a>
+      </Tooltip>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
new file mode 100644 (file)
index 0000000..9d68ef7
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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 { connect } from 'react-redux';
+import Tooltip from './Tooltip';
+import HomeIcon from '../icons-components/HomeIcon';
+import { CurrentUser, isLoggedIn, HomePage, isSameHomePage } from '../../app/types';
+import { translate } from '../../helpers/l10n';
+import { getCurrentUser } from '../../store/rootReducer';
+import { setHomePage } from '../../store/users/actions';
+
+interface StateProps {
+  currentUser: CurrentUser;
+}
+
+interface DispatchProps {
+  setHomePage: (homepage: HomePage) => void;
+}
+
+interface Props extends StateProps, DispatchProps {
+  className?: string;
+  currentPage: HomePage;
+}
+
+class HomePageSelect extends React.PureComponent<Props> {
+  handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.setHomePage(this.props.currentPage);
+  };
+
+  render() {
+    const { currentPage, currentUser } = this.props;
+
+    if (!isLoggedIn(currentUser)) {
+      return null;
+    }
+
+    const { homepage } = currentUser;
+    const checked = homepage !== undefined && isSameHomePage(homepage, currentPage);
+    const tooltip = checked ? translate('homepage.current') : translate('homepage.check');
+
+    return (
+      <Tooltip overlay={tooltip}>
+        {checked ? (
+          <span className={classNames('display-inline-block', this.props.className)}>
+            <HomeIcon filled={checked} />
+          </span>
+        ) : (
+          <a
+            className={classNames(
+              'link-no-underline',
+              'display-inline-block',
+              this.props.className
+            )}
+            href="#"
+            onClick={this.handleClick}>
+            <HomeIcon filled={checked} />
+          </a>
+        )}
+      </Tooltip>
+    );
+  }
+}
+
+const mapStateToProps = (state: any): StateProps => ({
+  currentUser: getCurrentUser(state)
+});
+
+const mapDispatchToProps: DispatchProps = { setHomePage };
+
+export default connect(mapStateToProps, mapDispatchToProps)(HomePageSelect);
index 98dc0d71b5ba373b4b50cc51b28761eb6b8363fe..bb842b986b068d50a49985ebdd6b68606931dd80 100644 (file)
@@ -1,25 +1,35 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render favorite 1`] = `
-<a
-  className="link-no-underline"
-  href="#"
-  onClick={[Function]}
+<Tooltip
+  overlay="favorite.current"
+  placement="bottom"
 >
-  <FavoriteIcon
-    favorite={true}
-  />
-</a>
+  <a
+    className="display-inline-block link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <FavoriteIcon
+      favorite={true}
+    />
+  </a>
+</Tooltip>
 `;
 
 exports[`should render not favorite 1`] = `
-<a
-  className="link-no-underline"
-  href="#"
-  onClick={[Function]}
+<Tooltip
+  overlay="favorite.check"
+  placement="bottom"
 >
-  <FavoriteIcon
-    favorite={false}
-  />
-</a>
+  <a
+    className="display-inline-block link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <FavoriteIcon
+      favorite={false}
+    />
+  </a>
+</Tooltip>
 `;
index a07533ae13faba8e9ee970907bba6543c5aad087..abf8844a6cfccfe0c372e26b6dcf8c59ac65fdaf 100644 (file)
  */
 import * as React from 'react';
 import * as classNames from 'classnames';
+import { IconProps } from './types';
+import * as theme from '../../app/theme';
 
-interface Props {
-  className?: string;
+export interface Props extends IconProps {
   favorite: boolean;
-  size?: number;
 }
 
-export default function FavoriteIcon({ className, favorite, size = 16 }: Props) {
+export default function FavoriteIcon({
+  className,
+  favorite,
+  fill = theme.orange,
+  size = 16
+}: Props) {
   return (
-    <span className={classNames('icon-star', { 'icon-star-favorite': favorite }, className)}>
-      <svg width={size} height={size} viewBox="0 0 16 16">
-        <path d="M15.4275,5.77678C15.4275,5.90773 15.3501,6.05059 15.1953,6.20536L11.9542,9.36608L12.7221,13.8304C12.728,13.872 12.731,13.9316 12.731,14.0089C12.731,14.1339 12.6998,14.2396 12.6373,14.3259C12.5748,14.4122 12.484,14.4554 12.3649,14.4554C12.2518,14.4554 12.1328,14.4197 12.0078,14.3482L7.99888,12.2411L3.98995,14.3482C3.85901,14.4197 3.73996,14.4554 3.63281,14.4554C3.50781,14.4554 3.41406,14.4122 3.35156,14.3259C3.28906,14.2396 3.25781,14.1339 3.25781,14.0089C3.25781,13.9732 3.26377,13.9137 3.27567,13.8304L4.04353,9.36608L0.793531,6.20536C0.644719,6.04464 0.570313,5.90178 0.570313,5.77678C0.570313,5.55654 0.736979,5.41964 1.07031,5.36606L5.55245,4.71428L7.56138,0.651781C7.67447,0.407729 7.8203,0.285703 7.99888,0.285703C8.17745,0.285703 8.32328,0.407729 8.43638,0.651781L10.4453,4.71428L14.9274,5.36606C15.2608,5.41964 15.4274,5.55654 15.4274,5.77678L15.4275,5.77678Z" />
-      </svg>
-    </span>
+    <svg
+      className={classNames('icon-outline', { 'is-filled': favorite }, className)}
+      style={{ color: fill }}
+      width={size}
+      height={size}
+      viewBox="0 0 16 16"
+      version="1.1"
+      xmlnsXlink="http://www.w3.org/1999/xlink"
+      xmlSpace="preserve">
+      <g transform="matrix(0.988024,0,0,0.988024,0.0957953,0.717719)">
+        <path d="M15.428,5.777C15.428,5.908 15.35,6.051 15.195,6.205L11.954,9.366L12.722,13.83C12.728,13.872 12.731,13.932 12.731,14.009C12.731,14.134 12.7,14.24 12.637,14.326C12.575,14.412 12.484,14.455 12.365,14.455C12.252,14.455 12.133,14.42 12.008,14.348L7.999,12.241L3.99,14.348C3.859,14.42 3.74,14.455 3.633,14.455C3.508,14.455 3.414,14.412 3.352,14.326C3.289,14.24 3.258,14.134 3.258,14.009C3.258,13.973 3.264,13.914 3.276,13.83L4.044,9.366L0.794,6.205C0.645,6.045 0.57,5.902 0.57,5.777C0.57,5.557 0.737,5.42 1.07,5.366L5.552,4.714L7.561,0.652C7.674,0.408 7.82,0.286 7.999,0.286C8.177,0.286 8.323,0.408 8.436,0.652L10.445,4.714L14.927,5.366C15.261,5.42 15.427,5.557 15.427,5.777L15.428,5.777Z" />
+      </g>
+    </svg>
   );
 }
diff --git a/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx
new file mode 100644 (file)
index 0000000..c63d84f
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 { IconProps } from './types';
+import * as theme from '../../app/theme';
+
+export interface Props extends IconProps {
+  filled?: boolean;
+}
+
+export default function HomeIcon({
+  className,
+  fill = theme.orange,
+  filled = false,
+  size = 16
+}: Props) {
+  return (
+    <svg
+      className={classNames(className, 'icon-outline', { 'is-filled': filled })}
+      style={{ color: fill }}
+      width={size}
+      height={size}
+      viewBox="0 0 16 16"
+      version="1.1"
+      xmlnsXlink="http://www.w3.org/1999/xlink"
+      xmlSpace="preserve">
+      <g transform="matrix(0.870918,0,0,0.870918,0.978227,0.978227)">
+        <path d="M15.9,7.8L8.2,0.1C8.1,0 7.9,0 7.8,0.1L0.1,7.8C0,7.9 0,8.1 0.1,8.2C0.2,8.3 0.2,8.3 0.3,8.3L2.2,8.3L2.2,15.8C2.2,15.9 2.2,15.9 2.3,16C2.3,16 2.4,16.1 2.5,16.1L6.2,16.1C6.3,16.1 6.5,16 6.5,15.8L6.5,10.5L9.7,10.5L9.7,15.8C9.7,15.9 9.8,16.1 10,16.1L13.7,16.1C13.8,16.1 14,16 14,15.8L14,8.2L15.9,8.2C16,8.2 16,8.2 16.1,8.1C16,8 16.1,7.9 15.9,7.8Z" />
+      </g>
+    </svg>
+  );
+}
index 489747900bf669b7f8dedab272d0abcf4fa12fd5..81a469b239bf6ee3c7381d1f595fc17875edf288 100644 (file)
 }
 
 .navbar-context-header {
-  display: inline-block;
+  display: inline-flex;
+  align-items: center;
   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;
   position: absolute;
   top: 0;
   right: 0;
-  line-height: calc(4 * var(--gridSize));
-  padding: 0 10px;
+  display: flex;
+  align-items: center;
+  height: calc(4 * var(--gridSize));
+  padding: 0 20px;
   color: var(--secondFontColor);
   font-size: var(--smallFontSize);
   text-align: right;
 }
 
+.navbar-context-meta-secondary {
+  position: absolute;
+  top: 36px;
+  right: 0;
+  padding: 0 20px;
+}
+
 .navbar-context-description {
   display: inline-block;
   line-height: var(--controlHeight);
index ffe6fa177940bdebbddb7464673b71820277e0b0..10ab02703cbb01fb1bd06d563ad097951dbedf94 100644 (file)
@@ -21,7 +21,7 @@ import { stringify } from 'querystring';
 import { omitBy, isNil } from 'lodash';
 import { isShortLivingBranch } from './branches';
 import { getProfilePath } from '../apps/quality-profiles/utils';
-import { Branch } from '../app/types';
+import { Branch, HomePage } from '../app/types';
 
 interface Query {
   [x: string]: string | undefined;
@@ -167,3 +167,23 @@ export function getMarkdownHelpUrl(): string {
 export function getCodeUrl(project: string, branch?: string, selected?: string) {
   return { pathname: '/code', query: { id: project, branch, selected } };
 }
+
+export function getOrganizationUrl(organization: string) {
+  return `/organizations/${organization}`;
+}
+
+export function getHomePageUrl(homepage: HomePage) {
+  switch (homepage.type) {
+    case 'project':
+      return getProjectUrl(homepage.key!);
+    case 'organization':
+      return getOrganizationUrl(homepage.key!);
+    case 'my-projects':
+      return '/projects';
+    case 'my-issues':
+      return { pathname: '/issues', query: { resolved: 'false' } };
+  }
+
+  // should never happen, but just in case...
+  return '/projects';
+}
diff --git a/server/sonar-web/src/main/js/store/users/actions.js b/server/sonar-web/src/main/js/store/users/actions.js
deleted file mode 100644 (file)
index aaee449..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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 { getCurrentUser } from '../../api/users';
-
-export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
-export const RECEIVE_USER = 'RECEIVE_USER';
-export const SKIP_ONBOARDING = 'SKIP_ONBOARDING';
-
-export const receiveCurrentUser = user => ({
-  type: RECEIVE_CURRENT_USER,
-  user
-});
-
-export const receiveUser = user => ({
-  type: RECEIVE_USER,
-  user
-});
-
-export const skipOnboarding = () => ({ type: SKIP_ONBOARDING });
-
-export const fetchCurrentUser = () => dispatch =>
-  getCurrentUser().then(user => dispatch(receiveCurrentUser(user)));
diff --git a/server/sonar-web/src/main/js/store/users/actions.ts b/server/sonar-web/src/main/js/store/users/actions.ts
new file mode 100644 (file)
index 0000000..c55fff0
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 * as api from '../../api/users';
+import { CurrentUser, HomePage } from '../../app/types';
+
+export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
+export const RECEIVE_USER = 'RECEIVE_USER';
+export const SKIP_ONBOARDING = 'SKIP_ONBOARDING';
+export const SET_HOMEPAGE = 'SET_HOMEPAGE';
+
+export const receiveCurrentUser = (user: CurrentUser) => ({
+  type: RECEIVE_CURRENT_USER,
+  user
+});
+
+export const receiveUser = (user: any) => ({
+  type: RECEIVE_USER,
+  user
+});
+
+export const skipOnboarding = () => ({ type: SKIP_ONBOARDING });
+
+export const fetchCurrentUser = () => (dispatch: Dispatch<any>) => {
+  return api.getCurrentUser().then(user => dispatch(receiveCurrentUser(user)));
+};
+
+export const setHomePage = (homepage: HomePage) => (dispatch: Dispatch<any>) => {
+  api.setHomePage(homepage).then(
+    () => {
+      dispatch({ type: SET_HOMEPAGE, homepage });
+    },
+    () => {}
+  );
+};
diff --git a/server/sonar-web/src/main/js/store/users/reducer.js b/server/sonar-web/src/main/js/store/users/reducer.js
deleted file mode 100644 (file)
index 79472f1..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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 { combineReducers } from 'redux';
-import { uniq, keyBy } from 'lodash';
-import { RECEIVE_CURRENT_USER, RECEIVE_USER, SKIP_ONBOARDING } from './actions';
-import { actions as membersActions } from '../organizationsMembers/actions';
-
-const usersByLogin = (state = {}, action = {}) => {
-  switch (action.type) {
-    case RECEIVE_CURRENT_USER:
-    case RECEIVE_USER:
-      return { ...state, [action.user.login]: action.user };
-    case membersActions.RECEIVE_MEMBERS:
-    case membersActions.RECEIVE_MORE_MEMBERS:
-      return { ...state, ...keyBy(action.members, 'login') };
-    case membersActions.ADD_MEMBER:
-      return { ...state, [action.member.login]: action.member };
-    default:
-      return state;
-  }
-};
-
-const userLogins = (state = [], action = {}) => {
-  switch (action.type) {
-    case RECEIVE_CURRENT_USER:
-    case RECEIVE_USER:
-      return uniq([...state, action.user.login]);
-    case membersActions.RECEIVE_MEMBERS:
-    case membersActions.RECEIVE_MORE_MEMBERS:
-      return uniq([...state, action.members.map(member => member.login)]);
-    case membersActions.ADD_MEMBER: {
-      return uniq([...state, action.member.login]).sort();
-    }
-    default:
-      return state;
-  }
-};
-
-const currentUser = (state = null, action = {}) => {
-  if (action.type === RECEIVE_CURRENT_USER) {
-    return action.user;
-  }
-  if (action.type === SKIP_ONBOARDING) {
-    return state ? { ...state, showOnboardingTutorial: false } : null;
-  }
-  return state;
-};
-
-export default combineReducers({ usersByLogin, userLogins, currentUser });
-
-export const getCurrentUser = state => state.currentUser;
-export const getUserLogins = state => state.userLogins;
-export const getUserByLogin = (state, login) => state.usersByLogin[login];
-export const getUsersByLogins = (state, logins) =>
-  logins.map(login => getUserByLogin(state, login));
-export const getUsers = state => getUsersByLogins(state, getUserLogins(state));
diff --git a/server/sonar-web/src/main/js/store/users/reducer.ts b/server/sonar-web/src/main/js/store/users/reducer.ts
new file mode 100644 (file)
index 0000000..1cee5ba
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * 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 { combineReducers } from 'redux';
+import { uniq, keyBy } from 'lodash';
+import { RECEIVE_CURRENT_USER, RECEIVE_USER, SKIP_ONBOARDING, SET_HOMEPAGE } from './actions';
+import { actions as membersActions } from '../organizationsMembers/actions';
+import { CurrentUser } from '../../app/types';
+
+interface UsersByLogin {
+  [login: string]: any;
+}
+
+const usersByLogin = (state: UsersByLogin = {}, action: any = {}) => {
+  switch (action.type) {
+    case RECEIVE_CURRENT_USER:
+    case RECEIVE_USER:
+      return { ...state, [action.user.login]: action.user };
+    case membersActions.RECEIVE_MEMBERS:
+    case membersActions.RECEIVE_MORE_MEMBERS:
+      return { ...state, ...keyBy(action.members, 'login') };
+    case membersActions.ADD_MEMBER:
+      return { ...state, [action.member.login]: action.member };
+    default:
+      return state;
+  }
+};
+
+type UserLogins = string[];
+
+const userLogins = (state: UserLogins = [], action: any = {}) => {
+  switch (action.type) {
+    case RECEIVE_CURRENT_USER:
+    case RECEIVE_USER:
+      return uniq([...state, action.user.login]);
+    case membersActions.RECEIVE_MEMBERS:
+    case membersActions.RECEIVE_MORE_MEMBERS:
+      return uniq([...state, action.members.map((member: any) => member.login)]);
+    case membersActions.ADD_MEMBER: {
+      return uniq([...state, action.member.login]).sort();
+    }
+    default:
+      return state;
+  }
+};
+
+const currentUser = (state: CurrentUser | null = null, action: any = {}) => {
+  if (action.type === RECEIVE_CURRENT_USER) {
+    return action.user;
+  }
+  if (action.type === SKIP_ONBOARDING) {
+    return state ? { ...state, showOnboardingTutorial: false } : null;
+  }
+  if (action.type === SET_HOMEPAGE) {
+    return state && { ...state, homepage: action.homepage };
+  }
+  return state;
+};
+
+interface State {
+  usersByLogin: UsersByLogin;
+  userLogins: UserLogins;
+  currentUser: CurrentUser | null;
+}
+
+export default combineReducers({ usersByLogin, userLogins, currentUser });
+
+export const getCurrentUser = (state: State) => state.currentUser!;
+export const getUserLogins = (state: State) => state.userLogins;
+export const getUserByLogin = (state: State, login: string) => state.usersByLogin[login];
+export const getUsersByLogins = (state: State, logins: string[]) =>
+  logins.map(login => getUserByLogin(state, login));
+export const getUsers = (state: State) => getUsersByLogins(state, getUserLogins(state));
index 1fa402c992e129fb6b61d2d390aeef4e4722dbe4..b23f715de55112d970c9b08aa7120202b5b8878c 100644 (file)
@@ -893,8 +893,9 @@ shortcuts.section.rules.deactivate=deactivate selected rule
 tutorials.onboarding=Analyze a new project
 tutorials.skip=Skip this tutorial
 tutorials.finish=Finish this tutorial
-tutorials.follow_later=Follow the tutorial later in the Help section
+tutorials.follow_later=You can always follow the tutorial later
 tutorials.find_it_back_in_help=Find it back anytime in the Help section
+tutorials.find_it_back_in_plus=Find it back anytime in the "+" menu
 
 
 #------------------------------------------------------------------------------
@@ -2717,3 +2718,23 @@ maintenance.sonarqube_is_up=SonarQube is up
 maintenance.all_systems_opetational=All systems operational.
 maintenance.sonarqube_is_offline=SonarQube is offline
 maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Please contact your system administrator.
+
+
+
+#------------------------------------------------------------------------------
+#
+# HOMEPAGE
+#
+#------------------------------------------------------------------------------
+homepage.current=This page is your homepage. Click on the top-left logo to find it anytime.
+homepage.check=Check to make the current page your homepage
+
+
+
+#------------------------------------------------------------------------------
+#
+# FAVORITE
+#
+#------------------------------------------------------------------------------
+favorite.current=This is your favorite component. Click to unset.
+favorite.check=Click to mark this component as favorite.