AEM CQ 56 - New (Tag Tree) Tab in Sidekick

Goal


This post is on adding a new tab to the Sidekick. Here we extend Sidekick and add a new tab showing CQ Tag Tree. To add new tags or remove tags for a page, multiple clicks are involved in opening Sidekick -> Page Tab -> Page Properties -> Basic -> Tags

If adding and removing tags is a common use-case in your project, adding the tag tree in a sidekick tab could be useful and saves some clicks. The new tab loads a checkbox tree; to add a tag check the box and to remove uncheck. If a tag is checked, parent tags are shown in bold to identify the tags checked, deep down in the tree. Source code, demo video and package install are available for download

For TouchUI check this post



Prerequisites


If you are new to CQ

1) Read this post on how to create a sample page component

2) Read this post on how to setup your IDE and create an OSGI component

Tags Servlet


First, we need a servlet (deployed as OSGI component ) to feed the Tag tree ( explained in next section ) with tags json data. Create servlet GetTagsCheckedForPage and add the following code

package apps.mysample.sidekick;

import com.day.cq.commons.LabeledResource;
import com.day.cq.tagging.JcrTagManagerFactory;
import com.day.cq.tagging.Tag;
import com.day.cq.tagging.TagManager;
import com.day.text.Text;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

@SlingServlet (
        paths="/bin/mycomponents/sidekick/tags",
        methods = "GET",
        metatype = true,
        label = "Tags Servlet"
)
public class GetTagsCheckedForPage extends SlingAllMethodsServlet {
    private static final Logger LOG = LoggerFactory.getLogger(GetTagsCheckedForPage.class);

    @Reference
    JcrTagManagerFactory tmf;

    @Override
    protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        String pagePath = request.getParameter("page");
        String tagPath = request.getParameter("tag");

        try{
            ResourceResolver resolver = request.getResourceResolver();
            Session session = resolver.adaptTo(Session.class);
            TagManager tMgr = tmf.getTagManager(session);

            JSONWriter jw = new JSONWriter(response.getWriter());

            if(StringUtils.isEmpty(pagePath) || StringUtils.isEmpty(tagPath)){
                jw.object();
                jw.key("error").value("required parameters page and tag missing");
                jw.endObject();
                return;
            }

            Resource resource = resolver.getResource(pagePath + "/jcr:content");

            if(resource == null){
                jw.object();
                jw.key("error").value("resource " + pagePath + " not found");
                jw.endObject();
                return;
            }

            Resource parentTag = resolver.getResource(tagPath);

            if(parentTag == null){
                jw.object();
                jw.key("error").value("tag " + parentTag + " not found");
                jw.endObject();
                return;
            }

            Tag[] pageTags = tMgr.getTags(resource);
            List<String> pageTagsList = new ArrayList<String>();

            for(Tag t : pageTags){
                pageTagsList.add(t.getPath());
            }

            Iterator<Resource> itr = parentTag.listChildren();

            Resource tag = null;
            Node node = null;
            String parentPath = null, cls = null;

            jw.array();

            while(itr.hasNext()){
                tag = itr.next();

                if(!tag.getResourceType().equals("cq/tagging/components/tag")){
                    continue;
                }

                parentPath = tag.getParent().getPath();

                jw.object();

                jw.key("name").value(tag.getPath().substring(1));

                cls = parentPath.equals("/etc/tags") || parentPath.equals("/etc") ? "folder" : "tag x-tree-node-icon";

                for(Tag t : pageTags){
                    if(t.getPath().indexOf(tag.getPath()) == 0){
                    //Make the breadcrumb trail bold, the css class x-menu-item-default is defined in CQ as
                    //.x-menu-item-default SPAN { font-weight:bold; }
                    cls = "x-menu-item-default " + cls;
                        break;
                    }
                }

                jw.key("cls").value(cls);

                node = tag.adaptTo(Node.class);

                if(node.hasProperty("jcr:title")){
                    jw.key("text").value(node.getProperty("jcr:title").getString());
                }else{
                    jw.key("text").value(node.getName());
                }

                jw.key("checked").value(pageTagsList.contains(tag.getPath()));

                jw.endObject();
            }

            jw.endArray();
        }catch(Exception e){
            LOG.error("Error getting tags",e);
            throw new ServletException(e);
        }
    }
}


UI Extension


1) Login to CRXDE Lite, create folder (nt:folder) /apps/sidekicktags

2) Create clientlib (type cq:ClientLibraryFolder/apps/sidekicktags/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/sidekicktags/clientlib/js.txt, add the following

                         addsktab.js

4) Create file ( type nt:file ) /apps/sidekicktags/clientlib/addsktab.js, add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.Sidekick = {
    SK_TAB_PANEL: "cq-sk-tabpanel",
    TAGADMIN_TREE_ID: "myclientlib-cq-tagadmin-tree",
    TAGS: "TAGS",

    addTagsPanel: function (sk) {
        var CONTEXTS = CQ.wcm.Sidekick.CONTEXTS;

        if (($.inArray(this.TAGS, CONTEXTS) != -1) || sk.panels[this.TAGS]) {
            return;
        }

        CONTEXTS.push(this.TAGS);

        var tabPanel = sk.findById(this.SK_TAB_PANEL);

        var treeLoader = new CQ.Ext.tree.TreeLoader({
            dataUrl: "/bin/mycomponents/sidekick/tags",
            requestMethod: "GET",
            baseParams: {
                page: sk.getPath()
            },
            listeners: {
                beforeload: function (tl, node) {
                    this.baseParams.tag = "/" + node.attributes.name;
                }
            }
        });

        var treeRoot = new CQ.Ext.tree.AsyncTreeNode({
            name: "etc/tags",
            text: CQ.I18n.getMessage("Tags"),
            expanded: true
        });

        var tree = new CQ.Ext.tree.TreePanel({
            "id": this.TAGADMIN_TREE_ID,
            "margins": "5 0 5 5",
            "width": 200,
            "animate": true,
            "loader": treeLoader,
            "root": treeRoot,
            "rootVisible": false,
            "tbar": [
                {
                    "iconCls": "cq-siteadmin-refresh",
                    "handler": function () {
                        CQ.Ext.getCmp(MyClientLib.Sidekick.TAGADMIN_TREE_ID).getRootNode().reload();
                    },
                    "tooltip": {
                        "text": CQ.I18n.getMessage("Refresh the tree")
                    }
                }
            ],
            listeners: {
                checkchange: function (cNode, checked) {
                    var tagTree = CQ.Ext.getCmp(MyClientLib.Sidekick.TAGADMIN_TREE_ID);
                    var tag = cNode.attributes.name;

                    //to create something like geometrixx-media:entertainment/music
                    tag = tag.substr("etc/tags".length + 1);
                    tag = tag.substr(0, tag.indexOf("/")) + ":" + tag.substr(tag.indexOf("/") + 1);

                    var data = { "./cq:tags@TypeHint": "String[]", "./cq:tags@Patch": "true",
                        "./cq:tags": (checked ? "+" : "-") + tag };

                    $.ajax({
                        url: sk.getPath() + "/jcr:content",
                        dataType: "json",
                        data: data,
                        success: function (rdata) {
                            var pNodes = [];
                            var pNode = cNode.parentNode;

                            while (true) {
                                if (pNode.attributes.name == "etc/tags") {
                                    break;
                                }

                                pNodes.push(pNode);
                                pNode = pNode.parentNode;
                            }

                            var dec = pNodes.length - 1;

                            var callBack = function (rNode) {
                                if (dec < 0) {
                                    return;
                                }

                                var toRefresh;

                                CQ.Ext.each(rNode.childNodes, function (child) {
                                    if (!toRefresh && (child.attributes.name == pNodes[dec].attributes.name)) {
                                        toRefresh = child;
                                    }
                                });

                                if (toRefresh) {
                                    dec--;
                                    toRefresh.reload(callBack);
                                }
                            };

                            tagTree.getRootNode().reload(callBack);
                        },
                        type: 'POST'
                    });
                }
            }
        });

        sk.panels[this.TAGS] = new CQ.Ext.Panel({
            "border": false,
            //"autoScroll": true,
            "layout": "fit",
            items: [tree],
            "id": "cq-sk-tab-" + this.TAGS
        });

        tabPanel.add({
            "tabTip": "Tags",
            "iconCls": "cq-sidekick-tab cq-cft-tab-icon full",
            "items": sk.panels[this.TAGS],
            "layout": "fit"
        });

        sk.doLayout();
    }
};

(function () {
    if (window.location.pathname.indexOf("/cf") == 0 || window.location.pathname.indexOf("/content") == 0) {
        var s = MyClientLib.Sidekick;

        var SK_INTERVAL = setInterval(function () {
            var sk = CQ.WCM.getSidekick();

            if (sk && sk.findById(s.SK_TAB_PANEL)) {
                clearInterval(SK_INTERVAL);
                s.addTagsPanel(sk);
            }
        }, 250);
    }
})();

3 comments:

  1. Not work correctly. Tags tab is loaded and is visible, when page is loaded first time. Than if I go to some another page (clicking to link on first time loaded page), sidekick is reloaded and then Tags tab is not visible. I'm usign AEM 5.6.1. Used exact copy of code from this page.

    I don't know where can be problem, I would like to use this example for adding new tab with buttons (dependent on current application)

    ReplyDelete
    Replies
    1. yes, I too observed same behavior , we add a new tab or we add additional buttons in [Page] tab , it renders for the first load of page and in subsequent load / sidekick refresh the changes are not loaded, looks like we need to trap page load event and call the function again

      Delete
  2. Do you have a guide for this using Touch UI? (extending the left side panel)

    ReplyDelete