if (more) {
data.more = more;
}
- return getJSON('/api/components/suggestions', data);
+ return getJSON('/api/components/suggestions', data).catch(throwGlobalError);
}
export function getComponentForSourceViewer(
* 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);
}
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;
+++ /dev/null
-/*
- * 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 (
- <section className="boxed-group">
- <h2>{translate('my_profile.overall_notifications.title')}</h2>
-
- <div className="boxed-group-inner">
- <table className="form">
- <thead>
- <tr>
- <th />
- {props.channels.map(channel => (
- <th key={channel} className="text-center">
- <h4>{translate('notification.channel', channel)}</h4>
- </th>
- ))}
- </tr>
- </thead>
-
- <NotificationsList
- notifications={props.notifications}
- channels={props.channels}
- types={props.types}
- checkboxId={(d, c) => `global-notification-${d}-${c}`}
- onAdd={props.addNotification}
- onRemove={props.removeNotification}
- />
- </table>
- </div>
- </section>
- );
-}
-
-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;
--- /dev/null
+/*
+ * 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 (
+ <section className="boxed-group">
+ <h2>{translate('my_profile.overall_notifications.title')}</h2>
+
+ <div className="boxed-group-inner">
+ <table className="form">
+ <thead>
+ <tr>
+ <th />
+ {props.channels.map(channel => (
+ <th className="text-center" key={channel}>
+ <h4>{translate('notification.channel', channel)}</h4>
+ </th>
+ ))}
+ </tr>
+ </thead>
+
+ <NotificationsList
+ channels={props.channels}
+ checkboxId={getCheckboxId}
+ notifications={props.notifications}
+ onAdd={props.addNotification}
+ onRemove={props.removeNotification}
+ types={props.types}
+ />
+ </table>
+ </div>
+ </section>
+ );
+}
+
+function getCheckboxId(type: string, channel: string) {
+ return `global-notification-${type}-${channel}`;
+}
+++ /dev/null
-/*
- * 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;
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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<Props, 'fetchOrganizations'>;
+
+export default connect(null, mapDispatchToProps)(Notifications);
+++ /dev/null
-/*
- * 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 (
- <tbody>
- {types.map(type => (
- <tr key={type}>
- <td>{this.getDispatcherLabel(type)}</td>
- {channels.map(channel => (
- <td key={channel} className="text-center">
- <Checkbox
- checked={this.isEnabled(type, channel)}
- id={checkboxId(type, channel)}
- onCheck={checked => this.handleCheck(type, channel, checked)}
- />
- </td>
- ))}
- </tr>
- ))}
- </tbody>
- );
- }
-}
--- /dev/null
+/*
+ * 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<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) {
+ 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 (
+ <tbody>
+ {types.map(type => (
+ <tr key={type}>
+ <td>{this.getDispatcherLabel(type)}</td>
+ {channels.map(channel => (
+ <td className="text-center" key={channel}>
+ <Checkbox
+ checked={this.isEnabled(type, channel)}
+ id={checkboxId(type, channel)}
+ onCheck={checked => this.handleCheck(type, channel, checked)}
+ />
+ </td>
+ ))}
+ </tr>
+ ))}
+ </tbody>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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 (
- <table key={project.key} className="form big-spacer-bottom">
- <thead>
- <tr>
- <th>
- <span className="text-normal">
- <Organization organizationKey={project.organization} />
- </span>
- <h4 className="display-inline-block">
- <Link to={getProjectUrl(project.key)}>{project.name}</Link>
- </h4>
- </th>
- {channels.map(channel => (
- <th key={channel} className="text-center">
- <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)}
- project={true}
- />
- </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;
--- /dev/null
+/*
+ * 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<Props> {
+ 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 (
+ <table className="form big-spacer-bottom" key={project.key}>
+ <thead>
+ <tr>
+ <th>
+ <span className="text-normal">
+ <Organization organizationKey={project.organization} />
+ </span>
+ <h4 className="display-inline-block">
+ <Link to={getProjectUrl(project.key)}>{project.name}</Link>
+ </h4>
+ </th>
+ {channels.map(channel => (
+ <th className="text-center" key={channel}>
+ <h4>{translate('notification.channel', channel)}</h4>
+ </th>
+ ))}
+ </tr>
+ </thead>
+ <NotificationsList
+ channels={this.props.channels}
+ checkboxId={this.getCheckboxId}
+ notifications={this.props.notifications}
+ onAdd={this.handleAddNotification}
+ onRemove={this.handleRemoveNotification}
+ project={true}
+ types={this.props.types}
+ />
+ </table>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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 (
- <span>
- <Organization organizationKey={option.organization} link={false} />
- <strong>{option.label}</strong>
- </span>
- );
- };
-
- render() {
- const allProjects = [...this.props.projects, ...this.state.addedProjects];
-
- return (
- <section className="boxed-group">
- <h2>{translate('my_profile.per_project_notifications.title')}</h2>
-
- <div className="boxed-group-inner">
- {allProjects.length === 0 && (
- <div className="note">{translate('my_account.no_project_notifications')}</div>
- )}
-
- {allProjects.map(project => <ProjectNotifications key={project.key} project={project} />)}
-
- <div className="spacer-top panel bg-muted">
- <span className="text-middle spacer-right">
- {translate('my_account.set_notifications_for')}:
- </span>
- <AsyncSelect
- autoload={false}
- cache={false}
- name="new_project"
- style={{ width: '300px' }}
- loadOptions={this.loadOptions}
- minimumInput={2}
- optionRenderer={this.renderOption}
- onChange={this.handleAddProject}
- placeholder={translate('my_account.search_project')}
- />
- </div>
- </div>
- </section>
- );
- }
-}
-
-const mapStateToProps = state => ({
- projects: getProjectsWithNotifications(state)
-});
-
-export default connect(mapStateToProps)(Projects);
-
-export const UnconnectedProjects = Projects;
--- /dev/null
+/*
+ * 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<Props, State> {
+ 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 (
+ <span>
+ <Organization link={false} organizationKey={option.organization} />
+ <strong>{option.label}</strong>
+ </span>
+ );
+ };
+
+ render() {
+ const allProjects = [...this.props.projects, ...this.state.addedProjects];
+
+ return (
+ <section className="boxed-group">
+ <h2>{translate('my_profile.per_project_notifications.title')}</h2>
+
+ <div className="boxed-group-inner">
+ {allProjects.length === 0 && (
+ <div className="note">{translate('my_account.no_project_notifications')}</div>
+ )}
+
+ {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">
+ {translate('my_account.set_notifications_for')}:
+ </span>
+ <AsyncSelect
+ autoload={false}
+ cache={false}
+ className="input-super-large"
+ loadOptions={this.loadOptions}
+ minimumInput={2}
+ name="new_project"
+ onChange={this.handleAddProject}
+ optionRenderer={this.renderOption}
+ placeholder={translate('my_account.search_project')}
+ />
+ </div>
+ </div>
+ </section>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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(
- <UnconnectedGlobalNotifications
- notifications={notifications}
- channels={channels}
- types={types}
- addNotification={jest.fn()}
- removeNotification={jest.fn()}
- />
- )
- ).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(
+ <GlobalNotifications
+ addNotification={jest.fn()}
+ channels={channels}
+ notifications={notifications}
+ removeNotification={jest.fn()}
+ types={types}
+ />
+ )
+ ).toMatchSnapshot();
+});
+++ /dev/null
-/*
- * 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(<UnconnectedNotifications fetchNotifications={jest.fn()} />)).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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;
+}
+++ /dev/null
-/*
- * 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(
- <NotificationsList
- onAdd={jest.fn()}
- onRemove={jest.fn()}
- channels={channels}
- checkboxId={checkboxId}
- types={types}
- notifications={notifications}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('renders project-specific labels', () => {
- hasMessage.mockImplementation(() => true);
- expect(
- shallow(
- <NotificationsList
- onAdd={jest.fn()}
- onRemove={jest.fn()}
- channels={channels}
- checkboxId={checkboxId}
- project={true}
- types={types}
- notifications={notifications}
- />
- )
- ).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(
- <NotificationsList
- onAdd={onAdd}
- onRemove={onRemove}
- channels={channels}
- checkboxId={checkboxId}
- types={types}
- notifications={notifications}
- />
- );
- 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' });
-});
--- /dev/null
+/*
+ * 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<any>).mockImplementation(() => false).mockClear();
+});
+
+it('should match snapshot', () => {
+ expect(
+ shallow(
+ <NotificationsList
+ channels={channels}
+ checkboxId={checkboxId}
+ notifications={notifications}
+ onAdd={jest.fn()}
+ onRemove={jest.fn()}
+ types={types}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders project-specific labels', () => {
+ (hasMessage as jest.Mock<any>).mockImplementation(() => true);
+ expect(
+ shallow(
+ <NotificationsList
+ channels={channels}
+ checkboxId={checkboxId}
+ notifications={notifications}
+ onAdd={jest.fn()}
+ onRemove={jest.fn()}
+ project={true}
+ types={types}
+ />
+ )
+ ).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(
+ <NotificationsList
+ channels={channels}
+ checkboxId={checkboxId}
+ notifications={notifications}
+ onAdd={onAdd}
+ onRemove={onRemove}
+ types={types}
+ />
+ );
+ 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' });
+});
+++ /dev/null
-/*
- * 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(
- <UnconnectedProjectNotifications
- project={{ key: 'foo', name: 'Foo' }}
- notifications={notifications}
- channels={channels}
- types={types}
- addNotification={jest.fn()}
- removeNotification={jest.fn()}
- />
- )
- ).toMatchSnapshot();
-});
-
-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}
- addNotification={addNotification}
- removeNotification={removeNotification}
- />
- );
- 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'
- });
-});
--- /dev/null
+/*
+ * 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(
+ <ProjectNotifications
+ addNotification={jest.fn()}
+ channels={channels}
+ notifications={notifications}
+ project={{ key: 'foo', name: 'Foo', organization: 'org' }}
+ removeNotification={jest.fn()}
+ types={types}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('should call `addNotification` and `removeNotification`', () => {
+ const addNotification = jest.fn();
+ const removeNotification = jest.fn();
+ const wrapper = shallow(
+ <ProjectNotifications
+ addNotification={addNotification}
+ channels={channels}
+ notifications={notifications}
+ project={{ key: 'foo', name: 'Foo', organization: 'org' }}
+ removeNotification={removeNotification}
+ types={types}
+ />
+ );
+ const notificationsList = wrapper.find('NotificationsList');
+
+ notificationsList.prop<Function>('onAdd')({ channel: 'channel2', type: 'type1' });
+ expect(addNotification).toHaveBeenCalledWith({
+ channel: 'channel2',
+ organization: 'org',
+ project: 'foo',
+ projectName: 'Foo',
+ type: 'type1'
+ });
+
+ jest.resetAllMocks();
+
+ notificationsList.prop<Function>('onRemove')({ channel: 'channel1', type: 'type1' });
+ expect(removeNotification).toHaveBeenCalledWith({
+ channel: 'channel1',
+ type: 'type1',
+ project: 'foo'
+ });
+});
+++ /dev/null
-/*
- * 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(<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();
-});
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should match snapshot 1`] = `
-<section
- className="boxed-group"
->
- <h2>
- my_profile.overall_notifications.title
- </h2>
- <div
- className="boxed-group-inner"
- >
- <table
- className="form"
- >
- <thead>
- <tr>
- <th />
- <th
- className="text-center"
- key="channel1"
- >
- <h4>
- notification.channel.channel1
- </h4>
- </th>
- <th
- className="text-center"
- key="channel2"
- >
- <h4>
- notification.channel.channel2
- </h4>
- </th>
- </tr>
- </thead>
- <NotificationsList
- channels={
- Array [
- "channel1",
- "channel2",
- ]
- }
- checkboxId={[Function]}
- notifications={
- Array [
- Object {
- "channel": "channel1",
- "type": "type1",
- },
- Object {
- "channel": "channel1",
- "type": "type2",
- },
- Object {
- "channel": "channel2",
- "type": "type2",
- },
- ]
- }
- onAdd={[MockFunction]}
- onRemove={[MockFunction]}
- types={
- Array [
- "type1",
- "type2",
- ]
- }
- />
- </table>
- </div>
-</section>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should match snapshot 1`] = `
+<section
+ className="boxed-group"
+>
+ <h2>
+ my_profile.overall_notifications.title
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <table
+ className="form"
+ >
+ <thead>
+ <tr>
+ <th />
+ <th
+ className="text-center"
+ key="channel1"
+ >
+ <h4>
+ notification.channel.channel1
+ </h4>
+ </th>
+ <th
+ className="text-center"
+ key="channel2"
+ >
+ <h4>
+ notification.channel.channel2
+ </h4>
+ </th>
+ </tr>
+ </thead>
+ <NotificationsList
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ checkboxId={[Function]}
+ notifications={
+ Array [
+ Object {
+ "channel": "channel1",
+ "type": "type1",
+ },
+ Object {
+ "channel": "channel1",
+ "type": "type2",
+ },
+ Object {
+ "channel": "channel2",
+ "type": "type2",
+ },
+ ]
+ }
+ onAdd={[MockFunction]}
+ onRemove={[MockFunction]}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+ </table>
+ </div>
+</section>
+`;
+++ /dev/null
-// 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>
-`;
--- /dev/null
+// 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>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders project-specific labels 1`] = `
-<tbody>
- <tr
- key="type1"
- >
- <td>
- notification.dispatcher.type1.project
- </td>
- <td
- className="text-center"
- key="channel1"
- >
- <Checkbox
- checked={true}
- id="checkbox-io-type1-channel1"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- <td
- className="text-center"
- key="channel2"
- >
- <Checkbox
- checked={false}
- id="checkbox-io-type1-channel2"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- </tr>
- <tr
- key="type2"
- >
- <td>
- notification.dispatcher.type2.project
- </td>
- <td
- className="text-center"
- key="channel1"
- >
- <Checkbox
- checked={true}
- id="checkbox-io-type2-channel1"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- <td
- className="text-center"
- key="channel2"
- >
- <Checkbox
- checked={true}
- id="checkbox-io-type2-channel2"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- </tr>
-</tbody>
-`;
-
-exports[`should match snapshot 1`] = `
-<tbody>
- <tr
- key="type1"
- >
- <td>
- notification.dispatcher.type1
- </td>
- <td
- className="text-center"
- key="channel1"
- >
- <Checkbox
- checked={true}
- id="checkbox-io-type1-channel1"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- <td
- className="text-center"
- key="channel2"
- >
- <Checkbox
- checked={false}
- id="checkbox-io-type1-channel2"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- </tr>
- <tr
- key="type2"
- >
- <td>
- notification.dispatcher.type2
- </td>
- <td
- className="text-center"
- key="channel1"
- >
- <Checkbox
- checked={true}
- id="checkbox-io-type2-channel1"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- <td
- className="text-center"
- key="channel2"
- >
- <Checkbox
- checked={true}
- id="checkbox-io-type2-channel2"
- onCheck={[Function]}
- thirdState={false}
- />
- </td>
- </tr>
-</tbody>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders project-specific labels 1`] = `
+<tbody>
+ <tr
+ key="type1"
+ >
+ <td>
+ notification.dispatcher.type1.project
+ </td>
+ <td
+ className="text-center"
+ key="channel1"
+ >
+ <Checkbox
+ checked={true}
+ id="checkbox-io-type1-channel1"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ <td
+ className="text-center"
+ key="channel2"
+ >
+ <Checkbox
+ checked={false}
+ id="checkbox-io-type1-channel2"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ </tr>
+ <tr
+ key="type2"
+ >
+ <td>
+ notification.dispatcher.type2.project
+ </td>
+ <td
+ className="text-center"
+ key="channel1"
+ >
+ <Checkbox
+ checked={true}
+ id="checkbox-io-type2-channel1"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ <td
+ className="text-center"
+ key="channel2"
+ >
+ <Checkbox
+ checked={true}
+ id="checkbox-io-type2-channel2"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ </tr>
+</tbody>
+`;
+
+exports[`should match snapshot 1`] = `
+<tbody>
+ <tr
+ key="type1"
+ >
+ <td>
+ notification.dispatcher.type1
+ </td>
+ <td
+ className="text-center"
+ key="channel1"
+ >
+ <Checkbox
+ checked={true}
+ id="checkbox-io-type1-channel1"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ <td
+ className="text-center"
+ key="channel2"
+ >
+ <Checkbox
+ checked={false}
+ id="checkbox-io-type1-channel2"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ </tr>
+ <tr
+ key="type2"
+ >
+ <td>
+ notification.dispatcher.type2
+ </td>
+ <td
+ className="text-center"
+ key="channel1"
+ >
+ <Checkbox
+ checked={true}
+ id="checkbox-io-type2-channel1"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ <td
+ className="text-center"
+ key="channel2"
+ >
+ <Checkbox
+ checked={true}
+ id="checkbox-io-type2-channel2"
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ </tr>
+</tbody>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should match snapshot 1`] = `
-<table
- className="form big-spacer-bottom"
- key="foo"
->
- <thead>
- <tr>
- <th>
- <span
- className="text-normal"
- >
- <Connect(Organization) />
- </span>
- <h4
- className="display-inline-block"
- >
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "id": "foo",
- },
- }
- }
- >
- Foo
- </Link>
- </h4>
- </th>
- <th
- className="text-center"
- key="channel1"
- >
- <h4>
- notification.channel.channel1
- </h4>
- </th>
- <th
- className="text-center"
- key="channel2"
- >
- <h4>
- notification.channel.channel2
- </h4>
- </th>
- </tr>
- </thead>
- <NotificationsList
- channels={
- Array [
- "channel1",
- "channel2",
- ]
- }
- checkboxId={[Function]}
- notifications={
- Array [
- Object {
- "channel": "channel1",
- "type": "type1",
- },
- Object {
- "channel": "channel1",
- "type": "type2",
- },
- Object {
- "channel": "channel2",
- "type": "type2",
- },
- ]
- }
- onAdd={[Function]}
- onRemove={[Function]}
- project={true}
- types={
- Array [
- "type1",
- "type2",
- ]
- }
- />
-</table>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should match snapshot 1`] = `
+<table
+ className="form big-spacer-bottom"
+ key="foo"
+>
+ <thead>
+ <tr>
+ <th>
+ <span
+ className="text-normal"
+ >
+ <Connect(Organization)
+ organizationKey="org"
+ />
+ </span>
+ <h4
+ className="display-inline-block"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ Foo
+ </Link>
+ </h4>
+ </th>
+ <th
+ className="text-center"
+ key="channel1"
+ >
+ <h4>
+ notification.channel.channel1
+ </h4>
+ </th>
+ <th
+ className="text-center"
+ key="channel2"
+ >
+ <h4>
+ notification.channel.channel2
+ </h4>
+ </th>
+ </tr>
+ </thead>
+ <NotificationsList
+ channels={
+ Array [
+ "channel1",
+ "channel2",
+ ]
+ }
+ checkboxId={[Function]}
+ notifications={
+ Array [
+ Object {
+ "channel": "channel1",
+ "type": "type1",
+ },
+ Object {
+ "channel": "channel1",
+ "type": "type2",
+ },
+ Object {
+ "channel": "channel2",
+ "type": "type2",
+ },
+ ]
+ }
+ onAdd={[Function]}
+ onRemove={[Function]}
+ project={true}
+ types={
+ Array [
+ "type1",
+ "type2",
+ ]
+ }
+ />
+</table>
+`;
+++ /dev/null
-// 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 [],
-}
-`;
--- /dev/null
+// 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 [],
+}
+`;
+++ /dev/null
-/*
- * 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));
--- /dev/null
+/*
+ * 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;
+}
},
{
path: 'notifications',
- component: lazyLoad(() => import('./notifications/Notifications'))
+ component: lazyLoad(() => import('./notifications/NotificationsContainer'))
},
{
path: 'organizations',
}
// 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} />;
}
+++ /dev/null
-/*
- * 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;
-}
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';
languages,
marketplace,
metrics,
- notifications,
organizations,
organizationsMembers,
users,
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);