Porting an APEX Navigation Menu into another APEX Application

Porting an APEX Navigation Menu into another APEX Application

Background

You have a two applications called Blue and another called Pink. Both are on the same workspace and together they are part of a group of "Mini Apps" (Note: this is a nod to Page 7 of the Oracle APEX Lifecycle Technical Paper).

To the users Blue and Pink appear as same app, and navigation between the two are seamless. To APEX developers, they are two separate applications.

Blue and Pink however have two completely different Navigation Menu entries i.e. Blue has Blue related entries and Pink has Pink related entries.. easy right?... each click navigates around their own application's pages.

The use case is: See that Orange menu entry?... well, both Applications need to open Orange, giving the appearance of the entire Orange application being embedded in to that calling application. Why? well this is a SaaS application (read my SaaS blogs) and the Orange module is only licensed by certain tenant as a feature of Blue and Pink and uses the contexts of the calling App. The kicker is, part of illusion is that I want the menu of the calling application to port across to Orange.

How not to do it?

  1. Duplicate the entire Orange code base between the two applications Blue and Pink.. Oh and duplicate again whenever Aquamarine comes along too. If anything changes... replicate it in 3 places.

  2. Use a URL Region (i.e an IFRAME to you and me). Here is a picture of Orange embedded in to Blue.

To achieve this you need to change the embed in Frames property

You'll also need to spend hours using a custom page template that deactivates the Navigation Bar + Menu. You'll have need to deactivate the style, breadcrumb (maybe) and play with the IFRAME settings to get it "just right". When you've got it exactly how you want it.... let me know how that looks on mobile, yeah.

Menu Porting

Menu Porting is an approach that I've found and works ridiculously well. It involves the following steps.

  1. A JavaScript on the Blue/Pink application rips the Navigation menu to JSON format and stores in a page item. This is awesome as rather than trying to work out which menu entries should be displayed, we just get APEX to render it and then we rip the menu to JSON. The correct event to use is when the treeView has been drawn and is called theme42ready.

  2. We then replace the link on the Menu Entry for Orange with one that passed the JSON menu stored in a page item

  3. When we click on Orange and open up the home page a JavaScript merges the Menu of the calling Application with Orange's own menu

  4. The result is a concatenated menu, that allows the user to seamlessly navigate back to Blue/Pink (with check-sums intact), or navigate around Orange's own Navigation menu.

In the above picture we are seeing Orange's Navigation menu which has been ported across from Blue. Orange's own menu has been added as a child in the "🎫Orange" menu entry (which was ported across from the Blue menu - see early picture of the Blue menu).

Instructions

Blue/Pink Application

  1. Edit the "🎫Orange" menu entry and give it an ID of orange. This will instruct the Orange application where to nest its own menu

  2. Edit Page 0

  3. Add a P0_NAV_MENU_JSON page item (hidden or otherwise)

  4. Add a Custom theme42ready Event with the following attributes

  5. Create a Execute JavaScript Code True Action with the following code

     const targetNodeId = 'orange';
     var tree$ = $( "#t_TreeNav" );
     var nodeAdapter = tree$.treeView('getNodeAdapter');
     var parent = nodeAdapter.root();
     navMenuNodes = nodeAdapter.data.children 
    
     // Function to remove a specified attribute recursively
     function removeAttr(nodes, attributeName) {
         nodes.forEach(function (node) {
             delete node[attributeName];
             if (node.children) {
                 removeAttr(node.children, attributeName);
             }
         });
     }
    
     // Remove parent references from original nodes to avoid circular error
     removeAttr(navMenuNodes, '_parent'); 
    
     encodedMenu = encodeURIComponent(JSON.stringify(navMenuNodes));
     apex.item('P0_NAV_MENU_JSON').setValue(encodedMenu);
    
     // Find the element with the ID  and update its link
     navMenuNodes.forEach(function(item) {
         if (item.id === targetNodeId) {
             item.link = item.link.replace('home?', 'home?p0_nav_menu_json=' + encodedMenu + '&');
         }
     });
    

Orange Application

  1. Page 1 should be set to Unrestricted Page Access

  2. Edit Page 0

  3. Add a P0_NAV_MENU_JSON page item (hidden or otherwise)

  4. Add a Modify Navigation (theme42ready) Dynamic Action with the following attributes

  5. Create a Execute JavaScript Code True Action called Merge Navigation with the following code

     const receivedTargetNodeId = 'orange';
     const menUJsonPageItem = 'P0_NAV_MENU_JSON';
    
     // Function to build hierarchy recursively
     function buildHierarchy(node, data) {
         // Create a new node
         var newNode = {
             label: data.label,
             link: data.link,
             icon: data.icon || 'fa fa-file-o', // Default icon value if not provided
             current: data.current,
             children: []
         };
         node.children.push(newNode);
    
         // Recursively build children
         if (data.children) {
             data.children.forEach(child => {
                 buildHierarchy(newNode, child);
             });
         }
     }
    
     // Get tree element and node adapter
     var tree$ = $("#t_TreeNav");
     var nodeAdapter = tree$.treeView('getNodeAdapter');
     var parent = nodeAdapter.root();
    
     // Function to remove a specified attribute recursively
     function removeAttr(nodes, attributeName) {
         nodes.forEach(function (node) {
             delete node[attributeName];
             if (node.children) {
                 removeAttr(node.children, attributeName);
             }
         });
     }
    
     // Remove parent references from original nodes
     removeAttr(nodeAdapter.data.children, '_parent');
    
     // Parse and retrieve nodes to copy
     var recievedNodes = JSON.parse(apex.item(menUJsonPageItem).getValue()) || [];
    
     // Remove current from recieved nodes
     removeAttr(recievedNodes, 'current');
    
     // Find the node to append new children
     var transplantNode = recievedNodes.find(function (item) {
         return item.id === receivedTargetNodeId;
     });
    
     // Add new child nodes under the node
     if (transplantNode) {
         transplantNode.current = false;
         transplantNode.children = transplantNode.children || [];
         nodeAdapter.data.children.forEach(data => {
             buildHierarchy(transplantNode, data);
         });
     }
    
     // Replace the tree's children with the updated nodes
     nodeAdapter.data.children = recievedNodes;
     parent.children = parent.children || [];
    
     // Refresh and expand the tree
     tree$.treeView('refresh').treeView("expand");
    
     // If current item not visible, then expand a further level
     if (tree$.find('.a-TreeView-label.is-current').length == 0) {
         tree$.treeView("expandAll");
     }
    
     // Find the label element that is marked as current, then find its parent node and add classes to its row
     tree$.find('.a-TreeView-label.is-current')
         .closest('.a-TreeView-node')
         .find('.a-TreeView-row')
         .addClass('is-selected is-hover');
    
     // Default Page Focus
     $('body').focus();
    
     // Show Menu
     $('div#t_Body_nav').css('content-visibility','inherit');
    
  6. Create a Execute JavaScript Code False Action called Show Navigation with the following code

     // Show Menu
     $('div#t_Body_nav').css('content-visibility','inherit');
    
  7. Create a Static Application File called treeViewNav.css with the following code

     div#t_Body_nav {
         content-visibility: hidden;
     }
    

  8. In Shared Components > User Interface > CSS add the following File URL

     #APP_FILES#treeViewNav#MIN#.css
    

All done

That final bit, containing the CSS... this is a measure to prevent the Orange menu being shown briefly before it is merged. To the users, this doesn't look so nice, so the CSS is there to only show the menu when its been fully merged.

Those is authenticated conditions are to stop things happening on the login page.

Those conditions checking the P0 item is not null are there to allow users to log directly in to Orange, without navigating through Blue/Pink, and allow the exclusive use of the Orange menu - without any sort of merging.

You might wonder why Orange is Orange-themed? this is to clearly illustrate it being a seperate applicationto you in the blog. You may wish to port the theme style across too... this is completely possible too, yet unfortunately not covered in this blog.

Pretty smart? let me know your comments.

What's the picture? Daffodils near North Rigton. Visit Yorkshire!