Skip to content

Commit

Permalink
feat(Canvas): Add CustomGroups
Browse files Browse the repository at this point in the history
As a preparation for adding containers for branches, we
need to introduce a new `CustomGroup` component that can render its
label on top, among other topics.

This commit moves the `CustomGroup` and `CustomNode` to dedicated
folders while also reuse the same `NodeContextMenu` component.

fix: #368
  • Loading branch information
lordrip committed Aug 7, 2024
1 parent fe03804 commit 921b007
Show file tree
Hide file tree
Showing 35 changed files with 1,233 additions and 758 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ContextMenuItem } from '@patternfly/react-topology';
import { FunctionComponent, PropsWithChildren, useCallback, useContext } from 'react';
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 { 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';

interface ItemAddStepProps extends PropsWithChildren<IDataTestID> {
mode: AddStepMode.PrependStep | AddStepMode.AppendStep;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { TrashIcon } from '@patternfly/react-icons';
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 { EntitiesContext } from '../../../providers/entities.provider';
import { IDataTestID } from '../../../../models';
import { IVisualizationNode } from '../../../../models/visualization/base-visual-entity';
import { DeleteModalContext } from '../../../../providers/delete-modal.provider';
import { EntitiesContext } from '../../../../providers/entities.provider';

interface ItemDeleteGroupProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { TrashIcon } from '@patternfly/react-icons';
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 { EntitiesContext } from '../../../providers/entities.provider';
import { DeleteModalContext } from '../../../providers/delete-modal.provider';
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { BanIcon, CheckIcon } from '@patternfly/react-icons';
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 { EntitiesContext } from '../../../providers/entities.provider';
import { setValue } from '../../../utils/set-value';
import { IDataTestID } from '../../../../models';
import { IVisualizationNode } from '../../../../models/visualization/base-visual-entity';
import { EntitiesContext } from '../../../../providers/entities.provider';
import { setValue } from '../../../../utils/set-value';

interface ItemDisableStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ContextMenuItem } from '@patternfly/react-topology';
import { FunctionComponent, PropsWithChildren, useCallback, useContext } from 'react';
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 { 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';

interface ItemInsertStepProps extends PropsWithChildren<IDataTestID> {
mode: AddStepMode.InsertChildStep | AddStepMode.InsertSpecialChildStep;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SyncAltIcon } from '@patternfly/react-icons';
import { ContextMenuItem } from '@patternfly/react-topology';
import { FunctionComponent, PropsWithChildren, useCallback, useContext } from 'react';
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 { 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';

interface ItemReplaceStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { ElementModel, GraphElement } from '@patternfly/react-topology';
import { render } from '@testing-library/react';
import { CanvasNode } from '../../Canvas';
import { NodeContextMenu } from './NodeContextMenu';
import { createVisualizationNode, IVisualizationNode, NodeInteraction } from '../../../../models';

describe('NodeContextMenu', () => {
let element: GraphElement<ElementModel, CanvasNode['data']>;
let vizNode: IVisualizationNode | undefined;
let nodeInteractions: NodeInteraction;

beforeEach(() => {
nodeInteractions = {
canHavePreviousStep: false,
canHaveNextStep: false,
canHaveChildren: false,
canHaveSpecialChildren: false,
canReplaceStep: false,
canRemoveStep: false,
canRemoveFlow: false,
canBeDisabled: false,
};
vizNode = createVisualizationNode('test', {});
jest.spyOn(vizNode, 'getNodeInteraction').mockReturnValue(nodeInteractions);
element = {
getData: () => {
return { vizNode } as CanvasNode['data'];
},
} as unknown as GraphElement<ElementModel, CanvasNode['data']>;
});

it('should render an empty component when there is no vizNode', () => {
vizNode = undefined;
const { container } = render(<NodeContextMenu element={element} />);

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

it('should render a PrependStep item if canHavePreviousStep is true', () => {
nodeInteractions.canHavePreviousStep = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-item-prepend');

expect(item).toBeInTheDocument();
});

it('should render an AppendStep item if canHaveNextStep is true', () => {
nodeInteractions.canHaveNextStep = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-item-append');

expect(item).toBeInTheDocument();
});

it('should render an InsertStep item if canHaveChildren is true', () => {
nodeInteractions.canHaveChildren = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-item-insert');

expect(item).toBeInTheDocument();
});

it('should render an InsertSpecialStep item if canHaveSpecialChildren is true', () => {
nodeInteractions.canHaveSpecialChildren = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-item-insert-special');

expect(item).toBeInTheDocument();
});

it('should render an ItemDisableStep item if canBeDisabled is true', () => {
nodeInteractions.canBeDisabled = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-item-disable');

expect(item).toBeInTheDocument();
});

it('should render an ItemReplaceStep item if canReplaceStep is true', () => {
nodeInteractions.canReplaceStep = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-item-replace');

expect(item).toBeInTheDocument();
});

it('should render an ItemDeleteStep item if canRemoveStep is true', () => {
nodeInteractions.canRemoveStep = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-item-delete');

expect(item).toBeInTheDocument();
});

it('should render an ItemDeleteGroup item if canRemoveFlow is true', () => {
nodeInteractions.canRemoveFlow = true;
const wrapper = render(<NodeContextMenu element={element} />);

const item = wrapper.getByTestId('context-menu-container-remove');

expect(item).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { ArrowDownIcon, ArrowUpIcon, CodeBranchIcon, PlusIcon } from '@patternfly/react-icons';
import { ContextMenuSeparator, ElementModel, GraphElement } from '@patternfly/react-topology';
import { forwardRef, ReactElement } from 'react';
import { AddStepMode } from '../../../../models/visualization/base-visual-entity';
import { CanvasNode } from '../../Canvas/canvas.models';
import { ItemAddStep } from './ItemAddStep';
import { ItemDeleteGroup } from './ItemDeleteGroup';
import { ItemDeleteStep } from './ItemDeleteStep';
import { ItemDisableStep } from './ItemDisableStep';
import { ItemInsertStep } from './ItemInsertStep';
import { ItemReplaceStep } from './ItemReplaceStep';

export const NodeContextMenuFn = (element: GraphElement<ElementModel, CanvasNode['data']>) => {
const items: ReactElement[] = [];
const vizNode = element.getData()?.vizNode;
if (!vizNode) return items;

const nodeInteractions = vizNode.getNodeInteraction();

if (nodeInteractions.canHavePreviousStep) {
items.push(
<ItemAddStep
key="context-menu-item-prepend"
data-testid="context-menu-item-prepend"
mode={AddStepMode.PrependStep}
vizNode={vizNode}
>
<ArrowUpIcon /> Prepend
</ItemAddStep>,
);
}
if (nodeInteractions.canHaveNextStep) {
items.push(
<ItemAddStep
key="context-menu-item-append"
data-testid="context-menu-item-append"
mode={AddStepMode.AppendStep}
vizNode={vizNode}
>
<ArrowDownIcon /> Append
</ItemAddStep>,
);
}
if (nodeInteractions.canHavePreviousStep || nodeInteractions.canHaveNextStep) {
items.push(<ContextMenuSeparator key="context-menu-separator-add" />);
}

if (nodeInteractions.canHaveChildren) {
items.push(
<ItemInsertStep
key="context-menu-item-insert"
data-testid="context-menu-item-insert"
mode={AddStepMode.InsertChildStep}
vizNode={vizNode}
>
<PlusIcon /> Add step
</ItemInsertStep>,
);
}
if (nodeInteractions.canHaveSpecialChildren) {
items.push(
<ItemInsertStep
key="context-menu-item-insert-special"
data-testid="context-menu-item-insert-special"
mode={AddStepMode.InsertSpecialChildStep}
vizNode={vizNode}
>
<CodeBranchIcon /> Add branch
</ItemInsertStep>,
);
}
if (nodeInteractions.canHaveChildren || nodeInteractions.canHaveSpecialChildren) {
items.push(<ContextMenuSeparator key="context-menu-separator-insert" />);
}

if (nodeInteractions.canBeDisabled) {
items.push(
<ItemDisableStep key="context-menu-item-disable" data-testid="context-menu-item-disable" vizNode={vizNode} />,
);
}
if (nodeInteractions.canReplaceStep) {
items.push(
<ItemReplaceStep key="context-menu-item-replace" data-testid="context-menu-item-replace" vizNode={vizNode} />,
);
}
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}
/>,
);
}
if (nodeInteractions.canRemoveFlow) {
items.push(
<ItemDeleteGroup
key="context-menu-container-remove"
data-testid="context-menu-container-remove"
vizNode={vizNode}
/>,
);
}

return items;
};

export const NodeContextMenu = forwardRef<HTMLDivElement, { element: GraphElement<ElementModel, CanvasNode['data']> }>(
({ element }, forwardedRef) => {
return (
<div data-testid="node-context-menu" ref={forwardedRef}>
{NodeContextMenuFn(element)}
</div>
);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NodeContextMenu should render an empty component when there is no vizNode 1`] = `
<div>
<div
data-testid="node-context-menu"
/>
</div>
`;
Loading

0 comments on commit 921b007

Please sign in to comment.