Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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: 8 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1819,6 +1819,7 @@
"XOAcjQ": "(UTC+03:00) Nairobi",
"XOzn/3": "Connection name",
"XPBoDw": "Select an option",
"XQ3pyc": "Invalid JSON. Fix the errors before saving.",
"XQ4OCV": "(UTC+03:00) Baghdad",
"XR4Sd/": "Like",
"XR5izH": "Connected",
Expand Down Expand Up @@ -3782,6 +3783,7 @@
"_XOAcjQ.comment": "Time zone value ",
"_XOzn/3.comment": "This is for a label for a badge, it is used for screen readers and not shown on the screen.",
"_XPBoDw.comment": "Select option placeholder",
"_XQ3pyc.comment": "Validation error shown when the edited action code is not valid JSON",
"_XQ4OCV.comment": "Time zone value ",
"_XR4Sd/.comment": "Chatbot user feedback like button title",
"_XR5izH.comment": "Label text to connected status",
Expand Down Expand Up @@ -3974,6 +3976,7 @@
"_avaDe+.comment": "Title for the MCP server workflows section",
"_ayaW+i.comment": "Label for the MCP server workflows field",
"_az+QCK.comment": "Logic app name validation message text",
"_b/Zr/q.comment": "Label shown while saving the edited action code",
"_b0wO2+.comment": "Stateless workflow description",
"_b2aL+f.comment": "Text indicating a menu button to pin an action to the side panel",
"_b6G9bq.comment": "Label for description of custom encodeUriComponent Function",
Expand Down Expand Up @@ -4725,6 +4728,7 @@
"_p+zBbS.comment": "Hint shown after 10 seconds when loading Foundry agents takes longer than expected.",
"_p/0r2N.comment": "Required string parameter to be used as key for formDataValue function",
"_p/Pfr/.comment": "Message indicating that the user does not have write permissions for the role",
"_p/Vu7Z.comment": "Button to discard edits made to the action code",
"_p0BE2D.comment": "Button text to trigger clone in the create workflow panel",
"_p1IEXb.comment": "Label for button to open dynamic content token picker",
"_p2eSD1.comment": "Button text for opening panel for editing workflows",
Expand Down Expand Up @@ -4976,6 +4980,7 @@
"_ti2c1D.comment": "Button text for moving to the next tab with action count",
"_ti5TEd.comment": "Text for cancel button",
"_tjQdhq.comment": "Solution type of the template",
"_tjwYOf.comment": "Button to save the edited action code",
"_tkkN++.comment": "Description for template profile tab",
"_toHITB.comment": "BizTalk Migration category",
"_toWTrl.comment": "Search from file list",
Expand Down Expand Up @@ -5353,6 +5358,7 @@
"avaDe+": "MCP API key",
"ayaW+i": "Workflows",
"az+QCK": "Logic app name must start with a letter and can only contain letters, digits, \"_\" and \"-\".",
"b/Zr/q": "Saving…",
"b0wO2+": "Optimized for low latency, ideal for request-response and processing IoT events.",
"b2aL+f": "Pin action",
"b6G9bq": "URL encodes the input string",
Expand Down Expand Up @@ -6104,6 +6110,7 @@
"p+zBbS": "This may take a moment for new connections.",
"p/0r2N": "Required. The key name of the form data value to return.",
"p/Pfr/": "Missing role write permissions",
"p/Vu7Z": "Discard",
"p0BE2D": "Clone",
"p1IEXb": "Enter the data from previous step. You can also add data by typing the '/' character.",
"p2eSD1": "Edit",
Expand Down Expand Up @@ -6355,6 +6362,7 @@
"ti2c1D": "Next ({count} selected)",
"ti5TEd": "Cancel",
"tjQdhq": "Type",
"tjwYOf": "Save",
"tkkN++": "Add details to help template users evaluate this template. The profile includes the information shown to users and settings that control how the template is filtered and displayed.",
"toHITB": "BizTalk Migration",
"toWTrl": "Search",
Expand Down
112 changes: 112 additions & 0 deletions libs/designer-ui/src/lib/peek/editableCodeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { MonacoEditor } from '../editor/monaco';
import { EditorLanguage } from '@microsoft/logic-apps-shared';
import { Button, Spinner, Text, makeStyles, tokens } from '@fluentui/react-components';
import { usePeekStyles } from './styles';

const useEditableCodeViewStyles = makeStyles({
container: {
display: 'flex',
flexDirection: 'column',
height: '100%',
gap: '8px',
},
toolbar: {
display: 'flex',
alignItems: 'center',
gap: '8px',
},
spacer: {
flexGrow: 1,
},
error: {
color: tokens.colorPaletteRedForeground1,
},
editorWrapper: {
flexGrow: 1,
minHeight: 0,
},
});

export interface EditableCodeViewProps {
/** The current text shown in the editor (controlled). */
value: string;
/** Called on every edit with the new editor contents. */
onChange: (value: string) => void;
/** Called when the user clicks Save. */
onSave: () => void;
/** Called when the user clicks Discard. */
onDiscard: () => void;
/** Whether the editor contents differ from the last applied value. */
isDirty?: boolean;
/** Whether a save is currently in progress. */
isSaving?: boolean;
/** Validation/error message to surface above the editor. */
errorMessage?: string;
/** Whether editing is disabled (renders a read-only editor). */
readOnly?: boolean;
labels: {
save: string;
saving: string;
discard: string;
};
}

export function EditableCodeView({
value,
onChange,
onSave,
onDiscard,
isDirty = false,
isSaving = false,
errorMessage,
readOnly = false,
labels,
}: EditableCodeViewProps): JSX.Element {
const peekStyles = usePeekStyles();
const styles = useEditableCodeViewStyles();

const hasError = !!errorMessage;
const saveDisabled = readOnly || !isDirty || isSaving || hasError;
const discardDisabled = readOnly || !isDirty || isSaving;

return (
<div className={styles.container} data-automation-id="msla-editable-code-view">
<div className={styles.toolbar}>
{errorMessage ? (
<Text size={200} className={styles.error} role="alert" data-automation-id="msla-editable-code-view-error">
{errorMessage}
</Text>
) : null}
<div className={styles.spacer} />
{isSaving ? <Spinner size="tiny" label={labels.saving} labelPosition="after" /> : null}
<Button size="small" appearance="secondary" onClick={onDiscard} disabled={discardDisabled}>
{labels.discard}
</Button>
<Button
size="small"
appearance="primary"
onClick={onSave}
disabled={saveDisabled}
data-automation-id="msla-editable-code-view-save"
>
{labels.save}
</Button>
</div>
<div className={`msla-card-inner-body ${peekStyles.root} ${styles.editorWrapper}`}>
<MonacoEditor
className={'msla-monaco-peek'}
value={value}
onContentChanged={(e) => onChange(e.value ?? '')}
fontSize={13}
readOnly={readOnly}
folding={false}
lineNumbers={'on'}
language={EditorLanguage.json}
label="editable-code-view"
wordWrap={'on'}
monacoContainerStyle={{ height: '100%' }}
/>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions libs/designer-ui/src/lib/peek/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { MonacoEditor } from '../editor/monaco';
import { EditorLanguage } from '@microsoft/logic-apps-shared';
import { usePeekStyles } from './styles';

export * from './editableCodeView';

export interface PeekProps {
input: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ const processChildGraphAndItsInputs = (
return nodesData;
};

const updateTokenMetadataInParameters = (
export const updateTokenMetadataInParameters = (
nodes: NodeDataWithOperationMetadata[],
operations: Operations,
workflowParameters: Record<string, WorkflowParameter>,
Expand Down Expand Up @@ -583,7 +583,7 @@ const updateTokenMetadataInParameters = (
}
};

const initializeOutputTokensForOperations = (
export const initializeOutputTokensForOperations = (
allNodesData: NodeDataWithOperationMetadata[],
operations: Operations,
graph: WorkflowNode,
Expand Down Expand Up @@ -645,7 +645,7 @@ const initializeOutputTokensForOperations = (
return result;
};

const initializeVariables = (
export const initializeVariables = (
operations: Operations,
allNodesData: NodeDataWithOperationMetadata[]
): Record<string, VariableDeclaration[]> => {
Expand All @@ -670,7 +670,7 @@ const initializeVariables = (
return declarations;
};

const initializeRepetitionInfos = async (
export const initializeRepetitionInfos = async (
triggerNodeId: string,
allOperations: Operations,
nodesData: NodeDataWithOperationMetadata[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { getTriggerNodeId, type RootState } from '../..';
import type { LogicAppsV2 } from '@microsoft/logic-apps-shared';
import { OperationManifestService, getRecordEntry } from '@microsoft/logic-apps-shared';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { setIsPanelLoading } from '../../state/panel/panelSlice';
import { initializeNodes } from '../../state/operation/operationMetadataSlice';
import { initializeTokensAndVariables } from '../../state/tokens/tokensSlice';
import { replaceOperationDefinition } from '../../state/workflow/workflowSlice';
import { isManagedMcpOperation } from '../../state/workflow/helper';
import { isTriggerNode } from '../../utils/graph';
import Constants from '../../../common/constants';
import { initializeConnectorOperationDetails } from './agent';
import { updateAllUpstreamNodes } from './initialize';
import {
initializeDynamicDataInNodes,
initializeOperationDetailsForManagedMcpServer,
initializeOperationDetailsForManifest,
initializeOutputTokensForOperations,
initializeRepetitionInfos,
initializeVariables,
updateTokenMetadataInParameters,
type NodeDataWithOperationMetadata,
} from './operationdeserializer';
import { initializeOperationDetailsForSwagger } from '../../utils/swagger/operation';

export interface UpdateNodeFromCodeViewPayload {
nodeId: string;
serializedOperation: LogicAppsV2.OperationDefinition;
}

/**
* Applies an edited single-operation definition (from the per-action Code view) back into
* the designer state. Only the edited node is re-initialized; other nodes (and their
* unsaved edits) are left untouched.
*
* Note: structural/graph changes (renaming the action key, changing runAfter / nesting,
* adding or removing child actions) are NOT applied here -- those still require the
* whole-workflow code view.
*/
export const updateNodeFromCodeView = createAsyncThunk(
'updateNodeFromCodeView',
async (payload: UpdateNodeFromCodeViewPayload, { dispatch, getState }): Promise<void> => {
const { nodeId, serializedOperation } = payload;
const state = getState() as RootState;

if (!getRecordEntry(state.workflow.operations, nodeId)) {
return;
}

dispatch(setIsPanelLoading(true));
try {
// Persist the new definition so serialization/runAfter fallbacks stay in sync.
dispatch(replaceOperationDefinition({ nodeId, operationDefinition: serializedOperation }));

const updatedState = getState() as RootState;
const { operations, nodesMetadata, workflowKind, graph } = updatedState.workflow;
const references = updatedState.connections.connectionReferences;
const workflowParameters = updatedState.workflowParameters.definitions;
const isTrigger = isTriggerNode(nodeId, nodesMetadata);
const triggerNodeId = getTriggerNodeId(updatedState.workflow);

const operationManifestService = OperationManifestService();
let nodeData: NodeDataWithOperationMetadata[] | undefined;
if (isManagedMcpOperation(serializedOperation)) {
nodeData = await initializeOperationDetailsForManagedMcpServer(nodeId, serializedOperation, references, workflowKind, dispatch);
} else if (serializedOperation.type === Constants.NODE.TYPE.CONNECTOR) {
nodeData = await initializeConnectorOperationDetails(
nodeId,
serializedOperation as LogicAppsV2.ConnectorAction,
workflowKind,
dispatch
);
} else if (operationManifestService.isSupported(serializedOperation.type, serializedOperation.kind)) {
nodeData = await initializeOperationDetailsForManifest(
nodeId,
serializedOperation,
{} /* customCode */,
isTrigger,
workflowKind,
dispatch
);
} else {
nodeData = await initializeOperationDetailsForSwagger(nodeId, serializedOperation, references, isTrigger, workflowKind, dispatch);
}

if (!nodeData || nodeData.length === 0) {
return;
}

const repetitionInfos = await initializeRepetitionInfos(triggerNodeId, operations, nodeData, nodesMetadata);
updateTokenMetadataInParameters(nodeData, operations, workflowParameters, nodesMetadata, triggerNodeId, repetitionInfos);

dispatch(
initializeNodes({
nodes: nodeData.map((data) => {
const { id, nodeInputs, nodeOutputs, nodeDependencies, settings, operationMetadata, staticResult, supportedChannels } = data;
return {
id,
nodeInputs,
nodeOutputs,
nodeDependencies,
settings,
operationMetadata,
staticResult,
supportedChannels,
actionMetadata: getRecordEntry(nodesMetadata, id)?.actionMetadata,
repetitionInfo: getRecordEntry(repetitionInfos, id),
};
}),
clearExisting: false,
})
);

const singleOperation = { [nodeId]: serializedOperation };
const outputTokens = graph ? initializeOutputTokensForOperations(nodeData, singleOperation, graph, nodesMetadata) : {};
const variables = initializeVariables(singleOperation, nodeData);
dispatch(initializeTokensAndVariables({ outputTokens, variables }));

await initializeDynamicDataInNodes(getState as () => RootState, dispatch, [nodeId]);
updateAllUpstreamNodes(getState() as RootState, dispatch);
} finally {
dispatch(setIsPanelLoading(false));
}
}
);
Loading
Loading