Skip to content

Commit fde6c47

Browse files
[feat] Flow Scanner: resolve FlowDefinition ID and support FlowRecord pages
- Resolve FlowDefinition ID (300xxx) to active/latest Flow version when only that ID is available - Support opening Flow Scanner from FlowRecord pages (/lightning/r/FlowRecord/...) - Display the analyzed flow version number in the Flow Information card - Sanitize URL parameters to handle null/undefined string values gracefully
1 parent c32bc41 commit fde6c47

3 files changed

Lines changed: 141 additions & 6 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
## Version 2.0
99

1010
- `Data Export` Fix unrecognized Salesforce Ids [issue #984](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/984)
11+
- `Flow Scanner` Resolve FlowDefinition ID (300xxx) when only that ID is available, using the active version (or latest as fallback). Support opening Flow Scanner from FlowRecord pages (`/lightning/r/FlowRecord/...`). Display the analyzed flow version number in the Flow Information card (contribution by [Camille Guillory](https://github.com/CamilleGuillory))
1112
- `Data Import` Expose metadata updates through Tooling API (ie Bulk Deactivate Flows) [feature 1125](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1125)
1213
- Fix Lightning Navigation from Analytics / Tableau [issue #1121](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/1121)
1314
- `Popup` review cache management in "Preload SObjects before popup opens". If enabled, refresh of SObject definition is done every "SObjects List Cache" hours, else done in background when the popup is expanded

addon/flow-scanner.js

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ function getPrimaryAffectedElement(result) {
2929
return result.affectedElements?.[0] || {};
3030
}
3131

32+
/**
33+
* Sanitizes URL parameters by converting invalid values to null.
34+
* @param {string|null} param - The URL parameter to sanitize.
35+
* @returns {string|null} The sanitized parameter value.
36+
*/
37+
function sanitizeUrlParam(param) {
38+
return (!param || param === "null" || param === "undefined") ? null : param;
39+
}
40+
3241
/**
3342
* Generates a plan for purging old flow versions, determining which versions to keep and delete.
3443
* @param {Array} versions - Array of flow version objects.
@@ -367,6 +376,7 @@ class FlowScanner {
367376
type = "ScreenFlow";
368377
}
369378
const showProcessType = type !== processType;
379+
const versionNumber = versions.find(v => v.Id === this.flowId)?.VersionNumber ?? null;
370380

371381
// Construct complete flow information object
372382
const result = {
@@ -378,6 +388,7 @@ class FlowScanner {
378388
type,
379389
status,
380390
displayStatus,
391+
versionNumber,
381392
xmlData,
382393
triggerObjectLabel,
383394
triggerType,
@@ -1166,6 +1177,10 @@ function FlowInfoSection(props) {
11661177
id: "flow-status-badge"
11671178
}, flow.displayStatus)
11681179
),
1180+
h("div", {className: "flow-detail-item flow-version-item"},
1181+
h("span", {className: "detail-label"}, "Version"),
1182+
h("span", {className: "detail-value", id: "flow-version-number"}, flow.versionNumber ?? "—")
1183+
),
11691184
h("div", {className: "flow-detail-item flow-type-item"},
11701185
h("span", {className: "detail-label"}, "Type"),
11711186
h("span", {className: "detail-value", id: "flow-type"}, flow.type)
@@ -1421,6 +1436,92 @@ function PurgeModal(props) {
14211436
);
14221437
}
14231438

1439+
/**
1440+
* Resolves flow IDs from URL parameters, handling cases where flowId is a FlowDefinition ID
1441+
* or where one of the IDs is missing.
1442+
* @param {string|null} flowDefId - The flow definition ID from URL params.
1443+
* @param {string|null} flowId - The flow version ID from URL params.
1444+
* @returns {Promise<{flowDefId: string, flowId: string}>} Resolved IDs.
1445+
*/
1446+
async function resolveFlowIds(flowDefId, flowId) {
1447+
let resolvedDefId = flowDefId;
1448+
let resolvedFlowId = flowId;
1449+
1450+
// Both present and correct types - no resolution needed
1451+
if (resolvedDefId?.startsWith("300") && resolvedFlowId?.startsWith("301")) {
1452+
return {flowDefId: resolvedDefId, flowId: resolvedFlowId};
1453+
}
1454+
1455+
// Handle flowId being a FlowDefinition ID (300xxx)
1456+
if (resolvedFlowId?.startsWith("300")) {
1457+
resolvedDefId = resolvedDefId || resolvedFlowId;
1458+
resolvedFlowId = null;
1459+
}
1460+
1461+
// Resolve missing IDs
1462+
if (!resolvedDefId && resolvedFlowId?.startsWith("301")) {
1463+
resolvedDefId = await getDefinitionIdFromFlowVersion(resolvedFlowId);
1464+
}
1465+
if (!resolvedFlowId && resolvedDefId?.startsWith("300")) {
1466+
resolvedFlowId = await getFlowVersionFromDefinition(resolvedDefId);
1467+
}
1468+
1469+
if (!resolvedDefId || !resolvedFlowId) {
1470+
throw new Error(`Unable to resolve flow IDs: flowDefId=${resolvedDefId}, flowId=${resolvedFlowId}`);
1471+
}
1472+
1473+
return {flowDefId: resolvedDefId, flowId: resolvedFlowId};
1474+
}
1475+
1476+
/**
1477+
* Gets the FlowDefinition ID from a Flow version ID.
1478+
* @param {string} flowVersionId - The Flow version ID (301xxx).
1479+
* @returns {Promise<string>} The FlowDefinition ID.
1480+
*/
1481+
async function getDefinitionIdFromFlowVersion(flowVersionId) {
1482+
const query = `SELECT DefinitionId FROM Flow WHERE Id='${flowVersionId}'`;
1483+
const response = await sfConn.rest(
1484+
`/services/data/v${apiVersion}/tooling/query/?q=${encodeURIComponent(query)}`
1485+
);
1486+
1487+
if (!response?.records?.[0]?.DefinitionId) {
1488+
throw new Error(`Could not resolve FlowDefinition for Flow version '${flowVersionId}'.`);
1489+
}
1490+
1491+
return response.records[0].DefinitionId;
1492+
}
1493+
1494+
/**
1495+
* Gets a Flow version ID from a FlowDefinition ID.
1496+
* Tries to get the active version first, falls back to latest version.
1497+
* @param {string} flowDefId - The FlowDefinition ID (300xxx).
1498+
* @returns {Promise<string>} The Flow version ID.
1499+
*/
1500+
async function getFlowVersionFromDefinition(flowDefId) {
1501+
// Try active version first
1502+
const activeQuery = `SELECT Id FROM Flow WHERE DefinitionId='${flowDefId}' AND Status='Active' LIMIT 1`;
1503+
const activeResponse = await sfConn.rest(
1504+
`/services/data/v${apiVersion}/tooling/query/?q=${encodeURIComponent(activeQuery)}`
1505+
);
1506+
1507+
if (activeResponse?.records?.[0]?.Id) {
1508+
return activeResponse.records[0].Id;
1509+
}
1510+
1511+
// Fall back to latest version
1512+
console.warn(`No active Flow version found for FlowDefinition '${flowDefId}'. Falling back to latest version.`);
1513+
const latestQuery = `SELECT LatestVersionId FROM FlowDefinitionView WHERE DurableId='${flowDefId}'`;
1514+
const latestResponse = await sfConn.rest(
1515+
`/services/data/v${apiVersion}/query/?q=${encodeURIComponent(latestQuery)}`
1516+
);
1517+
1518+
if (!latestResponse?.records?.[0]?.LatestVersionId) {
1519+
throw new Error(`Could not resolve a Flow version for FlowDefinition '${flowDefId}'.`);
1520+
}
1521+
1522+
return latestResponse.records[0].LatestVersionId;
1523+
}
1524+
14241525
/**
14251526
* The main React component for the Flow Scanner application.
14261527
*/
@@ -1581,11 +1682,14 @@ class App extends React.Component {
15811682
this.setState({error: null});
15821683
const params = new URLSearchParams(window.location.search);
15831684
const sfHost = params.get("host");
1584-
const flowDefId = params.get("flowDefId");
1585-
const flowId = params.get("flowId");
1685+
const flowDefId = sanitizeUrlParam(params.get("flowDefId"));
1686+
const flowId = sanitizeUrlParam(params.get("flowId"));
15861687

1587-
if (!sfHost || !flowDefId || !flowId) {
1588-
throw new Error(`Missing required parameters: host=${sfHost}, flowDefId=${flowDefId}, flowId=${flowId}`);
1688+
if (!sfHost) {
1689+
throw new Error("Missing required parameter: host");
1690+
}
1691+
if (!flowDefId && !flowId) {
1692+
throw new Error("Missing required parameters: at least one of flowDefId or flowId must be provided.");
15891693
}
15901694

15911695
window.initButton(sfHost, true);
@@ -1596,6 +1700,9 @@ class App extends React.Component {
15961700

15971701
await sfConn.getSession(sfHost);
15981702

1703+
// Resolve and normalize flow IDs
1704+
const {flowDefId: resolvedDefId, flowId: resolvedFlowId} = await resolveFlowIds(flowDefId, flowId);
1705+
15991706
// Set org name from sfHost
16001707
const orgName = sfHost.split(".")[0]?.toUpperCase() || "";
16011708
this.setState({orgName});
@@ -1610,7 +1717,7 @@ class App extends React.Component {
16101717
});
16111718
});
16121719

1613-
this.flowScanner = new FlowScanner(sfHost, flowDefId, flowId);
1720+
this.flowScanner = new FlowScanner(sfHost, resolvedDefId, resolvedFlowId);
16141721
await this.flowScanner.init();
16151722

16161723
this.setState({isLoading: false, error: null});

addon/popup.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3891,8 +3891,21 @@ class AllDataSelection extends React.PureComponent {
38913891
);
38923892
}
38933893
getFlowScannerUrl() {
3894-
return `flow-scanner.html?host=${this.props.sfHost}&flowDefId=${this.state.flowDefinitionId}&flowId=${this.props.selectedValue.recordId}`;
3894+
const {sfHost, selectedValue} = this.props;
3895+
const recordId = selectedValue.recordId;
3896+
3897+
// FlowDefinition ID (300) - let flow-scanner resolve the version
3898+
if (recordId?.startsWith("300")) {
3899+
return `flow-scanner.html?host=${sfHost}&flowDefId=${recordId}`;
3900+
}
38953901

3902+
// FlowRecord (2aF) - use the resolved flowDefinitionId
3903+
if (recordId?.startsWith("2aF")) {
3904+
return `flow-scanner.html?host=${sfHost}&flowDefId=${this.state.flowDefinitionId}`;
3905+
}
3906+
3907+
// Flow version ID (301) - include both definition and version IDs
3908+
return `flow-scanner.html?host=${sfHost}&flowDefId=${this.state.flowDefinitionId}&flowId=${recordId}`;
38963909
}
38973910
getFlowCompareUrl() {
38983911
return getFlowCompareUrl(this.props.sfHost, this.props.selectedValue.recordId);
@@ -4042,6 +4055,20 @@ class AllDataSelection extends React.PureComponent {
40424055
});
40434056
} else if (recordId.startsWith("300")) {
40444057
this.setState({flowDefinitionId: recordId});
4058+
} else if (recordId.startsWith("2aF")) {
4059+
sfConn
4060+
.rest(
4061+
"/services/data/v"
4062+
+ apiVersion
4063+
+ "/query/?q=SELECT+FlowDefinition+FROM+FlowRecord+WHERE+Id='"
4064+
+ recordId
4065+
+ "'"
4066+
)
4067+
.then((res) => {
4068+
if (res.records && res.records.length > 0) {
4069+
this.setState({flowDefinitionId: res.records[0].FlowDefinition});
4070+
}
4071+
});
40454072
}
40464073
}
40474074
}

0 commit comments

Comments
 (0)