Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(account-settings): Enable component override for <DeleteUser /> #3123

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import { deleteUser, translate, getLogger } from '@aws-amplify/ui';

import { useAuth } from '../../../internal';
import { Flex } from '../../../primitives';
import {
DefaultWarning,
DefaultError,
DefaultSubmitButton,
} from './defaultComponents';
import DEFAULTS from './defaults';
import { DeleteUserProps, DeleteUserState } from './types';

const logger = getLogger('Auth');

function DeleteUser({
components,
onSuccess,
onError,
handleDelete,
Expand All @@ -26,6 +23,12 @@ function DeleteUser({

const { user, isLoading } = useAuth();

// subcomponents
const { Error, SubmitButton, Warning } = React.useMemo(
() => ({ ...DEFAULTS, ...(components || {}) }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nittish, prefer nullish coalescing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 45ca1e2 for both DeleteUser and ChangePassword

[components]
);

const startConfirmation = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
setState('CONFIRMATION');
Expand Down Expand Up @@ -67,7 +70,7 @@ function DeleteUser({
runDeleteUser();
}, [runDeleteUser]);

/** Return null if user isn't authenticated in the first place */
// Return null if user isn't authenticated in the first place
if (!user) {
logger.warn('<DeleteUser /> requires user to be authenticated.');
return null;
Expand All @@ -85,22 +88,26 @@ function DeleteUser({

return (
<Flex direction="column">
<DefaultSubmitButton
<SubmitButton
isDisabled={state === 'CONFIRMATION'}
onClick={startConfirmation}
>
{deleteAccountText}
</DefaultSubmitButton>
</SubmitButton>
{state === 'CONFIRMATION' || state === 'DELETING' ? (
<DefaultWarning
<Warning
onCancel={handleCancel}
isDisabled={state === 'DELETING'}
onConfirm={handleConfirmDelete}
/>
) : null}
{errorMessage ? <DefaultError>{errorMessage}</DefaultError> : null}
{errorMessage ? <Error>{errorMessage}</Error> : null}
</Flex>
);
}

DeleteUser.Error = DEFAULTS.Error;
DeleteUser.SubmitButton = DEFAULTS.SubmitButton;
DeleteUser.Warning = DEFAULTS.Warning;

export default DeleteUser;
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import {
render,
screen,
fireEvent,
waitFor,
act,
} from '@testing-library/react';

import * as UIModule from '@aws-amplify/ui';

import { Button, Flex, Heading, Text } from '../../../../primitives';
import { DeleteUserComponents } from '../types';
import DeleteUser from '../DeleteUser';

const user = {} as unknown as UIModule.AmplifyUser;
Expand All @@ -15,6 +23,29 @@ jest.mock('../../../../internal', () => ({

const deleteUserSpy = jest.spyOn(UIModule, 'deleteUser');

function CustomWarning({ onCancel, onConfirm, isDisabled }) {
return (
<Flex direction="column">
<Text variation="warning">Custom Warning Message</Text>
<Button onClick={onCancel}>Back</Button>
<Button variation="primary" onClick={onConfirm} isDisabled={isDisabled}>
Custom Confirm Button
</Button>
</Flex>
);
}

const components: DeleteUserComponents = {
SubmitButton: (props) => <Button {...props}>Custom Delete Button</Button>,
Warning: (props) => <CustomWarning {...props} />,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to just write this as:

Suggested change
Warning: (props) => <CustomWarning {...props} />,
Warning: CustomWarning,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bb9561a 😈

Error: ({ children }) => (
<>
<Heading>Custom Error Message</Heading>
<Text>{children}</Text>
</>
),
};

describe('ChangePassword', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -133,7 +164,88 @@ describe('ChangePassword', () => {

fireEvent.click(confirmDeleteButton);

// submit handling is async, wait for error to be displayed
await waitFor(() => expect(screen.findByText('Mock Error')).toBeDefined());
expect(await screen.findByText('Mock Error')).toBeDefined();
});

it('renders as expected with components overrides', async () => {
const { container } = render(<DeleteUser components={components} />);

const submitButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

expect(submitButton).toBeDefined();
expect(container).toMatchSnapshot();

fireEvent.click(submitButton);

expect(await screen.findByText('Custom Warning Message')).toBeDefined();
});

it('onSuccess is called with component overrides after successful user deletion', async () => {
deleteUserSpy.mockResolvedValue();

const onSuccess = jest.fn();
render(<DeleteUser components={components} onSuccess={onSuccess} />);

const deleteAccountButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

fireEvent.click(deleteAccountButton);

const confirmDeleteButton = await screen.findByRole('button', {
name: 'Custom Confirm Button',
});

fireEvent.click(confirmDeleteButton);

// submit handling is async, wait for onSuccess to be called
// https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
await waitFor(() => expect(onSuccess).toBeCalledTimes(1));
});

it('calls deleteUser with expected arguments and component overrides', async () => {
deleteUserSpy.mockResolvedValue();

const onSuccess = jest.fn();
render(<DeleteUser components={components} onSuccess={onSuccess} />);

const deleteAccountButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

fireEvent.click(deleteAccountButton);

const confirmDeleteButton = await screen.findByRole('button', {
name: 'Custom Confirm Button',
});

fireEvent.click(confirmDeleteButton);

expect(deleteUserSpy).toBeCalledWith();
expect(deleteUserSpy).toBeCalledTimes(1);
});

it('error message is displayed with component overrides after unsuccessful submit', async () => {
deleteUserSpy.mockRejectedValue(new Error('Mock Error'));

render(<DeleteUser components={components} />);

const deleteAccountButton = await screen.findByRole('button', {
name: 'Custom Delete Button',
});

fireEvent.click(deleteAccountButton);

const confirmDeleteButton = await screen.findByRole('button', {
name: 'Custom Confirm Button',
});

fireEvent.click(confirmDeleteButton);

await screen.findByText('Mock Error');

expect(await screen.findByText('Custom Error Message')).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,20 @@ exports[`ChangePassword renders as expected 1`] = `
</div>
</div>
`;

exports[`ChangePassword renders as expected with components overrides 1`] = `
<div>
<div
class="amplify-flex"
style="flex-direction: column;"
>
<button
class="amplify-button amplify-field-group__control"
data-fullwidth="false"
type="button"
>
Custom Delete Button
</button>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { translate } from '@aws-amplify/ui';
import React from 'react';
import { translate } from '@aws-amplify/ui';

import { Card, Flex, Text, Button, Alert } from '../../../primitives';
import { ErrorComponent, SubmitButtonComponent } from '../types';
import { DeleteUserWarningProps } from './types';

export const DefaultSubmitButton: SubmitButtonComponent = ({
children,
...rest
}) => {
return <Button {...rest}>{children}</Button>;
};
import { Button, Card, Flex, Text } from '../../../primitives';
import { DefaultError } from '../shared/Defaults';
import { DeleteUserComponents, WarningComponent } from './types';

export const DefaultWarning = ({
const DefaultWarning: WarningComponent = ({
onCancel,
onConfirm,
isDisabled,
}: DeleteUserWarningProps): JSX.Element => {
}) => {
// translations
// TODO: consolodiate translations to accountSettingsTextUtil
const warningText = translate(
Expand Down Expand Up @@ -46,10 +39,10 @@ export const DefaultWarning = ({
);
};

export const DefaultError: ErrorComponent = ({ children, ...rest }) => {
return (
<Alert variation="error" {...rest}>
{children}
</Alert>
);
const DEFAULTS: Required<DeleteUserComponents> = {
Error: DefaultError,
SubmitButton: Button,
Warning: DefaultWarning,
};

export default DEFAULTS;
25 changes: 21 additions & 4 deletions packages/react/src/components/AccountSettings/DeleteUser/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import React from 'react';

import { AmplifyUser } from '@aws-amplify/ui';

export interface DeleteUserWarningProps {
import { ErrorComponent, SubmitButtonComponent } from '../types';

export interface WarningProps {
/** called when end user cancels account deletion */
onCancel?: () => void;
onCancel: () => void;
/** called when user acknowledges account deletion */
onConfirm?: () => void;
onConfirm: () => void;
/** whether account deletion is in progress */
isDisabled?: boolean;
isDisabled: boolean;
Comment on lines -5 to +13
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made these required, because parent component always pass those in to the Warning components, and are required to function as expected

}

export type WarningComponent<Props = {}> = React.ComponentType<
Props & WarningProps
>;

export type DeleteUserState =
| 'IDLE'
| 'CONFIRMATION'
| 'DELETING'
| 'DONE'
| 'ERROR';

export interface DeleteUserComponents {
Error?: ErrorComponent;
SubmitButton?: SubmitButtonComponent;
Warning?: WarningComponent;
}

export interface DeleteUserProps {
/** custom delete user service override */
handleDelete?: (user: AmplifyUser) => Promise<void> | void;
Expand All @@ -25,4 +39,7 @@ export interface DeleteUserProps {

/** callback for unsuccessful user deletion */
onError?: (error: Error) => void;

/** custom component overrides */
components?: DeleteUserComponents;
}