It was the standard, so I never read it

I pasted the same Interactive Grid toolbar JavaScript into three applications over three years without ever reading it, because I was told it was the standard and it worked. A colleague who had never touched the block reviewed a story that included the script, pointed out some unused variables.

Share
It was the standard, so I never read it

Three applications, three years, and a colleague with no history with the code who spotted it on the first read.

TL;DR: I pasted the same Interactive Grid toolbar JavaScript into three applications over three years without ever reading it, because I was told it was the standard and it worked. A colleague who had never touched the block reviewed a story that included the script, pointed out some unused variables. That got me to read the rest and make some changes. Most of what was wrong in it had nothing to do with APEX. Exactly one item was APEX genuinely adding something new.


I did not write the Interactive Grid toolbar code in my applications. I inherited it. It was handed to me as the way we do buttons on a grid. I pasted it in, it worked, and that was the end of my involvement with it for three years.

Three applications. Never read it once. Then a colleague who had never used that block picked up my story for review that happened to include it, and when the review came back, he pointed at variables that were declared and never used. Small thing but it made me realise that I had been shipping something for three years that I could not explain.

Being told something is the standard was enough reason for me not to read it. And the person who saw the problem was the one with no history with it, looking at it for the first time, in a review that was supposed to be about something else.


The three piles

So I read it properly, not because anything was broken, but because I could not say I knew what it did.

I sorted what I found into three piles: never right, still fine, and actually outdated. Separating those turned out to be the useful part.

The example in this article is a review queue, a grid of drafts sitting in PENDING where a reviewer selects rows and clicks Approve or Reject. I wanted to tell the story with a project that is topical, but the pattern is generic. Anywhere you select rows and act on them server-side, this is the shape.

(For readers outside the Oracle world: an Interactive Grid is APEX's data grid component. Its toolbar is built from a JavaScript metadata array you can copy and modify, and buttons on it fire named actions rather than raw click handlers.)


Pile one: never right

Nothing in this pile is an APEX limitation. These were true on the day the code was written and are still true now.

The unused variables

Start with the one that got me here, because it is the smallest and it is the reason for everything else. Variables declared at the top of the handler, assigned, never read. On its own that costs nothing at runtime. What it costs is trust: if nobody noticed those in three years of the block being copied around, nobody has read the rest of it either. Dead code is not usually a bug. It is a signal about who has looked.

The magic index

The block built its toolbar like this:

var toolbarData = $.apex.interactiveGrid.copyDefaultToolbar();
toolbarData.splice(4, 1);
var group = toolbarData[4];
group.controls.push(/* my button */);

It splices out a group at index 4, then grabs index 4 again, except after the splice that is whatever shifted into the slot. Two guesses stacked on top of each other about a layout nobody controls. The default toolbar's group count changes depending on the region's configuration, things like saved reports, edit mode, and multiple views, and it can shift with an upgrade too. When that happens nothing errors. The buttons just end up somewhere else.

Now toolbarFind() locates a group by name instead of position. It has been in APEX since 5.1.1. and that was 2017. This was never a limitation anyone was working around. It was a shortcut that happened to work in whichever app it was first written for, and then it travelled. The number is no longer typed by hand. idx is computed from where the group actually is, not assumed from where it used to be.

const $ = apex.jQuery;
const toolbarData = $.apex.interactiveGrid.copyDefaultToolbar();
const actionsGroup = toolbarData.toolbarFind("actions1");   // by identity, not position
const idx = toolbarData.indexOf(actionsGroup);

// Own group, inserted right after Actions. The toolbar draws its divider
// at group boundaries, so this renders as:  Actions | Approve  Reject
toolbarData.splice(idx + 1, 0, {
  controls: [
    { type: "BUTTON", action: "approve-rows", icon: "fa fa-check-circle-o", iconBeforeLabel: true, hot: true },
    { type: "BUTTON", action: "reject-rows",  icon: "fa fa-ban",            iconBeforeLabel: true }
  ]
});
config.toolbarData = toolbarData;

Two copies of the same handler

Two action handlers: one for each button. It was roughly forty lines each, byte for byte identical except the process name. Not an APEX issue and never was, just plain duplication of the kind copy-paste breeds. Changed to one parameterised function:

config.initActions = function (actions) {
  actions.add({ name: "approve-rows", label: "Approve", action: () => processRows("APPROVE_CONTENT") });
  actions.add({ name: "reject-rows",  label: "Reject",  action: () => processRows("REJECT_CONTENT") });
};

Pile two: still fine, but there is a nicer way now

This one is softer. It was not wrong, the ergonomics just improved.

The block reported everything through alert():

success: json => { if (!json.success) alert(json.message); },
error:   request => alert(request.responseText)

alert() was never great. It blocks the page, it is unstyled, and dumping request.responseText into it can show the user a full HTML error page instead of a message. But it was the easy default for a long time and I understand why people reached for it. apex.message is not new either, it is just less typing than it used to feel like, and it does not block the page.

success: json => {
  if (json.success) {
    apex.message.showPageSuccess(json.processed + " row(s) processed.");
    apex.region("content_queue").refresh();
  } else {
    apex.message.showErrors([{ type: "error", location: "page", message: json.message }]);
  }
}

Pile three: APEX genuinely grew a feature

Out of the whole block, exactly one change was about APEX.

The block gathered the selection by hand:

const grid    = apex.region("content_queue").widget().interactiveGrid("getViews", "grid");
const model   = grid.model;
const records = grid.view$.grid("getSelectedRecords");
const ids     = records.map(r => model.getValue(r, "ID"));

For years this was simply the way to get a grid's selection to the server. It is not wrong. It was the state of the art and it is still the code I would write on any version before 24.1.

APEX 24.1 added selectionStateItem: a grid option that keeps a page item populated with the current selection as a colon-delimited list of the selected rows, refreshed on every selection change. You set it in the Initialization JavaScript Function and read the item server-side.

function (config) {
  config.defaultGridViewOptions = { selectionStateItem: "P1_SELECTED_IDS", multiple: true };
  return config;
}

This still works with the button-and-callback setup from the original. You submit the item with the AJAX process instead of building the ID list yourself:

apex.server.process("APPROVE_CONTENT",
  { pageItems: "#P1_SELECTED_IDS" },
  { dataType: "json", loadingIndicator: "#content_queue", loadingIndicatorPosition: "centered",
    success: json => { /* ... */ } });

selectionStateItem wins when the grid is read-only with a single-key identity and you only need the primary keys server-side. That is most of the time. Less JavaScript to write means fewer lines to get wrong, and the IDs sit in an item any process can read. On a new grid, this is my default now.

The manual getSelectedRecords loop still wins when you need the full record objects at click time, things like the current status, a version column for optimistic locking, or a display value. It wins because it hands you the model records rather than just keys. And it is the only option that works before 24.1, which matters if your clients are not all on the latest release.

One thing neither approach fixes: the client is not trusted either way. A page item and a hand-built ID list are equally easy to fake. Whatever your server process does with those IDs still needs to check whether this user is allowed to act on these rows.


What I did not change

The architecture. Selecting rows and firing an AJAX callback that returns a JSON verdict is still the right shape when the action should happen without a full page submit. The region is greyed out, the rows process, the grid refreshes in place.

Reading through inherited code did not mean rewriting all of it. Most of the structure held up fine. The problems were in the details.


The takeaway

Sort what you find into three piles: never right, still fine, actually outdated. The framework pile is almost always the smallest. In this block, the toolbar index was a shortcut with a documented alternative available since 5.1.1, the duplicated handler was ordinary copy-paste, and alert() was lazy but understandable. Exactly one item, selectionStateItem, was APEX adding something new.

But the pile-sorting is the easy lesson. The harder one is that I had three years and three applications to notice any of this and did not. All because "this is the standard" answered the question for me before I asked it. The person who saw it had no history with the code, was looking at it for the first time.

Fresh eyes on code that already works is not extra effort you can skip. It is the only thing that catches this, because everyone with history has stopped seeing it.

The code shown is illustrative. Region IDs, table, and columns are a review-queue stand-in for the pattern, not a specific application.