Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix(canvas): Show confirmation dialog for replacing a step with children #1441

Merged
merged 1 commit into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks a lot for adding tests 💪

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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks a lot for adding tests 💪

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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks a lot for adding tests 💪

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