Skip to content

Commit

Permalink
feat: add delete confirm modal for widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
tracy-french committed Sep 29, 2023
1 parent f59a069 commit 84fb016
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 37 deletions.
4 changes: 4 additions & 0 deletions packages/dashboard/e2e/tests/dashboard/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ test('dashboard add and remove multiple widgets', async ({ page }) => {

await page.keyboard.down('Delete');

const deleteBtn = await page.getByRole('button', { name: 'Delete', exact: true });

await deleteBtn.click();

const deletedWidgets = await grid.widgets();
expect(deletedWidgets).toHaveLength(0);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import ConfirmDeleteModal from './index';

describe('Confirm Delete Modal', () => {
test('renders correctly', () => {
const props = {
headerTitle: 'Confirmation',
submitTitle: 'Submit',
description: 'Are you sure you want to submit?',
visible: true,
handleDismiss: jest.fn(),
handleCancel: jest.fn(),
handleSubmit: jest.fn(),
};
const { getByText, getByTestId } = render(<ConfirmDeleteModal {...props} />);

// Assert that the header, description, cancel button, and submit button are rendered correctly
expect(getByText('Confirmation')).toBeInTheDocument();
expect(getByText('Are you sure you want to submit?')).toBeInTheDocument();
expect(getByText('Cancel')).toBeInTheDocument();
expect(getByTestId('custom-orange-button')).toBeInTheDocument();
});

test('calls handleCancel when cancel button is clicked', () => {
const handleCancel = jest.fn();
const props = {
headerTitle: 'Confirmation',
submitTitle: 'Submit',
description: 'Are you sure you want to submit?',
visible: true,
handleDismiss: jest.fn(),
handleCancel,
handleSubmit: jest.fn(),
};
const { getByText } = render(<ConfirmDeleteModal {...props} />);
const cancelButton = getByText('Cancel');

fireEvent.click(cancelButton); // Simulate clicking the cancel button

// Assert that handleCancel is called
expect(handleCancel).toHaveBeenCalled();
});

test('calls handleSubmit when submit button is clicked', () => {
const handleSubmit = jest.fn();
const props = {
headerTitle: 'Confirmation',
submitTitle: 'Submit',
description: 'Are you sure you want to submit?',
visible: true,
handleDismiss: jest.fn(),
handleCancel: jest.fn(),
handleSubmit,
};
const { getByTestId } = render(<ConfirmDeleteModal {...props} />);
const submitButton = getByTestId('custom-orange-button');

fireEvent.click(submitButton); // Simulate clicking the submit button

// Assert that handleSubmit is called
expect(handleSubmit).toHaveBeenCalled();
});
});
52 changes: 52 additions & 0 deletions packages/dashboard/src/components/confirmDeleteModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { ReactElement } from 'react';

import { Box, Button, Modal, SpaceBetween } from '@cloudscape-design/components';

import CustomOrangeButton from '../customOrangeButton';

interface ConfirmDeleteModalProps {
headerTitle: string;
cancelTitle?: string;
submitTitle: string;
description: ReactElement | string;
visible: boolean;
handleDismiss: () => void;
handleCancel: () => void;
handleSubmit: () => void;
}

const ConfirmDeleteModal = ({
headerTitle,
cancelTitle = 'Cancel',
submitTitle,
description,
visible,
handleDismiss,
handleCancel,
handleSubmit,
}: ConfirmDeleteModalProps) => {
return (
<Modal
visible={visible}
onDismiss={handleDismiss}
header={headerTitle}
data-testid='confirm-modal'
footer={
<Box float='right'>
<SpaceBetween direction='horizontal' size='xs'>
{cancelTitle && (
<Button variant='link' onClick={handleCancel}>
{cancelTitle}
</Button>
)}
<CustomOrangeButton title={submitTitle} handleClick={handleSubmit} />
</SpaceBetween>
</Box>
}
>
{description}
</Modal>
);
};

export default ConfirmDeleteModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.btn-custom-primary {
border-color: var(--colors-background-button) !important;
background-color: var(--colors-background-button) !important;
}

.btn-custom-primary:hover {
border-color: var(--colors-button-hover) !important;
background-color: var(--colors-button-hover) !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import CustomOrangeButton from './index';

describe('CustomOrangeButton', () => {
const title = 'Test Button';
const handleClick = jest.fn();

test('renders button with correct title', () => {
const { getByText } = render(<CustomOrangeButton title={title} handleClick={handleClick} />);
expect(getByText(title)).toBeInTheDocument();
});
test('calls handleClick when button is clicked', () => {
const { getByRole } = render(<CustomOrangeButton title={title} handleClick={handleClick} />);
const button = getByRole('button');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
20 changes: 20 additions & 0 deletions packages/dashboard/src/components/customOrangeButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

import { colorBackgroundHomeHeader } from '@cloudscape-design/design-tokens';
import { Button, ButtonProps } from '@cloudscape-design/components';

import './index.css';

const CustomOrangeButton = ({
title,
handleClick,
...rest
}: { title: string; handleClick: () => void } & ButtonProps) => {
return (
<Button className='btn-custom-primary' onClick={handleClick} data-testid='custom-orange-button' {...rest}>
<span style={{ color: colorBackgroundHomeHeader }}>{title}</span>
</Button>
);
};

export default CustomOrangeButton;
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export const usePointerTracker = ({ readOnly, enabled, union, click }: PointerTr

const onPointerUp: PointerEventHandler = (e) => {
if (cancelClick || !enabled || readOnly) return;

if (e.button === MouseClick.Left) {
click({
position: getDashboardPosition(e),
Expand Down
10 changes: 10 additions & 0 deletions packages/dashboard/src/components/internalDashboard/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ const EMPTY_DASHBOARD: DashboardWidgetsConfiguration = {
viewport: { duration: '10m' },
};

jest.mock('../../store/actions', () => {
const originalModule = jest.requireActual('../../store/actions');

return {
__esModule: true,
...originalModule,
onDeleteWidgetsAction: jest.fn(),
};
});

// TODO: fix these tests (likely need to mock TwinMaker client)
it.skip('saves when the save button is pressed with default grid settings provided', function () {
const onSave = jest.fn().mockImplementation(() => Promise.resolve());
Expand Down
35 changes: 33 additions & 2 deletions packages/dashboard/src/components/internalDashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type { WidgetsProps } from '../widgets/list';
import type { UserSelectionProps } from '../userSelection';
import type { DashboardState } from '~/store/state';
import { useSelectedWidgets } from '~/hooks/useSelectedWidgets';
import ConfirmDeleteModal from '../confirmDeleteModal';

import '@iot-app-kit/components/styles.css';
import './index.css';
Expand Down Expand Up @@ -87,6 +88,7 @@ const InternalDashboard: React.FC<InternalDashboardProperties> = ({ onSave, edit
const significantDigits = useSelector((state: DashboardState) => state.significantDigits);

const [viewFrame, setViewFrameElement] = useState<HTMLDivElement | undefined>(undefined);
const [visible, setVisible] = useState<boolean>(false);

const dispatch = useDispatch();
const createWidgets = (widgets: DashboardWidget[]) =>
Expand Down Expand Up @@ -121,19 +123,24 @@ const InternalDashboard: React.FC<InternalDashboardProperties> = ({ onSave, edit
};

const deleteWidgets = () => {
setVisible(true);
};

const onDelete = () => {
dispatch(
onDeleteWidgetsAction({
widgets: selectedWidgets,
})
);
setVisible(false);
};

const widgetLength = dashboardConfiguration.widgets.length;

/**
* setup keyboard shortcuts for actions
*/
useKeyboardShortcuts();
useKeyboardShortcuts({ deleteWidgets });

/**
* setup gesture handling for grid
Expand Down Expand Up @@ -281,7 +288,31 @@ const InternalDashboard: React.FC<InternalDashboardProperties> = ({ onSave, edit

return (
<TrendCursorSync>
<TimeSync group='dashboard-timesync'>{readOnly ? ReadOnlyComponent : EditComponent}</TimeSync>
<TimeSync initialViewport={{ duration: '5m' }} group='dashboard-timesync'>
{readOnly ? ReadOnlyComponent : EditComponent}
<ConfirmDeleteModal
visible={visible}
headerTitle={`Delete selected widget${selectedWidgets.length > 1 ? 's' : ''}?`}
cancelTitle='Cancel'
submitTitle='Delete'
description={
<Box>
<Box variant='p'>
{`Are you sure you want to delete the selected widget${
selectedWidgets.length > 1 ? 's' : ''
}? You'll lose all the progress you made to the
widget${selectedWidgets.length > 1 ? 's' : ''}`}
</Box>
<Box variant='p' padding={{ top: 'm' }}>
You cannot undo this action.
</Box>
</Box>
}
handleDismiss={() => setVisible(false)}
handleCancel={() => setVisible(false)}
handleSubmit={onDelete}
/>
</TimeSync>
</TrendCursorSync>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import { act } from '@testing-library/react';
import InternalDashboard from './index';
import { configureDashboardStore } from '../../store';

import {
onBringWidgetsToFrontAction,
onDeleteWidgetsAction,
onSelectWidgetsAction,
onSendWidgetsToBackAction,
} from '../../store/actions';
import { onBringWidgetsToFrontAction, onSelectWidgetsAction, onSendWidgetsToBackAction } from '../../store/actions';

jest.mock('../../store/actions', () => {
const originalModule = jest.requireActual('../../store/actions');
Expand Down Expand Up @@ -85,18 +80,6 @@ it.skip('can clear the selection', () => {
});
});

// TODO: fix these tests (likely need to mock TwinMaker client)
it.skip('can delete the selection', () => {
(onDeleteWidgetsAction as jest.Mock).mockImplementation(() => ({ type: '', payload: {} }));

renderDashboardAndPressKey({ key: 'Backspace', meta: false });

expect(onDeleteWidgetsAction).toBeCalledWith({
widgets: [],
});
});

// TODO: fix these tests (likely need to mock TwinMaker client)
it.skip('can send the selection to the back', () => {
(onSendWidgetsToBackAction as jest.Mock).mockImplementation(() => ({ type: '', payload: {} }));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import {
} from '../../store/actions';
import { DASHBOARD_CONTAINER_ID } from '../grid/getDashboardPosition';
import { useSelectedWidgets } from '~/hooks/useSelectedWidgets';
import { isFunction } from 'lodash';

export const useKeyboardShortcuts = () => {
type useKeyboardShortcutsProps = {
deleteWidgets?: () => void;
};

export const useKeyboardShortcuts = ({ deleteWidgets: handleDeleteWidgetModal }: useKeyboardShortcutsProps) => {
const dispatch = useDispatch();
const selectedWidgets = useSelectedWidgets();

Expand Down Expand Up @@ -46,11 +51,15 @@ export const useKeyboardShortcuts = () => {
};

const deleteWidgets = useCallback(() => {
dispatch(
onDeleteWidgetsAction({
widgets: selectedWidgets,
})
);
if (isFunction(handleDeleteWidgetModal)) {
handleDeleteWidgetModal();
} else {
dispatch(
onDeleteWidgetsAction({
widgets: selectedWidgets,
})
);
}
}, [selectedWidgets]);

/**
Expand All @@ -59,10 +68,12 @@ export const useKeyboardShortcuts = () => {
* other areas where we might use keyboard interactions such as
* the settings pane or a text area in a widget
*/

const keyPressFilter = (e: KeyboardEvent) =>
e.target !== null &&
e.target instanceof Element &&
(e.target.id === DASHBOARD_CONTAINER_ID || e.target === document.body);

useKeyPress('esc', { filter: keyPressFilter, callback: onClearSelection });
useKeyPress('backspace, del', { filter: keyPressFilter, callback: deleteWidgets });
useKeyPress('mod+c', { filter: keyPressFilter, callback: copyWidgets });
Expand Down
5 changes: 5 additions & 0 deletions packages/dashboard/src/components/widgets/tile/tile.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ describe('WidgetTile', () => {
removeButton?.click();
});

const deleteBtn = screen.getByText('Delete');
expect(deleteBtn).toBeInTheDocument();
act(() => {
deleteBtn?.click();
});
expect(store.getState().dashboardConfiguration.widgets).toEqual([]);
});

Expand Down

0 comments on commit 84fb016

Please sign in to comment.