Skip to content
This repository has been archived by the owner on Dec 9, 2024. It is now read-only.

fix(DataMapper): Delete DataMapper metadata entry when the step is de… #94

Merged
merged 1 commit into from
Oct 8, 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
8 changes: 7 additions & 1 deletion packages/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
} from './providers';
import { isDefined } from './utils';
import { CatalogSchemaLoader } from './utils/catalog-schema-loader';
import { RegisterNodeInteractionAddons } from './components/registers/RegisterNodeInteractionAddons';
import { NodeInteractionAddonProvider } from './components/registers/interactions/node-interaction-addon.provider';

function App() {
const ReloadProvider = useReload();
Expand All @@ -40,7 +42,11 @@ function App() {
<VisibleFlowsProvider>
<RenderingProvider>
<RegisterComponents>
<Outlet />
<NodeInteractionAddonProvider>
<RegisterNodeInteractionAddons>
<Outlet />
</RegisterNodeInteractionAddons>
</NodeInteractionAddonProvider>
</RegisterComponents>
</RenderingProvider>
</VisibleFlowsProvider>
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/components/DataMapper/on-delete-datamapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { DataMapperMetadataService } from '../../services/datamapper-metadata.service';
import { IMetadataApi } from '../../providers';
import { IVisualizationNode } from '../../models';

export const onDeleteDataMapper = (api: IMetadataApi, vizNode: IVisualizationNode) => {
const metadataId = DataMapperMetadataService.getDataMapperMetadataId(vizNode);
DataMapperMetadataService.deleteMetadata(api, metadataId);
// TODO DataMapperMetadataService.deleteXsltFile(api, metadataId);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { fireEvent, render } from '@testing-library/react';
import { createVisualizationNode } from '../../../../models';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { createVisualizationNode, IVisualizationNode } from '../../../../models';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { ItemDeleteGroup } from './ItemDeleteGroup';
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';
import { IInteractionAddonType } from '../../../registers/interactions/node-interaction-addon.model';

describe('ItemDeleteGroup', () => {
const vizNode = createVisualizationNode('test', {});
Expand Down Expand Up @@ -34,4 +36,30 @@ describe('ItemDeleteGroup', () => {
text: 'All steps will be lost.',
});
});

it('should process addon when deleting', async () => {
const mockDeleteModalContext = {
actionConfirmation: () => Promise.resolve(true),
};
const mockAddon = jest.fn();
const mockNodeInteractionAddonContext = {
registerInteractionAddon: jest.fn(),
getRegisteredInteractionAddons: (_interaction: IInteractionAddonType, _vizNode: IVisualizationNode) => [
{ type: IInteractionAddonType.ON_DELETE, activationFn: () => true, callback: mockAddon },
],
};
const wrapper = render(
<ActionConfirmationModalContext.Provider value={mockDeleteModalContext}>
<NodeInteractionAddonContext.Provider value={mockNodeInteractionAddonContext}>
<ItemDeleteGroup vizNode={vizNode} />
</NodeInteractionAddonContext.Provider>
</ActionConfirmationModalContext.Provider>,
);
act(() => {
fireEvent.click(wrapper.getByText('Delete'));
});
await waitFor(() => {
expect(mockAddon).toHaveBeenCalledWith(vizNode);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { IDataTestID } from '../../../../models';
import { IVisualizationNode } from '../../../../models/visualization/base-visual-entity';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { EntitiesContext } from '../../../../providers/entities.provider';
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';
import { IInteractionAddonType } from '../../../registers/interactions/node-interaction-addon.model';

interface ItemDeleteGroupProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand All @@ -15,6 +17,21 @@ export const ItemDeleteGroup: FunctionComponent<ItemDeleteGroupProps> = (props)
const deleteModalContext = useContext(ActionConfirmationModalContext);
const flowId = props.vizNode?.getId();

const { getRegisteredInteractionAddons } = useContext(NodeInteractionAddonContext);

const onDeleteRecursively = useCallback(
(parentVizNode: IVisualizationNode) => {
parentVizNode.getChildren()?.forEach((child) => {
onDeleteRecursively(child);
});
const addons = getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, parentVizNode);
addons.forEach((addon) => {
addon.callback(parentVizNode);
});
},
[getRegisteredInteractionAddons],
);
Comment on lines +22 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

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

This function looks very similar to the ItemDeleteStep and the ItemReplace components, we should extract them to a single place so we can also benefit from testing the function on isolation.

If you don't mind, let's create an issue for this and move forward 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think once we extract the function, we could use it like this: (we probably should pick a more appropriate name than onDelete since the function will live somewhere else now)

onDelete(IVizNode, (vizNode: IVisualizationNode => getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, parentVizNode)

Notice how we pass the current visualizationNode to the function as the first argument, and for the second argument, we pass a function that tells how and what type of interactions we are looking for, this way we bridge the React world (providers) with the outside, using regular Javascript.

Alternatively, since we're just traversing objects and we're not relying on any particular class instance, we might even simplify it as:

onDelete(IVizNode, getRegisteredInteractionAddons);

Then the onDelete function body will be in charge of passing the right VizNode and the right interaction type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


const onRemoveGroup = useCallback(async () => {
/** Open delete confirm modal, get the confirmation */
const isDeleteConfirmed = await deleteModalContext?.actionConfirmation({
Expand All @@ -24,9 +41,11 @@ export const ItemDeleteGroup: FunctionComponent<ItemDeleteGroupProps> = (props)

if (!isDeleteConfirmed) return;

onDeleteRecursively(props.vizNode);

entitiesContext?.camelResource.removeEntity(flowId);
entitiesContext?.updateEntitiesFromCamelResource();
}, [deleteModalContext, entitiesContext, flowId]);
}, [deleteModalContext, entitiesContext, flowId, onDeleteRecursively, props.vizNode]);

return (
<ContextMenuItem onClick={onRemoveGroup} data-testid={props['data-testid']}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { createVisualizationNode, IVisualizationNode } from '../../../../models';
import { ItemDeleteStep } from './ItemDeleteStep';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { IInteractionAddonType } from '../../../registers/interactions/node-interaction-addon.model';
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';

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

const mockDeleteModalContext = {
Expand Down Expand Up @@ -51,4 +54,30 @@ describe('ItemDeleteStep', () => {
expect(mockVizNode.removeChild).toHaveBeenCalled();
});
});

it('should process addon when deleting', async () => {
const mockDeleteModalContext = {
actionConfirmation: () => Promise.resolve(true),
};
const mockAddon = jest.fn();
const mockNodeInteractionAddonContext = {
registerInteractionAddon: jest.fn(),
getRegisteredInteractionAddons: (_interaction: IInteractionAddonType, _vizNode: IVisualizationNode) => [
{ type: IInteractionAddonType.ON_DELETE, activationFn: () => true, callback: mockAddon },
],
};
const wrapper = render(
<ActionConfirmationModalContext.Provider value={mockDeleteModalContext}>
<NodeInteractionAddonContext.Provider value={mockNodeInteractionAddonContext}>
<ItemDeleteStep vizNode={vizNode} loadActionConfirmationModal={false} />
</NodeInteractionAddonContext.Provider>
</ActionConfirmationModalContext.Provider>,
);
act(() => {
fireEvent.click(wrapper.getByText('Delete'));
});
await waitFor(() => {
expect(mockAddon).toHaveBeenCalledWith(vizNode);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { IDataTestID } from '../../../../models';
import { IVisualizationNode } from '../../../../models/visualization/base-visual-entity';
import { EntitiesContext } from '../../../../providers/entities.provider';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';
import { IInteractionAddonType } from '../../../registers/interactions/node-interaction-addon.model';

interface ItemDeleteStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand All @@ -14,6 +16,20 @@ interface ItemDeleteStepProps extends PropsWithChildren<IDataTestID> {
export const ItemDeleteStep: FunctionComponent<ItemDeleteStepProps> = (props) => {
const entitiesContext = useContext(EntitiesContext);
const deleteModalContext = useContext(ActionConfirmationModalContext);
const { getRegisteredInteractionAddons } = useContext(NodeInteractionAddonContext);

const onDeleteRecursively = useCallback(
(parentVizNode: IVisualizationNode) => {
parentVizNode.getChildren()?.forEach((child) => {
onDeleteRecursively(child);
});
const interactions = getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, parentVizNode);
interactions.forEach((interaction) => {
interaction.callback(parentVizNode);
});
},
[getRegisteredInteractionAddons],
);

const onRemoveNode = useCallback(async () => {
if (props.loadActionConfirmationModal) {
Expand All @@ -26,9 +42,11 @@ export const ItemDeleteStep: FunctionComponent<ItemDeleteStepProps> = (props) =>
if (!isDeleteConfirmed) return;
}

onDeleteRecursively(props.vizNode);

props.vizNode?.removeChild();
entitiesContext?.updateEntitiesFromCamelResource();
}, [deleteModalContext, entitiesContext, props.loadActionConfirmationModal, props.vizNode]);
}, [deleteModalContext, entitiesContext, onDeleteRecursively, 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
@@ -1,9 +1,12 @@
import { fireEvent, render } from '@testing-library/react';
import { createVisualizationNode } from '../../../../models';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { createVisualizationNode, DefinedComponent, IVisualizationNode } 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';
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';
import { IInteractionAddonType } from '../../../registers/interactions/node-interaction-addon.model';
import { CatalogModalContext } from '../../../../providers';

describe('ItemReplaceStep', () => {
const vizNode = createVisualizationNode('test', {});
Expand Down Expand Up @@ -49,4 +52,38 @@ describe('ItemReplaceStep', () => {
text: 'Step and its children will be lost.',
});
});

it('should process addon when replacing', async () => {
const mockCatalogModalContext = {
setIsModalOpen: jest.fn(),
getNewComponent: () => Promise.resolve({} as DefinedComponent),
};
const mockReplaceModalContext = {
actionConfirmation: () => Promise.resolve(true),
};
const mockAddon = jest.fn();
const mockNodeInteractionAddonContext = {
registerInteractionAddon: jest.fn(),
getRegisteredInteractionAddons: (_interaction: IInteractionAddonType, _vizNode: IVisualizationNode) => [
{ type: IInteractionAddonType.ON_DELETE, activationFn: () => true, callback: mockAddon },
],
};
const wrapper = render(
<EntitiesContext.Provider value={mockEntitiesContext}>
<CatalogModalContext.Provider value={mockCatalogModalContext}>
<ActionConfirmationModalContext.Provider value={mockReplaceModalContext}>
<NodeInteractionAddonContext.Provider value={mockNodeInteractionAddonContext}>
<ItemReplaceStep vizNode={vizNode} loadActionConfirmationModal={false} />
</NodeInteractionAddonContext.Provider>
</ActionConfirmationModalContext.Provider>
</CatalogModalContext.Provider>
</EntitiesContext.Provider>,
);
act(() => {
fireEvent.click(wrapper.getByText('Replace'));
});
await waitFor(() => {
expect(mockAddon).toHaveBeenCalledWith(vizNode);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { AddStepMode, IVisualizationNode } from '../../../../models/visualizatio
import { CatalogModalContext } from '../../../../providers/catalog-modal.provider';
import { EntitiesContext } from '../../../../providers/entities.provider';
import { ActionConfirmationModalContext } from '../../../../providers/action-confirmation-modal.provider';
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';
import { IInteractionAddonType } from '../../../registers/interactions/node-interaction-addon.model';

interface ItemReplaceStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand All @@ -16,6 +18,20 @@ export const ItemReplaceStep: FunctionComponent<ItemReplaceStepProps> = (props)
const entitiesContext = useContext(EntitiesContext);
const catalogModalContext = useContext(CatalogModalContext);
const replaceModalContext = useContext(ActionConfirmationModalContext);
const { getRegisteredInteractionAddons } = useContext(NodeInteractionAddonContext);

const onDeleteRecursively = useCallback(
(parentVizNode: IVisualizationNode) => {
parentVizNode.getChildren()?.forEach((child) => {
onDeleteRecursively(child);
});
const addons = getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, parentVizNode);
addons.forEach((addon) => {
addon.callback(parentVizNode);
});
},
[getRegisteredInteractionAddons],
);

const onReplaceNode = useCallback(async () => {
if (!props.vizNode || !entitiesContext) return;
Expand All @@ -40,12 +56,21 @@ export const ItemReplaceStep: FunctionComponent<ItemReplaceStepProps> = (props)
const definedComponent = await catalogModalContext?.getNewComponent(catalogFilter);
if (!definedComponent) return;

onDeleteRecursively(props.vizNode);

/** Add new node to the entities */
props.vizNode.addBaseEntityStep(definedComponent, AddStepMode.ReplaceStep);

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

return (
<ContextMenuItem onClick={onReplaceNode} data-testid={props['data-testid']}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { datamapperActivationFn } from './datamapper.activationfn';

export const RegisterComponents: FunctionComponent<PropsWithChildren> = ({ children }) => {
const { registerComponent } = useContext(RenderingAnchorContext);

const componentsToRegister = useRef<IRegisteredComponent[]>([
{
anchor: Anchors.CanvasFormHeader,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FunctionComponent, PropsWithChildren, useContext, useRef } from 'react';
import { datamapperActivationFn } from './datamapper.activationfn';
import { MetadataContext } from '../../providers';
import { onDeleteDataMapper } from '../DataMapper/on-delete-datamapper';
import { NodeInteractionAddonContext } from './interactions/node-interaction-addon.provider';
import { IInteractionAddonType, IRegisteredInteractionAddon } from './interactions/node-interaction-addon.model';

export const RegisterNodeInteractionAddons: FunctionComponent<PropsWithChildren> = ({ children }) => {
const metadataApi = useContext(MetadataContext)!;
const { registerInteractionAddon } = useContext(NodeInteractionAddonContext);
const addonsToRegister = useRef<IRegisteredInteractionAddon[]>([
{
type: IInteractionAddonType.ON_DELETE,
activationFn: datamapperActivationFn,
callback: (vizNode) => {
metadataApi && onDeleteDataMapper(metadataApi, vizNode);
},
},
]);

addonsToRegister.current.forEach((interaction) => {
registerInteractionAddon(interaction);
});

return <>{children}</>;
};
Loading