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
25 changes: 16 additions & 9 deletions apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,26 @@ import { parse } from 'yaml';
import { localize } from '../../../localize';
import { assetsFolderName, dataMapNameValidation } from '../../../constants';

export function getDataMapPanelKey(projectPath: string, dataMapName: string): string {
return `${projectPath}::${dataMapName}`;
}

export default class DataMapperExt {
public static async openDataMapperPanel(
context: IActionContext,
projectPath: string,
dataMapName?: string,
mapDefinitionData?: MapDefinitionData
): Promise<void> {
await startBackendRuntime(context, ext.defaultLogicAppPath);
await startBackendRuntime(context, projectPath);
const name =
dataMapName ??
(await context.ui.showInputBox({
placeHolder: localize('dataMapName', 'Data Map name'),
prompt: localize('dataMapNamePrompt', 'Enter a name for your Data Map'),
validateInput: async (input: string): Promise<string | undefined> => await DataMapperExt.validateDataMapName(input),
}));
DataMapperExt.createOrShow(name, mapDefinitionData);
DataMapperExt.createOrShow(name, projectPath, mapDefinitionData);
}

/*
Expand Down Expand Up @@ -70,13 +75,15 @@ export default class DataMapperExt {
return undefined;
}

private static createOrShow(dataMapName: string, mapDefinitionData?: MapDefinitionData) {
private static createOrShow(dataMapName: string, projectPath: string, mapDefinitionData?: MapDefinitionData) {
Comment thread
andrew-eldridge marked this conversation as resolved.
const panelKey = getDataMapPanelKey(projectPath, dataMapName);

// If a panel has already been created, re-show it
if (ext.dataMapPanelManagers[dataMapName]) {
if (ext.dataMapPanelManagers[panelKey]) {
// NOTE: Shouldn't need to re-send runtime port if webview has already been loaded/set up

window.showInformationMessage(`A Data Mapper panel is already open for this data map (${dataMapName}).`);
ext.dataMapPanelManagers[dataMapName].panel.reveal(ViewColumn.Active);
ext.dataMapPanelManagers[panelKey].panel.reveal(ViewColumn.Active);
return;
}

Expand All @@ -92,13 +99,13 @@ export default class DataMapperExt {
}
);

ext.dataMapPanelManagers[dataMapName] = new DataMapperPanel(panel, dataMapName);
ext.dataMapPanelManagers[dataMapName].panel.iconPath = {
ext.dataMapPanelManagers[panelKey] = new DataMapperPanel(panel, dataMapName, panelKey, projectPath);
ext.dataMapPanelManagers[panelKey].panel.iconPath = {
light: Uri.file(path.join(ext.context.extensionPath, assetsFolderName, 'light', 'wand.png')),
dark: Uri.file(path.join(ext.context.extensionPath, assetsFolderName, 'dark', 'wand.png')),
};
ext.dataMapPanelManagers[dataMapName].updateWebviewPanelTitle();
ext.dataMapPanelManagers[dataMapName].mapDefinitionData = mapDefinitionData;
ext.dataMapPanelManagers[panelKey].updateWebviewPanelTitle();
ext.dataMapPanelManagers[panelKey].mapDefinitionData = mapDefinitionData;

// From here, VSIX will handle any other initial-load-time events once receive webviewLoaded msg
}
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 { dataMapperVersionSetting, defaultDataMapperVersion, extensionCommand, vscodeFolderName } from '../../../constants';
import { dataMapperVersionSetting, defaultDataMapperVersion, vscodeFolderName } from '../../../constants';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { getWebViewHTML } from '../../utils/codeless/getWebViewHTML';
Expand Down Expand Up @@ -45,15 +45,19 @@ export default class DataMapperPanel {
public panel: WebviewPanel;
public dataMapVersion: number;
public dataMapName: string;
public panelKey: string;
public projectPath: string;
public dataMapStateIsDirty: boolean;
public mapDefinitionData: MapDefinitionData | undefined;

private telemetryPrefix = 'data-mapper-vscode-extension';

constructor(panel: WebviewPanel, dataMapName: string) {
constructor(panel: WebviewPanel, dataMapName: string, panelKey: string, projectPath: string) {
this.panel = panel;
this.dataMapVersion = this.getDataMapperVersion();
this.dataMapName = dataMapName;
this.panelKey = panelKey;
this.projectPath = projectPath;
this.dataMapStateIsDirty = false;
this.handleReadSchemaFileOptions = this.handleReadSchemaFileOptions.bind(this); // Bind these as they're used as callbacks
this._handleWebviewMsg = this._handleWebviewMsg.bind(this);
Expand All @@ -80,7 +84,7 @@ export default class DataMapperPanel {

this.panel.onDidDispose(
() => {
delete ext.dataMapPanelManagers[this.dataMapName];
delete ext.dataMapPanelManagers[this.panelKey];
if (schemaFolderWatcher) {
schemaFolderWatcher.dispose();
}
Expand All @@ -97,7 +101,7 @@ export default class DataMapperPanel {

private watchFolderForChanges(folderPath: string, fileExtensions: string[], fn: () => void) {
// Watch folder for changes to update available file list within Data Mapper
const absoluteFolderPath = path.join(ext.defaultLogicAppPath, folderPath);
const absoluteFolderPath = path.join(this.projectPath, folderPath);
if (fileExistsSync(absoluteFolderPath)) {
const folderWatcher = workspace.createFileSystemWatcher(new RelativePattern(absoluteFolderPath, `**/*.{${fileExtensions.join()}}`));
folderWatcher.onDidCreate(fn);
Expand All @@ -108,10 +112,10 @@ export default class DataMapperPanel {
}

private setCustomFolders() {
const customXsltFullPath = path.join(ext.defaultLogicAppPath, customXsltPath);
const customXsltFullPath = path.join(this.projectPath, customXsltPath);
mkdirSync(customXsltFullPath, { recursive: true });

const customFunctionsFullPath = path.join(ext.defaultLogicAppPath, customFunctionsPath);
const customFunctionsFullPath = path.join(this.projectPath, customFunctionsPath);
mkdirSync(customFunctionsFullPath, { recursive: true });
}

Expand All @@ -134,7 +138,7 @@ export default class DataMapperPanel {
// Send runtime port to webview
this.panel.webview.postMessage({
command: ExtensionCommand.setRuntimePort,
data: `${ext.designTimeInstances.get(ext.defaultLogicAppPath)?.port}`,
data: `${ext.designTimeInstances.get(this.projectPath)?.port}`,
});

// If loading a data map, handle that + xslt filename
Expand Down Expand Up @@ -249,7 +253,7 @@ export default class DataMapperPanel {
}

public getNestedFilePaths(fileName: string, parentPath: string, relativePath: string, filesToDisplay: string[], filetypes: string[]) {
const rootPath = path.join(ext.defaultLogicAppPath, relativePath);
const rootPath = path.join(this.projectPath, relativePath);
const absolutePath = path.join(rootPath, parentPath, fileName);
if (statSync(absolutePath).isDirectory()) {
readdirSync(absolutePath).forEach((childFileName) => {
Expand All @@ -272,7 +276,7 @@ export default class DataMapperPanel {
filesToDisplay: IFileSysTreeItem[],
filetypes: string[]
) {
const rootPath = path.join(ext.defaultLogicAppPath, relativePath);
const rootPath = path.join(this.projectPath, relativePath);
const absolutePath = path.join(rootPath, parentPath, fileName);
if (statSync(absolutePath).isDirectory()) {
const childrenFilesToDisplay: IFileSysTreeItem[] = [];
Expand Down Expand Up @@ -311,7 +315,7 @@ export default class DataMapperPanel {
if (this.dataMapVersion === 2) {
return this.getFilesTreeForPath(customXsltPath, supportedCustomXsltFileExts, ExtensionCommand.getAvailableCustomXsltPathsV2);
}
const absoluteFolderPath = path.join(ext.defaultLogicAppPath, customXsltPath);
const absoluteFolderPath = path.join(this.projectPath, customXsltPath);
if (fileExistsSync(absoluteFolderPath)) {
return this.getFilesForPath(customXsltPath, ExtensionCommand.getAvailableCustomXsltPaths, supportedCustomXsltFileExts);
}
Expand Down Expand Up @@ -344,7 +348,7 @@ export default class DataMapperPanel {
command: typeof ExtensionCommand.showAvailableSchemas | typeof ExtensionCommand.getAvailableCustomXsltPaths,
fileTypes: string[]
) {
fs.readdir(path.join(ext.defaultLogicAppPath, folderPath)).then((result) => {
fs.readdir(path.join(this.projectPath, folderPath)).then((result) => {
const filesToDisplay: string[] = [];
result.forEach((file) => {
this.getNestedFilePaths(file, '', folderPath, filesToDisplay, fileTypes);
Expand All @@ -361,7 +365,7 @@ export default class DataMapperPanel {
fileTypes: string[],
command: typeof ExtensionCommand.showAvailableSchemasV2 | typeof ExtensionCommand.getAvailableCustomXsltPathsV2
) {
fs.readdir(path.join(ext.defaultLogicAppPath, folderPath)).then((result) => {
fs.readdir(path.join(this.projectPath, folderPath)).then((result) => {
const filesToDisplay: IFileSysTreeItem[] = [];
result.forEach((file) => {
this.getNestedFileTreePaths(file, '', folderPath, filesToDisplay, fileTypes);
Expand Down Expand Up @@ -399,7 +403,7 @@ export default class DataMapperPanel {
}
const selectedFile = files[0];

const pathToWorkspaceSchemaFolder = path.join(ext.defaultLogicAppPath, schemasPath);
const pathToWorkspaceSchemaFolder = path.join(this.projectPath, schemasPath);
const primarySchemaFullPath = selectedFile.fsPath;
const pathToContainingFolder = path.dirname(primarySchemaFullPath);
const primarySchemaFileName = path.basename(primarySchemaFullPath);
Expand Down Expand Up @@ -433,7 +437,7 @@ export default class DataMapperPanel {
this.setDataMapperVersionForLogging(context);

const fileName = `${this.dataMapName}${mapDefinitionExtension}`;
const dataMapFolderPath = path.join(ext.defaultLogicAppPath, dataMapDefinitionsPath);
const dataMapFolderPath = path.join(this.projectPath, dataMapDefinitionsPath);
const filePath = path.join(dataMapFolderPath, fileName);

// Mkdir as extra insurance that directory exists so file can be written
Expand Down Expand Up @@ -465,7 +469,7 @@ export default class DataMapperPanel {
this.setDataMapperVersionForLogging(context);

const fileName = `${this.dataMapName}${mapXsltExtension}`;
const dataMapFolderPath = path.join(ext.defaultLogicAppPath, dataMapsPath);
const dataMapFolderPath = path.join(this.projectPath, dataMapsPath);
const filePath = path.join(dataMapFolderPath, fileName);

// Mkdir as extra insurance that directory exists so file can be written
Expand All @@ -489,7 +493,7 @@ export default class DataMapperPanel {

public saveDraftDataMapDefinition(mapDefFileContents: string) {
const mapDefileName = `${this.dataMapName}${draftMapDefinitionSuffix}${mapDefinitionExtension}`;
const dataMapDefFolderPath = path.join(ext.defaultLogicAppPath, dataMapDefinitionsPath);
const dataMapDefFolderPath = path.join(this.projectPath, dataMapDefinitionsPath);
const filePath = path.join(dataMapDefFolderPath, mapDefileName);

// Mkdir as extra insurance that directory exists so file can be written
Expand Down Expand Up @@ -542,7 +546,7 @@ export default class DataMapperPanel {

public deleteDraftDataMapDefinition() {
const draftMapDefinitionPath = path.join(
ext.defaultLogicAppPath,
this.projectPath,
dataMapDefinitionsPath,
`${this.dataMapName}${draftMapDefinitionSuffix}${mapDefinitionExtension}`
);
Expand All @@ -552,7 +556,7 @@ export default class DataMapperPanel {
}

public checkAndSetXslt() {
const expectedXsltPath = path.join(ext.defaultLogicAppPath, dataMapsPath, `${this.dataMapName}${mapXsltExtension}`);
const expectedXsltPath = path.join(this.projectPath, dataMapsPath, `${this.dataMapName}${mapXsltExtension}`);

if (fileExistsSync(expectedXsltPath)) {
fs.readFile(expectedXsltPath, 'utf-8').then((fileContents) => {
Expand Down Expand Up @@ -592,12 +596,11 @@ export default class DataMapperPanel {
}

private getMapMetadataPath() {
const projectPath = ext.defaultLogicAppPath;
let vscodeFolderPath = '';
if (this.dataMapVersion === 2) {
vscodeFolderPath = path.join(projectPath, vscodeFolderName, `${this.dataMapName}DataMapMetadata-v2.json`);
vscodeFolderPath = path.join(this.projectPath, vscodeFolderName, `${this.dataMapName}DataMapMetadata-v2.json`);
} else {
vscodeFolderPath = path.join(projectPath, vscodeFolderName, `${this.dataMapName}DataMapMetadata.json`);
vscodeFolderPath = path.join(this.projectPath, vscodeFolderName, `${this.dataMapName}DataMapMetadata.json`);
}
return vscodeFolderPath;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IActionContext } from '@microsoft/vscode-azext-utils';
import { ViewColumn, window } from 'vscode';
import { ext } from '../../../../extensionVariables';
import DataMapperExt, { getDataMapPanelKey } from '../DataMapperExt';

const mocks = vi.hoisted(() => {
const dataMapperPanelConstructor = vi.fn(function (panel: any, dataMapName: string, panelKey: string, projectPath: string) {
return {
panel,
dataMapName,
panelKey,
projectPath,
updateWebviewPanelTitle: vi.fn(),
mapDefinitionData: undefined,
};
});

return {
dataMapperPanelConstructor,
startBackendRuntime: vi.fn().mockResolvedValue(undefined),
};
});

vi.mock('../DataMapperPanel', () => ({
default: mocks.dataMapperPanelConstructor,
}));

vi.mock('../FxWorkflowRuntime', () => ({
startBackendRuntime: mocks.startBackendRuntime,
}));

vi.mock('../../../../localize', () => ({
localize: (_key: string, defaultMessage: string) => defaultMessage,
}));

interface MockWebviewPanel {
iconPath?: unknown;
reveal: ReturnType<typeof vi.fn>;
webview: {
html: string;
onDidReceiveMessage: ReturnType<typeof vi.fn>;
postMessage: ReturnType<typeof vi.fn>;
};
}

describe('DataMapperExt panel identity', () => {
const context = {
ui: {
showInputBox: vi.fn(),
},
} as unknown as IActionContext;

beforeEach(() => {
vi.clearAllMocks();
(ext as any).dataMapPanelManagers ??= {};
for (const panelKey of Object.keys(ext.dataMapPanelManagers)) {
delete ext.dataMapPanelManagers[panelKey];
}

(ext as any).context = {
extensionPath: '/extension',
subscriptions: [],
};

vi.mocked(window.createWebviewPanel).mockImplementation(
() =>
({
iconPath: undefined,
reveal: vi.fn(),
webview: {
html: '',
onDidReceiveMessage: vi.fn(),
postMessage: vi.fn(),
},
}) as any
);
});

it('reveals the existing panel for the same project path and map name', async () => {
const projectPath = '/projects/alpha';
const dataMapName = 'orders';

await DataMapperExt.openDataMapperPanel(context, projectPath, dataMapName);
const existingManager = ext.dataMapPanelManagers[getDataMapPanelKey(projectPath, dataMapName)];
const existingPanel = existingManager.panel as unknown as MockWebviewPanel;

await DataMapperExt.openDataMapperPanel(context, projectPath, dataMapName);

expect(window.createWebviewPanel).toHaveBeenCalledTimes(1);
expect(mocks.dataMapperPanelConstructor).toHaveBeenCalledTimes(1);
expect(existingPanel.reveal).toHaveBeenCalledWith(ViewColumn.Active);
expect(ext.dataMapPanelManagers[getDataMapPanelKey(projectPath, dataMapName)]).toBe(existingManager);
});

it('registers identically named maps from different projects under separate composite keys', async () => {
const dataMapName = 'orders';
const firstProjectPath = '/projects/alpha';
const secondProjectPath = '/projects/beta';

await DataMapperExt.openDataMapperPanel(context, firstProjectPath, dataMapName);
await DataMapperExt.openDataMapperPanel(context, secondProjectPath, dataMapName);

const firstKey = getDataMapPanelKey(firstProjectPath, dataMapName);
const secondKey = getDataMapPanelKey(secondProjectPath, dataMapName);
const firstManager = ext.dataMapPanelManagers[firstKey];
const secondManager = ext.dataMapPanelManagers[secondKey];

expect(firstKey).not.toBe(secondKey);
expect(firstManager).toBeDefined();
expect(secondManager).toBeDefined();
expect(firstManager).not.toBe(secondManager);
expect(mocks.dataMapperPanelConstructor).toHaveBeenNthCalledWith(1, expect.anything(), dataMapName, firstKey, firstProjectPath);
expect(mocks.dataMapperPanelConstructor).toHaveBeenNthCalledWith(2, expect.anything(), dataMapName, secondKey, secondProjectPath);
});

it('attaches map definition data only to the manager for its project', async () => {
const dataMapName = 'orders';
const firstProjectPath = '/projects/alpha';
const secondProjectPath = '/projects/beta';
const firstMapDefinitionData = { mapDefinition: { source: 'alpha' } } as any;
const secondMapDefinitionData = { mapDefinition: { source: 'beta' } } as any;

await DataMapperExt.openDataMapperPanel(context, firstProjectPath, dataMapName, firstMapDefinitionData);
await DataMapperExt.openDataMapperPanel(context, secondProjectPath, dataMapName, secondMapDefinitionData);

expect(ext.dataMapPanelManagers[getDataMapPanelKey(firstProjectPath, dataMapName)].mapDefinitionData).toBe(firstMapDefinitionData);
expect(ext.dataMapPanelManagers[getDataMapPanelKey(secondProjectPath, dataMapName)].mapDefinitionData).toBe(secondMapDefinitionData);
});
});
Loading
Loading