From f79ab22a93ead12949592ec5095401db718af63e Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 14 Feb 2019 18:22:10 +0100 Subject: [PATCH] SONARCLOUD-413 Add organizations list in onboarding modal --- .../main/js/app/components/StartupModal.tsx | 3 +- .../__tests__/StartupModal-test.tsx | 35 +++++- server/sonar-web/src/main/js/app/theme.js | 1 + .../organizationMembers/MembersPageHeader.tsx | 14 ++- .../OrganizationMembers.tsx | 13 ++ .../organizationMembers/SyncMemberForm.tsx | 15 ++- .../__tests__/MembersPageHeader-test.tsx | 1 + .../__tests__/OrganizationMembers-test.tsx | 27 ++++- .../__tests__/SyncMemberForm-test.tsx | 7 +- .../MembersPageHeader-test.tsx.snap | 2 + .../OrganizationMembers-test.tsx.snap | 2 + .../tutorials/onboarding/OnboardingModal.tsx | 98 ++++++++------- .../tutorials/onboarding/OnboardingPage.tsx | 26 +--- .../onboarding/OrganizationsShortList.tsx | 64 ++++++++++ .../onboarding/OrganizationsShortListItem.tsx | 52 ++++++++ .../__tests__/OnboardingModal-test.tsx | 50 +++++--- .../__tests__/OrganizationsShortList-test.tsx | 56 +++++++++ .../OrganizationsShortListItem-test.tsx | 50 ++++++++ .../OnboardingModal-test.tsx.snap | 114 ++++++++++++++++-- .../OrganizationsShortList-test.tsx.snap | 111 +++++++++++++++++ .../OrganizationsShortListItem-test.tsx.snap | 25 ++++ .../main/js/components/hoc/whenLoggedIn.tsx | 2 +- .../components/hoc/withUserOrganizations.tsx | 2 +- .../icons-components/OnboardingTeamIcon.tsx | 34 ++++++ .../src/main/js/components/ui/buttons.css | 27 ++++- .../src/main/js/components/ui/buttons.tsx | 10 ++ .../resources/org/sonar/l10n/core.properties | 3 +- 27 files changed, 735 insertions(+), 109 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortList.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortList-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx index c5d84e6c8eb..cbc48881b23 100644 --- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx +++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx @@ -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 { )} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx index b2476e2d50c..e61ab79265a 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx @@ -19,13 +19,14 @@ */ 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 = {}) { - return shallow( + return shallow( Promise; setCurrentUserSetting: (setting: T.CurrentUserSetting) => void; } @@ -50,7 +51,7 @@ export class MembersPageHeader extends React.PureComponent { }; 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 { {isAdmin && (
- {almKey && !showSyncNotif && } + {almKey && + !showSyncNotif && ( + + )} {!hasMemberSync && (
{ 'organization.members.auto_sync_with_x', translate(almKey) )}> - + )}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx index 46e1162372b..da2ea484d97 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx @@ -151,6 +151,18 @@ export default class OrganizationMembers extends React.PureComponent { + 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 {members !== undefined && paging !== undefined && ( diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx index 1a7a5a8400e..9ebb9f57e21 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx @@ -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; } interface State { @@ -47,15 +49,20 @@ export class SyncMemberForm extends React.PureComponent { } 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 { this.setState({ membersSync: true }); }; + handleMemberSync = () => { + return syncMembers(this.props.organization.key).then(this.props.refreshMembers); + }; + renderModalDescription = () => { return (

diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx index 7f74a8eff3c..bb2a10ad805 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx @@ -63,6 +63,7 @@ function shallowRender(props: Partial = {}) { loading={false} members={[]} organization={mockOrganization()} + refreshMembers={jest.fn()} setCurrentUserSetting={jest.fn()} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx index 7861581aa2e..eac606f79c0 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx @@ -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 = {}) { - return shallow( + return shallow( { }); 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 = {}) { ); diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap index 93646b3ecc1..0225976a5a6 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap @@ -143,6 +143,7 @@ exports[`should render for bound organization without sync 1`] = ` title="organization.members.auto_sync_with_x.github" >

diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap index dd26a5928f4..2d451e1086c 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap @@ -21,6 +21,7 @@ exports[`should fetch members and render for non-admin 1`] = ` "name": "Foo", } } + refreshMembers={[Function]} /> `; @@ -62,6 +63,7 @@ exports[`should fetch members and render for non-admin 2`] = ` "name": "Foo", } } + refreshMembers={[Function]} /> void; onOpenProjectOnboarding: () => void; + skipOnboarding: () => void; + userOrganizations: T.Organization[]; } -interface StateProps { - currentUser: T.CurrentUser; -} - -type Props = OwnProps & StateProps; - -export class OnboardingModal extends React.PureComponent { - 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 ( - -
-

{translate('onboarding.header')}

-

{translate('onboarding.header.description')}

-
-
+ const header = translate('onboarding.header'); + return ( + +
+

{translate('onboarding.header')}

+

{translate('onboarding.header.description')}

+
+
+
{translate('onboarding.analyze_your_code')}
-
-
- {translate('not_now')} -
- - ); - } + {organizations.length > 0 && ( + <> +
+
+
+
+ +
+ {translate('onboarding.browse_your_organizations')} +
+ +
+ + )} +
+
+ {translate('not_now')} +
+ + ); } -const mapStateToProps = (state: Store): StateProps => ({ currentUser: getCurrentUser(state) }); - -export default connect(mapStateToProps)(OnboardingModal); +export default withUserOrganizations(whenLoggedIn(OnboardingModal)); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx index 9533287403b..5bda42d861b 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx @@ -19,44 +19,30 @@ */ 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 { - state: State = { open: false }; - +export class OnboardingPage extends React.PureComponent { closeOnboarding = () => { this.props.skipOnboarding(); this.props.router.replace('/'); }; render() { - const { open } = this.state; - - if (!open) { - return null; - } - return ( {openProjectOnboarding => ( )} @@ -64,7 +50,7 @@ export class OnboardingPage extends React.PureComponent 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 ( +
+
    + {organizationsShown.map(organization => ( +
  • + +
  • + ))} +
+
+ + {translateWithParameters('x_of_y_shown', organizationsShown.length, organizations.length)} + + {organizations.length > 3 && ( + + {translate('see_all')} + + )} +
+
+ ); +} 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 index 00000000000..49be2296cb9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationsShortListItem.tsx @@ -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 { + handleClick = () => { + const { organization, router, skipOnboarding } = this.props; + skipOnboarding(); + router.push(getOrganizationUrl(organization.key)); + }; + + render() { + const { organization } = this.props; + return ( + +
+ + {organization.name} +
+
+ ); + } +} + +export default withRouter(OrganizationsShortListItem); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx index 7ae1c13dec6..2521fc0cba8 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx @@ -19,31 +19,18 @@ */ 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( - - ) - ).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( - - ); + 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 = {}) { + return shallow( + + ); +} 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 index 00000000000..f0e9b24e4d3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortList-test.tsx @@ -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 = {}) { + return shallow( + + ); +} 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 index 00000000000..bf84c7895ba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationsShortListItem-test.tsx @@ -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 = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap index daf332ad6a2..1c9eb96f30e 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap @@ -20,21 +20,111 @@ exports[`renders correctly 1`] = `

- -
- onboarding.analyze_your_code -
- +
+
+
+ - onboarding.project.create - + not_now + +
+
+`; + +exports[`should display organization list if any 1`] = ` + +
+

+ onboarding.header +

+

+ onboarding.header.description +

+
+
+
+ +
+ onboarding.analyze_your_code +
+ +
+
+
+
+
+ +
+ onboarding.browse_your_organizations +
+ +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ + x_of_y_shown.3.4 + + + see_all + +
+
+`; + +exports[`should render correctly 1`] = ` +
+
    +
  • + +
  • +
  • + +
  • +
+
+ + x_of_y_shown.2.2 + +
+
+`; 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 index 00000000000..ab088a9991b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OrganizationsShortListItem-test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + +
+ + + Foo + +
+
+`; diff --git a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx index 2fd85afbfbb..c9b44d0f7bd 100644 --- a/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx +++ b/server/sonar-web/src/main/js/components/hoc/whenLoggedIn.tsx @@ -23,7 +23,7 @@ import { withCurrentUser } from './withCurrentUser'; import { isLoggedIn } from '../../helpers/users'; import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication'; -export function whenLoggedIn

(WrappedComponent: React.ComponentClass

) { +export function whenLoggedIn

(WrappedComponent: React.ComponentType

) { class Wrapper extends React.Component

{ static displayName = getWrappedDisplayName(WrappedComponent, 'whenLoggedIn'); diff --git a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx index 991005055e2..c016ce0ffa5 100644 --- a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx @@ -29,7 +29,7 @@ interface OwnProps { } export function withUserOrganizations

( - WrappedComponent: React.ComponentClass

> + WrappedComponent: React.ComponentType

> ) { class Wrapper extends React.Component

{ 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 index 00000000000..ebedfeb2745 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx @@ -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 ( + + + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/ui/buttons.css b/server/sonar-web/src/main/js/components/ui/buttons.css index 05df1afe181..cede4ec4d10 100644 --- a/server/sonar-web/src/main/js/components/ui/buttons.css +++ b/server/sonar-web/src/main/js/components/ui/buttons.css @@ -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 */ @@ -113,12 +115,14 @@ .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; @@ -154,6 +158,7 @@ background: transparent !important; cursor: default; } + /* #endregion */ .button-small { @@ -217,6 +222,7 @@ margin: 0 8px; font-size: var(--smallFontSize); } + /* #endregion */ /* #region .button-icon */ @@ -261,4 +267,23 @@ .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); +} diff --git a/server/sonar-web/src/main/js/components/ui/buttons.tsx b/server/sonar-web/src/main/js/components/ui/buttons.tsx index a0dd58c1455..99e4c327a62 100644 --- a/server/sonar-web/src/main/js/components/ui/buttons.tsx +++ b/server/sonar-web/src/main/js/components/ui/buttons.tsx @@ -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, @@ -138,3 +139,12 @@ export function EditButton(props: ActionButtonProps) { ); } + +export function ListButton({ className, children, ...props }: ButtonProps) { + return ( + + ); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ac74be9ca5b..33a2cbb30af 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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}. -- 2.39.5