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 17, 2024
1 parent 9b7cd80 commit 2c279c8
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 70 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 @@ -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
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
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
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
6 changes: 3 additions & 3 deletions packages/ui/src/pages/Design/DesignPage.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,9 +11,9 @@ export const DesignPage: FunctionComponent<{ fallback?: ReactNode }> = (props) =

return (
<CatalogModalProvider>
<DeleteModalContextProvider>
<ActionConfirmationModalContextProvider>
<Visualization className="canvas-page" entities={visualEntities} fallback={props.fallback} />
</DeleteModalContextProvider>
</ActionConfirmationModalContextProvider>
</CatalogModalProvider>
);
};
Original file line number Diff line number Diff line change
@@ -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`] = `
<div
aria-describedby="pf-modal-part-4"
aria-labelledby="pf-modal-part-3"
aria-modal="true"
class="pf-v5-c-modal-box pf-m-warning pf-m-sm"
data-ouia-component-id="DeleteConfirmModal"
data-ouia-component-id="ActionConfirmationModal"
data-ouia-component-type="PF5/ModalContent"
data-ouia-safe="true"
id="pf-modal-part-2"
Expand All @@ -19,7 +19,7 @@ exports[`DeleteModalProvider should allow consumers to update the modal title an
aria-disabled="false"
aria-label="Close"
class="pf-v5-c-button pf-m-plain"
data-ouia-component-id="DeleteConfirmModal-ModalBoxCloseButton"
data-ouia-component-id="ActionConfirmationModal-ModalBoxCloseButton"
data-ouia-component-type="PF5/Button"
data-ouia-safe="true"
type="button"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,58 @@
import { Button, Modal, ModalVariant } from '@patternfly/react-core';
import { FunctionComponent, PropsWithChildren, createContext, useCallback, useMemo, useRef, useState } from 'react';

interface DeleteModalContextValue {
deleteConfirmation: (options: { title?: string; text?: string }) => Promise<boolean>;
interface ActionConfirmationModalContextValue {
actionConfirmation: (options: { title?: string; text?: string }) => Promise<boolean>;
}

export const DeleteModalContext = createContext<DeleteModalContextValue | undefined>(undefined);
export const ActionConfirmationModalContext = createContext<ActionConfirmationModalContextValue | 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.
* 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<PropsWithChildren> = (props) => {
export const ActionConfirmationModalContextProvider: FunctionComponent<PropsWithChildren> = (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<boolean>((resolve, reject) => {
const actionConfirmation = useCallback((options: { title?: string; text?: string } = {}) => {
const actionConfirmationPromise = new Promise<boolean>((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 (
<DeleteModalContext.Provider value={value}>
<ActionConfirmationModalContext.Provider value={value}>
{props.children}

{isModalOpen && (
Expand All @@ -62,9 +62,9 @@ export const DeleteModalContextProvider: FunctionComponent<PropsWithChildren> =
title={title}
titleIconVariant={'warning'}
onClose={handleCloseModal}
ouiaId="DeleteConfirmModal"
ouiaId="ActionConfirmationModal"
actions={[
<Button key="confirm" variant="danger" onClick={handleDeleteConfirm}>
<Button key="confirm" variant="danger" onClick={handleActionConfirm}>
Confirm
</Button>,
<Button key="cancel" variant="link" onClick={handleCloseModal}>
Expand All @@ -75,6 +75,6 @@ export const DeleteModalContextProvider: FunctionComponent<PropsWithChildren> =
{text}
</Modal>
)}
</DeleteModalContext.Provider>
</ActionConfirmationModalContext.Provider>
);
};
Loading

0 comments on commit 2c279c8

Please sign in to comment.