Skip to content

Commit

Permalink
fix: Warn users when trying to delete a container
Browse files Browse the repository at this point in the history
  • Loading branch information
Shivam Gupta authored and lhein committed May 8, 2024
1 parent b9effe1 commit d51e8e5
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 16 deletions.
6 changes: 6 additions & 0 deletions packages/ui-tests/cypress/support/next-commands/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,11 @@ Cypress.Commands.add('deleteRoute', (index: number) => {
cy.get('button[data-testid^="delete-btn-route"]').then((buttons) => {
cy.wrap(buttons[index]).click();
});
cy.get('body').then(($body) => {
if ($body.find('.pf-m-danger').length) {
// Delete Confirmation Modal appeared, click on the confirm button
cy.get('.pf-m-danger').click();
}
});
cy.closeFlowsListIfVisible();
});
6 changes: 6 additions & 0 deletions packages/ui-tests/cypress/support/next-commands/design.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ Cypress.Commands.add('checkConfigInputObject', (inputName: string, value: string

Cypress.Commands.add('removeNodeByName', (nodeName: string, nodeIndex?: number) => {
cy.performNodeAction(nodeName, 'delete', nodeIndex);
cy.get('body').then(($body) => {
if ($body.find('.pf-m-danger').length) {
// Delete Confirmation Modal appeared, click on the confirm button
cy.get('.pf-m-danger').click();
}
});
cy.get(nodeName).should('not.exist');
// wait for the canvas rerender
cy.wait(1000);
Expand Down
33 changes: 27 additions & 6 deletions packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TestProvidersWrapper } from '../../../stubs';
import { camelRouteJson } from '../../../stubs/camel-route';
import { kameletJson } from '../../../stubs/kamelet-route';
import { Canvas } from './Canvas';
import { DeleteModalContextProvider } from '../../../providers';

describe('Canvas', () => {
const entity = new CamelRouteVisualEntity(camelRouteJson);
Expand Down Expand Up @@ -56,9 +57,11 @@ describe('Canvas', () => {
} as unknown as VisibleFLowsContextResult,
});
const wrapper = render(
<Provider>
<Canvas entities={routeEntities} />
</Provider>,
<DeleteModalContextProvider>
<Provider>
<Canvas entities={routeEntities} />
</Provider>
</DeleteModalContextProvider>,
);

// Right click anywhere on the container label
Expand All @@ -75,6 +78,14 @@ describe('Canvas', () => {
fireEvent.click(deleteRoute);
});

// Deal with the Confirmation modal
const deleteConfirmation = screen.getByRole('button', { name: 'Confirm' });
expect(deleteConfirmation).toBeInTheDocument();

await act(async () => {
fireEvent.click(deleteConfirmation);
});

// Check if the remove function is called
expect(removeSpy).toHaveBeenCalled();
expect(removeSpy).toHaveBeenCalledWith('route-8888');
Expand All @@ -93,9 +104,11 @@ describe('Canvas', () => {
});

const wrapper = render(
<Provider>
<Canvas entities={kameletEntities} />
</Provider>,
<DeleteModalContextProvider>
<Provider>
<Canvas entities={kameletEntities} />
</Provider>
</DeleteModalContextProvider>,
);

// Right click anywhere on the container label
Expand All @@ -113,6 +126,14 @@ describe('Canvas', () => {
fireEvent.click(deleteKamelet);
});

// Deal with the Confirmation modal
const deleteConfirmation = screen.getByRole('button', { name: 'Confirm' });
expect(deleteConfirmation).toBeInTheDocument();

await act(async () => {
fireEvent.click(deleteConfirmation);
});

// Check if the remove function is called
expect(removeSpy).toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { FunctionComponent, useCallback, useContext, useRef } from 'react';
import { BaseVisualCamelEntity } from '../../../../models/visualization/base-visual-entity';
import { EntitiesContext } from '../../../../providers/entities.provider';
import { DeleteModalContext } from '../../../../providers/delete-modal.provider';
import { VisibleFlowsContext } from '../../../../providers/visible-flows.provider';
import { InlineEdit } from '../../../InlineEdit';
import './FlowsList.scss';
Expand All @@ -18,6 +19,7 @@ interface IFlowsList {
export const FlowsList: FunctionComponent<IFlowsList> = (props) => {
const { visualEntities, camelResource, updateEntitiesFromCamelResource } = useContext(EntitiesContext)!;
const { visibleFlows, visualFlowsApi } = useContext(VisibleFlowsContext)!;
const deleteModalContext = useContext(DeleteModalContext);

const isListEmpty = visualEntities.length === 0;

Expand Down Expand Up @@ -102,7 +104,11 @@ export const FlowsList: FunctionComponent<IFlowsList> = (props) => {
data-testid={`delete-btn-${flow.id}`}
icon={<TrashIcon />}
variant="plain"
onClick={(event) => {
onClick={async (event) => {
const isDeleteConfirmed = await deleteModalContext?.deleteConfirmation();

if (!isDeleteConfirmed) return;

camelResource.removeEntity(flow.id);
updateEntitiesFromCamelResource();
/** Required to avoid closing the Dropdown after clicking in the icon */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,14 @@ export const CustomNodeWithSelection: typeof DefaultNode = withSelection()(
}

if (nodeInteractions.canRemoveStep) {
const shouldConfirmBeforeDeletion = nodeInteractions.canHaveChildren || nodeInteractions.canHaveSpecialChildren;
items.push(
<ItemDeleteStep key="context-menu-item-delete" data-testid="context-menu-item-delete" vizNode={vizNode} />,
<ItemDeleteStep
key="context-menu-item-delete"
data-testid="context-menu-item-delete"
vizNode={vizNode}
loadModal={shouldConfirmBeforeDeletion}
/>,
);
}
if (nodeInteractions.canRemoveFlow) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ import { FunctionComponent, PropsWithChildren, useCallback, useContext } from 'r
import { IDataTestID } from '../../../models';
import { IVisualizationNode } from '../../../models/visualization/base-visual-entity';
import { EntitiesContext } from '../../../providers/entities.provider';
import { DeleteModalContext } from '../../../providers/delete-modal.provider';

interface ItemDeleteGroupProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
}

export const ItemDeleteGroup: FunctionComponent<ItemDeleteGroupProps> = (props) => {
const entitiesContext = useContext(EntitiesContext);
const deleteModalContext = useContext(DeleteModalContext);
const flowId = props.vizNode?.getBaseEntity()?.getId();

const onRemoveGroup = useCallback(() => {
const onRemoveGroup = useCallback(async () => {
/** Open delete confirm modal, get the confirmation */
const isDeleteConfirmed = await deleteModalContext?.deleteConfirmation();

if (!isDeleteConfirmed) return;

entitiesContext?.camelResource.removeEntity(flowId);
entitiesContext?.updateEntitiesFromCamelResource();
}, [entitiesContext, flowId]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ import { FunctionComponent, PropsWithChildren, useCallback, useContext } from 'r
import { IDataTestID } from '../../../models';
import { IVisualizationNode } from '../../../models/visualization/base-visual-entity';
import { EntitiesContext } from '../../../providers/entities.provider';
import { DeleteModalContext } from '../../../providers/delete-modal.provider';

interface ItemDeleteStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
loadModal: boolean;
}

export const ItemDeleteStep: FunctionComponent<ItemDeleteStepProps> = (props) => {
const entitiesContext = useContext(EntitiesContext);
const deleteModalContext = useContext(DeleteModalContext);

const onRemoveNode = useCallback(async () => {
if (props.loadModal) {
/** Open delete confirm modal, get the confirmation */
const isDeleteConfirmed = await deleteModalContext?.deleteConfirmation();

if (!isDeleteConfirmed) return;
}

const onRemoveNode = useCallback(() => {
props.vizNode?.removeChild();
entitiesContext?.updateEntitiesFromCamelResource();
}, [entitiesContext, props.vizNode]);
Expand Down
5 changes: 4 additions & 1 deletion packages/ui/src/multiplying-architecture/KaotoBridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CatalogLoaderProvider } from '../providers/catalog.provider';
import { SchemasLoaderProvider } from '../providers/schemas.provider';
import { SourceCodeApiContext } from '../providers/source-code.provider';
import { VisibleFlowsProvider } from '../providers/visible-flows.provider';
import { DeleteModalContextProvider } from '../providers/delete-modal.provider';
import { EventNotifier } from '../utils';

interface KaotoBridgeProps {
Expand Down Expand Up @@ -144,7 +145,9 @@ export const KaotoBridge = forwardRef<EditorApi, PropsWithChildren<KaotoBridgePr
<CatalogLoaderProvider catalogUrl={props.catalogUrl}>
<CatalogTilesProvider>
<VisibleFlowsProvider>
<CatalogModalProvider>{props.children}</CatalogModalProvider>
<CatalogModalProvider>
<DeleteModalContextProvider>{props.children}</DeleteModalContextProvider>
</CatalogModalProvider>
</VisibleFlowsProvider>
</CatalogTilesProvider>
</CatalogLoaderProvider>
Expand Down
13 changes: 8 additions & 5 deletions packages/ui/src/pages/Design/DesignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CatalogModalProvider } from '../../providers/catalog-modal.provider';
import { EntitiesContext } from '../../providers/entities.provider';
import './DesignPage.scss';
import { ReturnToSourceCodeFallback } from './ReturnToSourceCodeFallback';
import { DeleteModalContextProvider } from '../../providers/delete-modal.provider';

export const DesignPage: FunctionComponent = () => {
const entitiesContext = useContext(EntitiesContext);
Expand All @@ -12,11 +13,13 @@ export const DesignPage: FunctionComponent = () => {
return (
<div className="canvas-page">
<CatalogModalProvider>
<Visualization
className="canvas-page__canvas"
entities={visualEntities}
fallback={<ReturnToSourceCodeFallback />}
/>
<DeleteModalContextProvider>
<Visualization
className="canvas-page__canvas"
entities={visualEntities}
fallback={<ReturnToSourceCodeFallback />}
/>
</DeleteModalContextProvider>
</CatalogModalProvider>
</div>
);
Expand Down
56 changes: 56 additions & 0 deletions packages/ui/src/providers/delete-modal.provider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { useContext } from 'react';
import { DeleteModalContextProvider, DeleteModalContext } from './delete-modal.provider';

let deleteConfirmationResult: boolean | undefined;

describe('DeleteModalProvider', () => {
beforeEach(() => {
deleteConfirmationResult = undefined;
});

it('calls deleteConfirmation with true when Confirm button is clicked', async () => {
render(
<DeleteModalContextProvider>
<TestComponent />
</DeleteModalContextProvider>,
);

const deleteButton = screen.getByText('Delete');
fireEvent.click(deleteButton);

const confirmButton = await screen.findByRole('button', { name: 'Confirm' });
fireEvent.click(confirmButton);

// Wait for deleteConfirmation promise to resolve
await waitFor(() => expect(deleteConfirmationResult).toEqual(true));
});

it('calls deleteConfirmation with false when Cancel button is clicked', async () => {
render(
<DeleteModalContextProvider>
<TestComponent />
</DeleteModalContextProvider>,
);

const deleteButton = screen.getByText('Delete');
fireEvent.click(deleteButton);

const cancelButton = screen.getByRole('button', { name: 'Cancel' });
fireEvent.click(cancelButton);

// Wait for deleteConfirmation promise to resolve
await waitFor(() => expect(deleteConfirmationResult).toEqual(false));
});
});

const TestComponent = () => {
const { deleteConfirmation } = useContext(DeleteModalContext)!;

const handleDelete = async () => {
const confirmation = await deleteConfirmation();
deleteConfirmationResult = confirmation;
};

return <button onClick={handleDelete}>Delete</button>;
};
75 changes: 75 additions & 0 deletions packages/ui/src/providers/delete-modal.provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Modal, ModalVariant, Button } from '@patternfly/react-core';
import { FunctionComponent, PropsWithChildren, createContext, useCallback, useMemo, useRef, useState } from 'react';

interface DeleteModalContextValue {
deleteConfirmation: () => Promise<boolean>;
}

export const DeleteModalContext = createContext<DeleteModalContextValue | undefined>(undefined);

/**
* This provider is used to open the Delete Confirmation modal.
* The modal loads when the user clicks on the delete Routes/Kamelets of remove any Step from the Context Menu.
*/
export const DeleteModalContextProvider: FunctionComponent<PropsWithChildren> = (props) => {
const [isModalOpen, setIsModalOpen] = useState(false);

const deleteConfirmationRef = useRef<{
resolve: (confirm: boolean) => void;
reject: (error: unknown) => unknown;
}>();

const handleCloseModal = useCallback(() => {
setIsModalOpen(false);
deleteConfirmationRef.current?.resolve(false);
}, []);

const handleDeleteConfirm = useCallback(() => {
setIsModalOpen(false);
deleteConfirmationRef.current?.resolve(true);
}, []);

const deleteConfirmation = useCallback(() => {
const deleteConfirmationPromise = new Promise<boolean>((resolve, reject) => {
/** Set both resolve and reject functions to be used once the user choose an action */
deleteConfirmationRef.current = { resolve, reject };
});

setIsModalOpen(true);

return deleteConfirmationPromise;
}, []);

const value: DeleteModalContextValue = useMemo(
() => ({
deleteConfirmation,
}),
[],
);

return (
<DeleteModalContext.Provider value={value}>
{props.children}

{isModalOpen && (
<Modal
variant={ModalVariant.small}
title="Delete?"
isOpen
onClose={handleCloseModal}
ouiaId="DeleteConfirmModal"
actions={[
<Button key="confirm" variant="danger" onClick={handleDeleteConfirm}>
Confirm
</Button>,
<Button key="cancel" variant="link" onClick={handleCloseModal}>
Cancel
</Button>,
]}
>
Are you sure you want to delete?
</Modal>
)}
</DeleteModalContext.Provider>
);
};
1 change: 1 addition & 0 deletions packages/ui/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './schemas.provider';
export * from './source-code.provider';
export * from './catalog.provider';
export * from './catalog-tiles.provider';
export * from './delete-modal.provider';
export * from './catalog-modal.provider';

0 comments on commit d51e8e5

Please sign in to comment.