@@ -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 } ) ;
0 commit comments