Skip to content

Commit

Permalink
feat(designer): Adding support for dynamic input schema in dynamic pa…
Browse files Browse the repository at this point in the history
…rameters (#4844)

* feat(designer): Add support for dynamic input schema in dynamic parameters

* Fixing infinite dynamic call loop

* Adding tests

* Adding test for clearing dynamic inputs

---------

Co-authored-by: Priti Sambandam <psamband@microsoft.com>
  • Loading branch information
preetriti1 and Priti Sambandam committed May 31, 2024
1 parent 19b86d3 commit caa69b6
Show file tree
Hide file tree
Showing 12 changed files with 890 additions and 271 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface ChildWorkflowServiceOptions extends DynamicCallServiceOptions {
}

export class ChildWorkflowService {
private _workflowsRequestSchema: Record<string, any> = {};
private _workflowsRequestSchema: Record<string, any> | undefined;

constructor(private readonly options: ChildWorkflowServiceOptions) {
const { apiVersion, baseUrl, httpClient, siteResourceId, workflowName } = this.options;
Expand Down Expand Up @@ -79,7 +79,7 @@ export class ChildWorkflowService {
}

public async getLogicAppSwagger(workflowId: string): Promise<Record<string, any>> {
if (hasProperty(this._workflowsRequestSchema, workflowId)) {
if (hasProperty(this._workflowsRequestSchema ?? {}, workflowId)) {
return getPropertyValue(this._workflowsRequestSchema, workflowId);
}

Expand All @@ -90,6 +90,11 @@ export class ChildWorkflowService {
queryParameters: { 'api-version': apiVersion },
});
const schema = getTriggerSchema(workflowContent.properties?.definition?.triggers ?? {});

if (this._workflowsRequestSchema === undefined) {
this._workflowsRequestSchema = {};
}

this._workflowsRequestSchema[workflowId] = schema;

return schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,91 @@ const getDesignerServices = (
true /* supportsAuthenticationParameter */
);
},
getWorkflowSubSchema: (args: any) => {
const { parameters } = args;
if (parameters.param1 && parameters.param2 && parameters.param3) {
const objectType = `${parameters.param1}Type`;
return Promise.resolve({
type: 'object',
properties: {
numberType: {
type: 'number',
},
[objectType]: {
type: 'object',
properties: {
o1: {
type: 'integer',
},
dynamicObject2: {
type: 'object',
properties: {},
'x-ms-dynamic-properties': {
dynamicState: {
extension: {
operationId: 'getWorkflowSubSubSchema',
},
isInput: true,
},
parameters: {
param1: {
parameterReference: 'host.workflow.id',
required: true,
},
param2: {
parameterReference: 'body.objectType.p1',
required: true,
},
param3: {
parameterReference: `body.dynamicObject.${objectType}.o1`,
required: true,
},
},
},
},
},
},
},
});
}

return Promise.resolve({
type: 'object',
properties: {
mockString: {
type: 'string',
},
},
});
},
getWorkflowSubSubSchema: (args: any) => {
const { parameters } = args;
if (parameters.param1 && parameters.param2 && parameters.param3) {
return Promise.resolve({
type: 'object',
properties: {
[`${parameters.param1}-Type`]: {
type: 'string',
},
[`${parameters.param2}-Type`]: {
type: 'string',
},
[`${parameters.param3}-Type`]: {
type: 'string',
},
},
});
}

return Promise.resolve({
type: 'object',
properties: {
mockBool: {
type: 'boolean',
},
},
});
},
},
valuesClient: {
getWorkflows: () => childWorkflowService.getWorkflowsWithRequestTrigger(),
Expand Down
1 change: 1 addition & 0 deletions libs/designer-ui/src/lib/editor/models/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface ParameterDetails {
format?: string;
in?: string;
isDynamic?: boolean;
dynamicParameterReference?: string;
isEditorManagedItem?: boolean; // Note: Flag to indicate whether this parameter is managed by a specific editor
isUnknown?: boolean; // Whether the parameter is an unknown parameter (inferred to be 'any' type) sourced from the workflow definition
parentProperty?: any;
Expand Down
15 changes: 2 additions & 13 deletions libs/designer/src/lib/core/actions/bjsworkflow/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { getTriggerNodeId, isRootNodeInGraph } from '../../utils/graph';
import { getParameterFromName, updateDynamicDataInNode } from '../../utils/parameters/helper';
import { getInputParametersFromSwagger, getOutputParametersFromSwagger } from '../../utils/swagger/operation';
import { convertOutputsToTokens, getBuiltInTokens, getTokenNodeIds } from '../../utils/tokens';
import { getAllVariables, getVariableDeclarations, setVariableMetadata } from '../../utils/variables';
import { getVariableDeclarations, setVariableMetadata } from '../../utils/variables';
import { isConnectionRequiredForOperation, updateNodeConnection } from './connections';
import {
getInputParametersFromManifest,
Expand Down Expand Up @@ -251,18 +251,7 @@ export const initializeOperationDetails = async (
);
}
} else {
updateDynamicDataInNode(
nodeId,
isTrigger,
operationInfo,
undefined,
initData.nodeDependencies,
initData.nodeInputs,
initData.settings as Settings,
getAllVariables(getState().tokens.variables),
dispatch,
getState
);
updateDynamicDataInNode(nodeId, isTrigger, operationInfo, undefined, initData.nodeDependencies, dispatch, getState);
}

dispatch(setIsPanelLoading(false));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from '../../utils/connectors/connections';
import { isRootNodeInGraph } from '../../utils/graph';
import { updateDynamicDataInNode } from '../../utils/parameters/helper';
import { getAllVariables } from '../../utils/variables';
import type {
IOperationManifestService,
Connection,
Expand Down Expand Up @@ -89,13 +88,11 @@ const updateNodeConnectionAndProperties = async (
const newState = getState() as RootState;
const operationInfo = getRecordEntry(newState.operations.operationInfo, nodeId);
const dependencies = getRecordEntry(newState.operations.dependencies, nodeId);
const inputParameters = getRecordEntry(newState.operations.inputParameters, nodeId);
const settings = getRecordEntry(newState.operations.settings, nodeId);
const newlyAddedOperations = getRecordEntry(newState.workflow.newlyAddedOperations, nodeId);
const operation = getRecordEntry(newState.workflow.operations, nodeId);

// Shouldn't happen, but required for type checking
if (!operationInfo || !dependencies || !inputParameters || !settings) {
if (!operationInfo || !dependencies) {
return;
}

Expand All @@ -105,9 +102,6 @@ const updateNodeConnectionAndProperties = async (
operationInfo,
getConnectionReference(newState.connections, nodeId),
dependencies,
inputParameters,
settings,
getAllVariables(newState.tokens.variables),
dispatch,
getState,
newlyAddedOperations ? undefined : operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
import { isTokenValueSegment } from '../../utils/parameters/segment';
import { initializeOperationDetailsForSwagger } from '../../utils/swagger/operation';
import { convertOutputsToTokens, getBuiltInTokens, getTokenNodeIds } from '../../utils/tokens';
import { getAllVariables, getVariableDeclarations, setVariableMetadata } from '../../utils/variables';
import { getVariableDeclarations, setVariableMetadata } from '../../utils/variables';
import type { PasteScopeParams } from './copypaste';
import {
getCustomSwaggerIfNeeded,
Expand Down Expand Up @@ -559,11 +559,9 @@ export const initializeDynamicDataInNodes = async (
const rootState = getState();
const {
workflow: { nodesMetadata, operations },
operations: { inputParameters, settings, dependencies, operationInfo, errors },
tokens: { variables },
operations: { dependencies, operationInfo, errors },
connections,
} = rootState;
const allVariables = getAllVariables(variables);
for (const [nodeId, operation] of Object.entries(operations)) {
if (operationsToInitialize && !operationsToInitialize.includes(nodeId)) {
continue;
Expand All @@ -577,9 +575,8 @@ export const initializeDynamicDataInNodes = async (

const nodeOperationInfo = getRecordEntry(operationInfo, nodeId);
const nodeDependencies = getRecordEntry(dependencies, nodeId);
const nodeInputs = getRecordEntry(inputParameters, nodeId);
const nodeSettings = getRecordEntry(settings, nodeId);
if (!nodeOperationInfo || !nodeDependencies || !nodeInputs || !nodeSettings) {

if (!nodeOperationInfo || !nodeDependencies) {
continue;
}

Expand All @@ -592,9 +589,6 @@ export const initializeDynamicDataInNodes = async (
nodeOperationInfo,
connectionReference,
nodeDependencies,
nodeInputs,
nodeSettings,
allVariables,
dispatch,
getState,
operation
Expand All @@ -610,29 +604,14 @@ const updateDynamicDataForValidConnection = async (
operationInfo: NodeOperation,
reference: ConnectionReference | undefined,
dependencies: NodeDependencies,
nodeInputs: NodeInputs,
settings: Settings,
variables: any,
dispatch: Dispatch,
getState: () => RootState,
operation: LogicAppsV2.ActionDefinition | LogicAppsV2.TriggerDefinition
): Promise<void> => {
const isValidConnection = await isConnectionReferenceValid(operationInfo, reference);

if (isValidConnection) {
updateDynamicDataInNode(
nodeId,
isTrigger,
operationInfo,
reference,
dependencies,
nodeInputs,
settings,
variables,
dispatch,
getState,
operation
);
updateDynamicDataInNode(nodeId, isTrigger, operationInfo, reference, dependencies, dispatch, getState, operation);
} else {
LoggerService().log({
level: LogEntryLevel.Warning,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getInputDependencies } from '../../actions/bjsworkflow/initialize';
import type { Settings } from '../../actions/bjsworkflow/settings';
import type { NodeStaticResults } from '../../actions/bjsworkflow/staticresults';
import { StaticResultOption } from '../../actions/bjsworkflow/staticresults';
Expand All @@ -8,14 +7,7 @@ import { getTokenTitle, normalizeKey } from '../../utils/tokens';
import { resetNodesLoadStatus, resetWorkflowState } from '../global';
import { LogEntryLevel, LoggerService, filterRecord, getRecordEntry } from '@microsoft/logic-apps-shared';
import type { ParameterInfo } from '@microsoft/designer-ui';
import type {
FilePickerInfo,
InputParameter,
OutputParameter,
SwaggerParser,
OpenAPIV2,
OperationInfo,
} from '@microsoft/logic-apps-shared';
import type { FilePickerInfo, InputParameter, OutputParameter, OpenAPIV2, OperationInfo } from '@microsoft/logic-apps-shared';
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { WritableDraft } from 'immer/dist/internal';
Expand Down Expand Up @@ -184,14 +176,14 @@ export interface ClearDynamicIOPayload {
nodeIds?: string[];
inputs?: boolean;
outputs?: boolean;
dynamicParameterKeys?: string[];
}

interface AddDynamicInputsPayload {
nodeId: string;
groupId: string;
inputs: ParameterInfo[];
newInputs: InputParameter[];
swagger?: SwaggerParser;
dependencies?: Record<string, DependencyInfo>;
}

export interface UpdateParametersPayload {
Expand Down Expand Up @@ -251,26 +243,15 @@ export const operationMetadataSlice = createSlice({
});
},
addDynamicInputs: (state, action: PayloadAction<AddDynamicInputsPayload>) => {
const { nodeId, groupId, inputs, newInputs: rawInputs, swagger } = action.payload;
const { nodeId, groupId, inputs, dependencies } = action.payload;
const inputParameters = getRecordEntry(state.inputParameters, nodeId) ?? {
parameterGroups: {},
};
const parameterGroups = getRecordEntry(inputParameters?.parameterGroups, groupId);
if (parameterGroups) {
const { parameters } = parameterGroups;
const newParameters = [...parameters];
for (const input of inputs) {
const index = newParameters.findIndex((parameter) => parameter.parameterKey === input.parameterKey);
if (index > -1) {
newParameters.splice(index, 1, input);
} else {
newParameters.push(input);
}
}
parameterGroups.parameters = newParameters;
const parameterGroup = getRecordEntry(inputParameters?.parameterGroups, groupId);
if (parameterGroup) {
parameterGroup.parameters = inputs;
}

const dependencies = getInputDependencies(inputParameters, rawInputs, swagger);
if (dependencies) {
state.dependencies[nodeId].inputs = {
...state.dependencies[nodeId].inputs,
Expand All @@ -288,7 +269,7 @@ export const operationMetadataSlice = createSlice({
updateExistingInputTokenTitles(state, action.payload);
},
clearDynamicIO: (state, action: PayloadAction<ClearDynamicIOPayload>) => {
const { nodeId, nodeIds: _nodeIds, inputs = true, outputs = true } = action.payload;
const { nodeId, nodeIds: _nodeIds, inputs = true, outputs = true, dynamicParameterKeys = [] } = action.payload;
const nodeIds = _nodeIds ?? [nodeId];
for (const nodeId of nodeIds) {
const nodeErrors = getRecordEntry(state.errors, nodeId);
Expand All @@ -297,15 +278,26 @@ export const operationMetadataSlice = createSlice({
delete nodeErrors?.[ErrorLevel.DynamicInputs];

const inputParameters = getRecordEntry(state.inputParameters, nodeId);
const deletedDynamicParameters: string[] = [];
if (inputParameters) {
for (const group of Object.values(inputParameters.parameterGroups)) {
group.parameters = group.parameters.filter((parameter) => !parameter.info.isDynamic);
group.parameters = group.parameters.filter((parameter) => {
const shouldDelete =
parameter.info.isDynamic &&
(!dynamicParameterKeys.length || dynamicParameterKeys.includes(parameter.info.dynamicParameterReference ?? ''));
if (shouldDelete) {
deletedDynamicParameters.push(parameter.parameterKey);
return false;
}

return true;
});
}
}

const inputDependencies = getRecordEntry(state.dependencies, nodeId)?.inputs as WritableDraft<Record<string, DependencyInfo>>;
for (const inputKey of Object.keys(inputDependencies ?? {})) {
if (inputDependencies[inputKey].parameter?.isDynamic && inputDependencies[inputKey].dependencyType !== 'ApiSchema') {
if (inputDependencies[inputKey].parameter?.isDynamic && deletedDynamicParameters.includes(inputKey)) {
delete getRecordEntry(state.dependencies, nodeId)?.inputs[inputKey];
}
}
Expand Down
Loading

0 comments on commit caa69b6

Please sign in to comment.