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:
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:
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);
});
}
});
});
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.
Our customisation strategy is based on three points:
- leaving the Dossier.html template as it is – it's already capable of a rich visual presentation;
- creating a new Dossier.js component that overrides Item.js and specifically
provides a new implementation for
composeLines()
; - 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 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";
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.
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.