Просмотр исходного кода

feat(notifications): Confirmation Modal (#27728)

Co-authored-by: Nisanthan Nanthakumar <nisanthan.nanthakumar@sentry.io>
Marcos Gaeta 3 лет назад
Родитель
Сommit
603031351d

+ 11 - 7
static/app/components/confirm.tsx

@@ -27,6 +27,7 @@ export type ConfirmMessageRenderProps = {
    * This should be called in the components componentDidMount.
    */
   setConfirmCallback: (cb: () => void) => void;
+  selectedValue?: any;
 };
 
 export type ConfirmButtonsRenderProps = {
@@ -42,14 +43,14 @@ export type ConfirmButtonsRenderProps = {
 };
 
 type ChildrenRenderProps = {
-  open: () => void;
+  open: (e?: React.MouseEvent, selectedValue?: string) => void;
 };
 
 export type OpenConfirmOptions = {
   /**
    * Callback when user confirms
    */
-  onConfirm?: () => void;
+  onConfirm?: (selectedValue?: any) => void;
   /**
    * Custom function to render the confirm button
    */
@@ -106,6 +107,7 @@ export type OpenConfirmOptions = {
    * Text to show in the confirmation button
    */
   confirmText?: React.ReactNode;
+  selectedValue?: string;
 };
 
 type Props = OpenConfirmOptions & {
@@ -167,7 +169,7 @@ function Confirm({
   stopPropagation = false,
   ...openConfirmOptions
 }: Props) {
-  const triggerModal = (e?: React.MouseEvent) => {
+  const triggerModal = (e?: React.MouseEvent, selectedValue?: string) => {
     if (stopPropagation) {
       e?.stopPropagation();
     }
@@ -176,7 +178,7 @@ function Confirm({
       return;
     }
 
-    openConfirmModal(openConfirmOptions);
+    openConfirmModal({...openConfirmOptions, ...(selectedValue && {selectedValue})});
   };
 
   if (typeof children === 'function') {
@@ -205,6 +207,7 @@ type ModalProps = ModalRenderProps &
     | 'onConfirm'
     | 'onCancel'
     | 'disableConfirmButton'
+    | 'selectedValue'
   >;
 
 type ModalState = {
@@ -238,12 +241,12 @@ class ConfirmModal extends React.Component<ModalProps, ModalState> {
   };
 
   handleConfirm = () => {
-    const {onConfirm, closeModal} = this.props;
+    const {onConfirm, closeModal, selectedValue} = this.props;
 
     // `confirming` is used to ensure `onConfirm` or the confirm callback is
     // only called once
     if (!this.confirming) {
-      onConfirm?.();
+      onConfirm?.(selectedValue);
       this.state.confirmCallback?.();
     }
 
@@ -253,7 +256,7 @@ class ConfirmModal extends React.Component<ModalProps, ModalState> {
   };
 
   get confirmMessage() {
-    const {message, renderMessage} = this.props;
+    const {message, renderMessage, selectedValue} = this.props;
 
     if (typeof renderMessage === 'function') {
       return renderMessage({
@@ -263,6 +266,7 @@ class ConfirmModal extends React.Component<ModalProps, ModalState> {
           this.setState({disableConfirmButton: state}),
         setConfirmCallback: (confirmCallback: () => void) =>
           this.setState({confirmCallback}),
+        selectedValue,
       });
     }
 

+ 16 - 0
static/app/views/settings/account/notifications/constants.tsx

@@ -1,3 +1,5 @@
+import {t} from 'app/locale';
+
 export const ALL_PROVIDERS = {
   email: 'default',
   slack: 'never',
@@ -12,6 +14,7 @@ export const VALUE_MAPPING = {
   committed_only: 40,
 };
 
+export const MIN_PROJECTS_FOR_CONFIRMATION = 3;
 export const MIN_PROJECTS_FOR_SEARCH = 3;
 export const MIN_PROJECTS_FOR_PAGINATION = 100;
 
@@ -32,3 +35,16 @@ export const SELF_NOTIFICATION_SETTINGS_TYPES = [
   'personalActivityNotifications',
   'selfAssignOnResolve',
 ];
+
+export const CONFIRMATION_MESSAGE = (
+  <div>
+    <p style={{marginBottom: '20px'}}>
+      <strong>Are you sure you want to disable these notifications?</strong>
+    </p>
+    <p>
+      {t(
+        'Turning this off will irreversibly overwrite all of your fine-tuning settings to "off".'
+      )}
+    </p>
+  </div>
+);

+ 3 - 0
static/app/views/settings/account/notifications/fields2.tsx

@@ -1,3 +1,5 @@
+import * as React from 'react';
+
 import {t} from 'app/locale';
 
 export type NotificationSettingField = {
@@ -8,6 +10,7 @@ export type NotificationSettingField = {
   defaultValue?: string;
   defaultFieldName?: string;
   help?: string;
+  confirm?: {[key: string]: React.ReactNode | string};
 };
 
 export const NOTIFICATION_SETTING_FIELDS: Record<string, NotificationSettingField> = {

+ 29 - 15
static/app/views/settings/account/notifications/notificationSettings.tsx

@@ -6,6 +6,7 @@ import Link from 'app/components/links/link';
 import {IconMail} from 'app/icons';
 import {t} from 'app/locale';
 import {
+  CONFIRMATION_MESSAGE,
   NOTIFICATION_SETTINGS_TYPES,
   NotificationSettingsObject,
   SELF_NOTIFICATION_SETTINGS_TYPES,
@@ -16,6 +17,7 @@ import {
   decideDefault,
   getParentIds,
   getStateToPutForDefault,
+  isSufficientlyComplex,
   mergeNotificationSettings,
 } from 'app/views/settings/account/notifications/utils';
 import Form from 'app/views/settings/components/forms/form';
@@ -89,21 +91,33 @@ class NotificationSettings extends AsyncComponent<Props, State> {
   }
 
   getFields(): FieldObject[] {
-    return NOTIFICATION_SETTINGS_TYPES.map(
-      notificationType =>
-        Object.assign({}, NOTIFICATION_SETTING_FIELDS[notificationType], {
-          getData: data => this.getStateToPutForDefault(data, notificationType),
-          help: (
-            <React.Fragment>
-              {NOTIFICATION_SETTING_FIELDS[notificationType].help}
-              &nbsp;
-              <Link to={`/settings/account/notifications/${notificationType}`}>
-                Fine tune
-              </Link>
-            </React.Fragment>
-          ),
-        }) as FieldObject
-    );
+    const {notificationSettings} = this.state;
+
+    const fields: FieldObject[] = [];
+    for (const notificationType of NOTIFICATION_SETTINGS_TYPES) {
+      const field = Object.assign({}, NOTIFICATION_SETTING_FIELDS[notificationType], {
+        getData: data => this.getStateToPutForDefault(data, notificationType),
+        help: (
+          <React.Fragment>
+            {NOTIFICATION_SETTING_FIELDS[notificationType].help}
+            &nbsp;
+            <Link to={`/settings/account/notifications/${notificationType}`}>
+              Fine tune
+            </Link>
+          </React.Fragment>
+        ),
+      }) as any;
+
+      if (
+        isSufficientlyComplex(notificationType, notificationSettings) &&
+        typeof field !== 'function'
+      ) {
+        field.confirm = {never: CONFIRMATION_MESSAGE};
+      }
+
+      fields.push(field);
+    }
+    return fields;
   }
 
   renderBody() {

+ 13 - 4
static/app/views/settings/account/notifications/notificationSettingsByType.tsx

@@ -5,6 +5,7 @@ import {t} from 'app/locale';
 import {Organization, OrganizationSummary} from 'app/types';
 import withOrganizations from 'app/utils/withOrganizations';
 import {
+  CONFIRMATION_MESSAGE,
   NotificationSettingsByProviderObject,
   NotificationSettingsObject,
 } from 'app/views/settings/account/notifications/constants';
@@ -27,6 +28,7 @@ import {
   getStateToPutForProvider,
   isEverythingDisabled,
   isGroupedByProject,
+  isSufficientlyComplex,
   mergeNotificationSettings,
   providerListToString,
 } from 'app/views/settings/account/notifications/utils';
@@ -165,12 +167,19 @@ class NotificationSettingsByType extends AsyncComponent<Props, State> {
     const {notificationType} = this.props;
     const {notificationSettings} = this.state;
 
-    const fields = [
-      Object.assign({}, NOTIFICATION_SETTING_FIELDS[notificationType], {
+    const defaultField = Object.assign(
+      {},
+      NOTIFICATION_SETTING_FIELDS[notificationType],
+      {
         help: t('This is the default for all projects.'),
         getData: data => this.getStateToPutForDefault(data),
-      }),
-    ];
+      }
+    );
+    if (isSufficientlyComplex(notificationType, notificationSettings)) {
+      defaultField.confirm = {never: CONFIRMATION_MESSAGE};
+    }
+
+    const fields = [defaultField];
     if (!isEverythingDisabled(notificationType, notificationSettings)) {
       fields.push(
         Object.assign(

+ 12 - 0
static/app/views/settings/account/notifications/utils.tsx

@@ -4,6 +4,7 @@ import {t} from 'app/locale';
 import {OrganizationSummary, Project} from 'app/types';
 import {
   ALL_PROVIDERS,
+  MIN_PROJECTS_FOR_CONFIRMATION,
   NotificationSettingsByProviderObject,
   NotificationSettingsObject,
   VALUE_MAPPING,
@@ -254,6 +255,17 @@ export const getParentData = (
   );
 };
 
+export const isSufficientlyComplex = (
+  notificationType: string,
+  notificationSettings: NotificationSettingsObject
+): boolean => {
+  /** Are there are more than N project or organization settings? */
+  return (
+    getParentIds(notificationType, notificationSettings).length >
+    MIN_PROJECTS_FOR_CONFIRMATION
+  );
+};
+
 export const getStateToPutForProvider = (
   notificationType: string,
   notificationSettings: NotificationSettingsObject,

+ 30 - 7
static/app/views/settings/components/forms/selectField.tsx

@@ -1,7 +1,9 @@
 import * as React from 'react';
 import {OptionsType, ValueType} from 'react-select';
 
+import Confirm from 'app/components/confirm';
 import SelectControl, {ControlProps} from 'app/components/forms/selectControl';
+import {t} from 'app/locale';
 import {Choices, SelectValue} from 'app/types';
 import InputField from 'app/views/settings/components/forms/inputField';
 
@@ -79,18 +81,39 @@ export default class SelectField<
   };
 
   render() {
-    const {multiple, allowClear, small, ...otherProps} = this.props;
+    const {allowClear, confirm, multiple, small, ...otherProps} = this.props;
     return (
       <InputField
         {...otherProps}
         alignRight={small}
         field={({onChange, onBlur, required: _required, ...props}) => (
-          <SelectControl
-            {...props}
-            clearable={allowClear}
-            multiple={multiple}
-            onChange={this.handleChange.bind(this, onBlur, onChange)}
-          />
+          <Confirm
+            renderMessage={({selectedValue}) =>
+              confirm && selectedValue
+                ? confirm[selectedValue.value]
+                : // Set a default confirm message
+                  t('Continue with these changes?')
+            }
+            onCancel={() => {}}
+            onConfirm={this.handleChange.bind(this, onBlur, onChange)}
+          >
+            {({open}) => (
+              <SelectControl
+                {...props}
+                clearable={allowClear}
+                multiple={multiple}
+                onChange={val => {
+                  const previousValue = props.value?.toString();
+                  const newValue = val.value?.toString();
+                  if (confirm && confirm[newValue] && previousValue !== newValue) {
+                    open(undefined, val);
+                    return;
+                  }
+                  this.handleChange.bind(this, onBlur, onChange)(val);
+                }}
+              />
+            )}
+          </Confirm>
         )}
       />
     );

+ 1 - 3
static/app/views/settings/components/forms/type.tsx

@@ -31,8 +31,6 @@ export const FieldType = [
 
 export type FieldValue = any;
 
-type ConfirmKeyType = 'true' | 'false';
-
 // TODO(ts): A lot of these attributes are missing correct types. We'll likely
 // need to introduce some generics in here to get rid of some of these anys.
 
@@ -53,7 +51,7 @@ type BaseField = {
   updatesForm?: boolean;
   /** Does editing this field need to clear all other fields? */
   resetsForm?: boolean;
-  confirm?: {[key in ConfirmKeyType]?: string};
+  confirm?: {[key: string]: string};
   autosize?: boolean;
   maxRows?: number;
   extraHelp?: string;