Name

sn_canvas_core.UXPagePropertyUtil

Description

Utility scripts to process the UX Page Property values based on roles, canCreate, etc.

Script

var UXPagePropertyUtil = Class.create();

/**!
* @name UXPagePropertyUtil.get
* @description Gets the value at path of object. If the resolved value is undefined,
*  the defaultValue is returned in its place.
*
* @param {Object} object The object to query.
* @param {Array|string} path The path of the property to get.
* @param {*} [value] The default value returned for `undefined` resolved values.
* @returns {*} Returns the resolved value.
*
*/
UXPagePropertyUtil.get = function (object, path, value) {
  var pathArray;
  // If path is not defined or it has false value
  if (!path) return undefined;
  // Check if path is string or array. Regex : ensure that we do not have '.' and brackets
  pathArray = Array.isArray(path) ?
      path :
      path.split(/[,[\].]/g).filter(Boolean);

  // Find value if exist return otherwise return undefined value;
  var pathResult = pathArray.reduce(function (prevObj, key) {
          return prevObj && prevObj[key];
      }, object);
 return pathResult === undefined ?  value : pathResult;
};

/**!
* @name UXPagePropertyUtil.set
* @description Add items to an object at a specific path
*
* @param  {Object}       obj  The object
* @param  {String|Array} path The path to assign the value to
* @param  {*}            val  The value to assign
*/
UXPagePropertyUtil.set = function (obj, path, val) {

  /**
    * If the path is a string, convert it to an array
    * @param  {String|Array} path The path
    * @return {Array}             The path array
    */
  var stringToPath = function (path) {

      // If the path isn't a string, return it
      if (typeof path !== 'string') return path;

      // Create new array
      var output = [];

      // Split to an array with dot notation
      path.split('.').forEach(function (item, index) {

          // Split to an array with bracket notation
          item.split(/\[([^}]+)\]/g).forEach(function (key) {

              // Push to the new array
              if (key.length > 0) {
                  output.push(key);
              }

          });

      });

      return output;

  };

  // Convert the path to an array if not already
  path = stringToPath(path);

  // Cache the path length and current spot in the object
  var length = path.length;
  var current = obj;

  // Loop through the path
  path.forEach(function (key, index) {

      // Check if the assigned key should be an array
      var isArray = key.slice(-2) === '[]';

      // If so, get the true key name by removing the trailing []
      key = isArray ? key.slice(0, -2) : key;

      // If the key should be an array and isn't, create an array
      if (isArray && Object.prototype.toString.call(current[key]) !== '[object Array]') {
          current[key] = [];
      }

      // If this is the last item in the loop, assign the value
      if (index === length - 1) {

          // If it's an array, push the value
          // Otherwise, assign it
          if (isArray) {
              current[key].push(val);
          } else {
              current[key] = val;
          }
      } else { // Otherwise, update the current place in the object

          // If the key doesn't exist, create it
          if (!current[key]) {
              current[key] = {};
          }

          // Update the current place in the object
          current = current[key];

      }

  });

};

/**!
* @name UXPagePropertyUtil.handleTranslations
* @description I18N translation support
*
* @param  {Object}       property      object for I18N translation handling
*/
UXPagePropertyUtil.handleTranslations = function (property) {
  if (typeof property !== 'object' || !property)
      return;

  Object.keys(property).forEach(function (key) {
      if (property[key].hasOwnProperty("translatable") &&
          property[key].hasOwnProperty("message")) {
          var label = property[key]["message"];
          property[key] = property[key]["translatable"] === true ? gs.getMessage(label) : label;
      }
      if (property[key] !== null &&
          property[key] !== undefined &&
          typeof property[key] === 'object') {
          UXPagePropertyUtil.handleTranslations(property[key]);
      }
  });
};

/**!
* @name UXPagePropertyUtil.hasRoles
* @description checks to see if the user has roles. If roles is not an array it will return true
* 
* @param {Array.<string>} roles a list of roles to check against the current user.
* @returns {boolean}
*/
UXPagePropertyUtil.hasRoles = function(roles) {
  if (roles && Array.isArray(roles) && roles.length > 0) {
      var flag = false, i, length;
      for (i = 0, length = roles.length; i < length; i++) {
          if (gs.getUser().hasRole(roles[i])) {
              flag = true;
              break;
          }
      }
      return flag;
  }
  return true;
};

/**!
* @name UXPagePropertyUtil.isPluginActive
* @description checks to see if the plugin is active.
* 
* @param {string} plugin name of the plugin.
* @returns {boolean}
*/
UXPagePropertyUtil.isPluginActive = function(plugin) {
  if(plugin && plugin.length > 0)
      return hasPluginActivated = GlidePluginManager.isActive(plugin);
  return true;
};

/**!
* @name applyConditions
* @description applyConditions filters newMenu items array based on the conditions
* 1. if user has write access to the table
* 2. if user has any of roles , if specified
* 3. if a plugin, if specified, is active
* create records for the configured table.
*
* @param item
* @return {boolean|*}
*/
UXPagePropertyUtil.applyConditions = function (item) {
  var hasWriteAccess = true, hasRequiredRoles = true, hasPluginActivated = true;
  var hasCondition = !!UXPagePropertyUtil.get(item, 'item.condition', {});
  if (!hasCondition)
      return true;
  //can create check    
  var hasTableDescription = !!UXPagePropertyUtil.get(item, 'condition.tableDescription', {}),
  table = hasTableDescription ? UXPagePropertyUtil.get(item, 'condition.tableDescription.table', '') : '';

  if (table) {
      hasWriteAccess = new GlideRecord(table).canCreate();
  }

  //plugin check
  var plugin = UXPagePropertyUtil.get(item, 'condition.plugin', "");
  hasPluginActivated = UXPagePropertyUtil.isPluginActive(plugin);

  // Role check
  var requiredRoles = UXPagePropertyUtil.get(item, 'condition.roles', []);
  hasRequiredRoles = UXPagePropertyUtil.hasRoles(requiredRoles);

  return hasWriteAccess && hasRequiredRoles && hasPluginActivated;
};

/**!
* @name UXPagePropertyUtil.reducePrimary
* @description The reducePrimary method is a reducer function to compute build "primaryRoute" object data
* for the toolbar button with configured routeInfo
*
* @param retObj
* @param cur
* @return {*}
*/
UXPagePropertyUtil.reducePrimary = function (retObj, cur) {
  retObj[UXPagePropertyUtil.get(cur, 'routeInfo.route')] = cur;
  return retObj;
};

/**!
* @name UXPagePropertyUtil.applyAvailability
* @description The applyAvailability method checks for the availability conditions for toolbar
* button configuration
*
* @param buttonConfig
* @return {boolean}
*/
UXPagePropertyUtil.applyAvailability = function(buttonConfig) {
  var roles = UXPagePropertyUtil.get(buttonConfig, 'availability.roles', []),
      plugin = UXPagePropertyUtil.get(buttonConfig, 'availability.plugin', ''),
      rolesMatch = function(roles) {
          var match = false,
              i = 0;
          if (roles.length) {
              for (i = 0; i < roles.length; i++) {
                  match = gs.hasRole(roles[i]);
  				if (!match) {
  					break;
  				} else {
  					continue;
  				}
              }
          }
  		return match;
      };


  if (roles.length && plugin) {
  	// when both roles & plugin
  	return rolesMatch(roles) && UXPagePropertyUtil.isPluginActive(plugin);
  } else if (roles.length) {
  	// when only roles
  	return rolesMatch(roles);
  } else if (plugin) {
  	// when only plugin
  	return 	UXPagePropertyUtil.isPluginActive(plugin);
  } else {
  	// when availability conditions are not defined, we make it available
  	return true;
  }

};

/*!
* @name UXPagePropertyUtil.deepMerge
* @description Deep merge two or more objects together.
* @param   {Object}   objects  The objects to merge together
* @returns {Object}            Merged values of defaults and options
*
* example:
*   const default = {s: 4, d: 15};
*   const options = {a: 1, d: 55, b: { c: 2, d:3}};
*   deepMerge(default, options); // {s: 4, d: 55, a: 1, b: { c: 2, d:3}}
*/
UXPagePropertyUtil.deepMerge = function () {

  // Setup merged object
  var newObj = {};

  // Merge the object into the newObj object
  var merge = function (obj) {
      for (var prop in obj) {
          if (obj.hasOwnProperty(prop)) {
              // If property is an object, merge properties
              if (Object.prototype.toString.call(obj[prop]) === '[object Object]') {
                  newObj[prop] = UXPagePropertyUtil.deepMerge(newObj[prop], obj[prop]);
              } else {
                  newObj[prop] = obj[prop];
              }
          }
      }
  };

  // Loop through each object and conduct a merge
  for (var i = 0; i < arguments.length; i++) {
      merge(arguments[i]);
  }

  return newObj;

};

/**!
* @name parse
* @description Parse the JSON property values
* @param data
* @return {{}|any}
*/
UXPagePropertyUtil.parse = function (data) {
  try {
      if (typeof data === 'string') {
          return JSON.parse(data);
      } else {
          return data;
      }

  } catch (e) {
      gs.error('Error parsing value of type JSON', e);
      return {};
  }
};

/**!
* @name isAuthBinding
* @description Use duck typing to check if an object is an AUTH_ROUTE_BINDING.
* 
* @param {Object} obj 
* @returns {boolean}
* 
* @example
* // syntax of AUTH_ROUTE_BINDING usage
* { 
*   "type": "AUTH_ROUTE_BINDING",
*   "binding": {
*     "address": ["logout"],
*     "default": "/logout.do"
*   }
* }
*/
UXPagePropertyUtil.isAuthBinding = function (obj) {
  var hasValidType = obj.hasOwnProperty('type') && obj.type === 'AUTH_ROUTE_BINDING';
  var hasValidBindingAddress = Array.isArray(UXPagePropertyUtil.get(obj, 'binding.address', null));

  return hasValidType && hasValidBindingAddress;
};

/**!
* @name getRouteFromAuthValue
* @description Given an authObject convert it into a route value parseable by sn-nav-item.
* 
* @param {Object} authObj 
*/
UXPagePropertyUtil.getRouteFromAuthValue = function (authObj, authRoutes) {
  var address = UXPagePropertyUtil.get(authObj, 'binding.address', []);
  var extraParams = UXPagePropertyUtil.get(authObj, 'binding.params', {});
  var defaultValue = UXPagePropertyUtil.get(authObj, 'binding.default', '');
  var resolvedAddress = UXPagePropertyUtil.get(authRoutes, address.join('.'), null);
  var defaultValueRoute = defaultValue;
  var stringifiedProp = Object.prototype.toString.call(defaultValue);     

  // Routes from auth routes will be a route that leads to a page or routes that lead to a viewportModal.
  var resolvedAddressRoute = {};
  
  // Duck type to check if it should be of type viewportModal or route.
  if (resolvedAddress && resolvedAddress.hasOwnProperty('viewportElementId') && resolvedAddress.hasOwnProperty('route')) {
      resolvedAddressRoute = {
          type: 'viewportModal',
          value: UXPagePropertyUtil.deepMerge(extraParams, resolvedAddress)
      };
  } else {
      resolvedAddressRoute = {
          type: 'route',
          value: UXPagePropertyUtil.deepMerge(extraParams, { route: resolvedAddress })
      };
  }
  
  // If default value is a string covert it into an external route
  // If default value is an object convert it into a route use-able by `sn-canvas-router`
  if (stringifiedProp === '[object String]') {
      defaultValueRoute = {
          type: 'external',
          value: { href: defaultValue }
      };
  }
  
  return !resolvedAddress ? defaultValueRoute : resolvedAddressRoute;
};

/**!
* @name isNil
* @description Determines if an object is null or undefined.
* 
* @param {Object} value 
*/
UXPagePropertyUtil.isNil = function(value) {
  return value === null || value === undefined;
};

/**!
* @name processUserPreferences
* @description Process user preference values given _meta syntax.
* @example
* 
*  "_meta":{
*      "_type":"userPreferences",
*      "_path":"userPrefName",
*      "_value":{
*          "userPrefName":"workspace.showRibbon"
*      },
*      "_default":{
*          "userPrefName":"true"
*      }
*  }
* 
*  The above meta syntax will be process to be:
* 
* {
*      "label": "Show Ribbon",
*      "type": "toggle",
*      "value": {
*          "userPrefName": "workspace.showRibbon",
*          "action": {
*              "name": "USER_PREF_UPDATED",
*              "payload": {
*                  "name": "workspace.showRibbon"
*              }
*          },
*          "value": true
*      }
*   }
* 
* @param {Object} property to process.
*/

UXPagePropertyUtil.processUserPreferences = function (property) {
  var _META = '_meta';
  var _PATH = '_path';
  var _VALUE = '_value';
  var _DEFAULT = '_default';
  var _TYPE = '_type';

  function getMetaValue(property) {
      var metaProperty = property[_META];
      var metaValue = metaProperty[_VALUE];
      var defaultObject = metaProperty[_DEFAULT];
      var path = metaProperty[_PATH];
      UXPagePropertyUtil.set(metaValue, 'action.name', 'USER_PREF_UPDATED');
      UXPagePropertyUtil.set(metaValue, 'action.payload.name', metaValue[path]);
      UXPagePropertyUtil.set(metaValue, 'value', defaultObject[path] === 'true');
      if (!metaValue) {
          gs.info('getMetaValue returning null for ' + JSON.stringify(property));
          metaValue = {};
      }
      return metaValue;
  }
  if (typeof property !== 'object' || !property) return;
  Object.keys(property).forEach(function(key) {
      //gs.info('in foreach ' + key);
      var metaProperties = {};
      if (
          property.hasOwnProperty(_META) &&
          UXPagePropertyUtil.get(property, _META + '.' + _TYPE, '') === 'userPreferences'
      ) {
          metaProperties = getMetaValue(property);
      }
      Object.keys(metaProperties).forEach(function(metaKey) {
          property[metaKey] = metaProperties[metaKey];
      });
      delete property[_META];
      if (
          typeof property[key] === 'object' &&
          property[key] !== undefined &&
          property[key] !== null
      ) {
          UXPagePropertyUtil.processUserPreferences(property[key]);
      }
  });
};

UXPagePropertyUtil.prototype = {
  initialize: function () { },
  /**!
   * @name processChromeSearchConfig
   * @description By default the searchEnabled config property is in chromeHeader
   * page property (because canvasChrome uses it to conditionally render the search-input
   * slot) while the rest of the search config is in global_search_configurations.
   * 
   * When exposing search configuration to Polaris, it would be helpful to have 
   * all the searchConfiguration in one object.  This function, just looks for
   * the values of searchEnabled in chromeHeader and copies it to the search config.
   * Because chrome_header has route specific properties, we use that same structure
   * in order to capture the different route's searchEnabled property.
   * @param searchConfig
   * @param chromeHeaderConfig
   *
   * @return {{}} searchConfig
   */
  processSearch: function(searchConfig, chromeHeaderConfig){
  	var headerConfigForRoute, 
  		publicSearchEnabled, 
  		privateSearchEnabled,
          processedSearchConfig = {};
  	
      if (searchConfig && chromeHeaderConfig){
  		
          Object.keys(chromeHeaderConfig).forEach(function(headerKey){
              if (chromeHeaderConfig.hasOwnProperty(headerKey)) {
                  
  				headerConfigForRoute = UXPagePropertyUtil.get(chromeHeaderConfig, headerKey, {});
                  publicSearchEnabled = UXPagePropertyUtil.get(headerConfigForRoute, 'publicPage.searchEnabled', false);
                  privateSearchEnabled = UXPagePropertyUtil.get(headerConfigForRoute, 'privatePage.searchEnabled', false);
  				
  				var newAuthSpecificSearchEnabled = {
                                                     "publicPage": { "searchEnabled": publicSearchEnabled }, 
                                                     "privatePage": { "searchEnabled": privateSearchEnabled }
                                                     };
  				var authSpecificSearchConfig = UXPagePropertyUtil.get(searchConfig, headerKey, UXPagePropertyUtil.get(searchConfig, 'global', {}));
  				
  				var mergedSearchConfig = UXPagePropertyUtil.deepMerge(authSpecificSearchConfig, newAuthSpecificSearchEnabled);
  				UXPagePropertyUtil.set(processedSearchConfig, headerKey , mergedSearchConfig);
              }
          });
      }
      return Object.keys(processedSearchConfig).length > 0 ? processedSearchConfig : searchConfig;
  },
  /**!
    * @name processChromeTab
    * @description Processes the chrome_tab property configuration to determine
    *  - primary tab information using the chrome toolbar and
    *  - new tab menu items based on the current user access to create records for the configured table
    *
    * @param chromeToolbar
    * @param chromeTab
    *
    * @return {{}} chromeTabConfig
    */
  processChromeTab: function (chromeToolbar, chromeTab) {
      var newTabMenu,
          chromeTabConfig = UXPagePropertyUtil.deepMerge({}, chromeTab),
          i,
          key;

      // chromeToolbar property can't be null
      if (!chromeToolbar && typeof chromeToolbar !== 'object') {
          gs.error("Error: invalid input params:: chromeToolbar = " + chromeToolbar);
      }

      // chromeTab property can't be null
      if (!chromeTab && typeof chromeToolbar !== 'object') {
          gs.error("Error: invalid input params:: chromeTab = " + chromeTab);
      }

      // process chrome tab configuration for each global and route specific (if exists)
      for (i = 0; i < Object.keys(chromeTab).length; i++) {
          key = Object.keys(chromeTabConfig)[i];
          newTabMenu = {};

          // filter chrome tab's new tab menu items based on the current user
          // access to create records for the configured table
          newTabMenu = UXPagePropertyUtil.get(chromeTabConfig[key], 'newTabMenu', [])
              .filter(UXPagePropertyUtil.applyConditions);

          chromeTabConfig[key]['newTabMenu'] = newTabMenu;

          // update primary tab information using the chrome toolbar and
          chromeTabConfig[key]['primaryRoute'] = chromeToolbar[key]
              .filter(function (btn) {
                  return !!UXPagePropertyUtil.get(btn, 'routeInfo.route');
              })
              .reduce(UXPagePropertyUtil.reducePrimary, {});

      }

      return chromeTabConfig;
  },
  /**!
    * @name processChromeToolbar
    * @description Processes the chrome_toolbar property configuration to determine
    *  - sort the chrome toolbar buttons based on the order property value
    *
    * @param chromeToolbar
    *
    * @return {{}} newChromeToolbar
    */
  processChromeToolbar: function(chromeToolbar) {
      var newChromeToolbar = {},
          i,
          key,
          compareFn = function(a, b) {
              if (a.order < b.order) {
                  return -1;
              }
              if (a.order > b.order) {
                  return 1;
              }
              return 0;
          },
  		makeIdLowerCase = function(id) {
  			if (typeof id !== 'string') {
  				gs.error("Error: id must be string :: id = " + id);
  				return id;
  			}

  			return id.toLowerCase();
  		};

      // process chrome tab configuration for each global and route specific (if exists)
      for (i = 0; i < Object.keys(chromeToolbar).length; i++) {
          key = Object.keys(chromeToolbar)[i];
          newChromeToolbar[key] = chromeToolbar[key]
  			.filter(UXPagePropertyUtil.applyAvailability)
  			.sort(compareFn)
  			.map(function(ct) {
                  var toolbarObj = {};
  				Object.keys(ct).forEach(function(prop) {
                      if (prop === 'id') {
  						toolbarObj[prop] = makeIdLowerCase(ct[prop]);
                      } else {
  						toolbarObj[prop] = ct[prop];
  					}
  				});
  				return toolbarObj;
              });
      }
      return newChromeToolbar;
  },
    /**!
   * @name processChromeHeader
   * @description Processes the chrome_header property configuration to update.
   *  - configuration items sensitive to authorization routes
   *  - configuration items that require a certain role
   *
   * @param chromeHeader
   * @param authRoutes
   *
   * @return {{}} chromeHeaderConfig
   */
  processChromeHeader: function (chromeHeader, authRoutes) {
      var newChromeHeader = UXPagePropertyUtil.deepMerge({}, chromeHeader),
          key, 
          i = 0;
      // chromeHeader property can't be null
      if (!chromeHeader && typeof chromeHeader !== 'object') {
          gs.error("Error: invalid input params:: chromeHeader = " + chromeHeader);
      }
      // authRoutes property can't be null
      if (!authRoutes && typeof authRoutes !== 'object') {
          gs.error("Error: invalid input params:: authRoutes = " + authRoutes);
      }
      var filterOutUserRoles = function(property) {
          var hasRole = true;
          if (!Array.isArray(property)) return property;
          return property.filter(function (item) {
              var requiredConditionRoles = UXPagePropertyUtil.get(item, 'condition.roles', []);
              var requiredRoles = UXPagePropertyUtil.get(item, '_roles', []);
              hasRole = (
                  UXPagePropertyUtil.hasRoles(requiredConditionRoles) && UXPagePropertyUtil.hasRoles(requiredRoles)
              );
              return hasRole;
          });
      };
      /**
       * Checks if the the user has the roles to view the link to the UI Builder configuration.
       * There are additional ACL checks on the server side to make sure that the user has access to the page.
       * 
       * @param {Object} property 
       */
      var evaluateScreenLinkConfigurationAgainstRole = function(property) {
          var configurationItems = UXPagePropertyUtil.get(property, 'privatePage.currentScreenLinkConfiguration', {});
          if (configurationItems.roles) {
              if (!UXPagePropertyUtil.hasRoles(configurationItems.roles))
                  property.privatePage.currentScreenLinkConfiguration = {};
              else {
                  delete property.privatePage.currentScreenLinkConfiguration['roles'];
              }
          }
      };
      /**
       * Uses BFS strategy to evaluate the chrome header to contain items based on role, and also resolves
       * any "AUTH_ROUTE_BINDING"(s) along the way.
       * 
       * @param {Object} property
       */       
      var evaluateChromeHeader = function(property) {
          var queue = [property],
              current,
              isObjectOrArray = function(prop) {
                  var stringifiedProp = Object.prototype.toString.call(prop);
                  return stringifiedProp === '[object Object]' || stringifiedProp === '[object Array]';
              };
          while (queue.length > 0) {
              current = queue.shift();
              Object.keys(current).forEach(function (key) {
                  current[key] = filterOutUserRoles(current[key]);
                  if (UXPagePropertyUtil.isAuthBinding(current[key])) {
                      current[key] = UXPagePropertyUtil.getRouteFromAuthValue(current[key], authRoutes.auth_routes);
                  }
                  if (isObjectOrArray(current[key])) {
                      queue.push(current[key]);
                  }
              });
          }
      };
      for (i = 0; i < Object.keys(newChromeHeader).length; i++) {
          key = Object.keys(newChromeHeader)[i];
          evaluateChromeHeader(newChromeHeader[key]);
          evaluateScreenLinkConfigurationAgainstRole(newChromeHeader[key]);
          UXPagePropertyUtil.processUserPreferences(newChromeHeader[key]);
      }
      return newChromeHeader;
  },
  /**!
   * @name processChromeMenu
   * @description Processes the chrome_menu property configuration to filter based on conditions.
   * If an item has a condition property, it shall be an object with this structure: {roles: [], plugin: ''}.
   * If the current user does NOT have one of the roles OR if the plugin is not active, then we remove that item.
   * If no condition property exists, then we always display that item.
   * 
   * @param chromeMenu
   *
   * @return {{}} newChromeMenu
   */
  processChromeMenu: function(chromeMenu) {
      var newChromeMenu = UXPagePropertyUtil.deepMerge({}, chromeMenu),
          chromeMenuKeys = Object.keys(newChromeMenu),
          i,
          routeKey,
          menuItems;
          
      // chromeMenu property can't be null
      if (!chromeMenu && typeof chromeMenu !== 'object') {
          gs.error("Error: invalid input params:: chromeMenu = " + chromeMenu);
      }

      var hasRoleOrIsPluginActive = function(item) {
          var hasPluginActivated = true;
          var hasRequiredRoles = true;
          //plugin check
          var plugin = UXPagePropertyUtil.get(item, 'condition.plugin', "");
          hasPluginActivated = UXPagePropertyUtil.isPluginActive(plugin);

          // Role check
          var requiredRoles = UXPagePropertyUtil.get(item, 'condition.roles', []);
          hasRequiredRoles = UXPagePropertyUtil.hasRoles(requiredRoles);

          return hasPluginActivated && hasRequiredRoles;
      };

      var evaluateMenu = function(menu) {
          var queue = [];
          for (var i = 0; i < menu.length; i++) {
              queue.push(menu[i]);
          }
          while (queue.length > 0) {
              var current = queue.pop();
              var parent = UXPagePropertyUtil.get(current, 'children', {});
              var childItems = UXPagePropertyUtil.get(current,'children.items', []);
              // If no children then continue with other nodes
              if (childItems.length === 0) continue;
              
              var node = childItems.filter(hasRoleOrIsPluginActive);
              parent.items = node;
              for (var k = 0; k < node.length; k++) {
                  queue.push(node[k]);
              }
          }
          return menu;
      };

      for (i = 0; i < chromeMenuKeys.length; i++) {
          routeKey = chromeMenuKeys[i];
          menuItems = newChromeMenu[routeKey].filter(hasRoleOrIsPluginActive);
          newChromeMenu[routeKey] = evaluateMenu(menuItems);
      }
      
      return newChromeMenu;
  },

  type: 'UXPagePropertyUtil'
};

Sys ID

bcddfb76534e90105a20ddeeff7b1281

Offical Documentation

Official Docs: