.setRequired(true)
.setMaximumLength(100)
.setDescription("Setting key")
- .setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE);
+ .setPossibleValues("notifications.optOut", UserUpdater.NOTIFICATIONS_READ_DATE, "organizations.members.dismissSyncNotif");
action.createParam(PARAM_VALUE)
.setRequired(true)
.setParam("value", "true")
.execute();
+ ws.newRequest()
+ .setParam("key", "organizations.members.dismissSyncNotif")
+ .setParam("value", "org1,org2")
+ .execute();
+
assertThat(db.getDbClient().userPropertiesDao().selectByUser(db.getSession(), user))
.extracting(UserPropertyDto::getKey, UserPropertyDto::getValue)
.containsExactlyInAnyOrder(
tuple("notifications.readDate", "1234"),
- tuple("notifications.optOut", "true"));
+ tuple("notifications.optOut", "true"),
+ tuple("organizations.members.dismissSyncNotif", "org1,org2"));
}
@Test
tuple("key", true, 100),
tuple("value", true, 4000));
- assertThat(definition.param("key").possibleValues()).containsExactlyInAnyOrder("notifications.optOut", "notifications.readDate");
+ assertThat(definition.param("key").possibleValues()).containsExactlyInAnyOrder(
+ "notifications.optOut",
+ "notifications.readDate",
+ "organizations.members.dismissSyncNotif");
}
}
interface GetOrganizationNavigation {
adminPages: T.Extension[];
+ alm?: { key: string; membersSync: boolean; url: string };
canUpdateProjectsVisibilityToPrivate: boolean;
isDefault: boolean;
pages: T.Extension[];
export function getOrganizationBilling(organization: string): Promise<OrganizationBilling> {
return getJSON('/api/billing/show', { organization, p: 1, ps: 1 });
}
+
+export function setOrganizationMemberSync(data: { enabled: boolean; organization: string }) {
+ return post('/api/organizations/set_members_sync', data).catch(throwGlobalError);
+}
+
+export function syncMembers(organization: string) {
+ return post('/api/organizations/sync_members', { organization }).catch(throwGlobalError);
+}
<>
<li className="navbar-latest-notification" onClick={this.props.onClick}>
<div className="navbar-latest-notification-wrapper">
- <span className="badge">{translate('new')}</span>
+ <span className="badge badge-new">{translate('new')}</span>
<span className="label">{lastNews.notification}</span>
</div>
</li>
className="navbar-latest-notification-wrapper"
>
<span
- className="badge"
+ className="badge badge-new"
>
new
</span>
color: var(--sonarcloudBlack300);
}
-.navbar-latest-notification-wrapper .badge {
+.navbar-latest-notification-wrapper .badge-new {
position: absolute;
- height: 18px;
margin-right: var(--gridSize);
left: calc(var(--gridSize) / 2);
top: 5px;
- font-size: var(--verySmallFontSize);
- text-transform: uppercase;
- background-color: var(--lightBlue);
- color: var(--darkBlue);
}
.navbar-latest-notification-wrapper .label {
line-height: calc(var(--tinyControlHeight) - 1px) !important;
}
+.badge-new {
+ height: 18px;
+ font-size: var(--verySmallFontSize);
+ text-transform: uppercase;
+ background-color: var(--lightBlue);
+ color: var(--darkBlue);
+}
+
.badge-muted {
background-color: transparent;
color: var(--secondFontColor);
}
.modal-medium {
- width: 800px;
- margin-left: -400px;
+ width: 830px;
+ margin-left: -415px;
}
.modal-large {
module.exports = {
// colors
blue: '#4b9fd5',
+ veryLightBlue: '#f2faff',
lightBlue: '#cae3f2',
darkBlue: '#236a97',
green: '#00aa00',
value: string;
}
- type CurrentUserSettingNames = 'notifications.optOut' | 'notifications.readDate';
+ type CurrentUserSettingNames =
+ | 'notifications.optOut'
+ | 'notifications.readDate'
+ | 'organizations.members.dismissSyncNotif';
export interface CustomMeasure {
createdAt?: string;
export interface Organization extends OrganizationBase {
actions?: OrganizationActions;
- alm?: { key: string; url: string };
+ alm?: OrganizationAlm;
adminPages?: Extension[];
canUpdateProjectsVisibilityToPrivate?: boolean;
guarded?: boolean;
subscription?: OrganizationSubscription;
}
+ export interface OrganizationAlm {
+ key: string;
+ membersSync: boolean;
+ url: string;
+ }
+
export interface OrganizationBase {
avatar?: string;
description?: string;
+++ /dev/null
-/*
- * 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.
- */
-.card-plan {
- display: flex;
- flex-direction: column;
- width: 450px;
- height: 210px;
- background-color: #fff;
- border: solid 1px var(--barBorderColor);
- border-radius: 3px;
- box-sizing: border-box;
- margin-right: calc(2 * var(--gridSize));
- transition: all 0.2s ease;
-}
-
-.card-plan.animated {
- height: 0;
- border-width: 0;
- overflow: hidden;
-}
-
-.card-plan.animated.open {
- height: 210px;
- border-width: 1px;
-}
-
-.card-plan.highlight {
- box-shadow: var(--defaultShadow);
-}
-
-.card-plan:last-child {
- margin-right: 0;
-}
-
-.card-plan:focus {
- outline: none;
-}
-
-.card-plan-actionable {
- cursor: pointer;
-}
-
-.card-plan-actionable:not(.disabled):hover {
- box-shadow: var(--defaultShadow);
- transform: translateY(-2px);
-}
-
-.card-plan-actionable.selected {
- border-color: var(--darkBlue);
-}
-
-.card-plan-actionable.selected .card-plan-recommended {
- border: solid 1px var(--darkBlue);
- border-top: none;
-}
-
-.card-plan-actionable.disabled {
- cursor: not-allowed;
- background-color: var(--disableGrayBg);
- border-color: var(--disableGrayBorder);
-}
-
-.card-plan-actionable.disabled h2,
-.card-plan-actionable.disabled ul {
- color: var(--disableGrayText);
-}
-
-.card-plan-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0;
-}
-
-.card-plan-price {
- font-size: var(--bigFontSize);
-}
-
-.card-plan-body {
- flex-grow: 1;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
-}
-
-.card-plan-body .alert {
- margin-bottom: 0;
-}
-
-.card-plan-recommended {
- position: relative;
- padding: 6px calc(var(--gridSize) * 2);
- left: -1px;
- bottom: -1px;
- width: 450px;
- color: #fff;
- background-color: var(--blue);
- border-radius: 0 0 3px 3px;
- box-sizing: border-box;
- font-size: var(--smallFontSize);
-}
+++ /dev/null
-/*
- * 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 * as classNames from 'classnames';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router';
-import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages';
-import RecommendedIcon from '../../../components/icons-components/RecommendedIcon';
-import { Alert } from '../../../components/ui/Alert';
-import { formatPrice } from '../organization/utils';
-import { translate } from '../../../helpers/l10n';
-import './CardPlan.css';
-
-interface Props {
- className?: string;
- disabled?: boolean;
- onClick?: () => void;
- selected?: boolean;
- startingPrice?: number;
-}
-
-interface CardProps extends Props {
- children: React.ReactNode;
- recommended?: string;
- title: string;
-}
-
-export default function CardPlan(props: CardProps) {
- const { className, disabled, onClick, recommended, selected, startingPrice } = props;
- const isActionable = Boolean(onClick);
- return (
- <div
- aria-checked={selected}
- className={classNames(
- 'card-plan',
- { 'card-plan-actionable': isActionable, disabled, selected },
- className
- )}
- onClick={isActionable && !disabled ? onClick : undefined}
- role="radio"
- tabIndex={0}>
- <h2 className="card-plan-header big-spacer-bottom">
- <span className="display-flex-center">
- {isActionable && (
- <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
- )}
- {props.title}
- </span>
- {startingPrice !== undefined &&
- (startingPrice ? (
- <FormattedMessage
- defaultMessage={translate('billing.price_from_x')}
- id="billing.price_from_x"
- values={{
- price: <span className="card-plan-price">{formatPrice(startingPrice)}</span>
- }}
- />
- ) : (
- <span className="card-plan-price">{formatPrice(0)}</span>
- ))}
- </h2>
- <div className="card-plan-body">{props.children}</div>
- {recommended && (
- <div className="card-plan-recommended">
- <RecommendedIcon className="spacer-right" />
- <FormattedMessage
- defaultMessage={recommended}
- id={recommended}
- values={{ recommended: <strong>{translate('recommended')}</strong> }}
- />
- </div>
- )}
- </div>
- );
-}
-
-interface FreeProps extends Props {
- almName?: string;
- hasWarning: boolean;
-}
-
-export function FreeCardPlan({ almName, hasWarning, ...props }: FreeProps) {
- const showInfo = almName && props.disabled;
- const showWarning = almName && hasWarning && !props.disabled;
-
- return (
- <CardPlan startingPrice={0} title={translate('billing.free_plan.title')} {...props}>
- <>
- <div className="spacer-left">
- <ul className="big-spacer-left note">
- <li className="little-spacer-bottom">
- {translate('billing.free_plan.all_projects_analyzed_public')}
- </li>
- <li>{translate('billing.free_plan.anyone_can_browse_source_code')}</li>
- </ul>
- </div>
- {showWarning && (
- <Alert variant="warning">
- <FormattedMessage
- defaultMessage={translate('billing.free_plan.private_repo_warning')}
- id="billing.free_plan.private_repo_warning"
- values={{ alm: almName }}
- />
- </Alert>
- )}
- {showInfo && (
- <Alert variant="info">
- <FormattedMessage
- defaultMessage={translate('billing.free_plan.not_available_info')}
- id="billing.free_plan.not_available_info"
- values={{ alm: almName }}
- />
- </Alert>
- )}
- </>
- </CardPlan>
- );
-}
-
-interface PaidProps extends Props {
- isRecommended: boolean;
-}
-
-export function PaidCardPlan({ isRecommended, ...props }: PaidProps) {
- return (
- <CardPlan
- recommended={isRecommended ? translate('billing.paid_plan.recommended') : undefined}
- title={translate('billing.paid_plan.title')}
- {...props}>
- <>
- <UpgradeOrganizationAdvantages />
- <div className="big-spacer-left">
- <Link className="spacer-left" target="_blank" to="/about/pricing">
- {translate('billing.pricing.learn_more')}
- </Link>
- </div>
- </>
- </CardPlan>
- );
-}
--- /dev/null
+/*
+ * 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 { FormattedMessage } from 'react-intl';
+import RadioCard, { RadioCardProps } from '../../../components/controls/RadioCard';
+import { Alert } from '../../../components/ui/Alert';
+import { formatPrice } from '../organization/utils';
+import { translate } from '../../../helpers/l10n';
+
+interface Props extends RadioCardProps {
+ almName?: string;
+ hasWarning: boolean;
+}
+
+export default function FreeCardPlan({ almName, hasWarning, ...props }: Props) {
+ const showInfo = almName && props.disabled;
+ const showWarning = almName && hasWarning && !props.disabled;
+
+ return (
+ <RadioCard title={translate('billing.free_plan.title')} titleInfo={formatPrice(0)} {...props}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ <li className="little-spacer-bottom">
+ {translate('billing.free_plan.all_projects_analyzed_public')}
+ </li>
+ <li>{translate('billing.free_plan.anyone_can_browse_source_code')}</li>
+ </ul>
+ </div>
+ {showWarning && (
+ <Alert variant="warning">
+ <FormattedMessage
+ defaultMessage={translate('billing.free_plan.private_repo_warning')}
+ id="billing.free_plan.private_repo_warning"
+ values={{ alm: almName }}
+ />
+ </Alert>
+ )}
+ {showInfo && (
+ <Alert variant="info">
+ <FormattedMessage
+ defaultMessage={translate('billing.free_plan.not_available_info')}
+ id="billing.free_plan.not_available_info"
+ values={{ alm: almName }}
+ />
+ </Alert>
+ )}
+ </RadioCard>
+ );
+}
--- /dev/null
+/*
+ * 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 { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages';
+import RadioCard, { RadioCardProps } from '../../../components/controls/RadioCard';
+import { formatPrice } from '../organization/utils';
+import { translate } from '../../../helpers/l10n';
+
+interface Props extends RadioCardProps {
+ isRecommended: boolean;
+ startingPrice?: number;
+}
+
+export default function PaidCardPlan({ isRecommended, startingPrice, ...props }: Props) {
+ return (
+ <RadioCard
+ recommended={isRecommended ? translate('billing.paid_plan.recommended') : undefined}
+ title={translate('billing.paid_plan.title')}
+ titleInfo={
+ startingPrice !== undefined && (
+ <FormattedMessage
+ defaultMessage={translate('billing.price_from_x')}
+ id="billing.price_from_x"
+ values={{
+ price: <span className="big">{formatPrice(startingPrice)}</span>
+ }}
+ />
+ )
+ }
+ {...props}>
+ <UpgradeOrganizationAdvantages />
+ <div className="big-spacer-left">
+ <Link className="spacer-left" target="_blank" to="/about/pricing">
+ {translate('billing.pricing.learn_more')}
+ </Link>
+ </div>
+ </RadioCard>
+ );
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages';
import UpgradeOrganizationModal from './UpgradeOrganizationModal';
-import CardPlan from './CardPlan';
+import RadioCard from '../../../components/controls/RadioCard';
import { Button } from '../../../components/ui/buttons';
-import { translate, hasMessage } from '../../../helpers/l10n';
+import { formatPrice } from '../organization/utils';
import { getSubscriptionPlans } from '../../../api/billing';
+import { translate, hasMessage } from '../../../helpers/l10n';
interface Props {
className?: string;
return (
<>
- <CardPlan
+ <RadioCard
className={this.props.className}
- startingPrice={startingPrice}
- title={translate('billing.upgrade_box.header')}>
+ title={translate('billing.upgrade_box.header')}
+ titleInfo={
+ startingPrice !== undefined && (
+ <FormattedMessage
+ defaultMessage={translate('billing.price_from_x')}
+ id="billing.price_from_x"
+ values={{
+ price: <span className="big">{formatPrice(startingPrice)}</span>
+ }}
+ />
+ )
+ }>
<>
<UpgradeOrganizationAdvantages />
<div className="big-spacer-left">
</Link>
</div>
</>
- </CardPlan>
+ </RadioCard>
{upgradeOrganizationModal && (
<UpgradeOrganizationModal
insideModal={this.props.insideModal}
+++ /dev/null
-/*
- * 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 CardPlan, { FreeCardPlan, PaidCardPlan } from '../CardPlan';
-import { click } from '../../../../helpers/testUtils';
-
-it('should render correctly', () => {
- expect(
- shallow(
- <CardPlan recommended="Recommended for you" startingPrice={10} title="Paid Plan">
- <div>content</div>
- </CardPlan>
- )
- ).toMatchSnapshot();
-});
-
-it('should be actionable', () => {
- const onClick = jest.fn();
- const wrapper = shallow(
- <CardPlan onClick={onClick} title="Free Plan">
- <div>content</div>
- </CardPlan>
- );
-
- expect(wrapper).toMatchSnapshot();
- click(wrapper);
- wrapper.setProps({ selected: true, startingPrice: 0 });
- expect(wrapper).toMatchSnapshot();
-});
-
-describe('#FreeCardPlan', () => {
- it('should render', () => {
- expect(shallow(<FreeCardPlan hasWarning={false} />)).toMatchSnapshot();
- });
-
- it('should render with warning', () => {
- expect(
- shallow(<FreeCardPlan almName="GitHub" hasWarning={true} selected={true} />)
- ).toMatchSnapshot();
- });
-
- it('should render disabled with info', () => {
- expect(
- shallow(<FreeCardPlan almName="GitHub" disabled={true} hasWarning={false} />)
- ).toMatchSnapshot();
- });
-});
-
-describe('#PaidCardPlan', () => {
- it('should render', () => {
- expect(shallow(<PaidCardPlan isRecommended={true} startingPrice={10} />)).toMatchSnapshot();
- });
-});
--- /dev/null
+/*
+ * 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 FreeCardPlan from '../FreeCardPlan';
+
+it('should render', () => {
+ expect(shallow(<FreeCardPlan hasWarning={false} />)).toMatchSnapshot();
+});
+
+it('should render with warning', () => {
+ expect(
+ shallow(<FreeCardPlan almName="GitHub" hasWarning={true} selected={true} />)
+ ).toMatchSnapshot();
+});
+
+it('should render disabled with info', () => {
+ expect(
+ shallow(<FreeCardPlan almName="GitHub" disabled={true} hasWarning={false} />)
+ ).toMatchSnapshot();
+});
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationSelect, { getOptionRenderer } from '../OrganizationSelect';
+import { mockOrganization, mockOrganizationWithAlm } from '../../../../helpers/testMocks';
-const organizations = [
- { key: 'foo', name: 'Foo' },
- { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
-];
+const organizations = [mockOrganization(), mockOrganizationWithAlm({ key: 'bar', name: 'Bar' })];
it('should render correctly', () => {
expect(
--- /dev/null
+/*
+ * 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 PaidCardPlan from '../PaidCardPlan';
+
+it('should render correctly', () => {
+ expect(shallow(<PaidCardPlan isRecommended={true} startingPrice={10} />)).toMatchSnapshot();
+});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`#FreeCardPlan should render 1`] = `
-<CardPlan
- startingPrice={0}
- title="billing.free_plan.title"
->
- <div
- className="spacer-left"
- >
- <ul
- className="big-spacer-left note"
- >
- <li
- className="little-spacer-bottom"
- >
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
- </div>
-</CardPlan>
-`;
-
-exports[`#FreeCardPlan should render disabled with info 1`] = `
-<CardPlan
- disabled={true}
- startingPrice={0}
- title="billing.free_plan.title"
->
- <div
- className="spacer-left"
- >
- <ul
- className="big-spacer-left note"
- >
- <li
- className="little-spacer-bottom"
- >
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
- </div>
- <Alert
- variant="info"
- >
- <FormattedMessage
- defaultMessage="billing.free_plan.not_available_info"
- id="billing.free_plan.not_available_info"
- values={
- Object {
- "alm": "GitHub",
- }
- }
- />
- </Alert>
-</CardPlan>
-`;
-
-exports[`#FreeCardPlan should render with warning 1`] = `
-<CardPlan
- selected={true}
- startingPrice={0}
- title="billing.free_plan.title"
->
- <div
- className="spacer-left"
- >
- <ul
- className="big-spacer-left note"
- >
- <li
- className="little-spacer-bottom"
- >
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
- </div>
- <Alert
- variant="warning"
- >
- <FormattedMessage
- defaultMessage="billing.free_plan.private_repo_warning"
- id="billing.free_plan.private_repo_warning"
- values={
- Object {
- "alm": "GitHub",
- }
- }
- />
- </Alert>
-</CardPlan>
-`;
-
-exports[`#PaidCardPlan should render 1`] = `
-<CardPlan
- recommended="billing.paid_plan.recommended"
- startingPrice={10}
- title="billing.paid_plan.title"
->
- <UpgradeOrganizationAdvantages />
- <div
- className="big-spacer-left"
- >
- <Link
- className="spacer-left"
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/about/pricing"
- >
- billing.pricing.learn_more
- </Link>
- </div>
-</CardPlan>
-`;
-
-exports[`should be actionable 1`] = `
-<div
- className="card-plan card-plan-actionable"
- onClick={[MockFunction]}
- role="radio"
- tabIndex={0}
->
- <h2
- className="card-plan-header big-spacer-bottom"
- >
- <span
- className="display-flex-center"
- >
- <i
- className="icon-radio spacer-right"
- />
- Free Plan
- </span>
- </h2>
- <div
- className="card-plan-body"
- >
- <div>
- content
- </div>
- </div>
-</div>
-`;
-
-exports[`should be actionable 2`] = `
-<div
- aria-checked={true}
- className="card-plan card-plan-actionable selected"
- onClick={
- [MockFunction] {
- "calls": Array [
- Array [
- Object {
- "currentTarget": Object {
- "blur": [Function],
- },
- "preventDefault": [Function],
- "stopPropagation": [Function],
- "target": Object {
- "blur": [Function],
- },
- },
- ],
- ],
- "results": Array [
- Object {
- "isThrow": false,
- "value": undefined,
- },
- ],
- }
- }
- role="radio"
- tabIndex={0}
->
- <h2
- className="card-plan-header big-spacer-bottom"
- >
- <span
- className="display-flex-center"
- >
- <i
- className="icon-radio spacer-right is-checked"
- />
- Free Plan
- </span>
- <span
- className="card-plan-price"
- >
- billing.price_format.0
- </span>
- </h2>
- <div
- className="card-plan-body"
- >
- <div>
- content
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correctly 1`] = `
-<div
- className="card-plan"
- role="radio"
- tabIndex={0}
->
- <h2
- className="card-plan-header big-spacer-bottom"
- >
- <span
- className="display-flex-center"
- >
- Paid Plan
- </span>
- <FormattedMessage
- defaultMessage="billing.price_from_x"
- id="billing.price_from_x"
- values={
- Object {
- "price": <span
- className="card-plan-price"
- >
- billing.price_format.10
- </span>,
- }
- }
- />
- </h2>
- <div
- className="card-plan-body"
- >
- <div>
- content
- </div>
- </div>
- <div
- className="card-plan-recommended"
- >
- <RecommendedIcon
- className="spacer-right"
- />
- <FormattedMessage
- defaultMessage="Recommended for you"
- id="Recommended for you"
- values={
- Object {
- "recommended": <strong>
- recommended
- </strong>,
- }
- }
- />
- </div>
-</div>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<RadioCard
+ title="billing.free_plan.title"
+ titleInfo="billing.price_format.0"
+>
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
+</RadioCard>
+`;
+
+exports[`should render disabled with info 1`] = `
+<RadioCard
+ disabled={true}
+ title="billing.free_plan.title"
+ titleInfo="billing.price_format.0"
+>
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
+ <Alert
+ variant="info"
+ >
+ <FormattedMessage
+ defaultMessage="billing.free_plan.not_available_info"
+ id="billing.free_plan.not_available_info"
+ values={
+ Object {
+ "alm": "GitHub",
+ }
+ }
+ />
+ </Alert>
+</RadioCard>
+`;
+
+exports[`should render with warning 1`] = `
+<RadioCard
+ selected={true}
+ title="billing.free_plan.title"
+ titleInfo="billing.price_format.0"
+>
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
+ <Alert
+ variant="warning"
+ >
+ <FormattedMessage
+ defaultMessage="billing.free_plan.private_repo_warning"
+ id="billing.free_plan.private_repo_warning"
+ values={
+ Object {
+ "alm": "GitHub",
+ }
+ }
+ />
+ </Alert>
+</RadioCard>
+`;
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<RadioCard
+ recommended="billing.paid_plan.recommended"
+ title="billing.paid_plan.title"
+ titleInfo={
+ <FormattedMessage
+ defaultMessage="billing.price_from_x"
+ id="billing.price_from_x"
+ values={
+ Object {
+ "price": <span
+ className="big"
+ >
+ billing.price_format.10
+ </span>,
+ }
+ }
+ />
+ }
+>
+ <UpgradeOrganizationAdvantages />
+ <div
+ className="big-spacer-left"
+ >
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/about/pricing"
+ >
+ billing.pricing.learn_more
+ </Link>
+ </div>
+</RadioCard>
+`;
exports[`should render correctly 1`] = `
<Fragment>
- <CardPlan
- startingPrice={10}
+ <RadioCard
title="billing.upgrade_box.header"
+ titleInfo={
+ <FormattedMessage
+ defaultMessage="billing.price_from_x"
+ id="billing.price_from_x"
+ values={
+ Object {
+ "price": <span
+ className="big"
+ >
+ billing.price_format.10
+ </span>,
+ }
+ }
+ />
+ }
>
<UpgradeOrganizationAdvantages />
<div
billing.pricing.learn_more
</Link>
</div>
- </CardPlan>
+ </RadioCard>
</Fragment>
`;
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
export default function AlmApplicationInstalling({ almKey }: { almKey?: string }) {
return (
<div className="huge-spacer-top text-center">
<i className="spinner" />
<p className="big-spacer-top">
- {almKey
- ? translate('onboarding.import_organization.installing', almKey)
- : translate('onboarding.import_organization.installing')}
+ {translate(
+ 'onboarding.import_organization.installing',
+ sanitizeAlmId(almKey) || 'ALM'
+ )}
</p>
</div>
</div>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { FreeCardPlan, PaidCardPlan } from '../components/CardPlan';
+import FreeCardPlan from '../components/FreeCardPlan';
+import PaidCardPlan from '../components/PaidCardPlan';
import { translate } from '../../../helpers/l10n';
export enum Plan {
--- /dev/null
+/*
+ * 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 AlmApplicationInstalling from '../AlmApplicationInstalling';
+
+it('should render correctly', () => {
+ expect(shallow(<AlmApplicationInstalling />)).toMatchSnapshot();
+ expect(shallow(<AlmApplicationInstalling almKey="github" />)).toMatchSnapshot();
+});
import { getSubscriptionPlans } from '../../../../api/billing';
import { getOrganizations } from '../../../../api/organizations';
import { get, remove } from '../../../../helpers/storage';
-import { mockRouter } from '../../../../helpers/testMocks';
+import {
+ mockRouter,
+ mockOrganizationWithAdminActions,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
jest.mock('../../../../api/billing', () => ({
skipOnboarding={jest.fn()}
updateOrganization={jest.fn()}
userOrganizations={[
- { actions: { admin: true }, key: 'foo', name: 'Foo' },
- { actions: { admin: true }, alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' },
- { actions: { admin: false }, key: 'baz', name: 'Baz' }
+ mockOrganizationWithAdminActions(),
+ mockOrganizationWithAdminActions(mockOrganizationWithAlm({ key: 'bar', name: 'Bar' })),
+ mockOrganizationWithAdminActions({ key: 'baz', name: 'Baz' }, { admin: false })
]}
{...props}
/>
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<DeferredSpinner
+ customSpinner={
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="huge-spacer-top text-center"
+ >
+ <i
+ className="spinner"
+ />
+ <p
+ className="big-spacer-top"
+ >
+ onboarding.import_organization.installing.ALM
+ </p>
+ </div>
+ </div>
+ }
+ timeout={100}
+/>
+`;
+
+exports[`should render correctly 2`] = `
+<DeferredSpinner
+ customSpinner={
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <div
+ className="huge-spacer-top text-center"
+ >
+ <i
+ className="spinner"
+ />
+ <p
+ className="big-spacer-top"
+ >
+ onboarding.import_organization.installing.github
+ </p>
+ </div>
+ </div>
+ }
+ timeout={100}
+/>
+`;
import * as React from 'react';
import { shallow } from 'enzyme';
import AutoProjectCreate from '../AutoProjectCreate';
+import {
+ mockOrganizationWithAdminActions,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
const almApplication = {
backgroundColor: 'blue',
};
const boundOrganizations: T.Organization[] = [
- {
- actions: { admin: true },
- alm: { key: 'github', url: '' },
- key: 'foo',
- name: 'Foo',
- subscription: 'FREE'
- },
- {
- alm: { key: 'github', url: '' },
- key: 'bar',
- name: 'Bar',
- subscription: 'FREE'
- }
+ mockOrganizationWithAdminActions(mockOrganizationWithAlm({ subscription: 'FREE' })),
+ mockOrganizationWithAlm({ key: 'bar', name: 'Bar', subscription: 'FREE' })
];
it('should display the provider app install button', () => {
import { shallow } from 'enzyme';
import { CreateProjectPageSonarCloud } from '../CreateProjectPageSonarCloud';
import { getAlmAppInfo } from '../../../../api/alm-integration';
-import { mockRouter } from '../../../../helpers/testMocks';
+import {
+ mockRouter,
+ mockOrganizationWithAdminActions,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
jest.mock('../../../../api/alm-integration', () => ({
router={mockRouter()}
skipOnboarding={jest.fn()}
userOrganizations={[
- { actions: { provision: true }, key: 'foo', name: 'Foo' },
- { actions: { provision: true }, alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' },
- { actions: { provision: false }, key: 'baz', name: 'Baz' }
+ mockOrganizationWithAdminActions({}, { admin: false, provision: true }),
+ mockOrganizationWithAdminActions(mockOrganizationWithAlm({ key: 'bar', name: 'Bar' }), {
+ admin: false,
+ provision: true
+ }),
+ mockOrganizationWithAdminActions(
+ { key: 'baz', name: 'Baz' },
+ { admin: false, provision: false }
+ )
]}
{...props}
/>
import * as React from 'react';
import { shallow } from 'enzyme';
import { OrganizationInput } from '../OrganizationInput';
-import { mockRouter } from '../../../../helpers/testMocks';
+import {
+ mockRouter,
+ mockOrganization,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
-const organizations = [
- { key: 'foo', name: 'Foo' },
- { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
-];
+const organizations = [mockOrganization(), mockOrganizationWithAlm({ key: 'bar', name: 'Bar' })];
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
import RemoteRepositories from '../RemoteRepositories';
import { getRepositories } from '../../../../api/alm-integration';
import { waitAndUpdate } from '../../../../helpers/testUtils';
+import {
+ mockOrganizationWithAlm,
+ mockOrganizationWithAdminActions
+} from '../../../../helpers/testMocks';
jest.mock('../../../../api/alm-integration', () => ({
getRepositories: jest.fn().mockResolvedValue({
name: 'GitHub'
};
-const organization: T.Organization = {
- alm: { key: 'github', url: '' },
- key: 'sonarsource',
- name: 'SonarSource',
- subscription: 'FREE'
-};
+const organization: T.Organization = mockOrganizationWithAlm({ subscription: 'FREE' });
beforeEach(() => {
(getRepositories as jest.Mock<any>).mockClear();
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
- expect(getRepositories).toHaveBeenCalledWith({ organization: 'sonarsource' });
+ expect(getRepositories).toHaveBeenCalledWith({ organization: 'foo' });
});
it('should display the organization upgrade box', async () => {
repositories: [{ label: 'Bar Project', installationKey: 'github/bar', private: true }]
});
const wrapper = shallowRender({
- organization: {
- actions: { admin: true },
- alm: { key: 'github', url: '' },
- key: 'foobar',
- name: 'FooBar',
- subscription: 'PAID'
- }
+ organization: mockOrganizationWithAdminActions(
+ mockOrganizationWithAlm({ subscription: 'PAID' })
+ )
});
expect(wrapper.find('UpgradeOrganizationBox').exists()).toBe(false);
import SetupProjectBox from '../SetupProjectBox';
import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
import { provisionProject } from '../../../../api/alm-integration';
+import { mockOrganizationWithAlm } from '../../../../helpers/testMocks';
jest.mock('../../../../api/alm-integration', () => ({
provisionProject: jest
.mockResolvedValue({ projects: [{ projectKey: 'awesome' }, { projectKey: 'foo' }] })
}));
-const organization: T.Organization = {
- alm: { key: 'github', url: '' },
- key: 'sonarsource',
- name: 'SonarSource',
- subscription: 'FREE'
-};
-
-const selectedRepositories = [
- { label: 'Awesome Project', installationKey: 'github/awesome' },
- { label: 'Foo', installationKey: 'github/foo', private: true }
-];
-
it('should correctly create projects', async () => {
const onProjectCreate = jest.fn();
const wrapper = shallowRender({ onProjectCreate });
submit(wrapper.find('form'));
expect(provisionProject).toBeCalledWith({
installationKeys: ['github/awesome', 'github/foo'],
- organization: 'sonarsource'
+ organization: 'foo'
});
await waitAndUpdate(wrapper);
- expect(onProjectCreate).toBeCalledWith(['awesome', 'foo'], 'sonarsource');
+ expect(onProjectCreate).toBeCalledWith(['awesome', 'foo'], 'foo');
});
function shallowRender(props: Partial<SetupProjectBox['props']> = {}) {
<SetupProjectBox
onProjectCreate={jest.fn()}
onProvisionFail={jest.fn()}
- organization={organization}
- selectedRepositories={selectedRepositories}
+ organization={mockOrganizationWithAlm({ subscription: 'FREE' })}
+ selectedRepositories={[
+ { label: 'Awesome Project', installationKey: 'github/awesome' },
+ { label: 'Foo', installationKey: 'github/foo', private: true }
+ ]}
{...props}
/>
);
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "foo",
"name": "Foo",
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "foo",
"name": "Foo",
Array [
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
Array [
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"key": "foo",
},
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
Array [
Object {
"actions": Object {
+ "admin": false,
"provision": true,
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
"key": "bar",
"name": "Bar",
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
- "key": "sonarsource",
- "name": "SonarSource",
+ "key": "foo",
+ "name": "Foo",
"subscription": "FREE",
}
}
Object {
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
- "key": "sonarsource",
- "name": "SonarSource",
+ "key": "foo",
+ "name": "Foo",
"subscription": "FREE",
}
}
},
"alm": Object {
"key": "github",
- "url": "",
+ "membersSync": false,
+ "url": "https://github.com/foo",
},
- "key": "sonarsource",
- "name": "SonarSource",
+ "key": "foo",
+ "name": "Foo",
"subscription": "FREE",
}
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import HelpTooltip from '../../components/controls/HelpTooltip';
import SearchBox from '../../components/controls/SearchBox';
+import { getAlmMembersUrl, sanitizeAlmId } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
-import { translate } from '../../helpers/l10n';
-interface Props {
+export interface Props {
+ currentUser: T.LoggedInUser;
handleSearch: (query?: string) => void;
+ organization: T.Organization;
total?: number;
}
-export default function MembersListHeader({ handleSearch, total }: Props) {
+export default function MembersListHeader({
+ currentUser,
+ handleSearch,
+ organization,
+ total
+}: Props) {
return (
<div className="panel panel-vertical bordered-bottom spacer-bottom">
<SearchBox
{total !== undefined && (
<span className="pull-right little-spacer-top">
<strong>{formatMeasure(total, 'INT')}</strong> {translate('organization.members.members')}
+ {organization.alm &&
+ organization.alm.membersSync && (
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div className="abs-width-300 markdown cut-margins">
+ <p>
+ {translate(
+ 'organization.members.auto_sync_total_help',
+ sanitizeAlmId(organization.alm.key) || ''
+ )}
+ </p>
+ {currentUser.personalOrganization !== organization.key && (
+ <>
+ <hr />
+ <p>
+ <a
+ href={getAlmMembersUrl(organization.alm)}
+ rel="noopener noreferrer"
+ target="_blank">
+ {translateWithParameters(
+ 'organization.members.see_all_members_on_x',
+ translate(sanitizeAlmId(organization.alm.key) || '')
+ )}
+ </a>
+ </p>
+ </>
+ )}
+ </div>
+ }
+ />
+ )}
</span>
)}
</div>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
-import { translate } from '../../helpers/l10n';
+import AddMemberForm from './AddMemberForm';
+import SyncMemberForm from './SyncMemberForm';
import DeferredSpinner from '../../components/common/DeferredSpinner';
+import DocTooltip from '../../components/docs/DocTooltip';
+import NewInfoBox from '../../components/ui/NewInfoBox';
+import { sanitizeAlmId } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { getCurrentUserSetting, Store } from '../../store/rootReducer';
+import { setCurrentUserSetting } from '../../store/users';
interface Props {
- children?: React.ReactNode;
+ dismissSyncNotifOrg: string[];
+ handleAddMember: (member: T.OrganizationMember) => void;
loading: boolean;
+ members?: T.OrganizationMember[];
+ organization: T.Organization;
+ setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
}
-export default function MembersPageHeader(props: Props) {
- return (
- <header className="page-header">
- <h1 className="page-title">{translate('organization.members.page')}</h1>
- <DeferredSpinner loading={props.loading} />
- {props.children}
- <p className="page-description">
- <FormattedMessage
- defaultMessage={translate('organization.members.page.description')}
- id="organization.members.page.description"
- values={{
- link: (
- <Link to="/documentation/organizations/manage-team/">
- {translate('organization.members.manage_a_team')}
- </Link>
- )
- }}
- />
- </p>
- </header>
- );
+export class MembersPageHeader extends React.PureComponent<Props> {
+ handleDismissSyncNotif = () => {
+ const { dismissSyncNotifOrg, organization } = this.props;
+ this.props.setCurrentUserSetting({
+ key: 'organizations.members.dismissSyncNotif',
+ value: [...dismissSyncNotifOrg, organization.key].join(',')
+ });
+ };
+
+ render() {
+ const { dismissSyncNotifOrg, members, organization } = 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);
+ const hasMemberSync = organization.alm && organization.alm.membersSync;
+ const showSyncNotif =
+ isAdmin &&
+ organization.alm &&
+ !hasMemberSync &&
+ !dismissSyncNotifOrg.some(orgKey => orgKey === organization.key);
+
+ return (
+ <header className="page-header">
+ <h1 className="page-title">{translate('organization.members.page')}</h1>
+ <DeferredSpinner loading={this.props.loading} />
+ {isAdmin && (
+ <div className="page-actions text-right">
+ {almKey && !showSyncNotif && <SyncMemberForm organization={organization} />}
+ {!hasMemberSync && (
+ <div className="display-inline-block spacer-left spacer-bottom">
+ <AddMemberForm
+ addMember={this.props.handleAddMember}
+ memberLogins={memberLogins}
+ organization={organization}
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
+ />
+ </div>
+ )}
+ {almKey &&
+ showSyncNotif && (
+ <NewInfoBox
+ description={translate('organization.members.auto_sync_members_from_org', almKey)}
+ onClose={this.handleDismissSyncNotif}
+ title={translateWithParameters(
+ 'organization.members.auto_sync_with_x',
+ translate(almKey)
+ )}>
+ <SyncMemberForm organization={organization} />
+ </NewInfoBox>
+ )}
+ </div>
+ )}
+ <div className="page-description">
+ <FormattedMessage
+ defaultMessage={translate('organization.members.page.description')}
+ id="organization.members.page.description"
+ values={{
+ link: (
+ <Link to="/documentation/organizations/manage-team/">
+ {translate('organization.members.manage_a_team')}
+ </Link>
+ )
+ }}
+ />
+ </div>
+ </header>
+ );
+ }
}
+
+const mapStateToProps = (state: Store) => ({
+ dismissSyncNotifOrg: (
+ getCurrentUserSetting(state, 'organizations.members.dismissSyncNotif') || ''
+ ).split(',')
+});
+
+const mapDispatchToProps = { setCurrentUserSetting };
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MembersPageHeader);
import MembersPageHeader from './MembersPageHeader';
import MembersListHeader from './MembersListHeader';
import MembersList from './MembersList';
-import AddMemberForm from './AddMemberForm';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import ListFooter from '../../components/controls/ListFooter';
-import DocTooltip from '../../components/docs/DocTooltip';
import { translate } from '../../helpers/l10n';
import { searchMembers, addMember, removeMember } from '../../api/organizations';
import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../api/user_groups';
interface Props {
+ currentUser: T.LoggedInUser;
organization: T.Organization;
}
render() {
const { organization } = this.props;
const { groups, loading, members, paging } = this.state;
- const memberLogins = members ? members.map(member => member.login) : [];
return (
<div className="page page-limited">
<Helmet title={translate('organization.members.page')} />
<Suggestions suggestions="organization_members" />
- <MembersPageHeader loading={loading}>
- {organization.actions &&
- organization.actions.admin && (
- <div className="page-actions">
- <AddMemberForm
- addMember={this.handleAddMember}
- memberLogins={memberLogins}
- organization={organization}
- />
- <DocTooltip
- className="spacer-left"
- doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
- />
- </div>
- )}
- </MembersPageHeader>
+ <MembersPageHeader
+ handleAddMember={this.handleAddMember}
+ loading={loading}
+ members={members}
+ organization={organization}
+ />
{members !== undefined &&
paging !== undefined && (
<>
- <MembersListHeader handleSearch={this.handleSearchMembers} total={paging.total} />
+ <MembersListHeader
+ currentUser={this.props.currentUser}
+ handleSearch={this.handleSearchMembers}
+ organization={organization}
+ total={paging.total}
+ />
<MembersList
members={members}
organization={organization}
import { connect } from 'react-redux';
import OrganizationMembers from './OrganizationMembers';
import { getOrganizationByKey, Store } from '../../store/rootReducer';
+import { withCurrentUser } from '../../components/hoc/withCurrentUser';
interface OwnProps {
params: { organizationKey: string };
return { organization: getOrganizationByKey(state, ownProps.params.organizationKey) };
};
-export default connect(mapStateToProps)(OrganizationMembers);
+export default withCurrentUser(connect(mapStateToProps)(OrganizationMembers));
--- /dev/null
+/*
+ * 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 { connect } from 'react-redux';
+import { Link } from 'react-router';
+import ConfirmButton from '../../components/controls/ConfirmButton';
+import RadioCard from '../../components/controls/RadioCard';
+import { Alert } from '../../components/ui/Alert';
+import { Button } from '../../components/ui/buttons';
+import { setOrganizationMemberSync, syncMembers } from '../../api/organizations';
+import { sanitizeAlmId, isGithub } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { fetchOrganization } from '../../store/rootActions';
+
+interface Props {
+ fetchOrganization: (key: string) => void;
+ organization: T.Organization;
+}
+
+interface State {
+ membersSync: boolean;
+}
+
+export class SyncMemberForm extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ membersSync: Boolean(props.organization.alm && props.organization.alm.membersSync)
+ };
+ }
+
+ handleConfirm = () => {
+ const { organization } = this.props;
+ const { membersSync } = this.state;
+ 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 Promise.resolve();
+ });
+ };
+
+ handleManualClick = () => {
+ this.setState({ membersSync: false });
+ };
+
+ handleAutoClick = () => {
+ this.setState({ membersSync: true });
+ };
+
+ renderModalBody = () => {
+ const { membersSync } = this.state;
+ const { organization } = this.props;
+ const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
+ return (
+ <>
+ {translate('organization.members.management.description')}
+ <Link
+ className="spacer-left"
+ target="_blank"
+ to={{ pathname: '/documentation/organizations/manage-team/' }}>
+ {translate('learn_more')}
+ </Link>
+ <div className="display-flex-stretch big-spacer-top">
+ <RadioCard
+ onClick={this.handleManualClick}
+ selected={!membersSync}
+ title={translate('organization.members.management.manual')}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ <li className="spacer-bottom">
+ {translate('organization.members.management.manual.add_members_manually')}
+ </li>
+ <li>
+ {translate('organization.members.management.manual.choose_members_permissions')}
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={this.handleAutoClick}
+ selected={membersSync}
+ title={translateWithParameters(
+ 'organization.members.management.automatic',
+ translate(almKey || '')
+ )}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ {almKey && (
+ <>
+ <li className="spacer-bottom">
+ {translate(
+ 'organization.members.management.automatic.synchronized_from',
+ almKey
+ )}
+ </li>
+ <li className="spacer-bottom">
+ {translate(
+ 'organization.members.management.automatic.members_changes_reflected',
+ almKey
+ )}
+ </li>
+ </>
+ )}
+ <li>
+ {translate(
+ 'organization.members.management.automatic.still_choose_members_permissions'
+ )}
+ </li>
+ </ul>
+ </div>
+ {(!organization.alm || !organization.alm.membersSync) && (
+ <Alert className="big-spacer-top" variant="warning">
+ {translate('organization.members.management.automatic.warning')}
+ </Alert>
+ )}
+ </RadioCard>
+ </div>
+ </>
+ );
+ };
+
+ render() {
+ const { organization } = this.props;
+ const orgMemberSync = Boolean(organization.alm && organization.alm.membersSync);
+ return (
+ <ConfirmButton
+ cancelButtonText={translate('close')}
+ confirmButtonText={translate('save')}
+ confirmDisable={this.state.membersSync === orgMemberSync}
+ medium={true}
+ modalBody={this.renderModalBody()}
+ modalHeader={translate('organization.members.management.title')}
+ onConfirm={this.handleConfirm}>
+ {({ onClick }) => (
+ <Button onClick={onClick}>{translate('organization.members.config_synchro')}</Button>
+ )}
+ </ConfirmButton>
+ );
+ }
+}
+
+const mapDispatchToProps = { fetchOrganization };
+
+export default connect(
+ null,
+ mapDispatchToProps
+)(SyncMemberForm);
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import MembersListHeader from '../MembersListHeader';
+import MembersListHeader, { Props } from '../MembersListHeader';
+import {
+ mockOrganization,
+ mockCurrentUser,
+ mockOrganizationWithAlm
+} from '../../../helpers/testMocks';
it('should render without the total', () => {
- const wrapper = shallow(<MembersListHeader handleSearch={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender({ total: undefined })).toMatchSnapshot();
});
it('should render with the total', () => {
- const wrapper = shallow(<MembersListHeader handleSearch={jest.fn()} total={8} />);
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
+
+it('should render a help tooltip', () => {
+ expect(
+ shallowRender({ organization: mockOrganizationWithAlm({}, { membersSync: true }) }).find(
+ 'HelpTooltip'
+ )
+ ).toMatchSnapshot();
+ expect(
+ shallowRender({
+ organization: mockOrganizationWithAlm(
+ {},
+ { key: 'bitbucket', membersSync: true, url: 'https://bitbucket.com/foo' }
+ )
+ }).find('HelpTooltip')
+ ).toMatchSnapshot();
+});
+
+it('should not render link in help tooltip', () => {
+ expect(
+ shallowRender({
+ currentUser: mockCurrentUser({ personalOrganization: 'foo' }),
+ organization: mockOrganizationWithAlm({}, { membersSync: true })
+ }).find('HelpTooltip')
+ ).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <MembersListHeader
+ currentUser={mockCurrentUser()}
+ handleSearch={jest.fn()}
+ organization={mockOrganization()}
+ total={8}
+ {...props}
+ />
+ );
+}
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import MembersPageHeader from '../MembersPageHeader';
+import { MembersPageHeader } from '../MembersPageHeader';
+import {
+ mockOrganization,
+ mockOrganizationWithAlm,
+ mockOrganizationWithAdminActions
+} from '../../../helpers/testMocks';
-it('should render', () => {
- const wrapper = shallow(
- <MembersPageHeader loading={true}>
- <span>children test</span>
- </MembersPageHeader>
- );
- expect(wrapper).toMatchSnapshot();
+it('should render correctly', () => {
+ expect(shallowRender({ loading: true })).toMatchSnapshot();
+});
+
+it('should render for admin', () => {
+ expect(
+ shallowRender({ organization: mockOrganization({ actions: { admin: true } }) })
+ ).toMatchSnapshot();
+});
+
+it('should render for bound organization without sync', () => {
+ const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions());
+ expect(shallowRender({ organization })).toMatchSnapshot();
+
+ const wrapper = shallowRender({ organization, dismissSyncNotifOrg: [organization.key] });
+ expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
+ expect(wrapper.find('NewInfoBox').exists()).toBe(false);
+});
+
+it('should render for bound organization with sync', () => {
+ const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions(), {
+ membersSync: true
+ });
+ const wrapper = shallowRender({ organization });
+ expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
+ expect(wrapper.find('AddMemberForm').exists()).toBe(false);
+ expect(wrapper.find('NewInfoBox').exists()).toBe(false);
});
+
+function shallowRender(props: Partial<MembersPageHeader['props']> = {}) {
+ return shallow(
+ <MembersPageHeader
+ dismissSyncNotifOrg={[]}
+ handleAddMember={jest.fn()}
+ loading={false}
+ members={[]}
+ organization={mockOrganization()}
+ setCurrentUserSetting={jest.fn()}
+ {...props}
+ />
+ );
+}
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationMembers from '../OrganizationMembers';
-import { waitAndUpdate } from '../../../helpers/testUtils';
import { searchMembers, addMember, removeMember } from '../../../api/organizations';
import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
+import {
+ mockOrganization,
+ mockCurrentUser,
+ mockOrganizationWithAdminActions
+} from '../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../helpers/testUtils';
jest.mock('../../../api/organizations', () => ({
addMember: jest.fn().mockResolvedValue({ login: 'bar', name: 'Bar', groupCount: 1 }),
})
}));
-const organization = { key: 'foo', name: 'Foo' };
-
beforeEach(() => {
(searchMembers as jest.Mock).mockClear();
(searchUsersGroups as jest.Mock).mockClear();
});
it('should fetch members and render for non-admin', async () => {
- const wrapper = shallow(<OrganizationMembers organization={organization} />);
+ const wrapper = shallowRender({ organization: mockOrganization() });
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(searchMembers).toBeCalledWith({ organization: 'foo', ps: 50, q: undefined });
});
-it('should fetch members and groups and render for admin', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+it('should fetch members and groups for admin', async () => {
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
expect(searchMembers).toBeCalledWith({ organization: 'foo', ps: 50, q: undefined });
expect(searchUsersGroups).toBeCalledWith({ organization: 'foo' });
});
it('should search users', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersListHeader').prop<Function>('handleSearch')('user');
expect(searchMembers).lastCalledWith({ organization: 'foo', ps: 50, q: 'user' });
});
it('should load more members', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('ListFooter').prop<Function>('loadMore')();
expect(searchMembers).lastCalledWith({ organization: 'foo', p: 2, ps: 50, q: undefined });
});
it('should add new member', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
- wrapper.find('AddMemberForm').prop<Function>('addMember')({ login: 'bar' });
+ wrapper.find('Connect(MembersPageHeader)').prop<Function>('handleAddMember')({ login: 'bar' });
await waitAndUpdate(wrapper);
expect(
wrapper
});
it('should remove member', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersList').prop<Function>('removeMember')({ login: 'john' });
await waitAndUpdate(wrapper);
});
it('should update groups', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersList').prop<Function>('updateMemberGroups')(
{ login: 'john' },
expect(removeUserFromGroup).toHaveBeenCalledTimes(1);
expect(removeUserFromGroup).toBeCalledWith({ login: 'john', name: 'birds', organization: 'foo' });
});
+
+function shallowRender(props: Partial<OrganizationMembers['props']> = {}) {
+ return shallow(
+ <OrganizationMembers
+ currentUser={mockCurrentUser()}
+ organization={mockOrganizationWithAdminActions()}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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 { SyncMemberForm } from '../SyncMemberForm';
+import { setOrganizationMemberSync, syncMembers } from '../../../api/organizations';
+import { mockOrganizationWithAlm } from '../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+
+jest.mock('../../../api/organizations', () => ({
+ setOrganizationMemberSync: jest.fn().mockResolvedValue(undefined),
+ syncMembers: jest.fn().mockResolvedValue(undefined)
+}));
+
+beforeEach(() => {
+ (setOrganizationMemberSync as jest.Mock).mockClear();
+ (syncMembers as jest.Mock).mockClear();
+});
+
+it('should allow to switch to automatic mode with github', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({ fetchOrganization });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: true });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: true });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).toHaveBeenCalledWith('foo');
+});
+
+it('should allow to switch to automatic mode with bitbucket', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({
+ fetchOrganization,
+ organization: mockOrganizationWithAlm({}, { key: 'bitbucket' })
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: true });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: true });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).not.toHaveBeenCalled();
+});
+
+it('should allow to switch to manual mode', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({
+ fetchOrganization,
+ organization: mockOrganizationWithAlm({}, { membersSync: true })
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: false });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: false });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).not.toHaveBeenCalled();
+});
+
+function shallowRender(props: Partial<SyncMemberForm['props']> = {}) {
+ return shallow<SyncMemberForm>(
+ <SyncMemberForm
+ fetchOrganization={jest.fn()}
+ organization={mockOrganizationWithAlm()}
+ {...props}
+ />
+ );
+}
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should not render link in help tooltip 1`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.github
+ </p>
+ </div>
+ }
+/>
+`;
+
+exports[`should render a help tooltip 1`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.github
+ </p>
+ <React.Fragment>
+ <hr />
+ <p>
+ <a
+ href="https://github.com/orgs/foo/people"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ organization.members.see_all_members_on_x.github
+ </a>
+ </p>
+ </React.Fragment>
+ </div>
+ }
+/>
+`;
+
+exports[`should render a help tooltip 2`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.bitbucket
+ </p>
+ <React.Fragment>
+ <hr />
+ <p>
+ <a
+ href="https://bitbucket.com/foo/profile/members"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ organization.members.see_all_members_on_x.bitbucket
+ </a>
+ </p>
+ </React.Fragment>
+ </div>
+ }
+/>
+`;
+
exports[`should render with the total 1`] = `
<div
className="panel panel-vertical bordered-bottom spacer-bottom"
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render 1`] = `
+exports[`should render correctly 1`] = `
<header
className="page-header"
>
loading={true}
timeout={100}
/>
- <span>
- children test
- </span>
- <p
+ <div
className="page-description"
>
<FormattedMessage
}
}
/>
- </p>
+ </div>
+</header>
+`;
+
+exports[`should render for admin 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ organization.members.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions text-right"
+ >
+ <div
+ className="display-inline-block spacer-left spacer-bottom"
+ >
+ <AddMemberForm
+ addMember={[MockFunction]}
+ memberLogins={Array []}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </div>
+ </div>
+ <div
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="organization.members.page.description"
+ id="organization.members.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/organizations/manage-team/"
+ >
+ organization.members.manage_a_team
+ </Link>,
+ }
+ }
+ />
+ </div>
+</header>
+`;
+
+exports[`should render for bound organization without sync 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ organization.members.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions text-right"
+ >
+ <div
+ className="display-inline-block spacer-left spacer-bottom"
+ >
+ <AddMemberForm
+ addMember={[MockFunction]}
+ memberLogins={Array []}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "membersSync": false,
+ "url": "https://github.com/foo",
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </div>
+ <NewInfoBox
+ description="organization.members.auto_sync_members_from_org.github"
+ onClose={[Function]}
+ title="organization.members.auto_sync_with_x.github"
+ >
+ <Connect(SyncMemberForm)
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "membersSync": false,
+ "url": "https://github.com/foo",
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ </NewInfoBox>
+ </div>
+ <div
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="organization.members.page.description"
+ id="organization.members.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/organizations/manage-team/"
+ >
+ organization.members.manage_a_team
+ </Link>,
+ }
+ }
+ />
+ </div>
</header>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should fetch members and groups and render for admin 1`] = `
+exports[`should fetch members and render for non-admin 1`] = `
<div
className="page page-limited"
>
<Suggestions
suggestions="organization_members"
/>
- <MembersPageHeader
- loading={false}
- >
- <div
- className="page-actions"
- >
- <AddMemberForm
- addMember={[Function]}
- memberLogins={
- Array [
- "admin",
- "john",
- ]
- }
- organization={
- Object {
- "actions": Object {
- "admin": true,
- },
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <DocTooltip
- className="spacer-left"
- doc={Promise {}}
- />
- </div>
- </MembersPageHeader>
- <MembersListHeader
- handleSearch={[Function]}
- total={3}
- />
- <MembersList
- members={
- Array [
- Object {
- "avatar": "",
- "groupCount": 3,
- "login": "admin",
- "name": "Admin Istrator",
- },
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "groupCount": 1,
- "login": "john",
- "name": "John Doe",
- },
- ]
- }
+ <Connect(MembersPageHeader)
+ handleAddMember={[Function]}
+ loading={true}
organization={
Object {
- "actions": Object {
- "admin": true,
- },
"key": "foo",
"name": "Foo",
}
}
- organizationGroups={
- Array [
- Object {
- "default": true,
- "description": "",
- "id": 1,
- "membersCount": 2,
- "name": "Members",
- },
- Object {
- "default": false,
- "description": "",
- "id": 2,
- "membersCount": 0,
- "name": "Watchers",
- },
- ]
- }
- removeMember={[Function]}
- updateMemberGroups={[Function]}
- />
- <ListFooter
- count={2}
- loadMore={[Function]}
- ready={true}
- total={3}
- />
-</div>
-`;
-
-exports[`should fetch members and render for non-admin 1`] = `
-<div
- className="page page-limited"
->
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="organization.members.page"
- />
- <Suggestions
- suggestions="organization_members"
- />
- <MembersPageHeader
- loading={true}
/>
</div>
`;
<Suggestions
suggestions="organization_members"
/>
- <MembersPageHeader
+ <Connect(MembersPageHeader)
+ handleAddMember={[Function]}
loading={false}
+ members={
+ Array [
+ Object {
+ "avatar": "",
+ "groupCount": 3,
+ "login": "admin",
+ "name": "Admin Istrator",
+ },
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "groupCount": 1,
+ "login": "john",
+ "name": "John Doe",
+ },
+ ]
+ }
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
/>
<MembersListHeader
+ currentUser={
+ Object {
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "luke",
+ "name": "Skywalker",
+ "scmAccounts": Array [],
+ }
+ }
handleSearch={[Function]}
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
total={3}
/>
<MembersList
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should allow to switch to automatic mode with bitbucket 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.automatic.bitbucket"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.bitbucket
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.bitbucket
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ <Alert
+ className="big-spacer-top"
+ variant="warning"
+ >
+ organization.members.management.automatic.warning
+ </Alert>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
+
+exports[`should allow to switch to automatic mode with github 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.automatic.github"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.github
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.github
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ <Alert
+ className="big-spacer-top"
+ variant="warning"
+ >
+ organization.members.management.automatic.warning
+ </Alert>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
+
+exports[`should allow to switch to manual mode 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.automatic.github"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.github
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.github
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationNavigationHeader from '../OrganizationNavigationHeader';
+import { mockOrganizationWithAlm } from '../../../../helpers/testMocks';
it('renders', () => {
expect(
expect(
shallow(
<OrganizationNavigationHeader
- organization={{
- alm: { key: 'github', url: 'https://github.com/foo' },
- key: 'foo',
- name: 'Foo',
- projectVisibility: 'public'
- }}
+ organization={mockOrganizationWithAlm({ projectVisibility: 'public' })}
organizations={[]}
/>
)
Object {
"alm": Object {
"key": "github",
+ "membersSync": false,
"url": "https://github.com/foo",
},
"key": "foo",
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import ConfirmModal, { ConfirmModalProps } from './ConfirmModal';
import ModalButton, { ChildrenProps, ModalProps } from './ModalButton';
-import ConfirmModal from './ConfirmModal';
-export { ChildrenProps } from './ModalButton';
-
-interface Props {
+interface Props<T> extends ConfirmModalProps<T> {
children: (props: ChildrenProps) => React.ReactNode;
- cancelButtonText?: string;
- confirmButtonText: string;
- confirmData?: string;
- confirmDisable?: boolean;
- isDestructive?: boolean;
modalBody: React.ReactNode;
modalHeader: string;
- onConfirm: (data?: string) => void | Promise<void>;
}
interface State {
modal: boolean;
}
-export default class ConfirmButton extends React.PureComponent<Props, State> {
+export default class ConfirmButton<T> extends React.PureComponent<Props<T>, State> {
renderConfirmModal = ({ onClose }: ModalProps) => {
+ const { children, modalBody, modalHeader, ...confirmModalProps } = this.props;
return (
- <ConfirmModal
- cancelButtonText={this.props.cancelButtonText}
- confirmButtonText={this.props.confirmButtonText}
- confirmData={this.props.confirmData}
- confirmDisable={this.props.confirmDisable}
- header={this.props.modalHeader}
- isDestructive={this.props.isDestructive}
- onClose={onClose}
- onConfirm={this.props.onConfirm}>
- {this.props.modalBody}
+ <ConfirmModal header={modalHeader} onClose={onClose} {...confirmModalProps}>
+ {modalBody}
</ConfirmModal>
);
};
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { ModalProps } from './Modal';
import SimpleModal, { ChildrenProps } from './SimpleModal';
import DeferredSpinner from '../common/DeferredSpinner';
-import { translate } from '../../helpers/l10n';
import { SubmitButton, ResetButtonLink } from '../ui/buttons';
+import { translate } from '../../helpers/l10n';
-interface Props<T> {
- children: React.ReactNode;
+export interface ConfirmModalProps<T> extends ModalProps {
cancelButtonText?: string;
confirmButtonText: string;
confirmData?: T;
confirmDisable?: boolean;
- header: string;
isDestructive?: boolean;
+ onConfirm: (data?: T) => void | Promise<void | Response>;
+}
+
+interface Props<T> extends ConfirmModalProps<T> {
+ header: string;
onClose: () => void;
- onConfirm: (data?: T) => void | Promise<void>;
}
export default class ConfirmModal<T = string> extends React.PureComponent<Props<T>> {
};
render() {
- const { header } = this.props;
+ const { header, onClose, medium, noBackdrop, large, simple } = this.props;
+ const modalProps = { header, onClose, medium, noBackdrop, large, simple };
return (
- <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
+ <SimpleModal onSubmit={this.handleSubmit} {...modalProps}>
{this.renderModalContent}
</SimpleModal>
);
ReactModal.setAppElement('#content');
-interface OwnProps {
+export interface ModalProps {
+ children: React.ReactNode;
medium?: boolean;
noBackdrop?: boolean;
large?: boolean;
type MandatoryProps = Pick<ReactModal.Props, 'contentLabel'>;
-type Props = Partial<ReactModal.Props> & MandatoryProps & OwnProps;
+type Props = Partial<ReactModal.Props> & MandatoryProps & ModalProps;
export default function Modal(props: Props) {
return (
--- /dev/null
+/*
+ * 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.
+ */
+.radio-card {
+ display: flex;
+ flex-direction: column;
+ width: 450px;
+ min-height: 210px;
+ background-color: #fff;
+ border: solid 1px var(--barBorderColor);
+ border-radius: 3px;
+ box-sizing: border-box;
+ margin-right: calc(2 * var(--gridSize));
+ transition: all 0.2s ease;
+}
+
+.radio-card.animated {
+ height: 0;
+ border-width: 0;
+ overflow: hidden;
+}
+
+.radio-card.animated.open {
+ height: 210px;
+ border-width: 1px;
+}
+
+.radio-card.highlight {
+ box-shadow: var(--defaultShadow);
+}
+
+.radio-card:last-child {
+ margin-right: 0;
+}
+
+.radio-card:focus {
+ outline: none;
+}
+
+.radio-card-actionable {
+ cursor: pointer;
+}
+
+.radio-card-actionable:not(.disabled):hover {
+ box-shadow: var(--defaultShadow);
+ transform: translateY(-2px);
+}
+
+.radio-card-actionable.selected {
+ border-color: var(--darkBlue);
+}
+
+.radio-card-actionable.selected .radio-card-recommended {
+ border: solid 1px var(--darkBlue);
+ border-top: none;
+}
+
+.radio-card-actionable.disabled {
+ cursor: not-allowed;
+ background-color: var(--disableGrayBg);
+ border-color: var(--disableGrayBorder);
+}
+
+.radio-card-actionable.disabled h2,
+.radio-card-actionable.disabled ul {
+ color: var(--disableGrayText);
+}
+
+.radio-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0;
+}
+
+.radio-card-body {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
+}
+
+.radio-card-body .alert {
+ margin-bottom: 0;
+}
+
+.radio-card-recommended {
+ position: relative;
+ padding: 6px calc(var(--gridSize) * 2);
+ left: -1px;
+ bottom: -1px;
+ width: 450px;
+ color: #fff;
+ background-color: var(--blue);
+ border-radius: 0 0 3px 3px;
+ box-sizing: border-box;
+ font-size: var(--smallFontSize);
+}
--- /dev/null
+/*
+ * 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 * as classNames from 'classnames';
+import { FormattedMessage } from 'react-intl';
+import RecommendedIcon from '../icons-components/RecommendedIcon';
+import { translate } from '../../helpers/l10n';
+import './RadioCard.css';
+
+export interface RadioCardProps {
+ className?: string;
+ disabled?: boolean;
+ onClick?: () => void;
+ selected?: boolean;
+}
+
+interface Props extends RadioCardProps {
+ children: React.ReactNode;
+ recommended?: string;
+ title: React.ReactNode;
+ titleInfo?: React.ReactNode;
+}
+
+export default function RadioCard(props: Props) {
+ const { className, disabled, onClick, recommended, selected, titleInfo } = props;
+ const isActionable = Boolean(onClick);
+ return (
+ <div
+ aria-checked={selected}
+ className={classNames(
+ 'radio-card',
+ { 'radio-card-actionable': isActionable, disabled, selected },
+ className
+ )}
+ onClick={isActionable && !disabled ? onClick : undefined}
+ role="radio"
+ tabIndex={0}>
+ <h2 className="radio-card-header big-spacer-bottom">
+ <span className="display-flex-center">
+ {isActionable && (
+ <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
+ )}
+ {props.title}
+ </span>
+ {titleInfo}
+ </h2>
+ <div className="radio-card-body">{props.children}</div>
+ {recommended && (
+ <div className="radio-card-recommended">
+ <RecommendedIcon className="spacer-right" />
+ <FormattedMessage
+ defaultMessage={recommended}
+ id={recommended}
+ values={{ recommended: <strong>{translate('recommended')}</strong> }}
+ />
+ </div>
+ )}
+ </div>
+ );
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Modal from './Modal';
+import Modal, { ModalProps } from './Modal';
export interface ChildrenProps {
onCloseClick: (event?: React.SyntheticEvent<HTMLElement>) => void;
submitting: boolean;
}
-interface Props {
+interface Props extends ModalProps {
children: (props: ChildrenProps) => React.ReactNode;
header: string;
onClose: () => void;
};
render() {
+ const { children, header, onClose, onSubmit, ...modalProps } = this.props;
return (
- <Modal contentLabel={this.props.header} onRequestClose={this.props.onClose}>
- {this.props.children({
+ <Modal contentLabel={header} onRequestClose={onClose} {...modalProps}>
+ {children({
onCloseClick: this.handleCloseClick,
onFormSubmit: this.handleFormSubmit,
onSubmitClick: this.handleSubmitClick,
--- /dev/null
+/*
+ * 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 RadioCard from '../RadioCard';
+import { click } from '../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <RadioCard recommended="Recommended for you" title="Radio Card" titleInfo="info">
+ <div>content</div>
+ </RadioCard>
+ )
+ ).toMatchSnapshot();
+});
+
+it('should be actionable', () => {
+ const onClick = jest.fn();
+ const wrapper = shallow(
+ <RadioCard onClick={onClick} title="Radio Card">
+ <div>content</div>
+ </RadioCard>
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper);
+ wrapper.setProps({ selected: true, titleInfo: 'info' });
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should be actionable 1`] = `
+<div
+ className="radio-card radio-card-actionable"
+ onClick={[MockFunction]}
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right"
+ />
+ Radio Card
+ </span>
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should be actionable 2`] = `
+<div
+ aria-checked={true}
+ className="radio-card radio-card-actionable selected"
+ onClick={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "currentTarget": Object {
+ "blur": [Function],
+ },
+ "preventDefault": [Function],
+ "stopPropagation": [Function],
+ "target": Object {
+ "blur": [Function],
+ },
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right is-checked"
+ />
+ Radio Card
+ </span>
+ info
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="radio-card"
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ Radio Card
+ </span>
+ info
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+ <div
+ className="radio-card-recommended"
+ >
+ <RecommendedIcon
+ className="spacer-right"
+ />
+ <FormattedMessage
+ defaultMessage="Recommended for you"
+ id="Recommended for you"
+ values={
+ Object {
+ "recommended": <strong>
+ recommended
+ </strong>,
+ }
+ }
+ />
+ </div>
+</div>
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-export function getWrappedDisplayName(WrappedComponent: React.ComponentClass, hocName: string) {
+export function getWrappedDisplayName(WrappedComponent: React.ComponentType, hocName: string) {
const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
return `${hocName}(${wrappedDisplayName})`;
}
import { Store, getCurrentUser } from '../../store/rootReducer';
export function withCurrentUser<P>(
- WrappedComponent: React.ComponentClass<P & { currentUser: T.CurrentUser }>
+ WrappedComponent: React.ComponentType<P & { currentUser: T.CurrentUser }>
) {
class Wrapper extends React.Component<P & { currentUser: T.CurrentUser }> {
static displayName = getWrappedDisplayName(WrappedComponent, 'withCurrentUser');
--- /dev/null
+/*
+ * 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.
+ */
+.new-info-box {
+ display: flex;
+ padding: var(--gridSize);
+ background-color: var(--veryLightBlue);
+ border: 1px solid var(--alertBorderInfo);
+ border-radius: 2px;
+}
+
+.new-info-box-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
--- /dev/null
+/*
+ * 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 * as classNames from 'classnames';
+import { ButtonIcon } from './buttons';
+import ClearIcon from '../icons-components/ClearIcon';
+import { sonarcloudBlack500 } from '../../app/theme';
+import { translate } from '../../helpers/l10n';
+import './NewInfoBox.css';
+
+export interface Props {
+ children: React.ReactNode;
+ className?: string;
+ description: React.ReactNode;
+ onClose?: () => void;
+ title: string;
+}
+
+export default function NewInfoBox({ className, children, description, onClose, title }: Props) {
+ return (
+ <div className={classNames('new-info-box', className)} role="alert">
+ <div className="new-info-box-inner text-left">
+ <div className="new-info-box-header spacer-bottom">
+ <span className="display-inline-flex-center">
+ <span className="badge badge-new spacer-right">{translate('new')}</span>
+ <strong>{title}</strong>
+ </span>
+ </div>
+ <p className="note spacer-bottom">{description}</p>
+ {children}
+ </div>
+ <ButtonIcon className="button-small spacer-left" color={sonarcloudBlack500} onClick={onClose}>
+ <ClearIcon size={12} />
+ </ButtonIcon>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * 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 NewInfoBox from '../NewInfoBox';
+import { click } from '../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <NewInfoBox description="My description" onClose={jest.fn()} title="My title">
+ <div />
+ </NewInfoBox>
+ )
+ ).toMatchSnapshot();
+});
+
+it('should allow to opt out', () => {
+ const onClose = jest.fn();
+ const wrapper = shallow(
+ <NewInfoBox description="" onClose={onClose} title="">
+ <div />
+ </NewInfoBox>
+ );
+ click(wrapper.find('ButtonIcon'));
+ expect(onClose).toHaveBeenCalled();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="new-info-box"
+ role="alert"
+>
+ <div
+ className="new-info-box-inner text-left"
+ >
+ <div
+ className="new-info-box-header spacer-bottom"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <span
+ className="badge badge-new spacer-right"
+ >
+ new
+ </span>
+ <strong>
+ My title
+ </strong>
+ </span>
+ </div>
+ <p
+ className="note spacer-bottom"
+ >
+ My description
+ </p>
+ <div />
+ </div>
+ <ButtonIcon
+ className="button-small spacer-left"
+ color="#8a8c8f"
+ onClick={[MockFunction]}
+ >
+ <ClearIcon
+ size={12}
+ />
+ </ButtonIcon>
+</div>
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { isBitbucket, isGithub, isPersonal, isVSTS, sanitizeAlmId } from '../almIntegrations';
+import {
+ isBitbucket,
+ isGithub,
+ isPersonal,
+ isVSTS,
+ sanitizeAlmId,
+ getAlmMembersUrl
+} from '../almIntegrations';
+
+it('#getAlmMembersUrl', () => {
+ expect(
+ getAlmMembersUrl({ key: 'github', membersSync: true, url: 'https://github.com/Foo' })
+ ).toBe('https://github.com/orgs/Foo/people');
+ expect(
+ getAlmMembersUrl({ key: 'bitbucket', membersSync: true, url: 'https://bitbucket.com/Foo/' })
+ ).toBe('https://bitbucket.com/Foo/profile/members');
+});
it('#isBitbucket', () => {
expect(isBitbucket('bitbucket')).toBeTruthy();
*/
import { isLoggedIn } from './users';
+export function getAlmMembersUrl({ key, url }: T.OrganizationAlm): string {
+ if (!url.endsWith('/')) {
+ url += '/';
+ }
+ if (isGithub(key)) {
+ return url.replace('github.com/', 'github.com/orgs/') + 'people';
+ }
+ return url + 'profile/members';
+}
+
export function hasAdvancedALMIntegration(user: T.CurrentUser) {
return (
isLoggedIn(user) && (isBitbucket(user.externalProvider) || isGithub(user.externalProvider))
};
}
-export function mockCurrentUser(overrides: Partial<T.CurrentUser> = {}): T.CurrentUser {
+export function mockCurrentUser(overrides: Partial<T.LoggedInUser> = {}): T.LoggedInUser {
return {
+ groups: [],
isLoggedIn: true,
+ login: 'luke',
+ name: 'Skywalker',
+ scmAccounts: [],
...overrides
};
}
}
export function mockOrganization(overrides: Partial<T.Organization> = {}): T.Organization {
- return {
- key: 'foo',
- name: 'Foo',
+ return { key: 'foo', name: 'Foo', ...overrides };
+}
+
+export function mockOrganizationWithAdminActions(
+ overrides: Partial<T.Organization> = {},
+ actionsOverrides: Partial<T.Organization['actions']> = {}
+) {
+ return mockOrganization({ actions: { admin: true, ...actionsOverrides }, ...overrides });
+}
+
+export function mockOrganizationWithAlm(
+ overrides: Partial<T.Organization> = {},
+ almOverrides: Partial<T.Organization['alm']> = {}
+): T.Organization {
+ return mockOrganization({
+ alm: { key: 'github', membersSync: false, url: 'https://github.com/foo', ...almOverrides },
...overrides
- };
+ });
}
export function mockQualityProfile(overrides: Partial<Profile> = {}): Profile {
ascending=Ascending
assignee=Assignee
author=Author
+bitbucket=Bitbucket
back=Back
backup=Backup
backup_verb=Back up
format=Format
from=From
global=Global
+github=GitHub
help=Help
hide=Hide
inactive=Inactive
organization.members.members_groups={0}'s groups:
organization.members.manage_a_team=Manage a team
organization.members.add_to_members=Add to members
+organization.members.config_synchro=Configure Synchronization
+organization.members.auto_sync_with_x=Automatic sync with {0}
+organization.members.auto_sync_members_from_org.bitbucket=Members can be synchronized automatically from your Bitbucket team
+organization.members.auto_sync_members_from_org.github=Members can be synchronized automatically from your GitHub organization
+organization.members.auto_sync_total_help.bitbucket=You might not see all members from your Bitbucket team yet, as they need to reconnect to SonarCloud to be members of the organization.
+organization.members.auto_sync_total_help.github=You might not see all members from your GitHub organization yet, as they need to connect to SonarCloud at least once to appear in this list.
+organization.members.see_all_members_on_x=See all members on {0}
+organization.members.management.title=Members Management
+organization.members.management.description=Select your management mode for members of this organization
+organization.members.management.manual=Manual
+organization.members.management.manual.add_members_manually=Admin add members manually from Sonarcloud existing users
+organization.members.management.manual.choose_members_permissions=Admin chooses each member permissions
+organization.members.management.automatic=Automatic sync with {0}
+organization.members.management.automatic.synchronized_from.bitbucket=Members are synchronized automatically from your Bitbucket team
+organization.members.management.automatic.synchronized_from.github=Members are synchronized automatically from your GitHub organization
+organization.members.management.automatic.members_changes_reflected.bitbucket=Your team members must reconnect to SonarCloud to be automatically added to correct SonarCloud organization
+organization.members.management.automatic.members_changes_reflected.github=If you add or remove a member on GitHub, SonarCloud immediately reflect the changes
+organization.members.management.automatic.still_choose_members_permissions=Admin still manages permissions for each member in SonarCloud
+organization.members.management.automatic.warning=This will override your current Members and Permissions configuration
organization.paid_plan.badge=Paid plan
organization.default_visibility_of_new_projects=Default visibility of new projects:
organization.change_visibility_form.header=Set Default Visibility of New Projects
onboarding.import_organization.choose_organization=Choose an organization...
onboarding.import_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket
onboarding.import_organization.choose_organization_button.github=Choose an organization on GitHub
-onboarding.import_organization.installing=Finalize installation of the ALM application...
-onboarding.import_organization.installing.bitbucket=Finalize installation of the Bitbucket application..
-onboarding.import_organization.installing.github=Finalize installation of the GitHub application...
+onboarding.import_organization.installing=Finalize installation of the {0} application...
onboarding.import_organization.personal.page.header=Bind to your personal organization
onboarding.import_organization.personal.import_org_details=Import personal organization details
onboarding.import_organization.private.disabled=Selecting private repository is not available yet and will come soon. Meanwhile, you need to create the project manually.
-onboarding.import_organization.bitbucket=Import from BitBucket teams
+onboarding.import_organization.bitbucket=Import from Bitbucket teams
onboarding.import_organization.github=Import from GitHub organizations
onboarding.import_organization.bind_existing=Bind to an existing SonarCloud organization
onboarding.import_organization.create_new=Create new SonarCloud organization from it