]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-413 Add organizations list in onboarding modal
authorJeremy Davis <jeremy.davis@sonarsource.com>
Thu, 14 Feb 2019 17:22:10 +0000 (18:22 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 6 Mar 2019 10:30:42 +0000 (11:30 +0100)
27 files changed:
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx
server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx
server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/buttons.css
server/sonar-web/src/main/js/components/ui/buttons.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index c5d84e6c8eb19aa8f46648feb5733ba74c3ad1c2..cbc48881b23257039058955e4d5eabff3fcde40a 100644 (file)
@@ -58,7 +58,7 @@ interface WithRouterProps {
 
 type Props = StateProps & DispatchProps & OwnProps & WithRouterProps;
 
-enum ModalKey {
+export enum ModalKey {
   license,
   onboarding
 }
@@ -153,6 +153,7 @@ export class StartupModal extends React.PureComponent<Props, State> {
           <OnboardingModal
             onClose={this.closeOnboarding}
             onOpenProjectOnboarding={this.openProjectOnboarding}
+            skipOnboarding={this.props.skipOnboarding}
           />
         )}
       </OnboardingContext.Provider>
index b2476e2d50c6ccf7c59e3c151e1960325e47465a..e61ab79265ab255b10f9f0bc477efa4d2d77da05 100644 (file)
  */
 import * as React from 'react';
 import { shallow, ShallowWrapper } from 'enzyme';
-import { StartupModal } from '../StartupModal';
+import { StartupModal, ModalKey } from '../StartupModal';
 import { showLicense } from '../../../api/marketplace';
 import { save, get } from '../../../helpers/storage';
 import { hasMessage } from '../../../helpers/l10n';
 import { waitAndUpdate } from '../../../helpers/testUtils';
 import { differenceInDays, toShortNotSoISOString } from '../../../helpers/dates';
 import { EditionKey } from '../../../apps/marketplace/utils';
+import { mockOrganization, mockRouter } from '../../../helpers/testMocks';
 
 jest.mock('../../../api/marketplace', () => ({
   showLicense: jest.fn().mockResolvedValue(undefined)
@@ -110,6 +111,36 @@ it('should render license prompt', async () => {
   await shouldDisplayLicense(getWrapper());
 });
 
+describe('closeOnboarding', () => {
+  it('should set state and skip onboarding', () => {
+    const skipOnboarding = jest.fn();
+    const wrapper = getWrapper({ skipOnboarding });
+
+    wrapper.setState({ modal: ModalKey.onboarding });
+    wrapper.instance().closeOnboarding();
+
+    expect(wrapper.state('modal')).toBe(undefined);
+
+    expect(skipOnboarding).toHaveBeenCalledTimes(1);
+  });
+});
+
+describe('openProjectOnboarding', () => {
+  it('should set state and redirect', () => {
+    const push = jest.fn();
+    const wrapper = getWrapper({ router: mockRouter({ push }) });
+
+    wrapper.instance().openProjectOnboarding(mockOrganization());
+
+    expect(wrapper.state('modal')).toBe(undefined);
+
+    expect(push).toHaveBeenCalledWith({
+      pathname: `/projects/create`,
+      state: { organization: 'foo', tab: 'manual' }
+    });
+  });
+});
+
 async function shouldNotHaveModals(wrapper: ShallowWrapper) {
   await waitAndUpdate(wrapper);
   expect(wrapper.find('LicensePromptModal').exists()).toBeFalsy();
@@ -121,7 +152,7 @@ async function shouldDisplayLicense(wrapper: ShallowWrapper) {
 }
 
 function getWrapper(props: Partial<StartupModal['props']> = {}) {
-  return shallow(
+  return shallow<StartupModal>(
     <StartupModal
       canAdmin={true}
       currentEdition={EditionKey.enterprise}
index 25ce5c25956860233b499662506b7234d6475a63..4fd00b72e363534f2a7b464b7e341c4f604e7175 100644 (file)
@@ -95,6 +95,7 @@ module.exports = {
   bigFontSize: '16px',
   hugeFontSize: '24px',
 
+  hugeControlHeight: `${5 * grid}px`,
   largeControlHeight: `${4 * grid}px`,
   controlHeight: `${3 * grid}px`,
   smallControlHeight: `${2.5 * grid}px`,
index 8871a1933def7cd86647984d16858f5e1f26640b..9015c4ba8b2ac0cc9750effd0f7f180bee5d3836 100644 (file)
@@ -37,6 +37,7 @@ interface Props {
   loading: boolean;
   members?: T.OrganizationMember[];
   organization: T.Organization;
+  refreshMembers: () => Promise<void>;
   setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
 }
 
@@ -50,7 +51,7 @@ export class MembersPageHeader extends React.PureComponent<Props> {
   };
 
   render() {
-    const { dismissSyncNotifOrg, members, organization } = this.props;
+    const { dismissSyncNotifOrg, members, organization, refreshMembers } = this.props;
     const memberLogins = members ? members.map(member => member.login) : [];
     const isAdmin = organization.actions && organization.actions.admin;
     const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
@@ -67,7 +68,10 @@ export class MembersPageHeader extends React.PureComponent<Props> {
         <DeferredSpinner loading={this.props.loading} />
         {isAdmin && (
           <div className="page-actions text-right">
-            {almKey && !showSyncNotif && <SyncMemberForm organization={organization} />}
+            {almKey &&
+              !showSyncNotif && (
+                <SyncMemberForm organization={organization} refreshMembers={refreshMembers} />
+              )}
             {!hasMemberSync && (
               <div className="display-inline-block spacer-left spacer-bottom">
                 <AddMemberForm
@@ -93,7 +97,11 @@ export class MembersPageHeader extends React.PureComponent<Props> {
                     'organization.members.auto_sync_with_x',
                     translate(almKey)
                   )}>
-                  <SyncMemberForm organization={organization} />
+                  <SyncMemberForm
+                    dismissSyncNotif={this.handleDismissSyncNotif}
+                    organization={organization}
+                    refreshMembers={refreshMembers}
+                  />
                 </NewInfoBox>
               )}
           </div>
index 46e1162372ba579a1ffe83d7b2136b989f708468..da2ea484d97ae9a4b4210f6e0d13fc8086f87970 100644 (file)
@@ -151,6 +151,18 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
     );
   };
 
+  refreshMembers = () => {
+    return searchMembers({
+      organization: this.props.organization.key,
+      ps: PAGE_SIZE,
+      q: this.state.query || undefined
+    }).then(({ paging, users }) => {
+      if (this.mounted) {
+        this.setState({ members: users, paging });
+      }
+    });
+  };
+
   updateGroup = (
     login: string,
     updater: (member: T.OrganizationMember) => T.OrganizationMember
@@ -195,6 +207,7 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
           loading={loading}
           members={members}
           organization={organization}
+          refreshMembers={this.refreshMembers}
         />
         {members !== undefined &&
           paging !== undefined && (
index 1a7a5a8400e8c38a9563b13a9592a6fd2a98762e..9ebb9f57e219ee0a6737841a523b6c6b6213cb32 100644 (file)
@@ -30,8 +30,10 @@ import { translate, translateWithParameters } from '../../helpers/l10n';
 import { fetchOrganization } from '../../store/rootActions';
 
 interface Props {
+  dismissSyncNotif?: () => void;
   fetchOrganization: (key: string) => void;
   organization: T.Organization;
+  refreshMembers: () => Promise<void>;
 }
 
 interface State {
@@ -47,15 +49,20 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
   }
 
   handleConfirm = () => {
-    const { organization } = this.props;
+    const { dismissSyncNotif, organization } = this.props;
     const { membersSync } = this.state;
+
+    if (dismissSyncNotif) {
+      dismissSyncNotif();
+    }
+
     return setOrganizationMemberSync({
       organization: organization.key,
       enabled: membersSync
     }).then(() => {
       this.props.fetchOrganization(organization.key);
       if (membersSync && isGithub(organization.alm && organization.alm.key)) {
-        return syncMembers(organization.key);
+        return this.handleMemberSync();
       }
       return Promise.resolve();
     });
@@ -69,6 +76,10 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
     this.setState({ membersSync: true });
   };
 
+  handleMemberSync = () => {
+    return syncMembers(this.props.organization.key).then(this.props.refreshMembers);
+  };
+
   renderModalDescription = () => {
     return (
       <p className="spacer-top">
index 7f74a8eff3c0ddca0ca570cc922607dede06d9b5..bb2a10ad8052220e7599c86439eab7c41c20e470 100644 (file)
@@ -63,6 +63,7 @@ function shallowRender(props: Partial<MembersPageHeader['props']> = {}) {
       loading={false}
       members={[]}
       organization={mockOrganization()}
+      refreshMembers={jest.fn()}
       setCurrentUserSetting={jest.fn()}
       {...props}
     />
index 7861581aa2e8c3b1d3db01d939d40f8ad72fca86..eac606f79c0b1528806de388e73474f4728bf7b2 100644 (file)
@@ -54,8 +54,7 @@ jest.mock('../../../api/user_groups', () => ({
 }));
 
 beforeEach(() => {
-  (searchMembers as jest.Mock).mockClear();
-  (searchUsersGroups as jest.Mock).mockClear();
+  jest.clearAllMocks();
 });
 
 it('should fetch members and render for non-admin', async () => {
@@ -88,6 +87,28 @@ it('should load more members', async () => {
   expect(searchMembers).lastCalledWith({ organization: 'foo', p: 2, ps: 50, q: undefined });
 });
 
+it('should refresh members', async () => {
+  const paging = { pageIndex: 1, pageSize: 5, total: 3 };
+  const users = [
+    { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
+    { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 },
+    { login: 'stan', name: 'Stan Marsh', avatar: '', groupCount: 7 }
+  ];
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+
+  (searchMembers as jest.Mock).mockResolvedValueOnce({
+    paging,
+    users
+  });
+
+  await wrapper.instance().refreshMembers();
+  expect(searchMembers).toBeCalled();
+  expect(wrapper.state('members')).toEqual(users);
+  expect(wrapper.state('paging')).toEqual(paging);
+});
+
 it('should add new member', async () => {
   const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
@@ -140,7 +161,7 @@ it('should update groups', async () => {
 });
 
 function shallowRender(props: Partial<OrganizationMembers['props']> = {}) {
-  return shallow(
+  return shallow<OrganizationMembers>(
     <OrganizationMembers
       currentUser={mockCurrentUser()}
       organization={mockOrganizationWithAdminActions()}
index 4410d45c301c55760759987a1ec459483837d9c4..78dd230299a685f73304e1ea98b3a604eb6bedcb 100644 (file)
@@ -35,8 +35,10 @@ beforeEach(() => {
 });
 
 it('should allow to switch to automatic mode with github', async () => {
+  const dismissSyncNotif = jest.fn();
   const fetchOrganization = jest.fn();
-  const wrapper = shallowRender({ fetchOrganization });
+  const refreshMembers = jest.fn().mockResolvedValue({});
+  const wrapper = shallowRender({ dismissSyncNotif, fetchOrganization, refreshMembers });
   expect(wrapper).toMatchSnapshot();
 
   wrapper.setState({ membersSync: true });
@@ -46,6 +48,8 @@ it('should allow to switch to automatic mode with github', async () => {
   await waitAndUpdate(wrapper);
   expect(fetchOrganization).toHaveBeenCalledWith('foo');
   expect(syncMembers).toHaveBeenCalledWith('foo');
+  expect(refreshMembers).toBeCalledTimes(1);
+  expect(dismissSyncNotif).toBeCalledTimes(1);
 });
 
 it('should allow to switch to automatic mode with bitbucket', async () => {
@@ -87,6 +91,7 @@ function shallowRender(props: Partial<SyncMemberForm['props']> = {}) {
     <SyncMemberForm
       fetchOrganization={jest.fn()}
       organization={mockOrganizationWithAlm()}
+      refreshMembers={jest.fn().mockResolvedValue({})}
       {...props}
     />
   );
index 93646b3ecc130967e80a5b04dc379f558be59724..0225976a5a6f300f4b4778b296c64eecaeef56a1 100644 (file)
@@ -143,6 +143,7 @@ exports[`should render for bound organization without sync 1`] = `
       title="organization.members.auto_sync_with_x.github"
     >
       <Connect(SyncMemberForm)
+        dismissSyncNotif={[Function]}
         organization={
           Object {
             "actions": Object {
@@ -157,6 +158,7 @@ exports[`should render for bound organization without sync 1`] = `
             "name": "Foo",
           }
         }
+        refreshMembers={[MockFunction]}
       />
     </NewInfoBox>
   </div>
index dd26a5928f4f192467332279ad23c072a2b44fc7..2d451e1086c66abe7b9786048acb01bfe5e4eccb 100644 (file)
@@ -21,6 +21,7 @@ exports[`should fetch members and render for non-admin 1`] = `
         "name": "Foo",
       }
     }
+    refreshMembers={[Function]}
   />
 </div>
 `;
@@ -62,6 +63,7 @@ exports[`should fetch members and render for non-admin 2`] = `
         "name": "Foo",
       }
     }
+    refreshMembers={[Function]}
   />
   <MembersListHeader
     currentUser={
index adf205bd0ea5369626187b88e6de7df053f2c46b..1163399e78bb6e7072da7b66915b2f759deffb09 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { connect } from 'react-redux';
-import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import OrganizationsShortList from './OrganizationsShortList';
 import Modal from '../../../components/controls/Modal';
 import OnboardingProjectIcon from '../../../components/icons-components/OnboardingProjectIcon';
+import OnboardingTeamIcon from '../../../components/icons-components/OnboardingTeamIcon';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
+import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations';
 import { Button, ResetButtonLink } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-import { isLoggedIn } from '../../../helpers/users';
 import '../styles.css';
 
-interface OwnProps {
+export interface Props {
+  currentUser: T.LoggedInUser;
   onClose: () => void;
   onOpenProjectOnboarding: () => void;
+  skipOnboarding: () => void;
+  userOrganizations: T.Organization[];
 }
 
-interface StateProps {
-  currentUser: T.CurrentUser;
-}
-
-type Props = OwnProps & StateProps;
-
-export class OnboardingModal extends React.PureComponent<Props> {
-  componentDidMount() {
-    if (!isLoggedIn(this.props.currentUser)) {
-      handleRequiredAuthentication();
-    }
-  }
+export function OnboardingModal(props: Props) {
+  const {
+    currentUser,
+    onClose,
+    onOpenProjectOnboarding,
+    skipOnboarding,
+    userOrganizations
+  } = props;
 
-  render() {
-    if (!isLoggedIn(this.props.currentUser)) {
-      return null;
-    }
+  const organizations = userOrganizations.filter(o => o.key !== currentUser.personalOrganization);
 
-    const header = translate('onboarding.header');
-    return (
-      <Modal
-        contentLabel={header}
-        medium={true}
-        onRequestClose={this.props.onClose}
-        shouldCloseOnOverlayClick={false}>
-        <div className="modal-head">
-          <h2>{translate('onboarding.header')}</h2>
-          <p className="spacer-top">{translate('onboarding.header.description')}</p>
-        </div>
-        <div className="modal-body text-center huge-spacer-top huge-spacer-bottom">
+  const header = translate('onboarding.header');
+  return (
+    <Modal
+      contentLabel={header}
+      medium={true}
+      onRequestClose={onClose}
+      shouldCloseOnOverlayClick={false}>
+      <div className="modal-head">
+        <h2>{translate('onboarding.header')}</h2>
+        <p className="spacer-top">{translate('onboarding.header.description')}</p>
+      </div>
+      <div className="modal-body text-center display-flex-row huge-spacer-top huge-spacer-bottom">
+        <div className="flex-1">
           <OnboardingProjectIcon className="big-spacer-bottom" />
           <h6 className="onboarding-choice-name big-spacer-bottom">
             {translate('onboarding.analyze_your_code')}
           </h6>
-          <Button onClick={this.props.onOpenProjectOnboarding}>
+          <Button onClick={onOpenProjectOnboarding}>
             {translate('onboarding.project.create')}
           </Button>
         </div>
-        <div className="modal-foot text-right">
-          <ResetButtonLink onClick={this.props.onClose}>{translate('not_now')}</ResetButtonLink>
-        </div>
-      </Modal>
-    );
-  }
+        {organizations.length > 0 && (
+          <>
+            <div className="vertical-pipe-separator">
+              <div className="vertical-separator" />
+            </div>
+            <div className="flex-1">
+              <OnboardingTeamIcon className="big-spacer-bottom" />
+              <h6 className="onboarding-choice-name big-spacer-bottom">
+                {translate('onboarding.browse_your_organizations')}
+              </h6>
+              <OrganizationsShortList
+                organizations={organizations}
+                skipOnboarding={skipOnboarding}
+              />
+            </div>
+          </>
+        )}
+      </div>
+      <div className="modal-foot text-right">
+        <ResetButtonLink onClick={onClose}>{translate('not_now')}</ResetButtonLink>
+      </div>
+    </Modal>
+  );
 }
 
-const mapStateToProps = (state: Store): StateProps => ({ currentUser: getCurrentUser(state) });
-
-export default connect(mapStateToProps)(OnboardingModal);
+export default withUserOrganizations(whenLoggedIn(OnboardingModal));
index 9533287403bc79a07ec86a6f118b0b573bf7482b..5bda42d861b990984c2bd7d92498f096b93c6cbb 100644 (file)
  */
 import * as React from 'react';
 import { connect } from 'react-redux';
-import { InjectedRouter } from 'react-router';
 import OnboardingModal from './OnboardingModal';
 import { skipOnboarding } from '../../../store/users';
 import { OnboardingContext } from '../../../app/components/OnboardingContext';
+import { Router } from '../../../components/hoc/withRouter';
 
-interface DispatchProps {
+interface Props {
+  router: Router;
   skipOnboarding: () => void;
 }
 
-interface OwnProps {
-  router: InjectedRouter;
-}
-
-interface State {
-  open: boolean;
-}
-
-export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps, State> {
-  state: State = { open: false };
-
+export class OnboardingPage extends React.PureComponent<Props> {
   closeOnboarding = () => {
     this.props.skipOnboarding();
     this.props.router.replace('/');
   };
 
   render() {
-    const { open } = this.state;
-
-    if (!open) {
-      return null;
-    }
-
     return (
       <OnboardingContext.Consumer>
         {openProjectOnboarding => (
           <OnboardingModal
             onClose={this.closeOnboarding}
             onOpenProjectOnboarding={openProjectOnboarding}
+            skipOnboarding={this.props.skipOnboarding}
           />
         )}
       </OnboardingContext.Consumer>
@@ -64,7 +50,7 @@ export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps
   }
 }
 
-const mapDispatchToProps: DispatchProps = { skipOnboarding };
+const mapDispatchToProps = { skipOnboarding };
 
 export default connect(
   null,
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx
new file mode 100644 (file)
index 0000000..a7eecc8
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { sortBy } from 'lodash';
+import { Link } from 'react-router';
+import OrganizationsShortListItem from './OrganizationsShortListItem';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export interface Props {
+  organizations: T.Organization[];
+  skipOnboarding: () => void;
+}
+
+export default function OrganizationsShortList({ organizations, skipOnboarding }: Props) {
+  if (organizations.length === 0) {
+    return null;
+  }
+
+  const organizationsShown = sortBy(organizations, organization =>
+    organization.name.toLocaleLowerCase()
+  ).slice(0, 3);
+
+  return (
+    <div>
+      <ul className="account-projects-list">
+        {organizationsShown.map(organization => (
+          <li key={organization.key}>
+            <OrganizationsShortListItem
+              organization={organization}
+              skipOnboarding={skipOnboarding}
+            />
+          </li>
+        ))}
+      </ul>
+      <div className="big-spacer-top">
+        <span className="big-spacer-right">
+          {translateWithParameters('x_of_y_shown', organizationsShown.length, organizations.length)}
+        </span>
+        {organizations.length > 3 && (
+          <Link className="small" onClick={skipOnboarding} to="/account/organizations">
+            {translate('see_all')}
+          </Link>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx
new file mode 100644 (file)
index 0000000..49be229
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 OrganizationAvatar from '../../../components/common/OrganizationAvatar';
+import { ListButton } from '../../../components/ui/buttons';
+import { withRouter, Router } from '../../../components/hoc/withRouter';
+import { getOrganizationUrl } from '../../../helpers/urls';
+
+interface Props {
+  organization: T.Organization;
+  router: Router;
+  skipOnboarding: () => void;
+}
+
+export class OrganizationsShortListItem extends React.PureComponent<Props> {
+  handleClick = () => {
+    const { organization, router, skipOnboarding } = this.props;
+    skipOnboarding();
+    router.push(getOrganizationUrl(organization.key));
+  };
+
+  render() {
+    const { organization } = this.props;
+    return (
+      <ListButton className="abs-width-300" onClick={this.handleClick}>
+        <div className="display-flex-center">
+          <OrganizationAvatar className="spacer-right" organization={organization} />
+          <span>{organization.name}</span>
+        </div>
+      </ListButton>
+    );
+  }
+}
+
+export default withRouter(OrganizationsShortListItem);
index 7ae1c13dec6cd8d6061101d8129b7c1c4a23bf44..2521fc0cba8c43fa8db3d8ff750c3b3252492b18 100644 (file)
  */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import { OnboardingModal } from '../OnboardingModal';
+import { OnboardingModal, Props } from '../OnboardingModal';
 import { click } from '../../../../helpers/testUtils';
+import { mockCurrentUser, mockOrganization } from '../../../../helpers/testMocks';
 
 it('renders correctly', () => {
-  expect(
-    shallow(
-      <OnboardingModal
-        currentUser={{ isLoggedIn: true }}
-        onClose={jest.fn()}
-        onOpenProjectOnboarding={jest.fn()}
-      />
-    )
-  ).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot();
 });
 
-it('should correctly open the different tutorials', () => {
+it('should open project create page', () => {
   const onClose = jest.fn();
   const onOpenProjectOnboarding = jest.fn();
-  const wrapper = shallow(
-    <OnboardingModal
-      currentUser={{ isLoggedIn: true }}
-      onClose={onClose}
-      onOpenProjectOnboarding={onOpenProjectOnboarding}
-    />
-  );
+  const wrapper = shallowRender({ onClose, onOpenProjectOnboarding });
 
   click(wrapper.find('ResetButtonLink'));
   expect(onClose).toHaveBeenCalled();
@@ -51,3 +38,30 @@ it('should correctly open the different tutorials', () => {
   wrapper.find('Button').forEach(button => click(button));
   expect(onOpenProjectOnboarding).toHaveBeenCalled();
 });
+
+it('should display organization list if any', () => {
+  const wrapper = shallowRender({
+    currentUser: mockCurrentUser({ personalOrganization: 'personal' }),
+    userOrganizations: [
+      mockOrganization({ key: 'a', name: 'Arthur' }),
+      mockOrganization({ key: 'd', name: 'Daniel Inc' }),
+      mockOrganization({ key: 'personal', name: 'Personal' })
+    ]
+  });
+
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('OrganizationsShortList').prop('organizations')).toHaveLength(2);
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(
+    <OnboardingModal
+      currentUser={mockCurrentUser()}
+      onClose={jest.fn()}
+      onOpenProjectOnboarding={jest.fn()}
+      skipOnboarding={jest.fn()}
+      userOrganizations={[]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx
new file mode 100644 (file)
index 0000000..f0e9b24
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 OrganizationsShortList, { Props } from '../OrganizationsShortList';
+import { mockOrganization } from '../../../../helpers/testMocks';
+
+it('should render null with no orgs', () => {
+  expect(shallowRender().getElement()).toBe(null);
+});
+
+it('should render correctly', () => {
+  const wrapper = shallowRender({
+    organizations: [mockOrganization(), mockOrganization({ key: 'bar', name: 'Bar' })]
+  });
+
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('li')).toHaveLength(2);
+});
+
+it('should limit displayed orgs to the first three', () => {
+  const wrapper = shallowRender({
+    organizations: [
+      mockOrganization(),
+      mockOrganization({ key: 'zoo', name: 'Zoological' }),
+      mockOrganization({ key: 'bar', name: 'Bar' }),
+      mockOrganization({ key: 'kor', name: 'Kor' })
+    ]
+  });
+
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('li')).toHaveLength(3);
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(
+    <OrganizationsShortList organizations={[]} skipOnboarding={jest.fn()} {...props} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx
new file mode 100644 (file)
index 0000000..bf84c78
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { OrganizationsShortListItem } from '../OrganizationsShortListItem';
+import { mockRouter, mockOrganization } from '../../../../helpers/testMocks';
+import { click } from '../../../../helpers/testUtils';
+
+it('renders correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('calls skiponboarding and redirects to org page', () => {
+  const skipOnboarding = jest.fn();
+  const push = jest.fn();
+  const wrapper = shallowRender({ skipOnboarding, router: mockRouter({ push }) });
+
+  click(wrapper);
+
+  expect(skipOnboarding).toHaveBeenCalledTimes(1);
+  expect(push).toHaveBeenCalledWith('/organizations/foo');
+});
+
+function shallowRender(props: Partial<OrganizationsShortListItem['props']> = {}) {
+  return shallow(
+    <OrganizationsShortListItem
+      organization={mockOrganization()}
+      router={mockRouter()}
+      skipOnboarding={jest.fn()}
+      {...props}
+    />
+  );
+}
index daf332ad6a28eccf372a4efde500c2914fa91c65..1c9eb96f30e526129541bd3ea8e182d0de294be7 100644 (file)
@@ -20,21 +20,111 @@ exports[`renders correctly 1`] = `
     </p>
   </div>
   <div
-    className="modal-body text-center huge-spacer-top huge-spacer-bottom"
+    className="modal-body text-center display-flex-row huge-spacer-top huge-spacer-bottom"
   >
-    <OnboardingProjectIcon
-      className="big-spacer-bottom"
-    />
-    <h6
-      className="onboarding-choice-name big-spacer-bottom"
-    >
-      onboarding.analyze_your_code
-    </h6>
-    <Button
+    <div
+      className="flex-1"
+    >
+      <OnboardingProjectIcon
+        className="big-spacer-bottom"
+      />
+      <h6
+        className="onboarding-choice-name big-spacer-bottom"
+      >
+        onboarding.analyze_your_code
+      </h6>
+      <Button
+        onClick={[MockFunction]}
+      >
+        onboarding.project.create
+      </Button>
+    </div>
+  </div>
+  <div
+    className="modal-foot text-right"
+  >
+    <ResetButtonLink
       onClick={[MockFunction]}
     >
-      onboarding.project.create
-    </Button>
+      not_now
+    </ResetButtonLink>
+  </div>
+</Modal>
+`;
+
+exports[`should display organization list if any 1`] = `
+<Modal
+  contentLabel="onboarding.header"
+  medium={true}
+  onRequestClose={[MockFunction]}
+  shouldCloseOnOverlayClick={false}
+>
+  <div
+    className="modal-head"
+  >
+    <h2>
+      onboarding.header
+    </h2>
+    <p
+      className="spacer-top"
+    >
+      onboarding.header.description
+    </p>
+  </div>
+  <div
+    className="modal-body text-center display-flex-row huge-spacer-top huge-spacer-bottom"
+  >
+    <div
+      className="flex-1"
+    >
+      <OnboardingProjectIcon
+        className="big-spacer-bottom"
+      />
+      <h6
+        className="onboarding-choice-name big-spacer-bottom"
+      >
+        onboarding.analyze_your_code
+      </h6>
+      <Button
+        onClick={[MockFunction]}
+      >
+        onboarding.project.create
+      </Button>
+    </div>
+    <div
+      className="vertical-pipe-separator"
+    >
+      <div
+        className="vertical-separator"
+      />
+    </div>
+    <div
+      className="flex-1"
+    >
+      <OnboardingTeamIcon
+        className="big-spacer-bottom"
+      />
+      <h6
+        className="onboarding-choice-name big-spacer-bottom"
+      >
+        onboarding.browse_your_organizations
+      </h6>
+      <OrganizationsShortList
+        organizations={
+          Array [
+            Object {
+              "key": "a",
+              "name": "Arthur",
+            },
+            Object {
+              "key": "d",
+              "name": "Daniel Inc",
+            },
+          ]
+        }
+        skipOnboarding={[MockFunction]}
+      />
+    </div>
   </div>
   <div
     className="modal-foot text-right"
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap
new file mode 100644 (file)
index 0000000..5410482
--- /dev/null
@@ -0,0 +1,111 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should limit displayed orgs to the first three 1`] = `
+<div>
+  <ul
+    className="account-projects-list"
+  >
+    <li
+      key="bar"
+    >
+      <withRouter(OrganizationsShortListItem)
+        organization={
+          Object {
+            "key": "bar",
+            "name": "Bar",
+          }
+        }
+        skipOnboarding={[MockFunction]}
+      />
+    </li>
+    <li
+      key="foo"
+    >
+      <withRouter(OrganizationsShortListItem)
+        organization={
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          }
+        }
+        skipOnboarding={[MockFunction]}
+      />
+    </li>
+    <li
+      key="kor"
+    >
+      <withRouter(OrganizationsShortListItem)
+        organization={
+          Object {
+            "key": "kor",
+            "name": "Kor",
+          }
+        }
+        skipOnboarding={[MockFunction]}
+      />
+    </li>
+  </ul>
+  <div
+    className="big-spacer-top"
+  >
+    <span
+      className="big-spacer-right"
+    >
+      x_of_y_shown.3.4
+    </span>
+    <Link
+      className="small"
+      onClick={[MockFunction]}
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to="/account/organizations"
+    >
+      see_all
+    </Link>
+  </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div>
+  <ul
+    className="account-projects-list"
+  >
+    <li
+      key="bar"
+    >
+      <withRouter(OrganizationsShortListItem)
+        organization={
+          Object {
+            "key": "bar",
+            "name": "Bar",
+          }
+        }
+        skipOnboarding={[MockFunction]}
+      />
+    </li>
+    <li
+      key="foo"
+    >
+      <withRouter(OrganizationsShortListItem)
+        organization={
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          }
+        }
+        skipOnboarding={[MockFunction]}
+      />
+    </li>
+  </ul>
+  <div
+    className="big-spacer-top"
+  >
+    <span
+      className="big-spacer-right"
+    >
+      x_of_y_shown.2.2
+    </span>
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap
new file mode 100644 (file)
index 0000000..ab088a9
--- /dev/null
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<ListButton
+  className="abs-width-300"
+  onClick={[Function]}
+>
+  <div
+    className="display-flex-center"
+  >
+    <OrganizationAvatar
+      className="spacer-right"
+      organization={
+        Object {
+          "key": "foo",
+          "name": "Foo",
+        }
+      }
+    />
+    <span>
+      Foo
+    </span>
+  </div>
+</ListButton>
+`;
index 2fd85afbfbbcd57e51ca91a196fe3cba5f567d8c..c9b44d0f7bdd09b5a9122dbee945c27be55a763d 100644 (file)
@@ -23,7 +23,7 @@ import { withCurrentUser } from './withCurrentUser';
 import { isLoggedIn } from '../../helpers/users';
 import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
 
-export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
+export function whenLoggedIn<P>(WrappedComponent: React.ComponentType<P>) {
   class Wrapper extends React.Component<P & { currentUser: T.CurrentUser }> {
     static displayName = getWrappedDisplayName(WrappedComponent, 'whenLoggedIn');
 
index 991005055e21201aee657c390ebcc94146e68a21..c016ce0ffa523a8f4526442970f2e3ff5210bc3f 100644 (file)
@@ -29,7 +29,7 @@ interface OwnProps {
 }
 
 export function withUserOrganizations<P>(
-  WrappedComponent: React.ComponentClass<P & Partial<OwnProps>>
+  WrappedComponent: React.ComponentType<P & Partial<OwnProps>>
 ) {
   class Wrapper extends React.Component<P & OwnProps> {
     static displayName = getWrappedDisplayName(WrappedComponent, 'withUserOrganizations');
diff --git a/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
new file mode 100644 (file)
index 0000000..ebedfeb
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 Icon, { IconProps } from './Icon';
+import * as theme from '../../app/theme';
+
+export default function OnboardingTeamIcon({ className, fill = theme.darkBlue, size }: IconProps) {
+  return (
+    <Icon className={className} size={size || 64} viewBox="0 0 64 64">
+      <g fill="none" fillRule="evenodd" stroke={fill} strokeWidth="2">
+        <path d="M32 9v5M11.5195 43.0898l7.48-4.091m33.481-18.0994l-7.48 4.1m-33.481-4.1l7.48 4.1M45 38.999l7.48 4.101M32 50v5m15-23c0 8.284-6.715 15-15 15s-15-6.716-15-15c0-8.285 6.715-15 15-15s15 6.715 15 15z" />
+        <path d="M40 38c0 1.656-3.58 2-8 2s-8-.344-8-2m16 0v-3l-5-3-1-1m-10 7v-3l5-3 1-1m6-4c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm-.0098-21.71c7.18 1.069 13.439 4.96 17.609 10.51m-17.609 42.91c7.18-1.07 13.439-4.96 17.609-10.51M6.6299 41.25c-1.06-2.88-1.63-6-1.63-9.25s.57-6.37 1.63-9.25m3.7705-6.9502c4.17-5.55 10.43-9.44 17.609-10.51m-17.609 42.9104c4.17 5.55 10.43 9.439 17.609 10.51M57.3701 22.75c1.06 2.88 1.63 6 1.63 9.25s-.57 6.37-1.63 9.25" />
+        <path d="M36 5c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 19c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 45c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M36 59c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2" />
+      </g>
+    </Icon>
+  );
+}
index 05df1afe1819f2362d046e6343da5c4753f68817..cede4ec4d1078a505abc4e11a17029187864f403 100644 (file)
@@ -79,6 +79,7 @@
 .button-red:focus {
   box-shadow: 0 0 0 3px rgba(212, 51, 63, 0.25);
 }
+
 /* #endregion */
 
 /* #region .button-success */
@@ -96,6 +97,7 @@
 .button-success:focus {
   box-shadow: 0 0 0 3px rgba(0, 170, 0, 0.25);
 }
+
 /* #endregion */
 
 /* #region .button-grey */
 .button-grey:focus {
   box-shadow: 0 0 0 3px rgba(180, 180, 180, 0.25);
 }
+
 /* #endregion */
 
 /* #region .button-link */
 .button-link {
   display: inline-flex;
-  height: auto; /* Keep this to not inherit the height from .button */
+  height: auto;
+  /* Keep this to not inherit the height from .button */
   line-height: 1;
   margin: 0;
   padding: 0;
   background: transparent !important;
   cursor: default;
 }
+
 /* #endregion */
 
 .button-small {
   margin: 0 8px;
   font-size: var(--smallFontSize);
 }
+
 /* #endregion */
 
 /* #region .button-icon */
 .button-icon:focus svg {
   color: #fff;
 }
+
 /* #endregion */
+
+.button-list {
+  height: auto;
+  border: 1px solid var(--barBorderColor);
+  padding: var(--gridSize);
+  margin: calc(var(--gridSize) / 2);
+  text-align: left;
+  justify-content: space-between;
+  color: var(--secondFontColor);
+  font-weight: normal;
+}
+
+.button-list:hover {
+  background-color: white;
+  border-color: var(--blue);
+  color: var(--darkBlue);
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.175);
+}
index a0dd58c14554fcbc352c9c55ea08495e47d1af19..99e4c327a62e2b84f3171777f3f805d578531ea6 100644 (file)
@@ -24,6 +24,7 @@ import ClearIcon from '../icons-components/ClearIcon';
 import EditIcon from '../icons-components/EditIcon';
 import Tooltip from '../controls/Tooltip';
 import './buttons.css';
+import ChevronRightIcon from '../icons-components/ChevronRightcon';
 
 type AllowedButtonAttributes = Pick<
   React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -138,3 +139,12 @@ export function EditButton(props: ActionButtonProps) {
     </ButtonIcon>
   );
 }
+
+export function ListButton({ className, children, ...props }: ButtonProps) {
+  return (
+    <Button className={classNames('button-list', className)} {...props}>
+      {children}
+      <ChevronRightIcon />
+    </Button>
+  );
+}
index ac74be9ca5bde32d64bc97aaccb6cdd4ca577dd5..33a2cbb30af74d27eebe90b98709476ef89cfba6 100644 (file)
@@ -160,7 +160,7 @@ rule=Rule
 rules=Rules
 save=Save
 search_verb=Search
-see_all=See All
+see_all=See all
 select_verb=Select
 selected=Selected
 set=Set
@@ -2852,6 +2852,7 @@ onboarding.team.work_in_progress=We are currently working on a better way to joi
 onboarding.analyze_your_code.note=Free
 onboarding.analyze_your_code=Analyze your code
 onboarding.contribute_existing_project=Join a team
+onboarding.browse_your_organizations=Browse your organizations
 
 onboarding.token.header=Provide a token
 onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point of time in your {link}.