Oracle APEX, get your act together!

Don’t get me wrong, I have worked with Oracle APEX for many years now with satisfaction. But in my humble opinion they are moving us as developers to an unsustainable situation.
Since the beginning the focus of developing APEX applications has been on the single developer, single application, multiple users paradigm. So we build applications… first in our free time, later perhaps for a customer. Then we paired up with some colleges, formed several teams and now we are looking at a landscape of a lot of applications; some of them very large with a lot of pages.

So now we have large operations with one or many APEX applications, having to manage bug fixes and build new functionality, preferably in a continuous integration life cycle.
And here is where APEX falls short in my opinion.

The focus of APEX deployment has always been on full application export and still is. Even sqlcl has a APEX EXPORT command, but no APEX EXPORT PAGE command (as of this day, please correct me if I am wrong). Sure, you kan export a single part of an application like a theme or a page, but please do this exercise for me:

1) create in a local APEX environment an application
2) Export the application
3) Import the application in e.g. apex.oracle.com

All works well, but now:

4) export locally a single page of the application
5) try to import the page in apex.oracle.com

It won’t work (exported in another workspace-id etc.).

Now the savvy guys will tell you that you have to run som APEX installation packaged procedures and that you have to set workspace-id and the offset before importing the page export,
but when you come to think of it this is ridiculous. The APEX application import succeeds, the partial page import fails.
Try getting in your next sprint the requirements that when a user wants to use part X of your application, she first has to align with some internal keys… nonsense.

Today we are facing a development environment where alterations to the application can’t be managed when faced with more than say 5 developers. Even when you lock a page you’re still
able to export the application. Even when someone is working in it. A bugfix on page x that isn’t done yet is able to be present in an export. To capture these situations you have to rely on the test cycles afterwards, but the framework should help us preventing these situations.

Make the page lock more restrictive, make the export/import process work without the nonsense of workspace-ids and offsets.

Let’s take APEX development further and strive for a page-per-page low-code development cycle.

please!

Advertisements

Changing the status of an Oracle Apex application with pl/sql

I needed this. I needed this badly. And for once the Oracle Apex forum gave me the answer for my need..

create or replace procedure set_application_status
  ( p_application in apex_applications.application_id%type
  , p_status      in varchar2
  )
is
begin
  if p_status not in ( 'AVAILABLE'        , 'AVAILABLE_W_EDIT_LINK', 'DEVELOPER_ONLY'
                     , 'RESTRICTED_ACCESS', 'UNAVAILABLE'          , 'UNAVAILABLE_PLSQL'
	             , 'UNAVAILABLE_URL')
  then
    raise_application_error(-20000, 'Status '||p_status||' is not supported');
  end if;
  --
  for r_i in ( select app.workspace_id
               ,      app.application_id
               from   apex_applications app
               where  app.application_id = p_application
	     )
  loop
    wwv_flow_api.set_security_group_id
      ( p_security_group_id => r_i.workspace_id
      );
    wwv_flow_api.set_flow_status
      ( p_flow_id     => r_i.application_id
      , p_flow_status => p_status
      );
    commit;
  end loop;
end;

Component view in Apex 5.1

So I’m a dinosaur. I don’t care if I’m not one of the cool kids. I like component view, I started with component view, I’m comfortable in component view and I get the “flow” of component view.
Of course I’m using page designer, but when things get “buggy”, I switch in dinosaur-modus.

And than there is Apex 5.1….
Where is my component view?

First, there is Two pane mode
1_switch_to_two_pane
For me that is a big step in the right direction. I’m not always in the position to ask for a 24″ monitor for development. So, less is more in this case.
And look
2_two_pane_layout
Component view!

But it’s not the same. This Component View imposter doesn’t give me the original page for e.g. a region. It jumps to the right section of the page-designer.
Not fair! That’s cheating! …. given that it is also very handy and perhaps my new MO.

But look in the upper right corner.
3_goto_preferences
Here you can set some developer specific preferences, like

4_enable_component

This will change the original panel to
4_legacy_button

Legacy, deprecated, whatever. It’s still there.

Like I said, I will be using Two Pane Mode as default with component view as my prefered way of developing (it could be nice if I could set this somewhere in my preferences),
but it’s comforting to know I can still switch “back”.

APEX 5 – The title of a modal dialog

Is it bug or is it a feature? You tell me.

When you create a modal page in Apex 5, the title of the dialog is created before any change to session state. Dynamic titles, based on session state will therefore be funky.

Take this example.

1. Create a new page
2. Select as page type Form > Form on a Table with Report
3. next, next … until the Form page and set the property of the page mode to “Modal Dialog”
4. etc ect, finish

This creates a multirecord report with edit links.
When you click an edit link a dialog appears with the default title “Form on DEPT” (let’s say we had chosen DEPT as table).

But now I want to include the DEPTNO of the record in title. Set the property Title e.g. to “DEPT &P2_DEPTNO.”

Go to the report, click a record…. the deptno in the title is empty…
Go to the report, click another record… the title is not the deptno of the record you clicked, it is the former deptno.

Like I’ve said, is it a bug or a feature?

What happens:

The Apex team implemented the dialog page as an iframe within a jQuery dialog.
The links that are generated in the report look something like:

javascript:apex.navigation.dialog('f?p=x:x:x::NO::P1_DEPTNO:x
,{title:'DEPT ',height:'500',width:'720',maxWidth:'960',modal:true,dialog:null}
,'t-Dialog--standard',apex.jQuery('#Rx'));

And that is the problem in a nutshell. When the links in the report is generated, a JavaScript object is also generated for the dialog with e.g. the title as a property. This title is based on the current session state. However, the first argument, the link for the iframe, will alter session state to a new value. But that value is not propagated to the title of the dialog.

How to alter (solve?) this behaviour?

On the global page 0 I have created a Dynamic action, on Page load, executing the following JavaScript:

(function($){
   if ( $("body").hasClass("t-Dialog-page")) {
     $(".ui-dialog-title", parent.document).html($("title").html());
   }
})(jQuery); 

When in a dialog page, take the title of the page and replace the title of the parenting dialog.

The only issue I have with this solution is that sometimes you can see the title change. But still, it is better that nothing.

Mail a screenprint in Oracle APEX

We always build bug-free applications (hehe), but in the extremely rare occasion that the user encounters a bug, they always seem to use words like “it doesn’t” work and nothing more than that.

Well, I like to have more information, so on our site we wanted the user to be able to send some background info and a screen print from within the system.

Personally I like the Team Development framework but for our purpose we needed some lightweight solution. And sending a screen print should be cool, right?

Well, most of the heavy lifting of creating a screen print is done by the library HTML2Canvas

I included an “I want to report a problem” link in the navigation bar entries with an URL-target

  javascript:getScreen();

The javascript function that is called is the following:

function getScreen() {
  if (!!document.createElement("canvas").getContext) {
    html2canvas(window.document.body, {
      onrendered: function(canvas) {
      var dataUrl = canvas.toDataURL("image/jpeg");
      var clobObj = new apex.ajax.clob(
        function(p){
          if (p.readyState == 4){
            var get = new htmldb_Get(null,$v('pFlowId')
                     ,'APPLICATION_PROCESS=mailScreen',0);
            gReturn = get.get();
            alert('Mail has been send');
          }
        });
        clobObj._set(dataUrl );      
      }
    });
  } else {
    var get = new htmldb_Get(null,$v('pFlowId')
              ,'APPLICATION_PROCESS=mailScreen',0);
    gReturn = get.get();
    alert('Mail has been send');
  }  
}

Notice that I check if canvas is supported by the browser. If it isn’t supported we’re not able to create a screenshot and we just send some background information. When canvas is enabled I put the output string in the CLOB_CONTENT-collection.

On our site we are sending the mail to our customer to let them be able to give some more background information. But you could of course also navigate to a new page for that or just email the information directly to you.

The On Demand process handles the mailing part.

declare
  cursor c_variables
  is
    select item_name||' : '
    || apex_util.get_session_state(item_name) AS session_value
    from   apex_application_page_items
    where  application_id = :APP_ID
    and    page_id        = :G_CURRENT_PAGE
  UNION ALL
    select item_name||' : '
    || apex_util.get_session_state(item_name) 
    from   apex_application_items
    where  application_id = :APP_ID
  ;               
  l_id   number;
  l_blob blob;
  l_clob clob;
  l_body clob;
BEGIN
  l_body := to_clob(:APP_USER||', please give some more information before sending it to us'||utl_tcp.crlf);
  l_body := l_body||utl_tcp.crlf;  
  for r_variable in c_variables
  loop
    if ( c_variables%ROWCOUNT = 1 ) 
    then
      l_body := l_body||'Page variables in session state: '||utl_tcp.crlf;  
    end if;
    l_body:= l_body||r_variable.session_value||utl_tcp.crlf;
  end loop;
  --
  /* Add the screen print */
  FOR r_coll IN 
    ( SELECT coll.clob001
      FROM   apex_collections coll
      WHERE  coll.collection_name = 'CLOB_CONTENT'
    ) 
  loop
    dbms_lob.createtemporary(l_clob,false);
    dbms_lob.copy
      ( dest_lob   => l_clob
      , src_lob    => r_coll.clob001
      , amount     => dbms_lob.getlength(r_coll.clob001)
      , src_offset => dbms_lob.instr(r_coll.clob001,',',1,1)+1
      );   
    l_blob := apex_web_service.clobbase642blob(l_clob);
  end loop;
  --
  l_id:= apex_mail.send
      ( p_to  => :app_user||'@mydomain.nl'
      , p_from => 'myapp@mydomain.nl'
      , p_subj => 'Issue for application '
                 ||:APP_ID||' Page '||:APP_PAGE_ID
      , p_body => l_body
      );   
  if dbms_lob.getlength(l_blob) > 0
  then
    apex_mail.add_attachment
     ( p_mail_id    => l_id
     , p_attachment => l_blob
     , p_filename   => 'screenprint.jpg'
     , p_mime_type  => 'image/jpeg'
     );
  end if;
  --
  apex_mail.push_queue;
end;

And again, that’s it!

Oracle Apex Gantt collapse all tasks

When implementing the project Gantt chart in Apex, I thought it was annoying that nested tasks default expanded.
I wanted them to be collapsed by default.

For this I added a listener to the draw event of the AnyGantt object to collapse the Gantt chart:

  AnyGantt._charts.chart__0.addEventListener('draw',function(){
    AnyGantt._charts.chart__0.collapseAll();
  });

This snippet should be executed on load (Dynamic Action, Page JavaScript property, whatever).

In your case, chart__0 could be chart__N. You have to figure it out in your case.

Just a small blog post, but it could be handy at times.

Building a chrome extension for Oracle APEX Builder

As a developer I like to create my own tools to make my work easier. In our current Oracle APEX project we have build a custom authorization mechanism, based on page numbers. A drawback the development team faced was that we had to register every single page in our custom authorization tables. So I came to think that if I could extend the APEX builder with some custom code on the page builder page, it would make our life a lot easier. When working on a new page we could register the page with a simple click.

The technique I describe in this blog can be used to extract information from the APEX Builder and call some stored procedure in the database to do some magic with it.

I wanted to build an extension for Chrome. With some googling I stumbled upon Chrome extensions. As always, the documentation of these Google API’s is excellent.

I find that building a very, very simple extension for Chrome is relatively easy. Of course you can always be overcomplicating things, but following the KISS principle I was able to build an extension that served my purpose.

The idea behind my extension is, that when the extension is clicked, some JavaScript is fired that “gets” the application id and page id from the current tab and sends it to an API on the database to register the page. But the last API can of course do all kinds of magic with the application and page id.

This is what I did:

Create a directory to host the files for the extension. E.g.

D:\ChromeExtensions\AddToAuth

The extension needs a manifest file with the name manifest.json.
My manifest file became

{
  "name": "Add a page to Auth",
  "version": "1.0",
  "manifest_version": 2,

  "description": "With this extension one can add a page from APEX builder to Auth",
  "icons":{"128":"db.png"},

   "browser_action": {
    "name": "Add a pagina to Auth",
    "default_icon": "db.png"
  },

  "background": { "scripts": ["background.js"] },
  "permissions": [
    "http://*/*"
  ]
}

See Formats: Manifest Files for a full description of the fields of a manifest file.

If you look at the manifest two things pop up

  • db.png This is a 128×128 png file that is shown in the browser
  • background.js This is the event page of the extension.

When the extension – a browserAction – is clicked I have to fire some JavaScript. So, the background.js would have to add a listener to the event.
background.js

chrome.browserAction.onClicked.addListener(function(tab) {
  chrome.tabs.executeScript(null, {file:"worker.js"});
});

When the browserAction is clicked, the worker.js is executed
worker.js

var APPL_XPATH = '//*[@id="apex-breadcrumbs"]/a[3]';
var PAGE_XPATH = '//*[@id="apex-breadcrumbs"]/span[4]';

var
  application,
  local,
  page,
  tabQuery,
  xhr;

function getNumber(pXpath, pItem) {

  var
    nextResult,
    lReturn,
    xpathResult;

  xpathResult = document.evaluate(pXpath, document.body, null, XPathResult.ANY_TYPE, null);
  nextResult = xpathResult.iterateNext();

  while (nextResult) {
    if (nextResult.textContent.indexOf(pItem) !== -1) {
      lReturn = parseInt(nextResult.textContent.substr(pItem.length), 10);
    }
    nextResult = xpathResult.iterateNext();
  }
  return lReturn;
}

tabQuery = location.search.split('=')[1].split(':');
local = 'http://' + location.host + '/' + location.pathname.split('/')[1] + '/';

if (tabQuery[0] === '4000') {    /* Builder   */
  if (tabQuery[1] === '4150') {  /* Edit page */
    /* 
      Get the pagenumber from the body
    */
    page = getNumber(PAGE_XPATH, 'Page');

    if (page) {
      /* 
        The page is found. Now get the applicationnumber
      */
      application = getNumber(APPL_XPATH, 'Application');
    }
    if (application && page) {
      /* 
        Both items have been found. Call the stored procedure to do some magic.
      */
      xhr = new XMLHttpRequest();
      xhr.open("GET", local + 'AUTH_API.ADD_PAGE?p_application_id=' + application + '&p_page_id=' + page, true);
      xhr.send();
      alert('application: ' + application + ' page: ' + page + ' is added');
    }
  }
}

When the query part of the URL is like 4000:4150 we know that we are on the page edit page.Then we use some XPATH on the body of the page to get the application id and page id. The XPATH expressions are stored in two constant vars. The XPATH expressions you see are working for the current APEX 4.2 version; to get these expressions is very easy using Chrome:

  • In APEX Builder, go to the page edit page.
  • In the breadcrumbs go to the Page xx part.
  • Inspect the element using the context menu in chrome.
  • In the elements tab use the context menu Copy Xpath

The last step to take is to send the two id’s to the database.
For this, I just use a HTTP-request to a stored packaged procedure AUTH_API.ADD_PAGE with two parameters p_application_id and p_page_id.

In our architecture (OHS with mod_plsl and a standard dads.conf) for the development database we are allowed to fire stored procedures this way. In our development
database the wwv_flow_epg_include_mod_local is non-restrictive:

create or replace function wwv_flow_epg_include_mod_local
  ( procedure_name in varchar2
  ) return boolean
is
begin
  return true; 
end wwv_flow_epg_include_mod_local;

The connecting user, defined in the dads.conf, -e.g. APEX_PUBLIC_USER-, has to have proper grants on the stored procedure that is being called by the HTTP-GET.

Importing the extension.
– Go to the Extra > extentions part of the menu
– Enable developmentmodus
– Use the load button to load the directory where you put all your files.

The new browserAction should show up in the browser.

That’s it. With this you can build an extension in chrome to parse a page in APEX Builder and send the information to some API in the database.