Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"19gdw8": "Learn more",
"1A1P5b": "Comment",
"1AFYij": "This template contains one workflow, therefore it is classified as a Workflow.",
"1Ch1Od": "This name is reserved and cannot be used as a workflow name.",
"1D047X": "Required. The value to convert to XML.",
"1Fn5n+": "Required. The URI encoded string.",
"1GWzEL": "No results found for the specified filters",
Expand Down Expand Up @@ -2123,6 +2124,7 @@
"_19gdw8.comment": "Label to learn more about creating a new connection",
"_1A1P5b.comment": "Comment Label",
"_1AFYij.comment": "Content for the toaster for adding a single workflow.",
"_1Ch1Od.comment": "Error message when workflow name matches a reserved project folder name",
"_1D047X.comment": "Required string parameter to be converted using xml function",
"_1Fn5n+.comment": "Required URI encoded string parameter to be converted using uriComponentToBinary function",
"_1GWzEL.comment": "Text displayed when no results are found in the browse grid",
Expand Down Expand Up @@ -3940,6 +3942,7 @@
"_aGxYMY.comment": "Label to clear editor",
"_aGyVJT.comment": "Required number parameter to get number of objects to remove for skip function",
"_aI9W5L.comment": "Accessibility text to inform user this template does not contain connectors",
"_aIk72V.comment": "Warning message when workflow name collides with an existing workflow in the project",
"_aJg1gl.comment": "error message for maximum invalid retry interval",
"_aKf/r+.comment": "Run identifier not found error message",
"_aP1wk9.comment": "Label for the description of a custom 'indexOf' function",
Expand Down Expand Up @@ -5317,6 +5320,7 @@
"aGxYMY": "Clear editor",
"aGyVJT": "Required. The number of objects to remove from the front of Collection. Must be a positive integer.",
"aI9W5L": "This template does not have connectors",
"aIk72V": "A workflow with this name already exists in the selected project.",
"aJg1gl": "Retry policy maximum interval is invalid, must match ISO 8601 duration format",
"aKf/r+": "Specified run identifier not found",
"aP1wk9": "Returns the first index of a value within a string (case-insensitive, invariant culture)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ describe('workspace webview command wrappers', () => {
const logicAppsWithoutCustomCode = ['LogicApp'];
(vscode.workspace as any).workspaceFile = workspaceFile;
(vscode.workspace.fs.readFile as Mock).mockResolvedValue(Buffer.from(JSON.stringify(workspaceFileJson)));
(vscode.workspace.fs.readDirectory as Mock).mockResolvedValue([
['LogicApp', 'directory'],
['CSharpProject', 'directory'],
['MyWorkspace.code-workspace', 'file'],
]);
(getLogicAppWithoutCustomCode as Mock).mockResolvedValue(logicAppsWithoutCustomCode);

await createNewProject(context);
Expand All @@ -133,6 +138,7 @@ describe('workspace webview command wrappers', () => {
expect(config.extraInitializeData).toEqual({
workspaceFileJson,
logicAppsWithoutCustomCode,
existingFolders: ['LogicApp', 'CSharpProject'],
});
expect(config.dialogOptions?.workspace).toMatchObject({
canSelectMany: false,
Expand All @@ -147,6 +153,29 @@ describe('workspace webview command wrappers', () => {
expect(createLogicAppProject).toHaveBeenCalledWith(context, data, path.dirname(workspaceFile.fsPath));
});

it('getExistingFoldersOnDisk filters out non-directory entries using FileType mock', async () => {
const workspaceFile = { fsPath: 'D:\\workspace\\MyWorkspace.code-workspace' };
const workspaceFileJson = { folders: [{ path: './LogicApp' }] };
(vscode.workspace as any).workspaceFile = workspaceFile;
(vscode.workspace.fs.readFile as Mock).mockResolvedValue(Buffer.from(JSON.stringify(workspaceFileJson)));
(vscode.workspace.fs.readDirectory as Mock).mockResolvedValue([
['LogicApp', 'directory'],
['notes.txt', 'file'],
['SomeLink', 'symlink'],
['AnotherProject', 'directory'],
['UnknownEntry', ''],
]);

await createNewProject(context);

const config = getLastWebviewConfig();
expect(config.extraInitializeData).toEqual({
workspaceFileJson,
logicAppsWithoutCustomCode: [],
existingFolders: ['LogicApp', 'AnotherProject'],
});
});

it('createNewProject falls back to convertToWorkspace when no workspace file is open', async () => {
await createNewProject(context);

Expand All @@ -156,13 +185,17 @@ describe('workspace webview command wrappers', () => {

it('createWorkflow passes codeful metadata and wires createLogicAppWorkflow', async () => {
const projectRoot = path.join(workspaceRoot, 'CodefulLogicApp');
(getWorkspaceRoot as Mock).mockResolvedValue(workspaceRoot);
const folder = {
name: 'workspace',
uri: { fsPath: workspaceRoot },
index: 0,
} as vscode.WorkspaceFolder;
(vscode.workspace as any).workspaceFolders = [folder];
(tryGetLogicAppProjectRoot as Mock).mockResolvedValue(projectRoot);
(isCodefulProject as Mock).mockResolvedValue(true);

await createWorkflow(context);

expect(getWorkspaceRoot).toHaveBeenCalledWith(context);
expect(tryGetLogicAppProjectRoot).toHaveBeenCalledWith(context, workspaceRoot, true);
expect(isCodefulProject).toHaveBeenCalledWith(projectRoot);

Expand All @@ -175,10 +208,11 @@ describe('workspace webview command wrappers', () => {
extraInitializeData: {
logicAppType: ProjectType.codeful,
logicAppName: 'CodefulLogicApp',
availableProjects: [{ name: 'CodefulLogicApp', path: projectRoot, isCodeful: true }],
},
});

const data = { workflowName: 'ProcessOrder' };
const data = { workflowName: 'ProcessOrder', logicAppName: 'CodefulLogicApp' };
await config.createHandler(context, data);

expect(createLogicAppWorkflow).toHaveBeenCalledWith(context, data, projectRoot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ vi.mock('../../utils/telemetry', () => ({
runWithDurationTelemetry: vi.fn((_context: unknown, _eventName: string, callback: () => Promise<unknown>) => callback()),
}));

vi.mock('../../utils/delay', () => ({
delay: vi.fn(),
}));

vi.mock('../../utils/verifyIsProject', () => ({
tryGetLogicAppProjectRoot: vi.fn(),
}));
Expand All @@ -55,6 +59,10 @@ vi.mock('../../utils/vsCodeConfig/settings', () => ({
getWorkspaceSetting: vi.fn(),
}));

vi.mock('../../utils/codeful', () => ({
isCodefulProject: vi.fn(),
}));

vi.mock('../buildCustomCodeFunctionsProject', () => ({
tryBuildCustomCodeFunctionsProject: vi.fn(),
}));
Expand All @@ -68,6 +76,8 @@ import { preDebugValidate } from '../../debug/validatePreDebug';
import { getProjFiles } from '../../utils/dotnet/dotnet';
import { getFuncPortFromTaskOrProject, runningFuncTaskMap } from '../../utils/funcCoreTools/funcHostTask';
import { executeIfNotActive } from '../../utils/taskUtils';
import { delay } from '../../utils/delay';
import { isCodefulProject } from '../../utils/codeful';
import { tryGetLogicAppProjectRoot } from '../../utils/verifyIsProject';
import { getWorkspaceSetting } from '../../utils/vsCodeConfig/settings';
import { tryBuildCustomCodeFunctionsProject } from '../buildCustomCodeFunctionsProject';
Expand Down Expand Up @@ -111,8 +121,10 @@ describe('pickFuncProcessInternal', () => {
context.errorHandling = {};
runningFuncTaskMap.clear();
(preDebugValidate as any).mockResolvedValue(true);
(isCodefulProject as any).mockResolvedValue(true);
(tryBuildCustomCodeFunctionsProject as any).mockResolvedValue(true);
(publishCodefulProject as any).mockResolvedValue(undefined);
(delay as any).mockResolvedValue(undefined);
(getProjFiles as any).mockResolvedValue(['CodefulLogicApp.csproj']);
(getWorkspaceSetting as any).mockReturnValue(1);
(getFuncPortFromTaskOrProject as any).mockResolvedValue('7071');
Expand All @@ -134,7 +146,7 @@ describe('pickFuncProcessInternal', () => {
restoreProcessPlatform();
});

it('passes the build-populated codeful skip option from the debug path before starting the func task', async () => {
it('codeful project skips custom code build', async () => {
(vscode.tasks.fetchTasks as any).mockResolvedValue([]);

await expect(
Expand All @@ -146,11 +158,82 @@ describe('pickFuncProcessInternal', () => {
)
).rejects.toThrow('Failed to find "func: host start" task.');

expect(tryBuildCustomCodeFunctionsProject).toHaveBeenCalledWith(context, workspaceFolder.uri);
expect(isCodefulProject).toHaveBeenCalledWith(projectPath);
expect(tryBuildCustomCodeFunctionsProject).not.toHaveBeenCalled();
expect(publishCodefulProject).toHaveBeenCalledWith(context, workspaceFolder.uri, { skipIfBuildPopulatesCodeful: true });
expect(executeIfNotActive).not.toHaveBeenCalled();
});

it('custom code project skips codeful publish', async () => {
(isCodefulProject as any).mockResolvedValue(false);
(vscode.tasks.fetchTasks as any).mockResolvedValue([]);

await expect(
pickFuncProcessModule.pickFuncProcessInternal(
context,
{ type: 'logicapp', isCodeless: false, preLaunchTask: 'func: host start' },
workspaceFolder,
projectPath
)
).rejects.toThrow('Failed to find "func: host start" task.');

expect(isCodefulProject).toHaveBeenCalledWith(projectPath);
expect(tryBuildCustomCodeFunctionsProject).toHaveBeenCalledWith(context, workspaceFolder.uri);
expect(publishCodefulProject).not.toHaveBeenCalled();
expect(executeIfNotActive).not.toHaveBeenCalled();
});

it('waits for a previous func task to stop before codeful publish', async () => {
const events: string[] = [];
runningFuncTaskMap.set(workspaceFolder, { startTime: Date.now(), processId: 5678 });
(delay as any).mockImplementationOnce(async () => {
expect(publishCodefulProject).not.toHaveBeenCalled();
events.push('waited-for-stop');
runningFuncTaskMap.clear();
});
(publishCodefulProject as any).mockImplementation(async () => {
events.push('publish-codeful');
});
(vscode.tasks.fetchTasks as any).mockResolvedValue([]);

await expect(
pickFuncProcessModule.pickFuncProcessInternal(
context,
{ type: 'logicapp', isCodeless: false, preLaunchTask: 'func: host start' },
workspaceFolder,
projectPath
)
).rejects.toThrow('Failed to find "func: host start" task.');

expect(events).toEqual(['waited-for-stop', 'publish-codeful']);
});

it('waits for a previous func task to stop before custom code build', async () => {
const events: string[] = [];
(isCodefulProject as any).mockResolvedValue(false);
runningFuncTaskMap.set(workspaceFolder, { startTime: Date.now(), processId: 5678 });
(delay as any).mockImplementationOnce(async () => {
expect(tryBuildCustomCodeFunctionsProject).not.toHaveBeenCalled();
events.push('waited-for-stop');
runningFuncTaskMap.clear();
});
(tryBuildCustomCodeFunctionsProject as any).mockImplementation(async () => {
events.push('build-custom-code');
});
(vscode.tasks.fetchTasks as any).mockResolvedValue([]);

await expect(
pickFuncProcessModule.pickFuncProcessInternal(
context,
{ type: 'logicapp', isCodeless: false, preLaunchTask: 'func: host start' },
workspaceFolder,
projectPath
)
).rejects.toThrow('Failed to find "func: host start" task.');

expect(events).toEqual(['waited-for-stop', 'build-custom-code']);
});

it('starts the func task after publishing and returns the tracked workflow process id', async () => {
(getProjFiles as any).mockImplementation(async () => {
runningFuncTaskMap.set(workspaceFolder, { startTime: Date.now(), processId: 1234 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from '../utils/customCodeUtils';
import * as vscode from 'vscode';
import { isNullOrUndefined } from '@microsoft/logic-apps-shared';
import { invalidateCodefulSdkCacheIfNeeded } from '../utils/codeful';

/**
* Builds a custom code functions project if exists.
Expand Down Expand Up @@ -92,8 +91,6 @@ export async function buildWorkspaceCustomCodeFunctionsProjects(context: IAction
}

async function buildCustomCodeProject(functionsProjectPath: string): Promise<void> {
await invalidateCodefulSdkCacheIfNeeded(functionsProjectPath);

const tasks: vscode.Task[] = await vscode.tasks.fetchTasks();
const buildTask = tasks.find((task) => {
const currTaskPath = (task.scope as vscode.WorkspaceFolder)?.uri.fsPath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { localize } from '../../../../localize';
import { ext } from '../../../../extensionVariables';
import { getCustomCodeRuntime } from '../../../utils/debug';
import { isDebugConfigEqual } from '../../../utils/vsCodeConfig/launch';
import { binariesExist } from '../../../utils/binaries';
import { binariesExistSync } from '../../../utils/binaries';
import { tryGetLogicAppProjectRoot } from '../../../utils/verifyIsProject';
import {
type CustomCodeFunctionsProjectMetadata,
Expand Down Expand Up @@ -94,7 +94,7 @@ export async function writeExtensionsJson(webviewProjectContext: IWebviewProject
const getCodefulTasks = (targetFramework: string) => {
const commonDotnetArgs: string[] = ['/property:GenerateFullPaths=true', '/consoleloggerparameters:NoSummary'];
const releaseDotnetArgs: string[] = ['--configuration', 'Release'];
const funcBinariesExist = binariesExist(funcDependencyName);
const funcBinariesExist = binariesExistSync(funcDependencyName);
const debugSubpath = path.posix.join('bin', 'Debug', targetFramework);
const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv({ cwd: debugSubpath }) : {};
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ vi.mock('../../../../utils/fs', () => ({
}));
vi.mock('../../../../utils/binaries', () => ({
binariesExist: vi.fn().mockReturnValue(false),
binariesExistSync: vi.fn().mockReturnValue(false),
}));

describe('CreateLogicAppVSCodeContents', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { binariesExist } from '../../../utils/binaries';
import { binariesExistSync } from '../../../utils/binaries';
import { getFuncHostTaskEnv } from '../../../utils/codeless/funcHostTaskEnv';
import { extensionCommand, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../../constants';
import { InitCustomCodeScriptProjectStep } from './initCustomCodeScriptProjectStep';
Expand All @@ -11,7 +11,7 @@ import type { TaskDefinition } from 'vscode';

export class InitCustomCodeProjectStep extends InitCustomCodeScriptProjectStep {
protected getTasks(): TaskDefinition[] {
const funcBinariesExist = binariesExist(funcDependencyName);
const funcBinariesExist = binariesExistSync(funcDependencyName);
const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv() : {};
return [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { binariesExist } from '../../../utils/binaries';
import { binariesExistSync } from '../../../utils/binaries';
import { getFuncHostTaskEnv } from '../../../utils/codeless/funcHostTaskEnv';
import { extInstallTaskName, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../../constants';
import { getLocalFuncCoreToolsVersion } from '../../../utils/funcCoreTools/funcVersion';
Expand Down Expand Up @@ -49,7 +49,7 @@ export class InitCustomCodeScriptProjectStep extends InitCustomCodeProjectStepBa
}

protected getTasks(): TaskDefinition[] {
const funcBinariesExist = binariesExist(funcDependencyName);
const funcBinariesExist = binariesExistSync(funcDependencyName);
const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv() : {};
return [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ import path from 'path';
import { createLogicAppProject } from '../createNewCodeProject/CodeProjectBase/CreateLogicAppProjects';
import { getLogicAppWithoutCustomCode } from '../../utils/workspace';

/**
* Enumerates all directory names in the workspace root folder.
* This captures folders that may not be in the .code-workspace file (e.g., C# custom code projects).
*/
async function getExistingFoldersOnDisk(workspaceRootFolder: string): Promise<string[]> {
try {
const rootUri = vscode.Uri.file(workspaceRootFolder);
const entries = await vscode.workspace.fs.readDirectory(rootUri);
return entries.filter(([, type]) => type === vscode.FileType.Directory).map(([name]) => name);
} catch {
return [];
}
}

export async function createNewProject(context: IActionContext): Promise<void> {
// Determine if in workspace, if not in workspace but there is a logic app project found,
// prompt to see if they want to move the project over to a logic app workspace
Expand All @@ -33,6 +47,10 @@ export async function createNewProject(context: IActionContext): Promise<void> {
const workspaceFileJson = JSON.parse(workspaceFileContent.toString());
const logicAppsWithoutCustomCode = await getLogicAppWithoutCustomCode(context);

// Enumerate all existing directories on disk (includes C# project folders, etc.)
const existingFolders = await getExistingFoldersOnDisk(workspaceRootFolder);
ext.outputChannel.appendLog(`[createProject] workspaceRoot=${workspaceRootFolder}, existingFolders=${JSON.stringify(existingFolders)}`);

await createWorkspaceWebviewCommandHandler({
panelName: localize('createLogicAppProject', 'Create project'),
panelGroupKey: ext.webViewKey.createLogicApp,
Expand All @@ -52,6 +70,7 @@ export async function createNewProject(context: IActionContext): Promise<void> {
extraInitializeData: {
workspaceFileJson,
logicAppsWithoutCustomCode,
existingFolders,
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,19 @@ describe('createLogicAppWorkflow', () => {
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith('Finished creating workflow.');
});

it('shows an error and skips creation when no workspace file is open', async () => {
it('creates a workflow in a folder-opened Logic App project when no workspace file is open', async () => {
(vscode.workspace as any).workspaceFile = undefined;
const options: any = { logicAppName: 'Orders', logicAppType: ProjectType.logicApp };

await createLogicAppWorkflow(context, options, logicAppFolderPath);

expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
'Please open an existing logic app workspace before trying to add a new logic app project.'
);
expect(createLogicAppAndWorkflow).not.toHaveBeenCalled();
expect(options).toMatchObject({
logicAppType: ProjectType.logicApp,
shouldCreateLogicAppProject: false,
});
expect(options.workspaceFilePath).toBeUndefined();
expect(createLogicAppAndWorkflow).toHaveBeenCalledWith(options, logicAppFolderPath, context);
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled();
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith('Finished creating workflow.');
});
});
Loading
Loading