Skip to content
Open
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 @@ -400,6 +400,7 @@
"7PtWvu": "(UTC-05:00) Eastern Time (US & Canada)",
"7QymrD": "Required. The string from which the substring is taken.",
"7ScdN6": "Name",
"7V737/": "XSLT not yet generated. Save the map to generate XSLT output.",
"7X4UA/": "Waiting",
"7ZF1Hr": "Validation error",
"7ZR1xr": "Add an action",
Expand Down Expand Up @@ -1964,6 +1965,7 @@
"_7PtWvu.comment": "Time zone value ",
"_7QymrD.comment": "Required string parameter required to obtain substring",
"_7ScdN6.comment": "Deployment model resource name label",
"_7V737/.comment": "Message to display when XSLT content hasn't been generated yet",
"_7X4UA/.comment": "The status message to show waiting in monitoring view.",
"_7ZF1Hr.comment": "The title of the validation error field in the static result parseJson action",
"_7ZR1xr.comment": "Text on example action node",
Expand Down Expand Up @@ -3166,6 +3168,7 @@
"_arjUBV.comment": "Error message when the workflow dark image is empty",
"_auUI93.comment": "label to inform to upload or select source schema to be used",
"_auci7r.comment": "Error validation message for CSVs",
"_aulGxd.comment": "Message when XSLT content is not available",
"_aurgrg.comment": "Authentication type",
"_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 @@ -4275,6 +4278,7 @@
"arjUBV": "The dark image version of the workflow is required for publish.",
"auUI93": "Add or select a source schema to use for your map.",
"auci7r": "Enter a valid comma-separated string.",
"aulGxd": "Please save the map before testing",
"aurgrg": "Managed identity",
"b2aL+f": "Pin action",
"b6G9bq": "URL encodes the input string",
Expand Down
2 changes: 2 additions & 0 deletions apps/vs-code-designer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
"process-tree": "^1.0.3",
"ps-tree": "^1.2.0",
"recursive-copy": "^2.0.14",
"saxon-js": "^2.6.0",
"semver": "^6.3.1",
"tslib": "2.4.0",
"uuid": "^10.0.0",
"vscode-extension-tester": "^8.17.0",
"vscode-nls": "^5.2.0",
"xml2js": "0.6.2",
"xslt3": "^2.7.0",
"yaml": "^2.7.0",
"yaml-types": "^0.4.0",
"yargs-parser": "21.1.1",
Expand Down
154 changes: 154 additions & 0 deletions apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,63 @@ import { parse } from 'yaml';
import { localize } from '../../../localize';
import { assetsFolderName, dataMapNameValidation } from '../../../constants';

/**
* UI metadata for function positions and canvas state.
*/
interface XsltUiMetadata {
functionNodes: Array<{
reactFlowGuid: string;
functionKey: string;
position: { x: number; y: number };
connections: Array<{ name: string; inputOrder: number }>;
connectionShorthand: string;
}>;
canvasRect?: { x: number; y: number; width: number; height: number };
}

/**
* Metadata embedded in XSLT files for Data Mapper v2.
* Contains mapDefinition in the metadata (legacy format).
* @deprecated Use XsltMapMetadataV3 which doesn't embed mapDefinition
*/
interface XsltMapMetadataV2 {
version: '2.0';
sourceSchema: string;
targetSchema: string;
mapDefinition: MapDefinitionEntry;
metadata: XsltUiMetadata;
}

/**
* Metadata embedded in XSLT files for Data Mapper v3.
* Only embeds layout metadata - mapping logic is derived from XSLT content.
*/
interface XsltMapMetadataV3 {
version: '3.0';
sourceSchema: string;
targetSchema: string;
metadata: XsltUiMetadata;
}

/**
* Union type supporting both v2 and v3 metadata formats.
*/
type XsltMapMetadata = XsltMapMetadataV2 | XsltMapMetadataV3;

/**
* Type guard to check if metadata is v2 format (with mapDefinition).
*/
const isV2Metadata = (metadata: XsltMapMetadata): metadata is XsltMapMetadataV2 => {
return metadata.version === '2.0' && 'mapDefinition' in metadata;
};

/**
* Type guard to check if metadata is v3 format (without mapDefinition).
*/
const isV3Metadata = (metadata: XsltMapMetadata): metadata is XsltMapMetadataV3 => {
return metadata.version === '3.0';
};

export default class DataMapperExt {
public static async openDataMapperPanel(
context: IActionContext,
Expand Down Expand Up @@ -119,4 +176,101 @@ export default class DataMapperExt {
}
}
}

/**
* Regex to extract metadata JSON from XSLT comment.
*/
private static readonly METADATA_REGEX = /<!--\s*LogicAppsDataMapper:\s*([\s\S]*?)\s*-->/;

/**
* Checks if an XSLT string has embedded metadata.
*/
public static hasEmbeddedMetadata(xslt: string): boolean {
return DataMapperExt.METADATA_REGEX.test(xslt);
}

/**
* Extracts metadata from an XSLT string if present.
* Supports both v2 (with mapDefinition) and v3 (without mapDefinition) formats.
*/
public static extractMetadataFromXslt(xslt: string): XsltMapMetadata | null {
const match = xslt.match(DataMapperExt.METADATA_REGEX);

if (!match || !match[1]) {
return null;
}

try {
const metadata = JSON.parse(match[1]) as XsltMapMetadata;

// Validate required fields (common to both v2 and v3)
if (!metadata.version || !metadata.sourceSchema || !metadata.targetSchema) {
console.warn('XSLT metadata missing required fields');
return null;
}

// v2 requires mapDefinition, v3 does not
if (metadata.version === '2.0' && !('mapDefinition' in metadata)) {
console.warn('XSLT metadata v2.0 missing mapDefinition');
return null;
}

return metadata;
} catch (error) {
console.error('Failed to parse XSLT metadata JSON:', error);
return null;
}
}

/**
* Return type for loadMapFromXslt
*/
public static loadMapFromXslt(
xsltContent: string,
_extInstance: typeof ext
): {
mapDefinition: MapDefinitionEntry;
sourceSchemaFileName: string;
targetSchemaFileName: string;
metadata?: XsltUiMetadata;
xsltContent?: string;
isV3Format?: boolean;
} | null {
const metadata = DataMapperExt.extractMetadataFromXslt(xsltContent);

if (!metadata) {
return null;
}

// Handle v2 format (with embedded mapDefinition)
if (isV2Metadata(metadata)) {
// Fix custom values in the map definition (same processing as LML)
DataMapperExt.fixMapDefinitionCustomValues(metadata.mapDefinition);

return {
mapDefinition: metadata.mapDefinition,
sourceSchemaFileName: metadata.sourceSchema,
targetSchemaFileName: metadata.targetSchema,
metadata: metadata.metadata,
isV3Format: false,
};
}

// Handle v3 format (without embedded mapDefinition)
// For v3, we pass the raw XSLT content so the webview can parse it
// to derive the mapping connections from the actual XSLT
if (isV3Metadata(metadata)) {
return {
// Empty mapDefinition - webview will derive from XSLT
mapDefinition: {},
sourceSchemaFileName: metadata.sourceSchema,
targetSchemaFileName: metadata.targetSchema,
metadata: metadata.metadata,
xsltContent: xsltContent,
isV3Format: true,
};
}

return null;
}
}
Loading
Loading