aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-05-15 11:09:13 +0200
committerSonarTech <sonartech@sonarsource.com>2018-05-15 20:20:50 +0200
commit34db3b05cf4dcdb11faa5b392837cc4d979d19ff (patch)
treed0532c404f09982e0c38405c4d0fd72804b76cb7
parent254acb86a8bf05070940bbb94594bdf1c09707ee (diff)
downloadsonarqube-34db3b05cf4dcdb11faa5b392837cc4d979d19ff.tar.gz
sonarqube-34db3b05cf4dcdb11faa5b392837cc4d979d19ff.zip
rewrite notifications app in ts and drop from redux store (#233)
-rw-r--r--server/sonar-web/src/main/js/api/components.ts2
-rw-r--r--server/sonar-web/src/main/js/api/notifications.ts42
-rw-r--r--server/sonar-web/src/main/js/app/types.ts8
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js)56
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/Notifications.js55
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx179
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.js)24
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js)38
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js)76
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/Projects.js)130
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.js)12
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx108
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.js)26
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.tsx (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.js)36
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.tsx134
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.js.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.js.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.tsx.snap90
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.js.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap)4
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.js.snap196
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.tsx.snap375
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/actions.js60
-rw-r--r--server/sonar-web/src/main/js/apps/account/notifications/types.ts (renamed from server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.js)12
-rw-r--r--server/sonar-web/src/main/js/apps/account/routes.ts2
-rw-r--r--server/sonar-web/src/main/js/components/controls/Select.tsx2
-rw-r--r--server/sonar-web/src/main/js/store/notifications/duck.js204
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js18
28 files changed, 1083 insertions, 826 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index 4505e8e9282..78e2b72d96a 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -239,7 +239,7 @@ export function getSuggestions(
if (more) {
data.more = more;
}
- return getJSON('/api/components/suggestions', data);
+ return getJSON('/api/components/suggestions', data).catch(throwGlobalError);
}
export function getComponentForSourceViewer(
diff --git a/server/sonar-web/src/main/js/api/notifications.ts b/server/sonar-web/src/main/js/api/notifications.ts
index db846af96fc..d1f0db0e09c 100644
--- a/server/sonar-web/src/main/js/api/notifications.ts
+++ b/server/sonar-web/src/main/js/api/notifications.ts
@@ -17,37 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON, post, RequestData } from '../helpers/request';
+import { Notification } from '../app/types';
+import throwGlobalError from '../app/utils/throwGlobalError';
+import { getJSON, post } from '../helpers/request';
-export interface GetNotificationsResponse {
- notifications: Array<{
- channel: string;
- type: string;
- organization?: string;
- project?: string;
- projectName?: string;
- }>;
- channels: Array<string>;
- globalTypes: Array<string>;
- perProjectTypes: Array<string>;
+export function getNotifications(): Promise<{
+ channels: string[];
+ globalTypes: string[];
+ notifications: Notification[];
+ perProjectTypes: string[];
+}> {
+ return getJSON('/api/notifications/list').catch(throwGlobalError);
}
-export function getNotifications(): Promise<GetNotificationsResponse> {
- return getJSON('/api/notifications/list');
+export function addNotification(data: { channel: string; type: string; project?: string }) {
+ return post('/api/notifications/add', data).catch(throwGlobalError);
}
-export function addNotification(channel: string, type: string, project?: string): Promise<void> {
- const data: RequestData = { channel, type };
- if (project) {
- Object.assign(data, { project });
- }
- return post('/api/notifications/add', data);
-}
-
-export function removeNotification(channel: string, type: string, project?: string): Promise<void> {
- const data: RequestData = { channel, type };
- if (project) {
- Object.assign(data, { project });
- }
- return post('/api/notifications/remove', data);
+export function removeNotification(data: { channel: string; type: string; project?: string }) {
+ return post('/api/notifications/remove', data).catch(throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index ffaf58534f1..8fa11ed7fb1 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -312,6 +312,14 @@ export interface Metric {
type: string;
}
+export interface Notification {
+ channel: string;
+ organization?: string;
+ project?: string;
+ projectName?: string;
+ type: string;
+}
+
export interface Organization {
adminPages?: { key: string; name: string }[];
avatar?: string;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx
index 21b7c28f9d1..0ac75ce0151 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx
@@ -17,34 +17,20 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { connect } from 'react-redux';
+import * as React from 'react';
import NotificationsList from './NotificationsList';
-import { addNotification, removeNotification } from './actions';
+import { Notification } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
-import {
- getGlobalNotifications,
- getNotificationChannels,
- getNotificationGlobalTypes
-} from '../../../store/rootReducer';
-/*:: import type {
- Notification,
- NotificationsState,
- ChannelsState,
- TypesState
-} from '../../../store/notifications/duck'; */
-/*::
-type Props = {
- notifications: NotificationsState,
- channels: ChannelsState,
- types: TypesState,
- addNotification: (n: Notification) => void,
- removeNotification: (n: Notification) => void
-};
-*/
+interface Props {
+ addNotification: (n: Notification) => void;
+ channels: string[];
+ notifications: Notification[];
+ removeNotification: (n: Notification) => void;
+ types: string[];
+}
-function GlobalNotifications(props /*: Props */) {
+export default function GlobalNotifications(props: Props) {
return (
<section className="boxed-group">
<h2>{translate('my_profile.overall_notifications.title')}</h2>
@@ -55,7 +41,7 @@ function GlobalNotifications(props /*: Props */) {
<tr>
<th />
{props.channels.map(channel => (
- <th key={channel} className="text-center">
+ <th className="text-center" key={channel}>
<h4>{translate('notification.channel', channel)}</h4>
</th>
))}
@@ -63,12 +49,12 @@ function GlobalNotifications(props /*: Props */) {
</thead>
<NotificationsList
- notifications={props.notifications}
channels={props.channels}
- types={props.types}
- checkboxId={(d, c) => `global-notification-${d}-${c}`}
+ checkboxId={getCheckboxId}
+ notifications={props.notifications}
onAdd={props.addNotification}
onRemove={props.removeNotification}
+ types={props.types}
/>
</table>
</div>
@@ -76,14 +62,6 @@ function GlobalNotifications(props /*: Props */) {
);
}
-const mapStateToProps = state => ({
- notifications: getGlobalNotifications(state),
- channels: getNotificationChannels(state),
- types: getNotificationGlobalTypes(state)
-});
-
-const mapDispatchToProps = { addNotification, removeNotification };
-
-export default connect(mapStateToProps, mapDispatchToProps)(GlobalNotifications);
-
-export const UnconnectedGlobalNotifications = GlobalNotifications;
+function getCheckboxId(type: string, channel: string) {
+ return `global-notification-${type}-${channel}`;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.js b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.js
deleted file mode 100644
index 56148702a1d..00000000000
--- a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import GlobalNotifications from './GlobalNotifications';
-import Projects from './Projects';
-import { fetchNotifications } from './actions';
-import { translate } from '../../../helpers/l10n';
-
-class Notifications extends React.PureComponent {
- /*:: props: {
- fetchNotifications: () => void
- };
-*/
-
- componentDidMount() {
- this.props.fetchNotifications();
- }
-
- render() {
- return (
- <div className="account-body account-container">
- <Helmet title={translate('my_account.notifications')} />
- <p className="alert alert-info">{translate('notification.dispatcher.information')}</p>
- <GlobalNotifications />
- <Projects />
- </div>
- );
- }
-}
-
-const mapDispatchToProps = { fetchNotifications };
-
-export default connect(null, mapDispatchToProps)(Notifications);
-
-export const UnconnectedNotifications = Notifications;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx
new file mode 100644
index 00000000000..a6c2c76b631
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx
@@ -0,0 +1,179 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import { groupBy, partition, uniq, uniqBy, uniqWith } from 'lodash';
+import * as PropTypes from 'prop-types';
+import GlobalNotifications from './GlobalNotifications';
+import Projects from './Projects';
+import { NotificationProject } from './types';
+import * as api from '../../../api/notifications';
+import { Notification } from '../../../app/types';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate } from '../../../helpers/l10n';
+
+export interface Props {
+ fetchOrganizations: (organizations: string[]) => void;
+}
+
+interface State {
+ channels: string[];
+ globalTypes: string[];
+ loading: boolean;
+ notifications: Notification[];
+ perProjectTypes: string[];
+}
+
+export default class Notifications extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ static contextTypes = {
+ organizationsEnabled: PropTypes.bool
+ };
+
+ state: State = {
+ channels: [],
+ globalTypes: [],
+ loading: true,
+ notifications: [],
+ perProjectTypes: []
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchNotifications();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchNotifications = () => {
+ api.getNotifications().then(
+ response => {
+ if (this.mounted) {
+ if (this.context.organizationsEnabled) {
+ const organizations = uniq(response.notifications
+ .filter(n => n.organization)
+ .map(n => n.organization) as string[]);
+ this.props.fetchOrganizations(organizations);
+ }
+
+ this.setState({
+ channels: response.channels,
+ globalTypes: response.globalTypes,
+ loading: false,
+ notifications: response.notifications,
+ perProjectTypes: response.perProjectTypes
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ addNotificationToState = (added: Notification) => {
+ this.setState(state => ({
+ notifications: uniqWith([...state.notifications, added], areNotificationsEqual)
+ }));
+ };
+
+ removeNotificationFromState = (removed: Notification) => {
+ this.setState(state => ({
+ notifications: state.notifications.filter(
+ notification => !areNotificationsEqual(notification, removed)
+ )
+ }));
+ };
+
+ addNotification = (added: Notification) => {
+ // optimistic update
+ this.addNotificationToState(added);
+
+ // recreate `data` to omit `projectName` and `organization` from `Notification`
+ const data = { channel: added.channel, project: added.project, type: added.type };
+ api.addNotification(data).catch(() => {
+ this.removeNotificationFromState(added);
+ });
+ };
+
+ removeNotification = (removed: Notification) => {
+ // optimistic update
+ this.removeNotificationFromState(removed);
+
+ // recreate `data` to omit `projectName` and `organization` from `Notification`
+ const data = { channel: removed.channel, project: removed.project, type: removed.type };
+ api.removeNotification(data).catch(() => {
+ this.addNotificationToState(removed);
+ });
+ };
+
+ render() {
+ const [globalNotifications, projectNotifications] = partition(
+ this.state.notifications,
+ n => !n.project
+ );
+ const projects = uniqBy(
+ projectNotifications.map(n => ({
+ key: n.project,
+ name: n.projectName,
+ organization: n.organization
+ })) as NotificationProject[],
+ project => project.key
+ );
+ const notificationsByProject = groupBy(projectNotifications, n => n.project);
+
+ return (
+ <div className="account-body account-container">
+ <Helmet title={translate('my_account.notifications')} />
+ <p className="alert alert-info">{translate('notification.dispatcher.information')}</p>
+ <DeferredSpinner loading={this.state.loading}>
+ {this.state.notifications && (
+ <>
+ <GlobalNotifications
+ addNotification={this.addNotification}
+ channels={this.state.channels}
+ notifications={globalNotifications}
+ removeNotification={this.removeNotification}
+ types={this.state.globalTypes}
+ />
+ <Projects
+ addNotification={this.addNotification}
+ channels={this.state.channels}
+ notificationsByProject={notificationsByProject}
+ projects={projects}
+ removeNotification={this.removeNotification}
+ types={this.state.perProjectTypes}
+ />
+ </>
+ )}
+ </DeferredSpinner>
+ </div>
+ );
+ }
+}
+
+function areNotificationsEqual(a: Notification, b: Notification) {
+ return a.channel === b.channel && a.type === b.type && a.project === b.project;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.js b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.tsx
index 5f6598b5950..7c05e0f1616 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.tsx
@@ -17,24 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { shallow } from 'enzyme';
-import { UnconnectedProjects } from '../Projects';
+import { connect } from 'react-redux';
+import Notifications, { Props } from './Notifications';
+import { fetchOrganizations } from '../../../store/rootActions';
-const projects = [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }];
+const mapDispatchToProps = { fetchOrganizations } as Pick<Props, 'fetchOrganizations'>;
-const newProject = { key: 'qux', name: 'Qux' };
-
-it('should render projects', () => {
- const wrapper = shallow(<UnconnectedProjects projects={projects} />);
- expect(wrapper).toMatchSnapshot();
-
- // let's add a new project
- wrapper.setState({ addedProjects: [newProject] });
- expect(wrapper).toMatchSnapshot();
-
- // let's say we saved it, so it's passed back in `props`
- wrapper.setProps({ projects: [...projects, newProject] });
- expect(wrapper).toMatchSnapshot();
- expect(wrapper.state()).toMatchSnapshot();
-});
+export default connect(null, mapDispatchToProps)(Notifications);
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx
index 6ca7f02bdfe..35915a0f4f1 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx
@@ -17,35 +17,29 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import * as React from 'react';
+import { Notification } from '../../../app/types';
import Checkbox from '../../../components/controls/Checkbox';
import { translate, hasMessage } from '../../../helpers/l10n';
-/*:: import type {
- Notification,
- NotificationsState,
- ChannelsState,
- TypesState
-} from '../../../store/notifications/duck'; */
-export default class NotificationsList extends React.PureComponent {
- /*:: props: {
- onAdd: (n: Notification) => void,
- onRemove: (n: Notification) => void,
- channels: ChannelsState,
- checkboxId: (string, string) => string,
- project?: boolean,
- types: TypesState,
- notifications: NotificationsState
- };
-*/
+interface Props {
+ onAdd: (n: Notification) => void;
+ onRemove: (n: Notification) => void;
+ channels: string[];
+ checkboxId: (type: string, channel: string) => string;
+ project?: boolean;
+ types: string[];
+ notifications: Notification[];
+}
- isEnabled(type /*: string */, channel /*: string */) /*: boolean */ {
+export default class NotificationsList extends React.PureComponent<Props> {
+ isEnabled(type: string, channel: string) {
return !!this.props.notifications.find(
notification => notification.type === type && notification.channel === channel
);
}
- handleCheck(type /*: string */, channel /*: string */, checked /*: boolean */) {
+ handleCheck(type: string, channel: string, checked: boolean) {
if (checked) {
this.props.onAdd({ type, channel });
} else {
@@ -53,7 +47,7 @@ export default class NotificationsList extends React.PureComponent {
}
}
- getDispatcherLabel(dispatcher /*: string */) {
+ getDispatcherLabel(dispatcher: string) {
const globalMessageKey = ['notification.dispatcher', dispatcher];
const projectMessageKey = [...globalMessageKey, 'project'];
const shouldUseProjectMessage = this.props.project && hasMessage(...projectMessageKey);
@@ -71,7 +65,7 @@ export default class NotificationsList extends React.PureComponent {
<tr key={type}>
<td>{this.getDispatcherLabel(type)}</td>
{channels.map(channel => (
- <td key={channel} className="text-center">
+ <td className="text-center" key={channel}>
<Checkbox
checked={this.isEnabled(type, channel)}
id={checkboxId(type, channel)}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx
index 8fbf66c2d4c..f5e29f76ba1 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx
@@ -17,42 +17,30 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { connect } from 'react-redux';
+import * as React from 'react';
import { Link } from 'react-router';
import NotificationsList from './NotificationsList';
-import { addNotification, removeNotification } from './actions';
+import { NotificationProject } from './types';
+import { Notification } from '../../../app/types';
import Organization from '../../../components/shared/Organization';
import { translate } from '../../../helpers/l10n';
-import {
- getProjectNotifications,
- getNotificationChannels,
- getNotificationPerProjectTypes
-} from '../../../store/rootReducer';
-/*:: import type {
- Notification,
- NotificationsState,
- ChannelsState,
- TypesState
-} from '../../../store/notifications/duck'; */
import { getProjectUrl } from '../../../helpers/urls';
-class ProjectNotifications extends React.PureComponent {
- /*:: props: {
- project: {
- key: string,
- name: string,
- organization: string
- },
- notifications: NotificationsState,
- channels: ChannelsState,
- types: TypesState,
- addNotification: (n: Notification) => void,
- removeNotification: (n: Notification) => void
+interface Props {
+ addNotification: (n: Notification) => void;
+ channels: string[];
+ notifications: Notification[];
+ project: NotificationProject;
+ removeNotification: (n: Notification) => void;
+ types: string[];
+}
+
+export default class ProjectNotifications extends React.PureComponent<Props> {
+ getCheckboxId = (type: string, channel: string) => {
+ return `project-notification-${this.props.project.key}-${type}-${channel}`;
};
-*/
- handleAddNotification({ channel, type }) {
+ handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
this.props.addNotification({
channel,
type,
@@ -60,21 +48,21 @@ class ProjectNotifications extends React.PureComponent {
projectName: this.props.project.name,
organization: this.props.project.organization
});
- }
+ };
- handleRemoveNotification({ channel, type }) {
+ handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
this.props.removeNotification({
channel,
type,
project: this.props.project.key
});
- }
+ };
render() {
const { project, channels } = this.props;
return (
- <table key={project.key} className="form big-spacer-bottom">
+ <table className="form big-spacer-bottom" key={project.key}>
<thead>
<tr>
<th>
@@ -86,34 +74,22 @@ class ProjectNotifications extends React.PureComponent {
</h4>
</th>
{channels.map(channel => (
- <th key={channel} className="text-center">
+ <th className="text-center" key={channel}>
<h4>{translate('notification.channel', channel)}</h4>
</th>
))}
</tr>
</thead>
<NotificationsList
- notifications={this.props.notifications}
channels={this.props.channels}
- types={this.props.types}
- checkboxId={(d, c) => `project-notification-${project.key}-${d}-${c}`}
- onAdd={n => this.handleAddNotification(n)}
- onRemove={n => this.handleRemoveNotification(n)}
+ checkboxId={this.getCheckboxId}
+ notifications={this.props.notifications}
+ onAdd={this.handleAddNotification}
+ onRemove={this.handleRemoveNotification}
project={true}
+ types={this.props.types}
/>
</table>
);
}
}
-
-const mapStateToProps = (state, ownProps) => ({
- notifications: getProjectNotifications(state, ownProps.project.key),
- channels: getNotificationChannels(state),
- types: getNotificationPerProjectTypes(state)
-});
-
-const mapDispatchToProps = { addNotification, removeNotification };
-
-export default connect(mapStateToProps, mapDispatchToProps)(ProjectNotifications);
-
-export const UnconnectedProjectNotifications = ProjectNotifications;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Projects.js b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx
index 5799f59f7d2..77f7876bb7d 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/Projects.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx
@@ -17,93 +17,87 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { connect } from 'react-redux';
-import { differenceBy } from 'lodash';
+import * as React from 'react';
+import { differenceWith } from 'lodash';
import ProjectNotifications from './ProjectNotifications';
+import { NotificationProject } from './types';
+import { getSuggestions } from '../../../api/components';
+import { Notification } from '../../../app/types';
import { AsyncSelect } from '../../../components/controls/Select';
import Organization from '../../../components/shared/Organization';
import { translate } from '../../../helpers/l10n';
-import { getSuggestions } from '../../../api/components';
-import { getProjectsWithNotifications } from '../../../store/rootReducer';
-
-/*::
-type Props = {
- projects: Array<{
- key: string,
- name: string
- }>
-};
-*/
-/*::
-type State = {
- addedProjects: Array<{
- key: string,
- name: string
- }>
-};
-*/
+export interface Props {
+ addNotification: (n: Notification) => void;
+ channels: string[];
+ notificationsByProject: { [project: string]: Notification[] };
+ projects: NotificationProject[];
+ removeNotification: (n: Notification) => void;
+ types: string[];
+}
-class Projects extends React.PureComponent {
- /*:: props: Props; */
+interface State {
+ addedProjects: NotificationProject[];
+}
- state /*: State */ = {
- addedProjects: []
- };
+export default class Projects extends React.PureComponent<Props, State> {
+ state: State = { addedProjects: [] };
- componentWillReceiveProps(nextProps /*: Props */) {
+ componentWillReceiveProps(nextProps: Props) {
// remove all projects from `this.state.addedProjects`
// that already exist in `nextProps.projects`
- const nextAddedProjects = differenceBy(
- this.state.addedProjects,
- nextProps.projects,
- project => project.key
- );
-
- if (nextAddedProjects.length !== this.state.addedProjects) {
- this.setState({ addedProjects: nextAddedProjects });
- }
+ this.setState(state => ({
+ addedProjects: differenceWith(
+ state.addedProjects,
+ Object.keys(nextProps.projects),
+ (stateProject, propsProjectKey) => stateProject.key !== propsProjectKey
+ )
+ }));
}
- loadOptions = (query, cb) => {
+ loadOptions = (query: string) => {
if (query.length < 2) {
- cb(null, { options: [] });
- return;
+ return Promise.resolve({ options: [] });
}
- getSuggestions(query)
+ return getSuggestions(query)
.then(r => {
const projects = r.results.find(domain => domain.q === 'TRK');
return projects ? projects.items : [];
})
- .then(projects =>
- projects.map(project => ({
- value: project.key,
- label: project.name,
- organization: project.organization
- }))
- )
+ .then(projects => {
+ return projects
+ .filter(
+ project =>
+ !this.props.projects.find(p => p.key === project.key) &&
+ !this.state.addedProjects.find(p => p.key === project.key)
+ )
+ .map(project => ({
+ value: project.key,
+ label: project.name,
+ organization: project.organization
+ }));
+ })
.then(options => {
- cb(null, { options });
+ return { options };
});
};
- handleAddProject = selected => {
+ handleAddProject = (selected: { label: string; organization: string; value: string }) => {
const project = {
key: selected.value,
name: selected.label,
organization: selected.organization
};
- this.setState({
- addedProjects: [...this.state.addedProjects, project]
- });
+ this.setState(state => ({
+ addedProjects: [...state.addedProjects, project]
+ }));
};
- renderOption = option => {
+ renderOption = (option: { label: string; organization: string; value: string }) => {
return (
<span>
- <Organization organizationKey={option.organization} link={false} />
+ <Organization link={false} organizationKey={option.organization} />
<strong>{option.label}</strong>
</span>
);
@@ -121,7 +115,17 @@ class Projects extends React.PureComponent {
<div className="note">{translate('my_account.no_project_notifications')}</div>
)}
- {allProjects.map(project => <ProjectNotifications key={project.key} project={project} />)}
+ {allProjects.map(project => (
+ <ProjectNotifications
+ addNotification={this.props.addNotification}
+ channels={this.props.channels}
+ key={project.key}
+ notifications={this.props.notificationsByProject[project.key] || []}
+ project={project}
+ removeNotification={this.props.removeNotification}
+ types={this.props.types}
+ />
+ ))}
<div className="spacer-top panel bg-muted">
<span className="text-middle spacer-right">
@@ -130,12 +134,12 @@ class Projects extends React.PureComponent {
<AsyncSelect
autoload={false}
cache={false}
- name="new_project"
- style={{ width: '300px' }}
+ className="input-super-large"
loadOptions={this.loadOptions}
minimumInput={2}
- optionRenderer={this.renderOption}
+ name="new_project"
onChange={this.handleAddProject}
+ optionRenderer={this.renderOption}
placeholder={translate('my_account.search_project')}
/>
</div>
@@ -144,11 +148,3 @@ class Projects extends React.PureComponent {
);
}
}
-
-const mapStateToProps = state => ({
- projects: getProjectsWithNotifications(state)
-});
-
-export default connect(mapStateToProps)(Projects);
-
-export const UnconnectedProjects = Projects;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.js b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.tsx
index 8201a1bff14..7f5cb7287bd 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.tsx
@@ -17,9 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import * as React from 'react';
import { shallow } from 'enzyme';
-import { UnconnectedGlobalNotifications } from '../GlobalNotifications';
+import GlobalNotifications from '../GlobalNotifications';
it('should match snapshot', () => {
const channels = ['channel1', 'channel2'];
@@ -32,12 +32,12 @@ it('should match snapshot', () => {
expect(
shallow(
- <UnconnectedGlobalNotifications
- notifications={notifications}
- channels={channels}
- types={types}
+ <GlobalNotifications
addNotification={jest.fn()}
+ channels={channels}
+ notifications={notifications}
removeNotification={jest.fn()}
+ types={types}
/>
)
).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx
new file mode 100644
index 00000000000..e0124a0806c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/order */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Notifications, { Props } from '../Notifications';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/notifications', () => ({
+ addNotification: jest.fn(() => Promise.resolve()),
+ getNotifications: jest.fn(() =>
+ Promise.resolve({
+ channels: ['channel1', 'channel2'],
+ globalTypes: ['type-global', 'type-common'],
+ notifications: [
+ { channel: 'channel1', type: 'type-global' },
+ { channel: 'channel1', type: 'type-common' },
+ {
+ channel: 'channel2',
+ type: 'type-common',
+ project: 'foo',
+ projectName: 'Foo',
+ organization: 'org'
+ }
+ ],
+ perProjectTypes: ['type-common']
+ })
+ ),
+ removeNotification: jest.fn(() => Promise.resolve())
+}));
+
+const api = require('../../../../api/notifications');
+
+const addNotification = api.addNotification as jest.Mock<any>;
+const getNotifications = api.getNotifications as jest.Mock<any>;
+const removeNotification = api.removeNotification as jest.Mock<any>;
+
+beforeEach(() => {
+ addNotification.mockClear();
+ getNotifications.mockClear();
+ removeNotification.mockClear();
+});
+
+it('should fetch notifications and render', async () => {
+ const wrapper = await shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ expect(getNotifications).toBeCalled();
+});
+
+it('should add global notification', async () => {
+ const notification = { channel: 'channel2', type: 'type-global' };
+ const wrapper = await shallowRender();
+ wrapper.find('GlobalNotifications').prop<Function>('addNotification')(notification);
+ // `state` must be immediately updated
+ expect(wrapper.state('notifications')).toContainEqual(notification);
+ expect(addNotification).toBeCalledWith(notification);
+});
+
+it('should remove project notification', async () => {
+ const notification = { channel: 'channel2', project: 'foo', type: 'type-common' };
+ const wrapper = await shallowRender();
+ expect(wrapper.state('notifications')).toContainEqual({
+ ...notification,
+ organization: 'org',
+ projectName: 'Foo'
+ });
+ wrapper.find('Projects').prop<Function>('removeNotification')(notification);
+ // `state` must be immediately updated
+ expect(wrapper.state('notifications')).not.toContainEqual(notification);
+ expect(removeNotification).toBeCalledWith(notification);
+});
+
+it('should NOT fetch organizations', async () => {
+ const fetchOrganizations = jest.fn();
+ await shallowRender({ fetchOrganizations });
+ expect(getNotifications).toBeCalled();
+ expect(fetchOrganizations).not.toBeCalled();
+});
+
+it('should fetch organizations', async () => {
+ const fetchOrganizations = jest.fn();
+ await shallowRender({ fetchOrganizations }, { organizationsEnabled: true });
+ expect(getNotifications).toBeCalled();
+ expect(fetchOrganizations).toBeCalledWith(['org']);
+});
+
+async function shallowRender(props?: Partial<Props>, context?: any) {
+ const wrapper = shallow(<Notifications fetchOrganizations={jest.fn()} {...props} />, { context });
+ await waitAndUpdate(wrapper);
+ return wrapper;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.js b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.tsx
index 5e389545628..25044dbb717 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.tsx
@@ -24,7 +24,7 @@ jest.mock('../../../../helpers/l10n', () => {
return l10n;
});
-import React from 'react';
+import * as React from 'react';
import { shallow } from 'enzyme';
import NotificationsList from '../NotificationsList';
import Checkbox from '../../../../components/controls/Checkbox';
@@ -37,39 +37,39 @@ const notifications = [
{ channel: 'channel1', type: 'type2' },
{ channel: 'channel2', type: 'type2' }
];
-const checkboxId = (t, c) => `checkbox-io-${t}-${c}`;
+const checkboxId = (t: string, c: string) => `checkbox-io-${t}-${c}`;
beforeEach(() => {
- hasMessage.mockImplementation(() => false).mockClear();
+ (hasMessage as jest.Mock<any>).mockImplementation(() => false).mockClear();
});
it('should match snapshot', () => {
expect(
shallow(
<NotificationsList
- onAdd={jest.fn()}
- onRemove={jest.fn()}
channels={channels}
checkboxId={checkboxId}
- types={types}
notifications={notifications}
+ onAdd={jest.fn()}
+ onRemove={jest.fn()}
+ types={types}
/>
)
).toMatchSnapshot();
});
it('renders project-specific labels', () => {
- hasMessage.mockImplementation(() => true);
+ (hasMessage as jest.Mock<any>).mockImplementation(() => true);
expect(
shallow(
<NotificationsList
- onAdd={jest.fn()}
- onRemove={jest.fn()}
channels={channels}
checkboxId={checkboxId}
+ notifications={notifications}
+ onAdd={jest.fn()}
+ onRemove={jest.fn()}
project={true}
types={types}
- notifications={notifications}
/>
)
).toMatchSnapshot();
@@ -82,12 +82,12 @@ it('should call `onAdd` and `onRemove`', () => {
const onRemove = jest.fn();
const wrapper = shallow(
<NotificationsList
- onAdd={onAdd}
- onRemove={onRemove}
channels={channels}
checkboxId={checkboxId}
- types={types}
notifications={notifications}
+ onAdd={onAdd}
+ onRemove={onRemove}
+ types={types}
/>
);
const checkbox = wrapper.find(Checkbox).first();
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.js b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.tsx
index d05fe0824c0..2ebd284b381 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.tsx
@@ -17,10 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import * as React from 'react';
import { shallow } from 'enzyme';
-import { UnconnectedProjectNotifications } from '../ProjectNotifications';
-import NotificationsList from '../NotificationsList';
+import ProjectNotifications from '../ProjectNotifications';
const channels = ['channel1', 'channel2'];
const types = ['type1', 'type2'];
@@ -33,13 +32,13 @@ const notifications = [
it('should match snapshot', () => {
expect(
shallow(
- <UnconnectedProjectNotifications
- project={{ key: 'foo', name: 'Foo' }}
- notifications={notifications}
- channels={channels}
- types={types}
+ <ProjectNotifications
addNotification={jest.fn()}
+ channels={channels}
+ notifications={notifications}
+ project={{ key: 'foo', name: 'Foo', organization: 'org' }}
removeNotification={jest.fn()}
+ types={types}
/>
)
).toMatchSnapshot();
@@ -49,28 +48,29 @@ it('should call `addNotification` and `removeNotification`', () => {
const addNotification = jest.fn();
const removeNotification = jest.fn();
const wrapper = shallow(
- <UnconnectedProjectNotifications
- project={{ key: 'foo', name: 'Foo' }}
- notifications={notifications}
- channels={channels}
- types={types}
+ <ProjectNotifications
addNotification={addNotification}
+ channels={channels}
+ notifications={notifications}
+ project={{ key: 'foo', name: 'Foo', organization: 'org' }}
removeNotification={removeNotification}
+ types={types}
/>
);
- const notificationsList = wrapper.find(NotificationsList);
+ const notificationsList = wrapper.find('NotificationsList');
- notificationsList.prop('onAdd')({ channel: 'channel2', type: 'type1' });
+ notificationsList.prop<Function>('onAdd')({ channel: 'channel2', type: 'type1' });
expect(addNotification).toHaveBeenCalledWith({
channel: 'channel2',
- type: 'type1',
+ organization: 'org',
project: 'foo',
- projectName: 'Foo'
+ projectName: 'Foo',
+ type: 'type1'
});
jest.resetAllMocks();
- notificationsList.prop('onRemove')({ channel: 'channel1', type: 'type1' });
+ notificationsList.prop<Function>('onRemove')({ channel: 'channel1', type: 'type1' });
expect(removeNotification).toHaveBeenCalledWith({
channel: 'channel1',
type: 'type1',
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.tsx
new file mode 100644
index 00000000000..dc028125b04
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 Projects, { Props } from '../Projects';
+
+jest.mock('../../../../api/components', () => ({
+ getSuggestions: jest.fn(() =>
+ Promise.resolve({
+ results: [
+ {
+ q: 'TRK',
+ items: [
+ { key: 'foo', name: 'Foo', organization: 'org' },
+ { key: 'bar', name: 'Bar', organization: 'org' }
+ ]
+ },
+ // this file should be ignored
+ { q: 'FIL', items: [{ key: 'foo:file.js', name: 'file.js', organization: 'org' }] }
+ ]
+ })
+ )
+}));
+
+const channels = ['channel1', 'channel2'];
+const types = ['type1', 'type2'];
+
+const projectFoo = { key: 'foo', name: 'Foo', organization: 'org' };
+const projectBar = { key: 'bar', name: 'Bar', organization: 'org' };
+const projects = [projectFoo, projectBar];
+
+const newProject = { key: 'qux', name: 'Qux', organization: 'org' };
+
+it('should render projects', () => {
+ const wrapper = shallowRender({
+ notificationsByProject: {
+ foo: [
+ {
+ channel: 'channel1',
+ organization: 'org',
+ project: 'foo',
+ projectName: 'Foo',
+ type: 'type1'
+ },
+ {
+ channel: 'channel1',
+ organization: 'org',
+ project: 'foo',
+ projectName: 'Foo',
+ type: 'type2'
+ }
+ ]
+ },
+ projects
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ // let's add a new project
+ wrapper.setState({ addedProjects: [newProject] });
+ expect(wrapper).toMatchSnapshot();
+
+ // let's say we saved it, so it's passed back in `props`
+ wrapper.setProps({ projects: [...projects, newProject] });
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.state()).toMatchSnapshot();
+});
+
+it('should search projects', () => {
+ const wrapper = shallowRender({ projects: [projectBar] });
+ const loadOptions = wrapper.find('AsyncSelect').prop<Function>('loadOptions');
+ expect(loadOptions('')).resolves.toEqual({ options: [] });
+ // should not contain `projectBar`
+ expect(loadOptions('more than two symbols')).resolves.toEqual({
+ options: [{ label: 'Foo', organization: 'org', value: 'foo' }]
+ });
+});
+
+it('should add project', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.state('addedProjects')).toEqual([]);
+ wrapper.find('AsyncSelect').prop<Function>('onChange')({
+ label: 'Qwe',
+ organization: 'org',
+ value: 'qwe'
+ });
+ expect(wrapper.state('addedProjects')).toEqual([
+ { key: 'qwe', name: 'Qwe', organization: 'org' }
+ ]);
+});
+
+it('should render option', () => {
+ const wrapper = shallowRender();
+ const optionRenderer = wrapper.find('AsyncSelect').prop<Function>('optionRenderer');
+ expect(
+ shallow(
+ optionRenderer({
+ label: 'Qwe',
+ organization: 'org',
+ value: 'qwe'
+ })
+ )
+ ).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<Props>) {
+ return shallow(
+ <Projects
+ addNotification={jest.fn()}
+ channels={channels}
+ notificationsByProject={{}}
+ projects={[]}
+ removeNotification={jest.fn()}
+ types={types}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.tsx.snap
index 3d21dbbdf2a..3d21dbbdf2a 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.js.snap
deleted file mode 100644
index 855ccecb356..00000000000
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.js.snap
+++ /dev/null
@@ -1,20 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should match snapshot 1`] = `
-<div
- className="account-body account-container"
->
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="my_account.notifications"
- />
- <p
- className="alert alert-info"
- >
- notification.dispatcher.information
- </p>
- <Connect(GlobalNotifications) />
- <Connect(Projects) />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.tsx.snap
new file mode 100644
index 00000000000..4fd7bdf0e79
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.tsx.snap
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should fetch notifications and render 1`] = `
+<div
+ className="account-body account-container"
+>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="my_account.notifications"
+ />
+ <p
+ className="alert alert-info"
+ >
+ notification.dispatcher.information
+ </p>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <React.Fragment>
+ <GlobalNotifications
+ addNotification={[Function]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ notifications={
+ Array [
+ Object {
+ "channel": "channel1",
+ "type": "type-global",
+ },
+ Object {
+ "channel": "channel1",
+ "type": "type-common",
+ },
+ ]
+ }
+ removeNotification={[Function]}
+ types={
+ Array [
+ "type-global",
+ "type-common",
+ ]
+ }
+ />
+ <Projects
+ addNotification={[Function]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ notificationsByProject={
+ Object {
+ "foo": Array [
+ Object {
+ "channel": "channel2",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type-common",
+ },
+ ],
+ }
+ }
+ projects={
+ Array [
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "organization": "org",
+ },
+ ]
+ }
+ removeNotification={[Function]}
+ types={
+ Array [
+ "type-common",
+ ]
+ }
+ />
+ </React.Fragment>
+ </DeferredSpinner>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.tsx.snap
index 13a18bff3c7..13a18bff3c7 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap
index 33c554244f1..90b25b16cf1 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap
@@ -11,7 +11,9 @@ exports[`should match snapshot 1`] = `
<span
className="text-normal"
>
- <Connect(Organization) />
+ <Connect(Organization)
+ organizationKey="org"
+ />
</span>
<h4
className="display-inline-block"
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.js.snap
deleted file mode 100644
index 7f2fb250868..00000000000
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.js.snap
+++ /dev/null
@@ -1,196 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render projects 1`] = `
-<section
- className="boxed-group"
->
- <h2>
- my_profile.per_project_notifications.title
- </h2>
- <div
- className="boxed-group-inner"
- >
- <Connect(ProjectNotifications)
- key="foo"
- project={
- Object {
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <Connect(ProjectNotifications)
- key="bar"
- project={
- Object {
- "key": "bar",
- "name": "Bar",
- }
- }
- />
- <div
- className="spacer-top panel bg-muted"
- >
- <span
- className="text-middle spacer-right"
- >
- my_account.set_notifications_for
- :
- </span>
- <AsyncSelect
- autoload={false}
- cache={false}
- loadOptions={[Function]}
- minimumInput={2}
- name="new_project"
- onChange={[Function]}
- optionRenderer={[Function]}
- placeholder="my_account.search_project"
- style={
- Object {
- "width": "300px",
- }
- }
- />
- </div>
- </div>
-</section>
-`;
-
-exports[`should render projects 2`] = `
-<section
- className="boxed-group"
->
- <h2>
- my_profile.per_project_notifications.title
- </h2>
- <div
- className="boxed-group-inner"
- >
- <Connect(ProjectNotifications)
- key="foo"
- project={
- Object {
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <Connect(ProjectNotifications)
- key="bar"
- project={
- Object {
- "key": "bar",
- "name": "Bar",
- }
- }
- />
- <Connect(ProjectNotifications)
- key="qux"
- project={
- Object {
- "key": "qux",
- "name": "Qux",
- }
- }
- />
- <div
- className="spacer-top panel bg-muted"
- >
- <span
- className="text-middle spacer-right"
- >
- my_account.set_notifications_for
- :
- </span>
- <AsyncSelect
- autoload={false}
- cache={false}
- loadOptions={[Function]}
- minimumInput={2}
- name="new_project"
- onChange={[Function]}
- optionRenderer={[Function]}
- placeholder="my_account.search_project"
- style={
- Object {
- "width": "300px",
- }
- }
- />
- </div>
- </div>
-</section>
-`;
-
-exports[`should render projects 3`] = `
-<section
- className="boxed-group"
->
- <h2>
- my_profile.per_project_notifications.title
- </h2>
- <div
- className="boxed-group-inner"
- >
- <Connect(ProjectNotifications)
- key="foo"
- project={
- Object {
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <Connect(ProjectNotifications)
- key="bar"
- project={
- Object {
- "key": "bar",
- "name": "Bar",
- }
- }
- />
- <Connect(ProjectNotifications)
- key="qux"
- project={
- Object {
- "key": "qux",
- "name": "Qux",
- }
- }
- />
- <div
- className="spacer-top panel bg-muted"
- >
- <span
- className="text-middle spacer-right"
- >
- my_account.set_notifications_for
- :
- </span>
- <AsyncSelect
- autoload={false}
- cache={false}
- loadOptions={[Function]}
- minimumInput={2}
- name="new_project"
- onChange={[Function]}
- optionRenderer={[Function]}
- placeholder="my_account.search_project"
- style={
- Object {
- "width": "300px",
- }
- }
- />
- </div>
- </div>
-</section>
-`;
-
-exports[`should render projects 4`] = `
-Object {
- "addedProjects": Array [],
-}
-`;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.tsx.snap
new file mode 100644
index 00000000000..74aceb01f8f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.tsx.snap
@@ -0,0 +1,375 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render option 1`] = `
+<span>
+ <Connect(Organization)
+ link={false}
+ organizationKey="org"
+ />
+ <strong>
+ Qwe
+ </strong>
+</span>
+`;
+
+exports[`should render projects 1`] = `
+<section
+ className="boxed-group"
+>
+ <h2>
+ my_profile.per_project_notifications.title
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="foo"
+ notifications={
+ Array [
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type1",
+ },
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type2",
+ },
+ ]
+ }
+ project={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="bar"
+ notifications={Array []}
+ project={
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <div
+ className="spacer-top panel bg-muted"
+ >
+ <span
+ className="text-middle spacer-right"
+ >
+ my_account.set_notifications_for
+ :
+ </span>
+ <AsyncSelect
+ autoload={false}
+ cache={false}
+ className="input-super-large"
+ loadOptions={[Function]}
+ minimumInput={2}
+ name="new_project"
+ onChange={[Function]}
+ optionRenderer={[Function]}
+ placeholder="my_account.search_project"
+ />
+ </div>
+ </div>
+</section>
+`;
+
+exports[`should render projects 2`] = `
+<section
+ className="boxed-group"
+>
+ <h2>
+ my_profile.per_project_notifications.title
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="foo"
+ notifications={
+ Array [
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type1",
+ },
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type2",
+ },
+ ]
+ }
+ project={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="bar"
+ notifications={Array []}
+ project={
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="qux"
+ notifications={Array []}
+ project={
+ Object {
+ "key": "qux",
+ "name": "Qux",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <div
+ className="spacer-top panel bg-muted"
+ >
+ <span
+ className="text-middle spacer-right"
+ >
+ my_account.set_notifications_for
+ :
+ </span>
+ <AsyncSelect
+ autoload={false}
+ cache={false}
+ className="input-super-large"
+ loadOptions={[Function]}
+ minimumInput={2}
+ name="new_project"
+ onChange={[Function]}
+ optionRenderer={[Function]}
+ placeholder="my_account.search_project"
+ />
+ </div>
+ </div>
+</section>
+`;
+
+exports[`should render projects 3`] = `
+<section
+ className="boxed-group"
+>
+ <h2>
+ my_profile.per_project_notifications.title
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="foo"
+ notifications={
+ Array [
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type1",
+ },
+ Object {
+ "channel": "channel1",
+ "organization": "org",
+ "project": "foo",
+ "projectName": "Foo",
+ "type": "type2",
+ },
+ ]
+ }
+ project={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="bar"
+ notifications={Array []}
+ project={
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <ProjectNotifications
+ addNotification={[MockFunction]}
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ key="qux"
+ notifications={Array []}
+ project={
+ Object {
+ "key": "qux",
+ "name": "Qux",
+ "organization": "org",
+ }
+ }
+ removeNotification={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ <div
+ className="spacer-top panel bg-muted"
+ >
+ <span
+ className="text-middle spacer-right"
+ >
+ my_account.set_notifications_for
+ :
+ </span>
+ <AsyncSelect
+ autoload={false}
+ cache={false}
+ className="input-super-large"
+ loadOptions={[Function]}
+ minimumInput={2}
+ name="new_project"
+ onChange={[Function]}
+ optionRenderer={[Function]}
+ placeholder="my_account.search_project"
+ />
+ </div>
+ </div>
+</section>
+`;
+
+exports[`should render projects 4`] = `
+Object {
+ "addedProjects": Array [],
+}
+`;
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/actions.js b/server/sonar-web/src/main/js/apps/account/notifications/actions.js
deleted file mode 100644
index 29a2c1db3e6..00000000000
--- a/server/sonar-web/src/main/js/apps/account/notifications/actions.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import * as api from '../../../api/notifications';
-/*:: import type { GetNotificationsResponse } from '../../../api/notifications'; */
-import { onFail, fetchOrganizations } from '../../../store/rootActions';
-import {
- receiveNotifications,
- addNotification as addNotificationAction,
- removeNotification as removeNotificationAction
-} from '../../../store/notifications/duck';
-/*:: import type { Notification } from '../../../store/notifications/duck'; */
-
-export const fetchNotifications = () => (dispatch /*: Function */) => {
- const onFulfil = (response /*: GetNotificationsResponse */) => {
- const organizations = response.notifications
- .filter(n => n.organization)
- .map(n => n.organization);
-
- dispatch(fetchOrganizations(organizations)).then(() => {
- dispatch(
- receiveNotifications(
- response.notifications,
- response.channels,
- response.globalTypes,
- response.perProjectTypes
- )
- );
- });
- };
-
- return api.getNotifications().then(onFulfil, onFail(dispatch));
-};
-
-export const addNotification = (n /*: Notification */) => (dispatch /*: Function */) =>
- api
- .addNotification(n.channel, n.type, n.project)
- .then(() => dispatch(addNotificationAction(n)), onFail(dispatch));
-
-export const removeNotification = (n /*: Notification */) => (dispatch /*: Function */) =>
- api
- .removeNotification(n.channel, n.type, n.project)
- .then(() => dispatch(removeNotificationAction(n)), onFail(dispatch));
diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.js b/server/sonar-web/src/main/js/apps/account/notifications/types.ts
index ca99b42e1c6..58d5dc5948d 100644
--- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.js
+++ b/server/sonar-web/src/main/js/apps/account/notifications/types.ts
@@ -17,10 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import { shallow } from 'enzyme';
-import { UnconnectedNotifications } from '../Notifications';
-
-it('should match snapshot', () => {
- expect(shallow(<UnconnectedNotifications fetchNotifications={jest.fn()} />)).toMatchSnapshot();
-});
+export interface NotificationProject {
+ key: string;
+ name: string;
+ organization: string;
+}
diff --git a/server/sonar-web/src/main/js/apps/account/routes.ts b/server/sonar-web/src/main/js/apps/account/routes.ts
index 2683c0988f6..b65220d777e 100644
--- a/server/sonar-web/src/main/js/apps/account/routes.ts
+++ b/server/sonar-web/src/main/js/apps/account/routes.ts
@@ -36,7 +36,7 @@ const routes = [
},
{
path: 'notifications',
- component: lazyLoad(() => import('./notifications/Notifications'))
+ component: lazyLoad(() => import('./notifications/NotificationsContainer'))
},
{
path: 'organizations',
diff --git a/server/sonar-web/src/main/js/components/controls/Select.tsx b/server/sonar-web/src/main/js/components/controls/Select.tsx
index 85fa556edc9..8aa569de29b 100644
--- a/server/sonar-web/src/main/js/components/controls/Select.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Select.tsx
@@ -60,6 +60,6 @@ export function Creatable(props: ReactCreatableSelectProps) {
}
// TODO figure out why `ref` prop is incompatible
-export function AsyncSelect(props: ReactAsyncSelectProps & { ref: any }) {
+export function AsyncSelect(props: ReactAsyncSelectProps & { ref?: any }) {
return <Async {...props} />;
}
diff --git a/server/sonar-web/src/main/js/store/notifications/duck.js b/server/sonar-web/src/main/js/store/notifications/duck.js
deleted file mode 100644
index 0861944491c..00000000000
--- a/server/sonar-web/src/main/js/store/notifications/duck.js
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import { combineReducers } from 'redux';
-import { uniqBy, uniqWith } from 'lodash';
-
-/*::
-export type Notification = {
- channel: string,
- type: string,
- project?: string,
- projectName?: string,
- organization?: string
-};
-*/
-
-/*::
-export type NotificationsState = Array<Notification>;
-*/
-/*::
-export type ChannelsState = Array<string>;
-*/
-/*::
-export type TypesState = Array<string>;
-*/
-
-/*::
-type AddNotificationAction = {
- type: 'ADD_NOTIFICATION',
- notification: Notification
-};
-*/
-
-/*::
-type RemoveNotificationAction = {
- type: 'REMOVE_NOTIFICATION',
- notification: Notification
-};
-*/
-
-/*::
-type ReceiveNotificationsAction = {
- type: 'RECEIVE_NOTIFICATIONS',
- notifications: NotificationsState,
- channels: ChannelsState,
- globalTypes: TypesState,
- perProjectTypes: TypesState
-};
-*/
-
-/*::
-type Action = AddNotificationAction | RemoveNotificationAction | ReceiveNotificationsAction;
-*/
-
-export function addNotification(notification /*: Notification */) /*: AddNotificationAction */ {
- return {
- type: 'ADD_NOTIFICATION',
- notification
- };
-}
-
-export function removeNotification(
- notification /*: Notification */
-) /*: RemoveNotificationAction */ {
- return {
- type: 'REMOVE_NOTIFICATION',
- notification
- };
-}
-
-export function receiveNotifications(
- notifications /*: NotificationsState */,
- channels /*: ChannelsState */,
- globalTypes /*: TypesState */,
- perProjectTypes /*: TypesState */
-) /*: ReceiveNotificationsAction */ {
- return {
- type: 'RECEIVE_NOTIFICATIONS',
- notifications,
- channels,
- globalTypes,
- perProjectTypes
- };
-}
-
-function onAddNotification(state /*: NotificationsState */, notification /*: Notification */) {
- function isNotificationsEqual(a /*: Notification */, b /*: Notification */) {
- return a.channel === b.channel && a.type === b.type && a.project === b.project;
- }
-
- return uniqWith([...state, notification], isNotificationsEqual);
-}
-
-function onRemoveNotification(state /*: NotificationsState */, notification /*: Notification */) {
- return state.filter(
- n =>
- n.channel !== notification.channel ||
- n.type !== notification.type ||
- n.project !== notification.project
- );
-}
-
-function onReceiveNotifications(
- state /*: NotificationsState */,
- notifications /*: NotificationsState */
-) {
- return [...notifications];
-}
-
-function notifications(state /*: NotificationsState */ = [], action /*: Action */) {
- switch (action.type) {
- case 'ADD_NOTIFICATION':
- return onAddNotification(state, action.notification);
- case 'REMOVE_NOTIFICATION':
- return onRemoveNotification(state, action.notification);
- case 'RECEIVE_NOTIFICATIONS':
- return onReceiveNotifications(state, action.notifications);
- default:
- return state;
- }
-}
-
-function channels(state /*: ChannelsState */ = [], action /*: Action */) {
- if (action.type === 'RECEIVE_NOTIFICATIONS') {
- return action.channels;
- } else {
- return state;
- }
-}
-
-function globalTypes(state /*: TypesState */ = [], action /*: Action */) {
- if (action.type === 'RECEIVE_NOTIFICATIONS') {
- return action.globalTypes;
- } else {
- return state;
- }
-}
-
-function perProjectTypes(state /*: TypesState */ = [], action /*: Action */) {
- if (action.type === 'RECEIVE_NOTIFICATIONS') {
- return action.perProjectTypes;
- } else {
- return state;
- }
-}
-
-/*::
-type State = {
- notifications: NotificationsState,
- channels: ChannelsState,
- globalTypes: TypesState,
- perProjectTypes: TypesState
-};
-*/
-
-export default combineReducers({ notifications, channels, globalTypes, perProjectTypes });
-
-export function getGlobal(state /*: State */) /*: NotificationsState */ {
- return state.notifications.filter(n => !n.project);
-}
-
-export function getProjects(state /*: State */) /*: Array<{ key: string, name: string }> */ {
- // $FlowFixMe
- const allProjects = state.notifications.filter(n => n.project != null).map(n => ({
- key: n.project,
- name: n.projectName,
- organization: n.organization
- }));
-
- return uniqBy(allProjects, project => project.key);
-}
-
-export function getForProject(state /*: State */, project /*: string */) /*: NotificationsState */ {
- return state.notifications.filter(n => n.project === project);
-}
-
-export function getChannels(state /*: State */) /*: ChannelsState */ {
- return state.channels;
-}
-
-export function getGlobalTypes(state /*: State */) /*: TypesState */ {
- return state.globalTypes;
-}
-
-export function getPerProjectTypes(state /*: State */) /*: TypesState */ {
- return state.perProjectTypes;
-}
diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js
index 15c9d1ac30b..0ca7aab07c4 100644
--- a/server/sonar-web/src/main/js/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/store/rootReducer.js
@@ -24,7 +24,6 @@ import users, * as fromUsers from './users/reducer';
import favorites, * as fromFavorites from './favorites/duck';
import languages, * as fromLanguages from './languages/reducer';
import metrics, * as fromMetrics from './metrics/reducer';
-import notifications, * as fromNotifications from './notifications/duck';
import organizations, * as fromOrganizations from './organizations/duck';
import organizationsMembers, * as fromOrganizationsMembers from './organizationsMembers/reducer';
import globalMessages, * as fromGlobalMessages from './globalMessages/duck';
@@ -39,7 +38,6 @@ export default combineReducers({
languages,
marketplace,
metrics,
- notifications,
organizations,
organizationsMembers,
users,
@@ -89,22 +87,6 @@ export const getMetricByKey = (state, key) => fromMetrics.getMetricByKey(state.m
export const getMetricsKey = state => fromMetrics.getMetricsKey(state.metrics);
-export const getGlobalNotifications = state => fromNotifications.getGlobal(state.notifications);
-
-export const getProjectsWithNotifications = state =>
- fromNotifications.getProjects(state.notifications);
-
-export const getProjectNotifications = (state, project) =>
- fromNotifications.getForProject(state.notifications, project);
-
-export const getNotificationChannels = state => fromNotifications.getChannels(state.notifications);
-
-export const getNotificationGlobalTypes = state =>
- fromNotifications.getGlobalTypes(state.notifications);
-
-export const getNotificationPerProjectTypes = state =>
- fromNotifications.getPerProjectTypes(state.notifications);
-
export const getOrganizationByKey = (state, key) =>
fromOrganizations.getOrganizationByKey(state.organizations, key);