Skip to content

Commit

Permalink
Add links in package.xml Markdown documentation (#972)
Browse files Browse the repository at this point in the history
* Add links in package.xml Markdown documentation

* cspell
  • Loading branch information
nvuillam authored Jan 4, 2025
1 parent 6791c8a commit 34e9bb0
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 9 deletions.
1 change: 1 addition & 0 deletions .github/linters/.cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"VCAS",
"VERSIONNUMBER",
"Viewfile",
"Visualforce",
"Visualiser",
"Vuillamy",
"WIPO",
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image
- New command [hardis:doc:mkdocs-to-salesforce](https://sfdx-hardis.cloudity.com/hardis/doc/mkdocs-to-salesforce/) to generate static HTML doc and host it in a Static Resource and a VisualForce page
- Remove hyperlinks from MermaidJs on Pull Request comments, to improve display on GitHub & Gitlab
- Upgrade base image to python:3.12.8-alpine3.20, so mkdocs can be installed and run if necessary
- Add links in package.xml Markdown documentation

## [5.11.0] 2025-01-03

Expand Down
6 changes: 4 additions & 2 deletions src/commands/hardis/doc/packagexml2markdown.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* jscpd:ignore-start */
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { SfCommand, Flags, optionalOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { AnyJson } from '@salesforce/ts-types';
import { WebSocketClient } from '../../../common/websocketClient.js';
Expand Down Expand Up @@ -38,6 +38,7 @@ export default class PackageXml2Markdown extends SfCommand<any> {
skipauth: Flags.boolean({
description: 'Skip authentication check when a default username is required',
}),
"target-org": optionalOrgFlagWithDeprecations
};

// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
Expand All @@ -55,7 +56,8 @@ export default class PackageXml2Markdown extends SfCommand<any> {
this.debugMode = flags.debug || false;

// Generate markdown for package.xml
this.outputFile = await generatePackageXmlMarkdown(this.inputFile, this.outputFile);
const instanceUrl = flags?.['target-org']?.getConnection()?.instanceUrl;
this.outputFile = await generatePackageXmlMarkdown(this.inputFile, this.outputFile, null, instanceUrl);

// Open file in a new VsCode tab if available
WebSocketClient.requestOpenFile(this.outputFile);
Expand Down
10 changes: 6 additions & 4 deletions src/commands/hardis/doc/project2markdown.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* jscpd:ignore-start */
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { SfCommand, Flags, optionalOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import fs from 'fs-extra';
import c from "chalk";
import * as path from "path";
Expand Down Expand Up @@ -97,6 +97,7 @@ ${this.htmlInstructions}
skipauth: Flags.boolean({
description: 'Skip authentication check when a default username is required',
}),
"target-org": optionalOrgFlagWithDeprecations
};

// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
Expand Down Expand Up @@ -152,7 +153,8 @@ ${this.htmlInstructions}
// List SFDX packages and generate a manifest for each of them, except if there is only force-app with a package.xml
this.packageXmlCandidates = this.listPackageXmlCandidates();
await this.manageLocalPackages();
await this.generatePackageXmlMarkdown(this.packageXmlCandidates);
const instanceUrl = flags?.['target-org']?.getConnection()?.instanceUrl;
await this.generatePackageXmlMarkdown(this.packageXmlCandidates, instanceUrl);
const packageLines = await this.buildPackagesIndex();
this.mdLines.push(...packageLines);
await fs.writeFile(path.join(this.outputMarkdownRoot, "manifests.md"), packageLines.join("\n") + `\n${this.footer}\n`);
Expand Down Expand Up @@ -488,12 +490,12 @@ ${Project2Markdown.htmlInstructions}
return packageLines;
}

private async generatePackageXmlMarkdown(packageXmlCandidates) {
private async generatePackageXmlMarkdown(packageXmlCandidates, instanceUrl) {
// Generate packageXml doc when found
for (const packageXmlCandidate of packageXmlCandidates) {
if (fs.existsSync(packageXmlCandidate.path)) {
// Generate markdown for package.xml
const packageMarkdownFile = await generatePackageXmlMarkdown(packageXmlCandidate.path, null, packageXmlCandidate);
const packageMarkdownFile = await generatePackageXmlMarkdown(packageXmlCandidate.path, null, packageXmlCandidate, instanceUrl);
// Open file in a new VsCode tab if available
WebSocketClient.requestOpenFile(packageMarkdownFile);
packageXmlCandidate.markdownFile = packageMarkdownFile;
Expand Down
124 changes: 121 additions & 3 deletions src/common/utils/docUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { countPackageXmlItems, parsePackageXmlFile } from "./xmlUtils.js";
import { CONSTANTS } from "../../config/index.js";
import { SfError } from "@salesforce/core";

export async function generatePackageXmlMarkdown(inputFile: string | null, outputFile: string | null = null, packageXmlDefinition: any = null) {
export async function generatePackageXmlMarkdown(inputFile: string | null, outputFile: string | null = null, packageXmlDefinition: any = null, rootSalesforceUrl: string = "") {
// Find packageXml to parse if not defined
if (inputFile == null) {
inputFile = path.join(process.cwd(), "manifest", "package.xml");
Expand Down Expand Up @@ -58,11 +58,18 @@ export async function generatePackageXmlMarkdown(inputFile: string | null, outpu
const members = packageXmlContent[metadataType];
members.sort();
const memberLengthLabel = members.length === 1 && members[0] === "*" ? "*" : members.length;
mdLines.push(`<details><summary>${metadataType} (${memberLengthLabel})</summary>`);
mdLines.push(`<details><summary>${metadataType} (${memberLengthLabel})</summary>\n\n`);
for (const member of members) {
const memberLabel = member === "*" ? "ALL (wildcard *)" : member;
mdLines.push(` • ${memberLabel}<br/>`);
const setupUrl = SalesforceSetupUrlBuilder.getSetupUrl(metadataType, member);
if (setupUrl && rootSalesforceUrl) {
mdLines.push(` • <a href="${rootSalesforceUrl}${setupUrl}" target="_blank">${memberLabel}</a><br/>`);
}
else {
mdLines.push(` • ${memberLabel}<br/>`);
}
}
mdLines.push("");
mdLines.push("</details>");
mdLines.push("");
}
Expand Down Expand Up @@ -105,4 +112,115 @@ export async function writeMkDocsFile(mkdocsYmlFile: string, mkdocsYml: any) {
.replace("'!!python/name:pymdownx.superfences.fence_code_format'", '!!python/name:pymdownx.superfences.fence_code_format');
await fs.writeFile(mkdocsYmlFile, mkdocsYmlStr);
uxLog(this, c.cyan(`Updated mkdocs-material config file at ${c.green(mkdocsYmlFile)}`));
}

const alreadySaid: string[] = [];

export class SalesforceSetupUrlBuilder {
/**
* Map of metadata types to their Lightning Experience setup paths.
*/
private static readonly setupAreaMap: Record<string, string> = {
'ActionLinkGroupTemplate': '/lightning/setup/ActionLinkTemplates/home',
'AppMenu': '/lightning/setup/NavigationMenus/home',
'ApprovalProcess': '/lightning/setup/ApprovalProcesses/home',
'AssignmentRules': '/lightning/setup/AssignmentRules/home',
'AuthProvider': '/lightning/setup/AuthProviders/home',
'AutoResponseRules': '/lightning/setup/AutoResponseRules/home',
'ApexClass': '/lightning/setup/ApexClasses/home',
'ApexPage': '/lightning/setup/VisualforcePages/home',
'ApexTrigger': '/lightning/setup/ApexTriggers/home',
'BusinessProcess': '/lightning/setup/ObjectManager/{objectName}/BusinessProcesses/view',
'CompactLayout': '/lightning/setup/ObjectManager/{objectName}/CompactLayouts/view',
'ConnectedApp': '/lightning/setup/ConnectedApps/home',
'ContentAsset': '/lightning/setup/ContentAssets/home',
'CustomApplication': '/lightning/setup/NavigationMenus/home',
'CustomField': '/lightning/setup/ObjectManager/{objectName}/FieldsAndRelationships/{apiName}/view',
'CustomHelpMenu': '/lightning/setup/CustomHelpMenu/home',
'CustomLabel': '/lightning/setup/CustomLabels/home',
'CustomMetadata': '/lightning/setup/CustomMetadataTypes/home',
'CustomNotificationType': '/lightning/setup/CustomNotifications/home',
'CustomObject': '/lightning/setup/ObjectManager/{objectName}/Details/view',
'CustomPermission': '/lightning/setup/CustomPermissions/home',
'CustomSetting': '/lightning/setup/ObjectManager/{objectName}/Details/view',
'CustomSite': '/lightning/setup/Sites/home',
'CustomTab': '/lightning/setup/Tabs/home',
'Dashboard': '/lightning/setup/Dashboards/home',
'DashboardFolder': '/lightning/setup/DashboardFolders/home',
'DataCategoryGroup': '/lightning/setup/DataCategories/home',
'EmailServicesFunction': '/lightning/setup/EmailServices/home',
'EmailTemplate': '/lightning/setup/EmailTemplates/home',
'EntitlementTemplate': '/lightning/setup/EntitlementTemplates/home',
'EscalationRules': '/lightning/setup/EscalationRules/home',
'EventSubscription': '/lightning/setup/PlatformEvents/home',
'ExternalDataSource': '/lightning/setup/ExternalDataSources/home',
'ExternalService': '/lightning/setup/ExternalServices/home',
'FieldSet': '/lightning/setup/ObjectManager/{objectName}/FieldSets/view',
'Flexipage': '/lightning/setup/FlexiPageList/home',
'Flow': '/lightning/setup/Flows/home',
'GlobalPicklist': '/lightning/setup/Picklists/home',
'Group': '/lightning/setup/PublicGroups/home',
'HomePageLayout': '/lightning/setup/HomePageLayouts/home',
'Layout': '/lightning/setup/ObjectManager/{objectName}/PageLayouts/view',
'LightningComponentBundle': '/lightning/setup/LightningComponents/home',
'MilestoneType': '/lightning/setup/Milestones/home',
'NamedCredential': '/lightning/setup/NamedCredentials/home',
'OmniChannelSettings': '/lightning/setup/OmniChannelSettings/home',
'PermissionSet': '/lightning/setup/PermissionSets/home',
'PermissionSetGroup': '/lightning/setup/PermissionSetGroups/home',
'PlatformEvent': '/lightning/setup/PlatformEvents/home',
'Profile': '/lightning/setup/Profiles/home',
'Queue': '/lightning/setup/Queues/home',
'RecordType': '/lightning/setup/ObjectManager/{objectName}/RecordTypes/view',
'RemoteSiteSetting': '/lightning/setup/RemoteSites/home',
'Report': '/lightning/setup/Reports/home',
'ReportFolder': '/lightning/setup/ReportFolders/home',
'Role': '/lightning/setup/Roles/home',
'ServiceChannel': '/lightning/setup/ServiceChannels/home',
'SharingRules': '/lightning/setup/SharingRules/home',
'StaticResource': '/lightning/setup/StaticResources/home',
'Territory': '/lightning/setup/Territories/home',
'TerritoryModel': '/lightning/setup/TerritoryManagement/home',
'Translation': '/lightning/setup/Translations/home',
'ValidationRule': '/lightning/setup/ObjectManager/{objectName}/ValidationRules/view',
'VisualforcePage': '/lightning/setup/VisualforcePages/home',
'Workflow': '/lightning/setup/Workflow/home',
// Add more metadata types if needed
};

/**
* Generates the setup URL for a given metadata type and API name (if required).
* @param metadataType The metadata type (e.g., "CustomObject", "ApexClass").
* @param apiName The API name of the metadata (optional, e.g., "Account").
* @returns The constructed setup URL.
* @throws Error if the metadata type is unsupported or the API name is missing for required types.
*/
public static getSetupUrl(metadataType: string, apiName: string): string | null {
const pathTemplate = this.setupAreaMap[metadataType];

if (!pathTemplate) {
if (!alreadySaid.includes(metadataType)) {
uxLog(this, c.grey(`Unsupported metadata type: ${metadataType}`));
alreadySaid.push(metadataType);
}
return null;
}

let apiNameFinal = apiName + "";
let objectName = ""
if (apiName.includes(".") && apiName.split(".").length === 2) {
[objectName, apiNameFinal] = apiName.split(".")[1];
}

// Replace placeholders in the path template with the provided API name
const urlPath = pathTemplate
.replace(/\{objectName\}/g, objectName || '')
.replace(/\{apiName\}/g, apiNameFinal || '');

if (urlPath.includes('{apiName}') || urlPath.includes('{objectName}')) {
uxLog(this, c.grey(`Wrong replacement in ${urlPath} with values apiName:${apiNameFinal} and objectName:${objectName}`));
}

return urlPath;
}
}

0 comments on commit 34e9bb0

Please sign in to comment.