*/
import { Heading } from '@sonarsource/echoes-react';
-import { Table } from 'design-system';
import * as React from 'react';
+import NotificationsList from '../../../components/notifications/NotificationsList';
import { translate } from '../../../helpers/l10n';
-import { Notification, NotificationGlobalType } from '../../../types/notifications';
-import NotificationsList from './NotificationsList';
-interface Props {
- addNotification: (n: Notification) => void;
- channels: string[];
- header?: React.JSX.Element;
- notifications: Notification[];
- removeNotification: (n: Notification) => void;
- types: NotificationGlobalType[];
-}
-
-export default function GlobalNotifications(props: Readonly<Props>) {
+export default function GlobalNotifications() {
return (
<>
<Heading as="h2" hasMarginBottom>
{translate('my_profile.overall_notifications.title')}
</Heading>
- {!props.header && (
- <div className="sw-typo-semibold sw-mb-2">{translate('notifications.send_email')}</div>
- )}
-
- <Table className="sw-w-full" columnCount={2} header={props.header ?? null}>
- <NotificationsList
- channels={props.channels}
- checkboxId={getCheckboxId}
- notifications={props.notifications}
- onAdd={props.addNotification}
- onRemove={props.removeNotification}
- types={props.types}
- />
- </Table>
+ <NotificationsList />
</>
);
}
-
-function getCheckboxId(type: string, channel: string) {
- return `global-notification-${type}-${channel}`;
-}
import { Heading, Spinner } from '@sonarsource/echoes-react';
import { FlagMessage, GreySeparator } from 'design-system';
-import { isEmpty, partition } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
-import {
- WithNotificationsProps,
- withNotifications,
-} from '../../../components/hoc/withNotifications';
import { translate } from '../../../helpers/l10n';
+import { useNotificationsQuery } from '../../../queries/notifications';
import GlobalNotifications from './GlobalNotifications';
import Projects from './Projects';
-export function Notifications({
- addNotification,
- channels,
- globalTypes,
- loading,
- notifications,
- perProjectTypes,
- removeNotification,
-}: WithNotificationsProps) {
- const [globalNotifications, projectNotifications] = partition(notifications, (n) =>
- isEmpty(n.project),
- );
-
- const emailOnly = channels.length === 1 && channels[0] === 'EmailNotificationChannel';
+export default function Notifications() {
+ const { data: notificationResponse, isLoading } = useNotificationsQuery();
+ const { notifications } = notificationResponse || {
+ channels: [],
+ globalTypes: [],
+ perProjectTypes: [],
+ notifications: [],
+ };
- const header = emailOnly ? undefined : (
- <tr>
- <th className="sw-typo-semibold">{translate('events')}</th>
-
- {channels.map((channel) => (
- <th className="sw-typo-semibold sw-text-right" key={channel}>
- {translate('notification.channel', channel)}
- </th>
- ))}
- </tr>
- );
+ const projectNotifications = notifications.filter((n) => n.project !== undefined);
return (
<div className="it__account-body">
{translate('notification.dispatcher.information')}
</FlagMessage>
- <Spinner isLoading={loading}>
+ <Spinner isLoading={isLoading}>
{notifications && (
<>
<GreySeparator className="sw-mb-4 sw-mt-6" />
- <GlobalNotifications
- addNotification={addNotification}
- channels={channels}
- header={header}
- notifications={globalNotifications}
- removeNotification={removeNotification}
- types={globalTypes}
- />
+ <GlobalNotifications />
<GreySeparator className="sw-mb-4 sw-mt-6" />
- <Projects
- addNotification={addNotification}
- channels={channels}
- header={header}
- notifications={projectNotifications}
- removeNotification={removeNotification}
- types={perProjectTypes}
- />
+ <Projects notifications={projectNotifications} />
</>
)}
</Spinner>
</div>
);
}
-
-export default withNotifications(Notifications);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { Checkbox } from '@sonarsource/echoes-react';
-import { CellComponent, TableRowInteractive } from 'design-system';
-import * as React from 'react';
-import { hasMessage, translate, translateWithParameters } from '../../../helpers/l10n';
-import {
- Notification,
- NotificationGlobalType,
- NotificationProjectType,
-} from '../../../types/notifications';
-
-interface Props {
- channels: string[];
- checkboxId: (type: string, channel: string) => string;
- notifications: Notification[];
- onAdd: (n: Notification) => void;
- onRemove: (n: Notification) => void;
- project?: boolean;
- types: (NotificationGlobalType | NotificationProjectType)[];
-}
-
-export default function NotificationsList({
- channels,
- checkboxId,
- notifications,
- onAdd,
- onRemove,
- project,
- types,
-}: Readonly<Props>) {
- const isEnabled = (type: string, channel: string) =>
- !!notifications.find(
- (notification) => notification.type === type && notification.channel === channel,
- );
-
- const handleCheck = (type: string, channel: string, checked: boolean) => {
- if (checked) {
- onAdd({ type, channel });
- } else {
- onRemove({ type, channel });
- }
- };
-
- const getDispatcherLabel = (dispatcher: string) => {
- const globalMessageKey = ['notification.dispatcher', dispatcher];
- const projectMessageKey = [...globalMessageKey, 'project'];
- const shouldUseProjectMessage = project && hasMessage(...projectMessageKey);
-
- return shouldUseProjectMessage
- ? translate(...projectMessageKey)
- : translate(...globalMessageKey);
- };
-
- return types.map((type) => (
- <TableRowInteractive className="sw-h-9" key={type}>
- <CellComponent className="sw-py-0 sw-border-0">{getDispatcherLabel(type)}</CellComponent>
-
- {channels.map((channel) => (
- <CellComponent className="sw-py-0 sw-border-0" key={channel}>
- <div className="sw-justify-end sw-flex sw-items-center">
- <Checkbox
- ariaLabel={translateWithParameters(
- 'notification.dispatcher.description_x',
- getDispatcherLabel(type),
- )}
- checked={isEnabled(type, channel)}
- id={checkboxId(type, channel)}
- onCheck={(checked) => handleCheck(type, channel, checked as boolean)}
- />
- </div>
- </CellComponent>
- ))}
- </TableRowInteractive>
- ));
-}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
-import { Link, Table } from 'design-system';
+import { Link } from 'design-system';
import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
+import NotificationsList from '../../../components/notifications/NotificationsList';
import { getProjectUrl } from '../../../helpers/urls';
-import {
- Notification,
- NotificationProject,
- NotificationProjectType,
-} from '../../../types/notifications';
-import NotificationsList from './NotificationsList';
+import { NotificationProject } from '../../../types/notifications';
interface Props {
- addNotification: (n: Notification) => void;
- channels: string[];
- header?: React.JSX.Element;
- notifications: Notification[];
project: NotificationProject;
- removeNotification: (n: Notification) => void;
- types: NotificationProjectType[];
}
-export default function ProjectNotifications({
- addNotification,
- channels,
- header,
- notifications,
- project,
- removeNotification,
- types,
-}: Readonly<Props>) {
- const getCheckboxId = (type: string, channel: string) => {
- return `project-notification-${project.project}-${type}-${channel}`;
- };
-
- const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
- addNotification({ ...project, channel, type });
- };
-
- const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
- removeNotification({
- ...project,
- channel,
- type,
- });
- };
-
+export default function ProjectNotifications({ project }: Readonly<Props>) {
return (
<div className="sw-my-6">
<div className="sw-mb-4">
<Link to={getProjectUrl(project.project)}>{project.projectName}</Link>
</div>
- {!header && (
- <div className="sw-typo-semibold sw-mb-2">{translate('notifications.send_email')}</div>
- )}
- <Table
- className={classNames('sw-w-full', { 'sw-mt-4': header })}
- columnCount={2}
- header={header ?? null}
- >
- <NotificationsList
- channels={channels}
- checkboxId={getCheckboxId}
- notifications={notifications}
- onAdd={handleAddNotification}
- onRemove={handleRemoveNotification}
- project
- types={types}
- />
- </Table>
+ <NotificationsList project={project.project} />
</div>
);
}
import { Button, ButtonVariety, Heading } from '@sonarsource/echoes-react';
import { InputSearch, Note } from 'design-system';
-import { groupBy, sortBy, uniqBy } from 'lodash';
+import { sortBy, uniqBy } from 'lodash';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import {
- Notification,
- NotificationProject,
- NotificationProjectType,
-} from '../../../types/notifications';
+import { Notification, NotificationProject } from '../../../types/notifications';
import ProjectModal from './ProjectModal';
import ProjectNotifications from './ProjectNotifications';
export interface Props {
- addNotification: (n: Notification) => void;
- channels: string[];
- header?: React.JSX.Element;
notifications: Notification[];
- removeNotification: (n: Notification) => void;
- types: NotificationProjectType[];
}
interface State {
this.setState({ showModal: true });
};
- removeNotification = (removed: Notification, allProjects: NotificationProject[]) => {
- const projectToRemove = allProjects.find((p) => p.project === removed.project);
-
- if (projectToRemove) {
- this.handleAddProject(projectToRemove);
- }
-
- this.props.removeNotification(removed);
- };
-
render() {
const { notifications } = this.props;
const { addedProjects, search } = this.state;
isNotificationProject,
) as NotificationProject[];
- const notificationsByProject = groupBy(notifications, (n) => n.project);
const allProjects = uniqBy([...addedProjects, ...projects], (project) => project.project);
const filteredProjects = sortBy(allProjects, 'projectName').filter((p) =>
)}
{filteredProjects.map((project) => (
- <ProjectNotifications
- addNotification={this.props.addNotification}
- channels={this.props.channels}
- header={this.props.header}
- key={project.project}
- notifications={notificationsByProject[project.project] || []}
- project={project}
- removeNotification={(n) => this.removeNotification(n, allProjects)}
- types={this.props.types}
- />
+ <ProjectNotifications key={project.project} project={project} />
))}
</div>
</section>
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Spinner } from '@sonarsource/echoes-react';
-import { Checkbox, FlagMessage, SubTitle } from 'design-system';
+import { FlagMessage, SubTitle } from 'design-system';
import * as React from 'react';
-import {
- WithNotificationsProps,
- withNotifications,
-} from '../../../components/hoc/withNotifications';
-import { hasMessage, translate, translateWithParameters } from '../../../helpers/l10n';
-import { NotificationProjectType } from '../../../types/notifications';
+import NotificationsList from '../../../components/notifications/NotificationsList';
+import { translate } from '../../../helpers/l10n';
import { Component } from '../../../types/types';
interface Props {
component: Component;
}
-export function ProjectNotifications(props: WithNotificationsProps & Props) {
- const { channels, component, loading, notifications, perProjectTypes } = props;
-
- const handleCheck = (type: NotificationProjectType, channel: string, checked: boolean) => {
- if (checked) {
- props.addNotification({ project: component.key, channel, type });
- } else {
- props.removeNotification({
- project: component.key,
- channel,
- type,
- });
- }
- };
-
- const getCheckboxId = (type: string, channel: string) => {
- return `project-notification-${component.key}-${type}-${channel}`;
- };
-
- const getDispatcherLabel = (dispatcher: string) => {
- const globalMessageKey = ['notification.dispatcher', dispatcher];
- const projectMessageKey = [...globalMessageKey, 'project'];
- const shouldUseProjectMessage = hasMessage(...projectMessageKey);
- return shouldUseProjectMessage
- ? translate(...projectMessageKey)
- : translate(...globalMessageKey);
- };
-
- const isEnabled = (type: string, channel: string) => {
- return !!notifications.find(
- (notification) =>
- notification.type === type &&
- notification.channel === channel &&
- notification.project === component.key,
- );
- };
-
- const emailChannel = channels[0];
+export default function ProjectNotifications(props: Props) {
+ const { component } = props;
return (
<form aria-labelledby="notifications-update-title">
{translate('notification.dispatcher.information')}
</FlagMessage>
- <Spinner className="sw-mt-6" isLoading={loading}>
- <h3 id="notifications-update-title" className="sw-mt-6">
- {translate('notifications.send_email')}
- </h3>
- <ul className="sw-list-none sw-mt-4 sw-pl-0">
- {perProjectTypes.map((type) => (
- <li className="sw-pl-0 sw-p-2" key={type}>
- <Checkbox
- right
- className="sw-flex sw-justify-between"
- label={translateWithParameters(
- 'notification.dispatcher.description_x',
- getDispatcherLabel(type),
- )}
- checked={isEnabled(type, emailChannel)}
- id={getCheckboxId(type, emailChannel)}
- onCheck={(checked: boolean) => handleCheck(type, emailChannel, checked)}
- >
- {getDispatcherLabel(type)}
- </Checkbox>
- </li>
- ))}
- </ul>
- </Spinner>
+ <NotificationsList className="sw-mt-6" project={component.key} />
</form>
);
}
-
-export default withNotifications(ProjectNotifications);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import { getNotifications } from '../../../../api/notifications';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockNotification } from '../../../../helpers/testMocks';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { NotificationGlobalType, NotificationProjectType } from '../../../../types/notifications';
-import ProjectNotifications from '../ProjectNotifications';
-
-jest.mock('../../../../api/notifications', () => ({
- addNotification: jest.fn().mockResolvedValue(undefined),
- removeNotification: jest.fn().mockResolvedValue(undefined),
- getNotifications: jest.fn(),
-}));
-
-beforeAll(() => {
- jest.mocked(getNotifications).mockResolvedValue({
- channels: ['channel1'],
- globalTypes: [NotificationGlobalType.MyNewIssues],
- notifications: [
- mockNotification({}),
- mockNotification({ type: NotificationProjectType.NewAlerts }),
- ],
- perProjectTypes: [NotificationProjectType.NewAlerts, NotificationProjectType.NewIssues],
- });
-});
-
-it('should render correctly', async () => {
- const user = userEvent.setup();
- renderProjectNotifications();
-
- expect(await screen.findByText('notifications.send_email')).toBeInTheDocument();
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.description_x.notification.dispatcher.NewAlerts.project',
- ),
- ).toBeChecked();
-
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.description_x.notification.dispatcher.NewIssues.project',
- ),
- ).not.toBeChecked();
-
- // Toggle New Alerts
- await user.click(
- screen.getByLabelText(
- 'notification.dispatcher.description_x.notification.dispatcher.NewAlerts.project',
- ),
- );
-
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.description_x.notification.dispatcher.NewAlerts.project',
- ),
- ).not.toBeChecked();
-
- // Toggle New Issues
- await user.click(
- screen.getByLabelText(
- 'notification.dispatcher.description_x.notification.dispatcher.NewIssues.project',
- ),
- );
-
- expect(
- screen.getByLabelText(
- 'notification.dispatcher.description_x.notification.dispatcher.NewIssues.project',
- ),
- ).toBeChecked();
-});
-
-function renderProjectNotifications() {
- return renderComponent(
- <ProjectNotifications component={mockComponent({ key: 'foo', name: 'Foo' })} />,
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 { getWrappedDisplayName } from '~sonar-aligned/components/hoc/utils';
-import { addNotification, getNotifications, removeNotification } from '../../api/notifications';
-import {
- Notification,
- NotificationGlobalType,
- NotificationProjectType,
-} from '../../types/notifications';
-
-interface State {
- channels: string[];
- globalTypes: NotificationGlobalType[];
- loading: boolean;
- notifications: Notification[];
- perProjectTypes: NotificationProjectType[];
-}
-
-export interface WithNotificationsProps {
- addNotification: (added: Notification) => void;
- channels: string[];
- globalTypes: NotificationGlobalType[];
- loading: boolean;
- notifications: Notification[];
- perProjectTypes: NotificationProjectType[];
- removeNotification: (removed: Notification) => void;
-}
-
-export function withNotifications<P>(
- WrappedComponent: React.ComponentType<React.PropsWithChildren<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: Notification) => {
- this.setState((state) => {
- const notifications = uniqWith([...state.notifications, added], this.areNotificationsEqual);
- return { notifications };
- });
- };
-
- removeNotificationFromState = (removed: Notification) => {
- this.setState((state) => {
- const notifications = state.notifications.filter(
- (notification) => !this.areNotificationsEqual(notification, removed),
- );
- return { notifications };
- });
- };
-
- 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 };
- 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 };
- removeNotification(data).catch(() => {
- this.addNotificationToState(removed);
- });
- };
-
- areNotificationsEqual = (a: Notification, b: 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;
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { Checkbox, Spinner } from '@sonarsource/echoes-react';
+import classNames from 'classnames';
+import { CellComponent, Table, TableRowInteractive } from 'design-system';
+import * as React from 'react';
+import { useCallback } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { hasMessage, translate, translateWithParameters } from '../../helpers/l10n';
+import {
+ useAddNotificationMutation,
+ useNotificationsQuery,
+ useRemoveNotificationMutation,
+} from '../../queries/notifications';
+
+interface Props {
+ className?: string;
+ project?: string;
+}
+
+function getDispatcherLabel(dispatcher: string, project?: string) {
+ const globalMessageKey = ['notification.dispatcher', dispatcher];
+ const projectMessageKey = [...globalMessageKey, 'project'];
+ const shouldUseProjectMessage = project !== undefined && hasMessage(...projectMessageKey);
+
+ return shouldUseProjectMessage ? translate(...projectMessageKey) : translate(...globalMessageKey);
+}
+
+export default function NotificationsList({ project, className = '' }: Readonly<Props>) {
+ const { data, isLoading } = useNotificationsQuery();
+ const { mutate: add, isPending: isPendingAdd } = useAddNotificationMutation();
+ const { mutate: remove, isPending: isPendingRemove } = useRemoveNotificationMutation();
+ const types = (project ? data?.perProjectTypes : data?.globalTypes) || [];
+ const channels = data?.channels || [];
+
+ const checkboxId = useCallback(
+ (type: string, channel: string) => {
+ return project === undefined
+ ? `global-notification-${type}-${channel}`
+ : `project-notification-${project}-${type}-${channel}`;
+ },
+ [project],
+ );
+
+ const isEnabled = useCallback(
+ (type: string, channel: string) =>
+ !!data?.notifications.find(
+ (notification) =>
+ notification.type === type &&
+ notification.channel === channel &&
+ notification.project === project,
+ ),
+ [data?.notifications, project],
+ );
+
+ const handleCheck = useCallback(
+ (type: string, channel: string, checked: boolean) => {
+ if (checked) {
+ add({ type, channel, project });
+ } else {
+ remove({ type, channel, project });
+ }
+ },
+ [add, project, remove],
+ );
+
+ return (
+ <Spinner isLoading={isLoading}>
+ <Table
+ className={classNames('sw-w-full', className)}
+ columnCount={channels.length + 1}
+ header={
+ <tr>
+ <th className="sw-typo-semibold">{translate('notification.for')}</th>
+
+ {channels.map((channel) => (
+ <th className="sw-typo-semibold sw-text-right" key={channel}>
+ <FormattedMessage
+ id="notification.by"
+ values={{ channel: <FormattedMessage id={`notification.channel.${channel}`} /> }}
+ />
+ </th>
+ ))}
+ </tr>
+ }
+ >
+ {types.map((type) => (
+ <TableRowInteractive className="sw-h-9" key={type}>
+ <CellComponent className="sw-py-0 sw-border-0">
+ {getDispatcherLabel(type, project)}
+ </CellComponent>
+
+ {channels.map((channel) => (
+ <CellComponent className="sw-py-0 sw-border-0" key={channel}>
+ <div className="sw-justify-end sw-flex sw-items-center">
+ <Checkbox
+ ariaLabel={translateWithParameters(
+ 'notification.dispatcher.description_x',
+ getDispatcherLabel(type, project),
+ )}
+ isDisabled={isPendingRemove || isPendingAdd}
+ checked={isEnabled(type, channel)}
+ id={checkboxId(type, channel)}
+ onCheck={(checked) => handleCheck(type, channel, checked as boolean)}
+ />
+ </div>
+ </CellComponent>
+ ))}
+ </TableRowInteractive>
+ ))}
+ </Table>
+ </Spinner>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query';
+import { uniqWith } from 'lodash';
+import { addNotification, getNotifications, removeNotification } from '../api/notifications';
+import { Notification } from '../types/notifications';
+import { createQueryHook, StaleTime } from './common';
+
+const KEY_PREFIX = 'notifications';
+
+const notificationQuery = queryOptions({
+ queryKey: [KEY_PREFIX],
+ queryFn: () => getNotifications(),
+ staleTime: StaleTime.NEVER,
+});
+
+function areNotificationsEqual(a: Notification, b: Notification) {
+ return a.channel === b.channel && a.type === b.type && a.project === b.project;
+}
+
+export const useNotificationsQuery = createQueryHook(() => notificationQuery);
+
+export function useAddNotificationMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: addNotification,
+ onSuccess: (_, { channel, type, project }) => {
+ queryClient.setQueryData(notificationQuery.queryKey, (previous) => {
+ if (previous === undefined) {
+ return previous;
+ }
+ return {
+ ...previous,
+ notifications: uniqWith(
+ [...previous.notifications, { channel, type, project }],
+ areNotificationsEqual,
+ ),
+ };
+ });
+ queryClient.invalidateQueries({ queryKey: [KEY_PREFIX] });
+ },
+ });
+}
+
+export function useRemoveNotificationMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: removeNotification,
+ onSuccess: (_, removed) => {
+ queryClient.setQueryData(notificationQuery.queryKey, (previous) => {
+ if (previous === undefined) {
+ return previous;
+ }
+ return {
+ ...previous,
+ notifications: previous.notifications.filter(
+ (notification) => !areNotificationsEqual(notification, removed),
+ ),
+ };
+ });
+ queryClient.invalidateQueries({ queryKey: [KEY_PREFIX] });
+ },
+ });
+}
#
#------------------------------------------------------------------------------
notification.notification=Notification
+notification.for=Notify me for
+notification.by=by {channel}
notification.channel.EmailNotificationChannel=Email
notification.dispatcher.information=A notification is never sent to the author of the event.
notification.dispatcher.ChangesOnMyIssue=Changes in issues/hotspots assigned to me
my_account.preferences.keyboard_shortcuts=Enable Keyboard Shortcuts
my_account.preferences.keyboard_shortcuts.description=Some actions can be performed using keyboard shortcuts. If you do not want to use these shortcuts, you can disable them here (this won't disable navigation shortcuts, which include the arrows, escape, and enter keys). For a list of available keyboard shortcuts, use the question mark shortcut (hit {questionMark} on your keyboard).
-notifications.send_email=Send me an email for:
#------------------------------------------------------------------------------
#