{renderSettingRow(intlText.DOTNET_FRAMEWORK_REVIEW, getDotNetFrameworkDisplay(targetFramework))}
{renderSettingRow(intlText.CUSTOM_CODE_FOLDER, functionFolderName)}
{renderSettingRow(intlText.CUSTOM_CODE_LOCATION, functionLocationPath)}
@@ -148,7 +156,7 @@ export const ReviewCreateStep: React.FC = () => {
{needsFunctionConfiguration && (
-
Function Configuration
+
Function configuration
{renderSettingRow(intlText.RULES_ENGINE_FOLDER, functionFolderName)}
{renderSettingRow(intlText.RULES_ENGINE_LOCATION, functionLocationPath)}
{renderSettingRow(intlText.FUNCTION_WORKSPACE, functionNamespace)}
@@ -158,7 +166,7 @@ export const ReviewCreateStep: React.FC = () => {
{shouldShowWorkflowSection && (
-
Workflow Configuration
+
Workflow configuration
{renderSettingRow(intlText.WORKFLOW_NAME_REVIEW, workflowName)}
{renderSettingRow(intlText.WORKFLOW_TYPE_REVIEW, getWorkflowTypeDisplay(workflowType))}
diff --git a/apps/vs-code-react/src/app/createWorkspace/steps/workflowTypeStep.tsx b/apps/vs-code-react/src/app/createWorkspace/steps/workflowTypeStep.tsx
index 30adcc99793..eb4a1a1fdc8 100644
--- a/apps/vs-code-react/src/app/createWorkspace/steps/workflowTypeStep.tsx
+++ b/apps/vs-code-react/src/app/createWorkspace/steps/workflowTypeStep.tsx
@@ -4,41 +4,65 @@
*--------------------------------------------------------------------------------------------*/
import { Option, Text, Field, Input, Label, Dropdown } from '@fluentui/react-components';
import type { DropdownProps } from '@fluentui/react-components';
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { useCreateWorkspaceStyles } from '../createWorkspaceStyles';
import type { RootState } from '../../../state/store';
-import type { CreateWorkspaceState } from '../../../state/createWorkspaceSlice';
import { setWorkflowType, setWorkflowName } from '../../../state/createWorkspaceSlice';
import { useSelector, useDispatch } from 'react-redux';
-import { validateWorkflowName } from '../validation/helper';
+import { validateWorkflowName } from '../utils/validation';
+import { ProjectType, WorkflowType } from '@microsoft/vscode-extension-logic-apps';
import { useIntlMessages, workspaceMessages } from '../../../intl';
+const logicAppCodeTypes = {
+ CODELESS: 'CODELESS',
+ CODEFUL: 'CODEFUL',
+};
+
export const WorkflowTypeStep: React.FC = () => {
const dispatch = useDispatch();
const styles = useCreateWorkspaceStyles();
- const createWorkspaceState = useSelector((state: RootState) => state.createWorkspace) as CreateWorkspaceState;
- const { workflowType, workflowName } = createWorkspaceState;
+ const createWorkspaceState = useSelector((state: RootState) => state.createWorkspace);
+ const { workflowType, workflowName, logicAppType } = createWorkspaceState;
+ const intlText = useIntlMessages(workspaceMessages);
// Validation state
const [workflowNameError, setWorkflowNameError] = useState
(undefined);
- const intlText = useIntlMessages(workspaceMessages);
-
const handleWorkflowTypeChange: DropdownProps['onOptionSelect'] = (event, data) => {
if (data.optionValue) {
dispatch(setWorkflowType(data.optionValue));
}
};
+ const workflowTypes: Record = useMemo(() => {
+ return {
+ [logicAppCodeTypes.CODELESS]: {
+ [WorkflowType.stateful]: intlText.STATEFUL_TITLE,
+ [WorkflowType.stateless]: intlText.STATELESS_TITLE,
+ [WorkflowType.agentic]: intlText.AUTONOMOUS_TITLE,
+ [WorkflowType.agent]: intlText.AGENT_TITLE,
+ },
+ [logicAppCodeTypes.CODEFUL]: {
+ [WorkflowType.statefulCodeful]: intlText.STATEFUL_TITLE,
+ [WorkflowType.agenticCodeful]: intlText.AUTONOMOUS_TITLE,
+ [WorkflowType.agentCodeful]: intlText.AGENT_TITLE,
+ },
+ };
+ }, [intlText]);
+
const handleWorkflowNameChange = (event: React.ChangeEvent) => {
dispatch(setWorkflowName(event.target.value));
setWorkflowNameError(validateWorkflowName(event.target.value, intlText));
};
+ const selectWorkflowTypes = useMemo(() => {
+ const logicAppCodeType = logicAppType === ProjectType.codeful ? logicAppCodeTypes.CODEFUL : logicAppCodeTypes.CODELESS;
+ return workflowTypes[logicAppCodeType];
+ }, [logicAppType, workflowTypes]);
+
return (
{intlText.WORKFLOW_CONFIGURATION}
-
{
className={styles.inputControl}
/>
-
-
-
-
-
+ {Object.keys(selectWorkflowTypes).map((optionKey, index) => {
+ const optionText = selectWorkflowTypes[optionKey];
+ return (
+
+ );
+ })}
- {workflowType && (
-
- {workflowType === 'Stateful-Codeless' && intlText.STATEFUL_DESCRIPTION}
- {workflowType === 'Stateless-Codeless' && intlText.STATELESS_DESCRIPTION}
- {workflowType === 'Agentic-Codeless' && intlText.AUTONOMOUS_DESCRIPTION}
- {workflowType === 'Agent-Codeless' && intlText.AGENT_DESCRIPTION}
-
- )}
);
diff --git a/apps/vs-code-react/src/app/createWorkspace/steps/workspaceNameStep.tsx b/apps/vs-code-react/src/app/createWorkspace/steps/workspaceNameStep.tsx
index 735faf25239..479986f92f7 100644
--- a/apps/vs-code-react/src/app/createWorkspace/steps/workspaceNameStep.tsx
+++ b/apps/vs-code-react/src/app/createWorkspace/steps/workspaceNameStep.tsx
@@ -13,7 +13,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { VSCodeContext } from '../../../webviewCommunication';
import { useContext, useState, useCallback, useEffect } from 'react';
import { ExtensionCommand } from '@microsoft/vscode-extension-logic-apps';
-import { nameValidation } from '../validation/helper';
+import { nameValidation } from '../utils/validation';
export const WorkspaceNameStep: React.FC = () => {
const dispatch = useDispatch();
diff --git a/apps/vs-code-react/src/app/createWorkspace/utils/__test__/validation.test.ts b/apps/vs-code-react/src/app/createWorkspace/utils/__test__/validation.test.ts
new file mode 100644
index 00000000000..9b017ca4c72
--- /dev/null
+++ b/apps/vs-code-react/src/app/createWorkspace/utils/__test__/validation.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from 'vitest';
+import { validateFunctionName, validateFunctionNamespace } from '../validation';
+
+describe('create workspace validation utilities', () => {
+ const intlText = {
+ FUNCTION_NAMESPACE_EMPTY: 'Function namespace cannot be empty.',
+ FUNCTION_NAMESPACE_VALIDATION: 'Function namespace must be a valid C# namespace.',
+ FUNCTION_NAME_EMPTY: 'Function name cannot be empty.',
+ FUNCTION_NAME_VALIDATION: 'Function name must start with a letter and can only contain letters, digits, and "_".',
+ };
+
+ it('rejects function names that are not valid C# identifiers', () => {
+ expect(validateFunctionName('my-function', intlText)).toBe(intlText.FUNCTION_NAME_VALIDATION);
+ expect(validateFunctionName('1function', intlText)).toBe(intlText.FUNCTION_NAME_VALIDATION);
+ });
+
+ it('allows function names with letters, digits, and underscores', () => {
+ expect(validateFunctionName('MyFunction_1', intlText)).toBeUndefined();
+ });
+
+ it('returns function namespace validation messages from workspace message keys', () => {
+ expect(validateFunctionNamespace('', intlText)).toBe(intlText.FUNCTION_NAMESPACE_EMPTY);
+ expect(validateFunctionNamespace('Invalid-Namespace', intlText)).toBe(intlText.FUNCTION_NAMESPACE_VALIDATION);
+ });
+});
diff --git a/apps/vs-code-react/src/app/createWorkspace/utils/validation.ts b/apps/vs-code-react/src/app/createWorkspace/utils/validation.ts
new file mode 100644
index 00000000000..89160585a8f
--- /dev/null
+++ b/apps/vs-code-react/src/app/createWorkspace/utils/validation.ts
@@ -0,0 +1,67 @@
+import { ProjectType } from '@microsoft/vscode-extension-logic-apps';
+
+export const nameValidation = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*$/i;
+export const namespaceValidation = /^([A-Za-z_][A-Za-z0-9_]*)(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
+export const functionNameValidation = /^[a-z][a-z\d_]*$/i;
+
+export const validateWorkflowName = (name: string, intlText: any) => {
+ if (!name) {
+ return intlText.EMPTY_WORKFLOW_NAME;
+ }
+ if (!functionNameValidation.test(name)) {
+ return intlText.WORKFLOW_NAME_VALIDATION_MESSAGE;
+ }
+ return undefined;
+};
+
+export const validateFunctionNamespace = (namespace: string, intlText: any) => {
+ if (!namespace) {
+ return intlText.FUNCTION_NAMESPACE_EMPTY;
+ }
+ if (!namespaceValidation.test(namespace)) {
+ return intlText.FUNCTION_NAMESPACE_VALIDATION;
+ }
+ return undefined;
+};
+
+export const validateFunctionName = (name: string, intlText: any) => {
+ if (!name) {
+ return intlText.FUNCTION_NAME_EMPTY;
+ }
+ if (!functionNameValidation.test(name)) {
+ return intlText.FUNCTION_NAME_VALIDATION;
+ }
+ return undefined;
+};
+
+// Get validation requirements based on flow type
+export const getValidationRequirements = (flowType: string, logicAppType: string) => {
+ const requirements = {
+ needsPackagePath: flowType === 'createWorkspaceFromPackage',
+ needsWorkspacePath: flowType !== 'createLogicApp',
+ needsWorkspaceName: flowType !== 'createLogicApp',
+ needsLogicAppType: flowType !== 'convertToWorkspace', // convertToWorkspace doesn't need logic app type
+ needsLogicAppName: flowType !== 'convertToWorkspace', // convertToWorkspace doesn't need logic app name
+ needsWorkflowFields: false, // convertToWorkspace only needs workspace path and name
+ needsFunctionFields: false, // convertToWorkspace doesn't need function fields
+ };
+
+ // Override for specific flow types that need more fields
+ if (flowType === 'createWorkspace' || flowType === 'createLogicApp') {
+ requirements.needsLogicAppType = true;
+ requirements.needsLogicAppName = true;
+ requirements.needsWorkflowFields = true;
+ requirements.needsFunctionFields = logicAppType === ProjectType.customCode || logicAppType === ProjectType.rulesEngine;
+ }
+
+ // Override for specific flow types that need more fields
+ if (flowType === 'createWorkflow') {
+ requirements.needsLogicAppType = false;
+ requirements.needsLogicAppName = false;
+ requirements.needsWorkspaceName = false;
+ requirements.needsWorkspacePath = false;
+ requirements.needsWorkflowFields = true;
+ }
+
+ return requirements;
+};
diff --git a/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx b/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx
index 9ff89b5b9c8..16eefd50548 100644
--- a/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx
+++ b/apps/vs-code-react/src/app/designer/DesignerCommandBar/index.tsx
@@ -71,6 +71,7 @@ export interface DesignerCommandBarProps {
runId: string;
kind?: string;
getAgentUrl?: () => Promise;
+ supportsUnitTest?: boolean;
}
export const DesignerCommandBar: React.FC = ({
@@ -80,9 +81,9 @@ export const DesignerCommandBar: React.FC = ({
onRefresh,
isDarkMode,
isUnitTest,
- isLocal,
runId,
getAgentUrl,
+ supportsUnitTest,
}) => {
const vscode = useContext(VSCodeContext);
const dispatch = DesignerStore.dispatch;
@@ -309,7 +310,7 @@ export const DesignerCommandBar: React.FC = ({
onResubmit();
},
},
- ...(isLocal
+ ...(supportsUnitTest
? [
{
key: 'CreateUnitTestFromRun',
diff --git a/apps/vs-code-react/src/app/designer/DesignerCommandBar/indexV2.tsx b/apps/vs-code-react/src/app/designer/DesignerCommandBar/indexV2.tsx
index 9258541d8e4..fffef24212f 100644
--- a/apps/vs-code-react/src/app/designer/DesignerCommandBar/indexV2.tsx
+++ b/apps/vs-code-react/src/app/designer/DesignerCommandBar/indexV2.tsx
@@ -99,12 +99,13 @@ export interface DesignerCommandBarProps {
switchToDesignerView: () => void;
switchToCodeView: () => void;
switchToMonitoringView: () => void;
+ supportsUnitTest?: boolean;
+ showRunHistory?: boolean;
}
export const DesignerCommandBar: React.FC = ({
isDarkMode,
isUnitTest,
- isLocal,
runId,
saveWorkflow: _saveWorkflow,
saveWorkflowFromCode: _saveWorkflowFromCode,
@@ -115,6 +116,8 @@ export const DesignerCommandBar: React.FC = ({
switchToDesignerView,
switchToCodeView,
switchToMonitoringView,
+ supportsUnitTest,
+ showRunHistory = true,
}) => {
const vscode = useContext(VSCodeContext);
const dispatch = DesignerStore.dispatch;
@@ -274,18 +277,20 @@ export const DesignerCommandBar: React.FC = ({
>
Code
-
+ {showRunHistory ? (
+
+ ) : null}
);
@@ -371,7 +376,7 @@ export const DesignerCommandBar: React.FC = ({
- {isLocal && (
+ {supportsUnitTest && (
}>
{intlText.CREATE_UNIT_TEST_FROM_RUN}
diff --git a/apps/vs-code-react/src/app/designer/app.tsx b/apps/vs-code-react/src/app/designer/app.tsx
index 2a4bc868444..10a9ae08bb0 100644
--- a/apps/vs-code-react/src/app/designer/app.tsx
+++ b/apps/vs-code-react/src/app/designer/app.tsx
@@ -56,6 +56,7 @@ const DesignerAppV1 = () => {
isUnitTest,
unitTestDefinition,
workflowRuntimeBaseUrl,
+ supportsUnitTest,
} = vscodeState;
const [standardApp, setStandardApp] = useState(panelMetaData?.standardApp);
const [customCode, setCustomCode] = useState | undefined>(panelMetaData?.customCodeData);
@@ -251,6 +252,7 @@ const DesignerAppV1 = () => {
isLocal={isLocal}
runId={runId}
getAgentUrl={getAgentUrl}
+ supportsUnitTest={supportsUnitTest}
/>
);
diff --git a/apps/vs-code-react/src/app/designer/appV2.tsx b/apps/vs-code-react/src/app/designer/appV2.tsx
index 586056d2b55..8e670e9e8b4 100644
--- a/apps/vs-code-react/src/app/designer/appV2.tsx
+++ b/apps/vs-code-react/src/app/designer/appV2.tsx
@@ -32,6 +32,7 @@ export const DesignerApp = () => {
const vscode = useContext(VSCodeContext);
const dispatch: AppDispatch = useDispatch();
const vscodeState = useSelector((state: RootState) => state.designer);
+ const { supportsUnitTest } = vscodeState;
const styles = useAppStyles();
const {
panelMetaData,
@@ -60,6 +61,7 @@ export const DesignerApp = () => {
const [initialWorkflow, setInitialWorkflow] = useState(panelMetaData?.standardApp);
const [workflow, setWorkflow] = useState(panelMetaData?.standardApp);
const [customCode, setCustomCode] = useState | undefined>(panelMetaData?.customCodeData);
+ const isCodefulWorkflow = panelMetaData?.localSettings?.WORKFLOW_CODEFUL_ENABLED === 'true';
const [designerID, setDesignerID] = useState(guid());
const [workflowDefinitionId, setWorkflowDefinitionId] = useState(guid());
@@ -355,6 +357,8 @@ export const DesignerApp = () => {
switchToDesignerView={switchToDesignerView}
switchToCodeView={switchToCodeView}
switchToMonitoringView={switchToMonitoringView}
+ supportsUnitTest={supportsUnitTest}
+ showRunHistory={!isCodefulWorkflow}
/>
{!isCodeView && (
diff --git a/apps/vs-code-react/src/app/designer/servicesHelper.ts b/apps/vs-code-react/src/app/designer/servicesHelper.ts
index 4e08c05e49a..fa4e7a15072 100644
--- a/apps/vs-code-react/src/app/designer/servicesHelper.ts
+++ b/apps/vs-code-react/src/app/designer/servicesHelper.ts
@@ -77,7 +77,7 @@ export const getDesignerServices = (
hostVersion: string,
queryClient: QueryClient,
sendMsgToVsix: (msg: MessageToVsix) => void,
- setRunId: (runId: string) => void
+ setRunId?: (runId: string) => void
): IDesignerServices => {
let authToken = '';
let panelId = '';
@@ -368,7 +368,7 @@ export const getDesignerServices = (
title,
});
},
- openRun: (runId: string) => setRunId(runId),
+ openRun: (runId: string) => setRunId?.(runId),
};
const runService = new StandardRunService({
diff --git a/apps/vs-code-react/src/app/export/navigation/__test__/navigation.test.tsx b/apps/vs-code-react/src/app/export/navigation/__test__/navigation.test.tsx
new file mode 100644
index 00000000000..9865e2e3d22
--- /dev/null
+++ b/apps/vs-code-react/src/app/export/navigation/__test__/navigation.test.tsx
@@ -0,0 +1,170 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { configureStore } from '@reduxjs/toolkit';
+import { ExtensionCommand, RouteName } from '@microsoft/vscode-extension-logic-apps';
+import { Provider } from 'react-redux';
+import React from 'react';
+import { ValidationStatus } from '../../../../run-service';
+import { Status } from '../../../../state/WorkflowSlice';
+import { VSCodeContext } from '../../../../webviewCommunication';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { Navigation } from '../navigation';
+
+const mocks = vi.hoisted(() => ({
+ navigate: vi.fn(),
+ pathname: '/instance-selection',
+}));
+
+vi.mock('react-router-dom', () => ({
+ useLocation: () => ({ pathname: mocks.pathname }),
+ useNavigate: () => mocks.navigate,
+}));
+
+vi.mock('../../../../run-service', () => ({
+ ValidationStatus: {
+ failed: 'Failed',
+ succeeded: 'Succeeded',
+ succeeded_with_warnings: 'SucceededWithWarning',
+ },
+}));
+
+vi.mock('../../../../state/WorkflowSlice', () => ({
+ Status: {
+ Failed: 'Failed',
+ InProgress: 'InProgress',
+ Succeeded: 'Succeeded',
+ },
+}));
+
+vi.mock('../../../../webviewCommunication', async () => {
+ const React = await import('react');
+ return {
+ VSCodeContext: React.createContext({ postMessage: vi.fn() }),
+ };
+});
+
+vi.mock('../../exportStyles', () => ({
+ useExportStyles: () => ({
+ navigationPanel: 'navigation-panel',
+ navigationPanelButton: 'navigation-panel-button',
+ }),
+}));
+
+vi.mock('../../../../intl', () => ({
+ exportMessages: {},
+ useIntlMessages: () => ({
+ BACK: 'Back',
+ EXPORT: 'Export',
+ EXPORT_WITH_WARNINGS: 'Export with warnings',
+ FINISH: 'Finish',
+ NEXT: 'Next',
+ }),
+}));
+
+vi.mock('@fluentui/react-components', () => ({
+ Button: ({ children, disabled, onClick, 'aria-label': ariaLabel }: any) => (
+
+ ),
+}));
+
+function createWorkflowState(overrides: Record = {}) {
+ return {
+ finalStatus: Status.InProgress,
+ exportData: {
+ location: 'westus',
+ managedConnections: {
+ isManaged: false,
+ resourceGroup: undefined,
+ resourceGroupLocation: undefined,
+ },
+ packageUrl: 'https://package',
+ selectedIse: '',
+ selectedSubscription: 'subscription-id',
+ selectedWorkflows: ['workflow-a'],
+ targetDirectory: { path: '/tmp/export' },
+ validationState: ValidationStatus.succeeded,
+ },
+ ...overrides,
+ };
+}
+
+function renderNavigation(workflowOverrides: Record