diff --git a/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx b/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx index bc1739622..989d77519 100644 --- a/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx +++ b/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx @@ -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); @@ -56,11 +56,11 @@ describe('Canvas', () => { } as unknown as VisibleFLowsContextResult, }); const wrapper = render( - + - , + , ); // Right click anywhere on the container label @@ -103,11 +103,11 @@ describe('Canvas', () => { }); const wrapper = render( - + - , + , ); // Right click anywhere on the container label diff --git a/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.test.tsx b/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.test.tsx index 00ef16201..debdf06aa 100644 --- a/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.test.tsx +++ b/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.test.tsx @@ -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; @@ -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( + + + + + , + ); + + 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); diff --git a/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx b/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx index 8489c463b..6b440f740 100644 --- a/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx +++ b/packages/ui/src/components/Visualization/ContextToolbar/Flows/FlowsList.tsx @@ -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'; @@ -19,7 +19,7 @@ interface IFlowsList { export const FlowsList: FunctionComponent = (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; @@ -105,7 +105,7 @@ export const FlowsList: FunctionComponent = (props) => { icon={} 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.', }); diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteGroup.test.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteGroup.test.tsx new file mode 100644 index 000000000..a9190970f --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteGroup.test.tsx @@ -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(); + + expect(container).toMatchSnapshot(); + }); + + it('should open delete confirmation modal on click', async () => { + const wrapper = render( + + + , + ); + + fireEvent.click(wrapper.getByText('Delete')); + + expect(mockDeleteModalContext.actionConfirmation).toHaveBeenCalledWith({ + title: 'Permanently delete flow?', + text: 'All steps will be lost.', + }); + }); +}); diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteGroup.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteGroup.tsx index 10c31727f..1cc5c555a 100644 --- a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteGroup.tsx +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteGroup.tsx @@ -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 { @@ -12,12 +12,12 @@ interface ItemDeleteGroupProps extends PropsWithChildren { export const ItemDeleteGroup: FunctionComponent = (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.', }); diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteStep.test.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteStep.test.tsx new file mode 100644 index 000000000..3c389dfa5 --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteStep.test.tsx @@ -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(); + + expect(container).toMatchSnapshot(); + }); + + it('should open delete confirmation modal on click', async () => { + const wrapper = render( + + + , + ); + + 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( + + + , + ); + fireEvent.click(wrapper.getByText('Delete')); + + await waitFor(() => { + expect(mockVizNode.removeChild).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteStep.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteStep.tsx index 093a6ae9f..e4c7c4efe 100644 --- a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteStep.tsx +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemDeleteStep.tsx @@ -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 { vizNode: IVisualizationNode; - loadModal: boolean; + loadActionConfirmationModal: boolean; } export const ItemDeleteStep: FunctionComponent = (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.', }); @@ -28,7 +28,7 @@ export const ItemDeleteStep: FunctionComponent = (props) => props.vizNode?.removeChild(); entitiesContext?.updateEntitiesFromCamelResource(); - }, [deleteModalContext, entitiesContext, props.loadModal, props.vizNode]); + }, [deleteModalContext, entitiesContext, props.loadActionConfirmationModal, props.vizNode]); return ( diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemReplaceStep.test.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemReplaceStep.test.tsx new file mode 100644 index 000000000..195609d3f --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemReplaceStep.test.tsx @@ -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(); + + expect(container).toMatchSnapshot(); + }); + + it('should open replace confirmation modal on click', async () => { + const wrapper = render( + + + + + , + ); + + fireEvent.click(wrapper.getByText('Replace')); + + expect(mockReplaceModalContext.actionConfirmation).toHaveBeenCalledWith({ + title: 'Replace step?', + text: 'Step and its children will be lost.', + }); + }); +}); diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemReplaceStep.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemReplaceStep.tsx index c96db72b0..ba58475d5 100644 --- a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemReplaceStep.tsx +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemReplaceStep.tsx @@ -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 { vizNode: IVisualizationNode; + loadActionConfirmationModal: boolean; } export const ItemReplaceStep: FunctionComponent = (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, @@ -32,7 +45,7 @@ export const ItemReplaceStep: FunctionComponent = (props) /** Update entity */ entitiesContext.updateEntitiesFromCamelResource(); - }, [catalogModalContext, entitiesContext, props.vizNode]); + }, [replaceModalContext, catalogModalContext, entitiesContext, props.vizNode]); return ( diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.tsx index a4e0934d5..25d27b617 100644 --- a/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.tsx +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.tsx @@ -16,6 +16,8 @@ export const NodeContextMenuFn = (element: GraphElement 0; if (nodeInteractions.canHavePreviousStep) { items.push( @@ -80,7 +82,12 @@ export const NodeContextMenuFn = (element: GraphElement, + , ); } if (nodeInteractions.canBeDisabled || nodeInteractions.canReplaceStep) { @@ -88,14 +95,12 @@ export const NodeContextMenuFn = (element: GraphElement 0; items.push( , ); } diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemDeleteGroup.test.tsx.snap b/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemDeleteGroup.test.tsx.snap new file mode 100644 index 000000000..1f84c8845 --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemDeleteGroup.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ItemDeleteGroup should render delete ContextMenuItem 1`] = ` +
+
  • + +
  • +
    +`; diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemDeleteStep.test.tsx.snap b/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemDeleteStep.test.tsx.snap new file mode 100644 index 000000000..00ffb76df --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemDeleteStep.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ItemDeleteStep should render delete ContextMenuItem 1`] = ` +
    +
  • + +
  • +
    +`; diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemReplaceStep.test.tsx.snap b/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemReplaceStep.test.tsx.snap new file mode 100644 index 000000000..37ae1eb9e --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/__snapshots__/ItemReplaceStep.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ItemReplaceStep should render replace ContextMenuItem 1`] = ` +
    +
  • + +
  • +
    +`; diff --git a/packages/ui/src/pages/Design/DesignPage.tsx b/packages/ui/src/pages/Design/DesignPage.tsx index bdb0be6e5..e066af9a2 100644 --- a/packages/ui/src/pages/Design/DesignPage.tsx +++ b/packages/ui/src/pages/Design/DesignPage.tsx @@ -1,7 +1,7 @@ import { FunctionComponent, ReactNode, useContext } from 'react'; import { Visualization } from '../../components/Visualization'; import { CatalogModalProvider } from '../../providers/catalog-modal.provider'; -import { DeleteModalContextProvider } from '../../providers/delete-modal.provider'; +import { ActionConfirmationModalContextProvider } from '../../providers/action-confirmation-modal.provider'; import { EntitiesContext } from '../../providers/entities.provider'; import './DesignPage.scss'; @@ -11,9 +11,9 @@ export const DesignPage: FunctionComponent<{ fallback?: ReactNode }> = (props) = return ( - + - + ); }; diff --git a/packages/ui/src/providers/__snapshots__/delete-modal.provider.test.tsx.snap b/packages/ui/src/providers/__snapshots__/action-confirmaton-modal.provider.test.tsx.snap similarity index 92% rename from packages/ui/src/providers/__snapshots__/delete-modal.provider.test.tsx.snap rename to packages/ui/src/providers/__snapshots__/action-confirmaton-modal.provider.test.tsx.snap index f72625bdd..5144bfe4e 100644 --- a/packages/ui/src/providers/__snapshots__/delete-modal.provider.test.tsx.snap +++ b/packages/ui/src/providers/__snapshots__/action-confirmaton-modal.provider.test.tsx.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DeleteModalProvider should allow consumers to update the modal title and text 1`] = ` +exports[`ActionConfirmationModalProvider should allow consumers to update the modal title and text 1`] = `
    Promise; +interface ActionConfirmationModalContextValue { + actionConfirmation: (options: { title?: string; text?: string }) => Promise; } -export const DeleteModalContext = createContext(undefined); +export const ActionConfirmationModalContext = createContext(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. + * This provider is used to open the Action Confirmation modal. + * The modal loads when the user clicks on the delete Routes/Kamelets or remove/replace any Step from the Context Menu. */ -export const DeleteModalContextProvider: FunctionComponent = (props) => { +export const ActionConfirmationModalContextProvider: FunctionComponent = (props) => { const [isModalOpen, setIsModalOpen] = useState(false); const [title, setTitle] = useState(''); const [text, setText] = useState(''); - const deleteConfirmationRef = useRef<{ + const actionConfirmationRef = useRef<{ resolve: (confirm: boolean) => void; reject: (error: unknown) => unknown; }>(); const handleCloseModal = useCallback(() => { setIsModalOpen(false); - deleteConfirmationRef.current?.resolve(false); + actionConfirmationRef.current?.resolve(false); }, []); - const handleDeleteConfirm = useCallback(() => { + const handleActionConfirm = useCallback(() => { setIsModalOpen(false); - deleteConfirmationRef.current?.resolve(true); + actionConfirmationRef.current?.resolve(true); }, []); - const deleteConfirmation = useCallback((options: { title?: string; text?: string } = {}) => { - const deleteConfirmationPromise = new Promise((resolve, reject) => { + const actionConfirmation = useCallback((options: { title?: string; text?: string } = {}) => { + const actionConfirmationPromise = new Promise((resolve, reject) => { /** Set both resolve and reject functions to be used once the user choose an action */ - deleteConfirmationRef.current = { resolve, reject }; + actionConfirmationRef.current = { resolve, reject }; }); - setTitle(options.title || 'Delete?'); - setText(options.text || 'Are you sure you want to delete?'); + setTitle(options.title ?? 'Delete?'); + setText(options.text ?? 'Are you sure you want to delete?'); setIsModalOpen(true); - return deleteConfirmationPromise; + return actionConfirmationPromise; }, []); - const value: DeleteModalContextValue = useMemo( + const value: ActionConfirmationModalContextValue = useMemo( () => ({ - deleteConfirmation, + actionConfirmation: actionConfirmation, }), - [deleteConfirmation], + [actionConfirmation], ); return ( - + {props.children} {isModalOpen && ( @@ -62,9 +62,9 @@ export const DeleteModalContextProvider: FunctionComponent = title={title} titleIconVariant={'warning'} onClose={handleCloseModal} - ouiaId="DeleteConfirmModal" + ouiaId="ActionConfirmationModal" actions={[ - , ; diff --git a/packages/ui/src/providers/index.ts b/packages/ui/src/providers/index.ts index 4032fbdc6..44019b2a8 100644 --- a/packages/ui/src/providers/index.ts +++ b/packages/ui/src/providers/index.ts @@ -2,7 +2,7 @@ export * from './canvas-form-tabs.provider'; export * from './catalog-modal.provider'; export * from './catalog-tiles.provider'; export * from './catalog.provider'; -export * from './delete-modal.provider'; +export * from './action-confirmation-modal.provider'; export * from './entities.provider'; export * from './filtered-field.provider'; export * from './settings.provider';