Skip to content
7 changes: 6 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "all",
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"@typescript-eslint/explicit-module-boundary-types": "off",
Expand Down
5 changes: 5 additions & 0 deletions apps/Standalone/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ module.exports = {
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ export const DesignerCommandBar = ({
);
},
onClick: () => {
isDesignerView ? saveWorkflowMutate() : saveWorkflowFromCode(() => dispatch(resetDesignerDirtyState(undefined)));
if (isDesignerView) {
saveWorkflowMutate();
} else {
saveWorkflowFromCode(() => dispatch(resetDesignerDirtyState(undefined)));
}
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const Artifact = {
ConnectionsFile: 'connections.json',
ParametersFile: 'parameters.json',
WorkflowFile: 'workflow.json',
HostFile: 'host.json',
} as const;

export interface ArtifactProperties {
Expand Down Expand Up @@ -105,7 +106,7 @@ export interface ConnectionsData {
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ParametersData extends Record<string, Parameter> {}
export type ParametersData = Record<string, Parameter>;

export interface Parameter {
name?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useConnectionsData = (appId?: string) => {
}
const { error } = health;
throw new Error(error.message);
} catch (error) {
} catch (_error) {
return {};
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const fetchAppsByQuery = async (query: string): Promise<any[]> => {
return await requestPage(value, pageNum + 1, $skipToken);
}
return value;
} catch (error) {
} catch (_e) {
return value;
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/ban-types */
import { environment } from '../../../environments/environment';
import type { AppDispatch, RootState } from '../../state/store';
import { setIsChatBotEnabled } from '../../state/workflowLoadingSlice';
Expand Down Expand Up @@ -567,7 +566,7 @@ const getDataForConsumption = (data: any) => {
return { workflow, connectionReferences, parameters };
};

const removeProperties = (obj: any = {}, props: string[] = []): Object => {
const removeProperties = (obj: any = {}, props: string[] = []): object => {
return Object.fromEntries(Object.entries(obj).filter(([key]) => !props.includes(key)));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
supportedSchemaFileExts,
supportedCustomXsltFileExts,
} from './extensionConfig';
import type { SchemaType, MapMetadata, IFileSysTreeItem } from '@microsoft/logic-apps-shared';
import type { SchemaType, IFileSysTreeItem, MapMetadata } from '@microsoft/logic-apps-shared';
import type { IActionContext } from '@microsoft/vscode-azext-utils';
import { callWithTelemetryAndErrorHandlingSync } from '@microsoft/vscode-azext-utils';
import type { MapDefinitionData, MessageToVsix, MessageToWebview } from '@microsoft/vscode-extension-logic-apps';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import {
addConnectionData,
getConnectionsAndSettingsToUpdate,
getConnectionsFromFile,
getCustomCodeFromFiles,
getCustomCodeToUpdate,
getLogicAppProjectRoot,
getParametersFromFile,
saveConnectionReferences,
saveCustomCodeStandard,
} from '../../../utils/codeless/connection';
import { saveWorkflowParameter } from '../../../utils/codeless/parameter';
import { startDesignTimeApi } from '../../../utils/codeless/startDesignTimeApi';
Expand Down Expand Up @@ -245,14 +248,14 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {

await window.withProgress(options, async () => {
try {
const { definition, connectionReferences, parameters } = workflowToSave;
const { definition, connectionReferences, parameters, customCodeData } = workflowToSave;
const definitionToSave: any = definition;
const parametersFromDefinition = parameters;
const projectPath = await getLogicAppProjectRoot(this.context, filePath);

workflow.definition = definitionToSave;

if (connectionReferences) {
const projectPath = await getLogicAppProjectRoot(this.context, filePath);
const connectionsAndSettingsToUpdate = await getConnectionsAndSettingsToUpdate(
this.context,
projectPath,
Expand All @@ -265,6 +268,11 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {
await saveConnectionReferences(this.context, projectPath, connectionsAndSettingsToUpdate);
}

if (customCodeData) {
const customCodeToUpdate = await getCustomCodeToUpdate(this.context, filePath, customCodeData);
await saveCustomCodeStandard(filePath, customCodeToUpdate);
}

if (parametersFromDefinition) {
delete parametersFromDefinition.$connections;
for (const parameterKey of Object.keys(parametersFromDefinition)) {
Expand Down Expand Up @@ -411,6 +419,9 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {
const connectionsData: string = await getConnectionsFromFile(this.context, this.workflowFilePath);
const projectPath: string | undefined = await getLogicAppProjectRoot(this.context, this.workflowFilePath);
const parametersData: Record<string, Parameter> = await getParametersFromFile(this.context, this.workflowFilePath);
const customCodeData: Record<string, string> = await getCustomCodeFromFiles(this.workflowFilePath);
const workflowDetails = await getManualWorkflowsInLocalProject(projectPath, this.workflowName);
const artifacts = await getArtifactsInLocalProject(projectPath);
let localSettings: Record<string, string>;
let azureDetails: AzureConnectorDetails;

Expand All @@ -426,14 +437,15 @@ export default class OpenDesignerForLocalProject extends OpenDesignerBase {
appSettingNames: Object.keys(localSettings),
standardApp: getStandardAppData(this.workflowName, workflowContent),
connectionsData,
customCodeData,
parametersData,
localSettings,
azureDetails,
accessToken: azureDetails.accessToken,
workflowContent,
workflowDetails: await getManualWorkflowsInLocalProject(projectPath, this.workflowName),
workflowDetails,
workflowName: this.workflowName,
artifacts: await getArtifactsInLocalProject(projectPath),
artifacts,
schemaArtifacts: this.schemaArtifacts,
mapArtifacts: this.mapArtifacts,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
getAzureConnectorDetailsForLocalProject,
getStandardAppData,
} from '../../../utils/codeless/common';
import { getConnectionsFromFile, getLogicAppProjectRoot, getParametersFromFile } from '../../../utils/codeless/connection';
import {
getConnectionsFromFile,
getCustomCodeFromFiles,
getLogicAppProjectRoot,
getParametersFromFile,
} from '../../../utils/codeless/connection';
import { sendRequest } from '../../../utils/requestUtils';
import { OpenMonitoringViewBase } from './openMonitoringViewBase';
import { getTriggerName, HTTP_METHODS } from '@microsoft/logic-apps-shared';
Expand Down Expand Up @@ -161,6 +166,7 @@ export default class OpenMonitoringViewForLocal extends OpenMonitoringViewBase {
const projectPath: string | undefined = await getLogicAppProjectRoot(this.context, this.workflowFilePath);
const workflowContent: any = JSON.parse(readFileSync(this.workflowFilePath, 'utf8'));
const parametersData: Record<string, Parameter> = await getParametersFromFile(this.context, this.workflowFilePath);
const customCodeData: Record<string, string> = await getCustomCodeFromFiles(this.workflowFilePath);
let localSettings: Record<string, string>;
let azureDetails: AzureConnectorDetails;

Expand All @@ -177,6 +183,7 @@ export default class OpenMonitoringViewForLocal extends OpenMonitoringViewBase {
connectionsData,
localSettings,
parametersData,
customCodeData,
azureDetails,
accessToken: azureDetails.accessToken,
workflowName: this.workflowName,
Expand Down
65 changes: 64 additions & 1 deletion apps/vs-code-designer/src/app/utils/codeless/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getContainingWorkspace } from '../workspace';
import { getWorkflowParameters } from './common';
import { getAuthorizationToken } from './getAuthorizationToken';
import { getParametersJson, saveWorkflowParameterRecords } from './parameter';
import { deleteCustomCode, getCustomCode, getCustomCodeAppFilesToUpdate, uploadCustomCode } from './customcode';
import { addNewFileInCSharpProject } from './updateBuildFile';
import { HTTP_METHODS, isString } from '@microsoft/logic-apps-shared';
import type { ParsedSite } from '@microsoft/vscode-azext-azureappservice';
Expand All @@ -23,6 +24,8 @@ import type {
ConnectionAcl,
ConnectionAndAppSetting,
Parameter,
CustomCodeFileNameMapping,
AllCustomCodeFiles,
} from '@microsoft/vscode-extension-logic-apps';
import { JwtTokenHelper, JwtTokenConstants, resolveConnectionsReferences } from '@microsoft/vscode-extension-logic-apps';
import axios from 'axios';
Expand All @@ -42,6 +45,20 @@ export async function getParametersFromFile(context: IActionContext, workflowFil
return getParametersJson(projectRoot);
}

async function getCustomCodeAppFiles(
context: IActionContext,
workflowFilePath: string,
customCode: CustomCodeFileNameMapping
): Promise<Record<string, string>> {
const projectRoot: string = await getLogicAppProjectRoot(context, workflowFilePath);
return getCustomCodeAppFilesToUpdate(projectRoot, customCode);
}

export async function getCustomCodeFromFiles(workflowFilePath: string): Promise<Record<string, string>> {
const workspaceFolder = path.dirname(workflowFilePath);
return getCustomCode(workspaceFolder);
}

export async function getConnectionsJson(projectRoot: string): Promise<string> {
const connectionFilePath: string = path.join(projectRoot, connectionsFileName);
if (await fse.pathExists(connectionFilePath)) {
Expand Down Expand Up @@ -270,6 +287,52 @@ export async function getConnectionsAndSettingsToUpdate(
};
}

export async function getCustomCodeToUpdate(
context: IActionContext,
filePath: string,
customCode: CustomCodeFileNameMapping
): Promise<AllCustomCodeFiles | undefined> {
const filteredCustomCodeMapping: CustomCodeFileNameMapping = {};
const originalCustomCodeData = Object.keys(await getCustomCodeFromFiles(filePath));
if (!customCode || Object.keys(customCode).length === 0) {
return;
}

const appFiles = await getCustomCodeAppFiles(context, filePath, customCode);
Object.entries(customCode).forEach(([fileName, customCodeData]) => {
const { isModified, isDeleted } = customCodeData;
if ((isDeleted && originalCustomCodeData.includes(fileName)) || (isModified && !isDeleted)) {
filteredCustomCodeMapping[fileName] = { ...customCodeData };
}
});
return { customCodeFiles: filteredCustomCodeMapping, appFiles };
}

export async function saveCustomCodeStandard(filePath: string, allCustomCodeFiles?: AllCustomCodeFiles): Promise<void> {
const { customCodeFiles: customCode, appFiles } = allCustomCodeFiles ?? {};
if (!customCode || Object.keys(customCode).length === 0) {
return;
}
try {
const projectPath = await getLogicAppProjectRoot(this.context, filePath);
const workspaceFolder = path.dirname(filePath);
// to prevent 404's we first check which custom code files are already present before deleting
Object.entries(customCode).forEach(([fileName, customCodeData]) => {
const { isModified, isDeleted, fileData } = customCodeData;
if (isDeleted) {
deleteCustomCode(workspaceFolder, fileName);
} else if (isModified && fileData) {
uploadCustomCode(workspaceFolder, fileName, fileData);
}
});
// upload the app files needed for powershell actions
Object.entries(appFiles ?? {}).forEach(([fileName, fileData]) => uploadCustomCode(projectPath, fileName, fileData));
} catch (error) {
const errorMessage = `Failed to save custom code: ${error}`;
throw new Error(errorMessage);
}
}

export async function saveConnectionReferences(
context: IActionContext,
projectPath: string,
Expand Down Expand Up @@ -352,7 +415,7 @@ export async function createAclInConnectionIfNeeded(
try {
const response = await sendAzureRequest(url, identityWizardContext, HTTP_METHODS.GET, site.subscription);
connectionAcls = response.parsedBody.value;
} catch (error) {
} catch (_error) {
connectionAcls = [];
}

Expand Down
93 changes: 93 additions & 0 deletions apps/vs-code-designer/src/app/utils/codeless/customcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as path from 'path';
import * as fse from 'fs-extra';
import { localize } from '../../../localize';
import { parseError } from '@microsoft/vscode-azext-utils';
import { hostFileName, powershellRequirementsFileName, workflowFileName } from '../../../constants';
import type { CustomCodeFileNameMapping } from '@microsoft/vscode-extension-logic-apps';
import { parseJson } from '../parseJson';
import { getAppFileForFileExtension } from '@microsoft/logic-apps-shared';
/**
* Retrieves the custom code files.
* @param {string} workflowFilePath The path to the workflow folder.
* @returns A promise that resolves to a Record<string, string> object representing the custom code files.
* @throws An error if the custom code files cannot be parsed.
*/
export async function getCustomCode(workflowFilePath: string): Promise<Record<string, string>> {
const customCodeFiles: Record<string, string> = {};
try {
const subPaths: string[] = await fse.readdir(workflowFilePath);
for (const subPath of subPaths) {
const fullPath: string = path.join(workflowFilePath, subPath);

if ((await fse.pathExists(fullPath)) && subPath !== workflowFileName) {
if ((await fse.stat(fullPath)).isFile()) {
customCodeFiles[subPath] = await fse.readFile(fullPath, 'utf8');
}
}
}
} catch (error) {
const message: string = localize('failedToParse', 'Failed to parse "{0}": {1}.', workflowFilePath, parseError(error).message);
throw new Error(message);
}

return customCodeFiles;
}

export async function getCustomCodeAppFilesToUpdate(
workflowFilePath: string,
customCodeFiles?: CustomCodeFileNameMapping
): Promise<Record<string, string>> {
// // only powershell files have custom app files
// // to reduce the number of requests, we only check if there are any modified powershell files
if (!customCodeFiles || !Object.values(customCodeFiles).some((file) => file.isModified && file.fileExtension === '.ps1')) {
return {};
}
const appFiles: Record<string, string> = {};
const hostFilePath: string = path.join(workflowFilePath, hostFileName);
if (await fse.pathExists(hostFilePath)) {
const data: string = (await fse.readFile(hostFilePath)).toString();
if (/[^\s]/.test(data)) {
try {
const hostFile: any = parseJson(data);
if (!hostFile.managedDependency?.enabled) {
hostFile.managedDependency = {
enabled: true,
};
appFiles['host.json'] = JSON.stringify(hostFile, null, 2);
}
} catch (error) {
const message: string = localize('failedToParse', 'Failed to parse "{0}": {1}.', hostFileName, parseError(error).message);
throw new Error(message);
}
}
}
const requirementsFilePath: string = path.join(workflowFilePath, powershellRequirementsFileName);
if (!(await fse.pathExists(requirementsFilePath))) {
appFiles['requirements.psd1'] = getAppFileForFileExtension('.ps1');
}
return appFiles;
}

export async function uploadCustomCode(workflowFilePath: string, fileName: string, fileData: string): Promise<void> {
const filePath: string = path.join(workflowFilePath, fileName);
try {
await fse.writeFile(filePath, fileData, 'utf8');
} catch (error) {
const message: string = localize('Failed to write file at "{0}": {1}', filePath, parseError(error).message);
throw new Error(message);
}
}

export async function deleteCustomCode(workflowFilePath: string, fileName: string): Promise<void> {
const filePath: string = path.join(workflowFilePath, fileName);
try {
if (await fse.pathExists(filePath)) {
await fse.unlink(filePath);
} else {
console.warn(`File at "${filePath}" does not exist.`);
}
} catch (error) {
const message = localize('Failed to delete file at "{0}": {1}', filePath, parseError(error).message);
throw new Error(message);
}
}
Loading
Loading