<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="https://cloud.jaris.fi/ords/r/jaris/blog/pgm?request=application_process%3Drss.xsl" media="screen"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <atom:link rel="self" href="https://cloud.jaris.fi/ords/r/jaris/blog/pgm?request=application_process%3Drss.xml" type="application/rss+xml"/>
    <title>Jari&apos;s APEX Blog</title>
    <link>https://cloud.jaris.fi/ords/r/jaris/blog/home</link>
    <description>About Oracle Application Express (APEX) and Related Stuff</description>
    <language>en</language>
    <item>
      <title>Revisiting Interactive Report Control Breaks</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20260926175725069466</link>
      <description>Revisiting a 2022 Interactive Report control break solution and turning it into a reusable Oracle APEX Dynamic Action plug-in with remembered state, accessibility, and a small JavaScript API.</description>
      <content:encoded><![CDATA[<div class="ck-content"><div class="raw-html-embed">
                <img class="blog-post--banner" src="https://static.jaris.fi/b/i/P20260926175725069466/banner.png">
            
    </div><p>In 2022 I wrote about adding expand and collapse controls to Interactive Report control breaks with a Dynamic Action and a small amount of JavaScript. Interactive Grid already had similar functionality, while Interactive Report did not, so the solution simply added a button to each control break header and toggled the rows belonging to that group. A few years later I revisited the code with Oracle APEX 26.1 and decided that, rather than publishing another updated JavaScript snippet, it made more sense to turn the idea into a reusable Dynamic Action plug-in.</p><p>The original post is still a useful example of how Interactive Report control breaks can be manipulated on the client side: <a href="https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20220406135639638575">Collapse Interactive Report Control Break</a></p><p>This post looks at what changed when turning that small solution into a proper plug-in.</p><h3>The Original Approach</h3><p>The original implementation was intentionally simple.</p><p>A Dynamic Action was attached to the Interactive Report After Refresh event, and the JavaScript it executed searched for control break headers using:</p><pre><code class="language-css">th.a-IRR-header--group</code></pre><p>For each control break header, the code added a button. Clicking the button found the following report rows up to the next control break header and toggled their visibility.</p><p>Conceptually, the important part was only this:</p><pre><code class="language-js">this$
  .closest("tr")
  .nextUntil(":has(" + lCtrBreak + ")")
  .toggle();</code></pre><p>That remains the basic idea in the new implementation.</p><p>The old code also needed to run after every Interactive Report refresh because APEX replaces the generated report markup when the report is refreshed.</p><h3>Why Revisit It?</h3><p>For a single page, the original snippet is perfectly reasonable.</p><p>Once the same behavior is needed in several places, however, more questions appear:</p><ul><li>Should groups initially be expanded or collapsed?</li><li>What happens after the report refreshes?</li><li>Should a user's choices be remembered?</li><li>How should multiple reports on the same page be handled?</li><li>Can application developers expand or collapse all groups programmatically?</li><li>How should the generated controls expose their state to assistive technology?</li><li>How can another Dynamic Action react when a control break changes?</li></ul><p>One question on the original post was also how to have the groups initially collapsed rather than expanded. The original solution could do that with a small code change, but configuration is a better fit once the behavior becomes reusable.</p><p>Those were good reasons to move the implementation out of Dynamic Action JavaScript and into a plug-in.</p><h3>IR Control Break Toggler</h3><p>The result is IR Control Break Toggler, a Dynamic Action plug-in. The basic page setup is still very similar to the original post.</p><p>Create a Dynamic Action on the Interactive Report:</p><ul><li><strong>Event:</strong> After Refresh</li><li><strong>Selection Type:</strong> Region</li><li><strong>Region:</strong> <i>&lt;Your Interactive Report&gt;</i></li></ul><div class="raw-html-embed">
                <img src="https://static.jaris.fi/b/i/P20260926175725069466/control-break-toggler-dynamic-action.png" alt="Dynamic Action" width="1089" height="497">
            
    </div><p>Then add a True Action:</p><ul><li><strong>Action:</strong> IR Control Break Toggler [Plug-In]</li><li><strong>Fire on Initialization:</strong> Yes</li></ul><div class="raw-html-embed">
                <img src="https://static.jaris.fi/b/i/P20260926175725069466/control-break-toggler-action.png" alt="Dynamic Action True Action" width="1093" height="522">
            
    </div><p>There is no JavaScript to copy into the Dynamic Action anymore.</p><p>The plug-in receives the report that triggered the Dynamic Action and initializes the control break buttons for that region.</p><h3>Remembering Control Break State</h3><p>One of the larger differences from the original implementation is that the state of individual groups can now optionally be remembered.</p><p>The plug-in can use APEX scoped browser storage for either the current browser session or persistent browser storage.</p><p>This means a user can collapse a particular group, refresh the Interactive Report, and have that group remain collapsed.</p><p>The state is kept separate for each application, page, and report region.</p><p>I also wanted changing the plug-in configuration to behave predictably. For that reason, the configured initial state is stored as metadata with the remembered state. If Initially Expanded is later changed, previously remembered group states are discarded and the new configuration takes effect.</p><p>One detail that turned out to matter was group identity. APEX-generated control break header IDs can be reused when the report is paginated, so they cannot safely be used as persistent state keys. The plug-in therefore uses a separate key for remembered group state instead of relying on the DOM IDs of the currently rendered report.</p><h3>Button Position and Direction</h3><p>The original example always placed the button before the control break header text.</p><p>The plug-in can now place it at either the logical Start or End of the control break header.</p><p>Using logical positions instead of left and right also makes the behavior work with right-to-left layouts.</p><p>There is another small detail here: the default expand chevron changes direction depending on where the button is positioned.</p><p>For example, in a left-to-right layout:</p><ul><li>START → fa-chevron-right</li><li>END → fa-chevron-left</li></ul><p>The icon therefore points toward the content rather than always pointing in the same physical direction.</p><p>An explicitly configured Expand Icon still takes precedence.</p><h3>Accessibility</h3><p>The first version already used a native button and maintained <code>aria-expanded</code> and an accessible label.</p><p>For the plug-in I extended this by adding <code>aria-controls</code>.</p><p>Each toggle button references the report rows it controls:</p><pre><code class="language-html">&lt;button
  type="button"
  aria-expanded="true"
  aria-controls="row_1 row_2"
  aria-label="Collapse"&gt;
  ...
&lt;/button&gt;</code></pre><p>Interactive Report data rows do not always have IDs suitable for this relationship, so the plug-in assigns IDs where needed while preserving any existing IDs.</p><p>The icon itself remains decorative with:</p><pre><code class="language-html">aria-hidden="true"</code></pre><h3>A Small Public API</h3><p>One limitation of page-level Dynamic Action JavaScript is that it becomes awkward to control from elsewhere in the application.</p><p>The plug-in exposes a small API:</p><pre><code class="language-js">fi_jaris_plugin.ir.controlBreakToggler.expandAll(region);
fi_jaris_plugin.ir.controlBreakToggler.collapseAll(region);
fi_jaris_plugin.ir.controlBreakToggler.resetState(region);</code></pre><p>Each method intentionally controls one Interactive Report region.</p><p>With the <strong>Button Trigger Action</strong> introduced in APEX 26.1, this works particularly well because the selected region can be passed directly to the API.</p><p>For example, a Collapse All button can use:</p><ul><li><strong>Triggered Action:</strong> Execute JavaScript Code</li><li><strong>Selection Type:</strong> Region</li><li><strong>Region:</strong> <i>&lt;Your Interactive Report&gt;</i></li><li><strong>Code:</strong></li></ul><pre><code class="language-js">fi_jaris_plugin.ir.controlBreakToggler.collapseAll(
  this.affectedElements
);</code></pre><p>There is no need to hard-code the report Static ID into the JavaScript.</p><p>An Expand All button uses the same setup, but with this JavaScript code:</p><pre><code class="language-js">fi_jaris_plugin.ir.controlBreakToggler.expandAll(
  this.affectedElements
);</code></pre><div class="raw-html-embed">
                <img src="https://static.jaris.fi/b/i/P20260926175725069466/expand-button-action.png" alt="Button Action" width="1089" height="560">
            
    </div><h3>Reacting to Changes</h3><p>The plug-in also exposes a Dynamic Action event:</p><pre><code class="language-plaintext">Control Break Change [IR Control Break Toggler]</code></pre><p>This means another Dynamic Action can react when a user expands or collapses a group, without manually wiring up a JavaScript event listener.</p><p>The same event is raised when the public API changes the state.</p><p>Internally the JavaScript event is:</p><pre><code class="language-js">ircontrolbreakchange</code></pre><p>and its event data includes the report region, affected control break header IDs, resulting state, and the source of the change.</p><p>For bulk operations such as Expand All or Collapse All, one event is raised after the operation if one or more groups changed state, rather than one event for every control break header.</p><h3>The Interesting Part Hasn't Changed</h3><p>Despite the extra functionality, the central technique is still the one from the 2022 post. Find the control break header:</p><pre><code class="language-css">th.a-IRR-header--group</code></pre><p>and consider the rows after its <code>&lt;tr&gt;</code> up to the next control break header to be the rows belonging to that group.</p><p>Everything else in the plug-in is mostly about making that behavior reusable:</p><ul><li>configuration,</li><li>refresh handling,</li><li>state management,</li><li>accessibility,</li><li>application integration,</li><li>and avoiding duplicate initialization.</li></ul><p>That is usually how small APEX utilities evolve. The first version proves that the idea works; the reusable version is mostly about dealing cleanly with everything around that idea.</p><h3>Source Code</h3><p>The plug-in source, installable APEX export, and documentation are available on GitHub:<br><a href="https://github.com/jariolaine/apex-ir-control-break-toggler" target="_blank">https://github.com/jariolaine/apex-ir-control-break-toggler</a></p><p>The repository also contains the unminified JavaScript, CSS, render procedure, and plug-in export, so the implementation can be inspected or modified without having to extract files from the APEX plug-in.</p><p>See a working example on <a href="https://oracleapex.com/ords/f?p=39006:68" target="_blank">oracleapex.com</a>.</p></div>]]></content:encoded>
      <pubDate>Sun, 27 Sep 2026 18:34:00 GMT</pubDate>
      <guid isPermaLink="false">20260926175725069466</guid>
    </item>
    <item>
      <title>Closing Non-Chained Modal Drawers Using Breadcrumbs in APEX</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20260823072549461470</link>
      <description>This blog post demonstrates how to improve user navigation in APEX by allowing users to close multiple non-chained modal drawers at once. By combining custom breadcrumb URL targets with page-level JavaScript and &quot;Dialog Close&quot; Dynamic Actions, developers can pass parameters back through the drawer hierarchy to close previous steps seamlessly, significantly reducing unnecessary clicks for the user.</description>
      <content:encoded><![CDATA[<div class="ck-content"><div class="raw-html-embed"><img class="blog-post--banner" src="https://static.jaris.fi/b/i/P20260823072549461470/banner.png"></div><p>When building drill down interfaces in Oracle APEX, non-chained modal drawers are one way to display hierarchical data. However, requiring users to manually close each drawer one by one to return to a previous step can lead to unnecessary clicks. Here is one approach to using custom URL actions and breadcrumbs to close multiple non-chained drawer pages at once.</p><p>In my example, I have 4 drawer pages to drill down into. Because the drawers are not chained, each new drawer opens on top of the previous one. If a user drills down to the last drawer, they can easily close the previous drawers directly from the breadcrumb.</p><p>You can use an existing breadcrumb for this, but in my example, I created a totally new breadcrumb just for my drawers.</p><h3>Creating the Breadcrumb</h3><p>Navigate to <i>Application Shared Components -&gt; Breadcrumbs</i> and click <i>Create Breadcrumb</i>.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/breadcrumbs-1.png" alt="APEX Application Breadcrumbs" width="939" height="400"></p><p>Give a name to the breadcrumb and click <i>Create</i>.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/create-breadcrumb.png" alt="Create Breadcrumb" width="829" height="443"></p><p>Click <i>New Entry</i>.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/create-breadcrumb-entry.png" alt="Create Breadcrumb Entry" width="1181" height="423"></p><ul><li>Set <strong>Page</strong> to the page number where you open the first drawer page.</li><li>Give it a <strong>Short Name</strong>.</li><li>Change <strong>Target is a</strong> to <i>URL</i>.</li><li>Enter the following into <strong>URL Target</strong>:</li></ul><pre><code class="language-js">&#35;action$go-to-home?close-step-4&amp;close-step-3=true&amp;close-step-2=true&amp;close-step-1=true</code></pre><h4>URL Target Breakdown:</h4><ul><li><strong>&#35;action$</strong> : This prefix indicates the action is triggered via the URL.</li><li><strong>go-to-home</strong> : This is the name of the action being triggered. This needs to be different for each breadcrumb entry.</li><li><strong>?close-step-4&amp;close-step-3=true...</strong> : These are URL parameters passed along with the action.<br>The parameter values do not matter, parameter names indicate which drawers need to be closed. It might be clearer to use parameter names like <code>close-page-&lt;page number&gt;=true</code>.</li></ul><p>Click <i>Create and Create Another</i>.</p><h3>Adding Child Breadcrumb Entries</h3><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/breadcrumb-entry-1.png" alt="Creating First Breadcrumb Entry" width="1170" height="594"></p><ul><li>Set <strong>Page</strong> to the page number of the first drawer page.</li><li>For <strong>Parent Entry</strong>, select the breadcrumb entry created previously.</li><li>Give it a <strong>Short Name</strong>.</li><li>Change <strong>Target is a</strong> to <i>URL</i>.</li><li>Enter the following into <strong>URL Target</strong>:</li></ul><pre><code class="language-js">&#35;action$go-to-step-1?close-step-4&amp;close-step-3=true&amp;close-step-2=true</code></pre><p>In the <strong>URL Target</strong>, give a different action name and remove the parameter name indicating the closing of the drawer page associated with this breadcrumb entry. In this example, it was <code>&amp;close-step-1=true</code>.</p><p>Click <i>Create and Create Another</i>.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/breadcrumb-entry-2.png" alt="Creating Another Breadcrumb Entry" width="1170" height="594"></p><p>Repeat the above steps similarly by setting the correct <strong>Page</strong>, <strong>Parent Entry</strong>, <strong>Short Name</strong>, and <strong>URL Target</strong>, except on the last child breadcrumb entry, set <strong>Target is a</strong> to <i>- No Target -</i>.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/breadcrumb-entry-last.png" alt="Last Breadcrumb Entry" width="1170" height="594"></p><h3>Adding the Breadcrumb Region to Drawer Pages</h3><p>Ensure the newly created custom breadcrumb is displayed on each of the drawer pages.</p><p>Create a new region in the <strong>Slot</strong> <i>Dialog Header</i> and set the <strong>Type</strong> to <i>Breadcrumb</i>.</p><p>Under the region's Source properties, select your custom <strong>Breadcrumb</strong>.</p><p>Repeat this step for all drawer pages so the user has a consistent navigation experience as they drill down.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-breadcrumb-region.png" alt="Drawer Breadcrumb Region" width="930" height="345"></p><h3>Configuring the First Drawer Page</h3><p>Edit your first drawer page and add the following JavaScript to <strong>Execute When Page Loads</strong>:</p><pre><code class="language-js">/* Register a custom action triggered by the breadcrumb URL. */
apex.actions.add([{
  name: "go-to-home",
  action: function(event, element, args) {
    /* Close this drawer and pass the URL parameters (args) to the parent page. */
    apex.navigation.dialog.close( true, args );
  }
}]);</code></pre><p>The <strong>name</strong> parameter is the breadcrumb entry URL action name. The action closes the drawer and passes the URL parameters as event data.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-page-1-javascript.png" alt="First Drawer Page Javascript - Execute When Page Loads" width="692" height="352"></p><p>Create a Dynamic Action:</p><ul><li><strong>Event:</strong> <i>Dialog Close</i></li><li><strong>Selection Type:</strong> <i>Region</i></li><li><strong>Region:</strong> <i>&lt;Select the region name where the link or button opens the next drawer page&gt;</i></li></ul><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-page-1-dynamic-action.png" alt="First Drawer Page Dynamic Action" width="913" height="573"></p><p>Edit the <i>True Event</i>:</p><ul><li><strong>Action:</strong> <i>Close Dialog</i></li><li><strong>Client-side Condition -&gt; Type:</strong> <i>JavaScript Expression</i></li><li><strong>JavaScript Expression:</strong></li></ul><pre><code class="language-js">/* Check if the closed dialog passed a 'close-step-1' parameter in event data. */
this.data.hasOwnProperty( "close-step-1" );</code></pre><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-page-1-dynamic-action-true-event.png" alt="First Drawer Page Dynamic Action Event" width="913" height="591"></p><h3>Configuring Subsequent Drawer Pages</h3><p>Edit the next drawer page and add this JavaScript to <strong>Execute When Page Loads</strong>:</p><pre><code class="language-js">apex.actions.add([{
  name: "go-to-home",
  action: function(event, element, args) {
    apex.navigation.dialog.close( true, args );
  }
},{
  name: "go-to-step-1",
  action: function(event, element, args) {
    apex.navigation.dialog.close( true, args );
  }
}]);</code></pre><p>Here we similarly add an action for the next breadcrumb entry.</p><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-page-2-javascript.png" alt="Next Drawer Page Javascript - Execute When Page Loads" width="561" height="443"></p><p>Create a Dynamic Action:</p><ul><li><strong>Event:</strong> <i>Dialog Close</i></li><li><strong>Selection Type:</strong> <i>Region</i></li><li><strong>Region:</strong> <i>&lt;Select the region name where the link or button opens the next drawer page&gt;</i></li></ul><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-page-2-dynamic-action.png" width="922" height="543"></p><p>Edit the <i>True Event</i>:</p><ul><li><strong>Action:</strong> <i>Execute JavaScript Code</i></li><li><strong>Code:</strong></li></ul><pre><code class="language-js">apex.navigation.dialog.close( true, this.data );</code></pre><ul><li><strong>Client-side Condition -&gt; Type:</strong> <i>JavaScript Expression</i></li><li><strong>JavaScript Expression:</strong></li></ul><pre><code class="language-js">this.data.hasOwnProperty( "close-step-2" );</code></pre><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-page-2-dynamic-action-true-event.png" width="922" height="714"></p><p>Repeat the above steps similarly, adding actions to the page's JavaScript <strong>Execute When Page Loads</strong> and creating corresponding dynamic actions for the other drawer pages. Note that on the very last drawer page, you do not need to create a dynamic action.</p><p>Here is how the last drawer page JavaScript <strong>Execute When Page Loads</strong> looks:</p><pre><code class="language-js">apex.actions.add([{
  name: "go-to-home",
  action: function(event, element, args) {
    apex.navigation.dialog.close( true, args );
  }
},{
  name: "go-to-step-1",
  action: function(event, element, args) {
    apex.navigation.dialog.close( true, args );
  }
},{
  name: "go-to-step-2",
  action: function(event, element, args) {
    apex.navigation.dialog.close( true, args );
  }
},{
  name: "go-to-step-3",
  action: function(event, element, args) {
    apex.navigation.dialog.close( true, args );
  }
}]);</code></pre><p><img src="https://static.jaris.fi/b/v2/i/P20260823072549461470/drawer-page-4-javascript.png" alt="Last Drawer Page Javascript - Execute When Page Loads" width="559" height="565"></p><h3>Conclusion</h3><p>By implementing this custom breadcrumb and dynamic action logic, you give your users a seamless way to navigate out of deep drill-down paths without endless clicking. <a href="https://oracleapex.com/ords/f?p=39006:81" target="_blank"><strong>See working example in action</strong></a>.</p></div>]]></content:encoded>
      <pubDate>Sun, 23 Aug 2026 14:41:00 GMT</pubDate>
      <guid isPermaLink="false">20260823072549461470</guid>
    </item>
    <item>
      <title>Spin Up an Oracle APEX Local Development Environment</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>Local Development</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20260809172419086332</link>
      <description>Spin up a local Oracle APEX development environment using Docker Compose. This declarative, YAML-driven approach automatically sets up APEX instance parameters and creates workspaces and users on startup, eliminating the need for manual configuration.</description>
      <content:encoded><![CDATA[<div class="ck-content"><div class="raw-html-embed"><img class="blog-post--banner" src="https://static.jaris.fi/b/i/P20260809172419086332/local_dev_env_banner.png"></div><p>If you spend any time developing with Oracle APEX, you likely already have a preferred way to run a local environment. The Oracle community has produced several excellent local setups (e.g., Docker/Podman-based options) over the years, and we are fortunate to have <a href="https://container-registry.oracle.com/" target="_blank">Oracle Container Registry</a> making this very easy.</p><p>However, everyone has their own specific preferences for how a local stack should be managed. Recently, I wanted to spin up the 26ai Free database, ORDS, and the new APEX 26.1, but I wanted to eliminate as much of the manual post-installation clicking as possible.</p><p>I ended up building my own Docker Compose environment to automate this process. I am sharing it today on GitHub as just one more option for the community. <strong>Please keep in mind that this setup is intended strictly for local development, testing, and learning purposes—it is not secured or optimized for production use.</strong> If you prefer an automated, configuration-driven approach to your local stack, this might fit nicely into your workflow.</p><h3>The Problem with Tearing Down and Rebuilding</h3><p>Local development containers are meant to be disposable. However, whenever I rebuilt a fresh local APEX development environment, I found myself repeating the same tedious steps: configuring APEX instance parameters and creating workspaces.</p><h3>The Goal: Configuration as Code</h3><p>Rather than writing extensive setup guides or manual SQL scripts, I wanted to make the environment declarative, much like the Oracle APEX low-code development platform itself.</p><p>The core philosophy of this repository is that your desired state should be defined in simple files before the database even starts.</p><p>Instead of configuring APEX through the browser or SQL scripts, you simply define what you want in YAML templates:</p><ul><li><strong>Need a new workspace?</strong> Add it to the configuration file.</li><li><strong>Need a specific developer user assigned to that workspace?</strong> Define it upfront.</li><li><strong>Need to toggle an instance-level parameter like </strong><a href="https://www.oracle.com/database/sqldeveloper/technologies/db-actions/" target="_blank"><strong>Database Actions</strong></a><strong>?</strong> Set it before you spin up the container.</li></ul><p>How does this actually work under the hood? At the highest level, <strong>Docker Compose</strong> acts as the orchestrator. It allows you to define and manage multiple interconnected containers (like the Oracle Database and ORDS) as a single unified application. Instead of running e.g. command-line scripts to link containers, Docker Compose reads your configuration files to automatically wire up the internal networks, mount persistent storage volumes, and inject the correct environment variables.</p><p>Once Docker Compose starts the environment, the database is automatically set up. Next, the ORDS container takes over to automatically install APEX into the database. After this base installation is complete, the custom scripts located in the <i>ords-entrypoint.d</i> directory take over. These scripts parse YAML configuration files and dynamically generate the corresponding SQL statements. The container then executes these generated SQL files against the database to handle the necessary schema creations, grant the required privileges, provision the APEX workspaces, and set up APEX instance parameters exactly as specified.</p><p>You can copy these generated SQL files from the ORDS container and reuse them to easily provision non-local environments, like a shared development or test database.</p><p>New schemas created by this process are REST-enabled. This unlocks the browser-based Database Actions and allows you to start developing e.g. ORDS REST modules. You don't necessarily need to install local client tools like <a href="https://www.oracle.com/database/sqldeveloper/technologies/sqlcl/" target="_blank">SQLcl</a> or <a href="https://www.oracle.com/database/sqldeveloper/" target="_blank">SQL Developer for VS Code</a> on your machine to start working—though having them is still incredibly helpful!</p><h3>Available on GitHub</h3><p>There is no shortage of great ways to set up local environments, but if you appreciate a YAML-driven, automated approach, feel free to give this one a try.</p><p><a href="https://github.com/jariolaine/local-apex-dev/tree/v1.1" target="_blank"><strong>GitHub Repository: local-apex-dev (v1.1 Release)</strong></a></p><p><i>(Note: If you are reading this in the future, be sure to check the </i><a href="https://github.com/jariolaine/local-apex-dev" target="_blank"><i>main branch</i></a><i> for the latest updates and features!)</i></p><p><i>Feedback, forks, and pull requests are always welcome!</i></p></div>]]></content:encoded>
      <pubDate>Sun, 09 Aug 2026 17:29:00 GMT</pubDate>
      <guid isPermaLink="false">20260809172419086332</guid>
    </item>
    <item>
      <title>Authenticate APEX workspace with Oracle cloud identity domain</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20250309143806936499</link>
      <description>Integrating Oracle Cloud Identity Domain with your APEX environment offers a streamlined and secure user management experience, enabling seamless single sign-on capabilities. This blog post provides a step-by-step guide to authenticating your Oracle APEX services, ensuring you harness advanced identity management features while enhancing security and user experience.</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>By integrating Oracle Cloud Identity Domain into your APEX environment, you create a robust authentication mechanism that not only streamlines user management but also enhances security. This seamless integration allows organizations to leverage advanced identity management features, enabling secure single sign-on (SSO) capabilities while simplifying user experience. In this blog post, we will explore the steps to authenticate your Oracle APEX development and administration services with Oracle Cloud Identity Domain.</p><p>Access your Oracle Cloud tenancy and go to <i>Identity &amp; Security → Domains</i>. Choose the Default domain and make a note of the <i>Domain URL</i>. You'll need this information later when setting up the APEX authentication scheme.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/domain_overview.png"></p><p>First, we need to create an integrated application to authenticate APEX workspaces. Navigate to the <i>Integrated Applications</i> section and click the "<i>Add Application</i>" button.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/integrated_applications.png"></p><p>Select <i>Confidential Application</i> and click “<i>Launch workflow</i>” button.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/add_application.png"></p><p>Enter <i>Name</i> for application. Optionally add also <i>Description</i> and upload <i>Application Icon</i>.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/add_application_details.png"></p><p>Enter to <i>Application URL</i> “https://<i>&lt;your ORDS sever host&gt;</i>/ords/r/apex/workspace-sign-in/select-workspace” and click "<i>Next</i>" button.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/add_application_details_2.png"></p><p>Choose the option to <i>Configure this application as client now</i> and input the required details.</p><ul><li>Make sure to check the <i><strong>Authorization code</strong></i> checkbox.</li><li>For the <i><strong>Redirect URL</strong></i>, enter: <i>https://&lt;your ORDS server host&gt;/ords/apex_authentication.callback</i>.</li><li>For the <i><strong>Post-logout redirect URL</strong></i>, use: <i>https://&lt;your ORDS server host&gt;/ords/apex</i>.</li></ul><p>After that, click the "<i>Next</i>" button, and on the following page of the wizard, click the “<i>Finish</i>” button.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/configure_oauth.png"></p><p>Once the application is created, click the "<i>Activate</i>" button.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/activate_application.png"></p><p>Scroll down to <i>Resource server configuration</i> and find the <i><strong>Client ID</strong></i> and make a note of it. Then, click on <i>Show Secret</i> to reveal the <i><strong>Client Secret</strong></i> and jot it down. You will need these details later when setting up the APEX authentication scheme.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/resource_server_configuration.png"></p><p>Before configuring authentication schemes in the APEX development environment, log in to the database as an admin user and check the current value of <i><strong>APEX_BUILDER_AUTHENTICATION</strong></i> from the <i>APEX_INSTANCE_PARAMETERS</i> view. Make a note of this value in case you need to revert the authentication scheme to its original state.</p><pre><code class="language-sql">select
 name
, value
from APEX_INSTANCE_PARAMETERS
where 1 = 1
 and name = ‘APEX_BUILDER_AUTHENTICATION’
;</code></pre><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/apex_builder_authentication.png"></p><p>To authorize access to the APEX development environment and administration services, you must create users in APEX workspaces, even when using external authentication.</p><p>APEX documentation says:</p><blockquote><p><a href="https://docs.oracle.com/en/database/oracle/apex/24.2/aeadm/configuring-authentication-schemes-for-instance.html&#35;GUID-A1EFFB66-8D37-437C-9183-A230213CBDCC" target="_blank">Even for external authentication schemes (such as HTTP Header Variable), make sure that users exist as developers or administrators in your workspace. Otherwise, APEX will not be able to verify in which workspace a user is allowed to work.</a></p></blockquote><p>Log in to APEX Administration Services and go to <i>Manage Workspaces → Manage Developers and Users</i>. Create a new user in the <strong>INTERNAL</strong> workspace, using the same username as your Oracle Cloud Identity Domain account. Ensure that the <i>User is an administrator</i> option is set to <i>Yes</i>.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/new_apex_admin_user.png"></p><p>You can also create your user in other workspaces as needed or update your existing users username to match your Oracle Cloud username.</p><p>Next navigate <i>Manage Instance → Security</i> and scroll down and edit <i>Social Sign-In</i> authorization scheme.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/security_settings.png"></p><p>Enter needed information:</p><ul><li><i><strong>Client ID</strong></i>: &lt;Identity Domain application client id noted down earlier&gt;</li><li><i><strong>Client Secret</strong></i>: &lt;Identity Domain application client secret noted down earlier&gt;</li><li><i><strong>Confirm Client Secret</strong></i>: &lt;Identity Domain application client secret noted down earlier&gt;</li><li><i><strong>Authentication Provider</strong></i>: OpenID Connection Provider</li><li><i><strong>Discovery UR</strong></i>L: &lt;Domain URL you noted down earlier&gt;/.well-known/openid-configuration</li><li><i><strong>Scope</strong></i>: profile</li><li><i><strong>Username Attribute</strong></i>: sub</li><li><i><strong>Verify Username</strong></i>: Yes</li></ul><p>Then click "<i>Apply Changes</i>".</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/edit_authentication.png"></p><p>Edit again <i>Social Sign-In</i> authorization scheme and click <i>Make Current Scheme</i>.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/make_current_authorization_scheme.png"></p><p>Click “<i>OK</i>” to confirm authorization scheme change.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/confirm_authenticate_scheme_change.png"></p><p>Log out of the APEX Administration Service, then return to your APEX instance login URL.</p><p>Now you should see Oracle cloud login screen.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/oci_login.png"></p><p>Log in to see a list of workspaces you are authorized to access.</p><p><img src="https://static.jaris.fi/b/v1/i/P20250309143806936499/workspace_select.png"></p><p>If an issue arises, you can restore the APEX instance authentication scheme by logging into the database as an admin user and executing the following command. Replace <i>DB</i> with the value you obtained earlier from the <i>APEX_INSTANCE_PARAMETERS</i> view query.</p><pre><code class="language-sql">begin
 apex_instance_admin.set_parameter( 'APEX_BUILDER_AUTHENTICATION', 'DB' );
end;
/</code></pre></div>]]></content:encoded>
      <pubDate>Sun, 09 Mar 2025 17:37:00 GMT</pubDate>
      <guid isPermaLink="false">20250309143806936499</guid>
    </item>
    <item>
      <title>Report refresh interval using dynamic action</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20231119172012444607</link>
      <description>How refresh classic or interactive report without writing any line of JavaScript.</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>Here is how you can easily set report refresh interval just using dynamic action without writing any line of JavaScript.</p><p>You need set report attribute Lazy Loading to on.</p><p><img src="https://static.jaris.fi/b/v1/i/P20231119172012444607/report_attributes.png" alt="Report Lazy Loading attribute"></p><p>Create dynamic action:</p><ul><li>Type: Debounce</li><li>Time: &lt; enter time in milliseconds how often report is refreshed, e.g. 10000 for every 10 seconds &gt;</li><li>Event: After Refresh</li><li>Selection: Type: Region</li><li>Region: &lt; select report region to refresh &gt;</li></ul><p><img src="https://static.jaris.fi/b/v1/i/P20231119172012444607/dynamic_action.png" alt="Dynamic action"></p><p>Create true action</p><ul><li>Action: Refresh</li><li>Selection Type: Triggering Element</li><li>Fire on Initialization: Off</li></ul><p><img src="https://static.jaris.fi/b/v1/i/P20231119172012444607/dynamic_action_true_action.png" alt="Dynamic action true action"></p><p>See <a href="https://apex.oracle.com/pls/apex/f?p=39006:76" target="_blank">working example</a>.</p><p>If you don't want use report Lazy loading, you need write little bit JavaScript and define dynamic action bit differently:</p><ul><li>Type: Debounce</li><li>Time: &lt; enter time in milliseconds how often report is refreshed, e.g. 10000 for every 10 seconds &gt;</li><li>Event: Custom</li><li>Custom Event: custom-event-refresh</li><li>Selection: Type: Region</li><li>Region: &lt; select report region to refresh &gt;</li></ul><p><img src="https://static.jaris.fi/b/v1/i/P20231119172012444607/dynamic_action_alternative.png" alt="Dynamic action alternative"></p><p>Create true action:</p><ul><li>Action: Refresh</li><li>Selection Type: Triggering Element</li><li>Fire on Initialization: Off</li></ul><p><img src="https://static.jaris.fi/b/v1/i/P20231119172012444607/dynamic_action_true_action.png" alt="Dynamic action first true action"></p><p>Create another true action:</p><ul><li>Action: Execute JavaScript Code</li><li>Selection Type: Triggering Element</li><li>Fire on Initialization: On</li><li>Code:</li></ul><pre><code class="language-js">this.affectedElements.trigger( "custom-event-refresh" );</code></pre><p><img src="https://static.jaris.fi/b/v1/i/P20231119172012444607/dynamic_action_another_true_action.png" alt="Dynamic action second true action"></p></div>]]></content:encoded>
      <pubDate>Sun, 19 Nov 2023 18:35:00 GMT</pubDate>
      <guid isPermaLink="false">20231119172012444607</guid>
    </item>
    <item>
      <title>Drag &amp; Drop Interactive Grid rows</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20230819081331172765</link>
      <description>Arrange the Interactive Grid rows by drag &amp; drop with the mouse.</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>Here's an example that lets you drag and drop the rows of an interactive grid to arrange the records the way you want. A table used in an interactive grid must have a column that stores the sort order. If your table doesn't already have a column that can be used for this, edit the table and add a new column.</p><pre><code class="language-sql">alter table &lt;your table name&gt; add (display_seq number);
update &lt;your table name&gt; set display_seq = rownum;
alter table &lt;your table name&gt; modify display_seq not null;</code></pre><p>First, create an editable interactive grid and run the page. Sort the interactive grid in ascending order by display sequence column.</p><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/sort_ig.png"></p><p>Then hide the display sequence column from the report.</p><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/hide_display_sequence_column.png"></p><p>Save the report.</p><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/save_report_default.png"></p><p>Create new column to the interactive grid .</p><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/add_ig_column.png"></p><p>Enter a column name. Change the type to “HTML expression”. Add to the HTML Expression:</p><pre><code class="language-html language-sql">&lt;span aria-hidden="true" class="fa fa-sort z-sortable--handle"&gt;&lt;/span&gt;</code></pre><p>Change the Source Type to None. Add a Column Initialization JavaScript Function:</p><pre><code class="language-js">sortableIG.initHandleColumn</code></pre><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/drag_handle_column.png"></p><p>Edit the other interactive grid columns and disable Sorting for all columns except the display sequence column.</p><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/sort_off_from_columns.png"></p><p>Edit the display sequence column and add a Column Initialization JavaScript Function:</p><pre><code class="language-js">sortableIG.initSequenceColumn</code></pre><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/sequence_column.png"></p><p>Add to the interactive grid region Initialization JavaScript Function:</p><pre><code class="language-js">sortableIG.initRegion</code></pre><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/region_initialization_javascript_function.png">&nbsp;</p><p>Add to the page JavaScript Function and Global Variable Declaration:</p><pre><code class="language-js">// create a namespace for sortable IG
var sortableIG = {};

( function( $, region, sortableIG ) {

  // Set defaults
  sortableIG.options = {
    region: {
      defaultModelOptions: {
        sequenceStep: 10
      }
      ,defaultGridViewOptions: {
        reorderColumns: false
      }
    }
    ,handleColumn: {
      defaultGridColumnOptions: {
        noHeaderActivate: true
      }
      ,layout: {
        columnAlignment: "center"
        ,noStretch: true
      }
    }
    ,sequenceColumn: {
      features: {
      	sort: true
        ,canHide: false
        ,aggregate: false
        ,compute: false
        ,controlBreak: false
        ,groupBy: false
        ,highlight: false
        ,pivot: false
      }
    }
  }

  // sortable IG handle column Initialization code
  // call function in IG column Advanced: Column JavaScript Initialization Code
  sortableIG.initHandleColumn = function( options ) {

    return $.extend( true, options, sortableIG.options.handleColumn );

  }

  // sortable IG sequence column Initialization code
  // call function in IG column Advanced: Column JavaScript Initialization Code
  sortableIG.initSequenceColumn = function( options ) {

		// get sequence column name and store it for region Initialization JavaScript Function
    sortableIG.options.region.defaultModelOptions.sequenceField = options.name;

    return $.extend( true, options, sortableIG.options.sequenceColumn );

  }

  // sortable IG initialization code
  // call function in IG region Advanced: JavaScript Initialization Code
  sortableIG.initRegion = function( options ) {

    $( $x( options.regionStaticId ) ).on( "interactivegridviewchange interactivegridreportsettingschange", function( _event, ui ) {

      var view          = region( this.id ).call( "getViews", "grid" )
        , sortableArea$ = $( $x( this.id ) )
        , helperBgClass = "u-color-29-bg"
      ;

      // https://api.jqueryui.com/sortable/
      sortableArea$.sortable({
        handle:       ".z-sortable--handle"
        ,items:       "tr:has(.z-sortable--handle)"
        ,axis:        "y"
        ,tolerance:   "pointer"
        ,scrollSpeed: 8
        ,placeholder: "a-GV-cell u-color-1-bg"
        
        ,helper: function( _event, ui ){
          ui.children().each( function(){
            $( this ).width( $( this ).width() ).addClass( helperBgClass )
          })
          return ui;
        }
        
        ,update: function( _event, ui ){
          
          var model = view.model
            , item$ = $( ui.item )
            // get moved record
            , item = ( item$.length ) ? model.getRecord( item$.data( "id" ) ) : null
            // get next or previous row from visible table depending is record moved up or down
            , after$ = ( ui.originalPosition.top &gt; ui.position.top ) ? item$.next() : item$.prev()
            , after = ( after$.length ) ? model.getRecord( after$.data( "id" ) ) : null
          ;
          
          // remove helper background class, if row isn't actually moved
          item$.children().removeClass( helperBgClass );
          // check if we have found record and place where record is moved
          if( item &amp;&amp; after ){
            // if record is moved upwards, we need get previous record from model
            after = ( ui.originalPosition.top &gt; ui.position.top ) ? model.recordAt( model.indexOf( after ) - 1 ) : after;
            // move record
             if( item !== after ){
              model.moveRecords( [item], null, after );
            }
          }
          
        }
      
      });

    });

    return $.extend( true, options, sortableIG.options.region );

  }

})( apex.jQuery, apex.region, sortableIG );</code></pre><p>Add to the Page CSS Inline:</p><pre><code class="language-css">.z-sortable--handle{
  cursor: move;
}
.ui-sortable-helper{
  display: inline-flex;
}</code></pre><p><img src="https://static.jaris.fi/b/v1/i/P20230819081331172765/page_attributes.png"></p><p>Now when you run a page, you can drag and drop to arrange the rows of the interactive grid from the handle column.</p><p><a href="https://apex.oracle.com/pls/apex/f?p=39006:35" target="_blank">See working example in apex.oracle.com.</a></p><p>If you need support for touch events, you can try <a href="https://github.com/furf/jquery-ui-touch-punch" target="_blank">jQuery UI Touch Punch</a>. Download <a href="https://github.com/furf/jquery-ui-touch-punch/blob/master/jquery.ui.touch-punch.js" target="_blank">jquery.ui.touch-punch.js</a> and upload the file to Static Application Files. Reference the file e.g. on page JavaScript File URLs:</p><pre><code class="language-js">&#35;APP_FILES&#35;jquery.ui.touch-punch.js</code></pre></div>]]></content:encoded>
      <pubDate>Sat, 19 Aug 2023 11:00:00 GMT</pubDate>
      <guid isPermaLink="false">20230819081331172765</guid>
    </item>
    <item>
      <title>Export APEX workspace and schema in Oracle cloud</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>Oracle Cloud</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20230325083035133857</link>
      <description>How to export APEX workspaces, applications and parsing schemas to object storage bucket in the Oracle cloud.</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>You can create APEX workspaces, applications and schemas exports from database using PL/SQL. This might be useful e.g. if you working on Oracle cloud always free autonomous databases. In this example we use always free autonomous database workload type APEX and store exports to object storage bucket in the Oracle cloud. You can setup object versioning and lifecycle policy rules e.g. to move old versions to infrequent access or archive storage and delete old versions after some period. You can also create database job to schedule exports.</p><p>First create new compartment to isolate needed cloud resources.</p><p>Login to your Oracle cloud tenancy, navigate to “Identity &amp; Security” → “Compartments” and click “Create Compartment”.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/compartment_setup1.png" alt="Compartments page"></p><p>Enter name and description, select root compartment as “Parent Compartment” and click “Create Compartment”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/compartment_setup2.png" alt="Create Compartment popup"></p><p>Once the compartment is created, it will appear in the list.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/compartment_setup3.png" alt="Compartments page"></p><p>We need to add a policy to the object storage service order to use lifecycle policy rules in the bucket.</p><p>Navigate to “Identity &amp; Security” → “Policies”, select root compartment and click “Create Policy”.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/root_policy_setup1.png" alt="Policies page"></p><p>Enter name, description, change “Show manual editor” to on and enter policy statements.</p><pre><code class="language-plaintext">Allow service objectstorage-&lt;oci region identifier&gt; to manage object-family in tenancy</code></pre><p>Note! Replace part <i>&lt;oci region identifier&gt;</i> in statement with your region identifier.&nbsp;</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/root_policy_setup2.png" alt="Create policy popup"></p><p>Once the policy is created, you can see its details.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/root_policy_setup3.png" alt="Policy detail page"></p><p>Next create object storage bucket and set lifecycle policy rules for the bucket.</p><p>Navigate to “Storage” → “Buckets”, select compartment you created earlier and click “Create Bucket”.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup1.png" alt="Object storage &amp; archive storage page"></p><p>Enter bucket name, check “Enable Object Versioning” and click “Create”.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup2.png" alt="Create bucket popup"></p><p>Once the bucket is created, it will appear in the list.</p><p>Click the bucket name to view its detail.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup3.png" alt="Object storage &amp; archive storage page"></p><p>Note down namespace as you will need it later.</p><p>From left side menu select “Lifecycle Policy Rules” and click “Create Rule”.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup5.png" alt="Bucket lifecycle policy rules page"></p><p>Enter name, select “Previous Versions of Objects”, set “Lifecycle Action” to “Move Infrequent Access”, set “Number of Days” and click “Create”.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup6.png" alt="Create lifecycle rule popupCreate lifecycle rule popup"></p><p>Once the lifecycle policy rule is created, it will appear in the list.</p><p>Click “Create Rule” to create another rule.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup7.png" alt="Bucket lifecycle policy rules page"></p><p>Enter name, select “Previous Versions of Objects”, set “Lifecycle Action” to “Delete”, set “Number of Days” and click “Create”.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup8.png" alt="Create lifecycle rule popup"></p><p>Once the lifecycle policy rule is created, it will appear in the list.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_setup9.png" alt="Bucket lifecycle policy rules page"></p><p>Next create group, policy and user to access objects storage bucket.</p><p>Navigate to “Identity &amp; Security” → “Groups” and click “Create Group”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/group_setup1.png" alt="Groups page"></p><p>Enter name, description and click “Create”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/group_setup2.png" alt="Create group popup"></p><p>Once the group is created, you can see its details.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/group_setup3.png" alt="Group details page"></p><p>Navigate to “Identity &amp; Security” → “Policies” and select compartment you created earlier.</p><p>Click “Create Policy”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/policy_setup1.png" alt="Policies page"></p><p>Enter name, description, change “Show manual editor” to on and enter policy statements.</p><pre><code class="language-plaintext">Allow group &lt;group name&gt; to read buckets in compartment &lt;compartment name&gt; where any {target.bucket.name='&lt;bucket name&gt;'}
Allow group &lt;group name&gt; to manage objects in compartment &lt;compartment name&gt; where any {target.bucket.name='&lt;bucket name&gt;'}</code></pre><p>Note! Replace parts <i>&lt;group name&gt;</i>, <i>&lt;compartment name&gt;</i> and <i>&lt;bucket name&gt;</i> in statement with names you have used for those objects earlier.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/policy_setup2.png" alt="Create policy popup"></p><p>When the policy is created you see its details.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/policy_setup3.png" alt="Policy details page"></p><p>Navigate to “Identity &amp; Security” → “Users” and click “Create User”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup1.png" alt="Users page"></p><p>Select “IAM &nbsp;User”, enter name and description and then click “Create”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup2.png" alt="Create user popup"></p><p>Once the user is created, you can see its details.</p><p>Click “Add User to Group”</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup3.png" alt="User details page"></p><p>Select group you create earlier and click “Add”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup4.png" alt="Add user to group popup"></p><p>Once a user is added to a group, the group name will appear in the "Groups" list.</p><p>Click “Edit User Capabilities”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup5.png" alt="User details page"></p><p>Limit user capabilities only to “Auth Token” and click “Save Changes”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup6.png" alt="Edit user capabilities popup"></p><p>Next, we need to create an authentication token so that the database can move the exports to object storage bucket.</p><p>Navigate to user “Auth Tokens” and click “Generate Token”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup7.png" alt="User auth tokens page"></p><p>Give token a description and click “Generate Token”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup8.png" alt="Generate token popup"></p><p>Copy the token and place it somewhere safe as you can not view it later and then click “Close”.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup9.png" alt="Generated token popup"></p><p>Once the token is generated, it will appear to "Auth Tokens" list.</p><p><img src="https://static.jaris.fi/b/i/P20230325083035133857/user_setup10.png" alt="User auth tokens page"></p><p>Next login to your autonomous database e.g. using Database Actions as admin user.</p><p>Create new database user, grant needed privileges and enable database actions.</p><pre><code class="language-sql">create user &lt;username&gt; identified by "&lt;password&gt;";

-- Grant privileges
grant connect, resource, dwrole, apex_administrator_role, datapump_cloud_exp, datapump_cloud_imp to &lt;username&gt;;
grant execute on dbms_cloud to &lt;username&gt;;

alter user &lt;username&gt; default role all;

-- Add quota for user
alter user &lt;username&gt; quota unlimited on data;

-- Enable database actions
begin
  ords.enable_schema(
    p_enabled              	=&gt; true
    ,p_schema               =&gt; '&lt;username&gt;'
    ,p_url_mapping_type     =&gt; 'BASE_PATH'
  	,p_url_mapping_pattern	=&gt; '&lt;username&gt;'
  );
end;
/</code></pre><p>Note! Replace parts <i>&lt;username&gt;</i> and <i>&lt;password&gt;</i> with values you like to use.</p><p>Create database package to to new database user schema containing needed procedures to create exports . Package has procedure that exports all APEX workspaces, workspace files, applications and application parsing schemas and move those to object storage bucket. Package contains also procedure to import schema from object storage bucket.</p><p>You can review package code from <a href="https://gist.githubusercontent.com/jariolaine/60bfc2c08c0aa01658c78ea5cc918b46">Github Gist</a> before creating it using below commands.</p><pre><code class="language-sql">alter session set current_schema = &lt;username&gt;;

declare
	l_pkg_sql clob;
begin

-- Fetch package code from GitHub Gist.
  l_pkg_sql :=
		apex_web_service.make_rest_request(
    	p_url =&gt; 'https://gist.githubusercontent.com/jariolaine/60bfc2c08c0aa01658c78ea5cc918b46/raw/backup_api.sql'
      ,p_http_method =&gt; 'GET'
    )
	;

-- Install package.
	dbms_cloud_repo.install_sql(
  	content =&gt; l_pkg_sql
  );

end;
/</code></pre><p>Note! Replace part <i>&lt;username&gt;</i> with database user you created earlier.</p><p>Login to your database using database user you created earlier. You can view user packages to verify earlier created package exists.</p><p>Create credential to access object storage.</p><pre><code class="language-sql">begin

	dbms_cloud.create_credential(
		credential_name	=&gt; 'OBJECT_STORAGE_CRED'
		,username       =&gt; '&lt;iam user&gt;'
		,password       =&gt; '&lt;auth token&gt;'
	);

end;
/</code></pre><p>Note! Replace parts <i>&lt;iam user&gt;</i> and <i>&lt;auth token&gt;</i> with IAM user and token you created earlier.</p><p>Now you can test to create exports by running the procedure.</p><pre><code class="language-plaintext">begin

	backup_api.export_apex_workspaces(
  	p_credential_name	=&gt; 'OBJECT_STORAGE_CRED'
    ,p_region         =&gt; '&lt;oci region identifier&gt;'
    ,p_namespace      =&gt; '&lt;object storage namespace&gt;'
    ,p_bucket         =&gt; '&lt;bucke name&gt;'
	);

end;
/</code></pre><p>Note! Replace parts <i>&lt;oci region identifier&gt;</i>, <i>&lt;object storage namespace&gt;</i> and <i>&lt;bucket name&gt; &nbsp;</i>with your region identifier, namespace and bucket name.</p><p>View your object storage bucket objects. APEX exports have prefix <i>APEX/&lt;workspace name&gt;</i> and schema exports <i>datapump</i>.</p><p>If you have run database procedure multiple times you can see objects have versions.</p><p><img src="https://static.jaris.fi/b//i/P20230325083035133857/bucket_objects.png" alt="Bucket objects"></p><p>We can also create job and schedule exports. Login to database using user you created earlier and create job.</p><pre><code class="language-sql">declare
	l_job_name varchar2(256);
begin

-- Set job name
	l_job_name := 'APEX_EXPORT_JOB';

-- Create scheduler job
	dbms_scheduler.create_job(
  	job_name              =&gt; l_job_name
    ,job_type             =&gt; 'STORED_PROCEDURE'
    ,job_action           =&gt; 'backup_api.export_apex_workspaces'
    ,number_of_arguments	=&gt; 4
    ,start_date           =&gt; to_timestamp_tz( to_char( sysdate + 1, 'DD.MM.YYYY' ) || ' 01:00:00 +02:00' ,'DD.MM.YYYY HH24:MI:SS TZR' )
    ,repeat_interval      =&gt; 'FREQ=DAILY;'
    ,enabled              =&gt; false
    ,auto_drop            =&gt; false
    ,comments             =&gt; 'Daily APEX workspace export job'
	);

-- Set job arguments
	dbms_scheduler.set_job_argument_value(
		job_name            =&gt; l_job_name
		,argument_position  =&gt; 1
		,argument_value     =&gt; 'OBJECT_STORAGE_CRED'
	);

	dbms_scheduler.set_job_argument_value(
		job_name            =&gt; l_job_name
		,argument_position  =&gt; 2
		,argument_value     =&gt; '&lt;oci region identifier&gt;'
	);

	dbms_scheduler.set_job_argument_value(
		job_name            =&gt; l_job_name
		,argument_position  =&gt; 3
		,argument_value     =&gt; '&lt;object storage namespace&gt;'
	);

	dbms_scheduler.set_job_argument_value(
		job_name            =&gt; l_job_name
		,argument_position  =&gt; 4
		,argument_value     =&gt; '&lt;bucket name&gt;'
	);

-- Enable job
	dbms_scheduler.enable( l_job_name );

end;
/</code></pre><p>Note! Replace parts <i>&lt;oci region identifier&gt;</i>, <i>&lt;object storage namespace&gt;</i> and <i>&lt;bucket name&gt;</i> with your region identifier, namespace and bucket name.</p><p>Now we have set daily exports. Later if you see object storage bucket objects, you can see lifecycle policy rules move and delete object versions after days you have defined. Please note that latest version of the objects are not moved or deleted.</p><p>If you wish import schema from object storage bucket you can run procedure:</p><pre><code class="language-sql">begin

	backup_api.import_schema(
		p_credential_name	=&gt; 'OBJECT_STORAGE_CRED'
		,p_region         =&gt; '&lt;oci region identifier&gt;'
		,p_namespace      =&gt; '&lt;object storage namespace&gt;'
		,p_bucket         =&gt; '&lt;bucket name&gt;'
		,p_schema         =&gt; '&lt;schema name&gt;'
		,p_new_schema     =&gt; '&lt;new schema name&gt;'
	);

end;
/</code></pre><p>Note! Replace parts <i>&lt;oci region identifier&gt;</i>, <i>&lt;object storage namespace&gt;,</i> <i>&lt;bucket name&gt;</i>, <i>&lt;schema name&gt;</i> and <i>&lt;new schema name&gt;</i> with your region identifier, namespace, bucket name, schema name and new schema name.</p><p>If you omit parameter p_new_schema schema will not be remapped to new name.</p></div>]]></content:encoded>
      <pubDate>Sat, 25 Mar 2023 19:03:00 GMT</pubDate>
      <guid isPermaLink="false">20230325083035133857</guid>
    </item>
    <item>
      <title>Collapse Interactive Report Control Break</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20220406135639638575</link>
      <description>How to add button to interactive report control break to collapse and expand rows</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>The interactive grid has a feature that collapse and expands the control break. This feature is missing from interactive reports, but it's relatively easy to add the same feature with dynamic action and JavaScript.</p><p>First create interactive report and then add dynamic action</p><ul><li>Event: After Refresh</li><li>Selection Type: Region</li><li>Region: {select your interactive report region}</li><li>Action: Execute JavaScript Code</li><li>Fire on Initialization: On</li><li>Code:</li></ul><pre><code class="language-js">let lButton         = apex.util.htmlBuilder()
// Get messages for button title. Reusing IG messages
  , lCollapseTitle  = apex.lang.getMessage( "APEX.GV.BREAK_COLLAPSE" )
  , lExpandTitle    = apex.lang.getMessage( "APEX.GV.BREAK_EXPAND" )
// Button icon classes for collapse and expand
  , lBtnIcons       = [ "fa-chevron-down", "fa-chevron-right" ]
// IR control break row jQuery selector
  , lCtrBreak       = "th.a-IRR-header--group"
;
// Prepare button HTML
lButton.markup( "&lt;button" )
  .attr( "type", "button" )
  .attr( "aria-expanded", "true" )
  .attr( "title", lCollapseTitle )
  .attr( "aria-label", lCollapseTitle )
  .attr( "class", "t-Button t-Button--noLabel t-Button--icon t-Button--small t-Button--gapRight" )
  .markup( "&gt;&lt;span" )
  .attr( "aria-hidden", "true" )
  .attr( "class", "t-Icon fa " + lBtnIcons[0] )
  .markup( "&gt;&lt;/span&gt;&lt;/button&gt;" )
;
// Set click event to button
let button$ = $( lButton.toString() ).click( function(){
  let this$     = $( this )
    , lExpanded = this$.attr( "aria-expanded" ) === "true" ? "false" : "true"
    , lNewTitle = lExpanded === "true" ? lCollapseTitle : lExpandTitle
  ;
// Change button attributes and icon
  this$.attr({
    "title": lNewTitle
  , "aria-label": lNewTitle
  , "aria-expanded": lExpanded
  }).children().toggleClass( lBtnIcons ).end()
  // Hide/show IR rows from row clicked to the next control break
    .closest( "tr" ).nextUntil( ":has(" + lCtrBreak + ")" ).toggle()
  ;
});
// Attach button to IR control break rows
$( this.triggeringElement ).find( lCtrBreak ).prepend( button$ )
// Uncomment below line to initiallyt collapse control breaks.
// Remember also change button initial attributes.
//  .closest( "tr" ).nextUntil( ":has(" + lCtrBreak + ")" ).toggle()
;</code></pre><p>Run the page and add a control break from the interactive report action menu.</p><p><a href="https://apex.oracle.com/pls/apex/f?p=39006:68" target="_blank">See working example</a></p></div>]]></content:encoded>
      <pubDate>Tue, 05 Apr 2022 22:00:00 GMT</pubDate>
      <guid isPermaLink="false">20220406135639638575</guid>
    </item>
    <item>
      <title>Add tooltip to Interactive Grid column headers</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20220326091656733057</link>
      <description>How to add tooltip for Interactive Grid column headers.</description>
      <content:encoded><![CDATA[<p>Here is how you can add tooltip to Interactive Grid column headers. For this I have placed tooltip text to IG column help and add title attribute column headers using small JavaScript code.</p>

<p>First navigate to application shared components and under Other Components click Shortcuts.</p>

<p>Create new Shortcut from scratch. Give name for Scortcut e.g. IG_TOOLTIP_JSON. Change Type to Function Body returning VARCHAR2. Place code to Shortcut and press Create.</p>

<pre class="blog-program-code">
declare
  l_json varchar2(32700);
begin
  select json_arrayagg(
    json_object(
      &#39;staticId&#39; value static_id || &#39;_HDR&#39;,
      &#39;columnId&#39; value &#39;C&#39; || column_id || &#39;_HDR&#39;,
      &#39;helpText&#39; value help_text
    )
  ) as d
  into l_json
  from apex_appl_page_ig_columns
  where application_id = :APP_ID
    and page_id = :APP_PAGE_ID
    and help_text is not null
  ;
  return l_json;
end;
</pre>

<p>Then edit your Interactive Grid region and set Static ID for region.</p>

<p>Add JavaScript to page Function and Global Variable Declaration.</p>

<pre class="blog-program-code">
(function($){

  $(function(){

    /* 
      MY_IG_REGION is IG region Static ID.
      Change variable value to match IG region static ID
    */
    var regionStaticId = &quot;MY_IG_REGION&quot;;

    setIgHeaderHelp(regionStaticId).on(&quot;interactivegridviewchange interactivegridreportsettingschange&quot;, function(event, ui) {
      setIgHeaderHelp(this.id);
    });

  });

  function setIgHeaderHelp(region){

    var region$ = $($x(region));

    /* IG_TOOLTIP_JSON is Shortcut name */
    $.each(&quot;IG_TOOLTIP_JSON&quot;, function(i, tooltipData){
      region$.find([$x(tooltipData.columnId), $x(tooltipData.staticId)]).parent(&quot;th&quot;).attr({&quot;title&quot; : tooltipData.helpText});
    });

    return region$;

  }

})(apex.jQuery);
</pre>

<p>Remember change variable regionStaticId value to match your IG region Static ID.</p>

<p>For last edit IG columns and add needed text to Help Text field.</p>

<p><a href="https://apex.oracle.com/pls/apex/f?p=39006:58" target="_blank">See working example</a></p>
]]></content:encoded>
      <pubDate>Sat, 26 Mar 2022 09:15:00 GMT</pubDate>
      <guid isPermaLink="false">20220326091656733057</guid>
    </item>
    <item>
      <title>C-ICAP and ClamAV for ORDS</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>Oracle Database</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=20210408172721693098</link>
      <description>How to install C-ICAP and ClamAV on Ubuntu 18.04 server for ORDS</description>
      <content:encoded><![CDATA[<div class="ck-content"><p><img class="u-pullRight" src="https://static.jaris.fi/b/i/P20210408172721693098/clamav-128.png" alt="" width="128" height="128">Oracle REST Data Service (ORDS) has feature to scan uploaded files for viruses. You can install e.g. C-ICAP and ClamAV for that purpose. Here are steps how to do that on Ubuntu 18.04 server assuming you have already working ORDS installation on same or different server.</p><p class="blog-clear-both">First install needed packages and dependencies.</p><pre class="blog-program-code">sudo apt install libc-icap-mod-virus-scan libclamunrar9</pre><p>After packages are installed, edit file /etc/default/c-icap to allow C-ICAP start</p><pre class="blog-program-code">sudo sed -i 's/START=no/START=yes/g' /etc/default/c-icap
</pre><p>Edit file /etc/c-icap/c-icap.conf and add below lines to end of file</p><pre class="blog-program-code">Include virus_scan.conf
Include clamav_mod.conf
virus_scan.DefaultEngine clamav
virus_scan.ScanFileTypes TEXT DATA EXECUTABLE ARCHIVE GRAPHICS STREAM DOCUMENT
ServiceAlias AVSCAN virus_scan?allow204=on&amp;sizelimit=off&amp;mode=simple</pre><p>Restart C-ICAP server</p><pre class="blog-program-code">sudo systemctl restart c-icap
</pre><p>Next edit ORDS configuration file default.xml and add below lines to it before closing properties tag</p><pre class="blog-program-code">&lt;entry key="icap.port"&gt;1344&lt;/entry&gt;
&lt;entry key="icap.server"&gt;127.0.0.1&lt;/entry&gt;
</pre><p>Change key icap.server value to point server where you did install C-ICAP if ORDS is running on different server.</p><p>After you have modify ORDS configuration, restart ORDS standalone or web server where ORDS is deployed.</p><p>With your own risk, you can try virus scanner with&nbsp;test files from&nbsp;<a href="https://www.eicar.org/?page_id=3950" target="_blank">www.eicar.org</a>. Please read instructions,&nbsp;before downloading test files.</p></div>]]></content:encoded>
      <pubDate>Thu, 08 Apr 2021 18:40:00 GMT</pubDate>
      <guid isPermaLink="false">20210408172721693098</guid>
    </item>
    <item>
      <title>APEX and shuttle filter - Revisited</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX and jQuery</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=202002211441057383</link>
      <description>Shuttle item filtering JavaScript code updated to APEX 19.2 and new features.</description>
      <content:encoded><![CDATA[<div class="ck-content"><p><img alt="" height="128" src="https://static.jaris.fi/b/i/P202002211441057383/shuttle.png" class="u-pullRight" width="128">Previously I did write post about <a href="/ords/r/jaris/blog/post?p2_post_id=201410131041380630">APEX and shuttle filter</a>. I received comments that filtering does not work when shuttle have parent cascading item. Issue was that JavaScript code stores shuttle values to variable for filtering only on page load.</p>

<p>I did receive also proposal to add button for clearing filter item. This is also included to new JavaScript code along with some style changes for APEX 19.2.</p>

<p>Here is modified code for page JavaScript Function and Global Variable Declaration:</p>

<pre class="blog-program-code">(function($) {
  $.fn.htmldbShuttlefilter = function(options) {

    options = $.extend({}, {
      "filterPlaceholder": "Filter",
      "buttonTitle": "Clear filter",
      "buttonClass": [
        ,"a-Button"
        ,"a-Button--noLabel"
        ,"a-Button--withIcon"
        ,"a-Button--small"
        ,"a-Button--noUI"
        ,"a-Button--shuttle"
        ,"margin-top-none"
      ],
      "buttonIcon": "fa-times"
    }, options);

    function getShuttleValues(elem$) {
      return elem$.children("option").map(function() {
        return {
          text: $(this).text(),
          value: $(this).val()
        };
      });
    }

    return this.each(function(i) {

      var $self = $(this),

        shuttleResetBtnId = $self.attr("id") + "_RESET",
        $select = $self.find("select"),

        // get shuttle values
        shuttleValues = getShuttleValues($select),

        // filter reset button
        $resetBtn = $("&lt;button/&gt;", {
          "type": "button",
          "title": options.buttonTitle,
          "aria-label": options.buttonTitle,
          "class": options.buttonClass.join(" "),
          "css": {
            "padding": "4px"
          }
        }).append(
          $("&lt;span/&gt;", {
            "aria-hidden": "true",
            "class": "a-Icon fa " + options.buttonIcon,
          })
        ).click(function() {
          // clear filter text field
          $filter.val("").keyup();
        }),

        // filter text field
        $filter = $("&lt;input/&gt;", {
          "type": "text",
          "value": "",
          "autocomplete": "off",
          "placeholder": options.filterPlaceholder,
          "class": "text_field apex-item-text",
          "css": {
            "width": "100%"
          }
        }).keyup(function() {

          // filter shuttle when typing to text field
          var filterval = new RegExp("^" + $(this).val() + ".*", "i"),
            selectedValues = $select.eq(1).children("option").map(function() {
              return $(this).val();
            });

          // empty shuttle available values
          $select.eq(0).empty();
          // add values that match filter criteria
          $.each(shuttleValues, function(idx, obj) {
            if (
              obj.text.match(filterval) &amp;&amp;
              $.inArray(obj.value, selectedValues) &lt; 0
            ) {
              $select.eq(0).append(new Option(obj.text, obj.value));
            }
          });
        });

      // add needed elements to page
      $self.prepend(
        $("&lt;div/&gt;", {
          "class": "t-Form-itemWrapper"
        })
        .append($filter).append(
          $("&lt;span/&gt;", {
            "class": "t-Form-itemText t-Form-itemText--post"
          }).append($resetBtn)
        )

      ).on("apexafterrefresh", function() {
        // initiliaze filter when shuttle is refreshed by parent item
        $filter.val("");
        shuttleValues = getShuttleValues($select);
      });

      // clear also filter from shuttle reset button
      $("&#35;" + shuttleResetBtnId).click(function() {
        $filter.val("");
      });
    });
  }
})(apex.jQuery);
</pre>

<p>If you like add filter to all shuttles in page, then add to page JavaScript Execute when Page Loads:</p>

<pre class="blog-program-code">$(".apex-item-group--shuttle").htmldbShuttlefilter();</pre>

<p>Depending on your need, change jQuery selector.</p>

<p><a href="https://apex.oracle.com/pls/apex/f?p=39006:52" target="_blank">See working example</a> where Dept shuttle is parent for Emp.</p>
</div>]]></content:encoded>
      <pubDate>Fri, 21 Feb 2020 11:20:00 GMT</pubDate>
      <guid isPermaLink="false">202002211441057383</guid>
    </item>
    <item>
      <title>Stopping XE 18c properly when shutting down CentOs 7</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>Oracle Database</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201902171002516775</link>
      <description>Stopping Oracle XE 18c database properly using systemd when shutting down CentOs 7 system</description>
      <content:encoded><![CDATA[<div class="ck-content"><p><img class="u-pullRight" src="https://static.jaris.fi/b/i/P201902171002516775/system-shutdown-150x150.png" alt="" width="150" height="150">You can configure automatically start and stop Oracle XE 18c database on operating system startup and shutdown. See details from <a href="https://docs.oracle.com/en/database/oracle/oracle-database/18/xeinl/starting-and-stopping-oracle-database.html" target="_blank">installation guide</a>. I did install XE 18c on CentOs 7 and executed steps mentioned in installation guide. When viewing database alert.log after system reboot, noticed that database wasn't actually stopped properly. Processes were just killed. I didn't investigate more why that happens on my system. For workaround I did revert steps mentioned in installation guide and created service file for systemd.</p><p>First stop XE database and revert automatically start and stop:</p><pre class="blog-program-code"><code class="language-plaintext">systemctl stop oracle-xe-18c
systemctl disable oracle-xe-18c
</code></pre><p>Create service file:</p><pre class="blog-program-code"><code class="language-plaintext">cat &gt;&gt; /usr/lib/systemd/system/ora-xe-18c.service &lt;&lt; EOF
[Unit]
Description=Oracle 18c XE databases service
After=syslog.target network.target

[Service]
Type=forking
Restart=no
KillMode=none
TimeoutStopSec=10min
RemainAfterExit=yes
User=oracle
Group=oinstall
ExecStart=/opt/oracle/product/18c/dbhomeXE/bin/dbstart /opt/oracle/product/18c/dbhomeXE &amp;
ExecStop=/opt/oracle/product/18c/dbhomeXE/bin/dbshut /opt/oracle/product/18c/dbhomeXE

[Install]
WantedBy=multi-user.target
EOF</code></pre><p>Backup and modify /etc/oratab:</p><pre class="blog-program-code"><code class="language-plaintext">cp /etc/oratab /etc/oratab.backup_`date +%Y%m%d`
sed -i 's/XE:\/opt\/oracle\/product\/18c\/dbhomeXE:N/XE:\/opt\/oracle\/product\/18c\/dbhomeXE:Y/g' /etc/oratab</code></pre><p>Enable new service and start XE database again:</p><pre class="blog-program-code"><code class="language-plaintext">systemctl daemon-reload
systemctl enable ora-xe-18c
systemctl start ora-xe-18c
</code></pre></div>]]></content:encoded>
      <pubDate>Sun, 17 Feb 2019 10:02:00 GMT</pubDate>
      <guid isPermaLink="false">201902171002516775</guid>
    </item>
    <item>
      <title>Caching APEX static files</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX and Apache HTTPD</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201803041325416124</link>
      <description>Configuring Apache HTTPD mod_file_cache, mod_cache and mod_cache_disk for APEX static files in server and database</description>
      <content:encoded><![CDATA[<div class="ck-content"><p><img class="u-pullRight" src="https://static.jaris.fi/b/i/P201803041325416124/apache_logo.png" alt="Apache HTTP Server" width="128" height="128">The APEX installation includes static files that are located on the web server. These&nbsp;files may changed normally only when you upgrade or apply patch to APEX. Because files are changing rarely, they are good subject stored to cache. If you use Apache HTTPD to serve static files, you can configure <a href="https://httpd.apache.org/docs/2.4/mod/mod_file_cache.html" target="_blank">mod_file_cache</a> for ones that are used most in your applications.</p><p>You might have also other static content e.g. images, JavaScript and CSS files that are stored to APEX workspace or application static files. Also these files aren't usually changed often and you can store files to cache and reduce calls to database. This can be done using HTTPD modules&nbsp;<a href="https://httpd.apache.org/docs/2.4/mod/mod_cache.html" target="_blank">mod_cache</a> and&nbsp;<a href="https://httpd.apache.org/docs/2.4/mod/mod_cache_disk.html" target="_blank">mod_cache_disk</a>.</p><p>I provide here only basic information that you are aware about these HTTPD cache features and how those may used with APEX. Assumption is that you have knowledge how&nbsp;configure&nbsp;HTTPD.&nbsp;You need test and see how these works on your system.</p><p>First check are needed modules loaded to HTTPD.&nbsp;In Linux machine you can use command <i>httpd -M</i> to list all modules that are loaded.<br>You should look cache_module, cache_disk_module and file_cache_module.</p><p>Create configuration file called e.g. apex_file_cache.conf to specify the APEX static files you like cache. Here is example configuration file content for APEX 5.1.4 when applications use&nbsp;<i>Universal Theme</i>&nbsp;and styles&nbsp;<i>Vita</i>:</p><pre class="blog-program-code"><code class="language-plaintext">&#35; APEX 5.1.4 mod_file_cache configuration for Universal Theme style Vita
&lt;IfModule file_cache_module&gt;
&nbsp; &#35; CSS
&nbsp; CacheFile /apex/images/app_ui/css/Core.min.css
&nbsp; CacheFile /apex/images/app_ui/css/Theme-Standard.min.css
&nbsp; CacheFile /apex/images/libraries/jquery-ui/1.10.4/themes/base/jquery-ui.min.css
&nbsp; CacheFile /apex/images/libraries/font-apex/1.0/css/font-apex.min.css
&nbsp; CacheFile /apex/images/themes/theme_42/1.1/css/Core.min.css
&nbsp; CacheFile /apex/images/themes/theme_42/1.1/css/Vita.min.css
  &#35; JavaScript
&nbsp; CacheFile /apex/images/libraries/apex/minified/desktop.min.js
&nbsp; CacheFile /apex/images/libraries/hammer/2.0.4/hammer-2.0.4.min.js
&nbsp; CacheFile /apex/images/libraries/apex/minified/widget.apexTabs.min.js
&nbsp; CacheFile /apex/images/libraries/apex/minified/widget.stickyWidget.min.js
&nbsp; CacheFile /apex/images/libraries/apex/minified/widget.stickyTableHeader.min.js
&nbsp; CacheFile /apex/images/themes/theme_42/1.1/js/modernizr-custom.min.js
&nbsp; CacheFile /apex/images/themes/theme_42/1.1/js/theme42.min.js
&lt;/IfModule&gt;
</code></pre><p>In example I use&nbsp;<a href="https://httpd.apache.org/docs/2.4/mod/mod_file_cache.html&#35;cachefile" target="_blank">CacheFile</a>&nbsp;directive&nbsp;but you should test does your platform support it or do you get better result with <a href="https://httpd.apache.org/docs/2.4/mod/mod_file_cache.html&#35;mmapfile" target="_blank">MMapFile</a>&nbsp;directive. Check also path where the files are located in server.&nbsp;Include configuration file to your httpd.conf file, run test for configuration&nbsp;and restart HTTPD.</p><p>NOTE! when you upgrade or patch APEX, you need check are same files still relevant for cache and do changes accordingly to the configuration file. Also you need restart HTTPD to avoid end up serving requests that are completely bogus as <a href="https://httpd.apache.org/docs/2.4/mod/mod_file_cache.html&#35;using" target="_blank">mod_file_cache</a> documentation says.<br>You shouldn't try cache all APEX +12 000 static files,&nbsp;that installation contains, with <a href="https://httpd.apache.org/docs/2.4/mod/mod_file_cache.html&#35;mmapfile" target="_blank">mod_file_cache</a>. Most probably your applications isn't using those all even in special cases. You need carefully select only most&nbsp;used files.</p><p>To configure cache for APEX workspace and application static files create configuration file called e.g.&nbsp;apex_cache_disk.conf. Here is example configuration for workspaces&nbsp;ws1, ws2, ws3 and ws4:</p><pre class="blog-program-code"><code class="language-plaintext">&#35; APEX 5.1.4 mod_cache configuration for static workspace and application files
&lt;IfModule cache_disk_module&gt;
&nbsp; CacheRoot &nbsp; &nbsp; &nbsp;/var/cache/httpd/proxy
&nbsp; CacheEnable &nbsp; &nbsp;disk /apex/ws1/r/
&nbsp; CacheEnable &nbsp; &nbsp;disk /apex/ws2/r/
&nbsp; CacheEnable &nbsp; &nbsp;disk /apex/ws3/r/
&nbsp; CacheEnable &nbsp; &nbsp;disk /apex/ws4/r/
&nbsp; CacheLock &nbsp; &nbsp; &nbsp;on
&lt;/IfModule&gt;
</code></pre><p>You need check what are URL's in your system relating to APEX workspace and application static files and adjust configuration accordingly. Include configuration file to your httpd.conf file, run test for configuration&nbsp;and restart HTTPD.</p><p>Optionally you can also add <a href="https://httpd.apache.org/docs/2.4/mod/mod_cache.html&#35;cacheignorecachecontrol" target="_blank">CacheIgnoreCacheControl</a> directive that&nbsp;clients can't force refresh cache in HTTPD. Then it is recommended where you refer static file add query string to end. That way developers&nbsp;can force HTTPD fetch&nbsp;new file when files are changed. You can use e.g. APP_VERSION substitution string&nbsp;like:</p><pre class="blog-program-code"><code class="language-plaintext">&#35;APP_IMAGES&#35;style&#35;MIN&#35;.css?v=&#35;APP_VERSION&#35;
</code></pre><p>If you change&nbsp;file in workspace or application static files, change also version attribute in <a href="https://docs.oracle.com/database/apex-5.1/HTMDB/managing-application-attributes.htm&#35;GUID-4C7EB51C-C36A-4A52-86BE-60FEC6FCAB71" target="_blank">application definitions</a>&nbsp;so HTTPD knows reload file.<br>After enabling mod_cache, you should also see and setup <a href="https://httpd.apache.org/docs/2.4/programs/htcacheclean.html" target="_blank">htcachecleanup</a>.</p><p>Depending if you aren't using RESTful services from APEX workspaces, after configuring mod_cache for workspace and application files, you might like check and change ORDS configuration for&nbsp;APEX_REST_PUBLIC_USER and APEX_LISTENER users pool. See if you can&nbsp;lower those pools jdbc.MinLimit so there aren't connections waiting for nothing while&nbsp;HTTPD serves workspace and application static files from cache.</p></div>]]></content:encoded>
      <pubDate>Sun, 04 Mar 2018 13:25:00 GMT</pubDate>
      <guid isPermaLink="false">201803041325416124</guid>
    </item>
    <item>
      <title>Changing Report Column Date Format</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201802111100046420</link>
      <description>Simple trick how change APEX application report date columns format mask to SINCE, and save selection to user preferences for next session.</description>
      <content:encoded><![CDATA[<div class="ck-content"><p><img class="u-pullRight" src="https://static.jaris.fi/b/i/P201802111100046420/clock.png" alt="" width="128" height="128">In APEX there is build in date formats SINCE and SINCE_SHORT that I use often in report date columns. Unfortunately you can't use SINCE format in application Globalization Attributes, so you need separately set it to columns. But, sometimes you need to see the actual date value in column.<br><br>Here is simple example how you can switch between application global date format and SINCE.</p><p>First create application item G_DATE_DISPLAY_FORMAT.</p><div><img src="https://static.jaris.fi/b/i/P201802111100046420/application_item.png" alt="Application item" width="391" height="168"></div><p>Create two entries to Navigation Bar list as follows.<br>First list entry:</p><ul><li>List Entry Label: Show Since</li><li>Target page: &amp;APP_PAGE_ID.</li><li>Request: SET_DATE_FORMAT_SINCE_ON</li><li>Condition: Value of User Preference in Expression 1 != Expression 2<ul><li>Expression 1: U_DATE_DISPLAY_FORMAT</li><li>Expression 2: SINCE</li></ul></li></ul><p>Second list entry is almost same. Just change label, request and condition</p><ul><li>List Entry Label: Show Date</li><li>Target page: &amp;APP_PAGE_ID.</li><li>Request: SET_DATE_FORMAT_SINCE_OFF</li><li>Condition: Value of User Preference in Expression 1 = Expression 2<ul><li>Expression 1: U_DATE_DISPLAY_FORMAT</li><li>Expression 2: SINCE</li></ul></li></ul><p>Create <i>On Load: Before Header</i> application process.<br>PL/SQL Code:</p><pre class="blog-program-code"><code class="language-sql">declare
  l_since varchar2(40);
begin

  l_since := 
    case when :REQUEST = 'SET_DATE_FORMAT_SINCE_ON'
    then 'SINCE'
    end
  ;
  :G_DATE_DISPLAY_FORMAT  := l_since;

  /* save user selection to preference */
  apex_util.set_preference(
     p_preference =&gt; 'U_DATE_DISPLAY_FORMAT'
    ,p_value      =&gt; l_since
    ,p_user       =&gt; :APP_USER
  );

end;</code></pre><p>Set process conditionally by request containing SET_DATE_FORMAT_SINCE_ON and SET_DATE_FORMAT_SINCE_OFF</p><div><img src="https://static.jaris.fi/b/i/P201802111100046420/application_process_condition.png" alt="Application process condition" width="475" height="132"></div><p>Application process stores information also to preference. If you like you can set application item value for new sessions in Post-Authentication Procedure.</p><p>Place code to authentication schema source PL/SQL Code and Enter to Post-Authentication Procedure Name field <i><strong>set_date_display_format</strong></i></p><pre class="blog-program-code"><code class="language-sql">procedure set_date_display_format
as
begin
  :G_DATE_DISPLAY_FORMAT := apex_util.get_preference(
     p_preference =&gt; 'U_DATE_DISPLAY_FORMAT'
    ,p_user =&gt; :APP_USER
  );
end;</code></pre><div><img src="https://static.jaris.fi/b/i/P201802111100046420/authentication_schema.png" alt="Authentication schema" width="511" height="112"></div><p>Then edit report columns and set format mask to application item substitution string &amp;G_DATE_DISPLAY_FORMAT.</p><div><img src="https://static.jaris.fi/b/i/P201802111100046420/column_format_mask.png" alt="Report column format mask" width="371" height="123"></div><p>Here you can see <a href="https://apex.oracle.com/pls/apex/f?p=45689:1" target="_blank">example</a> where right top there is link to switch date format mask. When you click the link in navigation bar, you can see report <i>Hired on</i> column format changes.</p></div>]]></content:encoded>
      <pubDate>Sun, 11 Feb 2018 10:00:00 GMT</pubDate>
      <guid isPermaLink="false">201802111100046420</guid>
    </item>
    <item>
      <title>Oracle Database Developer Choice Awards</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Community</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201510100617461772</link>
      <description>Oracle Database Developer Choice Awards 2015</description>
      <content:encoded><![CDATA[<div class="ck-content"><p><img alt="" height="100" src="https://static.jaris.fi/b/i/P201510100617461772/award.png" class="u-pullRight" width="100">I'm finalist for <a href="https://community.oracle.com/community/database/awards/apex-voting" target="_blank">Oracle Database Developer Choice Awards in Oracle Application Express (APEX) category</a>.<br>
I'm very impressed and surprised for this honor. I want thank all who have filled nomination form and voted. Special thanks to all who have also leave comments and supported me by blog posts and spreading word other ways.<br>
If you haven't vote yet, now it's time. Voting ends at 15th of October. Please remember you can vote multiple persons in all categories. I hope <a href="http://deneskubicek.blogspot.fi/2015/10/oracle-database-developer-choice-awards.html" target="_blank">Denes Kubicek's blog post</a> encourage everybody to vote.</p>
</div>]]></content:encoded>
      <pubDate>Sat, 10 Oct 2015 06:17:00 GMT</pubDate>
      <guid isPermaLink="false">201510100617461772</guid>
    </item>
    <item>
      <title>SQL Developer ORDS administration &quot;peer not authenticated&quot; error</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>Oracle SQL Developer</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201503071132220465</link>
      <description>How to resolve &quot;peer not authenticated&quot; error on SQL Developer ORDS administration</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>If your web server use self signed certificate for https you most probably get "peer not authenticated" error when you try administrate Oracle REST Data Service (ORDS) using Oracle SQL Developer.<br><img src="https://static.jaris.fi/b/i/P201503071132220465/sql-developer-peer-not-authenticated.png" alt="Peer not authenticated error message" height="139" width="395"><br>To resolve this issue, you need import server certificate to Java cacerts keystore.</p><p>You can download server certificate e.g. using browser. In this example I use Firefox.</p><p>First navigate to your server URL and click lock icon next to URL. From popup click More Information.<br><br><img src="https://static.jaris.fi/b/i/P201503071132220465/ff-get-cert-1.png" alt="Get server certificate using Firefox step 1" height="389" width="655"></p><p>From opening window click View Certificate<br><br><img src="https://static.jaris.fi/b/i/P201503071132220465/ff-get-cert-2.png" alt="Get server certificate using Firefox step 2" height="479" width="655"></p><p>Click Details tab and then Export button<br><br><img src="https://static.jaris.fi/b/i/P201503071132220465/ff-get-cert-3.png" alt="Get server certificate using Firefox step 3" height="691" width="655"></p><p>Change the ‘Save as type’ to ‘X.509 Certificate with chain (PEM)’ and save the certificates to a file.<br><br><img src="https://static.jaris.fi/b/i/P201503071132220465/ff-get-cert-4.png" alt="Get server certificate using Firefox step 4" height="409" width="655"></p><p>Open command prompt and import certificate to Java cacert keystore. Please note that you need import certificate to Java installation/version witch SQL Developer uses.</p><p>Here is example of command how import certificate in Windows</p><pre class="blog-program-code">C:\sqldeveloper\jdk\jre\bin\keytool.exe -import -noprompt -trustcacerts ^
-alias vbox-apex.localdomain ^
-file C:\Temp\vbox-apex.localdomain.crt ^
-keystore C:\sqldeveloper\jdk\jre\lib\security\cacerts ^
-storepass changeit</pre><p><img src="https://static.jaris.fi/b/i/P201503071132220465/cmd-import-cert.png" alt="Import certificate using Java keytool" height="216" width="653"></p><p><br>Now administrating Oracle REST Data Service using Oracle SQL Developer over https works.<br><br><img src="https://static.jaris.fi/b/i/P201503071132220465/ords-connection.png" alt="ORDS connection" height="262" width="369"></p></div>]]></content:encoded>
      <pubDate>Sat, 07 Mar 2015 11:32:00 GMT</pubDate>
      <guid isPermaLink="false">201503071132220465</guid>
    </item>
    <item>
      <title>New version of APEX Blogging Platform released</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Sample Applications</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201412281415140822</link>
      <description>Completly rewritten new version of APEX Blogging Platform is now released.</description>
      <content:encoded><![CDATA[<p>Finally I did have some time finish and publish new version of APEX Blogging Platform. Application is completely rewritten and have much more features and new theme. It is simple to install also to hosted APEX instance.<br />
<br />
<img alt="New APEX Blogging Platform" height="287" src="https://static.jaris.fi/b/i/P201412281415140822/newblog2901.png" width="650" /></p>

<p>You can download application from <a href="https://sourceforge.net/projects/apex-blog/" target="_blank">SourceForge</a>. If you have comments or enhancement ideas please submit those to project page Tickets or Discussion.</p>
]]></content:encoded>
      <pubDate>Sun, 28 Dec 2014 13:15:00 GMT</pubDate>
      <guid isPermaLink="false">201412281415140822</guid>
    </item>
    <item>
      <title>APEX 4.2 interactive report with row detail</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX and jQuery</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201411021608150690</link>
      <description>Interactive report with the functionality to show a detail row per each row</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>Here is how create interactive report with the functionality to show a detail row per each row on APEX 4.2.</p><p>Below you can see interactive report query I did use for this example:</p><pre class="blog-program-code">SELECT p.product_id
,p.product_name
,p.category
,p.product_avail
,p.list_price
,(
  SELECT sum(quantity)
  FROM demo_order_items
  WHERE product_id = p.product_id
  ) AS units
,(
  SELECT sum(quantity * p.list_price)
  FROM demo_order_items
  WHERE product_id = p.product_id
  ) AS sales
,(
  SELECT count(o.customer_id)
  FROM demo_orders o
   ,demo_order_items t
  WHERE o.order_id = t.order_id
   AND t.product_id = p.product_id
  GROUP BY p.product_id
  ) AS customers
,(
  SELECT max(o.order_timestamp) od
  FROM demo_orders o
   ,demo_order_items i
  WHERE o.order_id = i.order_id
   AND i.product_id = p.product_id
  ) AS last_date_sold
,(
  SELECT APEX_LANG.LANG('Details')
  FROM DUAL
  ) AS details
FROM demo_product_info p</pre><p>Edit DETAILS column attributes and make column as link</p><ul><li>Link Text : &#35;DETAILS&#35;</li><li>Link Attributes : class="product-details" data-product="&#35;PRODUCT_ID&#35;"</li><li>Target : URL</li><li>URL : &#35;</li></ul><p>Add to page JavaScript File URLs</p><pre class="blog-program-code">&#35;IMAGE_PREFIX&#35;libraries/jquery-ui/1.8.22/ui/minified/jquery.ui.button.min.js</pre><p>and to Function and Global Variable Declaration</p><pre class="blog-program-code">var gDetailCache = new Object();
(function($){
 $.fn.htmldbDetailRow=function(options){
  options=$.extend({},{
   "trIdPrefix":"D",
   "btnShowClass":"ui-icon-plusthick",
   "btnHideClass":"ui-icon-minusthick",
   "btnAjaxClass":"ui-icon-refresh"
  },options);
  this.each(function(){
   var $Self  = $(this).removeAttr("href").button({icons:{primary:options.btnShowClass},text:false}),
       $Row   = $Self.closest("tr"),
       $Ico   = $Self.children("span.ui-button-icon-primary"),
       lC     = $Row.children("td").length,
       lId    = $Self.data(options.btnData),
       lTrId  = options.trIdPrefix+lId,
       lClass = options.btnShowClass + " " + options.btnHideClass
   ;
   $Self.click(function(){
    $Tr=$($x(lTrId));
    if($Tr.length===0){
     $Self.button("option",{icons:{primary:options.btnAjaxClass},"disabled":true});
     apex.server.process(options.onDemanProcess,
      {x01:lId},{dataType:"text",success:function(d){
       var $Tr=$(
        '&lt;tr id="' + lTrId + '"&gt;' +
        '&lt;td class="' + options.tdClass + '" colspan="' + lC + '"&gt;'
        + d +
        '&lt;/td&gt;' +
        '&lt;/tr&gt;'
       ),lA=new Object();
       lA[lTrId]={d:$Tr,s:true};
       $.extend(gDetailCache,lA);
       $Row.after($Tr);
       $Ico=$Self.button("option",{icons:{primary:options.btnHideClass},"disabled":false})
       .children("span.ui-button-icon-primary");
      }
     });
    }else{
     $Tr.toggle(0,function(){
      $Ico.toggleClass(lClass);
      gDetailCache[lTrId].s=!gDetailCache[lTrId].s
     })
    }
   });
   if(lTrId in gDetailCache){
    gDetailCache[lTrId].d.children().attr({"colspan":lC});
    $Row.after(gDetailCache[lTrId].d);
    if(gDetailCache[lTrId].s){
     $Ico.toggleClass(lClass)
    }else{
     gDetailCache[lTrId].d.hide()
    }
   }
  })
  return this
 }
})(apex.jQuery);</pre><p>Add to page CSS Inline</p><pre class="blog-program-code">.prodinfo{
 padding:6px!important;
 font-size:12pt!important;
 color:&#35;660000!important;
 font-weight:bold!important;
 text-align:center!important;
}</pre><p>Create on demand process GET_PRODUCT_INFO</p><pre class="blog-program-code">DECLARE
  l_info VARCHAR2(32000);
BEGIN
  SELECT product_description
  INTO l_info
  FROM demo_product_info
  WHERE product_id = apex_application.g_x01; 
  HTP.PRN(l_info);
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    HTP.PRN('No additional information');
  WHEN OTHERS THEN
    HTP.PRN(sqlerrm);
END;</pre><p>Create dynamic action</p><ul><li>Name: IR detail row</li><li>Event: After Refresh</li><li>Selection Type: Region</li><li>Region: <i>{select IR region}</i></li><li>Condition: -No Condition-</li><li>Action: Execute JavaScript code</li><li>Fire On Page Load: True</li><li><p>Code:</p><pre class="blog-program-code">$(this.triggeringElement)
.find('a.product-details')
.htmldbDetailRow({
 onDemanProcess:"GET_PRODUCT_INFO", // on demand process name
 tdClass:"prodinfo",                // details class
 btnData:"product"                  // button data name
});
</pre></li><li>Selection Type: None</li></ul><p>Now when you run page you have button on each row to expand/collapse row details.<br><br>Please note that when you e.g. paginate to next page and back, example remember rows that were expanded =).<br><a href="https://apex.oracle.com/pls/apex/f?p=39006:53" target="_blank">See working example</a>.</p><p><img src="https://static.jaris.fi/b/i/P201411021608150690/ir_detail_row.png" alt="IR detail row" width="655" height="354"></p></div>]]></content:encoded>
      <pubDate>Sun, 02 Nov 2014 15:08:00 GMT</pubDate>
      <guid isPermaLink="false">201411021608150690</guid>
    </item>
    <item>
      <title>APEX and shuttle filter</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX and jQuery</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201410131041380630</link>
      <description>How to create filter / search field for APEX shuttle item values using jQuery.</description>
      <content:encoded><![CDATA[<p>Here is small tip how to create filter / search field for APEX shuttle item values. For this you need just place jQuery code to page JavaScript Function and Global Variable Declaration.</p>

<pre class="blog-program-code">
(function($){
$.fn.htmldbShuttlefilter=function(options){
  options=$.extend({},{&quot;label&quot;:&quot;Filter&quot;},options);
  return this.each(function(i){
   var $self      = $(this)
   ,filterId      = $self.attr(&quot;id&quot;) + &quot;_FILTER&quot;
   ,$select       = $self.find(&quot;select&quot;)
   ,shuttleValues = $select.children(&quot;option&quot;).map(function(){
    return {text:$(this).text(),value:$(this).val(),option:this}
   })
   ,$filter = $(&quot;&lt;input/&gt;&quot;,{&quot;type&quot;:&quot;text&quot;,&quot;value&quot;:&quot;&quot;,&quot;size&quot;:&quot;255&quot;,&quot;autocomplete&quot;:&quot;off&quot;,&quot;id&quot;:filterId})
   .keyup(function(){
    var filterval   = new RegExp(&quot;^&quot;+$(this).val()+&quot;.*&quot;,&quot;i&quot;)
    ,selectedValues = $select.eq(1).children(&quot;option&quot;).map(function(){
     return $(this).val();
    });
    $select.eq(0).empty();
    $.each(shuttleValues,function(idx,obj){
     if(obj[&quot;text&quot;].match(filterval) &amp;&amp; $.inArray(obj[&quot;value&quot;],selectedValues)&lt;0){
      $select.eq(0).append(obj[&quot;option&quot;]);
     }
    });
   })
   .width($self.width());
   $(&quot;&lt;div/&gt;&quot;,{&quot;css&quot;:{&quot;padding-bottom&quot;:&quot;5px&quot;}})
   .insertBefore($self)
   .append(
    $(&quot;&lt;label/&gt;&quot;,{&quot;for&quot;:filterId})
    .append($(&quot;&lt;span/&gt;&quot;,{&quot;css&quot;:{&quot;font-weight&quot;:&quot;bold&quot;}}).text(options.label))
   )
   .append(&quot;&lt;br/&gt;&quot;).append($filter);
   $self.find(&quot;img[alt=&#39;Reset&#39;]&quot;).click(function(){$filter.val(&quot;&quot;)});
  });
}
})(jQuery);</pre>

<p>Then add to page JavaScript Execute when Page Loads</p>

<pre class="blog-program-code">
$(&quot;&#35;Px_SHUTTLE_ITEM_NAME&quot;).htmldbShuttlefilter({});</pre>

<p>Replace Px_SHUTTLE_ITEM_NAME with your real shuttle item name.</p>

<p><a href="https://apex.oracle.com/pls/apex/f?p=39006:52" target="_blank">See working example</a>.</p>

<p><img alt="Shuttle with filter" height="304" src="https://static.jaris.fi/b/i/P201410131041380630/shuttle_with_filter.png" width="588" /></p>
]]></content:encoded>
      <pubDate>Mon, 13 Oct 2014 08:41:00 GMT</pubDate>
      <guid isPermaLink="false">201410131041380630</guid>
    </item>
    <item>
      <title>Yet another way to create &quot;editable interactive report&quot; part 2</title>
      <dc:creator>Jari Laine</dc:creator>
      <category>APEX Tips and Tricks</category>
      <link>https://cloud.jaris.fi/ords/r/jaris/blog/post?p2_post_id=201406180631350353</link>
      <description>Enhance editable interactive report whit select list and textarea on APEX 4.2</description>
      <content:encoded><![CDATA[<div class="ck-content"><p>While back I did write blog post <a href="/ords/r/jaris/blog/post?p2_post_id=201402122308060084">Yet another way to create "editable interactive report"</a>. This post is part 2, where I explain how you can add select lists and text area to report. In example I have use <a href="http://docs.oracle.com/cd/E37097_01/doc/doc.42/e35127/apex_collection.htm&#35;AEAPI531" target="_blank">APEX_COLLECTION</a>, but you can modify code to work with real table.</p><p>First create Before Regions PL/SQL process that creates apex_collection:</p><pre class="blog-program-code">APEX_COLLECTION.CREATE_COLLECTION_FROM_QUERYB2 (
      p_collection_name =&gt; 'EMP',
      p_query =&gt; '
        SELECT e1.empno
          , e1.sal
          , COALESCE(e1.comm, 0) AS comm
          , e1.mgr
          , e1.deptno
          , e1.hiredate
          , NULL
          , NULL
          , NULL
          , NULL
          , e1.ename
          , e1.job
       FROM emp e1
      '
    );
    </pre><p>Then go to Shared Components &gt; Lists of Values and create new LOV:</p><ul><li>From Scratch</li><li>name: MANAGER</li><li>type: Dynamic</li><li>Query:</li></ul><pre class="blog-program-code">select ename d, empno r
    from   emp
    order by 1</pre><p>Create another new LOV:</p><ul><li>From Scratch</li><li>name: DEPARTMENT</li><li>type: Dynamic</li><li>Query:</li></ul><pre class="blog-program-code">select dname d, deptno r
    from   dept
    order by 1</pre><p>Go back to edit page and create interactive report from query:</p><pre class="blog-program-code">SELECT seq_id
      ,n001    AS empno
      ,c001    AS ename
      ,c002    AS job
      ,n004    AS mgr
      ,d001    AS hiredate
      ,n002    AS sal
      ,n003    AS comm
      ,n005    AS dname
      ,clob001 AS enotes
      ,n004    AS mgrno
      ,n005    AS deptno
    FROM apex_collections
    WHERE collection_name = 'EMP'</pre><p>Change ENOTES column <i>Display as</i>&nbsp;to <i>standard report column</i>, MGRNO and DEPTNO columns to <i>hidden</i>.</p><p><img src="https://static.jaris.fi/b/i/P201406180631350353/eir2_column_attributes.png" alt="Column attributes" height="138" width="685"></p><p>Edit ENOTES column attributes and add to column formatting:</p><pre class="blog-program-code">&lt;textarea class="ir-edit-input" id="CLOB-&#35;SEQ_ID&#35;-1" cols="30" rows="3"&gt;&#35;ENOTES&#35;&lt;/textarea&gt;</pre><p><img src="https://static.jaris.fi/b/i/P201406180631350353/eir2_clob_column_formatting.png" alt="Clob column formatting" height="230" width="590"></p><p>You can control text are width and height using attributes cols and rows.</p><p>Edit MGR column attributes and change Display Type to Display as Text (based on LOV, escape special characters).<br>Set Named List of Values to MANAGER</p><p><img src="https://static.jaris.fi/b/i/P201406180631350353/eir2_mgr_column.png" alt="Manager column attributes" height="498" width="685"></p><p>Set column formatting</p><pre class="blog-program-code">&lt;select class="ir-edit-input" data-lov="MANAGER" data-display-null="- Select -" data-last-value="&#35;MGRNO&#35;" id="N-&#35;SEQ_ID&#35;-4"&gt;&lt;/select&gt;</pre><p>Attribute data-lov defines shared components LOV name used for&nbsp;column select list.<br>Include&nbsp;attribute data-display-null if LOV should have null value. This attribute value is used as null display value.</p><p>From report query you can see that apex_collection columns n004 and n005 are defined two times with different alias. We did set columns with alias MGRNO and DETPNO as hidden column. Attribute data-last-value value should be set to that hidden column substitution string because we like have real column value to this attribute. If we use column substitution string value from column where Display Type was set to Display as Text (based on LOV, escape special characters), then attribute gets LOV display value. And select list do not work correctly in that case.</p><p><img src="https://static.jaris.fi/b/i/P201406180631350353/eir2_mgr_column_formatting.png" alt="Manager column formatting" height="215" width="590"></p><p>Edit DNAME column attributes and change Display Type to Display as Text (based on LOV, escape special characters).<br>Set Named List of Values to DEPARTMENT</p><p><img src="https://static.jaris.fi/b/i/P201406180631350353/eir2_dname_column.png" alt="Department column attributes" height="493" width="685"></p><p>Set column formatting</p><pre class="blog-program-code">&lt;select class="ir-edit-input" data-lov="DEPARTMENT" data-last-value="&#35;DEPTNO&#35;" id="N-&#35;SEQ_ID&#35;-5"&gt;&lt;/select&gt;</pre><p><img src="https://static.jaris.fi/b/i/P201406180631350353/eir2_dname_column_formatting.png" alt="Department column formatting" height="210" width="590"></p><p>Setup for other columns see <a href="/ords/r/jaris/blog/post?p2_post_id=201402122308060084">previous blog post</a>.</p><p>Create After Footer PL/SQL process:</p><pre class="blog-program-code">DECLARE
      cur  SYS_REFCURSOR;
      lov_d VARCHAR2(4000);
      lov_r VARCHAR2(4000);
      l_cnt PLS_INTEGER;
    BEGIN
      htp.prn('&lt;script type="text/javascript"&gt;');
      FOR c1 IN (
        SELECT list_of_values_query
         ,list_of_values_name
        FROM apex_application_lovs
        WHERE application_id = :APP_ID
        AND list_of_values_name IN('MANAGER', 'DEPARTMENT')
      ) LOOP
        l_cnt := 0;
        OPEN cur FOR c1.list_of_values_query;
        LOOP
        FETCH cur INTO lov_d, lov_r;
        EXIT WHEN cur%NOTFOUND;
          l_cnt := cur%ROWCOUNT;
          IF l_cnt = 1 THEN
            htp.prn('var ' || c1.list_of_values_name || ' = [{');
          ELSE
            htp.prn(',{');
          END IF;
          htp.prn('"D":"' || lov_d || '","R":"' || lov_r || '"}');
        END LOOP;
        CLOSE cur;
        IF l_cnt = 0 THEN
          htp.p('var ' || c1.list_of_values_name || ' = [];');
        ELSE
          htp.p('];');
        END IF;
      END LOOP;
      htp.prn('&lt;/script&gt;');
    END;</pre><p>This process will print LOV values as JSON objects.</p><p>We need JavaScript function that creates select list options from JSON objects.<br>Add to page JavaScript Function and Global Variable Declaration:</p><pre class="blog-program-code">(function($){
     $.fn.htmldbCreateSelect = function(){
      this.filter("select").empty().each(function(i){
       lSelf = $(this);
       lov = eval(lSelf.data("lov"));
       if(lSelf.data("display-null")){
        lSelf.append($("&lt;option/&gt;",{"value":"","text":lSelf.data("display-null")}));
       }
       $.each(lov,function(i,d){
        lSelf.append($("&lt;option/&gt;",{"value":d["R"],"text":d["D"]}));
       });
       lSelf.val(lSelf.data("last-value"));
      });
      return this;
     }
    })(apex.jQuery);</pre><p>Next create dynamic action</p><ul><li>Name: Edit IR</li><li>Event: After Refresh</li><li>Selection Type: Region</li><li>Region: <i>{select IR region}</i></li><li>Condition: -No Condition-</li><li>Action: Execute JavaScript code</li><li>Fire On Page Load: True</li><li>Selection Type: None</li><li>Code:</li></ul><pre class="blog-program-code">$(this.triggeringElement)
    .find("td .ir-edit-input")
    .change({region:this.triggeringElement},function(e){
     var $lThis=$(this), $lRegion=$(e.data.region);
     apex.server.process("SAVE_DATA",{
      x01:this.id,
      f01:$s_Split(this.value, 4000)
     },{
      dataType:"html",
      beforeSend:function(){
       $lThis.prop("disabled", true).parent("td").addClass("ui-autocomplete-loading");
      },
      success:function(pData){
       if(pData!=="OK"){
        alert(pData);
        $lThis.val($lThis.data("last-value")).prop("disabled", false).parent("td").removeClass("ui-autocomplete-loading");
        return false;
       }
       /* Refresh IR after value is saved */
       $lRegion.trigger("apexrefresh");
      }
     });
    })
    .htmldbCreateSelect()
    .filter("[id^=D]")
    .datepicker({
     dateFormat:"dd.mm.yy",
     constrainInput:true,
     changeMonth:true,
     changeYear:true,
     showButtonPanel:true,
     showOn:"both",
     buttonImageOnly:true,
     buttonImage:"&#35;IMAGE_PREFIX&#35;asfdcldr.gif",
     showAnim:"slideDown"
    });
    </pre><p>Then create On Demand process called SAVE_DATA</p><pre class="blog-program-code">DECLARE
      l_val      VARCHAR2(32700);
      l_len      PLS_INTEGER := 0;
      l_data     CLOB;
      l_date_val DATE;
      l_num_val  NUMBER;
      l_arr      APEX_APPLICATION_GLOBAL.VC_ARR2;
    BEGIN
      l_len := APEX_APPLICATION.G_f01.COUNT;
     
      IF l_len &gt; 0 THEN
        l_val := APEX_APPLICATION.G_f01(1);
      END IF;
      l_arr := APEX_UTIL.STRING_TO_TABLE(APEX_APPLICATION.G_X01, '-');
      IF l_arr(1) = 'C' THEN
        APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE (
          p_collection_name =&gt; 'EMP',
          p_seq             =&gt; l_arr(2),
          p_attr_number     =&gt; l_arr(3),
          p_attr_value      =&gt; l_val
        );
      ELSIF l_arr(1) = 'N' THEN
        BEGIN
          l_num_val := TO_NUMBER(l_val);
        EXCEPTION WHEN VALUE_ERROR
        THEN
          HTP.PRN(q'[Couldn't save value. Please check that value is valid number.]');
          RETURN;
        END;
        APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE (
          p_collection_name =&gt; 'EMP',
          p_seq             =&gt; l_arr(2),
          p_attr_number     =&gt; l_arr(3),
          p_number_value    =&gt; l_num_val
        );
      ELSIF l_arr(1) = 'D' THEN
        BEGIN
          l_date_val := TO_DATE(l_val, 'DD.MM.YYYY');
        EXCEPTION WHEN OTHERS
        THEN
          HTP.PRN(q'[Couldn't save value. Please check that value is valid date.]');
          RETURN;
        END;
        APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE (
          p_collection_name =&gt; 'EMP',
          p_seq             =&gt; l_arr(2),
          p_attr_number     =&gt; l_arr(3),
          p_date_value      =&gt; l_date_val
        );
      ELSIF l_arr(1) = 'CLOB' THEN
       
        IF l_len = 0
        OR COALESCE(LENGTH(APEX_APPLICATION.G_f01(1)), 0) = 0
        THEN
          APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE (
            p_collection_name =&gt; 'EMP',
            p_seq             =&gt; l_arr(2),
            p_clob_number     =&gt; 1,
            p_clob_value      =&gt; NULL
          );
        ELSE
         
          dbms_lob.createtemporary(
            lob_loc =&gt; l_data,
            cache   =&gt; TRUE,
            dur     =&gt; dbms_lob.session
          );
          dbms_lob.open(l_data, dbms_lob.lob_readwrite);
       
          FOR i IN 1 .. l_len
          LOOP
            dbms_lob.writeappend(
              lob_loc =&gt; l_data,
              amount  =&gt; LENGTH(APEX_APPLICATION.G_f01(i)),
              buffer  =&gt; APEX_APPLICATION.G_f01(i)
            );
          END LOOP;
          dbms_lob.close(l_data);
          APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE (
            p_collection_name =&gt; 'EMP',
            p_seq             =&gt; l_arr(2),
            p_clob_number     =&gt; 1,
            p_clob_value      =&gt; l_data
          );
        END IF;
      END IF;
     
      HTP.PRN('OK');
    END;</pre><p>And for last add to page CSS inline:</p><pre class="blog-program-code">.ui-autocomplete-loading {
     background: url("&#35;IMAGE_PREFIX&#35;libraries/jquery-ui/1.8/themes/base/images/ui-anim_basic_16x16.gif") no-repeat scroll 98% 10% &#35;EFEFEF !important;
    }
    .apexir_WORKSHEET_DATA td {
     padding-right: 22px !important;
     white-space: nowrap;
     vertical-align: top;
    }
    .apexir_WORKSHEET_DATA .ir-edit-input {
     color: inherit;
     text-align: inherit;
    }</pre><p>Now when you run page, you should have "editable IR" where is select list for manager and department, and text area for notes.</p><p><img src="https://static.jaris.fi/b/i/P201406180631350353/eir2_apex42_example.png" alt="APEX 4.2 editable interactive report" height="299" width="685"></p><p><a href="https://apex.oracle.com/pls/apex/f?p=39006:47" target="_blank">See working example</a>.</p><p>Sample is also available for <a href="https://static.jaris.fi/b/download/editable_ir_apex42.zip">download</a>.</p></div>]]></content:encoded>
      <pubDate>Wed, 18 Jun 2014 04:31:00 GMT</pubDate>
      <guid isPermaLink="false">201406180631350353</guid>
    </item>
  </channel>
</rss>
