mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9799a28fad
commit
266751b96d
@ -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 userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ConfirmModal } from './ConfirmModal';
|
import { ConfirmModal } from './ConfirmModal';
|
||||||
|
|
||||||
describe('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', () => {
|
it('should render correct title, body, dismiss-, alternative- and confirm-text', () => {
|
||||||
render(
|
render(
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@ -76,7 +89,7 @@ describe('ConfirmModal', () => {
|
|||||||
dismissText="Dismiss Text"
|
dismissText="Dismiss Text"
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
confirmationText="My confirmation text"
|
confirmationText="My confirmation text"
|
||||||
onConfirm={() => {}}
|
onConfirm={mockOnConfirm}
|
||||||
onDismiss={() => {}}
|
onDismiss={() => {}}
|
||||||
onAlternative={() => {}}
|
onAlternative={() => {}}
|
||||||
/>
|
/>
|
||||||
@ -85,7 +98,49 @@ describe('ConfirmModal', () => {
|
|||||||
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeDisabled();
|
expect(screen.getByRole('button', { name: 'Please Confirm' })).toBeDisabled();
|
||||||
|
|
||||||
await userEvent.type(screen.getByPlaceholderText('Type "My confirmation text" to confirm'), 'mY CoNfIrMaTiOn TeXt');
|
await user.type(screen.getByPlaceholderText('Type "My confirmation text" to confirm'), 'mY CoNfIrMaTiOn TeXt');
|
||||||
expect(screen.getByRole('button', { name: 'Please Confirm' })).not.toBeDisabled();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -37,8 +37,10 @@ export interface ConfirmModalProps {
|
|||||||
alternativeText?: string;
|
alternativeText?: string;
|
||||||
/** Confirm button variant */
|
/** Confirm button variant */
|
||||||
confirmButtonVariant?: ButtonVariant;
|
confirmButtonVariant?: ButtonVariant;
|
||||||
/** Confirm action callback */
|
/** Confirm action callback
|
||||||
onConfirm(): void;
|
* Return a promise to disable the confirm button until the promise is resolved
|
||||||
|
*/
|
||||||
|
onConfirm(): void | Promise<void>;
|
||||||
/** Dismiss action callback */
|
/** Dismiss action callback */
|
||||||
onDismiss(): void;
|
onDismiss(): void;
|
||||||
/** Alternative action callback */
|
/** Alternative action callback */
|
||||||
@ -83,6 +85,15 @@ export const ConfirmModal = ({
|
|||||||
}
|
}
|
||||||
}, [isOpen, confirmationText]);
|
}, [isOpen, confirmationText]);
|
||||||
|
|
||||||
|
const onConfirmClick = async () => {
|
||||||
|
setDisabled(true);
|
||||||
|
try {
|
||||||
|
await onConfirm();
|
||||||
|
} finally {
|
||||||
|
setDisabled(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className={cx(styles.modal, modalClass)} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
|
<Modal className={cx(styles.modal, modalClass)} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
|
||||||
<div className={styles.modalText}>
|
<div className={styles.modalText}>
|
||||||
@ -102,7 +113,7 @@ export const ConfirmModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={confirmButtonVariant}
|
variant={confirmButtonVariant}
|
||||||
onClick={onConfirm}
|
onClick={onConfirmClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
data-testid={selectors.pages.ConfirmModal.delete}
|
data-testid={selectors.pages.ConfirmModal.delete}
|
||||||
|
@ -56,7 +56,11 @@ export const CloneRuleButton = React.forwardRef<HTMLAnchorElement, CloneRuleButt
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
confirmText="Copy"
|
confirmText="Copy"
|
||||||
onConfirm={() => provRuleCloneUrl && locationService.push(provRuleCloneUrl)}
|
onConfirm={() => {
|
||||||
|
if (provRuleCloneUrl) {
|
||||||
|
locationService.push(provRuleCloneUrl);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onDismiss={() => setProvRuleCloneUrl(undefined)}
|
onDismiss={() => setProvRuleCloneUrl(undefined)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { Space } from '@grafana/experimental';
|
import { Space } from '@grafana/experimental';
|
||||||
import { ConfirmModal } from '@grafana/ui';
|
import { ConfirmModal } from '@grafana/ui';
|
||||||
@ -10,15 +10,22 @@ import { DescendantCount } from './DescendantCount';
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: () => Promise<void>;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
selectedItems: DashboardTreeSelection;
|
selectedItems: DashboardTreeSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
||||||
const onDelete = () => {
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
onConfirm();
|
const onDelete = async () => {
|
||||||
onDismiss();
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await onConfirm();
|
||||||
|
setIsDeleting(false);
|
||||||
|
onDismiss();
|
||||||
|
} catch {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -31,7 +38,7 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
confirmationText="Delete"
|
confirmationText="Delete"
|
||||||
confirmText="Delete"
|
confirmText={isDeleting ? 'Deleting...' : 'Delete'}
|
||||||
onDismiss={onDismiss}
|
onDismiss={onDismiss}
|
||||||
onConfirm={onDelete}
|
onConfirm={onDelete}
|
||||||
title="Delete"
|
title="Delete"
|
||||||
|
@ -11,20 +11,27 @@ import { DescendantCount } from './DescendantCount';
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onConfirm: (targetFolderUid: string) => void;
|
onConfirm: (targetFolderUid: string) => Promise<void>;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
selectedItems: DashboardTreeSelection;
|
selectedItems: DashboardTreeSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
||||||
const [moveTarget, setMoveTarget] = useState<string>();
|
const [moveTarget, setMoveTarget] = useState<string>();
|
||||||
|
const [isMoving, setIsMoving] = useState(false);
|
||||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||||
|
|
||||||
const onMove = () => {
|
const onMove = async () => {
|
||||||
if (moveTarget !== undefined) {
|
if (moveTarget !== undefined) {
|
||||||
onConfirm(moveTarget);
|
setIsMoving(true);
|
||||||
|
try {
|
||||||
|
await onConfirm(moveTarget);
|
||||||
|
setIsMoving(false);
|
||||||
|
onDismiss();
|
||||||
|
} catch {
|
||||||
|
setIsMoving(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onDismiss();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -45,8 +52,8 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
|
|||||||
<Button onClick={onDismiss} variant="secondary" fill="outline">
|
<Button onClick={onDismiss} variant="secondary" fill="outline">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={moveTarget === undefined} onClick={onMove} variant="primary">
|
<Button disabled={moveTarget === undefined || isMoving} onClick={onMove} variant="primary">
|
||||||
Move
|
{isMoving ? 'Moving...' : 'Move'}
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.ButtonRow>
|
</Modal.ButtonRow>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
Loading…
Reference in New Issue
Block a user