Skip to content

Commit

Permalink
Fix(canvas): Show confirmation dialog for replacing a step with children
Browse files Browse the repository at this point in the history
  • Loading branch information
shivamG640 committed Sep 18, 2024
1 parent 6eb1529 commit 97fe010
Show file tree
Hide file tree
Showing 18 changed files with 394 additions and 71 deletions.
10 changes: 5 additions & 5 deletions packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,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';
import { ActionConfirmationModalContextProvider } from '../../../providers';

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

// Right click anywhere on the container label
Expand Down Expand Up @@ -103,11 +103,11 @@ describe('Canvas', () => {
});

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

// Right click anywhere on the container label
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { VisualFlowsApi } from '../../../../models/visualization/flows/support/f
import { VisibleFLowsContextResult } from '../../../../providers/visible-flows.provider';
import { TestProvidersWrapper } from '../../../../stubs';
import { FlowsList } from './FlowsList';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';

describe('FlowsList.tsx', () => {
let camelResource: CamelRouteResource;
Expand Down Expand Up @@ -102,6 +103,30 @@ describe('FlowsList.tsx', () => {
expect(onCloseSpy).toHaveBeenCalledTimes(1);
});

it('should show delete confirmation modal when clicking on a delete icon', async () => {
const mockDeleteModalContext = {
actionConfirmation: jest.fn(),
};

const { Provider } = TestProvidersWrapper({ camelResource });
const wrapper = render(
<Provider>
<ActionConfirmationModalContext.Provider value={mockDeleteModalContext}>
<FlowsList />
</ActionConfirmationModalContext.Provider>
</Provider>,
);

act(() => {
fireEvent.click(wrapper.getByTestId('delete-btn-route-1234'));
});

expect(mockDeleteModalContext.actionConfirmation).toHaveBeenCalledWith({
title: 'Permanently delete flow?',
text: 'All steps will be lost.',
});
});

it('should toggle the visibility of a flow clicking on the Eye icon', async () => {
let resId = '';
const visualFlowsApi = new VisualFlowsApi(jest.fn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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 { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { VisibleFlowsContext } from '../../../../providers/visible-flows.provider';
import { InlineEdit } from '../../../InlineEdit';
import './FlowsList.scss';
Expand All @@ -19,7 +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 deleteModalContext = useContext(ActionConfirmationModalContext);

const isListEmpty = visualEntities.length === 0;

Expand Down Expand Up @@ -105,7 +105,7 @@ export const FlowsList: FunctionComponent<IFlowsList> = (props) => {
icon={<TrashIcon />}
variant="plain"
onClick={async (event) => {
const isDeleteConfirmed = await deleteModalContext?.deleteConfirmation({
const isDeleteConfirmed = await deleteModalContext?.actionConfirmation({
title: 'Permanently delete flow?',
text: 'All steps will be lost.',
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { fireEvent, render } from '@testing-library/react';
import { createVisualizationNode } from '../../../../models';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { ItemDeleteGroup } from './ItemDeleteGroup';

describe('ItemDeleteGroup', () => {
const vizNode = createVisualizationNode('test', {});

const mockDeleteModalContext = {
actionConfirmation: jest.fn(),
};

afterEach(() => {
jest.clearAllMocks();
});

it('should render delete ContextMenuItem', () => {
const { container } = render(<ItemDeleteGroup vizNode={vizNode} />);

expect(container).toMatchSnapshot();
});

it('should open delete confirmation modal on click', async () => {
const wrapper = render(
<ActionConfirmationModalContext.Provider value={mockDeleteModalContext}>
<ItemDeleteGroup vizNode={vizNode} />
</ActionConfirmationModalContext.Provider>,
);

fireEvent.click(wrapper.getByText('Delete'));

expect(mockDeleteModalContext.actionConfirmation).toHaveBeenCalledWith({
title: 'Permanently delete flow?',
text: 'All steps will be lost.',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ContextMenuItem } from '@patternfly/react-topology';
import { FunctionComponent, PropsWithChildren, useCallback, useContext } from 'react';
import { IDataTestID } from '../../../../models';
import { IVisualizationNode } from '../../../../models/visualization/base-visual-entity';
import { DeleteModalContext } from '../../../../providers/delete-modal.provider';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { EntitiesContext } from '../../../../providers/entities.provider';

interface ItemDeleteGroupProps extends PropsWithChildren<IDataTestID> {
Expand All @@ -12,12 +12,12 @@ interface ItemDeleteGroupProps extends PropsWithChildren<IDataTestID> {

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

const onRemoveGroup = useCallback(async () => {
/** Open delete confirm modal, get the confirmation */
const isDeleteConfirmed = await deleteModalContext?.deleteConfirmation({
const isDeleteConfirmed = await deleteModalContext?.actionConfirmation({
title: 'Permanently delete flow?',
text: 'All steps will be lost.',
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { createVisualizationNode, IVisualizationNode } from '../../../../models';
import { ItemDeleteStep } from './ItemDeleteStep';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';

describe('ItemDeleteStep', () => {
const vizNode = createVisualizationNode('test', {});
const mockVizNode = {
removeChild: jest.fn(),
} as unknown as IVisualizationNode;

const mockDeleteModalContext = {
actionConfirmation: jest.fn(),
};

afterEach(() => {
jest.clearAllMocks();
});

it('should render delete ContextMenuItem', () => {
const { container } = render(<ItemDeleteStep vizNode={vizNode} loadActionConfirmationModal={false} />);

expect(container).toMatchSnapshot();
});

it('should open delete confirmation modal on click', async () => {
const wrapper = render(
<ActionConfirmationModalContext.Provider value={mockDeleteModalContext}>
<ItemDeleteStep vizNode={vizNode} loadActionConfirmationModal={true} />
</ActionConfirmationModalContext.Provider>,
);

fireEvent.click(wrapper.getByText('Delete'));

expect(mockDeleteModalContext.actionConfirmation).toHaveBeenCalledWith({
title: 'Permanently delete step?',
text: 'Step and its children will be lost.',
});
});

it('should call removechild if deletion is confirmed', async () => {
mockDeleteModalContext.actionConfirmation.mockResolvedValueOnce(true);
const wrapper = render(
<ActionConfirmationModalContext.Provider value={mockDeleteModalContext}>
<ItemDeleteStep vizNode={mockVizNode} loadActionConfirmationModal={true} />
</ActionConfirmationModalContext.Provider>,
);
fireEvent.click(wrapper.getByText('Delete'));

await waitFor(() => {
expect(mockVizNode.removeChild).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ 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';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';

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

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

const onRemoveNode = useCallback(async () => {
if (props.loadModal) {
if (props.loadActionConfirmationModal) {
/** Open delete confirm modal, get the confirmation */
const isDeleteConfirmed = await deleteModalContext?.deleteConfirmation({
const isDeleteConfirmed = await deleteModalContext?.actionConfirmation({
title: 'Permanently delete step?',
text: 'Step and its children will be lost.',
});
Expand All @@ -28,7 +28,7 @@ export const ItemDeleteStep: FunctionComponent<ItemDeleteStepProps> = (props) =>

props.vizNode?.removeChild();
entitiesContext?.updateEntitiesFromCamelResource();
}, [deleteModalContext, entitiesContext, props.loadModal, props.vizNode]);
}, [deleteModalContext, entitiesContext, props.loadActionConfirmationModal, props.vizNode]);

return (
<ContextMenuItem onClick={onRemoveNode} data-testid={props['data-testid']}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { fireEvent, render } from '@testing-library/react';
import { createVisualizationNode } from '../../../../models';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { ItemReplaceStep } from './ItemReplaceStep';
import { EntitiesContext } from '../../../../providers/entities.provider';
import { CamelRouteResource } from '../../../../models/camel/camel-route-resource';

describe('ItemReplaceStep', () => {
const vizNode = createVisualizationNode('test', {});

const camelResource = new CamelRouteResource();
const mockEntitiesContext = {
camelResource,
entities: camelResource.getEntities(),
visualEntities: camelResource.getVisualEntities(),
currentSchemaType: camelResource.getType(),
updateSourceCodeFromEntities: jest.fn(),
updateEntitiesFromCamelResource: jest.fn(),
setCurrentSchemaType: jest.fn(),
};

const mockReplaceModalContext = {
actionConfirmation: jest.fn(),
};

afterEach(() => {
jest.clearAllMocks();
});

it('should render replace ContextMenuItem', () => {
const { container } = render(<ItemReplaceStep vizNode={vizNode} loadActionConfirmationModal={false} />);

expect(container).toMatchSnapshot();
});

it('should open replace confirmation modal on click', async () => {
const wrapper = render(
<EntitiesContext.Provider value={mockEntitiesContext}>
<ActionConfirmationModalContext.Provider value={mockReplaceModalContext}>
<ItemReplaceStep vizNode={vizNode} loadActionConfirmationModal={true} />
</ActionConfirmationModalContext.Provider>
</EntitiesContext.Provider>,
);

fireEvent.click(wrapper.getByText('Replace'));

expect(mockReplaceModalContext.actionConfirmation).toHaveBeenCalledWith({
title: 'Replace step?',
text: 'Step and its children will be lost.',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,31 @@ import { IDataTestID } from '../../../../models';
import { AddStepMode, IVisualizationNode } from '../../../../models/visualization/base-visual-entity';
import { CatalogModalContext } from '../../../../providers/catalog-modal.provider';
import { EntitiesContext } from '../../../../providers/entities.provider';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';

interface ItemReplaceStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
loadActionConfirmationModal: boolean;
}

export const ItemReplaceStep: FunctionComponent<ItemReplaceStepProps> = (props) => {
const entitiesContext = useContext(EntitiesContext);
const catalogModalContext = useContext(CatalogModalContext);
const replaceModalContext = useContext(ActionConfirmationModalContext);

const onReplaceNode = useCallback(async () => {
if (!props.vizNode || !entitiesContext) return;

if (props.loadActionConfirmationModal) {
/** Open delete confirm modal, get the confirmation */
const isReplaceConfirmed = await replaceModalContext?.actionConfirmation({
title: 'Replace step?',
text: 'Step and its children will be lost.',
});

if (!isReplaceConfirmed) return;
}

/** Find compatible components */
const catalogFilter = entitiesContext.camelResource.getCompatibleComponents(
AddStepMode.ReplaceStep,
Expand All @@ -32,7 +45,7 @@ export const ItemReplaceStep: FunctionComponent<ItemReplaceStepProps> = (props)

/** Update entity */
entitiesContext.updateEntitiesFromCamelResource();
}, [catalogModalContext, entitiesContext, props.vizNode]);
}, [replaceModalContext, catalogModalContext, entitiesContext, props.vizNode]);

return (
<ContextMenuItem onClick={onReplaceNode} data-testid={props['data-testid']}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const NodeContextMenuFn = (element: GraphElement<ElementModel, CanvasNode
if (!vizNode) return items;

const nodeInteractions = vizNode.getNodeInteraction();
const childrenNodes = vizNode.getChildren();
const isStepWithChildren = childrenNodes !== undefined && childrenNodes.length > 0;

if (nodeInteractions.canHavePreviousStep) {
items.push(
Expand Down Expand Up @@ -80,22 +82,25 @@ export const NodeContextMenuFn = (element: GraphElement<ElementModel, CanvasNode
}
if (nodeInteractions.canReplaceStep) {
items.push(
<ItemReplaceStep key="context-menu-item-replace" data-testid="context-menu-item-replace" vizNode={vizNode} />,
<ItemReplaceStep
key="context-menu-item-replace"
data-testid="context-menu-item-replace"
vizNode={vizNode}
loadActionConfirmationModal={isStepWithChildren}
/>,
);
}
if (nodeInteractions.canBeDisabled || nodeInteractions.canReplaceStep) {
items.push(<ContextMenuSeparator key="context-menu-separator-replace" />);
}

if (nodeInteractions.canRemoveStep) {
const childrenNodes = vizNode.getChildren();
const shouldConfirmBeforeDeletion = childrenNodes !== undefined && childrenNodes.length > 0;
items.push(
<ItemDeleteStep
key="context-menu-item-delete"
data-testid="context-menu-item-delete"
vizNode={vizNode}
loadModal={shouldConfirmBeforeDeletion}
loadActionConfirmationModal={isStepWithChildren}
/>,
);
}
Expand Down
Loading

0 comments on commit 97fe010

Please sign in to comment.