Skip to content

Commit

Permalink
chore!: move confirm dialog to admin package and refactor (#19782)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuaellis committed Mar 15, 2024
1 parent 98ecaa5 commit f2e8926
Show file tree
Hide file tree
Showing 33 changed files with 434 additions and 565 deletions.
147 changes: 147 additions & 0 deletions packages/core/admin/admin/src/components/ConfirmDialog.tsx
@@ -0,0 +1,147 @@
import * as React from 'react';

import {
Button,
ButtonProps,
Dialog,
DialogBody,
DialogFooter,
Flex,
Typography,
DialogBodyProps,
DialogProps,
DialogFooterProps,
} from '@strapi/design-system';
import { ExclamationMarkCircle } from '@strapi/icons';
import { useIntl } from 'react-intl';

/* -------------------------------------------------------------------------------------------------
* ConfirmDialog
* -----------------------------------------------------------------------------------------------*/
interface ConfirmDialogProps
extends Omit<DialogProps, 'title'>,
Partial<Pick<DialogProps, 'title'>>,
Pick<ButtonProps, 'variant'>,
Partial<DialogFooterProps>,
Pick<DialogBodyProps, 'icon'> {
onConfirm?: () => Promise<void> | void;
}

/**
* @beta
* @public
* @description A simple confirm dialog that out of the box can be used to confirm an action.
* The component can additionally be customised if required e.g. the footer actions can be
* completely replaced, but cannot be removed. Passing a string as the children prop will render
* the string as the body of the dialog. If you need more control over the body, you can pass a
* custom component as the children prop.
* @example
* ```tsx
* const DeleteAction = ({ id }) => {
* const [isOpen, setIsOpen] = React.useState(false);
*
* const [delete] = useDeleteMutation()
* const handleConfirm = async () => {
* await delete(id)
* }
*
* return (
* <>
* <Button onClick={() => setIsOpen(true)}>Delete</Button>
* <ConfirmDialog onConfirm={handleConfirm} onClose={() => setIsOpen(false)} isOpen={isOpen} />
* </>
* )
* }
* ```
*/
const ConfirmDialog = ({
children,
icon = <ExclamationMarkCircle />,
onClose,
onConfirm,
variant = 'danger',
startAction,
endAction,
...props
}: ConfirmDialogProps) => {
const { formatMessage } = useIntl();
const [isConfirming, setIsConfirming] = React.useState(false);

const content =
children ||
formatMessage({
id: 'app.confirm.body',
defaultMessage: 'Are you sure?',
});

const handleConfirm = async () => {
if (!onConfirm) {
return;
}

try {
setIsConfirming(true);
await onConfirm();
onClose();
} finally {
setIsConfirming(false);
}
};

return (
<Dialog
title={formatMessage({
id: 'app.components.ConfirmDialog.title',
defaultMessage: 'Confirmation',
})}
onClose={onClose}
{...props}
>
<DialogBody icon={icon}>
{typeof content === 'string' ? <DefaultBodyWrapper>{content}</DefaultBodyWrapper> : content}
</DialogBody>
<DialogFooter
startAction={
startAction || (
<Button onClick={onClose} variant="tertiary">
{formatMessage({
id: 'app.components.Button.cancel',
defaultMessage: 'Cancel',
})}
</Button>
)
}
endAction={
endAction || (
<Button onClick={handleConfirm} variant={variant} loading={isConfirming}>
{formatMessage({
id: 'app.components.Button.confirm',
defaultMessage: 'Confirm',
})}
</Button>
)
}
/>
</Dialog>
);
};

/* -------------------------------------------------------------------------------------------------
* DefaultBodyWrapper
* -----------------------------------------------------------------------------------------------*/
interface DefaultBodyWrapperProps {
children: React.ReactNode;
}

const DefaultBodyWrapper = ({ children }: DefaultBodyWrapperProps) => {
return (
<Flex direction="column" alignItems="stretch" gap={2}>
<Flex justifyContent="center">
<Typography variant="omega">{children}</Typography>
</Flex>
</Flex>
);
};

export { ConfirmDialog };
export type { ConfirmDialogProps };
@@ -0,0 +1,72 @@
import { render, screen } from '@tests/utils';

import { ConfirmDialog } from '../ConfirmDialog';

describe('ConfirmDialog', () => {
it('should render the ConfirmDialog with bare minimal props', () => {
render(<ConfirmDialog onConfirm={() => {}} onClose={() => {}} isOpen={true} />);

expect(screen.getByRole('dialog', { name: 'Confirmation' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Confirmation' })).toBeInTheDocument();
expect(screen.getByText('Are you sure?')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument();
});

it("should call onConfirm and onClose when the 'Confirm' button is clicked", async () => {
const onConfirm = jest.fn();
const onClose = jest.fn();
const { user } = render(
<ConfirmDialog onConfirm={onConfirm} onClose={onClose} isOpen={true} />
);

await user.click(screen.getByRole('button', { name: 'Confirm' }));

expect(onConfirm).toBeCalled();
expect(onClose).toBeCalled();
});

it("should call onClose when the 'Cancel' button is clicked", async () => {
const onClose = jest.fn();
const { user } = render(<ConfirmDialog onConfirm={() => {}} onClose={onClose} isOpen={true} />);

await user.click(screen.getByRole('button', { name: 'Cancel' }));

expect(onClose).toBeCalled();
});

it('should not render if isOpen is false', () => {
render(<ConfirmDialog onConfirm={() => {}} onClose={() => {}} isOpen={false} />);

expect(screen.queryByRole('dialog', { name: 'Confirmation' })).not.toBeInTheDocument();
});

it('should let me change the title', () => {
render(
<ConfirmDialog onConfirm={() => {}} onClose={() => {}} isOpen={true} title="Woh there kid!" />
);

expect(screen.getByRole('dialog', { name: 'Woh there kid!' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Woh there kid!' })).toBeInTheDocument();
});

it('should let me change the content', () => {
render(
<ConfirmDialog onConfirm={() => {}} onClose={() => {}} isOpen={true}>
{"Well, i'd be careful if I were you."}
</ConfirmDialog>
);

expect(screen.getByText("Well, i'd be careful if I were you.")).toBeInTheDocument();
});

it('should let me render a complete custom body', () => {
render(
<ConfirmDialog onConfirm={() => {}} onClose={() => {}} isOpen={true}>
<h2>WARNING</h2>
</ConfirmDialog>
);

expect(screen.getByRole('heading', { name: 'WARNING' })).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions packages/core/admin/admin/src/index.ts
Expand Up @@ -8,6 +8,7 @@ export * from './render';
* components
*/
export { BackButton, type BackButtonProps } from './features/BackButton';
export * from './components/ConfirmDialog';
export * from './components/Form';
export * from './components/FormInputs/Renderer';
export * from './components/PageHelpers';
Expand Down
@@ -1,10 +1,11 @@
import * as React from 'react';

import { Button, Flex, HeaderLayout } from '@strapi/design-system';
import { ConfirmDialog, useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
import { useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
import { Check, Refresh } from '@strapi/icons';
import { MessageDescriptor, useIntl } from 'react-intl';

import { ConfirmDialog } from '../../../../components/ConfirmDialog';
import { BackButton } from '../../../../features/BackButton';
import { useRegenerateTokenMutation } from '../../../../services/api';

Expand Down Expand Up @@ -76,28 +77,30 @@ const Regenerate = ({ onRegenerate, url }: RegenerateProps) => {
</Button>

<ConfirmDialog
bodyText={{
id: 'Settings.tokens.popUpWarning.message',
defaultMessage: 'Are you sure you want to regenerate this token?',
}}
iconRightButton={<Refresh />}
isConfirmButtonLoading={isLoadingConfirmation}
isOpen={showConfirmDialog}
onToggleDialog={() => setShowConfirmDialog(false)}
onConfirm={handleConfirmRegeneration}
leftButtonText={{
id: 'Settings.tokens.Button.cancel',
defaultMessage: 'Cancel',
}}
rightButtonText={{
id: 'Settings.tokens.Button.regenerate',
defaultMessage: 'Regenerate',
}}
title={{
onClose={() => setShowConfirmDialog(false)}
title={formatMessage({
id: 'Settings.tokens.RegenerateDialog.title',
defaultMessage: 'Regenerate token',
}}
/>
})}
endAction={
<Button
startIcon={<Refresh />}
loading={isLoadingConfirmation}
onClick={handleConfirmRegeneration}
>
{formatMessage({
id: 'Settings.tokens.Button.regenerate',
defaultMessage: 'Regenerate',
})}
</Button>
}
>
{formatMessage({
id: 'Settings.tokens.popUpWarning.message',
defaultMessage: 'Are you sure you want to regenerate this token?',
})}
</ConfirmDialog>
</>
);
};
Expand Down
Expand Up @@ -2,14 +2,15 @@ import * as React from 'react';

import { Box, Flex, IconButton, Typography, useCollator } from '@strapi/design-system';
import { Link } from '@strapi/design-system/v2';
import { ConfirmDialog, useQueryParams, useTracking } from '@strapi/helper-plugin';
import { useQueryParams, useTracking } from '@strapi/helper-plugin';
import { Pencil, Trash } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { NavLink, useNavigate } from 'react-router-dom';
import styled from 'styled-components';

import { ApiToken } from '../../../../../../shared/contracts/api-token';
import { SanitizedTransferToken } from '../../../../../../shared/contracts/transfer';
import { ConfirmDialog } from '../../../../components/ConfirmDialog';
import { RelativeTime } from '../../../../components/RelativeTime';
import { Table as TableImpl } from '../../../../components/Table';

Expand Down Expand Up @@ -225,7 +226,7 @@ const DeleteButton = ({ tokenName, onClickDelete, tokenType }: DeleteButtonProps
icon={<Trash />}
/>
<ConfirmDialog
onToggleDialog={() => setShowConfirmDialog(false)}
onClose={() => setShowConfirmDialog(false)}
onConfirm={handleClickDelete}
isOpen={showConfirmDialog}
/>
Expand Down
Expand Up @@ -16,7 +16,6 @@ import {
VisuallyHidden,
} from '@strapi/design-system';
import {
ConfirmDialog,
getFetchClient,
useAPIErrorHandler,
useFocusWhenNavigate,
Expand All @@ -31,6 +30,7 @@ import { Helmet } from 'react-helmet';
import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';

import { ConfirmDialog } from '../../../../components/ConfirmDialog';
import { Page } from '../../../../components/PageHelpers';
import { SearchInput } from '../../../../components/SearchInput';
import { useTypedSelector } from '../../../../core/store/hooks';
Expand Down Expand Up @@ -61,10 +61,7 @@ const ListPage = () => {
);

const navigate = useNavigate();
const [{ showModalConfirmButtonLoading, roleToDelete }, dispatch] = React.useReducer(
reducer,
initialState
);
const [{ roleToDelete }, dispatch] = React.useReducer(reducer, initialState);

const { post } = getFetchClient();

Expand Down Expand Up @@ -275,8 +272,7 @@ const ListPage = () => {
<ConfirmDialog
isOpen={isWarningDeleteAllOpened}
onConfirm={handleDeleteData}
isConfirmButtonLoading={showModalConfirmButtonLoading}
onToggleDialog={handleToggleModal}
onClose={handleToggleModal}
/>
</Main>
);
Expand Down

0 comments on commit f2e8926

Please sign in to comment.