]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19613 Migrate project notification to new UI
authorMathieu Suen <mathieu.suen@sonarsource.com>
Tue, 20 Jun 2023 14:58:04 +0000 (16:58 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Jun 2023 20:03:55 +0000 (20:03 +0000)
server/sonar-web/design-system/src/components/Checkbox.tsx
server/sonar-web/src/main/js/apps/projectInformation/notifications/ProjectNotifications.tsx
server/sonar-web/src/main/js/apps/projectInformation/notifications/__tests__/ProjectNotifications-test.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 6b4a77a3c985872ac3f2d8168807e12255d7d135..238a52cba6e6386d70b263172b18bfa6eeccddc1 100644 (file)
@@ -140,6 +140,7 @@ export const AccessibleCheckbox = styled.input`
   padding: 0;
   white-space: nowrap;
   width: 1px;
+  appearance: none;
 
   &:focus,
   &:active {
index d0ddd4fd9fcfd637cd784e0c51d49059b5b41b50..760cc32b7d2c24407d0d49b8fc34a94a588d875d 100644 (file)
  * 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, FlagMessage, SubTitle } from 'design-system';
 import * as React from 'react';
 import {
-  withNotifications,
   WithNotificationsProps,
+  withNotifications,
 } from '../../../components/hoc/withNotifications';
-import { Alert } from '../../../components/ui/Alert';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate } from '../../../helpers/l10n';
+import { hasMessage, translate, translateWithParameters } from '../../../helpers/l10n';
+import { NotificationProjectType } from '../../../types/notifications';
 import { Component } from '../../../types/types';
-import NotificationsList from '../../account/notifications/NotificationsList';
 
 interface Props {
   component: Component;
@@ -34,70 +34,76 @@ interface Props {
 
 export function ProjectNotifications(props: WithNotificationsProps & Props) {
   const { channels, component, loading, notifications, perProjectTypes } = props;
-  const heading = React.useRef<HTMLHeadingElement>(null);
 
-  React.useEffect(() => {
-    if (heading.current) {
-      // a11y: provide focus to the heading when the info drawer page is opened.
-      heading.current.focus();
+  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,
+      });
     }
-  }, [heading]);
+  };
 
-  const handleAddNotification = ({ channel, type }: { channel: string; type: string }) => {
-    props.addNotification({ project: component.key, channel, type });
+  const getCheckboxId = (type: string, channel: string) => {
+    return `project-notification-${component.key}-${type}-${channel}`;
   };
 
-  const handleRemoveNotification = ({ channel, type }: { channel: string; type: string }) => {
-    props.removeNotification({
-      project: component.key,
-      channel,
-      type,
-    });
+  const getDispatcherLabel = (dispatcher: string) => {
+    const globalMessageKey = ['notification.dispatcher', dispatcher];
+    const projectMessageKey = [...globalMessageKey, 'project'];
+    const shouldUseProjectMessage = hasMessage(...projectMessageKey);
+    return shouldUseProjectMessage
+      ? translate(...projectMessageKey)
+      : translate(...globalMessageKey);
   };
 
-  const getCheckboxId = (type: string, channel: string) => {
-    return `project-notification-${component.key}-${type}-${channel}`;
+  const isEnabled = (type: string, channel: string) => {
+    return !!notifications.find(
+      (notification) =>
+        notification.type === type &&
+        notification.channel === channel &&
+        notification.project === component.key
+    );
   };
 
-  const projectNotifications = notifications.filter(
-    (n) => n.project && n.project === component.key
-  );
+  const emailChannel = channels[0];
 
   return (
-    <div>
-      <h3 tabIndex={-1} ref={heading}>
-        {translate('project.info.notifications')}
-      </h3>
+    <form aria-labelledby="notifications-update-title">
+      <SubTitle>{translate('project.info.notifications')}</SubTitle>
 
-      <Alert className="spacer-top" variant="info">
+      <FlagMessage className="spacer-top" variant="info">
         {translate('notification.dispatcher.information')}
-      </Alert>
+      </FlagMessage>
 
       <DeferredSpinner loading={loading}>
-        <table className="data zebra notifications-table">
-          <thead>
-            <tr>
-              <th aria-label={translate('project')} />
-              {channels.map((channel) => (
-                <th className="text-center" key={channel}>
-                  <h4>{translate('notification.channel', channel)}</h4>
-                </th>
-              ))}
-            </tr>
-          </thead>
-
-          <NotificationsList
-            channels={channels}
-            checkboxId={getCheckboxId}
-            notifications={projectNotifications}
-            onAdd={handleAddNotification}
-            onRemove={handleRemoveNotification}
-            project
-            types={perProjectTypes}
-          />
-        </table>
+        <h3 id="notifications-update-title" className="sw-mt-6">
+          {translate('project_information.project_notifications.title')}
+        </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.descrption_x',
+                  getDispatcherLabel(type)
+                )}
+                checked={isEnabled(type, emailChannel)}
+                id={getCheckboxId(type, emailChannel)}
+                onCheck={(checked: boolean) => handleCheck(type, emailChannel, checked)}
+              >
+                {getDispatcherLabel(type)}
+              </Checkbox>
+            </li>
+          ))}
+        </ul>
       </DeferredSpinner>
-    </div>
+    </form>
   );
 }
 
index e7d187c574f54ee50b8e2f65265d3084ed332405..73cd0028e2c1fa0aa7df64af754f71907da70fbb 100644 (file)
@@ -49,7 +49,9 @@ it('should render correctly', async () => {
   const user = userEvent.setup();
   renderProjectNotifications();
 
-  expect(await screen.findByText('notification.channel.channel1')).toBeInTheDocument();
+  expect(
+    await screen.findByText('project_information.project_notifications.title')
+  ).toBeInTheDocument();
   expect(
     screen.getByLabelText(
       'notification.dispatcher.descrption_x.notification.dispatcher.NewAlerts.project'
index 99d7d002a8a23cb0ce0ed5f869a5f56de94c5af6..76613b49e155506c7f7c97f79020e1f309f66965 100644 (file)
@@ -1828,7 +1828,7 @@ application.info.title=Application Information
 project.info.description=Description
 project.info.quality_gate=Quality Gate used
 project.info.to_notifications=Set notifications
-project.info.notifications=Set notifications
+project.info.notifications=Notifications
 project.info.main_branch=Main branch
 project.info.see_more_info_on_x_locs=See more information on your {0} lines of code
 
@@ -3416,6 +3416,13 @@ 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