How to customise the results view - softwareloop/alfresco-inboxes GitHub Wiki

A previous page has shown how alfresco-inboxes can be used to run CMIS queries against a custom content model. One of the queries was SELECT * FROM owd:dossier and produced the following visual result:

alfresco-inboxes: dossier view before customisations

While this is an acceptable starting point, it is clear that these custom owd:dossier objects are being treated as if they were regular folders. The specificity of owd:dossier does not show through. One could imagine that, scratching beneath the surface of a custom content type, a rich set of additional metadata could be exposed, if only the user interface was able to reveal it.

Specifically the results view has the following problems/limitations:

  • the standard folder image is presented, while a dossier picture is available and could be used instead;
  • the dossiers have a name, but no title and no description as they don't implement the "cm:titled" aspect
  • no version label is present
  • the dossiers have attributes (e.g., status, personnel number, knowledge) that would be worth displaying
  • the download link is not really meaningful and could be used for other purposes;
  • the approve/reject actions need to be contextualised to the dossier status workflow.

To summarise, here is a screenshot of what this page will try to achieve:

alfresco-inboxes: dossier view after customisations

Download the code

The source code of alfresco-inboxes is available from GitHub as a Git project or as simple zip file.

The customisations are applied to these two files:

inboxes.get.config.xml:

<inboxes>
    <group id="my-documents">
        <inbox id="Dossiers" iconClass="foundicon-paper-clip" itemClass="softwareloop/inboxes/Dossier">
            <query><![CDATA[
            SELECT *
            FROM owd:dossier
            ]]></query>
        </inbox>
        <inbox id="for-my-approval" iconClass="foundicon-inbox">
            <query><![CDATA[
            SELECT d.*, t.*
            FROM cmis:document AS d
            JOIN cm:titled AS t on d.cmis:objectId = t.cmis:objectId
            WHERE contains(d, 'PATH:"/app:company_home/st:sites/cm:swsdp/cm:documentLibrary/cm:Budget_x0020_Files/cm:Invoices/*"')
            ]]></query>
        </inbox>
        <inbox id="overdue" iconClass="foundicon-clock">
            <query><![CDATA[
            SELECT d.*, t.*
            FROM cmis:document AS d
            JOIN cm:titled AS t on d.cmis:objectId = t.cmis:objectId
            WHERE contains(d, 'PATH:"/app:company_home/st:sites/cm:swsdp/cm:documentLibrary/cm:Agency_x0020_Files/cm:Contracts/*"')
            ]]></query>
        </inbox>
        <inbox id="high-priority" iconClass="foundicon-flag">
            <query><![CDATA[
            SELECT d.*, t.*
            FROM cmis:document AS d
            JOIN cm:titled AS t on d.cmis:objectId = t.cmis:objectId
            WHERE contains(d, 'PATH:"/app:company_home/st:sites/cm:swsdp/cm:documentLibrary/cm:Agency_x0020_Files/cm:Mock-Ups/*"')
            ]]></query>
        </inbox>
    </group>
    <group id="archive">
        <inbox id="invoices" iconClass="foundicon-page">
            <query><![CDATA[
            SELECT d.*, t.*
            FROM cmis:document AS d
            JOIN cm:titled AS t on d.cmis:objectId = t.cmis:objectId
            WHERE d.cmis:objectTypeId='cmis:document'
            AND d.cmis:createdBy = 'mjackson'
            ]]></query>
        </inbox>
        <inbox id="purchase-orders" iconClass="foundicon-left-arrow">
            <query><![CDATA[
            SELECT d.*, t.*
            FROM cmis:document AS d
            JOIN cm:titled AS t on d.cmis:objectId = t.cmis:objectId
            WHERE d.cmis:objectTypeId='cmis:document'
            AND d.cmis:createdBy = 'abeecher'
            ]]></query>
        </inbox>
        <inbox id="quotations" iconClass="foundicon-right-arrow">
            <query><![CDATA[
            SELECT d.*, t.*
            FROM cmis:document AS d
            JOIN cm:titled AS t on d.cmis:objectId = t.cmis:objectId
            WHERE contains(d, 'PATH:"/app:company_home/st:sites/cm:swsdp/cm:documentLibrary/cm:Presentations/*"')
            ]]></query>
        </inbox>
        <inbox id="marketing-documents" iconClass="foundicon-globe">
            <query><![CDATA[
            SELECT d.*, t.*
            FROM cmis:document AS d
            JOIN cm:titled AS t on d.cmis:objectId = t.cmis:objectId
            WHERE contains(d, 'PATH:"/app:company_home/st:sites/cm:swsdp/cm:documentLibrary/cm:Agency_x0020_Files/cm:Images/*"')
            ]]></query>
        </inbox>
    </group>
</inboxes>

Dossier.js:

define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/date/locale",
    "./Item",
    "softwareloop/cmis/cmis"
], function (declare, lang, locale, Item, cmis) {
    return declare([Item], {

        composeLines: function () {
            var dossierPicture = this.entry.getAttributeValue("owd:dossierPicture");
            this.previewUrl = lang.replace(
                "{proxyUri}api/node/workspace/SpacesStore/{entryId}/content/thumbnails/doclib?c=queue&ph=true&lastModified=1",
                {
                    proxyUri: Alfresco.constants.PROXY_URI,
                    entryId: dossierPicture.substring(24)
                }
            );
        
            this.escapedLine1 = this.encodeHTML(
                this.entry.getAttributeValue("cmis:name"));
        
            var dossierPersonnelNumber =
                this.entry.getAttributeValue("owd:dossierPersonnelNumber");
            this.escapedLine2 = this.encodeHTML(
                    "Personnel number: " + dossierPersonnelNumber);
        
            var line3 = this.message(
                "modified.on.by",
                {
                    date: locale.format(this.entry.getAttributeValue("cmis:lastModificationDate"), {
                        formatLength: "medium",
                        locale: Alfresco.constants.JS_LOCALE.substring(0, 2)
                    }),
                    user: this.entry.getAttributeValue("cmis:lastModifiedBy")
                }
            );
            this.escapedLine3 = this.encodeHTML(line3);
        
            var line4 = "Knowledge: " +
                this.entry.getAttributeValues("owd:knowledge").join(", ");
            this.escapedLine4 = this.encodeHTML(line4);
        
            this.escapedTag = this.encodeHTML(
                this.entry.getAttributeValue("owd:dossierStatus"));
        
            this.approveLabel = "Mark active";
            this.rejectLabel = "Mark retired";
        
            var filter = "path|" +
                encodeURIComponent(this.entry.getAttributeValue("cmis:path"));
            this.downloadUrl = lang.replace(
                "{pageContext}repository#filter={filter}&page=1",
                {
                    pageContext: Alfresco.constants.URL_PAGECONTEXT,
                    filter: encodeURIComponent(filter)
                }
            );
            this.downloadLabel = "View in repository";
        },

        approveAction: function () {
            this.updateStatus("Active");
        },

        rejectAction: function () {
            this.updateStatus("Retired");
        },

        updateStatus: function (status) {
            var url = lang.replace(
                "{proxyUri}cmis/s/workspace:SpacesStore/i/{entryId}",
                {
                    proxyUri: Alfresco.constants.PROXY_URI,
                    entryId: this.entry.id
                }
            );
            var dossierStatus = this.entry.attributes["owd:dossierStatus"];
            dossierStatus.values[0] = status;
            var updateAttributes = {};
            updateAttributes["owd:dossierStatus"] = dossierStatus;
            cmis.updateEntry(url, updateAttributes, function() {
                location.reload(false);
            });
        }


    });
});

The standard Item view

Before we start to apply customisations, we need to understand how the standard view works. The results view is implemented by a Dojo/Aikau component called Item.js located at src/main/amp/web/js/softwareloop/inboxes/ in the source code.

The component is a template-based widget, meaning that its presentation relies on a separate HTML template file called Item.html in the templates subfolder. We are not going to modify this file but a look at the html helps to understand what goes on under the hood:

<div class="inboxes-item">
  <div class="inbox-item-icon"><img src="${previewUrl}"></div>
  <div class="inbox-item-description">
    <div class="inboxes-item-line1">
      <div class="inboxes-item-float-right inboxes-item-show-on-hover">
        <button class="inboxes-item-button inboxes-item-button-approve"
            data-dojo-attach-event="click:approveAction">${approveLabel}
        </button>
        <button class="inboxes-item-button inboxes-item-button-reject"
            data-dojo-attach-event="click:rejectAction">${rejectLabel}
        </button>
      </div>
      <h2>
        ${escapedLine1}
        <span class="inboxes-item-tag">${escapedTag}</span>
      </h2>
    </div>
    <div class="inboxes-item-line2">
      <h3>${escapedLine2}</h3>
    </div>
    <div class="inboxes-item-line3">
      <div class="inboxes-item-float-right inboxes-item-show-on-hover">
        <a class="download-link" href="${downloadUrl}">${downloadLabel}</a>
      </div>
      ${escapedLine3}
    </div>
    <div class="inboxes-item-line4">
      ${escapedLine4}
    </div>
  </div>
</div>

From a bird's eye view:

  • a preview image is placed on the left;
  • four lines of text are the main content – line1 as h2, line2 as h3 and the remaining two as plain text;
  • a tag is attached to line1;
  • the approve/reject action buttons and a download link are placed on the right.

Being a template, Item.html uses placeholder parameters in lieu of real text or links. There are ten parameters in all:

  • {previewUrl} – the url of the preview image
  • {escapedLine1} – line1's text
  • {escapedLine2} – line2's text
  • {escapedLine3} – line3's text
  • {escapedLine4} – line4's text
  • {escapedTag} – the tag attached to line1
  • {approveLabel} – the label of the approve button
  • {rejectLabel} – the label of the reject button
  • {downloadUrl} – the download url
  • {downloadLabel} – the label of the download link

These parameters take values from the Item.js component. Looking at the component's source code we can see their definition:

previewUrl: "",
downloadUrl: "",
escapedLine1: "",
escapedLine2: "",
escapedLine3: "",
escapedLine4: "",
escapedTag: "",

approveLabel: "approve",
rejectLabel: "reject",
downloadLabel: "download",

More interestingly, Item.js contains a function called composeLines() which is responsible for composing the text dynamically, based on the attributes of the retrieved objects. The standard implementation uses generic attributes such cmis:name, cm:title, cmis:lastModificationDate, cmis:lastModifiedBy, etc, but a customisation could override the function and use the attributes available on the custom content type.

Customisation strategy

Our customisation strategy is based on three points:

  1. leaving the Dossier.html template as it is – it's already capable of a rich visual presentation;
  2. creating a new Dossier.js component that overrides Item.js and specifically provides a new implementation for composeLines();
  3. using the new Dossier component in the plugin's configuration.

Point 1 is trivial, so really we have to address only the last two points.

The new Dossier component

The new component will be called Dossier.js and will be located at src/main/amp/web/js/softwareloop/inboxes/, i.e. in the same directory as Item.js. Its initial content can be something as simple as the following:

define([
  "dojo/_base/declare",
  "./Item"
], function (declare, Item) {
  return declare([Item], {

    composeLines: function () {
      // populate the template parameters
    }
  });
});

Notice the first parameter of declare(): it means that we declare a new component that inherits from Item, our default component.

The the purpose of composeLines() was set out in the previous sections: to provide a value for the ten template parameters. Let's see them one by one.

To set the {previewUrl} parameter:

var dossierPicture = this.entry.getAttributeValue("owd:dossierPicture");
this.previewUrl = lang.replace(
    "{proxyUri}api/node/workspace/SpacesStore/{entryId}/content/thumbnails/doclib?c=queue&ph=true&lastModified=1",
    {
        proxyUri: Alfresco.constants.PROXY_URI,
        entryId: dossierPicture.substring(24)
    }
);

To set the {escapedLine1} parameter:

this.escapedLine1 = this.encodeHTML(
    this.entry.getAttributeValue("cmis:name"));

To set the {escapedLine2} parameter:

var dossierPersonnelNumber =
    this.entry.getAttributeValue("owd:dossierPersonnelNumber");
this.escapedLine2 = this.encodeHTML(
        "Personnel number: " + dossierPersonnelNumber);

To set the {escapedLine3} parameter:

var line3 = this.message(
    "modified.on.by",
    {
        date: locale.format(this.entry.getAttributeValue("cmis:lastModificationDate"), {
            formatLength: "medium",
            locale: Alfresco.constants.JS_LOCALE.substring(0, 2)
        }),
        user: this.entry.getAttributeValue("cmis:lastModifiedBy")
    }
);
this.escapedLine3 = this.encodeHTML(line3);

To set the {escapedLine4} parameter:

var line4 = "Knowledge: " +
    this.entry.getAttributeValues("owd:knowledge").join(", ");
this.escapedLine4 = this.encodeHTML(line4);

To set the {escapedTag} parameter:

this.escapedTag = this.encodeHTML(
    this.entry.getAttributeValue("owd:dossierStatus"));

To set the {approveLabel} and {rejectLabel} parameters:

this.approveLabel = "Mark active";
this.rejectLabel = "Mark retired";

To set the {downloadUrl} and {downloadLabel} parameters:

var filter = "path|" +
    encodeURIComponent(this.entry.getAttributeValue("cmis:path"));
this.downloadUrl = lang.replace(
    "{pageContext}repository#filter={filter}&page=1",
    {
        pageContext: Alfresco.constants.URL_PAGECONTEXT,
        filter: encodeURIComponent(filter)
    }
);
this.downloadLabel = "View in repository";

Using the new component

Now that the Dossier component is implemented, we need to instruct alfresco-inboxes to use it. This is accomplished using the itemClass attribute in the plugin's configuration.

The main configuration file is inboxes.get.config.xml located at src/main/amp/config/alfresco/web-extension/site-webscripts/softwareloop/inboxes/ in the source code.

Here is how to set up an inbox to use the Dossier component:

<inbox id="Dossiers"
       iconClass="foundicon-paper-clip" 
       itemClass="softwareloop/inboxes/Dossier">
  <query><![CDATA[
  SELECT *
  FROM owd:dossier AS d
  ]]></query>
</inbox>

Notice that the itemClass attribute is set on a per-inbox basis. This means that each inbox could have its own customised view implementation. There could even be a library of reusable components suitable to display different content types for a custom content model.

Conclusions

This page has shown how to customise alfresco-inboxes results view so that it exposes the richness of an underlying custom content type, like the owd:dossier of the example. It also shows that the view can be made visually gratifying and customised even in its finest details.

The next page revises the Dossier component, this time focusing on the two action buttons, which still need to be backed-up by useful action implementations. This will also be an opportunity to learn how to update Alfresco's objects via CMIS.

⚠️ **GitHub.com Fallback** ⚠️