From c283ec2e5226e2916eac4294b3e41049c5502278 Mon Sep 17 00:00:00 2001 From: David Cho-Lerat Date: Tue, 30 Jan 2024 10:00:42 +0100 Subject: [PATCH] SONAR-21482 User notifications page adopts the new UI --- .../js/apps/account/__tests__/Account-it.tsx | 34 ++-- .../notifications/GlobalNotifications.tsx | 46 ++--- .../account/notifications/Notifications.tsx | 57 ++++-- .../notifications/NotificationsList.tsx | 84 +++++---- .../account/notifications/ProjectModal.tsx | 177 ++++++++++-------- .../notifications/ProjectNotifications.tsx | 67 +++---- .../apps/account/notifications/Projects.tsx | 72 ++++--- .../js/apps/account/security/Security.tsx | 6 +- .../notifications/ProjectNotifications.tsx | 4 +- .../__tests__/ProjectNotifications-test.tsx | 16 +- .../resources/org/sonar/l10n/core.properties | 11 +- 11 files changed, 312 insertions(+), 262 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx index b9e31528a25..f39901cffa2 100644 --- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx +++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx @@ -178,10 +178,15 @@ it('should render the top menu', () => { expect(screen.getByText(name)).toBeInTheDocument(); - expect(screen.getByText('my_account.profile')).toBeInTheDocument(); - expect(screen.getByText('my_account.security')).toBeInTheDocument(); - expect(screen.getByText('my_account.notifications')).toBeInTheDocument(); - expect(screen.getByText('my_account.projects')).toBeInTheDocument(); + const topMenuNavigationItems = [ + 'my_account.profile', + 'my_account.security', + 'my_account.notifications', + 'my_account.projects', + ]; + topMenuNavigationItems.forEach((itemName) => { + expect(byRole('navigation').byRole('link', { name: itemName }).get()).toBeInTheDocument(); + }); }); describe('profile page', () => { @@ -477,10 +482,10 @@ describe('notifications page', () => { addButton: byRole('button', { name: 'my_profile.per_project_notifications.add' }), addModalButton: byRole('button', { name: 'add_verb' }), searchInput: byRole('searchbox', { name: 'search.placeholder' }), - sonarQubeProject: byRole('heading', { name: 'SonarQube' }), + sonarQubeProject: byRole('link', { name: 'SonarQube' }), checkbox: (type: NotificationProjectType) => byRole('checkbox', { - name: `notification.dispatcher.descrption_x.notification.dispatcher.${type}.project`, + name: `notification.dispatcher.description_x.notification.dispatcher.${type}.project`, }), }; @@ -489,7 +494,7 @@ describe('notifications page', () => { noNotificationForProject: byText('my_account.no_project_notifications'), checkbox: (type: NotificationGlobalType) => byRole('checkbox', { - name: `notification.dispatcher.descrption_x.notification.dispatcher.${type}`, + name: `notification.dispatcher.description_x.notification.dispatcher.${type}`, }), }; @@ -536,7 +541,8 @@ describe('notifications page', () => { await user.click(await projectUI.addButton.find()); expect(projectUI.addModalButton.get()).toBeDisabled(); - await user.type(projectUI.searchInput.get(), 'sonar'); + + await user.keyboard('sonar'); // navigate within the two results, choose the first: await user.keyboard('[ArrowDown][ArrowDown][ArrowUp][Enter]'); await user.click(projectUI.addModalButton.get()); @@ -558,10 +564,10 @@ describe('notifications page', () => { renderAccountApp(mockLoggedInUser(), notificationsPagePath); - await user.click( - await screen.findByRole('button', { name: 'my_profile.per_project_notifications.add' }), - ); - expect(screen.getByLabelText('search.placeholder', { selector: 'input' })).toBeInTheDocument(); + await user.click(await projectUI.addButton.find()); + + expect(screen.getByLabelText('my_account.set_notifications_for.title')).toBeInTheDocument(); + await user.keyboard('sonarqube'); await user.click(screen.getByText('SonarQube')); @@ -574,11 +580,11 @@ describe('notifications page', () => { await user.click(screen.getByRole('searchbox')); await user.keyboard('bla'); - expect(screen.queryByRole('heading', { name: 'SonarQube' })).not.toBeInTheDocument(); + expect(projectUI.sonarQubeProject.query()).not.toBeInTheDocument(); await user.keyboard('[Backspace>3/]'); - expect(await screen.findByRole('heading', { name: 'SonarQube' })).toBeInTheDocument(); + expect(await projectUI.sonarQubeProject.find()).toBeInTheDocument(); }); }); diff --git a/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx index 278f28fd03d..700f7f6e08a 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.tsx @@ -17,6 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { PageTitle, Table } from 'design-system'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; import { Notification, NotificationGlobalType } from '../../../types/notifications'; @@ -25,40 +27,32 @@ 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: Props) { +export default function GlobalNotifications(props: Readonly) { return ( -
-

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

+ <> + -
- - - - - {props.channels.map((channel) => ( - - ))} - - + {!props.header && ( +
{translate('notifications.send_email')}
+ )} - -
{translate('notification.notification')} -

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

-
-
-
+ + +
+ ); } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx index 86f6354b0b4..445202080da 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/Notifications.tsx @@ -17,49 +17,74 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { FlagMessage, GreySeparator, Spinner, Title } from 'design-system'; import { partition } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { - withNotifications, WithNotificationsProps, + withNotifications, } from '../../../components/hoc/withNotifications'; -import { Alert } from '../../../components/ui/Alert'; -import Spinner from '../../../components/ui/Spinner'; import { translate } from '../../../helpers/l10n'; import GlobalNotifications from './GlobalNotifications'; import Projects from './Projects'; -export function Notifications(props: WithNotificationsProps) { - const { - addNotification, - channels, - globalTypes, - loading, - notifications, - perProjectTypes, - removeNotification, - } = props; - +export function Notifications({ + addNotification, + channels, + globalTypes, + loading, + notifications, + perProjectTypes, + removeNotification, +}: Readonly) { const [globalNotifications, projectNotifications] = partition(notifications, (n) => !n.project); + const emailOnly = channels.length === 1 && channels[0] === 'EmailNotificationChannel'; + + const header = emailOnly ? undefined : ( + + {translate('events')} + + {channels.map((channel) => ( + + {translate('notification.channel', channel)} + + ))} + + ); + return ( -
+
- {translate('notification.dispatcher.information')} + + {translate('my_account.notifications')} + + + {translate('notification.dispatcher.information')} + + {notifications && ( <> + + + + + void; - onRemove: (n: Notification) => void; channels: string[]; checkboxId: (type: string, channel: string) => string; + notifications: Notification[]; + onAdd: (n: Notification) => void; + onRemove: (n: Notification) => void; project?: boolean; types: (NotificationGlobalType | NotificationProjectType)[]; - notifications: Notification[]; } -export default class NotificationsList extends React.PureComponent { - isEnabled(type: string, channel: string) { - return !!this.props.notifications.find( +export default function NotificationsList({ + channels, + checkboxId, + notifications, + onAdd, + onRemove, + project, + types, +}: Readonly) { + const isEnabled = (type: string, channel: string) => + !!notifications.find( (notification) => notification.type === type && notification.channel === channel, ); - } - handleCheck(type: string, channel: string, checked: boolean) { + const handleCheck = (type: string, channel: string, checked: boolean) => { if (checked) { - this.props.onAdd({ type, channel }); + onAdd({ type, channel }); } else { - this.props.onRemove({ type, channel }); + onRemove({ type, channel }); } - } + }; - getDispatcherLabel(dispatcher: string) { + const getDispatcherLabel = (dispatcher: string) => { const globalMessageKey = ['notification.dispatcher', dispatcher]; const projectMessageKey = [...globalMessageKey, 'project']; - const shouldUseProjectMessage = this.props.project && hasMessage(...projectMessageKey); + const shouldUseProjectMessage = project && hasMessage(...projectMessageKey); + return shouldUseProjectMessage ? translate(...projectMessageKey) : translate(...globalMessageKey); - } + }; - render() { - const { channels, checkboxId, types } = this.props; + return types.map((type) => ( + + {getDispatcherLabel(type)} - return ( - - {types.map((type) => ( - - {this.getDispatcherLabel(type)} - {channels.map((channel) => ( - - this.handleCheck(type, channel, checked)} - /> - - ))} - - ))} - - ); - } + {channels.map((channel) => ( + +
+ handleCheck(type, channel, checked)} + /> +
+
+ ))} +
+ )); } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx b/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx index 757b75a6d50..c2d7778f1c7 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/ProjectModal.tsx @@ -17,15 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; + +import { + ButtonPrimary, + DropdownMenu, + InputSearch, + ItemButton, + Modal, + Popup, + PopupPlacement, + PopupZLevel, + Spinner, +} from 'design-system'; import * as React from 'react'; import { getSuggestions } from '../../../api/components'; -import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; -import { DropdownOverlay } from '../../../components/controls/Dropdown'; -import SearchBox from '../../../components/controls/SearchBox'; -import SimpleModal from '../../../components/controls/SimpleModal'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate } from '../../../helpers/l10n'; +import { ComponentQualifier } from '../../../types/component'; import { NotificationProject } from '../../../types/notifications'; interface Props { @@ -38,7 +46,6 @@ interface State { highlighted?: NotificationProject; loading?: boolean; query?: string; - open?: boolean; selectedProject?: NotificationProject; suggestions?: NotificationProject[]; } @@ -66,10 +73,12 @@ export default class ProjectModal extends React.PureComponent { event.preventDefault(); this.handleSelectHighlighted(); break; + case KeyboardKeys.UpArrow: event.preventDefault(); this.handleHighlightPrevious(); break; + case KeyboardKeys.DownArrow: event.preventDefault(); this.handleHighlightNext(); @@ -79,6 +88,7 @@ export default class ProjectModal extends React.PureComponent { getCurrentIndex = () => { const { highlighted, suggestions } = this.state; + return highlighted && suggestions ? suggestions.findIndex((suggestion) => suggestion.project === highlighted.project) : -1; @@ -86,12 +96,14 @@ export default class ProjectModal extends React.PureComponent { highlightIndex = (index: number) => { const { suggestions } = this.state; + if (suggestions && suggestions.length > 0) { if (index < 0) { index = suggestions.length - 1; } else if (index >= suggestions.length) { index = 0; } + this.setState({ highlighted: suggestions[index], }); @@ -107,13 +119,10 @@ export default class ProjectModal extends React.PureComponent { }; handleSelectHighlighted = () => { - const { highlighted, selectedProject } = this.state; + const { highlighted } = this.state; + if (highlighted !== undefined) { - if (selectedProject !== undefined && highlighted.project === selectedProject.project) { - this.handleSubmit(); - } else { - this.handleSelect(highlighted); - } + this.handleSelect(highlighted); } }; @@ -121,16 +130,20 @@ export default class ProjectModal extends React.PureComponent { const { addedProjects } = this.props; if (query.length < 2) { - this.setState({ open: false, query }); + this.setState({ query, selectedProject: undefined, suggestions: undefined }); + return; } - this.setState({ loading: true, query }); - getSuggestions(query).then( + this.setState({ loading: true, query, selectedProject: undefined }); + + getSuggestions(query, undefined, ComponentQualifier.Project).then( (r) => { if (this.mounted) { let suggestions = undefined; - const projects = r.results.find((domain) => domain.q === 'TRK'); + + const projects = r.results.find((domain) => domain.q === ComponentQualifier.Project); + if (projects && projects.items.length > 0) { suggestions = projects.items .filter((item) => !addedProjects.find((p) => p.project === item.key)) @@ -139,12 +152,13 @@ export default class ProjectModal extends React.PureComponent { projectName: item.name, })); } - this.setState({ loading: false, open: true, suggestions }); + + this.setState({ loading: false, suggestions }); } }, () => { if (this.mounted) { - this.setState({ loading: false, open: false }); + this.setState({ loading: false }); } }, ); @@ -152,14 +166,15 @@ export default class ProjectModal extends React.PureComponent { handleSelect = (selectedProject: NotificationProject) => { this.setState({ - open: false, query: selectedProject.projectName, selectedProject, + suggestions: undefined, }); }; handleSubmit = () => { const { selectedProject } = this.state; + if (selectedProject) { this.props.onSubmit(selectedProject); } @@ -167,66 +182,82 @@ export default class ProjectModal extends React.PureComponent { render() { const { closeModal } = this.props; - const { highlighted, loading, query, open, selectedProject, suggestions } = this.state; - const header = translate('my_account.set_notifications_for.title'); + const { highlighted, loading, query, selectedProject, suggestions } = this.state; + + const projectSuggestion = (suggestion: NotificationProject) => ( + this.handleSelect(suggestion)} + selected={ + highlighted?.project === suggestion.project || + selectedProject?.project === suggestion.project + } + > + {suggestion.projectName} + + ); + + const isSearching = query?.length && !selectedProject; + + const noResults = isSearching ? ( +
{translate('no_results')}
+ ) : undefined; + return ( - - {({ onCloseClick, onFormSubmit }) => ( -
-
-

{header}

-
-
-
- - - - {loading && } - - {!loading && open && ( -
- + + + {suggestions && suggestions.length > 0 ? ( -
    - {suggestions.map((suggestion) => ( -
  • this.handleSelect(suggestion)} - > - {suggestion.projectName} -
  • - ))} +
      + {suggestions.map((suggestion) => projectSuggestion(suggestion))}
    ) : ( -
    - {translate('no_results')} -
    + noResults )} - -
- )} -
-
-
-
- - {translate('add_verb')} - - {translate('cancel')} -
-
+ + + ) : undefined + } + placement={PopupPlacement.BottomLeft} + zLevel={PopupZLevel.Global} + > + + - )} -
+ } + headerTitle={translate('my_account.set_notifications_for.title')} + onClose={closeModal} + primaryButton={ + + {translate('add_verb')} + + } + secondaryButtonLabel={translate('cancel')} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx index 417ec217b62..48bfa4f5e8c 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/ProjectNotifications.tsx @@ -17,9 +17,11 @@ * 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 * as React from 'react'; -import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion'; import { translate } from '../../../helpers/l10n'; +import { getProjectUrl } from '../../../helpers/urls'; import { Notification, NotificationProject, @@ -30,63 +32,62 @@ import NotificationsList from './NotificationsList'; interface Props { addNotification: (n: Notification) => void; channels: string[]; - collapsed: boolean; + header?: React.JSX.Element; notifications: Notification[]; project: NotificationProject; removeNotification: (n: Notification) => void; types: NotificationProjectType[]; } -export default function ProjectNotifications(props: Props) { - const { collapsed, project, channels } = props; - const [isCollapsed, setCollapsed] = React.useState(collapsed); - +export default function ProjectNotifications({ + addNotification, + channels, + header, + notifications, + project, + removeNotification, + types, +}: Readonly) { const getCheckboxId = (type: string, channel: string) => { - return `project-notification-${props.project.project}-${type}-${channel}`; + return `project-notification-${project.project}-${type}-${channel}`; }; const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => { - props.addNotification({ ...props.project, channel, type }); + addNotification({ ...project, channel, type }); }; const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => { - props.removeNotification({ - ...props.project, + removeNotification({ + ...project, channel, type, }); }; - const toggleExpanded = () => setCollapsed(!isCollapsed); - return ( - {project.projectName}} - > - - - - - ))} - - +
+
+ {project.projectName} +
+ {!header && ( +
{translate('notifications.send_email')}
+ )} +
- {channels.map((channel) => ( - -

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

-
-
-
+ +
); } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx index bdd1658bf03..b99d5ef03fd 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx +++ b/server/sonar-web/src/main/js/apps/account/notifications/Projects.tsx @@ -17,10 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { ButtonPrimary, InputSearch, Note } from 'design-system'; import { groupBy, sortBy, uniqBy } from 'lodash'; import * as React from 'react'; -import { Button } from '../../../components/controls/buttons'; -import SearchBox from '../../../components/controls/SearchBox'; import { translate } from '../../../helpers/l10n'; import { Notification, @@ -33,13 +33,12 @@ 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[]; } -const THRESHOLD_COLLAPSED = 3; - interface State { addedProjects: NotificationProject[]; search: string; @@ -61,7 +60,7 @@ export default class Projects extends React.PureComponent { }; filterSearch = (project: NotificationProject, search: string) => { - return project.projectName && project.projectName.toLowerCase().includes(search); + return project.projectName?.toLowerCase().includes(search); }; handleAddProject = (project: NotificationProject) => { @@ -94,6 +93,7 @@ export default class Projects extends React.PureComponent { removeNotification = (removed: Notification, allProjects: NotificationProject[]) => { const projectToRemove = allProjects.find((p) => p.project === removed.project); + if (projectToRemove) { this.handleAddProject(projectToRemove); } @@ -108,25 +108,26 @@ export default class Projects extends React.PureComponent { const projects = uniqBy(notifications, ({ project }) => project).filter( 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) => this.filterSearch(p, search), ); - const shouldBeCollapsed = Object.keys(notificationsByProject).length > THRESHOLD_COLLAPSED; return ( -
-
-
- -
- -

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

+
+
+

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

+ + + + {translate('my_profile.per_project_notifications.add')} + +
{this.state.showModal && ( @@ -137,37 +138,32 @@ export default class Projects extends React.PureComponent { /> )} -
+
{allProjects.length === 0 && ( -
{translate('my_account.no_project_notifications')}
+ {translate('my_account.no_project_notifications')} )} {allProjects.length > 0 && ( -
- +
)} - {filteredProjects.map((project) => { - const collapsed = addedProjects.find((p) => p.project === project.project) - ? false - : shouldBeCollapsed; - return ( - this.removeNotification(n, allProjects)} - types={this.props.types} - /> - ); - })} + {filteredProjects.map((project) => ( + this.removeNotification(n, allProjects)} + types={this.props.types} + /> + ))}
); diff --git a/server/sonar-web/src/main/js/apps/account/security/Security.tsx b/server/sonar-web/src/main/js/apps/account/security/Security.tsx index fa7e912e9a2..b1ea17b4c5e 100644 --- a/server/sonar-web/src/main/js/apps/account/security/Security.tsx +++ b/server/sonar-web/src/main/js/apps/account/security/Security.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { PageTitle, SubHeading } from 'design-system'; +import { PageTitle } from 'design-system'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { useCurrentLoginUser } from '../../../app/components/current-user/CurrentUserContext'; @@ -35,14 +35,14 @@ export default function Security() { {currentUser.local && ( - + <> - + )} ); diff --git a/server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx b/server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx index c27c1d9e0ac..f8f6bb8e80f 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx @@ -80,7 +80,7 @@ export function ProjectNotifications(props: WithNotificationsProps & Props) {

- {translate('project_information.project_notifications.title')} + {translate('notifications.send_email')}

    {perProjectTypes.map((type) => ( @@ -89,7 +89,7 @@ export function ProjectNotifications(props: WithNotificationsProps & Props) { right className="sw-flex sw-justify-between" label={translateWithParameters( - 'notification.dispatcher.descrption_x', + 'notification.dispatcher.description_x', getDispatcherLabel(type), )} checked={isEnabled(type, emailChannel)} diff --git a/server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx index acd8ede2d54..a6de6c45878 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx @@ -49,44 +49,42 @@ it('should render correctly', async () => { const user = userEvent.setup(); renderProjectNotifications(); - expect( - await screen.findByText('project_information.project_notifications.title'), - ).toBeInTheDocument(); + expect(await screen.findByText('notifications.send_email')).toBeInTheDocument(); expect( screen.getByLabelText( - 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project', + 'notification.dispatcher.description_x.notification.dispatcher.NewAlerts.project', ), ).toBeChecked(); expect( screen.getByLabelText( - 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project', + 'notification.dispatcher.description_x.notification.dispatcher.NewIssues.project', ), ).not.toBeChecked(); // Toggle New Alerts await user.click( screen.getByLabelText( - 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project', + 'notification.dispatcher.description_x.notification.dispatcher.NewAlerts.project', ), ); expect( screen.getByLabelText( - 'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project', + 'notification.dispatcher.description_x.notification.dispatcher.NewAlerts.project', ), ).not.toBeChecked(); // Toggle New Issues await user.click( screen.getByLabelText( - 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project', + 'notification.dispatcher.description_x.notification.dispatcher.NewIssues.project', ), ); expect( screen.getByLabelText( - 'notification.dispatcher.descrption_x.notification.dispatcher.NewIssues.project', + 'notification.dispatcher.description_x.notification.dispatcher.NewIssues.project', ), ).toBeChecked(); }); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 7b41997b9cb..86cd9fa116b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2595,7 +2595,7 @@ notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive notification.dispatcher.SQ-MyNewIssues=My new issues notification.dispatcher.CeReportTaskFailure=Background tasks in failure on my administered projects notification.dispatcher.CeReportTaskFailure.project=Background tasks in failure -notification.dispatcher.descrption_x=Check to receive notification for {0} +notification.dispatcher.description_x=Check to receive notification for {0} #------------------------------------------------------------------------------ # @@ -2704,6 +2704,8 @@ my_account.preferences.keyboard_shortcuts.description=Some actions can be perfor my_account.preferences.keyboard_shortcuts.enabled=Keyboard shortcuts are enabled my_account.preferences.keyboard_shortcuts.disabled=Keyboard shortcuts are disabled +notifications.send_email=Send me an email for: + #------------------------------------------------------------------------------ # # PROJECT PROVISIONING @@ -3787,13 +3789,6 @@ project_dump.failed_import=The last import has failed. Please try once again. project_dump.import_form_description=A dump has been found on the file system for this project. You can import it by clicking on the button below. project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition. -#------------------------------------------------------------------------------ -# -# Project Information -# -#------------------------------------------------------------------------------ -project_information.project_notifications.title=Send me an email when: - #------------------------------------------------------------------------------ # # SYSTEM -- 2.39.5