From: Stas Vilchik Date: Tue, 15 May 2018 09:09:13 +0000 (+0200) Subject: rewrite notifications app in ts and drop from redux store (#233) X-Git-Tag: 7.5~1184 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=34db3b05cf4dcdb11faa5b392837cc4d979d19ff;p=sonarqube.git rewrite notifications app in ts and drop from redux store (#233) --- 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; - globalTypes: Array; - perProjectTypes: Array; +export function getNotifications(): Promise<{ + channels: string[]; + globalTypes: string[]; + notifications: Notification[]; + perProjectTypes: string[]; +}> { + return getJSON('/api/notifications/list').catch(throwGlobalError); } -export function getNotifications(): Promise { - 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 { - 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 { - 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.js deleted file mode 100644 index 21b7c28f9d1..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js +++ /dev/null @@ -1,89 +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. - */ -import React from 'react'; -import { connect } from 'react-redux'; -import NotificationsList from './NotificationsList'; -import { addNotification, removeNotification } from './actions'; -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 -}; -*/ - -function GlobalNotifications(props /*: Props */) { - return ( -
-

{translate('my_profile.overall_notifications.title')}

- -
- - - - - ))} - - - - `global-notification-${d}-${c}`} - onAdd={props.addNotification} - onRemove={props.removeNotification} - /> -
- {props.channels.map(channel => ( - -

{translate('notification.channel', channel)}

-
-
-
- ); -} - -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; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx new file mode 100644 index 00000000000..0ac75ce0151 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx @@ -0,0 +1,67 @@ +/* + * 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 NotificationsList from './NotificationsList'; +import { Notification } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + addNotification: (n: Notification) => void; + channels: string[]; + notifications: Notification[]; + removeNotification: (n: Notification) => void; + types: string[]; +} + +export default function GlobalNotifications(props: Props) { + return ( +
+

{translate('my_profile.overall_notifications.title')}

+ +
+ + + + + ))} + + + + +
+ {props.channels.map(channel => ( + +

{translate('notification.channel', channel)}

+
+
+
+ ); +} + +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 ( -
- -

{translate('notification.dispatcher.information')}

- - -
- ); - } -} - -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 { + 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 ( +
+ +

{translate('notification.dispatcher.information')}

+ + {this.state.notifications && ( + <> + + + + )} + +
+ ); + } +} + +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/NotificationsContainer.tsx b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.tsx new file mode 100644 index 00000000000..7c05e0f1616 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsContainer.tsx @@ -0,0 +1,26 @@ +/* + * 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 { connect } from 'react-redux'; +import Notifications, { Props } from './Notifications'; +import { fetchOrganizations } from '../../../store/rootActions'; + +const mapDispatchToProps = { fetchOrganizations } as Pick; + +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.js deleted file mode 100644 index 6ca7f02bdfe..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.js +++ /dev/null @@ -1,87 +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. - */ -import React from 'react'; -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 - }; -*/ - - isEnabled(type /*: string */, channel /*: string */) /*: boolean */ { - return !!this.props.notifications.find( - notification => notification.type === type && notification.channel === channel - ); - } - - handleCheck(type /*: string */, channel /*: string */, checked /*: boolean */) { - if (checked) { - this.props.onAdd({ type, channel }); - } else { - this.props.onRemove({ type, channel }); - } - } - - getDispatcherLabel(dispatcher /*: string */) { - const globalMessageKey = ['notification.dispatcher', dispatcher]; - const projectMessageKey = [...globalMessageKey, 'project']; - const shouldUseProjectMessage = this.props.project && hasMessage(...projectMessageKey); - return shouldUseProjectMessage - ? translate(...projectMessageKey) - : translate(...globalMessageKey); - } - - render() { - const { channels, checkboxId, types } = this.props; - - return ( - - {types.map(type => ( - - {this.getDispatcherLabel(type)} - {channels.map(channel => ( - - this.handleCheck(type, channel, checked)} - /> - - ))} - - ))} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx new file mode 100644 index 00000000000..35915a0f4f1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/NotificationsList.tsx @@ -0,0 +1,81 @@ +/* + * 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 { Notification } from '../../../app/types'; +import Checkbox from '../../../components/controls/Checkbox'; +import { translate, hasMessage } from '../../../helpers/l10n'; + +interface Props { + onAdd: (n: Notification) => void; + onRemove: (n: Notification) => void; + channels: string[]; + checkboxId: (type: string, channel: string) => string; + project?: boolean; + types: string[]; + notifications: Notification[]; +} + +export default class NotificationsList extends React.PureComponent { + isEnabled(type: string, channel: string) { + return !!this.props.notifications.find( + notification => notification.type === type && notification.channel === channel + ); + } + + handleCheck(type: string, channel: string, checked: boolean) { + if (checked) { + this.props.onAdd({ type, channel }); + } else { + this.props.onRemove({ type, channel }); + } + } + + getDispatcherLabel(dispatcher: string) { + const globalMessageKey = ['notification.dispatcher', dispatcher]; + const projectMessageKey = [...globalMessageKey, 'project']; + const shouldUseProjectMessage = this.props.project && hasMessage(...projectMessageKey); + return shouldUseProjectMessage + ? translate(...projectMessageKey) + : translate(...globalMessageKey); + } + + render() { + const { channels, checkboxId, types } = this.props; + + return ( + + {types.map(type => ( + + {this.getDispatcherLabel(type)} + {channels.map(channel => ( + + this.handleCheck(type, channel, checked)} + /> + + ))} + + ))} + + ); + } +} 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.js deleted file mode 100644 index 8fbf66c2d4c..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.js +++ /dev/null @@ -1,119 +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. - */ -import React from 'react'; -import { connect } from 'react-redux'; -import { Link } from 'react-router'; -import NotificationsList from './NotificationsList'; -import { addNotification, removeNotification } from './actions'; -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 - }; -*/ - - handleAddNotification({ channel, type }) { - this.props.addNotification({ - channel, - type, - project: this.props.project.key, - projectName: this.props.project.name, - organization: this.props.project.organization - }); - } - - handleRemoveNotification({ channel, type }) { - this.props.removeNotification({ - channel, - type, - project: this.props.project.key - }); - } - - render() { - const { project, channels } = this.props; - - return ( - - - - - {channels.map(channel => ( - - ))} - - - `project-notification-${project.key}-${d}-${c}`} - onAdd={n => this.handleAddNotification(n)} - onRemove={n => this.handleRemoveNotification(n)} - project={true} - /> -
- - - -

- {project.name} -

-
-

{translate('notification.channel', channel)}

-
- ); - } -} - -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/ProjectNotifications.tsx b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx new file mode 100644 index 00000000000..f5e29f76ba1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx @@ -0,0 +1,95 @@ +/* + * 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 { Link } from 'react-router'; +import NotificationsList from './NotificationsList'; +import { NotificationProject } from './types'; +import { Notification } from '../../../app/types'; +import Organization from '../../../components/shared/Organization'; +import { translate } from '../../../helpers/l10n'; +import { getProjectUrl } from '../../../helpers/urls'; + +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 { + getCheckboxId = (type: string, channel: string) => { + return `project-notification-${this.props.project.key}-${type}-${channel}`; + }; + + handleAddNotification = ({ channel, type }: { channel: string; type: string }) => { + this.props.addNotification({ + channel, + type, + project: this.props.project.key, + projectName: this.props.project.name, + organization: this.props.project.organization + }); + }; + + handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => { + this.props.removeNotification({ + channel, + type, + project: this.props.project.key + }); + }; + + render() { + const { project, channels } = this.props; + + return ( + + + + + {channels.map(channel => ( + + ))} + + + +
+ + + +

+ {project.name} +

+
+

{translate('notification.channel', channel)}

+
+ ); + } +} 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.js deleted file mode 100644 index 5799f59f7d2..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/Projects.js +++ /dev/null @@ -1,154 +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. - */ -import React from 'react'; -import { connect } from 'react-redux'; -import { differenceBy } from 'lodash'; -import ProjectNotifications from './ProjectNotifications'; -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 - }> -}; -*/ - -class Projects extends React.PureComponent { - /*:: props: Props; */ - - state /*: State */ = { - addedProjects: [] - }; - - 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 }); - } - } - - loadOptions = (query, cb) => { - if (query.length < 2) { - cb(null, { options: [] }); - 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(options => { - cb(null, { options }); - }); - }; - - handleAddProject = selected => { - const project = { - key: selected.value, - name: selected.label, - organization: selected.organization - }; - this.setState({ - addedProjects: [...this.state.addedProjects, project] - }); - }; - - renderOption = option => { - return ( - - - {option.label} - - ); - }; - - render() { - const allProjects = [...this.props.projects, ...this.state.addedProjects]; - - return ( -
-

{translate('my_profile.per_project_notifications.title')}

- -
- {allProjects.length === 0 && ( -
{translate('my_account.no_project_notifications')}
- )} - - {allProjects.map(project => )} - -
- - {translate('my_account.set_notifications_for')}: - - -
-
-
- ); - } -} - -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/Projects.tsx b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx new file mode 100644 index 00000000000..77f7876bb7d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx @@ -0,0 +1,150 @@ +/* + * 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 { 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'; + +export interface Props { + addNotification: (n: Notification) => void; + channels: string[]; + notificationsByProject: { [project: string]: Notification[] }; + projects: NotificationProject[]; + removeNotification: (n: Notification) => void; + types: string[]; +} + +interface State { + addedProjects: NotificationProject[]; +} + +export default class Projects extends React.PureComponent { + state: State = { addedProjects: [] }; + + componentWillReceiveProps(nextProps: Props) { + // remove all projects from `this.state.addedProjects` + // that already exist in `nextProps.projects` + this.setState(state => ({ + addedProjects: differenceWith( + state.addedProjects, + Object.keys(nextProps.projects), + (stateProject, propsProjectKey) => stateProject.key !== propsProjectKey + ) + })); + } + + loadOptions = (query: string) => { + if (query.length < 2) { + return Promise.resolve({ options: [] }); + } + + return getSuggestions(query) + .then(r => { + const projects = r.results.find(domain => domain.q === 'TRK'); + return projects ? projects.items : []; + }) + .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 => { + return { options }; + }); + }; + + handleAddProject = (selected: { label: string; organization: string; value: string }) => { + const project = { + key: selected.value, + name: selected.label, + organization: selected.organization + }; + this.setState(state => ({ + addedProjects: [...state.addedProjects, project] + })); + }; + + renderOption = (option: { label: string; organization: string; value: string }) => { + return ( + + + {option.label} + + ); + }; + + render() { + const allProjects = [...this.props.projects, ...this.state.addedProjects]; + + return ( +
+

{translate('my_profile.per_project_notifications.title')}

+ +
+ {allProjects.length === 0 && ( +
{translate('my_account.no_project_notifications')}
+ )} + + {allProjects.map(project => ( + + ))} + +
+ + {translate('my_account.set_notifications_for')}: + + +
+
+
+ ); + } +} 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.js deleted file mode 100644 index 8201a1bff14..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.js +++ /dev/null @@ -1,44 +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. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { UnconnectedGlobalNotifications } from '../GlobalNotifications'; - -it('should match snapshot', () => { - const channels = ['channel1', 'channel2']; - const types = ['type1', 'type2']; - const notifications = [ - { channel: 'channel1', type: 'type1' }, - { channel: 'channel1', type: 'type2' }, - { channel: 'channel2', type: 'type2' } - ]; - - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.tsx b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.tsx new file mode 100644 index 00000000000..7f5cb7287bd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/GlobalNotifications-test.tsx @@ -0,0 +1,44 @@ +/* + * 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 GlobalNotifications from '../GlobalNotifications'; + +it('should match snapshot', () => { + const channels = ['channel1', 'channel2']; + const types = ['type1', 'type2']; + const notifications = [ + { channel: 'channel1', type: 'type1' }, + { channel: 'channel1', type: 'type2' }, + { channel: 'channel2', type: 'type2' } + ]; + + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); 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/__tests__/Notifications-test.js deleted file mode 100644 index ca99b42e1c6..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Notifications-test.js +++ /dev/null @@ -1,26 +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. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { UnconnectedNotifications } from '../Notifications'; - -it('should match snapshot', () => { - expect(shallow()).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; +const getNotifications = api.getNotifications as jest.Mock; +const removeNotification = api.removeNotification as jest.Mock; + +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('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('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, context?: any) { + const wrapper = shallow(, { 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.js deleted file mode 100644 index 5e389545628..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.js +++ /dev/null @@ -1,102 +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. - */ -/* eslint-disable import/first */ -jest.mock('../../../../helpers/l10n', () => { - const l10n = require.requireActual('../../../../helpers/l10n'); - l10n.hasMessage = jest.fn(); - return l10n; -}); - -import React from 'react'; -import { shallow } from 'enzyme'; -import NotificationsList from '../NotificationsList'; -import Checkbox from '../../../../components/controls/Checkbox'; -import { hasMessage } from '../../../../helpers/l10n'; - -const channels = ['channel1', 'channel2']; -const types = ['type1', 'type2']; -const notifications = [ - { channel: 'channel1', type: 'type1' }, - { channel: 'channel1', type: 'type2' }, - { channel: 'channel2', type: 'type2' } -]; -const checkboxId = (t, c) => `checkbox-io-${t}-${c}`; - -beforeEach(() => { - hasMessage.mockImplementation(() => false).mockClear(); -}); - -it('should match snapshot', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); - -it('renders project-specific labels', () => { - hasMessage.mockImplementation(() => true); - expect( - shallow( - - ) - ).toMatchSnapshot(); - expect(hasMessage).toBeCalledWith('notification.dispatcher', 'type1', 'project'); - expect(hasMessage).toBeCalledWith('notification.dispatcher', 'type2', 'project'); -}); - -it('should call `onAdd` and `onRemove`', () => { - const onAdd = jest.fn(); - const onRemove = jest.fn(); - const wrapper = shallow( - - ); - const checkbox = wrapper.find(Checkbox).first(); - - checkbox.prop('onCheck')(true); - expect(onAdd).toHaveBeenCalledWith({ channel: 'channel1', type: 'type1' }); - - jest.resetAllMocks(); - - checkbox.prop('onCheck')(false); - expect(onRemove).toHaveBeenCalledWith({ channel: 'channel1', type: 'type1' }); -}); diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.tsx b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.tsx new file mode 100644 index 00000000000..25044dbb717 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/NotificationsList-test.tsx @@ -0,0 +1,102 @@ +/* + * 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/first */ +jest.mock('../../../../helpers/l10n', () => { + const l10n = require.requireActual('../../../../helpers/l10n'); + l10n.hasMessage = jest.fn(); + return l10n; +}); + +import * as React from 'react'; +import { shallow } from 'enzyme'; +import NotificationsList from '../NotificationsList'; +import Checkbox from '../../../../components/controls/Checkbox'; +import { hasMessage } from '../../../../helpers/l10n'; + +const channels = ['channel1', 'channel2']; +const types = ['type1', 'type2']; +const notifications = [ + { channel: 'channel1', type: 'type1' }, + { channel: 'channel1', type: 'type2' }, + { channel: 'channel2', type: 'type2' } +]; +const checkboxId = (t: string, c: string) => `checkbox-io-${t}-${c}`; + +beforeEach(() => { + (hasMessage as jest.Mock).mockImplementation(() => false).mockClear(); +}); + +it('should match snapshot', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders project-specific labels', () => { + (hasMessage as jest.Mock).mockImplementation(() => true); + expect( + shallow( + + ) + ).toMatchSnapshot(); + expect(hasMessage).toBeCalledWith('notification.dispatcher', 'type1', 'project'); + expect(hasMessage).toBeCalledWith('notification.dispatcher', 'type2', 'project'); +}); + +it('should call `onAdd` and `onRemove`', () => { + const onAdd = jest.fn(); + const onRemove = jest.fn(); + const wrapper = shallow( + + ); + const checkbox = wrapper.find(Checkbox).first(); + + checkbox.prop('onCheck')(true); + expect(onAdd).toHaveBeenCalledWith({ channel: 'channel1', type: 'type1' }); + + jest.resetAllMocks(); + + checkbox.prop('onCheck')(false); + expect(onRemove).toHaveBeenCalledWith({ channel: 'channel1', type: 'type1' }); +}); 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.js deleted file mode 100644 index d05fe0824c0..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.js +++ /dev/null @@ -1,79 +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. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { UnconnectedProjectNotifications } from '../ProjectNotifications'; -import NotificationsList from '../NotificationsList'; - -const channels = ['channel1', 'channel2']; -const types = ['type1', 'type2']; -const notifications = [ - { channel: 'channel1', type: 'type1' }, - { channel: 'channel1', type: 'type2' }, - { channel: 'channel2', type: 'type2' } -]; - -it('should match snapshot', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); - -it('should call `addNotification` and `removeNotification`', () => { - const addNotification = jest.fn(); - const removeNotification = jest.fn(); - const wrapper = shallow( - - ); - const notificationsList = wrapper.find(NotificationsList); - - notificationsList.prop('onAdd')({ channel: 'channel2', type: 'type1' }); - expect(addNotification).toHaveBeenCalledWith({ - channel: 'channel2', - type: 'type1', - project: 'foo', - projectName: 'Foo' - }); - - jest.resetAllMocks(); - - notificationsList.prop('onRemove')({ channel: 'channel1', type: 'type1' }); - expect(removeNotification).toHaveBeenCalledWith({ - channel: 'channel1', - type: 'type1', - project: 'foo' - }); -}); diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.tsx b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.tsx new file mode 100644 index 00000000000..2ebd284b381 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/ProjectNotifications-test.tsx @@ -0,0 +1,79 @@ +/* + * 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 ProjectNotifications from '../ProjectNotifications'; + +const channels = ['channel1', 'channel2']; +const types = ['type1', 'type2']; +const notifications = [ + { channel: 'channel1', type: 'type1' }, + { channel: 'channel1', type: 'type2' }, + { channel: 'channel2', type: 'type2' } +]; + +it('should match snapshot', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('should call `addNotification` and `removeNotification`', () => { + const addNotification = jest.fn(); + const removeNotification = jest.fn(); + const wrapper = shallow( + + ); + const notificationsList = wrapper.find('NotificationsList'); + + notificationsList.prop('onAdd')({ channel: 'channel2', type: 'type1' }); + expect(addNotification).toHaveBeenCalledWith({ + channel: 'channel2', + organization: 'org', + project: 'foo', + projectName: 'Foo', + type: 'type1' + }); + + jest.resetAllMocks(); + + notificationsList.prop('onRemove')({ channel: 'channel1', type: 'type1' }); + expect(removeNotification).toHaveBeenCalledWith({ + channel: 'channel1', + type: 'type1', + project: 'foo' + }); +}); 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/__tests__/Projects-test.js deleted file mode 100644 index 5f6598b5950..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/Projects-test.js +++ /dev/null @@ -1,40 +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. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { UnconnectedProjects } from '../Projects'; - -const projects = [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]; - -const newProject = { key: 'qux', name: 'Qux' }; - -it('should render projects', () => { - const wrapper = shallow(); - 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(); -}); 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('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('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('optionRenderer'); + expect( + shallow( + optionRenderer({ + label: 'Qwe', + organization: 'org', + value: 'qwe' + }) + ) + ).toMatchSnapshot(); +}); + +function shallowRender(props?: Partial) { + return shallow( + + ); +} 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.js.snap deleted file mode 100644 index 3d21dbbdf2a..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should match snapshot 1`] = ` -
-

- my_profile.overall_notifications.title -

-
- - - - - - - - -
- -

- notification.channel.channel1 -

-
-

- notification.channel.channel2 -

-
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.tsx.snap new file mode 100644 index 00000000000..3d21dbbdf2a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should match snapshot 1`] = ` +
+

+ my_profile.overall_notifications.title +

+
+ + + + + + + + +
+ +

+ notification.channel.channel1 +

+
+

+ notification.channel.channel2 +

+
+
+
+`; 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`] = ` -
- -

- notification.dispatcher.information -

- - -
-`; 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`] = ` +
+ +

+ notification.dispatcher.information +

+ + + + + + +
+`; 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.js.snap deleted file mode 100644 index 13a18bff3c7..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.js.snap +++ /dev/null @@ -1,127 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders project-specific labels 1`] = ` - - - - notification.dispatcher.type1.project - - - - - - - - - - - notification.dispatcher.type2.project - - - - - - - - - -`; - -exports[`should match snapshot 1`] = ` - - - - notification.dispatcher.type1 - - - - - - - - - - - notification.dispatcher.type2 - - - - - - - - - -`; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.tsx.snap new file mode 100644 index 00000000000..13a18bff3c7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/NotificationsList-test.tsx.snap @@ -0,0 +1,127 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders project-specific labels 1`] = ` + + + + notification.dispatcher.type1.project + + + + + + + + + + + notification.dispatcher.type2.project + + + + + + + + + +`; + +exports[`should match snapshot 1`] = ` + + + + notification.dispatcher.type1 + + + + + + + + + + + notification.dispatcher.type2 + + + + + + + + + +`; 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.js.snap deleted file mode 100644 index 33c554244f1..00000000000 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap +++ /dev/null @@ -1,88 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should match snapshot 1`] = ` - - - - - - - - - -
- - - -

- - Foo - -

-
-

- notification.channel.channel1 -

-
-

- notification.channel.channel2 -

-
-`; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap new file mode 100644 index 00000000000..90b25b16cf1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should match snapshot 1`] = ` + + + + + + + + + +
+ + + +

+ + Foo + +

+
+

+ notification.channel.channel1 +

+
+

+ notification.channel.channel2 +

+
+`; 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`] = ` -
-

- my_profile.per_project_notifications.title -

-
- - -
- - my_account.set_notifications_for - : - - -
-
-
-`; - -exports[`should render projects 2`] = ` -
-

- my_profile.per_project_notifications.title -

-
- - - -
- - my_account.set_notifications_for - : - - -
-
-
-`; - -exports[`should render projects 3`] = ` -
-

- my_profile.per_project_notifications.title -

-
- - - -
- - my_account.set_notifications_for - : - - -
-
-
-`; - -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`] = ` + + + + Qwe + + +`; + +exports[`should render projects 1`] = ` +
+

+ my_profile.per_project_notifications.title +

+
+ + +
+ + my_account.set_notifications_for + : + + +
+
+
+`; + +exports[`should render projects 2`] = ` +
+

+ my_profile.per_project_notifications.title +

+
+ + + +
+ + my_account.set_notifications_for + : + + +
+
+
+`; + +exports[`should render projects 3`] = ` +
+

+ my_profile.per_project_notifications.title +

+
+ + + +
+ + my_account.set_notifications_for + : + + +
+
+
+`; + +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/types.ts b/server/sonar-web/src/main/js/apps/account/notifications/types.ts new file mode 100644 index 00000000000..58d5dc5948d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/notifications/types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +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 ; } 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; -*/ -/*:: -export type ChannelsState = Array; -*/ -/*:: -export type TypesState = Array; -*/ - -/*:: -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);