Extend Tools Console AEM 6.5

Create an admin console page in tools

Goal

Create an admin console page in tools section in AEM 6.5. In this case, we will create a section for centralizing data, but there are different ways to use an admin console. For example, in a past project, I used this type of page also to show a report of our data import. It highly depends on your needs.

Procedure

First of all, you should check the video to see a demo of what we are going to develop:

If you are still interested, I’m happy to explain how to get this.

You should download the project because not all code will be presented in the article. The entire source code of the project is available here:

 In order to extend the tools section, we need to overlay the existing  libs/cq/core/content/nav/tools/operations nodes.  Let me also say that once you’ve read the article, If you are interested to understand how other AEM console pages are created, you should have a look at these nodes in the CRX/DE. The procedure is really really similar, and maybe you can take a cue for other custom pages 😉

Anyway, let’s begin! To overlay any kind of AEM content under “libs”, you have to create the same path under the path “apps” in your project. This is our example:

Under the “tools” folder, create your section (“lab“) and then the page (“promo-codes“). Of course you could have many of them if you want to. This is the result:

Before we forget, update the filter.xml in order to add this path to the package. It’s very important to use the specific path of your folder, because you could overwrite other configurations with a generic path, e.g. “/apps/cq”.

<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
    <filter root="/apps/Lab2020"/>
    <filter root="/apps/sling" />
    <filter root="/apps/cq/core/content/nav/tools/lab"/>
</workspaceFilter>

Regarding the file “.content.xml” under the folder “promo-codes”, you can decide your icon here: https://helpx.adobe.com/experience-manager/6-3/sites/developing/using/reference-materials/coral-ui/coralui3/Coral.Icon.html

The href property points to the location of the main page, defined later. You’ll see that a lot of components are defined by a .content.xml file.

Here the entire file:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" 
    xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:rep="internal"
    jcr:primaryType="nt:unstructured"
    jcr:title="Promo Codes"
    jcr:description="Manage Promo Codes entries"
    id="cq-tools-lab2020-promo-codes"
    href="/apps/Lab2020/admin/configurations/promo-codes/content.html"
    icon="project"/>

Our main page, defined under “/apps/Lab2020/admin/configurations/promo-codes/content”, contains a table, that relies on a datasource to provide data to the list.

You can find more info about the granite table here: https://helpx.adobe.com/experience-manager/6-3/sites/developing/using/reference-materials/granite-ui/api/jcr_root/libs/granite/ui/components/coral/foundation/table/index.html

A few notes:

  • A custom clientlib is included. It’s used to activate, deactivate, delete and update the single entry.
  • itemResourceType is used to display each entry of the list (datasource) in each row.
  • Within actions, the selection nodes (delete, edit, activate, deactivate) are used when selecting an item in the list. Each node has a granite id that is used in the Javascript.
  • Within actions, the secondary action always shows and is used for create. It’s uses a plain href property.
  • Data are stored under the path “/conf/global/settings/lab2020/configurations/promo-codes”. There is the property “path” in the datasource tag and in the Java code as well. You can choose for sure another path. In this case, the path needs to be created via CRX.
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:mixinTypes="[sling:VanityPath]"
    jcr:primaryType="nt:unstructured"
    jcr:title="Promo Codes"
    sling:resourceType="granite/ui/components/shell/collectionpage"
    console-id="cq-tools-lab2020-promo-codes"
    contentPath="/content"
    currentView="${state["shell.collectionpage.layoutId"].string}"
    modeGroup="granite-uptime-collection"
    pageURITemplate="/apps/Lab2020/admin/configurations/promo-codes/content.html"
    targetCollection=".granite-uptime-collection">
    <head jcr:primaryType="nt:unstructured">
        <clientlibs
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
            categories="[coralui3,granite.ui.coral.foundation,Lab2020.clientlibs-edit-entry]"/>
        <viewport
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
        <meta
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/meta"
            content="chrome=1"
            name="X-UA-Compatible"/>
    </head>
    <views jcr:primaryType="nt:unstructured">
        <list
            granite:rel="granite-uptime-collection"
            jcr:primaryType="nt:unstructured"
            jcr:title="List View"
            sling:resourceType="granite/ui/components/coral/foundation/table"
            draggable="{Boolean}false"
            icon="viewList"
            layoutId="list"
            limit="{Long}40"
            modeGroup="granite-uptime-collection"
            path="/content"
            selectionCount="single"
            selectionMode="row"
            size="40"
            sortMode="local"
            src="/libs/granite/backup/content/admin/views/list.html"
            stateId="shell.collectionpage">
            <datasource
                jcr:primaryType="nt:unstructured"
                sling:resourceType="/apps/Lab2020/admin/configurations/promo-codes/components/datasource"
                itemResourceType="/apps/Lab2020/admin/configurations/promo-codes/components/promo-code-entry"
                value="value"
                text="text"
                enabled="enabled"
                promoCode="promoCode"
                promoName="promoName"
                promoDescription="promoDescription"
                validDateFrom="validDateFrom"
                validDateTo="validDateTo"
                action="action"
                path="/conf/global/settings/lab2020/configurations/promo-codes"/>
            <columns jcr:primaryType="nt:unstructured">
                <select
                    jcr:primaryType="nt:unstructured"
                    select="{Boolean}true"/>
                <enabled
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Enabled"/>
                <promoCode
                    jcr:primaryType="nt:unstructured"
                    jcr:title="Promo Code"/>
                <promoName
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Promo Name"/>
                <promoDescription
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Promo Description"/>
                <validDateFrom
                    jcr:primaryType="nt:unstructured"
                    jcr:title="Valid Date (From)"/>
                <validDateTo
                    jcr:primaryType="nt:unstructured"
                    jcr:title="Valid Date (To)"/>
                <action
                    jcr:primaryType="nt:unstructured"
                    jcr:title="Action"/>
            </columns>
        </list>
    </views>
    <actions jcr:primaryType="nt:unstructured">
        <selection jcr:primaryType="nt:unstructured">
            <delete
                granite:id="toggleDeleteEntryDialog"
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                icon="delete"
                text="Delete"
                variant="actionBar"
                x-cq-linkchecker="skip"/>
            <edit
                granite:id="editEntryDialog"
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                icon="edit"
                text="Edit"
                variant="actionBar"
                x-cq-linkchecker="skip"/>
            <activate
                granite:id="activateEntryDialog"
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                icon="globe"
                text="Publish"
                variant="actionBar"
                x-cq-linkchecker="skip"/>
            <deactivate
                granite:id="deactivateEntryDialog"
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                icon="globe"
                text="Unpublish"
                variant="actionBar"
                x-cq-linkchecker="skip"/>
        </selection>
        <secondary jcr:primaryType="nt:unstructured">
            <create
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                href="/apps/Lab2020/admin/configurations/promo-codes/content/createEntry.html"
                text="Create"
                variant="primary"
                x-cq-linkchecker="skip"/>
        </secondary>
    </actions>
    <title
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/text"
        text="Promo Codes"/>
</jcr:root>

The datasource (/apps/Lab2020/admin/configurations/promo-codes/components/datasource) retrieves the list of resources that have been created and are displayed in the table. Each row is rendered with the itemResourceType (/apps/Lab2020/admin/configurations/promo-codes/components/promo-code-entry).

To create an entry, the following .content.xml (apps/Lab2020/admin/configurations/promo-codes/content/createEntry) is used:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
          jcr:mixinTypes="[sling:VanityPath]"
          jcr:primaryType="nt:unstructured"
          jcr:title="Edit Promo Code"
          sling:resourceType="granite/ui/components/shell/propertiespage"
          backHref="/apps/Lab2020/admin/configurations/promo-codes/content.html"
          consoleId="cq-tools-promo-codes-configurations-update-entry"
          formId="createPromoCodeEntryForm">
    <head jcr:primaryType="nt:unstructured">
        <clientlibs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
                categories="[coralui3,granite.ui.coral.foundation]"/>
        <viewport
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
        <meta
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/meta"
                content="chrome=1"
                name="X-UA-Compatible"/>
        <favicon
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/>
    </head>
    <content
            granite:class="centered"
            granite:id="createPromoCodeEntryForm"
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/form"
            action="/bin/configurations/promo-codes"
            method="post"
            style="vertical">
        <items jcr:primaryType="nt:unstructured">
            <general
                    jcr:primaryType="nt:unstructured"
                    jcr:title="General"
                    sling:resourceType="granite/ui/components/foundation/section">
                <layout
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                <items jcr:primaryType="nt:unstructured">
                    <column
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/foundation/container">
                       <items jcr:primaryType="nt:unstructured">
                           <promoCodeForm
                               jcr:primaryType="nt:unstructured"
                               sling:resourceType="Lab2020/admin/configurations/promo-codes/components/promo-code-form"/>
                       </items>
                    </column>
                </items>
            </general>
        </items>
    </content>
    <title
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/text"
            text="Edit Promo Code Entry"/>
</jcr:root>

It is completely equals to the editEntry file (apps/Lab2020/admin/configurations/promo-codes/content/createEntry) except for some text.

These two files use the HTML file “/apps/Lab2020/admin/configurations/promo-codes/components/promo-code-form” which allows to create/modify our data and its layout is the same of a component dialog. The model you’ll find in the HTML is used to retrieve data in case of an update.

Data are stored/updated with a servlet. You can notice in the “content” tag these two properties:

  • action=”/bin/configurations/promo-codes”
  • method=”post”

You can find all the java code in project. You can download it at the top of the article.

The servlet is split in more classes because they contain some common methods that can be reused inside your project, but it’s not mandatory of course. Organized them as you prefer

To modify/activate/deactivate/delete an entry, click on the related checkbox to open the menu. The clientlib will do the rest.

Here the clientlib:

(function(document, $) {
    "use strict";

    //var URL_BASE =  "/apps/Lab2020/admin/configurations/promo-codes/content";

    var URL_BASE = Granite.HTTP.getPath();
    var MAIN_PAGE_PATH = URL_BASE + ".html";
    var EDIT_PAGE_PATH = URL_BASE + "/editEntry.html"
    
    var ui = $(window).adaptTo("foundation-ui");

    $(document).on("foundation-selections-change", ".granite-uptime-collection", function() {
        var deleteButton = $("#toggleDeleteEntryDialog");
        var editButton = $("#editEntryDialog");
    });

    $(document).on("click", "#toggleDeleteEntryDialog", function(e) {
        var entryName = $(".foundation-selections-item").data("id");
        var dialog = document.querySelector("#deleteEntryDialog");
        if(!dialog) {
            dialog = new Coral.Dialog().set({
                variant: "warning",
                id: "deleteEntryDialog",
                header: {
                    innerHTML: Granite.I18n.get("Delete Selected Entry")
                },
                content: {
                    innerHTML: Granite.I18n.get("Are you sure you want to entry {0}?", entryName, "0 is the path of the entry to delete")
                },
                footer: {
                    innerHTML: "<button is='coral-button' variant='default' coral-close>" + Granite.I18n.get("No") +
                    "</button><button id='deleteEntryButton' is='coral-button' variant='primary'>" + Granite.I18n.get("Yes") + "</button>"
                }
            });
            document.body.appendChild(dialog);
        } else {
            dialog.content.innerHTML =
                Granite.I18n.get("Are you sure you want to delete entry {0}?", entryName, "0 is the path of the entry to delete");
        }
        dialog.show();
    });

    $(document).on("click", "#activateEntryDialog", function(e) {
        var selectedItem = $(".foundation-selections-item");
        var selectedItemPath = $(".foundation-selections-item").data("path");
        var dialog = document.querySelector("#deleteEntryDialog");
        var switcherItem = $(".foundation-mode-switcher-item");
        $.ajax({
            url: Granite.HTTP.externalize("/bin/replicate"),
            type: "POST",
            data: {
                "path":selectedItemPath,
                "cmd": "Activate"
            },
            success: function() {
                window.location.href = Granite.HTTP.externalize(MAIN_PAGE_PATH) ;
                //window.location.refresh??
            },
            error: function(xmlhttprequest, textStatus, message) {

                ui.notify(Granite.I18n.get("Error"), Granite.I18n.get("An error occurred while publish the entry: {0}.", message, "0 is the error message"), "error")
            }
        });
    });
    $(document).on("click", "#deactivateEntryDialog", function(e) {
        var selectedItem = $(".foundation-selections-item");
        var selectedItemPath = $(".foundation-selections-item").data("path");
        var dialog = document.querySelector("#deleteEntryDialog");
        var switcherItem = $(".foundation-mode-switcher-item");
        $.ajax({
            url: Granite.HTTP.externalize("/bin/replicate"),
            type: "POST",
            data: {
                "path":selectedItemPath,
                "cmd": "Deactivate"
            },
            success: function() {
                window.location.href = Granite.HTTP.externalize(MAIN_PAGE_PATH) ;
                //window.location.refresh??
            },
            error: function(xmlhttprequest, textStatus, message) {

                ui.notify(Granite.I18n.get("Error"), Granite.I18n.get("An error occurred while Unpublish the entry: {0}.", message, "0 is the error message"), "error")
            }
        });
    });

    $(document).on("click", "#deleteEntryButton", function(e) {
        var selectedItem = $(".foundation-selections-item");
        var selectedItemPath = $(".foundation-selections-item").data("path");
        var dialog = document.querySelector("#deleteEntryDialog");
        var switcherItem = $(".foundation-mode-switcher-item");
        $.ajax({
            url: Granite.HTTP.externalize(selectedItemPath),
            type: "POST",
            data: {
                ":operation": "delete"
            },
            success: function() {
                selectedItem.remove();
                if(dialog) {
                    dialog.hide();
                }
                if(switcherItem) {
                    switcherItem.removeClass('foundation-mode-switcher-item-active');
                }
                window.location.refresh
            },
            error: function(xmlhttprequest, textStatus, message) {
                if(dialog) {
                    dialog.hide();
                }
                ui.notify(Granite.I18n.get("Error"), Granite.I18n.get("An error occurred while deleting the entry: {0}.", message, "0 is the error message"), "error")
            }
        });
    });


    $(document).on("click", "#editEntryDialog", function(e) {
        var selectedEntry = $(".foundation-selections-item").data("path");
        window.location.href = Granite.HTTP.externalize(EDIT_PAGE_PATH) + "?path=" + selectedEntry;
    });
})(document, Granite.$);

That’s all. Please download the project to have the missing code I haven’t posted here.

A huge thanks and mention to this article (https://experiencemanaged.com/posts/how-to-create-an-aem-62-admin-console-for-touch-ui.html) which I followed a few years ago to develop some admin console pages.

This should work in AEM 6.3+, but can be easily adapted for other AEM versions.

Cheers mates! 🍻🍺

2 thoughts on “Extend Tools Console AEM 6.5

  1. unable to download the package. Can you please provide correct url for downloading the ZIP file.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: