Thursday, September 29, 2016

Customizing Cards Report

The other day I was working on a cards report for a customer and they wanted to have the cards on the same line be of equal height. They also wanted to have some kind of visual indicator of what card was currently being hovered.

Here's what the built-in card report looks like:


First, let's have a look at the equal height for the cards on the same line.
For this we'll be using the flexbox properties. If you're not familiar with flexbox, you can have a look at this article: A Complete Guide to Flexbox.

Add following class to you region's CSS Class Attribute: "t-Cards--equal-height" and the following CSS to your page.
.t-Cards--equal-height .t-Cards {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
        flex-wrap: wrap;
} 

.t-Cards--equal-height .t-Cards .t-Cards-item {
    margin-bottom: 10px; /* Adds some space between lines */
} 

.t-Cards--equal-height .t-Cards .t-Cards-item .t-Card,
.t-Cards--equal-height .t-Cards .t-Cards-item .t-Card .t-Card-wrap {
    height: 100%;
}

Result will look like this:


Now, let's highlight the header when hovering a card.
Add following class to you region's CSS Class Attribute: "t-Cards--header-highlight" and the following CSS to your page.
.t-Cards--header-highlight .t-Cards .t-Cards-item .t-Card:hover .t-Card-titleWrap{
    background-color: #c8102e;
}

.t-Cards--header-highlight .t-Cards .t-Cards-item .t-Card:hover .t-Card-titleWrap .t-Card-title {
    color: #fff;
}

.t-Cards--header-highlight .t-Cards .t-Cards-item .t-Card:hover .t-Card-wrap {
    border: 1px solid rgba(0,0,0,.05);
    box-shadow: 0 2px 2px rgba(0,0,0,.05);
}

/* Featured Style Padding/Margin Fix */
.t-Cards--header-highlight .t-Cards.t-Cards--featured .t-Cards-item .t-Card .t-Card-titleWrap {
    margin: 0 !important;
    padding: 24px 16px;
}

.t-Cards--header-highlight .t-Cards.t-Cards--featured .t-Cards-item .t-Card .t-Card-body {
   margin: 0 16px 24px 16px !important;
}

The second part of the above CSS is to change the cards's margin to padding, we need to do this because the background-color is not affecting the margins, but it is for the paddings.

Result will look like this:


The above CSS works for all cards report styles (Basic, Compact and Featured).

You can have a look at my Demo Application

EDIT: Apex 5.1 displays cards with equal height by default.

Tuesday, September 20, 2016

Securing Ajax Callback Process

When navigating from a report page to the corresponding detail page, it's always a good practice to enable the parameters checksum to prevent users from tampering with the item values that are in the url.

To enable arguments checksum, you must first set the "Page Access Protection" attribute under the page attribute security section to "Arguments must have checksum" and then for each item that is going to be assigned from the url, you need to set the "Session State Protection" to one of the "Checksum Required" value (have a look at the item's help for more details about the different types of checksum).



That way the user is not going to be able to change any value from the url.

I recently had to implement a similar concept using button in a report that were calling an AJAX Callback Process from a dynamic action.

Here's what my report's SQL Query looked like:
select '<button onclick="void(0);"'
           || ' data-data1="' || some_table_id || '"'
           || ' data-data2="' || some_value || '"'
           --|| ' data-data3="' || sys.dbms_crypto.hash(utl_raw.cast_to_raw(some_table_id || some_value || to_char(nvl(last_update_date, creation_date), :DATETIMEFORMAT)), 3) || '"' /* Where 3 -> SHA-1 from sys.dbms_crypto.hash_sh1 */
           || ' data-data3="' || apex_util.get_hash(apex_t_varchar2(some_table_id, some_value, to_char(nvl(last_update_date, creation_date), :DATETIMEFORMAT))) || '"'
           || ' class="t-Button t-Button--hot t-Button--small actionButton"'
           || ' type="button"><span class="t-Button-label">Some Action</span></button>'
       /* Rest of the query */
  from some_table
 where /* where clauses */
Where data1 and data2 are the values that are needed in the AJAX Callback Process and data3 is the checksum for the previous values.
To have a stronger checksum and to ensure that it is only used once, I added the record's last update date to it.
DATETIMEFORMAT is an application substitution string.

Note: in order to be able to use dbms_crypto, it needs to be granted to the current user (not required with the apex_util.get_hash function.
For more information on the Apex API, read this GET_HASH Function.

The corresponding dynamic action to handle the button click was as follow:
Dynamic Action: Event: Click
jQuery Selector: ".actionButton"
var lSpinner$ = apex.util.showSpinner();
var lReportRegion$ = $(this.affectedElements);

apex.server.process("AJAX_PROCESS_NAME",
                    {x01: $(this.triggeringElement).data('data1'),
                     x02: $(this.triggeringElement).data('data2'),
                     x03: $(this.triggeringElement).data('data3')
                     },
                    {success: function( pData ) {
                        if (pData.success === true){
                            /* Show Sucess Message */
                            showSuccessMessage(pData.message);
                            
                            /* Refresh Region */
                            lReportRegion$.trigger('apexrefresh');
                        }
                        else{
                            /* Show Error Message */
                            showErrorMessage([pData.message]);
                        }
                        
                        lSpinner$.remove();
                      }
                     }
                    );
In the previous code, I'm using the affected element attribute to refresh my report region.

Here's the AJAX Callback Process:
declare
    l_some_table_id number;
    l_some_value    varchar2(50);
    l_checksum      varchar2(32767);
    --l_checksum      raw(32767); --sys.dbms_crypto.hash returns raw
    --l_hash_type     pls_integer := sys.dbms_crypto.hash_sh1;
    
    l_last_update   varchar2(100);
begin
    /* Retrieve parameters */
    l_some_table_id := to_number(apex_application.g_x01);
    l_some_value    := apex_application.g_x02;
    l_checksum      := apex_application.g_x03;
    
    select to_char(nvl(last_update_date, creation_date), :DATETIMEFORMAT)
      into l_last_update
      from some_table
     where some_table_id = l_some_table_id;
    
    /* Validate the checksum */
    --if l_checksum = sys.dbms_crypto.hash(utl_raw.cast_to_raw(l_some_table_id || l_some_value || l_last_update), l_hash_type) then
    if l_checksum = apex_util.get_hash(apex_t_varchar2(l_some_table_id, l_some_value, l_last_update)) then
        /* Do something */
        
        apex_json.open_object;
        apex_json.write('success', true);
        apex_json.write('message', 'Action Processed.');
        apex_json.close_object;
    else
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', 'Invalid Action.');
        apex_json.close_object;
    end if;
exception
    when no_data_found then
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', 'Invalid Action.');
        apex_json.close_object;
    when others then
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', 'Invalid Action.');
        apex_json.close_object;
end;


Basically what we do in the process is to recreate a checksum based on the parameters' values and to compare it with the checksum parameter and if they match we execute the code that needs to run.

Note: Edited above code to use the Apex API: apex_util.get_hash. Commented out everything related to dbms_crypto for reference purpose.

Friday, September 9, 2016

Adding a Clear Icon to an Item

Here's how to add a clear icon to an item (similar to the calendar icon of the datepicker item).
Since the datepicker's icon has the same look and feel as what we want, let's reuse the same CSS and html.

Let's inspect the html code of the datepicker's icon:

<button class="ui-datepicker-trigger a-Button a-Button--calendar" type="button">
    <span class="a-Icon icon-calendar"></span>
    <span class="u-VisuallyHidden">Popup Calendar: Date Picker</span>
</button>

  • ui-datepicker-trigger: class used to handle the click event
    • Let's rename that to 'clearItem-trigger'
  • a-Button: standard look and feel of buttons
    • Let's reuse it as we want the same look and feel
  • a-Button--calendar: additional css for the calendar icon
    • Let's rename that to 'a-Button--clearInput'
  • icon-calendar: image of the calendar
    • Let's replace it with a clear icon: 'icon-remove'
We can then retrieve and adapt the CSS for the above classes from the Core.min.css.
We will then have the following CSS:
.t-Form-inputContainer .a-Button--clearInput{
    margin-left: -.1rem;
    order: 3; /* Apex 5.1: Set the correct order */
}

.t-Form--large .a-Button.a-Button--clearInput,
.t-Form-fieldContainer--large .a-Button.a-Button--clearInput {
    padding: .8rem 1.2rem
}

.t-Form--xlarge .a-Button.a-Button--clearInput,
.t-Form-fieldContainer--xlarge .a-Button.a-Button--clearInput {
    padding: 1.4rem 1.2rem
}

.t-Form--login .a-Button.a-Button--clearInput {
    padding: 1rem 1.2rem
}

That's for the visual aspect of the icon.
Let's now add the following JavaScript code that will add the remove icon and that will handle the click event.

JavaScript code on page page load:
$('.clearInput').each(function(){
    var lTriggerHtml = '<button class="clearInput-trigger a-Button a-Button--clearInput" data-item_id="' + $(this).attr('id') + '" type="button">'
                     +     '<span class="a-Icon icon-remove"></span>'
                     +     '<span class="u-VisuallyHidden">Clear Item\'s Value</span>'
                     + '</button>';
    $(this).after(lTriggerHtml);
});

$('.clearInput-trigger').click(function(){
    $('#' + $(this).data('item_id')).val('');
});

The first part will add the clear button after each element that has the 'clearInput' class.
The second part is used to handle the click event.

Now you simply have to add the 'clearInput' class (under the item's Advanced properties) to each item you want the clear button to be added.

One thing to note is that the above code will not trigger the item's change event.
If you would also like to trigger the change event, we could add an additionnal CSS class to the item like 'clearInput--change' and update the second part of the JavaScript code to:

We need to add the two following classes:
$('.clearInput-trigger').click(function(){
    var triggeringElement = $('#' + $(this).data('item_id'));
    
    triggeringElement.val('');
    
    if (triggeringElement.hasClass('clearInput--change')){
        triggeringElement.change();
    }
});

We also need to update the JavaScript Code that adds the button so that it assigns the corresponding CSS class to the button.
$('.clearInput').each(function(){
    var lClearInputClass = $(this).hasClass('clearInput--inside') ? 'a-Button--clearInputInside': 'a-Button--clearInput';
    var lTriggerHtml = '<button class="clearInput-trigger a-Button ' + lClearInputClass + '" data-item_id="' + $(this).attr('id') + '" type="button">'
                     +     '<span class="a-Icon icon-remove"></span>'
                     +     '<span class="u-VisuallyHidden">Clear Item\'s Value</span>'
                     + '</button>';
    
    $(this).after(lTriggerHtml);
});

Now supposed that you would like to have the icon be inside the element.
You need to have the element has right padding (equal to the width of the icon, 32px in our case) and you need to move the icon left by the same amount.

.clearInput--inside{
    padding-right: 32px !important;
}

.t-Form-inputContainer .a-Button--clearInputInside{
    margin-left: -32px;
}

Simply have to set the 'clearInput clearInput--inside' class (under the item's Advanced properties) to each item you want the clear button to be added inside.

Final JavaScript:
$('.clearInput').each(function(){
    var lClearInputClass = $(this).hasClass('clearInput--inside') ? 'a-Button--clearInputInside': 'a-Button--clearInput';
    var lTriggerHtml = '<button class="clearInput-trigger a-Button ' + lClearInputClass + '" data-item_id="' + $(this).attr('id') + '" type="button">'
                     +     '<span class="a-Icon icon-remove"></span>'
                     +     '<span class="u-VisuallyHidden">Clear Item\'s Value</span>'
                     + '</button>';
    
    $(this).after(lTriggerHtml);
});
 
$('.clearInput-trigger').click(function(){
    var triggeringElement = $('#' + $(this).data('item_id'));
     
    triggeringElement.val('');
     
    if (triggeringElement.hasClass('clearInput--change')){
        triggeringElement.change();
    }
});

Final CSS:
.clearInput--inside{
    padding-right: 32px !important;
}

.t-Form-inputContainer .a-Button--clearInputInside{
    margin-left: -32px;
}

.t-Form-inputContainer .a-Button--clearInput{
    margin-left: -.1rem;
    order: 3; /* Apex 5.1: Set the correct order */
}
 
.t-Form--large .a-Button.a-Button--clearInput,
.t-Form-fieldContainer--large .a-Button.a-Button--clearInput {
    padding: .8rem 1.2rem
}
 
.t-Form--xlarge .a-Button.a-Button--clearInput,
.t-Form-fieldContainer--xlarge .a-Button.a-Button--clearInput {
    padding: 1.4rem 1.2rem
}
 
.t-Form--login .a-Button.a-Button--clearInput {
    padding: 1rem 1.2rem
}

Here's the end result:


You can have a look at my Demo Application