Tuesday, September 14, 2010

Adding a Context Menu to a Tree Region

One of the nice new features of Application Express is the new tree region. It is based on jsTree and supports features such as tool-tips. jsTree is the same tree used for the new tree view of the Application Builder Page Definition page. That page supports a right click context menu which is missing from the current version of the Application Express new tree region. With a little help from Patrick, I was able to add a context menu to a tree region I was working on for an update to the Document Library packaged application. The purpose of this article is to describe how to do this for your tree regions. I will use the EMP table as a simple example.


Disclaimer:
Patrick wanted me to mention that we may change the implementation of the tree in the future, so you may have to adjust the JavaScript code listed here in future versions of Application Express.



Let's start by creating a copy of the emp table, emp2, so you don't actually mess with the emp data. Use the SQL Workshop SQL Command Processor and the code in listing 1 to create emp2.



Code Listing 1



create table emp2 as select * from emp
/

alter table emp2 add constraint emp2_pk primary key (empno)
/

alter table emp2 add constraint emp2_fk foreign key (mgr) references emp2(empno)
/






Create Application and Tree


With emp2 in place you are ready to create an application. Simply run the create application wizard and create a form and report on emp2. You will replace the report on page 1 with a tree region. After the application is created, edit page 1. Delete the report region and then create a new tree region specifying the following options:



  1. Display Attributes, Title: Employees

  2. Tree Template: Default

  3. Table/View: EMP2

  4. Confirm that all values are defaulted on the Query step

  5. Tooltip: Database Column

  6. Tooltip Column: HIREDATE

Run the page and confirm that your tree appears. When you right click on a node, you should only see that standard right click options of whatever browser you are using.




Create Context Menu


Now for the magic! By sprinkling a little JavaScript on the page here and there, we will get a nice right click context menu. First edit the tree region and give the region a static ID of EMP2 as in figure 1. We need this static ID to make it easier to write JavaScript and select this region with jQuery.


Figure 1







Now you just need to sprinkle a little JavaScript on the page! Edit the page definition of page one and add the code in code listing 2 into the Function and Global Variable Declaration text area and the code in code listing 3 into the Execute when Page Loads text area.


Code Listing 2

function doAction(pNode,pTree,a){
var l_action;
var l_id;

l_id = pNode.attr("id");

if (a=="create") { l_action = "f?p="+$v('pFlowId')+":2:"+$v('pInstance')+":::2:P2_MGR:"+l_id }
if (a=="delete") { deleteEmp(pNode,l_id); }
if (a=="edit") { l_action = "f?p="+$v('pFlowId')+":2:"+$v('pInstance')+"::::P2_EMPNO:"+l_id }

if (l_action != null) {document.location.href=l_action; }
}



Code Listing 3



var lTreeContextMenu={
items:{create:false,rename:false,remove:false,
contextmenu_create:{
label:"Add Employee",
icon: "",
visible: true,
action: function(pNode, pTree){doAction(pNode, pTree, "create");}
},
contextmenu_delete:{
label:"Delete",
icon: "",
visible: true,
action: function(pNode, pTree){doAction(pNode, pTree, "delete");}
},
contextmenu_edit:{
label:"Edit",
icon: "",
visible: true,
action: function(pNode, pTree){doAction(pNode, pTree, "edit");}
}
}};

// use jsTree to render the tree
var lTreeSel = apex.jQuery("#EMP2").find("div.tree");
var lTreeId = lTreeSel.attr("id");
var lDataId = lTreeId.replace("tree","");
var lTreeData = eval("l"+lDataId+"Data");
var lTree = lTreeSel.tree({
data:{
type:"json",
async:true,
opts:{
"static":lTreeData,
isTreeLoaded:false,
method:"POST",
url:"wwv_flow.show"
}
},
root:{
draggable:false,
valid_children: "folder"
},
folder:{
valid_children: "file"
},
file:{
valid_children: "none",
max_children: 0,
max_depth:0
},
opened:["7839"],
plugins:{contextmenu:lTreeContextMenu}
});

$.showTooltip = function(pEvent) {
var lAction = apex.jQuery(pEvent.target).attr("tooltip");
if (lAction && lAction != "") {
toolTip_enable(pEvent,this,apex.jQuery(this).attr("tooltip"));
}
}; // showTooltip

// Bind Tooltips for tree nodes
apex.jQuery('a[tooltip]', lTreeSel).bind("mouseover", $.showTooltip);

// Hack for right click problem on selected node
apex.jQuery("#EMP2").find("a").live("mouseup",function() {apex.jQuery("#EMP2").find("a").removeClass()});


Now when you run the page you should see a right click context menu when you right click on a node in your tree, like figure 2. Note that the Add Employee and Edit actions are implemented and working. The delete action is not yet working, that will be added next.




Figure 2




Implement Delete Action


Technically the delete action is already implemented. You could edit the employee and just click delete on the next page. What sounds like more fun is to call an on demand process using AJAX from some JavaScript, and then use jQuery to remove the node from the tree, display a confirmation message, all without doing a page submit. First things first though, create a region that will hold the message specifying the following options:

  1. Region Type: HTML

  2. Title: Message Container

  3. Region Template: No Template

  4. Sequence: 5

  5. Region Source: See Code Listing 4



Code Listing 4



<div class="success" id="success-message" style="display:none;">
<img src="#IMAGE_PREFIX#delete.gif" onclick="apex.jQuery('#success-message').hide()" style="float:right;" class="remove-message" alt="" />
<div id="theMessage">This is a placeholder for messages</div>
</div>



Now we have a place for messages. Next, create a process on page 1 with the following options:



  1. Process Type: PL/SQL

  2. Name: DELETE_EMPLOYEE

  3. Point: On Demand – Run this process when requested by AJAX

  4. PL/SQL Page Process: See Code Listing 5


Code Listing 5



begin
delete from emp2 where empno = apex_application.g_x01;
owa_util.mime_header('text/xml', FALSE );
htp.p('Cache-Control: no-cache');
htp.p('Pragma: no-cache');
owa_util.http_header_close;
htp.p('{result:"success",message:"Employee successfully deleted"}');
exception when others then
owa_util.mime_header('text/xml', FALSE );
htp.p('Cache-Control: no-cache');
htp.p('Pragma: no-cache');
owa_util.http_header_close;
htp.p('{result:"failed",message:"Error deleting employee: '||sqlerrm||'"}');
end;


Finally, you add the JavaScript to do the AJAX call, display the result message, and remove the node from the tree. Edit the page attributes and add the code in code listing 6 in the Function and Global Variable Declaration text area before the existing doAction function.


Code Listing 6



function confirmSubmit(){
var agree=confirm("Are you sure you wish to continue and delete?");
if (agree)
return true ;
else
return false ;
}

function deleteEmp(pNode,pId){
var lTest = confirmSubmit();
if (lTest) {
var get = new htmldb_Get(null,$v('pFlowId'),'APPLICATION_PROCESS=DELETE_EMPLOYEE',$v('pFlowStepId'));
get.addParam('x01',pId);
gReturn = get.get();
var j = eval("("+gReturn+")");
apex.jQuery("#theMessage").text(j.message);
apex.jQuery("#success-message").show();
if (j.result == "success") {
pNode.remove();
}

get = null;
}
}



Now run the page and unroll Clark. Right click on Miller and choose delete. Confirm the delete and the result should be similar to figure 3.


Figure 3






Tuesday, July 6, 2010

Application Express 4.0 Web Services Evaluation Guide

I just returned from ODTUG Kaliedoscope 2010, (awesome event, best one I have been to), and promised during my presentation that the Application Express 4.0 Web Services Evaluation Guide would be posted on the Application Express Web Services Integration page on OTN. Well it now is, so I kept my promise. ;)

If you missed me at ODTUG, you can see me give a very similar presentation this year at Oracle Open World. The session is at 12:30 on Thursday September 23rd, so don't get too crazy at the appreciation event the night before!

Update 7/28/2010: A patch set exception was created for the issue the Roel reported in the comments below. Log on to Metalink and search for 9848562.

Friday, February 26, 2010

Application Express 4.0 Early Adopter Phase II (EA2) Available

The Application Express 4.0 Early Adopter Phase II (EA2) is now available. Like EA1, this instance is running on Amazon EC2. The beauty of Amazon's elastic IP feature is that we could work on the EA2 instance and get it completely ready, then associate the elastic IP with the new instance, making the switch in less than 60 seconds. You can still access EA1 for the next couple of weeks in case you need anything from that instance. You will need to sign up for a new workspace on EA2.

One nugget I would pass on is that if you run Oracle in the cloud and use Elastic Block Storage for your data files, you should also change the parameter diagnostic_dest to point to your EBS volume instead of the volatile volume that only lives with the instance. The reason is that you could fill up that volume pretty quickly if there is a lot of action on your instance and cause your database to shutdown.

Have fun with EA2 and especially kicking the tires of Websheets!

Friday, January 8, 2010

OTN Developer Days in NYC

Are you in the NY, NY area? If so you should definitely attend the free OTN Developer Day- Hands-on Oracle Database 11g Applications Development event at the New York Marriot Marquis on January 13, 2010. I will not be there, but much smarter colleagues of mine will like Mike Hichwa (VP of Development Tools, father of Application Express), Marc Sewtz, David Peake and Christina Cho.


Bring your own laptop for the labs.



Monday, December 21, 2009

Application Express 4.0 EA Running on Amazon EC2

The Application Express 4.0 evaluation instance is running on an Amazon EC2 instance. You can access it at http://tryapexnow.com. You may also be interested in checking out the APEX 4.0 Web Services Evaluation Guide here.

Friday, November 20, 2009

flex_ws_api now on samplecode.oracle.com

Tyler recently made me aware of http://samplecode.oracle.com/. It is a great place to share code projects, allow others to contribute and comment through discussion forums. I have set up a project for the flex_ws_api and all further development will take place there. I have created discussions for general questions, bug reports, and enhancement requests. Feel free to participate there. All you need is an OTN account to login.

Friday, October 23, 2009

REST Now Supported in flex_ws_api and Other Good Stuff

I have finally added support for consuming RESTful Web services in the flex_ws_api API. I have also added new globals in the API to keep track of cookies, HTTP headers and the response code returned from a service. There is also a global you can populate prior to calling any of the make_request procedure/functions that will send cookies along with the request to the Web service.

To demonstrate using the new features of the API, I will walk you through creating a new application that will call the RESTful version of Amazon's Product Advertising API. You may recall from a previous post that Amazon now requires that all requests to this API are signed with the developer's secret key. This example will assume that you have compiled the following in your schema: the new flex_ws_api, the java source hmacSHA256, the pl/sql function hmacSHA256, and finally the amazon_signature function.

First create a new application with one blank page called Product Search. Modify the application attributes and add the following substitutions:

  1. AWSACCESSKEY
  2. ASSOCIATETAG
  3. AWSSECRETKEY

Enter the appropriate values with the values that match your AWS credentials. You will need to sign up for an AWS Access Key ID which will also give you your secret code and you also should sign up to be an associate which will give you your associate tag.

Next create an HTML region called Product Search (if it was not already created by the create application wizard) and place the following items in that region:

  1. Name: P1_KEYWORDS, Display As: Text Field
  2. Name: P1_SEARCHINDEX, Display As: Select List, List of Values – Display Extra Values: No, List of values definition: STATIC2:All;All,Apparel;Apparel,Automotive;Automotive,Baby;Baby,Beauty;Beauty,Books;Books,Classical;Classical,DigitalMusic;DigitalMusic,DVD;DVD,Electronics;Electronics,GourmetFood;GourmetFood,HealthPersonalCare;HealthPersonalCare,HomeGarden;HomeGarden,Industrial;Industrial,Jewelry;Jewelry,KindleStore;KindleStore,Kitchen;Kitchen,Magazines;Magazines,Merchants;Merchants,Miscellaneous;Miscellaneous,MP3Downloads;MP3Downloads,Music;Music,MusicalInstruments;MusicalInstruments,MusicTracks;MusicTracks,OfficeProducts;OfficeProducts,OutdoorLiving;OutdoorLiving,PCHardware;PCHardware,PetSupplies;PetSupplies,Photo;Photo,Shoes;Shoes,SilverMerchants;SilverMerchants,Software;Software,SportingGoods;SportingGoods,Tools;Tools,Toys;Toys,UnboxVideo;UnboxVideo,VHS;VHS,Video;Video,VideoGames;VideoGames,Watches;Watches,Wireless;Wireless,WirelessAccessories;WirelessAccessories
  3. Name: P1_TIMESTAMP, Display As: Hidden and Protected

Create a Submit button on the page that submits the page and branches back to page 1.

You must compute the value of P1_TIMESTAMP to a format that Amazon is expecting. The computation includes a GMT offset at the end so your computation will depend on where your database server is relative to GMT. For me it was -08:00. Create a computation on page 1 with the following attributes:

  1. Item Name: P1_TIMESTAMP
  2. Type: PL/SQL Function Body
  3. Computation Point: After Submit
  4. Computation: return to_char(sysdate + 8/24,'YYYY-MM-DD')||'T'||to_char(sysdate,'hh24:mi:ss')||'-08:00';

Now you create the process on that page that uses the flex_ws_api to call the Amazon Product Advertising API REST Web service. Create a process on the page with the following attributes:

  1. Name: Call Amazon Product Search API
  2. Type: PL/SQL anonymous block
  3. Process Point: On Submit – After Computations and Validations
  4. Source: <see code listing 1>
  5. Conditional Processing, When Button Pressed: SUBMIT

Code Listing 1, Call Amazon Product Search API Process


declare
l_signature varchar2(4000);
l_response clob;
l_parm_name_tab wwv_flow_global.vc_arr2;
l_parm_val_tab wwv_flow_global.vc_arr2;
i number;
secure varchar2(1);
begin

--compute the signature
l_signature := amazon_signature('GET'||chr(10)||'ecs.amazonaws.com'||chr(10)||'/onca/xml'||chr(10)||'AWSAccessKeyId='||:AWSACCESSKEY||'&AssociateTag='||:ASSOCIATETAG||'&Keywords='||replace(:P1_KEYWORDS,' ','%20')||'&Operation=ItemSearch&ResponseGroup=ItemAttributes%2CImages&SearchIndex='||:P1_SEARCHINDEX||'&Service=AWSECommerceService&Timestamp='||apex_util.url_encode(:P1_TIMESTAMP)||'&Version=2009-03-31',:AWSSECRETKEY);

--create the tables of parameter names and values
l_parm_name_tab := apex_util.string_to_table('Service:Version:Operation:Keywords:SearchIndex:AWSAccessKeyId:AssociateTag:ResponseGroup:Timestamp:Signature');
--need to use ~ instead of : for separator since timestamp will contain :
l_parm_val_tab := apex_util.string_to_table('AWSECommerceService~2009-03-31~ItemSearch~'||:P1_KEYWORDS||'~'||:P1_SEARCHINDEX||'~'||:AWSACCESSKEY||'~'||:ASSOCIATETAG||'~ItemAttributes,Images~'||:P1_TIMESTAMP,'~');
--need to add l_signature to the table separately, it may contain any character
l_parm_val_tab(l_parm_val_tab.count + 1) := l_signature;

--make the REST request
l_response := flex_ws_api.make_rest_request(
p_url => 'http://ecs.amazonaws.com/onca/xml',
p_http_method => 'GET',
p_parm_name => l_parm_name_tab,
p_parm_value => l_parm_val_tab );

--populate a collection with the response
apex_collection.create_or_truncate_collection('P1_ITEMSEARCH_RESPONSE');
apex_collection.add_member(
p_collection_name => 'P1_ITEMSEARCH_RESPONSE',
p_clob001 => l_response );

--populate a collection for any response http headers returned
apex_collection.create_or_truncate_collection('P1_RESP_HEADERS');

for i in 1.. flex_ws_api.g_headers.count loop
apex_collection.add_member(
p_collection_name => 'P1_RESP_HEADERS',
p_c001 => flex_ws_api.g_headers(i).name,
p_c002 => flex_ws_api.g_headers(i).value,
p_c003 => flex_ws_api.g_status_code);
end loop;

end;

Next you create two SQL Report regions. The first one reports on the response from the Web service and shows the products that match the search term and the second shows the HTTP headers that are returned with the response. Create a SQL Report region with the following attributes:

  1. Title: Results
  2. Type: SQL Query
  3. Source: <see code listing 2>
  4. Conditional Display, Condition Type: PL/SQL Expression
  5. Conditional Display, Expression 1: apex_collection.collection_exists('P1_ITEMSEARCH_RESPONSE')

Code Listing 2, Results SQL Query



select extractValue(value(t),'/*/ASIN','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"') ASIN
, extractValue(value(t),'/*/DetailPageURL','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"') DetailPageURL
, extractValue(value(t),'/*/ItemAttributes/Title','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"') Title
, '<img src="'||nvl(extractValue(value(t),'/*/SmallImage/URL','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"'),'http://ec1.images-amazon.com/images/G/01/x-locale/detail/thumb-no-image._V47060337_.gif')||'" width="'||nvl(extractValue(value(t),'/*/SmallImage/Width','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"'),'50')||'" height="'||nvl(extractValue(value(t),'/*/SmallImage/Height','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"'),'60')||'" />' "Image"
, extractValue(value(t),'/*/ItemLinks/ItemLink[4]/URL','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"') Link
from wwv_flow_collections c,
table(xmlsequence(extract(xmltype.createxml(c.clob001),'//Item','xmlns="http://webservices.amazon.com/AWSECommerceService/2009-03-31"'))) t
where c.collection_name = 'P1_ITEMSEARCH_RESPONSE'

The query above pulls out the ASIN, detail page URL, title, small image and wish list link from the XML document. You want to create links for the title point to the detail page and a link for the wishlist in your report. Make the following changes to the Results report by click on the Report link on the page definition.

  1. Uncheck Show for the column DETAILPAGEURL
  2. Enter the following HTML Expression for TITLE column: <a href="#DETAILPAGEURL#">#TITLE#</a>
  3. Enter the following HTML Expression for LINK column: <a href="#LINK#">[Add to Wishlisth]</a>

Create a SQL Report region for the HTTP headers with the following attributes:

  1. Title: Headers
  2. Type: SQL Query
  3. Source: <see code listing 3>
  4. Conditional Display, Condition Type: PL/SQL Expression
  5. Conditional Display, Expression 1: apex_collection.collection_exists('P1_RESP_HEADERS')

Code Listing 3, Header SQL Query

 select c001 name, c002 value, c003 status_code
from apex_collections
where collection_name = 'P1_RESP_HEADERS'

Now run the page, enter a search term, choose a category and click Submit. You should see a page similar to the one below. That's all there is to it!