@@ -114,7 +114,7 @@ interface FeatureProps { | |||
export function Feature({ feature }: FeatureProps) { | |||
return ( | |||
<div className="feature"> | |||
<ul className="categories"> | |||
<ul className="categories spacer-bottom"> | |||
{feature.categories.map(category => ( | |||
<li key={category.name} style={{ backgroundColor: category.color }}> | |||
{category.name} |
@@ -5,7 +5,7 @@ exports[`#Feature should render correctly 1`] = ` | |||
className="feature" | |||
> | |||
<ul | |||
className="categories" | |||
className="categories spacer-bottom" | |||
> | |||
<li | |||
key="BitBucket" | |||
@@ -37,7 +37,7 @@ exports[`#Feature should render correctly 2`] = ` | |||
className="feature" | |||
> | |||
<ul | |||
className="categories" | |||
className="categories spacer-bottom" | |||
> | |||
<li | |||
key="Java" |
@@ -19,7 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import NotificationsList from './NotificationsList'; | |||
import SonarCloudNotifications from './SonarCloudNotifications'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
interface Props { | |||
addNotification: (n: T.Notification) => void; | |||
@@ -31,33 +33,36 @@ interface Props { | |||
export default function GlobalNotifications(props: Props) { | |||
return ( | |||
<section className="boxed-group"> | |||
<h2>{translate('my_profile.overall_notifications.title')}</h2> | |||
<> | |||
<section className="boxed-group"> | |||
<h2>{translate('my_profile.overall_notifications.title')}</h2> | |||
<div className="boxed-group-inner"> | |||
<table className="form"> | |||
<thead> | |||
<tr> | |||
<th /> | |||
{props.channels.map(channel => ( | |||
<th className="text-center" key={channel}> | |||
<h4>{translate('notification.channel', channel)}</h4> | |||
</th> | |||
))} | |||
</tr> | |||
</thead> | |||
<div className="boxed-group-inner"> | |||
<table className="form"> | |||
<thead> | |||
<tr> | |||
<th /> | |||
{props.channels.map(channel => ( | |||
<th className="text-center" key={channel}> | |||
<h4>{translate('notification.channel', channel)}</h4> | |||
</th> | |||
))} | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={props.channels} | |||
checkboxId={getCheckboxId} | |||
notifications={props.notifications} | |||
onAdd={props.addNotification} | |||
onRemove={props.removeNotification} | |||
types={props.types} | |||
/> | |||
</table> | |||
</div> | |||
</section> | |||
<NotificationsList | |||
channels={props.channels} | |||
checkboxId={getCheckboxId} | |||
notifications={props.notifications} | |||
onAdd={props.addNotification} | |||
onRemove={props.removeNotification} | |||
types={props.types} | |||
/> | |||
</table> | |||
</div> | |||
</section> | |||
{isSonarCloud() && <SonarCloudNotifications />} | |||
</> | |||
); | |||
} | |||
@@ -0,0 +1,87 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 Checkbox from '../../../components/controls/Checkbox'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getCurrentUserSetting, Store } from '../../../store/rootReducer'; | |||
import { setCurrentUserSetting } from '../../../store/users'; | |||
interface Props { | |||
notificationsOptOut?: boolean; | |||
setCurrentUserSetting: (setting: T.CurrentUserSetting) => void; | |||
} | |||
export class SonarCloudNotifications extends React.PureComponent<Props> { | |||
handleCheckOptOut = (checked: boolean) => { | |||
this.props.setCurrentUserSetting({ | |||
key: 'notifications.optOut', | |||
value: checked ? 'false' : 'true' | |||
}); | |||
}; | |||
render() { | |||
return ( | |||
<section className="boxed-group"> | |||
<h2>{translate('my_profile.sonarcloud_feature_notifications.title')}</h2> | |||
<div className="boxed-group-inner"> | |||
<table className="form"> | |||
<thead> | |||
<tr> | |||
<th /> | |||
<th className="text-center"> | |||
<h4>{translate('activate')}</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr> | |||
<td>{translate('my_profile.sonarcloud_feature_notifications.description')}</td> | |||
<td className="text-center"> | |||
<Checkbox | |||
checked={!this.props.notificationsOptOut} | |||
onCheck={this.handleCheckOptOut} | |||
/> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
</section> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state: Store) => { | |||
const notificationsOptOut = getCurrentUserSetting(state, 'notifications.optOut') === 'true'; | |||
return { | |||
notificationsOptOut | |||
}; | |||
}; | |||
const mapDispatchToProps = { | |||
setCurrentUserSetting | |||
}; | |||
export default connect( | |||
mapStateToProps, | |||
mapDispatchToProps | |||
)(SonarCloudNotifications); |
@@ -20,8 +20,20 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import GlobalNotifications from '../GlobalNotifications'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); | |||
it('should match snapshot', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should show SonarCloud options if in SC context', () => { | |||
(isSonarCloud as jest.Mock).mockImplementation(() => true); | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props = {}) { | |||
const channels = ['channel1', 'channel2']; | |||
const types = ['type1', 'type2']; | |||
const notifications = [ | |||
@@ -30,15 +42,14 @@ it('should match snapshot', () => { | |||
{ channel: 'channel2', type: 'type2' } | |||
]; | |||
expect( | |||
shallow( | |||
<GlobalNotifications | |||
addNotification={jest.fn()} | |||
channels={channels} | |||
notifications={notifications} | |||
removeNotification={jest.fn()} | |||
types={types} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
return shallow( | |||
<GlobalNotifications | |||
addNotification={jest.fn()} | |||
channels={channels} | |||
notifications={notifications} | |||
removeNotification={jest.fn()} | |||
types={types} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,36 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 { SonarCloudNotifications } from '../SonarCloudNotifications'; | |||
it('should match snapshot', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<SonarCloudNotifications['props']> = {}) { | |||
return shallow( | |||
<SonarCloudNotifications | |||
notificationsOptOut={true} | |||
setCurrentUserSetting={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,73 +1,150 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should match snapshot 1`] = ` | |||
<section | |||
className="boxed-group" | |||
> | |||
<h2> | |||
my_profile.overall_notifications.title | |||
</h2> | |||
<div | |||
className="boxed-group-inner" | |||
<Fragment> | |||
<section | |||
className="boxed-group" | |||
> | |||
<table | |||
className="form" | |||
<h2> | |||
my_profile.overall_notifications.title | |||
</h2> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<thead> | |||
<tr> | |||
<th /> | |||
<th | |||
className="text-center" | |||
key="channel1" | |||
> | |||
<h4> | |||
notification.channel.channel1 | |||
</h4> | |||
</th> | |||
<th | |||
className="text-center" | |||
key="channel2" | |||
> | |||
<h4> | |||
notification.channel.channel2 | |||
</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={ | |||
Array [ | |||
"channel1", | |||
"channel2", | |||
] | |||
} | |||
checkboxId={[Function]} | |||
notifications={ | |||
Array [ | |||
Object { | |||
"channel": "channel1", | |||
"type": "type1", | |||
}, | |||
Object { | |||
"channel": "channel1", | |||
"type": "type2", | |||
}, | |||
Object { | |||
"channel": "channel2", | |||
"type": "type2", | |||
}, | |||
] | |||
} | |||
onAdd={[MockFunction]} | |||
onRemove={[MockFunction]} | |||
types={ | |||
Array [ | |||
"type1", | |||
"type2", | |||
] | |||
} | |||
/> | |||
</table> | |||
</div> | |||
</section> | |||
<table | |||
className="form" | |||
> | |||
<thead> | |||
<tr> | |||
<th /> | |||
<th | |||
className="text-center" | |||
key="channel1" | |||
> | |||
<h4> | |||
notification.channel.channel1 | |||
</h4> | |||
</th> | |||
<th | |||
className="text-center" | |||
key="channel2" | |||
> | |||
<h4> | |||
notification.channel.channel2 | |||
</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={ | |||
Array [ | |||
"channel1", | |||
"channel2", | |||
] | |||
} | |||
checkboxId={[Function]} | |||
notifications={ | |||
Array [ | |||
Object { | |||
"channel": "channel1", | |||
"type": "type1", | |||
}, | |||
Object { | |||
"channel": "channel1", | |||
"type": "type2", | |||
}, | |||
Object { | |||
"channel": "channel2", | |||
"type": "type2", | |||
}, | |||
] | |||
} | |||
onAdd={[MockFunction]} | |||
onRemove={[MockFunction]} | |||
types={ | |||
Array [ | |||
"type1", | |||
"type2", | |||
] | |||
} | |||
/> | |||
</table> | |||
</div> | |||
</section> | |||
</Fragment> | |||
`; | |||
exports[`should show SonarCloud options if in SC context 1`] = ` | |||
<Fragment> | |||
<section | |||
className="boxed-group" | |||
> | |||
<h2> | |||
my_profile.overall_notifications.title | |||
</h2> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<table | |||
className="form" | |||
> | |||
<thead> | |||
<tr> | |||
<th /> | |||
<th | |||
className="text-center" | |||
key="channel1" | |||
> | |||
<h4> | |||
notification.channel.channel1 | |||
</h4> | |||
</th> | |||
<th | |||
className="text-center" | |||
key="channel2" | |||
> | |||
<h4> | |||
notification.channel.channel2 | |||
</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<NotificationsList | |||
channels={ | |||
Array [ | |||
"channel1", | |||
"channel2", | |||
] | |||
} | |||
checkboxId={[Function]} | |||
notifications={ | |||
Array [ | |||
Object { | |||
"channel": "channel1", | |||
"type": "type1", | |||
}, | |||
Object { | |||
"channel": "channel1", | |||
"type": "type2", | |||
}, | |||
Object { | |||
"channel": "channel2", | |||
"type": "type2", | |||
}, | |||
] | |||
} | |||
onAdd={[MockFunction]} | |||
onRemove={[MockFunction]} | |||
types={ | |||
Array [ | |||
"type1", | |||
"type2", | |||
] | |||
} | |||
/> | |||
</table> | |||
</div> | |||
</section> | |||
<Connect(SonarCloudNotifications) /> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,47 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should match snapshot 1`] = ` | |||
<section | |||
className="boxed-group" | |||
> | |||
<h2> | |||
my_profile.sonarcloud_feature_notifications.title | |||
</h2> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<table | |||
className="form" | |||
> | |||
<thead> | |||
<tr> | |||
<th /> | |||
<th | |||
className="text-center" | |||
> | |||
<h4> | |||
activate | |||
</h4> | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr> | |||
<td> | |||
my_profile.sonarcloud_feature_notifications.description | |||
</td> | |||
<td | |||
className="text-center" | |||
> | |||
<Checkbox | |||
checked={false} | |||
onCheck={[Function]} | |||
thirdState={false} | |||
/> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
</section> | |||
`; |
@@ -7,6 +7,7 @@ | |||
action=Action | |||
actions=Actions | |||
active=Active | |||
activate=Activate | |||
add_verb=Add | |||
admin=Admin | |||
apply=Apply | |||
@@ -1500,6 +1501,8 @@ my_profile.password.submit=Change password | |||
my_profile.password.changed=The password has been changed! | |||
my_profile.notifications.submit=Save changes | |||
my_profile.overall_notifications.title=Overall notifications | |||
my_profile.sonarcloud_feature_notifications.title=SonarCloud new feature notifications | |||
my_profile.sonarcloud_feature_notifications.description=Display a notification in the header when new features are deployed | |||
my_profile.per_project_notifications.title=Notifications per project | |||
my_account.page=My Account |