NestedFolders: stay in the modal whilst actions complete (#69730)

* stay in the modal whilst actions complete

* don't return anything here to fix types

* ensure we're always resetting button state
This commit is contained in:
Ashley Harrison 2023-06-12 16:53:17 +01:00 committed by GitHub
parent 9799a28fad
commit 266751b96d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 20 deletions

View File

@ -1,10 +1,23 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ConfirmModal } from './ConfirmModal';
describe('ConfirmModal', () => {
const mockOnConfirm = jest.fn();
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
jest.useFakeTimers();
user = userEvent.setup({ delay: null });
});
afterEach(() => {
jest.useRealTimers();
});
it('should render correct title, body, dismiss-, alternative- and confirm-text', () => {
render(
<ConfirmModal
@ -76,7 +89,7 @@ describe('ConfirmModal', () => {
dismissText="Dismiss Text"
isOpen={true}
confirmationText="My confirmation text"
onConfirm={() => {}}
onConfirm={mockOnConfirm}
onDismiss={() => {}}
onAlternative={() => {}}
/>
@ -85,7 +98,49 @@ describe('ConfirmModal', () => {
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeDisabled();
await userEvent.type(screen.getByPlaceholderText('Type "My confirmation text" to confirm'), 'mY CoNfIrMaTiOn TeXt');
expect(screen.getByRole('button', { name: 'Please Confirm' })).not.toBeDisabled();
await user.type(screen.getByPlaceholderText('Type "My confirmation text" to confirm'), 'mY CoNfIrMaTiOn TeXt');
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeEnabled();
await user.click(screen.getByRole('button', { name: 'Please Confirm' }));
expect(mockOnConfirm).toHaveBeenCalled();
});
it('returning a promise in the onConfirm callback disables the button whilst the callback is in progress', async () => {
mockOnConfirm.mockImplementation(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('');
}, 1000);
});
});
render(
<ConfirmModal
title="Some Title"
body="Some Body"
confirmText="Please Confirm"
alternativeText="Alternative Text"
dismissText="Dismiss Text"
isOpen={true}
confirmationText="My confirmation text"
onConfirm={mockOnConfirm}
onDismiss={() => {}}
onAlternative={() => {}}
/>
);
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeDisabled();
await user.type(screen.getByPlaceholderText('Type "My confirmation text" to confirm'), 'My confirmation text');
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeEnabled();
await user.click(screen.getByRole('button', { name: 'Please Confirm' }));
expect(mockOnConfirm).toHaveBeenCalled();
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeDisabled();
jest.runAllTimers();
await waitFor(() => {
return expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeEnabled();
});
});
});

View File

@ -37,8 +37,10 @@ export interface ConfirmModalProps {
alternativeText?: string;
/** Confirm button variant */
confirmButtonVariant?: ButtonVariant;
/** Confirm action callback */
onConfirm(): void;
/** Confirm action callback
* Return a promise to disable the confirm button until the promise is resolved
*/
onConfirm(): void | Promise<void>;
/** Dismiss action callback */
onDismiss(): void;
/** Alternative action callback */
@ -83,6 +85,15 @@ export const ConfirmModal = ({
}
}, [isOpen, confirmationText]);
const onConfirmClick = async () => {
setDisabled(true);
try {
await onConfirm();
} finally {
setDisabled(false);
}
};
return (
<Modal className={cx(styles.modal, modalClass)} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
<div className={styles.modalText}>
@ -102,7 +113,7 @@ export const ConfirmModal = ({
</Button>
<Button
variant={confirmButtonVariant}
onClick={onConfirm}
onClick={onConfirmClick}
disabled={disabled}
ref={buttonRef}
data-testid={selectors.pages.ConfirmModal.delete}

View File

@ -56,7 +56,11 @@ export const CloneRuleButton = React.forwardRef<HTMLAnchorElement, CloneRuleButt
</div>
}
confirmText="Copy"
onConfirm={() => provRuleCloneUrl && locationService.push(provRuleCloneUrl)}
onConfirm={() => {
if (provRuleCloneUrl) {
locationService.push(provRuleCloneUrl);
}
}}
onDismiss={() => setProvRuleCloneUrl(undefined)}
/>
</>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Space } from '@grafana/experimental';
import { ConfirmModal } from '@grafana/ui';
@ -10,15 +10,22 @@ import { DescendantCount } from './DescendantCount';
export interface Props {
isOpen: boolean;
onConfirm: () => void;
onConfirm: () => Promise<void>;
onDismiss: () => void;
selectedItems: DashboardTreeSelection;
}
export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
const onDelete = () => {
onConfirm();
const [isDeleting, setIsDeleting] = useState(false);
const onDelete = async () => {
setIsDeleting(true);
try {
await onConfirm();
setIsDeleting(false);
onDismiss();
} catch {
setIsDeleting(false);
}
};
return (
@ -31,7 +38,7 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
</>
}
confirmationText="Delete"
confirmText="Delete"
confirmText={isDeleting ? 'Deleting...' : 'Delete'}
onDismiss={onDismiss}
onConfirm={onDelete}
title="Delete"

View File

@ -11,20 +11,27 @@ import { DescendantCount } from './DescendantCount';
export interface Props {
isOpen: boolean;
onConfirm: (targetFolderUid: string) => void;
onConfirm: (targetFolderUid: string) => Promise<void>;
onDismiss: () => void;
selectedItems: DashboardTreeSelection;
}
export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
const [moveTarget, setMoveTarget] = useState<string>();
const [isMoving, setIsMoving] = useState(false);
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
const onMove = () => {
const onMove = async () => {
if (moveTarget !== undefined) {
onConfirm(moveTarget);
}
setIsMoving(true);
try {
await onConfirm(moveTarget);
setIsMoving(false);
onDismiss();
} catch {
setIsMoving(false);
}
}
};
return (
@ -45,8 +52,8 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
<Button onClick={onDismiss} variant="secondary" fill="outline">
Cancel
</Button>
<Button disabled={moveTarget === undefined} onClick={onMove} variant="primary">
Move
<Button disabled={moveTarget === undefined || isMoving} onClick={onMove} variant="primary">
{isMoving ? 'Moving...' : 'Move'}
</Button>
</Modal.ButtonRow>
</Modal>