add support for disabling alert dialog forms
This commit is contained in:
@@ -52,9 +52,15 @@ export interface MigrationAlert extends Alert {
|
||||
* by status in a collapsed view below Alert Insights section and a badge will appear in Action Items
|
||||
* showing the total alerts of that status.
|
||||
*
|
||||
* Default forms can be overridden by providing a valid React form component. Form components
|
||||
* must return valid form elements, and accept a ref and onSubmit event handler. See /forms
|
||||
* for example implementations. Custom forms must implement a corresponding event hook.
|
||||
* Customizing Alerts
|
||||
* Default forms can be overridden in two ways - by setting a form property to null or defining a custom component.
|
||||
*
|
||||
* If a form property is set to null, the Dialog will not render a form. This can be useful in scenarios
|
||||
* where data isn't needed from the user such as when a user accepts an action item's recommendation.
|
||||
*
|
||||
* If a form property is set to a React component, the Dialog will render the form component in place of the default form.
|
||||
* Form components must return valid form elements, and accept a ref and onSubmit event handler.
|
||||
* Custom forms must implement the corresponding event hook. See /forms for example implementations.
|
||||
*/
|
||||
|
||||
export class KubernetesMigrationAlert implements MigrationAlert {
|
||||
@@ -64,7 +70,10 @@ export class KubernetesMigrationAlert implements MigrationAlert {
|
||||
subtitle =
|
||||
'Services running on Kubernetes are estimated to save 50% or more compared to Compute Engine.';
|
||||
|
||||
// Override default dismiss form with a custom form component.
|
||||
// Dialog will not render a form if form property set to null.
|
||||
AcceptForm = null;
|
||||
|
||||
// Overrides default Dismiss form with a custom form component.
|
||||
DismissForm: AlertForm<
|
||||
MigrationAlert,
|
||||
MigrationDismissFormData
|
||||
@@ -100,7 +109,7 @@ export class KubernetesMigrationAlert implements MigrationAlert {
|
||||
);
|
||||
}
|
||||
|
||||
/* Fires when the onSubmit event is raised on a DismissAlert form. Displays a custom dismiss form. */
|
||||
/* Fires when the onSubmit event is raised on a Dismiss form. Displays custom dismiss form. */
|
||||
async onDismissed(
|
||||
options: AlertOptions<MigrationDismissFormData>,
|
||||
): Promise<Alert[]> {
|
||||
@@ -117,7 +126,7 @@ export class KubernetesMigrationAlert implements MigrationAlert {
|
||||
);
|
||||
}
|
||||
|
||||
/* Fires when the onSubmit event is raised on an SnoozeAlert form. Displays default snooze form. */
|
||||
/* Fires when the onSubmit event is raised on a Snooze form. Displays default snooze form. */
|
||||
async onSnoozed(
|
||||
options: AlertOptions<AlertSnoozeFormData>,
|
||||
): Promise<Alert[]> {
|
||||
@@ -134,7 +143,7 @@ export class KubernetesMigrationAlert implements MigrationAlert {
|
||||
);
|
||||
}
|
||||
|
||||
/* Fires when the onSubmit event is raised on an AcceptAlert form. Displays default accept form. */
|
||||
/* Fires when the Accept button is clicked. Dialog does not render a form. See KubernetesMigrationAlert.AcceptForm */
|
||||
async onAccepted(options: AlertOptions<null>): Promise<Alert[]> {
|
||||
const alerts = await this.api.getAlerts(options.group);
|
||||
return new Promise(resolve =>
|
||||
|
||||
@@ -16,154 +16,169 @@
|
||||
import React from 'react';
|
||||
import { AlertDialog } from './AlertDialog';
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
Alert,
|
||||
AlertFormProps,
|
||||
AlertSnoozeOptions,
|
||||
AlertDismissOptions,
|
||||
} from '../../types';
|
||||
import { Alert, AlertFormProps } from '../../types';
|
||||
|
||||
type MockFormDataProps = AlertFormProps<Alert>;
|
||||
|
||||
const MockForm = React.forwardRef<HTMLFormElement, MockFormDataProps>(
|
||||
(props, ref) => (
|
||||
function createForm(title: string) {
|
||||
return React.forwardRef<HTMLFormElement, MockFormDataProps>((props, ref) => (
|
||||
<form ref={ref} onSubmit={props.onSubmit}>
|
||||
You. Complete. Me.
|
||||
You. {title}. Me.
|
||||
</form>
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
const snoozableAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'test-subtitle',
|
||||
onSnoozed: jest.fn(),
|
||||
};
|
||||
|
||||
const dimissableAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
onDismissed: jest.fn(),
|
||||
};
|
||||
|
||||
const acceptAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
onAccepted: jest.fn(),
|
||||
};
|
||||
|
||||
const customSnoozeAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
onSnoozed: jest.fn(),
|
||||
SnoozeForm: createForm('Snooze'),
|
||||
};
|
||||
|
||||
const customDismissAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
onDismissed: jest.fn(),
|
||||
DismissForm: createForm('Dismiss'),
|
||||
};
|
||||
|
||||
const customAcceptAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'test-subtitle',
|
||||
onAccepted: jest.fn(),
|
||||
AcceptForm: createForm('Accept'),
|
||||
};
|
||||
|
||||
const nullAcceptAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'test-subtitle',
|
||||
onAccepted: jest.fn(),
|
||||
AcceptForm: null,
|
||||
};
|
||||
|
||||
const nullDismissAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'test-subtitle',
|
||||
onDismissed: jest.fn(),
|
||||
DismissForm: null,
|
||||
};
|
||||
|
||||
const nullSnoozeAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'test-subtitle',
|
||||
onSnoozed: jest.fn(),
|
||||
SnoozeForm: null,
|
||||
};
|
||||
describe('<AlertDialog />', () => {
|
||||
const snoozableAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'test-subtitle',
|
||||
onSnoozed: jest.fn(),
|
||||
};
|
||||
describe.each`
|
||||
accepted | dismissed | snoozed | action | text
|
||||
${acceptAlert} | ${null} | ${null} | ${['Accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'}
|
||||
${null} | ${dimissableAlert} | ${null} | ${['Dismiss', 'dismissed']} | ${'Reason for dismissing?'}
|
||||
${null} | ${null} | ${snoozableAlert} | ${['Snooze', 'snoozed']} | ${'For how long?'}
|
||||
`(
|
||||
'Default forms',
|
||||
({ accepted, dismissed, snoozed, action: [action, actioned], text }) => {
|
||||
it(`Displays a default ${action} form`, () => {
|
||||
const { getByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={snoozed}
|
||||
accepted={accepted}
|
||||
dismissed={dismissed}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByText(text)).toBeInTheDocument();
|
||||
expect(getByText(`${action} this action item?`)).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(`This action item will be ${actioned} for all of Ramones.`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const dimissableAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
onDismissed: jest.fn(),
|
||||
};
|
||||
describe.each`
|
||||
accepted | dismissed | snoozed | action
|
||||
${customAcceptAlert} | ${null} | ${null} | ${['Accept', 'accepted']}
|
||||
${null} | ${customDismissAlert} | ${null} | ${['Dismiss', 'dismissed']}
|
||||
${null} | ${null} | ${customSnoozeAlert} | ${['Snooze', 'snoozed']}
|
||||
`(
|
||||
'Custom forms',
|
||||
({ accepted, dismissed, snoozed, action: [Action, actioned] }) => {
|
||||
it(`Displays a custom ${Action} form`, () => {
|
||||
const { getByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={snoozed}
|
||||
accepted={accepted}
|
||||
dismissed={dismissed}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByText(`You. ${Action}. Me.`)).toBeInTheDocument();
|
||||
expect(getByText(`${Action} this action item?`)).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(`This action item will be ${actioned} for all of Ramones.`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const customSnoozeAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
onSnoozed: jest.fn(),
|
||||
SnoozeForm: MockForm,
|
||||
};
|
||||
|
||||
const customDismissAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
onDismissed: jest.fn(),
|
||||
DismissForm: MockForm,
|
||||
};
|
||||
|
||||
const customAcceptAlert: Alert = {
|
||||
title: 'title',
|
||||
subtitle: 'test-subtitle',
|
||||
onAccepted: jest.fn(),
|
||||
AcceptForm: MockForm,
|
||||
};
|
||||
|
||||
it('Displays a default snooze form', () => {
|
||||
const { getByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={snoozableAlert}
|
||||
accepted={null}
|
||||
dismissed={null}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('For how long?')).toBeInTheDocument();
|
||||
expect(getByText('Snooze this action item?')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('This action item will be snoozed for all of Ramones.'),
|
||||
).toBeInTheDocument();
|
||||
AlertSnoozeOptions.forEach(a =>
|
||||
expect(getByText(a.label)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Displays a custom snooze form', () => {
|
||||
const { getByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={customSnoozeAlert}
|
||||
accepted={null}
|
||||
dismissed={null}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('You. Complete. Me.')).toBeInTheDocument();
|
||||
expect(getByText('Snooze this action item?')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('This action item will be snoozed for all of Ramones.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays a default dismiss form', () => {
|
||||
const { getByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={null}
|
||||
accepted={null}
|
||||
dismissed={dimissableAlert}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Dismiss this action item?')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('This action item will be dismissed for all of Ramones.'),
|
||||
).toBeInTheDocument();
|
||||
AlertDismissOptions.forEach(a =>
|
||||
expect(getByText(a.label)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('Displays a custom dismiss form', () => {
|
||||
const { getByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={null}
|
||||
accepted={null}
|
||||
dismissed={customDismissAlert}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Dismiss this action item?')).toBeInTheDocument();
|
||||
expect(getByText('You. Complete. Me.')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('This action item will be dismissed for all of Ramones.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays a custom accept form', () => {
|
||||
const { getByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={null}
|
||||
accepted={customAcceptAlert}
|
||||
dismissed={null}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Accept this action item?')).toBeInTheDocument();
|
||||
expect(getByText('You. Complete. Me.')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('This action item will be accepted for all of Ramones.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
describe.each`
|
||||
accepted | dismissed | snoozed | action | text
|
||||
${nullAcceptAlert} | ${null} | ${null} | ${['Accept', 'accept', 'accepted']} | ${'My team can commit to making this change soon, or has already.'}
|
||||
${null} | ${nullDismissAlert} | ${null} | ${['Dismiss', 'dismiss', 'dismissed']} | ${'Reason for dismissing?'}
|
||||
${null} | ${null} | ${nullSnoozeAlert} | ${['Snooze', 'snooze', 'snoozed']} | ${'For how long?'}
|
||||
`(
|
||||
'Null forms',
|
||||
({
|
||||
accepted,
|
||||
dismissed,
|
||||
snoozed,
|
||||
action: [Action, action, actioned],
|
||||
text,
|
||||
}) => {
|
||||
it(`Does NOT display a ${Action} form`, () => {
|
||||
const { getByText, getByRole, queryByText } = render(
|
||||
<AlertDialog
|
||||
open
|
||||
group="Ramones"
|
||||
snoozed={snoozed}
|
||||
accepted={accepted}
|
||||
dismissed={dismissed}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(queryByText(text)).not.toBeInTheDocument();
|
||||
expect(getByRole('button', { name: action })).toBeInTheDocument();
|
||||
expect(getByText(`${Action} this action item?`)).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(`This action item will be ${actioned} for all of Ramones.`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from '../../forms';
|
||||
import { useAlertDialogStyles as useStyles } from '../../utils/styles';
|
||||
import { choose } from '../../utils/alerts';
|
||||
import { Alert, Maybe } from '../../types';
|
||||
import { Alert, AlertForm, Maybe } from '../../types';
|
||||
|
||||
const DEFAULT_FORM_ID = 'alert-form';
|
||||
|
||||
@@ -79,20 +79,41 @@ export const AlertDialog = ({
|
||||
setDisabled(true);
|
||||
}
|
||||
|
||||
const SnoozeForm = snoozed?.SnoozeForm ?? AlertSnoozeForm;
|
||||
const AcceptForm = accepted?.AcceptForm ?? AlertAcceptForm;
|
||||
const DismissForm = dismissed?.DismissForm ?? AlertDismissForm;
|
||||
const SnoozeForm: Maybe<AlertForm> = snoozed?.SnoozeForm ?? AlertSnoozeForm;
|
||||
const AcceptForm: Maybe<AlertForm> = accepted?.AcceptForm ?? AlertAcceptForm;
|
||||
const DismissForm: Maybe<AlertForm> =
|
||||
dismissed?.DismissForm ?? AlertDismissForm;
|
||||
|
||||
const isSnoozeFormDisplayed = !!snoozed?.onSnoozed;
|
||||
const isAcceptFormDisplayed = !!accepted?.onAccepted;
|
||||
const isDismissFormDisplayed = !!dismissed?.onDismissed;
|
||||
const isSnoozingEnabled = !!snoozed?.onSnoozed;
|
||||
const isAcceptingEnabled = !!accepted?.onAccepted;
|
||||
const isDismissingEnabled = !!dismissed?.onDismissed;
|
||||
|
||||
const isSnoozeFormDisabled = snoozed?.SnoozeForm === null;
|
||||
const isAcceptFormDisabled = accepted?.AcceptForm === null;
|
||||
const isDismissFormDisabled = dismissed?.DismissForm === null;
|
||||
const isFormDisabled =
|
||||
isSnoozeFormDisabled || isAcceptFormDisabled || isDismissFormDisabled;
|
||||
|
||||
const status = [
|
||||
isAcceptFormDisplayed,
|
||||
isSnoozeFormDisplayed,
|
||||
isDismissFormDisplayed,
|
||||
isSnoozingEnabled,
|
||||
isAcceptingEnabled,
|
||||
isDismissingEnabled,
|
||||
] as const;
|
||||
|
||||
const [Action, action, actioned] =
|
||||
choose(status, [
|
||||
['Snooze', 'snooze', 'snoozed'],
|
||||
['Accept', 'accept', 'accepted'],
|
||||
['Dismiss', 'dismiss', 'dismissed'],
|
||||
]) ?? [];
|
||||
|
||||
const [title, subtitle] =
|
||||
choose(status, [
|
||||
[snoozed?.title, snoozed?.subtitle],
|
||||
[accepted?.title, accepted?.subtitle],
|
||||
[dismissed?.title, dismissed?.subtitle],
|
||||
]) ?? [];
|
||||
|
||||
const TransitionProps = {
|
||||
mountOnEnter: true,
|
||||
unmountOnExit: true,
|
||||
@@ -122,24 +143,20 @@ export const AlertDialog = ({
|
||||
<IconButton
|
||||
className={classes.icon}
|
||||
disableRipple
|
||||
aria-label="close dialog"
|
||||
onClick={onDialogClose}
|
||||
>
|
||||
<CloseIcon aria-label="close dialog" />
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<DialogContent className={classes.content}>
|
||||
<Box mb={1.5}>
|
||||
<Typography variant="h5">
|
||||
<b>
|
||||
{choose(status, ['Accept', 'Snooze', 'Dismiss'])} this action
|
||||
item?
|
||||
</b>
|
||||
<b>{Action} this action item?</b>
|
||||
</Typography>
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
<b>
|
||||
This action item will be{' '}
|
||||
{choose(status, ['accepted', 'snoozed', 'dismissed'])} for all of{' '}
|
||||
{group}.
|
||||
This action item will be {actioned} for all of {group}.
|
||||
</b>
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -152,23 +169,11 @@ export const AlertDialog = ({
|
||||
borderRadius={4}
|
||||
>
|
||||
<Typography>
|
||||
<b>
|
||||
{choose(status, [
|
||||
accepted?.title,
|
||||
snoozed?.title,
|
||||
dismissed?.title,
|
||||
])}
|
||||
</b>
|
||||
</Typography>
|
||||
<Typography color="textSecondary">
|
||||
{choose(status, [
|
||||
accepted?.subtitle,
|
||||
snoozed?.subtitle,
|
||||
dismissed?.subtitle,
|
||||
])}
|
||||
<b>{title}</b>
|
||||
</Typography>
|
||||
<Typography color="textSecondary">{subtitle}</Typography>
|
||||
</Box>
|
||||
{isSnoozeFormDisplayed && (
|
||||
{isSnoozingEnabled && !isSnoozeFormDisabled && (
|
||||
<SnoozeForm
|
||||
ref={snoozeRef}
|
||||
alert={snoozed!}
|
||||
@@ -176,7 +181,7 @@ export const AlertDialog = ({
|
||||
disableSubmit={disableSubmit}
|
||||
/>
|
||||
)}
|
||||
{isDismissFormDisplayed && (
|
||||
{isDismissingEnabled && !isDismissFormDisabled && (
|
||||
<DismissForm
|
||||
ref={dismissRef}
|
||||
alert={dismissed!}
|
||||
@@ -184,7 +189,7 @@ export const AlertDialog = ({
|
||||
disableSubmit={disableSubmit}
|
||||
/>
|
||||
)}
|
||||
{isAcceptFormDisplayed && (
|
||||
{isAcceptingEnabled && !isAcceptFormDisabled && (
|
||||
<AcceptForm
|
||||
ref={acceptRef}
|
||||
alert={accepted!}
|
||||
@@ -195,15 +200,28 @@ export const AlertDialog = ({
|
||||
</DialogContent>
|
||||
<Divider />
|
||||
<DialogActions className={classes.actions} disableSpacing>
|
||||
<Button
|
||||
disabled={isButtonDisabled}
|
||||
type="submit"
|
||||
form={DEFAULT_FORM_ID}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
{choose(status, ['Accept', 'Snooze', 'Dismiss'])}
|
||||
</Button>
|
||||
{isFormDisabled ? (
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
aria-label={action}
|
||||
onClick={() => onSubmit(null)}
|
||||
>
|
||||
{Action}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
form={DEFAULT_FORM_ID}
|
||||
aria-label={action}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
{Action}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -40,9 +40,9 @@ export type Alert = {
|
||||
status?: AlertStatus;
|
||||
url?: string;
|
||||
buttonText?: string; // Default: View Instructions
|
||||
SnoozeForm?: AlertForm;
|
||||
AcceptForm?: AlertForm;
|
||||
DismissForm?: AlertForm;
|
||||
SnoozeForm?: Maybe<AlertForm>;
|
||||
AcceptForm?: Maybe<AlertForm>;
|
||||
DismissForm?: Maybe<AlertForm>;
|
||||
onSnoozed?(options: AlertOptions): Promise<Alert[]>;
|
||||
onAccepted?(options: AlertOptions): Promise<Alert[]>;
|
||||
onDismissed?(options: AlertOptions): Promise<Alert[]>;
|
||||
|
||||
@@ -29,7 +29,7 @@ export const sumOfAllAlerts = (sum: number, alerts: Alert[]) =>
|
||||
export function choose<T>(
|
||||
status: readonly [boolean, boolean, boolean],
|
||||
values: [T, T, T],
|
||||
): T | null {
|
||||
): T | undefined {
|
||||
const i = status.indexOf(true);
|
||||
return i < 0 ? null : values[i];
|
||||
return values[i];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user