Skip to content

Commit

Permalink
fix(designer): Remove tokens from a node whena node is deleted (#4773)
Browse files Browse the repository at this point in the history
* fix(designer): Remove tokens from a node whena  node is deleted

* Finish writing tests
  • Loading branch information
hartra344 committed May 6, 2024
1 parent 430c5fc commit 0f960c9
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 9 deletions.
4 changes: 2 additions & 2 deletions __mocks__/workflows/Panel.json
Expand Up @@ -34,7 +34,7 @@
"Parse_JSON": {
"type": "ParseJson",
"inputs": {
"content": "@{triggerBody()?['string']}@{variables('ArrayVariable')}",
"content": "@{triggerBody()?['string']}@{variables('ArrayVariable')}@{parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')}",
"schema": {
"type": "array",
"items": {
Expand Down Expand Up @@ -67,7 +67,7 @@
"Filter_array": {
"type": "Query",
"inputs": {
"from": "@body('Parse_JSON')",
"from": "@{body('Parse_JSON')}test",
"where": "@not(contains(length(split(item(), '|')?[0]),length(split(item(), '|')?[0])))"
},
"runAfter": {
Expand Down
109 changes: 109 additions & 0 deletions e2e/designer/tokenRemoval.spec.ts
@@ -0,0 +1,109 @@
import test, { expect } from '@playwright/test';
import { GoToMockWorkflow } from './utils/GoToWorkflow';
import { getSerializedWorkflowFromState } from './utils/designerFunctions';

test.describe(
'Token Picker Tests',
{
tag: '@mock',
},
async () => {
test('Tokens should be removed from parameters when operation is deleted', async ({ page }) => {
await page.goto('/');
await GoToMockWorkflow(page, 'Panel');
const serializedOld: any = await getSerializedWorkflowFromState(page);
expect(serializedOld.definition.actions.Filter_array.inputs.from).toEqual("@{body('Parse_JSON')}test");
await page.getByLabel('Parse JSON operation, Data').click({
button: 'right',
});
await page.getByText('Delete', { exact: true }).click();
await page.getByRole('button', { name: 'OK' }).click();
const serializedNew: any = await getSerializedWorkflowFromState(page);
expect(serializedNew.definition.actions.Filter_array.inputs.from).toEqual('test');
expect(JSON.stringify(serializedNew)).not.toContain("@body('Parse_JSON')");
expect(JSON.stringify(serializedNew)).not.toContain("@{body('Parse_JSON')}");
});
test('Tokens should be removed from parameters when variable is deleted', async ({ page }) => {
await page.goto('/');

await GoToMockWorkflow(page, 'Panel');
const serializedOld: any = await getSerializedWorkflowFromState(page);
expect(serializedOld.definition.actions.Parse_JSON.inputs.content).toEqual(
"@{triggerBody()?['string']}@{variables('ArrayVariable')}@{parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')}"
);
expect(serializedOld.definition.actions.HTTP.inputs.body).toEqual("@variables('ArrayVariable')");
await page.getByLabel('Initialize variable operation').click({
button: 'right',
});
await page.getByText('Delete', { exact: true }).click();
await page.getByRole('button', { name: 'OK' }).click();
const serializedNew: any = await getSerializedWorkflowFromState(page);
expect(serializedNew.definition.actions.Parse_JSON.inputs.content).toEqual(
"@{triggerBody()?['string']}@{parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')}"
);
expect(serializedNew.definition.actions.HTTP.inputs.body).toBeUndefined();
expect(JSON.stringify(serializedNew)).not.toContain("@variables('ArrayVariable')");
expect(JSON.stringify(serializedNew)).not.toContain("@{variables('ArrayVariable')}");
});
test('Tokens should be removed from parameters when workflow parameter is deleted', async ({ page }) => {
await page.goto('/');

await GoToMockWorkflow(page, 'Panel');
const serializedOld: any = await getSerializedWorkflowFromState(page);
expect(serializedOld.definition.actions.Parse_JSON.inputs.content).toEqual(
"@{triggerBody()?['string']}@{variables('ArrayVariable')}@{parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')}"
);
await page.getByRole('button', { name: 'Workflow Parameters' }).click();
await page.getByTestId('parameter-edit-icon-button').click();
await page.getByLabel('Delete Parameter').click();
await page
.locator('div')
.filter({ hasText: /^Parameters$/ })
.getByRole('button')
.click();
const serializedNew: any = await getSerializedWorkflowFromState(page);
expect(serializedNew.definition.actions.Parse_JSON.inputs.content).toEqual(
"@{triggerBody()?['string']}@{variables('ArrayVariable')}"
);
expect(JSON.stringify(serializedNew)).not.toContain(
"@parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')"
);
expect(JSON.stringify(serializedNew)).not.toContain(
"@{parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')}"
);
});
test('Output should be correct when multiple tokens get removed by removing source', async ({ page }) => {
await page.goto('/');

await GoToMockWorkflow(page, 'Panel');
const serializedOld: any = await getSerializedWorkflowFromState(page);
expect(serializedOld.definition.actions.Parse_JSON.inputs.content).toEqual(
"@{triggerBody()?['string']}@{variables('ArrayVariable')}@{parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')}"
);
await page.getByRole('button', { name: 'Workflow Parameters' }).click();
await page.getByTestId('parameter-edit-icon-button').click();
await page.getByLabel('Delete Parameter').click();
await page
.locator('div')
.filter({ hasText: /^Parameters$/ })
.getByRole('button')
.click();
await page.getByLabel('Initialize variable operation').click({
button: 'right',
});
await page.getByText('Delete', { exact: true }).click();
await page.getByRole('button', { name: 'OK' }).click();
const serializedNew: any = await getSerializedWorkflowFromState(page);
expect(serializedNew.definition.actions.Parse_JSON.inputs.content).toEqual("@triggerBody()?['string']");
expect(JSON.stringify(serializedNew)).not.toContain("@variables('ArrayVariable')");
expect(JSON.stringify(serializedNew)).not.toContain("@{variables('ArrayVariable')}");
expect(JSON.stringify(serializedNew)).not.toContain(
"@parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')"
);
expect(JSON.stringify(serializedNew)).not.toContain(
"@{parameters('EILCO Admin Nominations-OCSA List (cr773_EILCOAdminNominations_OCSA_L2)')}"
);
expect(JSON.stringify(serializedNew)).toContain("@{body('Parse_JSON')}test");
});
}
);
66 changes: 62 additions & 4 deletions libs/designer/src/lib/core/actions/bjsworkflow/delete.ts
@@ -1,10 +1,10 @@
import { isCustomCode } from '@microsoft/designer-ui';
import { type ParameterInfo, isCustomCode } from '@microsoft/designer-ui';
import type { RootState } from '../../..';
import constants from '../../../common/constants';
import type { WorkflowNode } from '../../parsers/models/workflowNode';
import { removeNodeConnectionData } from '../../state/connection/connectionSlice';
import { deleteCustomCode } from '../../state/customcode/customcodeSlice';
import { deinitializeNodes, deinitializeOperationInfo } from '../../state/operation/operationMetadataSlice';
import { deinitializeNodes, deinitializeOperationInfo, updateNodeParameters } from '../../state/operation/operationMetadataSlice';
import { clearPanel } from '../../state/panel/panelSlice';
import { setValidationError } from '../../state/setting/settingSlice';
import { deinitializeStaticResultProperty } from '../../state/staticresultschema/staticresultsSlice';
Expand All @@ -16,6 +16,8 @@ import { WORKFLOW_NODE_TYPES, getRecordEntry } from '@microsoft/logic-apps-share
import type { Dispatch } from '@reduxjs/toolkit';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { batch } from 'react-redux';
import { deleteParameter } from '../../state/workflowparameters/workflowparametersSlice';
import { isParameterToken, isTokenValueSegment, isVariableToken } from '../../utils/parameters/segment';

type DeleteOperationPayload = {
nodeId: string;
Expand All @@ -27,6 +29,10 @@ export type DeleteGraphPayload = {
graphNode: WorkflowNode;
};

export const deleteWorkflowParameter = createAsyncThunk('deleteWorkflowParameter', async (parameterId: string, { getState, dispatch }) => {
removeAllTokensFromNode(getState() as RootState, dispatch, undefined, parameterId);
dispatch(deleteParameter(parameterId));
});
export const deleteOperation = createAsyncThunk(
'deleteOperation',
async (deletePayload: DeleteOperationPayload, { getState, dispatch }) => {
Expand All @@ -38,21 +44,73 @@ export const deleteOperation = createAsyncThunk(

dispatch(deleteNode(deletePayload));
deleteCustomCodeInfo(nodeId, dispatch, getState() as RootState);
deleteOperationDetails(nodeId, dispatch);
deleteOperationDetails(nodeId, dispatch, getState() as RootState);
updateAllUpstreamNodes(getState() as RootState, dispatch);
});
}
);

const deleteOperationDetails = async (nodeId: string, dispatch: Dispatch): Promise<void> => {
const deleteOperationDetails = async (nodeId: string, dispatch: Dispatch, state: RootState): Promise<void> => {
dispatch(removeNodeConnectionData({ nodeId }));
dispatch(deinitializeNodes([nodeId]));
removeAllTokensFromNode(state, dispatch, nodeId);
dispatch(deinitializeTokensAndVariables({ id: nodeId }));

dispatch(deinitializeOperationInfo({ id: nodeId }));
dispatch(setValidationError({ nodeId, errors: [] }));
dispatch(deinitializeStaticResultProperty({ id: nodeId + 0 }));
};

export const removeAllTokensFromNode = (state: RootState, dispatch: Dispatch, nodeId?: string, parameterId?: String): void => {
const variables = nodeId ? state.tokens.variables[nodeId] : [];
const nodeInputs = state.operations.inputParameters;
for (const [nid, inputParam] of Object.entries(nodeInputs)) {
for (const [, group] of Object.entries(inputParam.parameterGroups)) {
const parametersToUpdate: {
groupId: string;
parameterId: string;
propertiesToUpdate: Partial<ParameterInfo>;
}[] = [];
let updatedValue = false;
for (const param of group.parameters) {
let paramValue = [...param.value];
for (const value of param.value) {
if (isTokenValueSegment(value) && value.token) {
if (isVariableToken(value.token)) {
if (variables?.find((v) => v.name === value.token?.name)) {
paramValue = paramValue.filter((v) => v.id !== value.id);
updatedValue = true;
}
} else if (parameterId && isParameterToken(value.token) && value.token?.name === parameterId) {
paramValue = paramValue.filter((v) => v.id !== value.id);
updatedValue = true;
} else if (nodeId && value.token?.actionName === nodeId) {
paramValue = paramValue.filter((v) => v.id !== value.id);
updatedValue = true;
}
}
}
if (updatedValue) {
parametersToUpdate.push({
groupId: group.id,
parameterId: param.id,
propertiesToUpdate: { value: paramValue, preservedValue: undefined },
});
}
}
if (parametersToUpdate.length > 0) {
dispatch(
updateNodeParameters({
nodeId: nid,
isUserAction: true,
parameters: parametersToUpdate,
})
);
}
}
}
};

const deleteCustomCodeInfo = (nodeId: string, dispatch: Dispatch, state: RootState): void => {
const nodeInputs = getRecordEntry(state.operations.inputParameters, nodeId);
if (nodeInputs) {
Expand Down
@@ -1,5 +1,7 @@
import type { AppDispatch } from '../../../core/state/templates/store';
import { deleteWorkflowParameter } from '../../../core/actions/bjsworkflow/delete';
import { useLegacyWorkflowParameters, useReadOnly } from '../../../core/state/designerOptions/designerOptionsSelectors';
import { addParameter, deleteParameter, updateParameter } from '../../../core/state/workflowparameters/workflowparametersSlice';
import { addParameter, updateParameter } from '../../../core/state/workflowparameters/workflowparametersSlice';
import {
useWorkflowParameters,
useWorkflowParameterValidationErrors,
Expand All @@ -9,14 +11,14 @@ import { WorkflowParameters } from '@microsoft/designer-ui';
import { useDispatch } from 'react-redux';

export const WorkflowParametersPanel = (props: CommonPanelProps) => {
const dispatch = useDispatch();
const dispatch = useDispatch<AppDispatch>();
const readOnly = useReadOnly();
const useLegacy = useLegacyWorkflowParameters();
const workflowParameters = useWorkflowParameters();
const workflowParametersValidationErrors = useWorkflowParameterValidationErrors();

const onWorkflowParameterAdd = () => dispatch(addParameter());
const onDeleteWorkflowParameter = (event: { id: string }) => dispatch(deleteParameter(event.id));
const onDeleteWorkflowParameter = (event: { id: string }) => dispatch(deleteWorkflowParameter(event.id));
const onUpdateParameter = (event: WorkflowParameterUpdateEvent) => dispatch(updateParameter(event));

return (
Expand Down

0 comments on commit 0f960c9

Please sign in to comment.