How to Add a Comments Thread to Orders in Oracle APEX

A comments table with declarative defaults, the Comments page wizard, a new comment form, and a Delete action that the server limits to the author.

A comments thread lets users discuss a record right where they work on it. Sales notes a delivery request, the warehouse confirms the stock, and everyone sees who said what and when, without emails or spreadsheets on the side.

Oracle APEX has a Comments page type and a Comments region built on a template component. This guide stores the comments of each order in its own table, shows them as a chat thread with initials and "time ago" dates, adds a form for new comments, and lets users delete only their own comments, checked on the server.

The guide How to Build an Approval Flow for Orders with Human Tasks in Oracle APEX used the comments that come with a human task. Those comments belong to the task. The thread in this guide belongs to the order itself, so you can use the same pattern for customers, products or any other table.

Step 1: Create the Tables

The comments belong to rows of the ORDERS table. If you start from scratch, create ORDERS and CUSTOMERS with two sample orders, 149 and 150:

Example:

create table customers (
    customer_id number generated by default as identity
                constraint customers_pk primary key,
    name        varchar2(100) not null,
    email       varchar2(120) not null
);

create table orders (
    order_id    number generated by default as identity
                constraint orders_pk primary key,
    customer_id number not null
                constraint orders_customer_fk references customers,
    order_date  date default sysdate not null,
    status      varchar2(12) default 'NEW' not null
                constraint orders_status_ck
                check (status in ('NEW', 'APPROVED', 'SHIPPED', 'CANCELLED'))
);

create index orders_customer_ix on orders (customer_id);

insert into customers (customer_id, name, email)
values (4, 'Customer 4', 'customer4@example.com');
insert into customers (customer_id, name, email)
values (11, 'Customer 11', 'customer11@example.com');

insert into orders (order_id, customer_id, order_date, status)
values (149, 4, trunc(sysdate) - 4, 'CANCELLED');
insert into orders (order_id, customer_id, order_date, status)
values (150, 11, trunc(sysdate) - 3, 'NEW');
commit;

alter table customers modify customer_id
    generated by default as identity (start with limit value);
alter table orders modify order_id
    generated by default as identity (start with limit value);

The inserts use fixed IDs, so the two ALTER TABLE statements move the identity columns past them. New rows then get the next free number.

Now create the ORDER_COMMENTS table with a few sample comments:

Example:

create table order_comments (
    comment_id   number generated by default as identity
                 constraint order_comments_pk primary key,
    order_id     number not null
                 constraint order_comments_order_fk
                 references orders on delete cascade,
    comment_text varchar2(1000) not null
                 constraint order_comments_text_ck
                 check (trim(comment_text) is not null),
    created_by   varchar2(255) default on null
                 coalesce(sys_context('APEX$SESSION', 'APP_USER'),
                          sys_context('USERENV', 'SESSION_USER')),
    created_on   date default on null sysdate
);

create index order_comments_order_ix on order_comments (order_id, created_on);

insert into order_comments (order_id, comment_text, created_by, created_on)
values (150, 'Customer asked to deliver before Friday.', 'SALES_MANAGER', sysdate - 2);

insert into order_comments (order_id, comment_text, created_by, created_on)
values (150, 'Stock is reserved. Waiting for approval.', 'ORDER_EDITOR', sysdate - 1);

insert into order_comments (order_id, comment_text, created_by, created_on)
values (149, 'Cancelled at the customer''s request.', 'SALES_MANAGER', sysdate - 3);

insert into order_comments (order_id, comment_text)
values (149, 'Refund confirmed by finance.');

commit;

select comment_id, order_id, created_by,
       to_char(created_on, 'DD-MON HH24:MI') created_on, comment_text
from   order_comments
order  by order_id, created_on;

Output:

Table ORDER_COMMENTS created.

Index ORDER_COMMENTS_ORDER_IX created.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

   COMMENT_ID    ORDER_ID CREATED_BY       CREATED_ON      COMMENT_TEXT
_____________ ___________ ________________ _______________ ___________________________________________
            3         149 SALES_MANAGER    11-OCT 12:56    Cancelled at the customer's request.
            4         149 APXSOL           14-OCT 12:56    Refund confirmed by finance.
            1         150 SALES_MANAGER    12-OCT 12:56    Customer asked to deliver before Friday.
            2         150 ORDER_EDITOR     13-OCT 12:56    Stock is reserved. Waiting for approval.
  • ORDER_COMMENTS_ORDER_FK ties each comment to an order. ON DELETE CASCADE removes the comments when the order is deleted.
  • ORDER_COMMENTS_TEXT_CK refuses a comment that has only spaces.
  • CREATED_BY and CREATED_ON are filled by DEFAULT ON NULL, so no trigger is needed. Inside APEX, CREATED_BY gets the APEX user from APEX$SESSION. Outside APEX, it falls back to the database user.
  • That is why comment 4 shows APXSOL: it was inserted from SQL as the schema owner, not from an APEX page.
  • ORDER_COMMENTS_ORDER_IX covers the foreign key and the query of the thread, which reads the comments of one order sorted by date.

The constraints also work outside APEX. A blank comment and a comment for an order that does not exist both fail:

Example:

insert into order_comments (order_id, comment_text)
values (150, '   ');

insert into order_comments (order_id, comment_text)
values (9999, 'Order does not exist.');

Output:

Error starting at line : 1 File @ b31/01-constraints.sql
In command -
insert into order_comments (order_id, comment_text)
values (150, '   ')
Error report -
ORA-02290: check constraint (APXSOL.ORDER_COMMENTS_TEXT_CK) violated
Help: https://docs.oracle.com/error-help/db/ora-02290/

Error starting at line : 4 File @ b31/01-constraints.sql
In command -
insert into order_comments (order_id, comment_text)
values (9999, 'Order does not exist.')
Error report -
ORA-02291: integrity constraint (APXSOL.ORDER_COMMENTS_ORDER_FK) violated -
parent key not found
Help: https://docs.oracle.com/error-help/db/ora-02291/

Step 2: Create the Page with the Comments Wizard

In App Builder, open the application and click Create Page. On the Component tab, click Comments:

Create a Page wizard with the Comments tile marked with a red box
The Comments page type.

Enter Page Number 290 and Name Order Comments. In the Navigation section, turn off Use Breadcrumb, keep Use Navigation on, and set Icon to fa-comments. Click Create Page:

Create Comments wizard with Page Number 290, Name Order Comments, Use Breadcrumb off, Use Navigation on and Icon fa-comments marked with red boxes
The only screen of the Comments wizard.

The wizard has no data source screen. It creates a Comments region on the built-in sample data (Tasks) and a select list P290_ORDER_BY to sort it:

Page Designer after the wizard with the region Order Comments, its Columns, Actions and the item P290_ORDER_BY marked with a red box
The generated page uses sample data.

The next steps replace the sample data with ORDER_COMMENTS. Right-click P290_ORDER_BY and choose Delete. A thread is always sorted by date, so the sort item is not needed.

Step 3: Add the Order Selector

Right-click Body and choose Create Region. Set Name to Order and keep Type Static Content. Drag it above the comments region.

Right-click the Order region and choose Create Page Item. Set Name to P290_ORDER_ID, Type to Select List and Label to Order. In the Settings group, set Page Action on Selection to Redirect and Set Value. In List of Values, set Type to SQL Query, enter this query and set Null Display Value to - Select Order -:

Example:

select 'Order ' || o.order_id || ' - ' || c.name
       || ' (' || o.status || ')' as display_value,
       o.order_id as return_value
from   orders o
join   customers c on c.customer_id = o.customer_id
order by o.order_id desc
Page Designer showing P290_ORDER_ID with Type Select List, Label Order, Page Action on Selection Redirect and Set Value, List of Values SQL Query and Null Display Value marked with red boxes
Picking an order reloads the page for that order.

Redirect and Set Value stores the order in session state and reloads the page. The thread and the new comment form then work on that order.

Add one more item to the Order region: P290_COMMENT_ID, Type Hidden. Turn off Value Protected. The Delete action of Step 7 sets this item in the browser. Value Protected is meant for items whose value never changes on the client.

Page Designer showing P290_COMMENT_ID with Type Hidden and Value Protected off marked with red boxes
A hidden item for the comment to delete.

Step 4: Point the Comments Region to the Table

Click the region Order Comments and set Name to Comments. In the Source group, set Location to Local Database, Type to SQL Query, and enter this query. Set Page Items to Submit to P290_ORDER_ID:

Example:

select comment_id,
       comment_text,
       created_by,
       created_on,
       apex_string.get_initials(created_by) as initials
from   order_comments
where  order_id = :P290_ORDER_ID
order by created_on, comment_id
Page Designer showing the region Comments with Type Comments, Source Type SQL Query, the query and Page Items to Submit P290_ORDER_ID marked with red boxes
The thread reads the comments of the selected order.

APEX_STRING.GET_INITIALS builds the avatar text from the user name, for example SM for SALES_MANAGER and OE for ORDER_EDITOR.

In the Server-side Condition group, set Type to Item is NOT NULL and Item to P290_ORDER_ID. Before an order is selected, the region is not shown:

Page Designer showing the Server-side Condition of the Comments region with Type Item is NOT NULL and Item P290_ORDER_ID marked with a red box
Show the thread only after an order is selected.

Open the Attributes tab. Map the columns of the query to the parts of a comment: Comment Text to COMMENT_TEXT, User Name to CREATED_BY and Date to CREATED_ON. In the Avatar group, set Type to Initials and Initials to INITIALS. Keep Style Chat (Speech Bubbles).

Attributes tab of the Comments region with Comment Text, User Name, Date, Avatar Type Initials and Initials INITIALS marked with red boxes
Map the columns to the parts of a comment.

Further down in the same tab, set When No Data Found in the Messages group to No comments yet.

Expand Columns under the region and click CREATED_ON. Set Format Mask to SINCE. APEX then shows the date as "2 days ago" or "24 hours ago" instead of a timestamp:

Page Designer showing the column CREATED_ON with Format Mask SINCE marked with a red box
The SINCE format mask shows how long ago a comment was written.

Step 5: Add the New Comment Form

Right-click Body and choose Create Region. Set Name to Add a Comment and keep Type Static Content. Give it the same server-side condition as the thread: Item is NOT NULL, P290_ORDER_ID.

Right-click the new region and choose Create Page Item. Set Name to P290_COMMENT, Type to Textarea and Label to Comment. In the Validation group, turn on Value Required and set Maximum Length to 1000, the size of the column:

Page Designer showing P290_COMMENT with Type Textarea, Label Comment, Trim Spaces Leading and Trailing, Value Required on and Maximum Length 1000 marked with red boxes
The comment text item.

Keep Trim Spaces at its default, Leading and Trailing. A comment with only spaces then becomes empty, and Value Required stops it before it reaches the check constraint.

Right-click the region again and choose Create Button. Set Button Name to ADD_COMMENT and Label to Add Comment, turn on Hot, and keep Action Submit Page:

Page Designer showing the button ADD_COMMENT with Label Add Comment, Region Add a Comment, Hot on and Action Submit Page marked with red boxes
The button submits the page.

Step 6: Save the Comment and Return to the Order

Open the Processing tab, right-click Processes and choose Create Process. Set Name to Add comment, keep Type Execute Code, and enter this PL/SQL Code. Set Success Message to Comment added.

Example:

insert into order_comments (order_id, comment_text)
values (:P290_ORDER_ID, :P290_COMMENT);
Page Designer showing the process Add comment with Type Execute Code, the insert statement and Success Message Comment added marked with red boxes
The process inserts the comment.

The insert gives only the order and the text. The DEFAULT ON NULL clauses of Step 1 fill in the author and the time. Nobody can post a comment in someone else's name, because the page never sends a user name.

In the Server-side Condition group, set When Button Pressed to ADD_COMMENT:

Page Designer showing the process Add comment with When Button Pressed ADD_COMMENT marked with a red box
Run the process only for the Add Comment button.

Right-click Branches under After Processing and choose Create Branch. Set Name to Back to page 290. Set the Target to page 290, set item P290_ORDER_ID to &P290_ORDER_ID., and clear the cache of page 290. Clearing the cache empties the textarea for the next comment:

Page Designer showing the branch Back to page 290 with Type Page or URL and Target page 290 marked with red boxes
Return to the same order after saving.

Step 7: Add a Delete Action for Your Own Comments

Template components such as Comments can show actions on each row. In the Rendering tree, right-click Actions under the Comments region and choose Create Action. Set Position to Actions, Label to Delete and Type to Trigger Action.

In the Server-side Condition group, set Type to Expression, Language to PL/SQL, PL/SQL Expression to :CREATED_BY = :APP_USER, and keep Execute Condition For Each Row:

Page Designer showing the action Delete with Position Actions, Label Delete, Type Trigger Action and the server-side condition :CREATED_BY = :APP_USER For Each Row marked with red boxes
A Delete link only on the comments of the current user.

For Each Row checks the condition once per comment, and the condition can read the columns of that row. APEX renders the Delete link only on the comments that the signed-in user wrote.

Type Trigger Action adds a Triggered Actions node under the action. These work like the true actions of a dynamic action. APEX creates one Execute Server-side Code action there. Add three more and order them as shown in the next screenshots.

First, a Confirm action with Message Delete this comment?:

Page Designer showing the triggered action Confirm with the message Delete this comment? marked with red boxes
Ask before deleting.

Second, a Set Value action. Set Set Type to JavaScript Expression, enter this.recordId, and set Affected Elements to the item P290_COMMENT_ID:

Page Designer showing the triggered action Set Value with Set Type JavaScript Expression, this.recordId and Item P290_COMMENT_ID marked with red boxes
Copy the ID of the clicked comment into the hidden item.

Each Delete link carries the primary key of its row, the COMMENT_ID column. In a test that logged the context of the triggered action, this.recordId held that value.

Third, the Execute Server-side Code action. Enter this PL/SQL Code and set Items to Submit to P290_COMMENT_ID:

Example:

delete from order_comments
where  comment_id = :P290_COMMENT_ID
and    created_by = :APP_USER;

if sql%rowcount = 0 then
    raise_application_error(-20001,
        'You can delete only your own comments.');
end if;
Page Designer showing the triggered action Execute Server-side Code with the delete statement and Items to Submit P290_COMMENT_ID marked with red boxes
The server deletes only a comment of the current user.

The condition on the action only hides the link. The real rule is the created_by = :APP_USER filter in the DELETE statement. If the ID belongs to another user, no row is deleted and the code raises an error.

Last, a Refresh action on the region Comments, so the deleted comment disappears without a page reload:

Page Designer showing the triggered action Refresh with Selection Type Region and Region Comments marked with red boxes
Refresh the thread after the delete.

Click Save.

Step 8: Test the Comments Thread

Run the page as ORDER_EDITOR and select Order 150. The thread shows two comments. Only the comment written by ORDER_EDITOR has a Delete link:

Order Comments page for order 150 with two comments and the Delete link of the ORDER_EDITOR comment marked with a red box
The thread of order 150.

Enter only spaces in Comment and click Add Comment. Trim Spaces removes them, and Value Required shows an error:

Error Comment must have some value. under the Comment field marked with a red box
An empty comment is refused.

Enter Packed and ready to ship tomorrow. and click Add Comment. The success message appears, and the new comment shows at the end of the thread with the initials OE:

Success message Comment added. and the new comment Packed and ready to ship tomorrow. by ORDER_EDITOR marked with red boxes
The new comment.

Now sign in as SALES_USER, open the same order, and add Customer called to confirm the delivery address. SALES_USER sees a Delete link only on this new comment. The comments of ORDER_EDITOR have none:

Thread of order 150 as SALES_USER with four comments and a Delete link only on the SALES_USER comment, marked with a red box
Each user can delete only their own comments.

Sign in as ORDER_EDITOR again and click Delete on Packed and ready to ship tomorrow. The confirmation appears:

Confirmation dialog Delete this comment? with Cancel and OK buttons marked with a red box
The Confirm triggered action.

Click OK. The comment is deleted and the thread refreshes without reloading the page:

Thread of order 150 after the delete with three comments left
The comment is gone.

Step 9: Try to Delete Someone Else's Comment

A hidden link is not a security check. To test the server, run this in the browser console as ORDER_EDITOR on order 150. It makes the Set Value step write 26, the ID of the SALES_USER comment, and then clicks ORDER_EDITOR's own Delete link:

Example:

const item = apex.item("P290_COMMENT_ID");
const setOriginal = item.setValue.bind(item);
item.setValue = () => setOriginal("26");
document.querySelector(".t-Comments-actions a").click();

Click OK in the confirmation. The server refuses the delete:

Output:

Ajax call returned server error ORA-20001: You can delete only your own comments. for Execute Server-Side Code.
Error dialog Ajax call returned server error ORA-20001: You can delete only your own comments. marked with a red box
The DELETE statement found no row of this user.

In a first try, the console script only changed the recordId in the href of the Delete link and clicked it. Nothing happened at all: APEX did not run the triggered actions for a record that has no Delete action on the page. The script above goes one step further and still fails on the server.

Check the comments of order 150 in SQL. The SALES_USER comment is still there, and only the deleted comment of ORDER_EDITOR is gone:

Example:

select comment_id, created_by,
       to_char(created_on, 'DD-MON HH24:MI') created_on, comment_text
from   order_comments
where  order_id = 150
order  by created_on;

Output:

   COMMENT_ID CREATED_BY       CREATED_ON      COMMENT_TEXT
_____________ ________________ _______________ ___________________________________________________
            1 SALES_MANAGER    12-OCT 12:56    Customer asked to deliver before Friday.
            2 ORDER_EDITOR     13-OCT 12:56    Stock is reserved. Waiting for approval.
           26 SALES_USER       14-OCT 13:18    Customer called to confirm the delivery address.

Finally, deleting an order removes its comments through the cascade. This test creates an order with a comment, deletes the order, and rolls back:

Example:

variable order_id number

begin
    insert into orders (customer_id, order_date, status)
    values (1, sysdate, 'NEW')
    returning order_id into :order_id;

    insert into order_comments (order_id, comment_text)
    values (:order_id, 'Test comment for a new order.');
end;
/

select count(*) as comments_before
from   order_comments
where  order_id = :order_id;

delete from orders where order_id = :order_id;

select count(*) as comments_after
from   order_comments
where  order_id = :order_id;

rollback;

Output:

PL/SQL procedure successfully completed.

   COMMENTS_BEFORE
__________________
                 1

1 row deleted.

   COMMENTS_AFTER
_________________
                0

Rollback complete.

Things to Know

  • The Comments page wizard has only one screen and always starts on sample data. Change the region source to your own table right after creating the page.
  • A server-side condition with Execute Condition For Each Row can read the columns of each row, so actions can depend on the data of the row.
  • In a triggered action, this.recordId holds the key of the row whose action was clicked. In this example, COMMENT_ID is the column marked as Primary Key in the region columns.
  • Hiding a link is not security. Always repeat the rule in the PL/SQL that changes the data, here created_by = :APP_USER.
  • The error from Execute Server-side Code is shown as "Ajax call returned server error ... for Execute Server-Side Code." It is fine for a forged request, which a normal user never sends.
  • The SINCE format mask shows "1 seconds ago" for a brand-new comment and "Now" for a few moments before that.
  • To add comments to customers or products, copy the table with a foreign key to that table and change the order selector and the WHERE clause.

Related Guides

Conclusion

To add a comments thread to orders in Oracle APEX, store the comments in a child table with DEFAULT ON NULL for the author and the date, create a page with the Comments wizard, and point its region to that table. A textarea and one insert process add new comments.

A Delete action with a For Each Row condition shows the link only on the user's own comments. Its triggered actions confirm, pass the record ID, delete on the server with a created_by check, and refresh the thread, so even a forged request cannot remove another user's comment.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted