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

Commit

Permalink
fix(DataMapper): Show a confirmation dialog when DataMapper step is t…
Browse files Browse the repository at this point in the history
…o be deleted, with also offering an option to delete associated mapping file (XSLT)

Fixes: https://github.com/KaotoIO/kaoto-datamapper-integration/issues/97
  • Loading branch information
igarashitm committed Oct 9, 2024
1 parent 9365cea commit 0cd189e
Show file tree
Hide file tree
Showing 16 changed files with 147 additions and 33 deletions.
14 changes: 10 additions & 4 deletions packages/ui/src/components/DataMapper/on-delete-datamapper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { DataMapperMetadataService } from '../../services/datamapper-metadata.service';
import { IMetadataApi } from '../../providers';
import { ACTION_INDEX_DELETE_STEP_AND_FILE, IMetadataApi } from '../../providers';
import { IVisualizationNode } from '../../models';

export const onDeleteDataMapper = (api: IMetadataApi, vizNode: IVisualizationNode) => {
export const onDeleteDataMapper = async (
api: IMetadataApi,
vizNode: IVisualizationNode,
modalAnswer: number | undefined,
) => {
const metadataId = DataMapperMetadataService.getDataMapperMetadataId(vizNode);
DataMapperMetadataService.deleteMetadata(api, metadataId);
// TODO DataMapperMetadataService.deleteXsltFile(api, metadataId);
if (modalAnswer === ACTION_INDEX_DELETE_STEP_AND_FILE) {
await DataMapperMetadataService.deleteXsltFile(api, metadataId);
}
await DataMapperMetadataService.deleteMetadata(api, metadataId);
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('ItemDeleteGroup', () => {

it('should process addon when deleting', async () => {
const mockDeleteModalContext = {
actionConfirmation: () => Promise.resolve(true),
actionConfirmation: () => Promise.resolve(1),
};
const mockAddon = jest.fn();
const mockNodeInteractionAddonContext = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ActionConfirmationModalContext } from '../../../../providers/action-con
import { EntitiesContext } from '../../../../providers/entities.provider';
import { NodeInteractionAddonContext } from '../../../registers/interactions/node-interaction-addon.provider';
import { IInteractionAddonType } from '../../../registers/interactions/node-interaction-addon.model';
import { processNodeInteractionAddonRecursively } from './item-delete-helper';
import { findModalCustomizationRecursively, processNodeInteractionAddonRecursively } from './item-delete-helper';

interface ItemDeleteGroupProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand All @@ -21,15 +21,23 @@ export const ItemDeleteGroup: FunctionComponent<ItemDeleteGroupProps> = (props)
const { getRegisteredInteractionAddons } = useContext(NodeInteractionAddonContext);

const onRemoveGroup = useCallback(async () => {
const modalCustoms = findModalCustomizationRecursively(props.vizNode, (vn) =>
getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, vn),
);
let modalAnswer: number | undefined = 1;
const additionalModalText = modalCustoms.length > 0 ? modalCustoms[0].additionalText : undefined;
const buttonOptions = modalCustoms.length > 0 ? modalCustoms[0].buttonOptions : undefined;
/** Open delete confirm modal, get the confirmation */
const isDeleteConfirmed = await deleteModalContext?.actionConfirmation({
modalAnswer = await deleteModalContext?.actionConfirmation({
title: 'Permanently delete flow?',
text: 'All steps will be lost.',
additionalModalText,
buttonOptions,
});

if (!isDeleteConfirmed) return;
if (!modalAnswer) return;

processNodeInteractionAddonRecursively(props.vizNode, (vn) =>
processNodeInteractionAddonRecursively(props.vizNode, modalAnswer, (vn) =>
getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, vn),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('ItemDeleteStep', () => {

it('should process addon when deleting', async () => {
const mockDeleteModalContext = {
actionConfirmation: () => Promise.resolve(true),
actionConfirmation: () => Promise.resolve(1),
};
const mockAddon = jest.fn();
const mockNodeInteractionAddonContext = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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';
import { processNodeInteractionAddonRecursively } from './item-delete-helper';
import { findModalCustomizationRecursively, processNodeInteractionAddonRecursively } from './item-delete-helper';

interface ItemDeleteStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand All @@ -20,17 +20,26 @@ export const ItemDeleteStep: FunctionComponent<ItemDeleteStepProps> = (props) =>
const { getRegisteredInteractionAddons } = useContext(NodeInteractionAddonContext);

const onRemoveNode = useCallback(async () => {
if (props.loadActionConfirmationModal) {
const modalCustoms = findModalCustomizationRecursively(props.vizNode, (vn) =>
getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, vn),
);

let modalAnswer: number | undefined = 1;
if (props.loadActionConfirmationModal || modalCustoms.length > 0) {
const additionalModalText = modalCustoms.length > 0 ? modalCustoms[0].additionalText : undefined;
const buttonOptions = modalCustoms.length > 0 ? modalCustoms[0].buttonOptions : undefined;
/** Open delete confirm modal, get the confirmation */
const isDeleteConfirmed = await deleteModalContext?.actionConfirmation({
modalAnswer = await deleteModalContext?.actionConfirmation({
title: 'Permanently delete step?',
text: 'Step and its children will be lost.',
additionalModalText,
buttonOptions,
});

if (!isDeleteConfirmed) return;
if (!modalAnswer) return;
}

processNodeInteractionAddonRecursively(props.vizNode, (vn) =>
processNodeInteractionAddonRecursively(props.vizNode, modalAnswer, (vn) =>
getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, vn),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('ItemReplaceStep', () => {
getNewComponent: () => Promise.resolve({} as DefinedComponent),
};
const mockReplaceModalContext = {
actionConfirmation: () => Promise.resolve(true),
actionConfirmation: () => Promise.resolve(1),
};
const mockAddon = jest.fn();
const mockNodeInteractionAddonContext = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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';
import { processNodeInteractionAddonRecursively } from './item-delete-helper';
import { findModalCustomizationRecursively, processNodeInteractionAddonRecursively } from './item-delete-helper';

interface ItemReplaceStepProps extends PropsWithChildren<IDataTestID> {
vizNode: IVisualizationNode;
Expand All @@ -24,14 +24,22 @@ export const ItemReplaceStep: FunctionComponent<ItemReplaceStepProps> = (props)
const onReplaceNode = useCallback(async () => {
if (!props.vizNode || !entitiesContext) return;

if (props.loadActionConfirmationModal) {
const modalCustoms = findModalCustomizationRecursively(props.vizNode, (vn) =>
getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, vn),
);
let modalAnswer: number | undefined = 1;
if (props.loadActionConfirmationModal || modalCustoms.length > 0) {
const additionalModalText = modalCustoms.length > 0 ? modalCustoms[0].additionalText : undefined;
const buttonOptions = modalCustoms.length > 0 ? modalCustoms[0].buttonOptions : undefined;
/** Open delete confirm modal, get the confirmation */
const isReplaceConfirmed = await replaceModalContext?.actionConfirmation({
modalAnswer = await replaceModalContext?.actionConfirmation({
title: 'Replace step?',
text: 'Step and its children will be lost.',
additionalModalText,
buttonOptions,
});

if (!isReplaceConfirmed) return;
if (!modalAnswer) return;
}

/** Find compatible components */
Expand All @@ -44,7 +52,7 @@ export const ItemReplaceStep: FunctionComponent<ItemReplaceStepProps> = (props)
const definedComponent = await catalogModalContext?.getNewComponent(catalogFilter);
if (!definedComponent) return;

processNodeInteractionAddonRecursively(props.vizNode, (vn) =>
processNodeInteractionAddonRecursively(props.vizNode, modalAnswer, (vn) =>
getRegisteredInteractionAddons(IInteractionAddonType.ON_DELETE, vn),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('item-delete-helper', () => {
};
addons[childVn.id] = [mockAddon];
vizNode.addChild(childVn);
processNodeInteractionAddonRecursively(vizNode, (vn) => addons[vn.id] ?? []);
processNodeInteractionAddonRecursively(vizNode, 1, (vn) => addons[vn.id] ?? []);
expect(mockAddon.callback).toHaveBeenCalled();
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import { IVisualizationNode } from '../../../../models';
import { IRegisteredInteractionAddon } from '../../../registers/interactions/node-interaction-addon.model';
import {
IModalCustomization,
IRegisteredInteractionAddon,
} from '../../../registers/interactions/node-interaction-addon.model';

export const processNodeInteractionAddonRecursively = (
parentVizNode: IVisualizationNode,
modalAnswer: number | undefined,
getAddons: (vizNode: IVisualizationNode) => IRegisteredInteractionAddon[],
) => {
parentVizNode.getChildren()?.forEach((child) => {
processNodeInteractionAddonRecursively(child, getAddons);
processNodeInteractionAddonRecursively(child, modalAnswer, getAddons);
});
getAddons(parentVizNode).forEach((addon) => {
addon.callback(parentVizNode);
addon.callback(parentVizNode, modalAnswer);
});
};

export const findModalCustomizationRecursively = (
parentVizNode: IVisualizationNode,
getAddons: (vizNode: IVisualizationNode) => IRegisteredInteractionAddon[],
) => {
const modalCustomizations: IModalCustomization[] = [];
// going breadth-first while addon processes depth-first... do we want?
getAddons(parentVizNode).forEach((addon) => {
if (addon.modalCustomization && !modalCustomizations.includes(addon.modalCustomization)) {
modalCustomizations.push(addon.modalCustomization);
}
});
parentVizNode.getChildren()?.forEach((child) => {
findModalCustomizationRecursively(child, getAddons).forEach((custom) => {
if (!modalCustomizations.includes(custom)) {
modalCustomizations.push(custom);
}
});
});
return modalCustomizations;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FunctionComponent, PropsWithChildren, useContext, useRef } from 'react';
import { datamapperActivationFn } from './datamapper.activationfn';
import { MetadataContext } from '../../providers';
import { ACTION_INDEX_DELETE_STEP_AND_FILE, ACTION_INDEX_DELETE_STEP_ONLY, 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';
import { ButtonVariant } from '@patternfly/react-core';

export const RegisterNodeInteractionAddons: FunctionComponent<PropsWithChildren> = ({ children }) => {
const metadataApi = useContext(MetadataContext)!;
Expand All @@ -12,8 +13,24 @@ export const RegisterNodeInteractionAddons: FunctionComponent<PropsWithChildren>
{
type: IInteractionAddonType.ON_DELETE,
activationFn: datamapperActivationFn,
callback: (vizNode) => {
metadataApi && onDeleteDataMapper(metadataApi, vizNode);
callback: (vizNode, modalAnswer) => {
metadataApi && onDeleteDataMapper(metadataApi, vizNode, modalAnswer);
},
modalCustomization: {
additionalText: 'Do you also delete the mapping file (XSLT) associated with the Kaoto DataMapper step?',
buttonOptions: [
{
index: ACTION_INDEX_DELETE_STEP_AND_FILE,
variant: ButtonVariant.danger,
buttonText: 'Delete both step and file',
},
{
index: ACTION_INDEX_DELETE_STEP_ONLY,
variant: ButtonVariant.secondary,
isDanger: true,
buttonText: 'Delete the step, but keep the file',
},
],
},
},
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { IVisualizationNode } from '../../../models';
import { ActionConfirmationButtonOption } from '../../../providers';

export enum IInteractionAddonType {
ON_DELETE = 'onDelete',
}

export interface IModalCustomization {
buttonOptions: ActionConfirmationButtonOption[];
additionalText?: string;
}

export interface IRegisteredInteractionAddon {
type: IInteractionAddonType;
activationFn: (vizNode: IVisualizationNode) => boolean;
callback: (vizNode: IVisualizationNode) => void;
callback: (vizNode: IVisualizationNode, modalAnswer: number | undefined) => void;
modalCustomization?: IModalCustomization;
}

export interface INodeInteractionAddonContext {
Expand Down
18 changes: 16 additions & 2 deletions packages/ui/src/multiplying-architecture/KaotoBridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ interface KaotoBridgeProps {
*/
saveResourceContent(path: string, content: string): Promise<void>;

/**
* Delete resource using the channel API.
* @param path The path of the resource
*/
deleteResource(path: string): Promise<boolean>;

/**
* Show a Quick Pick widget and ask the user to select one or more files available in the workspace.
* @param include The filter expression for the files to include
Expand Down Expand Up @@ -109,6 +115,7 @@ export const KaotoBridge = forwardRef<EditorApi, PropsWithChildren<KaotoBridgePr
setMetadata,
getResourceContent,
saveResourceContent,
deleteResource,
askUserForFileSelection,
},
forwardedRef,
Expand All @@ -120,8 +127,15 @@ export const KaotoBridge = forwardRef<EditorApi, PropsWithChildren<KaotoBridgePr
const settingsAdapter = useContext(SettingsContext);
const catalogUrl = settingsAdapter.getSettings().catalogUrl;
const metadataApi = useMemo(
() => ({ getMetadata, setMetadata, getResourceContent, saveResourceContent, askUserForFileSelection }),
[getMetadata, setMetadata, getResourceContent, saveResourceContent, askUserForFileSelection],
() => ({
getMetadata,
setMetadata,
getResourceContent,
saveResourceContent,
deleteResource,
askUserForFileSelection,
}),
[getMetadata, setMetadata, getResourceContent, saveResourceContent, deleteResource, askUserForFileSelection],
);

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class KaotoEditorApp implements Editor {
this.setMetadata = this.setMetadata.bind(this);
this.getResourceContent = this.getResourceContent.bind(this);
this.saveResourceContent = this.saveResourceContent.bind(this);
this.deleteResource = this.deleteResource.bind(this);
this.askUserForFileSelection = this.askUserForFileSelection.bind(this);
}

Expand Down Expand Up @@ -115,6 +116,10 @@ export class KaotoEditorApp implements Editor {
return this.envelopeContext.channelApi.requests.saveResourceContent(path, content);
}

async deleteResource(path: string): Promise<boolean> {
return this.envelopeContext.channelApi.requests.deleteResource(path);
}

async askUserForFileSelection(
include: string,
exclude?: string,
Expand All @@ -139,6 +144,7 @@ export class KaotoEditorApp implements Editor {
setMetadata={this.setMetadata}
getResourceContent={this.getResourceContent}
saveResourceContent={this.saveResourceContent}
deleteResource={this.deleteResource}
askUserForFileSelection={this.askUserForFileSelection}
>
<RouterProvider router={kaotoEditorRouter} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { FunctionComponent, PropsWithChildren, createContext, useCallback, useMe

export const ACTION_INDEX_CANCEL = 0;
export const ACTION_INDEX_CONFIRM = 1;
export const ACTION_INDEX_DELETE_STEP_AND_FILE = 1;
export const ACTION_INDEX_DELETE_STEP_ONLY = 2;

export interface ActionConfirmationButtonOption {
index: number;
buttonText: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/src/providers/metadata.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export interface IMetadataApi {
*/
saveResourceContent(path: string, content: string): Promise<void>;

/**
* Delete resource
* @param path The path of the resource
*/
deleteResource(path: string): Promise<boolean>;

/**
* Show a Quick Pick widget and ask the user to select one or more files available in the workspace.
* @param include The filter expression for the files to include
Expand Down
9 changes: 7 additions & 2 deletions packages/ui/src/services/datamapper-metadata.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,12 @@ export class DataMapperMetadataService {
});
}

static deleteMetadata(api: IMetadataApi, metadataId: string) {
api.setMetadata(metadataId, undefined);
static async deleteMetadata(api: IMetadataApi, metadataId: string) {
await api.setMetadata(metadataId, undefined);
}

static async deleteXsltFile(api: IMetadataApi, metadataId: string) {
const metadata = (await api.getMetadata(metadataId)) as IDataMapperMetadata;
await api.deleteResource(metadata.xsltPath);
}
}

0 comments on commit 0cd189e

Please sign in to comment.