diff options
author | Siegfried Ehret <49895321+siegfried-ehret-sonarsource@users.noreply.github.com> | 2019-10-17 11:32:10 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-10-28 20:21:09 +0100 |
commit | cd2296809f5308f0cbf14100fcbff2846c3bf3da (patch) | |
tree | 9be140071398becdef60caf20b6555bba31e1ebd /server/sonar-web/src/main/js/components | |
parent | 3c93ddc45bbd9210d55a9d6b7a8802e330247640 (diff) | |
download | sonarqube-cd2296809f5308f0cbf14100fcbff2846c3bf3da.tar.gz sonarqube-cd2296809f5308f0cbf14100fcbff2846c3bf3da.zip |
SONAR-10037 Manage project notifications from project dashboard
Co-authored-by: Wouter Admiraal <wouter.admiraal@sonarsource.com>
Diffstat (limited to 'server/sonar-web/src/main/js/components')
3 files changed, 295 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/__snapshots__/withNotifications-test.tsx.snap b/server/sonar-web/src/main/js/components/hoc/__tests__/__snapshots__/withNotifications-test.tsx.snap new file mode 100644 index 00000000000..4af5332cd6c --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/__snapshots__/withNotifications-test.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should fetch notifications and render 1`] = ` +<X + addNotification={[Function]} + channels={ + Array [ + "channel1", + "channel2", + ] + } + globalTypes={ + Array [ + "type-global", + "type-common", + ] + } + loading={false} + notifications={ + Array [ + Object { + "channel": "channel1", + "organization": "org", + "project": "foo", + "projectName": "Foo", + "type": "type-global", + }, + Object { + "channel": "channel1", + "organization": "org", + "project": "bar", + "projectName": "Bar", + "type": "type-common", + }, + Object { + "channel": "channel2", + "organization": "org", + "project": "qux", + "projectName": "Qux", + "type": "type-common", + }, + ] + } + perProjectTypes={ + Array [ + "type-common", + ] + } + removeNotification={[Function]} +/> +`; diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withNotifications-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withNotifications-test.tsx new file mode 100644 index 00000000000..7d192fe72ef --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withNotifications-test.tsx @@ -0,0 +1,96 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { addNotification, getNotifications, removeNotification } from '../../../api/notifications'; +import { withNotifications, WithNotificationsProps } from '../withNotifications'; + +jest.mock('../../../api/notifications', () => ({ + addNotification: jest.fn().mockResolvedValue({}), + getNotifications: jest.fn(() => + Promise.resolve({ + channels: ['channel1', 'channel2'], + globalTypes: ['type-global', 'type-common'], + notifications: [ + { + channel: 'channel1', + type: 'type-global', + project: 'foo', + projectName: 'Foo', + organization: 'org' + }, + { + channel: 'channel1', + type: 'type-common', + project: 'bar', + projectName: 'Bar', + organization: 'org' + }, + { + channel: 'channel2', + type: 'type-common', + project: 'qux', + projectName: 'Qux', + organization: 'org' + } + ], + perProjectTypes: ['type-common'] + }) + ), + removeNotification: jest.fn().mockResolvedValue({}) +})); + +class X extends React.Component<WithNotificationsProps> { + render() { + return <div />; + } +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('should fetch notifications and render', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + expect(getNotifications).toBeCalled(); +}); + +it('should add and remove a notification', () => { + const wrapper = shallowRender(); + const notification = { + channel: 'EmailNotificationChannel', + project: 'foo', + type: 'SQ-MyNewIssues' + }; + + wrapper.prop('addNotification')(notification); + expect(addNotification).toHaveBeenCalledWith(notification); + + wrapper.prop('removeNotification')(notification); + expect(removeNotification).toHaveBeenCalledWith(notification); +}); + +function shallowRender() { + const UnderTest = withNotifications<{}>(X); + return shallow(<UnderTest />); +} diff --git a/server/sonar-web/src/main/js/components/hoc/withNotifications.tsx b/server/sonar-web/src/main/js/components/hoc/withNotifications.tsx new file mode 100644 index 00000000000..f327c46f38e --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/withNotifications.tsx @@ -0,0 +1,148 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { uniqWith } from 'lodash'; +import * as React from 'react'; +import { addNotification, getNotifications, removeNotification } from '../../api/notifications'; +import { getWrappedDisplayName } from './utils'; + +interface State { + channels: string[]; + globalTypes: string[]; + loading: boolean; + notifications: T.Notification[]; + perProjectTypes: string[]; +} + +export interface WithNotificationsProps { + addNotification: (added: T.Notification) => void; + channels: string[]; + globalTypes: string[]; + loading: boolean; + notifications: T.Notification[]; + perProjectTypes: string[]; + removeNotification: (removed: T.Notification) => void; +} + +export function withNotifications<P>( + WrappedComponent: React.ComponentType<P & WithNotificationsProps> +) { + class Wrapper extends React.Component<P, State> { + mounted = false; + static displayName = getWrappedDisplayName(WrappedComponent, 'withNotifications'); + + state: State = { + channels: [], + globalTypes: [], + loading: true, + notifications: [], + perProjectTypes: [] + }; + + componentDidMount() { + this.mounted = true; + this.fetchNotifications(); + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchNotifications = () => { + getNotifications().then( + response => { + if (this.mounted) { + 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: T.Notification) => { + this.setState(state => { + const notifications = uniqWith([...state.notifications, added], this.areNotificationsEqual); + return { notifications }; + }); + }; + + removeNotificationFromState = (removed: T.Notification) => { + this.setState(state => { + const notifications = state.notifications.filter( + notification => !this.areNotificationsEqual(notification, removed) + ); + return { notifications }; + }); + }; + + addNotification = (added: T.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 }; + addNotification(data).catch(() => { + this.removeNotificationFromState(added); + }); + }; + + removeNotification = (removed: T.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 }; + removeNotification(data).catch(() => { + this.addNotificationToState(removed); + }); + }; + + areNotificationsEqual = (a: T.Notification, b: T.Notification) => { + return a.channel === b.channel && a.type === b.type && a.project === b.project; + }; + + render() { + const { channels, globalTypes, loading, notifications, perProjectTypes } = this.state; + return ( + <WrappedComponent + {...this.props} + addNotification={this.addNotification} + channels={channels} + globalTypes={globalTypes} + loading={loading} + notifications={notifications} + perProjectTypes={perProjectTypes} + removeNotification={this.removeNotification} + /> + ); + } + } + + return Wrapper; +} |