Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all 513 articles
Browse latest View live

AEM 65 - Content Fragment Editor Required Validator for Items in Multifield

$
0
0

Goal


In Content Fragment Editor, Required attribute on multifield checks if the multifield is not empty (>=1 items should exist), but does not check if the items inside multifield are not empty, this extension fills that gap...

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-touchui-cfm-multifield-item-validator

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-touchui-cfm-multifield-item-validator/clientlib and set property categories of String[] type to dam.cfm.authoring.contenteditor.v2 and dependencies String[] to [lodash]

3) Create file ( type nt:file ) /apps/eaem-touchui-cfm-multifield-item-validator/clientlib/js.txt, add the following

  multifield-item-validator.js

4) Create file (type nt:file) /apps/eaem-touchui-cfm-multifield-item-validator/clientlib/multifield-item-validator.js, add the following code

(function ($, $document) {
var CORAL_MULTI_FIELD = "coral-multifield",
CORAL_MULTIFIELD_ITEM = "CORAL-MULTIFIELD-ITEM",
REQ_MF_SEL = "coral-multifield[aria-required='true']",
mfValidator;

$(REQ_MF_SEL).on("coral-collection:add", function(event){
Coral.commons.ready(event.detail.item, handleRequiredOnAdd);
});

$(REQ_MF_SEL).on("coral-collection:remove", function(){
handleRequired(this);
});

$document.on("change", REQ_MF_SEL, function() {
handleRequired(this);
});

function handleRequiredOnAdd(mfItem){
if(mfItem.tagName != CORAL_MULTIFIELD_ITEM){
return;
}

handleRequired($(mfItem).closest(CORAL_MULTI_FIELD)[0]);
}

function handleRequired(mField){
var $fields = $(mField).find(".coral-Form-field");

if(_.isEmpty($fields)){
return;
}

var valid = true;

$fields.each(function(i, field){
var $field = $(field),
val = $field.val().trim();

if(!val){
valid = false;
}
});

if(!mfValidator){
mfValidator = getMultifieldValidator();
}

if(valid){
$(mField).trigger("foundation-validation-valid");
mfValidator.clear(mField);
}else{
$(mField).trigger("foundation-validation-invalid");
mfValidator.show(mField, "Please fill the individual items");
}
}

function getMultifieldValidator(){
var registry = $(window).adaptTo("foundation-registry");

return _.reject(registry.get("foundation.validation.validator"), function(obj){
return (obj.selector.indexOf(".coral-Form-fieldwrapper .coral-Form-field") < 0);
})[0];
}
}(jQuery, jQuery(document)));


AEM 65 - Touch UI Tags Picker show Node Name or Path in Search Results

$
0
0

Goal


Show the tag name or full path in tags picker search results....

Demo | Package Install | Github


Product



Extension - Assets



Extension - Sites



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-touchui-tags-picker-search-show-nodename

2) Create node /apps/eaem-touchui-tags-picker-search-show-nodename/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.ui.coral.common.tagfield, String[] property dependencies with value lodash

3) Create file (nt:file) /apps/eaem-touchui-tags-picker-search-show-nodename/clientlib/js.txt, add

                        show-node-name.js

4) Create file (nt:file) /apps/eaem-touchui-tags-picker-search-show-nodename/clientlib/show-node-name.js, add the following code

(function($, $document) {
var TAGS_FIELD = "cq:tags",
extended = false;

$document.on("foundation-contentloaded", handleTagsPicker);

function handleTagsPicker(){
if(extended){
return;
}

var $tagsField = $("foundation-autocomplete[name$='" + TAGS_FIELD + "']");

if(_.isEmpty($tagsField)){
return;
}

extended = true;

extendPickerSearchResults();
}

function extendPickerSearchResults(){
var registry = $(window).adaptTo("foundation-registry"),
otbHander = getSearchResultsHander();

registry.register("foundation.form.response.ui.success", {
name: "granite.pickerdialog.search.result",
handler: function(formEl, config, data, textStatus, xhr, parsedResponse) {
otbHander.handler.call(this, formEl, config, data, textStatus, xhr, parsedResponse);

var $content = $("#granite-pickerdialog-search-result-content"),
$item, itemId;

$content.find("coral-masonry-item").each(function(index, item){
$item = $(item);

itemId = $item.data("foundationCollectionItemId");

$item.find("coral-card-content").append(getCardPropertyHtml(itemId));
})
}
});
}

function getCardPropertyHtml(content){
return '<coral-card-propertylist>' +
'<coral-card-property>' + content + '</coral-card-property>' +
'</coral-card-propertylist>';
}

function getSearchResultsHander(){
var registry = $(window).adaptTo("foundation-registry");

return _.reject(registry.get("foundation.form.response.ui.success"), function(obj){
return (obj.name != "granite.pickerdialog.search.result");
})[0];
}
})(jQuery, jQuery(document));


AEM 65 - Find Duplicate Assets (Binaries) in existing Repository

$
0
0

Goal


Duplicate binaries in AEM Assets are detected on upload using detect duplicate setting of Create Asset servlet (for more info check documentation)

It checks on upload, so to detect duplicates in an existing repository you can compare the jcr:content/metadata/dam:sha1 of asset nodes (index /oak:index/damAssetLucene/indexRules/dam:Asset/properties/damSha1 should speed it up). Following is a simple DavEx script for detecting duplicates (for more info on DavEx check this post)

Github



Solution


1) Add the necessary jars in classpath...



2) Execute a standalone program apps.FindDuplicateBinariesInAEM with the following code

package apps;

import org.apache.jackrabbit.commons.JcrUtils;

import javax.jcr.*;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import java.util.*;

public class FindDuplicateBinariesInAEM {
public static void main(String[] args) throws Exception{
String REPO = "http://localhost:4502/crx/server";
String WORKSPACE = "crx.default";

Repository repository = JcrUtils.getRepository(REPO);

Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()), WORKSPACE);
QueryManager qm = session.getWorkspace().getQueryManager();

String stmt = "SELECT * FROM [dam:Asset] WHERE ISDESCENDANTNODE(\"/content/dam\") ORDER BY 'jcr:content/metadata/dam:sha1'";
Query q = qm.createQuery(stmt, Query.JCR_SQL2);

NodeIterator results = q.execute().getNodes();
Node node = null, metadata;
String previousSha1 = null, currentSha1 = null, paths = null, previousPath = null;
Map<String, String> duplicates = new LinkedHashMap<String, String>();

while(results.hasNext()){
node = (Node)results.next();

metadata = node.getNode("jcr:content/metadata");

if(metadata.hasProperty("dam:sha1")){
currentSha1 = metadata.getProperty("dam:sha1").getString();
}else{
continue;
}

if(currentSha1.equals(previousSha1)){
paths = duplicates.get(currentSha1);

if( paths == null){
paths = previousPath;
}else{
if(!paths.contains(previousPath)){
paths = paths + "," + previousPath;
}
}

paths = paths + "," + node.getPath();

duplicates.put(currentSha1, paths);
}

previousSha1 = currentSha1;
previousPath = node.getPath();
}

String[] dupPaths = null;

System.out.println("--------------------------------------------------------------------");
System.out.println("Duplicate Binaries in Repository - " + REPO);
System.out.println("--------------------------------------------------------------------");

for(Map.Entry entry : duplicates.entrySet()){
System.out.println(entry.getKey());

dupPaths = String.valueOf(entry.getValue()).split(",");

for(String path : dupPaths){
System.out.println("\t" + path);
}
}

session.logout();
}
}

AEM 65 - Show Approved Assets in Asset Finder

$
0
0

Goal


Show approved assets in asset finder "jcr:content/metadata/dam:status=approved"

Demo | Package Install | Github


Review Task - Approve Assets



Approved Asset Metadata



Asset Finder - Approved Assets



Solution


1) To add the Images - Approved filter in Asset Finder, login to CRXDE Lite, create folder (nt:folder) /apps/eaem-asset-finder-show-approved-assets

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-asset-finder-show-approved-assets/clientlib and set a property categories of String[] type to [cq.authoring.editor.hook.assetfinder]dependencies of type String[] with value lodash

3) Create file ( type nt:file ) /apps/eaem-asset-finder-show-approved-assets/clientlib/js.txt, add the following

                         assetfinder-approved.js

4) Create file ( type nt:file ) /apps/eaem-asset-finder-show-approved-assets/clientlib/assetfinder-approved.js, add the following code

(function ($, $document, author) {
var self = {},
EAEM_APPROVED = 'Images - Approved';

var searchPath = self.searchRoot = "/content/dam",
imageServlet = '/bin/wcm/contentfinder/asset/view.html',
itemResourceType = 'cq/gui/components/authoring/assetfinder/asset';

self.loadAssets = function (query, lowerLimit, upperLimit) {
query = query.concat("\"jcr:content/metadata/dam:status\": \"approved\"");
query = query.concat("order:\"-jcr:content/jcr:lastModified\"");

var param = {
'_dc': new Date().getTime(),
'query': query.concat("order:\"-jcr:content/jcr:lastModified\""),
'mimeType': 'image,application/x-ImageSet,application/x-SpinSet,application/x-MixedMediaSet,application/x-CarouselSet',
'itemResourceType': itemResourceType,
'limit': lowerLimit + ".." + upperLimit,
'_charset_': 'utf-8'
};

return $.ajax({
type: 'GET',
dataType: 'html',
url: Granite.HTTP.externalize(imageServlet) + searchPath,
data: param
});
};

self.setSearchPath = function (spath) {
searchPath = spath;
};

author.ui.assetFinder.register(EAEM_APPROVED, self);
}(jQuery, jQuery(document), Granite.author));

AEM 65 - Touch UI Sort tags alphabetically in Tags Console

$
0
0

Goal


Sort tags alphabetically in Tags Console - http://localhost:4502/aem/tags

For sorting tags in picker check this post

Demo | Package Install | Github


Product (No Alpha Sort)



Extension (Alpha Sorted)



Solution


1) Create a Sling Filter to intercept request URIs starting with /libs/cq/tagging/gui/content/tags* and /libs/cq/tagging/gui/content/tags/jcr:content/views/column*, use a sling servlet request wrapper TagSortSlingServletRequestWrapper to return a sorted datasource iterator

package apps.experienceaem.tags;

import com.adobe.granite.ui.components.ds.AbstractDataSource;
import com.adobe.granite.ui.components.ds.DataSource;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;

import javax.servlet.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

@Component(
service = Filter.class,
immediate = true,
name = "Tags Console Sort Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=REQUEST",
"sling.filter.pattern=((/libs/cq/tagging/gui/content/tags*)|(/libs/cq/tagging/gui/content/tags/jcr:content/views/column*))"
}
)
public class TagsConsoleSortFilter implements Filter {
public static String DATA_SOURCE_NAME = DataSource.class.getName();

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(new TagSortSlingServletRequestWrapper((SlingHttpServletRequest)request), response);
}

@Override
public void destroy() {
}

private class TagSortSlingServletRequestWrapper extends SlingHttpServletRequestWrapper {
public TagSortSlingServletRequestWrapper(final SlingHttpServletRequest request) {
super(request);
}

@Override
public Object getAttribute(String attrName) {
if(!TagsConsoleSortFilter.DATA_SOURCE_NAME.equals(attrName)){
return super.getAttribute(attrName);
}

DataSource ds = (DataSource)super.getAttribute(attrName);

if(ds == null){
return ds;
}

final List<Resource> sortedList = new ArrayList<Resource>();
Iterator<Resource> items = ds.iterator();

while(items.hasNext()){
sortedList.add(items.next());
}

sortedList.sort(Comparator.comparing(Resource::getValueMap, (v1, v2) -> {
return v1.get("jcr:title", "").compareTo(v2.get("jcr:title", ""));
}));

ds = new AbstractDataSource() {
public Iterator<Resource> iterator() {
return sortedList.iterator();
}
};

return ds;
}
}
}


2) To increase the limit (number of tags loaded in column view) from default 40, user overlay and play with /apps/cq/tagging/gui/content/tags/jcr:content/views/column@limit, /libs/cq/tagging/gui/content/tags/jcr:content/views/column/datasource@limit






AEM 65 - Add Path Column to Assets Search Results

$
0
0

Goal


Add Path column to Assets search results console

Demo | Package Install | Github


Product



Extension


Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-add-path-column-in-search-results

2) Create node /apps/eaem-add-path-column-in-search-results/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.gui.common.admin.searchpanel, String[] property dependencies with value lodash

3) Create file (nt:file) /apps/eaem-add-path-column-in-search-results/clientlib/js.txt, add

                        add-path-column.js

4) Create file (nt:file) /apps/eaem-add-path-column-in-search-results/clientlib/add-path-column.js, add the following code

(function ($, $document) {
var FOUNDATION_CONTENT_LOADED = "foundation-contentloaded",
GRANITE_OMNI_SEARCH_RESULT = "#granite-omnisearch-result",
EAEM_SEARCH_PATH_COLUMN = "eaem-search-path-column",
EAEM_SEARCH_PATH_COLUMN_HEADER = "Path",
ROW_SELECTOR = "tr.foundation-collection-item",
GRANITE_OMNI_SEARCH_CONTENT = ".granite-omnisearch-content";

$document.on(FOUNDATION_CONTENT_LOADED, GRANITE_OMNI_SEARCH_CONTENT, function(event){
_.defer(function(){
handleContentLoad(event);
});
});

function handleContentLoad(event){
var layout = $(GRANITE_OMNI_SEARCH_RESULT).data("foundationLayout");

if(!layout || (layout.layoutId !== "list")){
return;
}

addColumnHeaders();

fillColumnData();
}

function fillColumnData(){
var $fui = $(window).adaptTo("foundation-ui");

$fui.wait();

$(ROW_SELECTOR).each(function(index, item){
itemHandler($(item) );
});

function itemHandler($row){
if(!_.isEmpty($row.find("[" + EAEM_SEARCH_PATH_COLUMN + "]"))){
return;
}

if(_.isEmpty($row.find("td.foundation-collection-item-title"))){
return;
}

var itemPath = $row.data("foundation-collection-item-id");

$row.find("td:last").before(getListCellHtml(itemPath));
}

$fui.clearWait();
}

function getListCellHtml(colValue){
return '<td is="coral-table-cell"' + EAEM_SEARCH_PATH_COLUMN + '>' + colValue + '</td>';
}

function addColumnHeaders(){
if(checkIFHeadersAdded()){
return;
}

var $container = $(GRANITE_OMNI_SEARCH_CONTENT),
$headRow = $container.find("thead > tr");

$headRow.append(getTableHeader(EAEM_SEARCH_PATH_COLUMN_HEADER));
}

function getTableHeader(colText) {
return '<th is="coral-table-headercell"' + EAEM_SEARCH_PATH_COLUMN + '>' + colText + '</th>';
}

function checkIFHeadersAdded(){
return !_.isEmpty($(GRANITE_OMNI_SEARCH_CONTENT).find("tr").find("[" + EAEM_SEARCH_PATH_COLUMN + "]"));
}
})(jQuery, jQuery(document));

AEM 65 - Support Required validation in Bulk Metadata Editor for Multiple Selections

$
0
0

Goal


AEM bulk metadata editor supports required validation for single asset selection; when multiple assets are selected, required validation of fields is ignored. Following solution provides this missing feature...

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-bulk-edit-show-required-warning

2) Create node /apps/eaem-bulk-edit-show-required-warning/clientlib of type cq:ClientLibraryFolder, add String property categories with value dam.gui.coral.metadataeditor, String[] property dependencies with value lodash

3) Create file (nt:file) /apps/eaem-bulk-edit-show-required-warning/clientlib/js.txt, add

                        required-warning.js

4) Create file (nt:file) /apps/eaem-bulk-edit-show-required-warning/clientlib/required-warning.js, add the following code

(function ($, $document) {
var FOUNDATION_CONTENT_LOADED = "foundation-contentloaded",
META_EDITOR_FORM_SEL = "#aem-assets-metadataeditor-formid",
SOFT_SUBMIT_SEL = "#soft-submit-popover",
FOUNDATION_SELECTIONS_CHANGE = "foundation-selections-change",
DAM_ADMIN_CHILD_PAGES_SEL = ".cq-damadmin-admin-childpages",
INVALIDS_KEY = "foundationValidationBind.internal.invalids";

$.fn.metadataUpdateErrorUI = $.fn.updateErrorUI;

$.fn.updateErrorUI = function() {
$.fn.superUpdateErrorUI.call(this);
$.fn.metadataUpdateErrorUI.call(this);
};

function validateRequiredFields() {
var $fields = $('.data-fields.active [aria-required="true"]'), $ele;

$fields.each(function(index, field){
Coral.commons.ready(field, function(elem) {
$ele = $(elem);

$ele.checkValidity();
$ele.updateErrorUI();
});
});
}

function showAlert(message, title, callback){
var fui = $(window).adaptTo("foundation-ui"),
options = [{
id: "ok",
text: "OK",
primary: true
}];

message = message || "Unknown Error";
title = title || "Error";

fui.prompt(title, message, "error", options, callback);
}

$document.on(FOUNDATION_CONTENT_LOADED, function() {
validateRequiredFields();
});

$document.on(FOUNDATION_SELECTIONS_CHANGE, DAM_ADMIN_CHILD_PAGES_SEL , function(e) {
validateRequiredFields();
});

$document.on("coral-overlay:open", SOFT_SUBMIT_SEL , function() {
var invalids = $(META_EDITOR_FORM_SEL).data(INVALIDS_KEY);

if (!invalids || (invalids.length === 0)) {
$(SOFT_SUBMIT_SEL).show();
}else{
$(SOFT_SUBMIT_SEL).hide();
showAlert("One or more required field(s) is/are empty.", "Error");
}
});
})(jQuery, jQuery(document));

AEM 65 - Touch UI Assets Bulk Update Append Single Line Text Fields of type String

$
0
0

Goal


Default behavior of AEM Assets Bulk Update Editor Append mode works for schema fields of data type Multiple (node.getProperty(prop).isMultiple()) only

This extension adds appending of single line string fields data in Append mode with comma delimiter...

Demo | Package Install | Github


Configuration



Append Extension




Solution


1) Create a Sling Request Post Processor apps.experienceaem.assets.EAEMBulkUpdateAppend and set it with lower ranking e.g Integer=-99 to make sure it executes after product's bulk update post processor com.day.cq.dam.core.impl.servlet.BulkUpdatePostProcessor

package apps.experienceaem.assets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.servlets.post.Modification;
import org.apache.sling.servlets.post.ModificationType;
import org.apache.sling.servlets.post.SlingPostProcessor;
import org.json.JSONObject;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.Session;
import java.util.List;

@Component(
immediate = true,
service = { SlingPostProcessor.class },
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
}
)
@Designate(ocd = EAEMBulkUpdateAppend.EAEMBulkUpdateAppendConfiguration.class)
public class EAEMBulkUpdateAppend implements SlingPostProcessor {
private static final String MODE = "mode";
private static final String MODE_SOFT = "soft";
private static final String BULK_UPDATE = "dam:bulkUpdate";
private static final String METADATA_KEY = "mdvm";

private static final String DEFAULT_FOR_APPEND = "eaemRequired" ;
private static final String SEPARATOR_FOR_APPEND = "," ;
private static final int JCR_STRING_TYPE = 1 ;

private String[] metadataForAppend = new String[] {DEFAULT_FOR_APPEND};

@Activate
protected void activate(EAEMBulkUpdateAppendConfiguration configuration) {
metadataForAppend = configuration.metadataFields();
}

public void process(SlingHttpServletRequest request, List<Modification> modifications) throws Exception {
ResourceResolver resolver = request.getResourceResolver();
String reqType = request.getParameter(BULK_UPDATE);
String reqMode = request.getParameter(MODE);

if ( (reqType == null) || (reqMode == null) || !reqMode.equals(MODE_SOFT)) {
return;
}

Session session = resolver.adaptTo(Session.class);
RequestParameter[] assets = request.getRequestParameters("asset");

if ( (session == null) || (assets == null)) {
return;
}

for (RequestParameter asset : assets) {
JSONObject assetJson = new JSONObject(asset.toString());

for (Modification change : modifications) {
if (!change.getType().equals(ModificationType.MODIFY)) {
continue;
}

processChanges(session, change, assetJson);
}
}

session.save();
}

private void processChanges(Session session, Modification change, JSONObject assetJson) throws Exception{
Item jcrItem = null;
JSONObject metaJson = null, propJson = null;
String assetPath = assetJson.getString("path");

String source = change.getSource();

if( (source == null) || (!source.startsWith(assetPath))){
return;
}

jcrItem = session.getItem(source);

if ( (jcrItem == null) || jcrItem.isNode()) {
return;
}

String metadataPath = assetPath + "/jcr:content/metadata";

if (assetJson.has(METADATA_KEY)) {
metaJson = (JSONObject) assetJson.get(METADATA_KEY);
}

if(metaJson == null){
return;
}

if (metaJson.has(metadataPath)) {
propJson = (JSONObject) metaJson.get(metadataPath);
}

if(propJson == null){
return;
}

Node metadataNode = session.getNode(metadataPath);
String existingValue = null;
Property metaProp = null;

for(String prop : metadataForAppend) {
if(!propJson.has(prop)){
continue;
}

existingValue = propJson.getString(prop);

if(metadataNode.hasProperty(prop)){
metaProp = metadataNode.getProperty(prop);
}

if( metaProp != null ){
if(metaProp.getType() != JCR_STRING_TYPE){
continue;
}

existingValue = existingValue + SEPARATOR_FOR_APPEND + metaProp.getString();
}

metadataNode.setProperty(prop, existingValue, JCR_STRING_TYPE);
}
}

@ObjectClassDefinition(
name = "EAEM Metadata fields",
description = "EAEM Metadata fields for append mode"
)
public @interface EAEMBulkUpdateAppendConfiguration {

@AttributeDefinition(
name = "Metadata fields",
description = "Metadata fields for append e.g. eaemRequired, service looks for this property of type STRING and ONLY in jcr:content/metadata",
type = AttributeType.STRING
)
String[] metadataFields() default { DEFAULT_FOR_APPEND, "node.getProperty(prop).isMultiple()" };
}
}


AEM 65 - Touch UI Assets Console Set Default to List View

$
0
0

Goal


Default view of AEM Assets console is Card, change it to List

The following post uses a sling filter to set cookie cq-assets-files=list on login, another way of handling it is by overlaying /libs/dam/gui/content/assets/jcr:content/views in /apps and changing the order of subnodes so list comes first

Demo | Package Install | Github


Default View - List



Browser Cookie set on Login



Solution


1) Create filter apps.experienceaem.assets.EAEMSetDefaultView for resource type granite/core/components/login and set the cookie cq-assets-files to list when empty

package apps.experienceaem.assets;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;

import javax.servlet.*;
import javax.servlet.http.Cookie;
import java.io.IOException;

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM Login page Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.resourceTypes=granite/core/components/login"
}
)
public class EAEMSetDefaultView implements Filter {
private static String ASSETS_VIEW_COOKIE = "cq-assets-files";

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;
SlingHttpServletResponse slingResponse = (SlingHttpServletResponse)response;

Cookie cookie = slingRequest.getCookie(ASSETS_VIEW_COOKIE);

if ( (cookie == null) || StringUtils.isEmpty(cookie.getValue())) {
cookie = new Cookie(ASSETS_VIEW_COOKIE, "list");
cookie.setPath("/");
slingResponse.addCookie(cookie);
}

chain.doFilter(slingRequest, response);
}

public void init(FilterConfig filterConfig) throws ServletException {
}

public void destroy() {
}
}



AEM 65 - Assets Admin Search Rail Predicate to check if Metadata Property exists

$
0
0

Goal


Add a new predicate to Assets Admin Search rail to filter assets based on the existence of metadata

Requirement could be to find all assets with specific metadata missing. Here a filter (predicate) is added to filter assets containing / not containing metadata property (configurable) jcr:content/metadata/dc:title

Demo | Package Install | Github


Search Filter



Search form Configuration




Solution


1) Login to CRXDe Lite and create the metadata exists predicate configuration metadataexists  in /apps/settings/dam/search/facets/formbuilderconfig/predicatetypes/items

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:Page">
<adminui/>
<search jcr:primaryType="nt:unstructured">
<facets jcr:primaryType="nt:unstructured">
<formbuilderconfig jcr:primaryType="nt:unstructured">
<predicatetypes jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<metadataexists
jcr:primaryType="nt:unstructured"
fieldLabel="Metadata Property"
fieldPropResourceType="/apps/eaem-search-form-metadata-exists-predicate/metadata-exists-field"
fieldResourceType="/apps/eaem-search-form-metadata-exists-predicate/metadata-exists-predicate"
fieldTitle="Experience AEM Metadata Exists Predicate"
fieldViewResourceType="granite/ui/components/foundation/form/formbuilder/checkbox"
renderType="checkbox"/>
</items>
</predicatetypes>
</formbuilderconfig>
</facets>
</search>
</jcr:root>


2) Create the configuration page for predicate (fieldPropResourceType in step 1) /apps/eaem-search-form-metadata-exists-predicate/metadata-exists-field/metadata-exists-field.jsp with the following code (metadataexists is for identifying the config page to render under /mnt/overlay/settings/dam/search/facets/formbuilderconfig/predicatetypes/items )

<%@include file="/libs/granite/ui/global.jsp" %>
<%@ page session="false" contentType="text/html" pageEncoding="utf-8"
import="com.adobe.granite.ui.components.Config" %>

<%
String key = resource.getName();
Config cfg = new Config(resource);
String metaType = "metadataexists";

String listOrder = cfg.get("listOrder", String.class);
listOrder = (listOrder == null) ? "" : listOrder;

String fieldLabel = i18n.get("Metadata Exists Predicate");
%>

<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key) %>">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/jcr:primaryType") %>" value="nt:unstructured">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/sling:resourceType") %>" value="/apps/eaem-search-form-metadata-exists-predicate/metadata-exists-predicate">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/fieldLabel") %>" value="<%= fieldLabel %>">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/metaType") %>" value="<%= metaType %>">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/listOrder@Delete") %>">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/listOrder@TypeHint") %>" value="String">
<input type="hidden" class="listOrder" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/listOrder") %>" value="<%= xssAPI.encodeForHTMLAttr(listOrder) %>">

<div><h3><%= i18n.get("Metadata Exists Predicate")%></h3></div>

<% request.setAttribute ("com.adobe.cq.datasource.fieldtextplaceholder", i18n.get("Metadata Property"));%>

<sling:include resource="<%= resource %>" resourceType="dam/gui/coral/components/admin/customsearch/formbuilder/predicatefieldproperties/fieldlabelpropertyfield"/>

<sling:include resource="<%= resource %>" resourceType="dam/gui/coral/components/admin/customsearch/formbuilder/predicatefieldproperties/maptopropertyfield"/>

<sling:include resource="<%= resource %>" resourceType="granite/ui/components/foundation/form/formbuilder/formfieldproperties/titlefields"/>

<sling:include resource="<%= resource %>" resourceType="granite/ui/components/foundation/form/formbuilder/formfieldproperties/deletefields"/>

3) Create the predicate widget render html (fieldResourceType in step 1) /apps/eaem-search-form-metadata-exists-predicate/metadata-exists-predicate/metadata-exists-predicate.jsp with following code (it includes clientlib eaem.dam.admin.metadataexists)

<%@include file="/libs/granite/ui/global.jsp" %>
<%@ page session="false" contentType="text/html" pageEncoding="utf-8"
import="com.adobe.granite.ui.components.Config"%>

<%
Config cfg = new Config(resource);
String metaPropName = cfg.get("name", "");

long predicateIndex = cfg.get("listOrder", 5000L);

String indexGroup = predicateIndex + "_group";
String predicateName = indexGroup + ".property";
String propertyOperation = predicateName + ".operation";

boolean foldableOpen = cfg.get("open", true);
String selected = foldableOpen ? "selected":"";
%>

<ui:includeClientLib categories="eaem.dam.admin.metadataexists" />

<coral-accordion variant="large">
<coral-accordion-item "<%=selected%>" data-metaType="checkboxgroup" data-type="metadataexists">
<coral-accordion-item-label><%= xssAPI.encodeForHTML(cfg.get("text", i18n.get("Property"))) %></coral-accordion-item-label>
<coral-accordion-item-content class="coral-Form coral-Form--vertical" id="<%= xssAPI.encodeForHTMLAttr(resource.getPath()) %>">
<input type="hidden" name="<%=predicateName%>" value="<%= xssAPI.encodeForHTMLAttr(metaPropName) %>">
<coral-checkbox class="coral-Form-field eaem-metadata-exists-predicate" name="<%=propertyOperation%>" value="not">
Not Exists
</coral-checkbox>
<coral-checkbox class="coral-Form-field eaem-metadata-exists-predicate" name="<%=propertyOperation%>" value="exists">
Exists
</coral-checkbox>
</coral-accordion-item-content>
</coral-accordion-item>
</coral-accordion>

4) Create node /apps/eaem-search-form-metadata-exists-predicate/metadata-exists-predicate/clientlib of type cq:ClientLibraryFolder, add String property categories with value eaem.dam.admin.metadataexists, String[] property dependencies with value lodash

6) Create file (nt:file) /apps/eaem-search-form-metadata-exists-predicate/metadata-exists-predicate/clientlib/js.txt, add

                        metadata-exists.js

7) Create file (nt:file) /apps/eaem-search-form-metadata-exists-predicate/metadata-exists-predicate/clientlib/metadata-exists.js, add the following code

(function($, $document) {
var EAEM_METADATA_EXISTS_PREDICATE = ".eaem-metadata-exists-predicate";

$document.on("change", EAEM_METADATA_EXISTS_PREDICATE, function(event) {
$(EAEM_METADATA_EXISTS_PREDICATE).removeAttr("checked");
event.currentTarget.checked = true;

var $form = $(this).closest(".granite-omnisearch-form");
$form.submit();
})

})(jQuery, jQuery(document));


AEM 65 - Assets Admin Search Rail Predicate to check if Node or Rendition or Thumbnail etc. exists

$
0
0

Goal


Create an Assets Search Form Predicate to give users flexibility searching for assets with missing renditions etc.

Demo | Package Install | Github


Solution


1) Install the extension for adding metadata exists predicate, discussed here https://experience-aem.blogspot.com/2019/07/aem-65-assets-search-predicate-to-check-if-metadata-property-exists.html

2) Add the predicate Experience AEM Metadata Exists Predicate in search form Assets Admin Search Rail http://localhost:4502/libs/cq/core/content/tools/customsearch.html/conf/global/settings/dam/search/facets/assets/jcr:content



3) Configure the rendition jcr:primaryType property e.g. to search for missing 48 x 48 renditions add jcr:content/renditions/cq5dam.thumbnail.48.48.png/jcr:content/jcr:primaryType



4) Predicate in search rail...



AEM 65 - Tags Picker Select Tags from Multiple Folders

$
0
0

Goal


In Tags Picker otb cq/gui/components/coral/common/form/tagfield when a user navigates between folders existing tag selections are cleared. This extension allows user to select tags from multiple folders

Demo | Package Install | Github




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-touchui-tags-picker-select-multiple

2) Create node /apps/eaem-touchui-tags-picker-select-multiple/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.ui.coral.common.tagfield, String[] property dependencies with value lodash

3) Create file (nt:file) /apps/eaem-touchui-tags-picker-select-multiple/clientlib/js.txt, add

                        select-multiple.js

4) Create file (nt:file) /apps/eaem-touchui-tags-picker-select-multiple/clientlib/select-multiple.js, add the following code

(function($, $document) {
var TAGS_FIELD = "./jcr:content/metadata/cq:tags",
SELECTED_TAGS_DIV = "eaem-column-view-selections",
FOUNDATION_SELECTIONS_CHANGE = "foundation-selections-change",
FOUNDATION_SELECTIONS_ITEM = "foundation-selections-item",
FOUNDATION_COLLECTION = ".foundation-collection",
FOUNDATION_COLLECTION_ITEM_VALUE = "foundation-picker-collection-item-value",
$tagsContainer,
extended = false;

$document.on("foundation-contentloaded", handleTagsPicker);

function handleTagsPicker(){
if(extended){
return;
}

var $tagsField = $("foundation-autocomplete[name='" + TAGS_FIELD + "']");

if(_.isEmpty($tagsField)){
return;
}

var pathField = $tagsField[0];

extended = true;

extendPicker(pathField);
}

function extendPicker(pathField){
var origShowPicker = pathField._showPicker,
origSetSelections = pathField._setSelections;

pathField._showPicker = function(){
origShowPicker.call(this);

var $dialog = $(this._picker.el),
$columnView = $dialog.find("coral-columnview");

addSelectedSection($columnView);

$dialog.on(FOUNDATION_SELECTIONS_CHANGE, FOUNDATION_COLLECTION, collectTags);
};

pathField._setSelections = function(selections, deferChangeEvent){
var $tags = $tagsContainer.find("coral-tag"), selectedTags = [];

_.each($tags, function(tag){
selectedTags.push({
text: $(tag).find("coral-tag-label").html(),
value: tag.value
});
});

origSetSelections.call(this, selectedTags, deferChangeEvent);
}
}

function collectTags(){
var $tag, tagValue, selectedTags = {};

$("." + FOUNDATION_SELECTIONS_ITEM).each(function(index, tag){
$tag = $(tag);

tagValue = $tag.data(FOUNDATION_COLLECTION_ITEM_VALUE);

if(_.isEmpty(tagValue)){
return;
}

selectedTags[tagValue] = $tag.data("foundation-picker-collection-item-text");
});

var $submit = $(this).closest("coral-dialog").find(".granite-pickerdialog-submit");

if(_.isEmpty(selectedTags)){
$submit.prop("disabled", _.isEmpty(getSelectedTagsInContainer()));
setSelCount();
return;
}

buildSelectedContainer(selectedTags, $tagsContainer);

$(this).adaptTo("foundation-selections").clear();

$submit.prop("disabled", false);
}

function buildSelectedContainer(selectedTags, $container) {
var $tagList = $container.find("coral-taglist");

_.each(selectedTags, function (text, value) {
$tagList.append(getTagHtml(text, value));
});

setSelCount();
}

function getTagHtml(title, value){
return '<coral-tag class="coral3-Tag" value="' + value + '">' +
'<coral-tag-label>' + title + '</coral-tag-label>' +
'</coral-tag>';
}

function addSelectedSection($columnView){
$columnView.css("height", "70%");

$tagsContainer = $("<div/>").appendTo($columnView.parent());

var html = "<div style='text-align:center; padding:1px; background-color: rgba(0,0,0,0.05)'>" +
"<h3>Selected Tags</h3>" +
"</div>" +
"<div style='margin: 15px' id='" + SELECTED_TAGS_DIV + "'>" +
"<coral-taglist class='coral3-TagList'></coral-taglist>" +
"</div>";

$(html).appendTo($tagsContainer);

$tagsContainer.find("coral-taglist").on("change", function(){
$tagsContainer.closest("coral-dialog").find(".granite-pickerdialog-submit").prop("disabled", _.isEmpty(this.values));
setSelCount();
})
}

function setSelCount(){
_.defer(function(){
$(".foundation-admin-selectionstatus").html(getSelectedTagsInContainer().length);
});
}

function getSelectedTagsInContainer(){
var $tagList = $tagsContainer.find("coral-taglist");

if(_.isEmpty($tagList)){
return [];
}

return $tagList[0].values;
}
})(jQuery, jQuery(document));



AEM 65 - Integrating AEM Projects with JIRA for Task Management

$
0
0

Goal


If AEM Inbox (http://localhost:4502/aem/inbox) is not enough for the customer's Task Management needs and would like to integrate with a more complex Task management system like Atlassian JIRA you can try the following solution..

Here the scenario is, gather a team (years of training in using JIRA) for review, approve and publish a bunch of Sites pages. The users are good with JIRA and would like to continue using JIRA, rather than getting trained in a new task management software (AEM Inbox) for assigning and completing the respective tasks.

The solution discussed below uses AEM Projects and Workflow processes for setting up a team, assign tasks for approving and publishing the pages using otb Request for Activation workflow

Check this Adobe tutorial for creating projects

For downloading jira check this page, and this one for REST api

Demo | Package Install | Github


AEM JIRA Integration Project Setup & Execution

1) Select the template Experience AEM Approve Pages and Assets Project (available in Package Install) to create a new project for reviewing the pages



2) Add the users (team) responsible for completing the review and publish page task



3) In the Workflows pod click on Start Workflow (project template is setup with workflow models for projects created using this template)



4) Select the workflow Experience AEM Request for Activation



5) Select the page path and user responsible for reviewing, approving and publishing the page (steps in workflow process)



6) A Task is created in JIRA and assigned to the respective user (assuming the user has same id in AEM and JIRA; additionally a task is created in AEM Inbox). The expectation is, user clicks on AEM page link in ticket description, publishes the page and resolves ticket in JIRA....



Solution


1) Create the AEM project template Experience AEM Approve Pages and Assets Project  /apps/eaem-review-assets-jira-tasks/projects/templates/approve-assets-project (available  in Package Install) with the pods for Team, Tasks, Workflows, Project Info...

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:description="A project to approve and publish Assets"
jcr:primaryType="cq:Template"
jcr:title="Experience AEM Approve Pages and Assets Project"
ranking="{Long}1"
wizard="/libs/cq/core/content/projects/wizard/steps/defaultproject.html">
<jcr:content
jcr:primaryType="nt:unstructured"
detailsHref="/projects/details.html"/>
<gadgets jcr:primaryType="nt:unstructured">
<team
jcr:primaryType="nt:unstructured"
jcr:title="Team"
sling:resourceType="cq/gui/components/projects/admin/pod/teampod"
cardWeight="60"/>
<tasks
jcr:primaryType="nt:unstructured"
jcr:title="Tasks"
sling:resourceType="cq/gui/components/projects/admin/pod/taskpod"
cardWeight="100"/>
<work
jcr:primaryType="nt:unstructured"
jcr:title="Workflows"
sling:resourceType="cq/gui/components/projects/admin/pod/workpod"
cardWeight="80"/>
<projectinfo
jcr:primaryType="nt:unstructured"
jcr:title="Project Info"
sling:resourceType="cq/gui/components/projects/admin/pod/projectinfopod"
cardWeight="100"/>
</gadgets>
<roles jcr:primaryType="nt:unstructured">
<approvers
jcr:primaryType="nt:unstructured"
jcr:title="EAEM Approvers"
roleclass="owner"
roleid="approvers"/>
</roles>
<workflows
jcr:primaryType="nt:unstructured"
tags="[]">
<models jcr:primaryType="nt:unstructured">
<assets-approval
jcr:primaryType="nt:unstructured"
modelId="/var/workflow/models/request_for_activation"
wizard="/apps/eaem-review-assets-jira-tasks/projects/wizards/assets-approval-start.html"/>
</models>
</workflows>
</jcr:root>


2) Line #46 with workflow model /var/workflow/models/request_for_activation specifies projects created using this template have access to workflow Experience AEM Request for Activation from the workflow pod

3) The workflow wizard /apps/eaem-review-assets-jira-tasks/projects/wizards/assets-approval-start.html in Line # 47 provides the necessary form for entering workflow specific information like payload, assignee...

4) Create a workflow process step apps.experienceaem.assets.EAEMJiraWFProcess for executing REST calls and create tasks / tickets in JIRA (in the demo a JIRA instance is running locally on http://localhost:8080)

package apps.experienceaem.assets;

import com.day.cq.commons.Externalizer;
import com.day.cq.wcm.api.Page;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.exec.WorkflowProcess;

import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.message.BasicHeader;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.StringEntity;

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import javax.jcr.Node;
import javax.jcr.Session;
import java.nio.charset.StandardCharsets;
import java.util.Collections;

@Component(
immediate = true,
service = {WorkflowProcess.class},
property = {
"process.label = Experience AEM JIRA Integration Workflow Step"
}
)
@Designate(ocd = EAEMJiraWFProcess.Configuration.class)
public class EAEMJiraWFProcess implements WorkflowProcess {
private static final Logger log = LoggerFactory.getLogger(EAEMJiraWFProcess.class);

private static final String JIRA_REST_API = "http://localhost:8080/rest/api/2/issue/";

@Reference
private ResourceResolverFactory resourceResolverFactory;

@Reference
private Externalizer externalizer;

private String jiraProjectKey , jiraUserName, jiraPassword;

@Activate
protected void activate(EAEMJiraWFProcess.Configuration configuration) {
jiraProjectKey = configuration.jiraProjectKey();
jiraUserName = configuration.jiraUserName();
jiraPassword = configuration.jiraPassword();
}

public void execute(WorkItem workItem, WorkflowSession wfSession, MetaDataMap mapArgs) throws WorkflowException {
try {
if(StringUtils.isEmpty(jiraProjectKey) || StringUtils.isEmpty(jiraPassword)
|| StringUtils.isEmpty(jiraUserName)){
throw new RuntimeException("Required jira details missing from configuration jiraProjectKey, jiraUserName, jiraPassword");
}

Session session = wfSession.getSession();
WorkflowData wfData = workItem.getWorkflowData();

String pagePath = null;
String payLoadType = wfData.getPayloadType();

if(payLoadType.equals("JCR_PATH") && wfData.getPayload() != null) {
if(session.itemExists((String)wfData.getPayload())) {
pagePath = (String)wfData.getPayload();
}
} else if( (wfData.getPayload() != null) && payLoadType.equals("JCR_UUID")) {
Node metaDataMap = session.getNodeByUUID((String)wfData.getPayload());
pagePath = metaDataMap.getPath();
}

if(StringUtils.isEmpty(pagePath)){
log.warn("Page path - " + wfData.getPayload() + ", does not exist");
return;
}

String assignee = (String)workItem.getWorkflow().getWorkflowData().getMetaDataMap().get("assignee").toString();

ResourceResolver resolver = getResourceResolver(session);
Resource pageResource = resolver.getResource(pagePath);

createTicket(createTicketBody(resolver, pageResource.adaptTo(Page.class), assignee));
} catch (Exception e) {
log.error("Error while creating JIRA ticket", e);
}
}

private String createTicketBody(ResourceResolver resolver, Page page, String user) throws Exception{
JSONObject request = new JSONObject();
JSONObject fields = new JSONObject();
JSONObject project = new JSONObject();
JSONObject issueType = new JSONObject();

String authorLink = externalizer.externalLink(resolver, Externalizer.AUTHOR, "/editor.html" + page.getPath() + ".html");

project.put("key", jiraProjectKey);
issueType.put("name", "Task");

fields.put("project", project);
fields.put("issuetype", issueType);
fields.put("summary", "Publish page : " + page.getTitle());
fields.put("description", "Review Approve and Publish page : " + page.getTitle() + "\n\n" + authorLink);

if(StringUtils.isNotEmpty(user)){
JSONObject assignee = new JSONObject();

assignee.put("name", user);
fields.put("assignee", assignee);
}

request.put("fields", fields);

return request.toString();
}

private void createTicket(String requestBody) throws Exception{
CloseableHttpClient client = null;
String responseBody = "";

try {
SocketConfig sc = SocketConfig.custom().setSoTimeout(180000).build();
client = HttpClients.custom().setDefaultSocketConfig(sc).build();

HttpPost post = new HttpPost(JIRA_REST_API);
StringEntity entity = new StringEntity(requestBody, "UTF-8");

post.addHeader("Content-Type", "application/json");
post.setEntity(entity);
post.setHeader(getAuthorizationHeader());

HttpResponse response = client.execute(post);

HttpEntity responseEntity = response.getEntity();

responseBody = IOUtils.toString(responseEntity.getContent(), "UTF-8");

log.debug("JIRA create ticket response - " + responseBody);
}finally{
if(client != null){
client.close();
}
}
}

private Header getAuthorizationHeader(){
String auth = jiraUserName + ":" + jiraPassword;

byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.ISO_8859_1));
String authHeader = "Basic " + new String(encodedAuth);

return new BasicHeader(HttpHeaders.AUTHORIZATION, authHeader);
}

private ResourceResolver getResourceResolver(final Session session) throws LoginException {
return resourceResolverFactory.getResourceResolver( Collections.<String, Object>
singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session));
}

@ObjectClassDefinition(
name = "Experience AEM JIRA Integration",
description = "Experience AEM JIRA Integration"
)
public @interface Configuration {

@AttributeDefinition(
name = "JIRA Project key",
description = "JIRA project key found in http://<jira server>/secure/BrowseProjects.jspa?selectedCategory=all&selectedProjectType=all",
type = AttributeType.STRING
)
String jiraProjectKey() default "";

@AttributeDefinition(
name = "JIRA User name",
description = "JIRA User name",
type = AttributeType.STRING
)
String jiraUserName() default "";

@AttributeDefinition(
name = "JIRA Password",
description = "JIRA Password",
type = AttributeType.PASSWORD
)
String jiraPassword() default "";
}
}

5) Add a workflow step Experience AEM - Create Ticket in JIRA with above process in the workflow Experience AEM Request for Activation

                   http://localhost:4502/editor.html/conf/global/settings/workflow/models/request_for_activation.html



6) Add the JIRA Project key, admin Username and Password in workflow step OSGI configuration

                   http://localhost:4502/system/console/configMgr/apps.experienceaem.assets.EAEMJiraWFProcess




AEM 6510 - Assets Admin Search Rail Predicate to search based on Partial Filename, Case Ignored

$
0
0

Goal


AEM Assets Omnisearch does a full text search and is not nodename (or filename) based. XPathquery generated by Omnisearch is something like below...

              (/jcr:root/content/dam//element(*, nt:folder)[(jcr:contains(., 'show'))] | /jcr:root/content/dam//element(*, dam:Asset)[(jcr:contains(., 'show'))])

Solution discussed below performs a search based on filename (and not file content) running the following query in AEM...

              (/jcr:root/content/dam//element(*, nt:folder)[(jcr:like(fn:lower-case(fn:name()), '%show%') and jcr:contains(., 'png'))] | /jcr:root/content/dam//element(*, dam:Asset)[(jcr:like(fn:lower-case(fn:name()), '%show%') and jcr:contains(., 'png'))])

For Assets search form Metadata Exists predicatecheck this post


Search Filter in Rail



Assets Search form Configuration




Solution


1) Login to CRXDe Lite and add the File name predicate configuration eaemignorecasenodename  in /apps/settings/dam/search/facets/formbuilderconfig/predicatetypes/items

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:Page">
<adminui/>
<search jcr:primaryType="nt:unstructured">
<facets jcr:primaryType="nt:unstructured">
<formbuilderconfig jcr:primaryType="nt:unstructured">
<predicatetypes jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<eaemignorecasenodename
jcr:primaryType="nt:unstructured"
fieldLabel="Filename"
fieldPropResourceType="/apps/eaem-search-form-nodename-predicate/nodename-field"
fieldResourceType="/apps/eaem-search-form-nodename-predicate/nodename-predicate"
fieldTitle="Experience AEM Filename Predicate"
fieldViewResourceType="granite/ui/components/foundation/form/textfield"
renderType="text"/>
</items>
</predicatetypes>
</formbuilderconfig>
</facets>
</search>
</jcr:root>

2) Create the configuration page for predicate (fieldPropResourceType in step 1) /apps/eaem-search-form-nodename-predicate/nodename-field/nodename-field.jsp with the following code

<%@include file="/libs/granite/ui/global.jsp" %>
<%@ page session="false" contentType="text/html" pageEncoding="utf-8"
import="com.adobe.granite.ui.components.Config" %>

<%
String key = resource.getName();
String metaType = "eaemignorecasenodename";

String fieldLabel = i18n.get("Filename");
%>

<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key) %>">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/jcr:primaryType") %>" value="nt:unstructured">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/sling:resourceType") %>" value="/apps/eaem-search-form-nodename-predicate/nodename-predicate">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/fieldLabel") %>" value="<%= fieldLabel %>">
<input type="hidden" name="<%= xssAPI.encodeForHTMLAttr("./items/" + key + "/metaType") %>" value="<%= metaType %>">

<div><h3><%= i18n.get("Filename predicate")%></h3></div>

<% request.setAttribute ("com.adobe.cq.datasource.fieldtextplaceholder", i18n.get("Filename"));%>

<sling:include resource="<%= resource %>" resourceType="dam/gui/coral/components/admin/customsearch/formbuilder/predicatefieldproperties/fieldlabelpropertyfield"/>

<sling:include resource="<%= resource %>" resourceType="granite/ui/components/foundation/form/formbuilder/formfieldproperties/titlefields"/>

<sling:include resource="<%= resource %>" resourceType="granite/ui/components/foundation/form/formbuilder/formfieldproperties/deletefields"/>


3) Create the predicate widget render html (fieldResourceType in step 1) /apps/eaem-search-form-nodename-predicate/nodename-predicate/nodename-predicate.jsp with following code

<%@include file="/libs/granite/ui/global.jsp" %>
<%@ page session="false" contentType="text/html" pageEncoding="utf-8"
import="com.adobe.granite.ui.components.Config"%>
<%@ page import="com.adobe.granite.ui.components.AttrBuilder" %>

<%
Config cfg = new Config(resource);
String name = cfg.get("text", i18n.get("Property"));
String metaType = cfg.get("metaType", "eaemignorecasenodename");

boolean foldableOpen = cfg.get("open", true);
String selected = foldableOpen ? "selected":"";

AttrBuilder inputAttrs = new AttrBuilder(request, xssAPI);
inputAttrs.add("type", "text");
inputAttrs.add("name", metaType);
inputAttrs.addClass("coral-Form-field coral-Textfield coral-DecoratedTextfield-input");
inputAttrs.add("placeholder", "Partial or full name, any case...");
%>
<coral-accordion variant="large">
<coral-accordion-item "<%=selected%>" data-metaType="checkboxgroup" data-type="eaemignorecasenodename">
<coral-accordion-item-label><%= xssAPI.encodeForHTML(name) %></coral-accordion-item-label>
<coral-accordion-item-content class="coral-Form coral-Form--vertical">
<input <%=inputAttrs.build()%>>
</coral-accordion-item-content>
</coral-accordion-item>
</coral-accordion>

4) Add the predicate evaluator apps.experienceaem.assets.IgnoreCaseNodeNamePredicateEvaluator for searching using partial file name, case ignored...

package apps.experienceaem.assets;

import com.day.cq.search.Predicate;
import com.day.cq.search.eval.AbstractPredicateEvaluator;
import com.day.cq.search.eval.EvaluationContext;
import com.day.cq.search.eval.JcrPropertyPredicateEvaluator;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Component;

@Component(
factory = "com.day.cq.search.eval.PredicateEvaluator/eaemignorecasenodename"
)
public class IgnoreCaseNodeNamePredicateEvaluator extends AbstractPredicateEvaluator {
public String getXPathExpression(Predicate predicate, EvaluationContext context) {
String value = predicate.get(predicate.getName());

if(StringUtils.isEmpty(value)){
return null;
}

return "jcr:like(fn:lower-case(fn:name()), '%" + value.toLowerCase() + "%')";
}
}


AEM 65 - Setup a Review Env for Preview before Publishing to Live

$
0
0

Goal


Business users with no access to AEM may want to review the page (content and layout) before authors activate the page to Publish (and make it Live). A typical review process may contains the following steps...

1) Replicate the page content to a Review environment

2) Preview the page in Review environment (pre production)

3) Approve the page (or provide feedback)

4) Activate the page to Publish

The following process provides a solution for the first two steps...


Demo | Package Install | Github


Publish to Review




Review in Progress - Card View



Review in Progress - List View



Review in Progress - Column View



Solution


1) Setup a publish environment behind the organization firewall and label it Review / Preview environment. Content is first pushed to this environment probably in a workflow process before replicating to Publish. Locally, for demo, its running on 4504

                         Author - http://localhost:4502
                         Publish - http://localhost:4503
                         Review - http://localhost:4504

2) Create a replication agent Publish to Review Agent(review_agent) to push content from Author to Review (Package Install has a sample replication agent /etc/replication/agents.author/review_agent)






3) Set the Transport URI, Transport Username and Password of the Review env in agent (adding admin user credentials is not recommended).

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:Page">
<jcr:content
cq:lastModified="{Date}2019-08-09T09:57:33.965-05:00"
cq:lastModifiedBy="admin"
cq:template="/libs/cq/replication/templates/agent"
jcr:lastModified="{Date}2019-08-09T09:57:33.957-05:00"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
jcr:title="Publish to Review Agent"
sling:resourceType="cq/replication/components/agent"
enabled="true"
transportPassword="\{40033b099a3b0d7e8f360c8623e446e1fd2171f5c621d72a3d30ba07cdc00793}"
transportUri="http://localhost:4504/bin/receive?sling:authRequestLogin=1"
transportUser="admin"
userId="admin"/>
</jcr:root>


4) Test, make sure the agent is enabled and reachable



5) To simplify the process add a button in action bar Publish to Review (like Quick Publish) to push page (and references) to Review with the following code, in node /apps/eaem-publish-page-to-review-env/content/publish-toreview-toolbar-ext

<?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"
granite:rel="cq-damadmin-admin-actions-publish-to-review-activator"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/collection/action"
activeSelectionCount="multiple"
icon="dimension"
target=".cq-damadmin-admin-childpages"
text="Publish to Review"
variant="actionBar">
<data
jcr:primaryType="nt:unstructured"
text="Page published to review environment"/>
</jcr:root>




6) In CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-publish-page-to-review-env

7) Create node /apps/eaem-publish-page-to-review-env/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.common.wcm, String[] property dependencies with value lodash

8) Create file (nt:file) /apps/eaem-publish-page-to-review-env/clientlib/js.txt, add

                        add-publish-to-review-action.js

9) Create file (nt:file) /apps/eaem-publish-page-to-review-env/clientlib/add-publish-to-review-action.js, add the following code

(function ($, $document) {
var BUTTON_URL = "/apps/eaem-publish-page-to-review-env/content/publish-toreview-toolbar-ext.html",
QUICK_PUBLISH_ACTIVATOR = "cq-siteadmin-admin-actions-quickpublish-activator",
REVIEW_STATUS_URL = "/bin/eaem/sites/review/status?parentPath=",
PUBLISH_TO_REVIEW = "/bin/eaem/sites/review/publish?pagePaths=",
F_CONTENT_LOADED = "foundation-contentloaded",
F_MODE_CHANGE = "foundation-mode-change",
F_SEL_CHANGE = "foundation-selections-change",
F_COL_ITEM_ID = "foundationCollectionItemId",
F_COL_ACTION = "foundationCollectionAction",
FOUNDATION_COLLECTION_ID = "foundation-collection-id",
LAYOUT_COL_VIEW = "column",
LAYOUT_LIST_VIEW = "list",
LAYOUT_CARD_VIEW = "card",
COLUMN_VIEW = "coral-columnview",
EVENT_COLUMNVIEW_CHANGE = "coral-columnview:change",
FOUNDATION_COLLECTION_ITEM = ".foundation-collection-item",
FOUNDATION_COLLECTION_ITEM_ID = "foundation-collection-item-id",
CORAL_COLUMNVIEW_PREVIEW = "coral-columnview-preview",
CORAL_COLUMNVIEW_PREVIEW_ASSET = "coral-columnview-preview-asset",
EAEM_BANNER_CLASS = "eaem-banner",
EAEM_BANNER = ".eaem-banner",
FOUNDATION_COLLECTION_ITEM_TITLE = ".foundation-collection-item-title",
SITE_ADMIN_CHILD_PAGES = ".cq-siteadmin-admin-childpages",
NEW_BANNER = "New",
colViewListenerAdded = false,
reviewButtonAdded = false;

$document.on(F_CONTENT_LOADED, removeNewBanner);

$document.on(F_CONTENT_LOADED, checkReviewStatus);

$document.on(F_SEL_CHANGE, function () {
if(reviewButtonAdded){
return;
}

reviewButtonAdded = true;

colViewListenerAdded = false;

checkReviewStatus();

$.ajax(BUTTON_URL).done(addButton);
});

function removeNewBanner(){
var $container = $(SITE_ADMIN_CHILD_PAGES), $label,
$items = $container.find(FOUNDATION_COLLECTION_ITEM);

_.each($items, function(item){
$label = $(item).find("coral-card-info coral-tag");

if(_.isEmpty($label) || $label.find("coral-tag-label").html().trim() != NEW_BANNER){
return;
}

$label.remove();
});
}

function checkReviewStatus(){
var parentPath = $(SITE_ADMIN_CHILD_PAGES).data(FOUNDATION_COLLECTION_ID);

if(_.isEmpty(parentPath)){
return;
}

$.ajax(REVIEW_STATUS_URL + parentPath).done(showBanners);
}

function showBanners(pathsObj){
if(isColumnView()){
handleColumnView();
}

if(_.isEmpty(pathsObj)){
return;
}

if(isCardView()){
addCardViewBanner(pathsObj);
}else if(isListView()){
addListViewBanner(pathsObj)
}
}

function handleColumnView(){
var $columnView = $(COLUMN_VIEW);

if(colViewListenerAdded){
return;
}

colViewListenerAdded = true;

$columnView.on(EVENT_COLUMNVIEW_CHANGE, handleColumnItemSelection);
}

function handleColumnItemSelection(event){
var detail = event.originalEvent.detail,
$page = $(detail.selection[0]),
pagePath = $page.data(FOUNDATION_COLLECTION_ITEM_ID);

if(_.isEmpty(pagePath)){
return;
}

$.ajax(REVIEW_STATUS_URL + pagePath).done(addColumnViewBanner);
}

function addColumnViewBanner(pageObj){
getUIWidget(CORAL_COLUMNVIEW_PREVIEW).then(handler);

function handler($colPreview){
var $pagePreview = $colPreview.find(CORAL_COLUMNVIEW_PREVIEW_ASSET),
pagePath = $colPreview.data("foundation-layout-columnview-columnid"),
state = pageObj[pagePath];

if(_.isEmpty(state)){
return;
}

$pagePreview.find(EAEM_BANNER).remove();

$pagePreview.prepend(getBannerColumnView(state));
}
}

function getBannerColumnView(state){
var ct = getColorText(state);

if(!ct.color){
return;
}

return "<coral-tag style='background-color: " + ct.color + ";z-index: 9999; width: 100%' class='" + EAEM_BANNER_CLASS + "'>" +
"<i class='coral-Icon coral-Icon--bell coral-Icon--sizeXS' style='margin-right: 10px'></i>" + ct.text +
"</coral-tag>";
}

function getBannerHtml(state){
var ct = getColorText(state);

if(!ct.color){
return;
}

return "<coral-alert style='background-color:" + ct.color + "' class='" + EAEM_BANNER_CLASS + "'>" +
"<coral-alert-content>" + ct.text + "</coral-alert-content>" +
"</coral-alert>";
}

function getColorText(state){
var color, text;

if(_.isEmpty(state)){
return
}

if(state == "IN_PROGRESS"){
color = "#ff7f7f";
text = "Review in progress"
}

return{
color: color,
text: text
}
}

function addListViewBanner(pathsObj){
var $container = $(SITE_ADMIN_CHILD_PAGES), $item, ct;

_.each(pathsObj, function(state, pagePath){
$item = $container.find("[data-" + FOUNDATION_COLLECTION_ITEM_ID + "='" + pagePath + "']");

if(!_.isEmpty($item.find(EAEM_BANNER))){
return;
}

ct = getColorText(state);

if(!ct.color){
return;
}

$item.find("td").css("background-color" , ct.color).addClass(EAEM_BANNER_CLASS);

$item.find(FOUNDATION_COLLECTION_ITEM_TITLE).prepend(getListViewBannerHtml());
});
}

function getListViewBannerHtml(){
return "<i class='coral-Icon coral-Icon--bell coral-Icon--sizeXS' style='margin-right: 10px'></i>";
}

function addCardViewBanner(pathsObj){
var $container = $(SITE_ADMIN_CHILD_PAGES), $item;

_.each(pathsObj, function(state, pagePath){
$item = $container.find("[data-" + FOUNDATION_COLLECTION_ITEM_ID + "='" + pagePath + "']");

if(_.isEmpty($item)){
return;
}

if(!_.isEmpty($item.find(EAEM_BANNER))){
return;
}

$item.find("coral-card-info").append(getBannerHtml(state));
});
}

function isColumnView(){
return ( getAssetsConsoleLayout() === LAYOUT_COL_VIEW );
}

function isListView(){
return ( getAssetsConsoleLayout() === LAYOUT_LIST_VIEW );
}

function isCardView(){
return (getAssetsConsoleLayout() === LAYOUT_CARD_VIEW);
}

function getAssetsConsoleLayout(){
var $childPage = $(SITE_ADMIN_CHILD_PAGES),
foundationLayout = $childPage.data("foundation-layout");

if(_.isEmpty(foundationLayout)){
return "";
}

return foundationLayout.layoutId;
}

function getUIWidget(selector){
if(_.isEmpty(selector)){
return;
}

var deferred = $.Deferred();

var INTERVAL = setInterval(function(){
var $widget = $(selector);

if(_.isEmpty($widget)){
return;
}

clearInterval(INTERVAL);

deferred.resolve($widget);
}, 250);

return deferred.promise();
}

function startsWith(val, start){
return val && start && (val.indexOf(start) === 0);
}

function addButton(html) {
var $eActivator = $("." + QUICK_PUBLISH_ACTIVATOR);

if ($eActivator.length == 0) {
return;
}

var $convert = $(html).css("margin-left", "20px").insertBefore($eActivator);

$convert.click(postPublishToReviewRequest);
}

function postPublishToReviewRequest(){
var actionConfig = ($(this)).data(F_COL_ACTION);

var $items = $(".foundation-selections-item"),
pagePaths = [];

$items.each(function () {
pagePaths.push($(this).data(F_COL_ITEM_ID));
});

$.ajax(PUBLISH_TO_REVIEW + pagePaths.join(",")).done(function(){
showAlert(actionConfig.data.text, "Publish");
});
}

function showAlert(message, title, callback){
var fui = $(window).adaptTo("foundation-ui"),
options = [{
id: "ok",
text: "OK",
primary: true
}];

message = message || "Unknown Error";
title = title || "Error";

fui.prompt(title, message, "default", options, callback);
}

}(jQuery, jQuery(document)));


10) Create a servlet apps.experienceaem.sites.PublishToReviewServlet to collect references in page and publish to Review using replication agent  review_agent


package apps.experienceaem.sites;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.replication.*;
import com.day.cq.wcm.api.reference.ReferenceProvider;
import org.apache.commons.lang3.StringUtils;
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.servlets.post.JSONResponse;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import com.day.cq.wcm.api.reference.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

@Component(
name = "Experience AEM Publish to Review Servlet",
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.methods=GET",
"sling.servlet.paths=/bin/eaem/sites/review/status",
"sling.servlet.paths=/bin/eaem/sites/review/publish"
}
)
public class PublishToReviewServlet extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(PublishToReviewServlet.class);

public static final String PUBLISH_TO_REVIEW_URL = "/bin/eaem/sites/review/publish";
public static final String STATUS_URL = "/bin/eaem/sites/review/status";

private static final String REVIEW_AGENT = "review_agent";
private static final String REVIEW_STATUS = "reviewStatus";
private static final String REVIEW_STATUS_IN_PROGRESS = "IN_PROGRESS";

@org.osgi.service.component.annotations.Reference
Replicator replicator;

@org.osgi.service.component.annotations.Reference(
service = ReferenceProvider.class,
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
private final List<ReferenceProvider> referenceProviders = new CopyOnWriteArrayList<ReferenceProvider>();

protected void bindReferenceProviders(ReferenceProvider referenceProvider) {
referenceProviders.add(referenceProvider);
}

protected void unbindReferenceProviders(ReferenceProvider referenceProvider) {
referenceProviders.remove(referenceProvider);
}

@Override
protected final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws
ServletException, IOException {
try {
addJSONHeaders(response);

if(PUBLISH_TO_REVIEW_URL.equals(request.getRequestPathInfo().getResourcePath())){
handlePublish(request, response);
}else{
handleStatus(request, response);
}
} catch (Exception e) {
log.error("Error processing publish to review...");
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}

private void handleStatus(SlingHttpServletRequest request, SlingHttpServletResponse response) throws Exception {
JSONObject jsonObject = new JSONObject();

String parentPath = request.getParameter("parentPath");

if(StringUtils.isEmpty(parentPath)){
jsonObject.put("error", "No parent path provided");
jsonObject.write(response.getWriter());
return;
}

ResourceResolver resolver = request.getResourceResolver();
Session session = resolver.adaptTo(Session.class);

if ((session != null) && session.isLive() && !session.nodeExists(parentPath)) {
log.debug("No such node {} ", parentPath);
return;
}

jsonObject = getReviewInProgressPages(resolver.getResource(parentPath));

jsonObject.write(response.getWriter());
}

private JSONObject getReviewInProgressPages(Resource resource) throws Exception{
final JSONObject pagePaths = new JSONObject();

final Iterator<Resource> childResItr = resource.listChildren();
Resource childRes, jcrContent;
Node jcrContentNode;

while (childResItr.hasNext()) {
childRes = childResItr.next();

if(childRes.getName().equals("jcr:content")){
jcrContent = childRes;
}else{
jcrContent = childRes.getChild("jcr:content");
}

if(jcrContent == null){
continue;
}

jcrContentNode = jcrContent.adaptTo(Node.class);

if (!jcrContentNode.hasProperty(REVIEW_STATUS)
|| !jcrContentNode.getProperty(REVIEW_STATUS).getString().equals(REVIEW_STATUS_IN_PROGRESS)) {
continue;
}

if(childRes.getName().equals("jcr:content")){
pagePaths.put(childRes.getParent().getPath(), REVIEW_STATUS_IN_PROGRESS);
}else{
pagePaths.put(childRes.getPath(), REVIEW_STATUS_IN_PROGRESS);
}
}

return pagePaths;
}


private void handlePublish(SlingHttpServletRequest request, SlingHttpServletResponse response) throws Exception {
JSONObject jsonResponse = new JSONObject();
List<String> publishPaths = new ArrayList<String>();

ResourceResolver resolver = request.getResourceResolver();
Session session = resolver.adaptTo(Session.class);
String pagePaths = request.getParameter("pagePaths");

for(String pagePath : pagePaths.split(",")){
Resource page = resolver.getResource(pagePath);
Resource jcrContent = resolver.getResource(page.getPath() + "/" + JcrConstants.JCR_CONTENT);

Set<Reference> allReferences = new TreeSet<Reference>(new Comparator<Reference>() {
public int compare(Reference o1, Reference o2) {
return o1.getResource().getPath().compareTo(o2.getResource().getPath());
}
});

for (ReferenceProvider referenceProvider : referenceProviders) {
allReferences.addAll(referenceProvider.findReferences(jcrContent));
}

for (Reference reference : allReferences) {
Resource resource = reference.getResource();

if (resource == null) {
continue;
}

boolean canReplicate = canReplicate(resource.getPath(), session);

if(!canReplicate){
log.warn("Skipping, No replicate permission on - " + resource.getPath());
continue;
}

if(shouldReplicate(reference)){
publishPaths.add(resource.getPath());
}
}

jcrContent.adaptTo(Node.class).setProperty(REVIEW_STATUS, REVIEW_STATUS_IN_PROGRESS);

publishPaths.add(pagePath);
}

session.save();

doReplicate(publishPaths, session);

jsonResponse.put("success", "true");

response.getWriter().write(jsonResponse.toString());
}

private static boolean canReplicate(String path, Session session) {
try {
AccessControlManager acMgr = session.getAccessControlManager();

return acMgr.hasPrivileges(path, new Privilege[]{
acMgr.privilegeFromName(Replicator.REPLICATE_PRIVILEGE)
});
} catch (RepositoryException e) {
return false;
}
}

private boolean shouldReplicate(Reference reference){
Resource resource = reference.getResource();
ReplicationStatus replStatus = resource.adaptTo(ReplicationStatus.class);

if (replStatus == null) {
return true;
}

boolean doReplicate = false, published = false, outdated = false;
long lastPublished = 0;

published = replStatus.isActivated();

if (published) {
lastPublished = replStatus.getLastPublished().getTimeInMillis();
outdated = lastPublished < reference.getLastModified();
}

if (!published || outdated) {
doReplicate = true;
}

return doReplicate;
}

private void doReplicate(List<String> paths, Session session) throws Exception{
ReplicationOptions opts = new ReplicationOptions();

opts.setFilter(new AgentFilter() {
public boolean isIncluded(com.day.cq.replication.Agent agent) {
return agent.getId().equalsIgnoreCase(REVIEW_AGENT);
}
});

for(String path : paths){
replicator.replicate(session, ReplicationActionType.ACTIVATE, path, opts);
}
}

public static void addJSONHeaders(SlingHttpServletResponse response){
response.setContentType(JSONResponse.RESPONSE_CONTENT_TYPE);
response.setHeader("Cache-Control", "nocache");
response.setCharacterEncoding("utf-8");
}
}

11) The necessary packages with components and templates are pre installed in Review (Package Install has sample template /apps/eaem-basic-htl-page-template)

12) Page in Review environment



AEM 65 - Assets Revert to a Version Binary and not Metadata (keeping it current)

$
0
0

Goal


AEM's Revert to this Version, reverts the asset as a whole (both binary and metadata) to selected version, however if you want the binary to be reverted to a specific version but keep metadata current, try the following approach...

Demo | Package Install | Github



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-revert-binary-not-metadata

2) Create node /apps/eaem-revert-binary-not-metadata/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.gui.coral.common.admin.timeline, String[] property dependencies with value lodash

3) Create file (nt:file) /apps/eaem-revert-binary-not-metadata/clientlib/js.txt, add

                        revert-binary-not-metadata.js

4) Create file (nt:file) /apps/eaem-revert-binary-not-metadata/clientlib/revert-binary-not-metadata.js, add the following code (logic injects a hidden field eaem-revert-toversion-file-only into the form posted and filter in next step reads it for taking a backup of metadata before its reverted to another version)

(function($, $document) {
var EAEM_REVERT_CSS = "eaem-revert-toversion-file-only",
REVERT_FILE_NOT_METADATA_TITLE = "Revert to this Version (File)",
REVERT_TO_VERSION_SEL = ".cq-common-admin-timeline-event-button";

$document.on("click", ".cq-common-admin-timeline-event", addRevertToThisVersionFile);

function addRevertToThisVersionFile(){
var $timeLineButton = $(this).find(REVERT_TO_VERSION_SEL);

if(!_.isEmpty($timeLineButton.next("." + EAEM_REVERT_CSS))){
return;
}

$(getButtonHtml()).insertAfter($timeLineButton).click(revertToVersion);
}

function getButtonHtml(){
return '<button is="coral-button" class="' + EAEM_REVERT_CSS + '" size="M" variant="secondary" style="width:100%; margin-top:.2rem">' +
'<coral-button-label>' + REVERT_FILE_NOT_METADATA_TITLE + '</coral-button-label>' +
'</button>'
}

function revertToVersion(event){
event.preventDefault();

var $form = $(this).closest("form"),
$otbRevert = $(this).prev(REVERT_TO_VERSION_SEL);

$form.append("<input type='hidden' name='" + EAEM_REVERT_CSS + "' value='true'/>");

$otbRevert.click();
}
})(jQuery, jQuery(document));


5) Add filter apps.experienceaem.assets.ExperienceAEMVersionFilter for taking backup of metadata before product's logic reverts both binary and metadata. Later, restore metadata from the backup, thereby keeping it current...

package apps.experienceaem.assets;

import com.day.cq.commons.jcr.JcrUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import javax.servlet.*;
import java.io.IOException;

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM Restore file only (not metadata) Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.selectors=version"
}
)
public class ExperienceAEMVersionFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(ExperienceAEMVersionFilter.class);

private static String EAEM_REVERT_PARAM = "eaem-revert-toversion-file-only";
private static String METADATA_BACKUP = "metadata-backup";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;

String assetPath = slingRequest.getParameter("path");

if(StringUtils.isEmpty(assetPath)){
chain.doFilter(slingRequest, response);
return;
}

String revertParam = slingRequest.getParameter(EAEM_REVERT_PARAM);

if(StringUtils.isEmpty(revertParam)){
chain.doFilter(slingRequest, response);
return;
}

ResourceResolver resolver = slingRequest.getResourceResolver();
Session session = resolver.adaptTo(Session.class);
Resource assetResource = resolver.getResource(assetPath);

if(assetResource == null){
chain.doFilter(slingRequest, response);
return;
}

String folderPath = assetPath.substring(0, assetPath.lastIndexOf("/"));
String backupName = assetResource.getName() + "-" + METADATA_BACKUP;
String backupPath = folderPath + "/" + backupName;

Resource metadata = assetResource.getChild("jcr:content/metadata");

if(metadata == null){
chain.doFilter(slingRequest, response);
return;
}

try{
if(session.itemExists(backupPath)){
session.removeItem(backupPath);
}

JcrUtil.copy(metadata.adaptTo(Node.class), resolver.getResource(folderPath).adaptTo(Node.class), backupName, true);

chain.doFilter(slingRequest, response);

String metadataPath = metadata.getPath();

session.removeItem(metadataPath);

session.move(backupPath, metadataPath);

session.save();
}catch(Exception e){
logger.error("Error taking metadata backup - " + assetPath , e);
}
}

@Override
public void destroy() {
}
}


AEM 6510 - Assets Bulk Update Editor Remove Tags (or values added using Coral 3 Autocomplete)

$
0
0

Goal


Assets Bulk Update Editor provides Append option for adding Multi-valued data (like cq:tags)

This post extends the editor and provides an option to remove previously selected values

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-bulk-update-remove-tags

2) Create node /apps/eaem-bulk-update-remove-tags/clientlib of type cq:ClientLibraryFolder, add String property categories with value dam.gui.coral.metadataeditor, String[] property dependencies with value lodash

3) Create file (nt:file) /apps/eaem-bulk-update-remove-tags/clientlib/js.txt, add

                        bulk-update-remove-tags.js

4) Create file (nt:file) /apps/eaem-bulk-update-remove-tags/clientlib/bulk-update-remove-tags.js, add the following code for extending autocomplete in metadata editor and provide the Remove selected tags checkbox (when checked sends the parameter suffixed with eaem-remove e.g. cq:tags-eaem-remove read by the sling post processor in step 5)

(function($, $document) {
var extended = false,
FOUNDATION_SELECTIONS_CHANGE = "foundation-selections-change",
FOUNDATION_CONTENT_LOADED = "foundation-contentloaded",
DAM_ADMIN_CHILD_PAGES_SEL = ".cq-damadmin-admin-childpages",
REMOVE_EAEM_CB_SUFFIX = "-eaem-remove";

$document.on(FOUNDATION_CONTENT_LOADED, addRemoveTags);

$document.on(FOUNDATION_SELECTIONS_CHANGE, DAM_ADMIN_CHILD_PAGES_SEL , showHideRemoveCheckbox);

function showHideRemoveCheckbox(event){
var $collection = $(event.target),
selectApi = $collection.adaptTo("foundation-selections"),
count = selectApi.count(),
$acFields = $("foundation-autocomplete"), $removeCB, $acField;

_.each($acFields, function(acField){
$acField = $(acField);

$removeCB = getRemoveCheckBox($acField);

if(count === 1){
$removeCB.attr("disabled", "disabled");
}else{
$removeCB.removeAttr("disabled").removeAttr("checked");
}
});
}

function addRemoveTags(){
if(extended){
return;
}

var $acFields = $("foundation-autocomplete"), $acField,
$removeCB, cbName;

if(_.isEmpty($acFields)){
return;
}

_.each($acFields, function(acField){
$acField = $(acField);

cbName = $acField.attr("name") + REMOVE_EAEM_CB_SUFFIX;

if(!_.isEmpty(getRemoveCheckBox($acField))){
return;
}

$removeCB = $(getRemoveHtml(cbName)).insertBefore($acField);
});
}

function getRemoveCheckBox($acField){
var cbName = $acField.attr("name") + REMOVE_EAEM_CB_SUFFIX;

return $acField.prev("[name='" + cbName + "']");
}

function getRemoveHtml(cbName){
return '<coral-checkbox class="coral-Form-field" name="' + cbName + '" value="true">Remove selected tags</coral-checkbox>';
}
})(jQuery, jQuery(document));


5) Add a Sling Post Processor apps.experienceaem.assets.EAEMBulkUpdateRemovePostProcessor for reading eaem-remove parameters and remove selected values

package apps.experienceaem.assets;

import org.apache.commons.lang.ArrayUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.servlets.post.Modification;
import org.apache.sling.servlets.post.SlingPostProcessor;
import org.json.JSONObject;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;

import javax.jcr.*;
import java.util.*;

@Component(
immediate = true,
service = { SlingPostProcessor.class },
property = {
Constants.SERVICE_RANKING + ":Integer=-99"
}
)
public class EAEMBulkUpdateRemovePostProcessor implements SlingPostProcessor {
private static final String MODE = "mode";
private static final String MODE_SOFT = "soft";
private static final String BULK_UPDATE = "dam:bulkUpdate";
private static final String EAEM_REMOVE_SUFFIX = "-eaem-remove";

public void process(SlingHttpServletRequest request, List<Modification> modifications) throws Exception {
ResourceResolver resolver = request.getResourceResolver();
String reqType = request.getParameter(BULK_UPDATE);
String reqMode = request.getParameter(MODE);

if ((reqType == null) || (reqMode == null) || !reqMode.equals(MODE_SOFT)) {
return;
}

Session session = resolver.adaptTo(Session.class);
RequestParameter[] assets = request.getRequestParameters("asset");

if ( (session == null) || (assets == null)) {
return;
}

session.refresh(true);

Map<String, String[]> removalMap = getValuesForRemoval(request);

if(removalMap.isEmpty()){
return;
}

for (RequestParameter asset : assets) {
JSONObject assetJson = new JSONObject(asset.toString());

processChanges(session, assetJson, removalMap);
}

session.save();
}

private Map<String, String[]> getValuesForRemoval(SlingHttpServletRequest request){
Map<String, String[]> removalMap = new HashMap<String, String[]>();

Map<String, String[]> params = request.getParameterMap();
String removeKey = null;
String[] removeValues = null;

for(String param : params.keySet()){
if(!param.endsWith(EAEM_REMOVE_SUFFIX)) {
continue;
}

removeKey = param.substring(0, param.lastIndexOf(EAEM_REMOVE_SUFFIX));

removeValues = params.get(removeKey);

if(removeValues == null){
continue;
}

removalMap.put(removeKey.substring(removeKey.lastIndexOf("/") + 1), removeValues);
}

return removalMap;
}

private void processChanges(Session session, JSONObject assetJson, Map<String, String[]> removalMap)
throws Exception{
String assetPath = assetJson.getString("path");
String metadataPath = assetPath + "/jcr:content/metadata";

Node metadataNode = session.getNode(metadataPath);
String removePropertyName;
Property property;

for(String removeKey : removalMap.keySet()){
if(!metadataNode.hasProperty(removeKey)){
continue;
}

property = metadataNode.getProperty(removeKey);

if(!property.isMultiple()){
continue;
}

Value values[] = getNewValues(property.getValues(), Arrays.asList(removalMap.get(removeKey)));

if(ArrayUtils.isEmpty(values)){
property.remove();
}else{
metadataNode.setProperty(removeKey, values);
}

removePropertyName = removeKey + EAEM_REMOVE_SUFFIX;

if(metadataNode.hasProperty(removePropertyName)){
metadataNode.getProperty(removePropertyName).remove();
}
}
}

private Value[] getNewValues(Value[] oldValues, List<String> removeValues) throws Exception{
List<Value> newValues = new ArrayList<Value>();

for(Value value : oldValues){
if(removeValues.contains(value.getString())){
continue;
}

newValues.add(value);
}

return newValues.toArray(new Value[newValues.size()]);
}
}

AEM 6510 - Sample Sling Scheduler

$
0
0

Goal


Create a sample Sling Scheduler

Package Install | Github

                          http://localhost:4502/system/console/configMgr/apps.experienceaem.assets.SampleScheduler




Solution


1) Create Scheduler configuration apps.experienceaem.assets.SampleSchedulerConfiguration

package apps.experienceaem.assets;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(
name = "Experience AEM: SlingSchedulerConfiguration",
description = "Sling scheduler configuration"
)
public @interface SampleSchedulerConfiguration {

@AttributeDefinition(
name = "Scheduler name",
description = "Name of the scheduler",
type = AttributeType.STRING)
public String schedulerName() default "Custom Sling Scheduler Configuration";

@AttributeDefinition(
name = "Enabled",
description = "True, if scheduler service is enabled",
type = AttributeType.BOOLEAN)
public boolean enabled() default false;

@AttributeDefinition(
name = "Cron Expression",
description = "Cron expression used by the scheduler",
type = AttributeType.STRING)
public String cronExpression() default "0/10 * * * * ?"; // runs every 10 seconds

@AttributeDefinition(
name = "Custom Path Parameter",
description = "Custom path parameter to be used by the scheduler",
type = AttributeType.STRING)
public String customPathParameter() default "/content/dam";
}


2) Create Scheduler class apps.experienceaem.assets.SampleScheduler implementing Runnable.class

package apps.experienceaem.assets;

import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.apache.sling.commons.scheduler.Scheduler;
import org.osgi.service.component.annotations.*;
import org.osgi.service.metatype.annotations.Designate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(immediate = true, service = Runnable.class)
@Designate(ocd = SampleSchedulerConfiguration.class)
public class SampleScheduler implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(SampleScheduler.class);

private String customPathParameter;
private int schedulerId;

@Reference
private Scheduler scheduler;

@Activate
protected void activate(SampleSchedulerConfiguration config) {
schedulerId = config.schedulerName().hashCode();
customPathParameter = config.customPathParameter();

addScheduler(config);
}

@Deactivate
protected void deactivate(SampleSchedulerConfiguration config) {
removeScheduler();
}

@Modified
protected void modified(SampleSchedulerConfiguration config) {
removeScheduler();

schedulerId = config.schedulerName().hashCode();

addScheduler(config);
}


private void removeScheduler() {
scheduler.unschedule(String.valueOf(schedulerId));
}

/**
* This method adds the scheduler
*
* @param config
*/
private void addScheduler(SampleSchedulerConfiguration config) {
if(config.enabled()) {
ScheduleOptions scheduleOptions = scheduler.EXPR(config.cronExpression());
scheduleOptions.name(config.schedulerName());
scheduleOptions.canRunConcurrently(false);

scheduler.schedule(this, scheduleOptions);
LOGGER.info("Experience AEM Scheduler added");
} else {
LOGGER.info("Experience AEM Scheduler disabled");
}
}

public void run() {
LOGGER.info("Experience AEM, customPathParameter {}", customPathParameter);
}
}


AEM 6510 - Add Photo Gallery Composite Multifield in Content Fragments

$
0
0

Goal


Create a Content Fragment Model with Multifield of Content References, show Preview and add field for Caption (making it somewhat Composite Multifield)

Demo | Package Install | Github


Photo Gallery Configuration



Configuration in CRX



Photo Gallery in Fragment



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-cf-photo-gallery-component

2) Create node /apps/eaem-cf-photo-gallery-component/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.cfm.admin.models.formbuilder, dam.cfm.authoring.contenteditor.v2], String[] property dependencies with value lodashcq.cfm.admin.models.formbuilder for Model Editor extension and dam.cfm.authoring.contenteditor.v2 for Fragment Editor extension

3) Create file (nt:file) /apps/eaem-cf-photo-gallery-component/clientlib/js.txt, add

                        photo-gallery-mf.js

4) Create file (nt:file) /apps/eaem-cf-photo-gallery-component/clientlib/photo-gallery-mf.js, add the following code

(function($, $document) {
var CF_MODEL_URL = "/mnt/overlay/dam/cfm/models/editor/content/editor.html",
MF_RES_TYPE = "granite/ui/components/coral/foundation/form/multifield",
EAEM_SUB_TYPE_PHOTO_GALLERY = "EAEM_PHOTO_GALLERY",
EAEM_SUB_TYPE_PHOTO_GALLERY_LABEL = "Experience AEM Photo Gallery",
EAEM_SUB_TYPE_CB_SUFFIX = "EaemSubType";

if(isModelEditor()){
extendModelEditor();
}

function extendModelEditor(){
$document.on("click", "#form-fields > li", addSubTypeCheckBox);

$document.on("change", "coral-select", handleCoralSelectInModel);
}

function handleCoralSelectInModel(){
var $select = $(this);

if(this.value != MF_RES_TYPE){
getPhotoGalleryCheckBox($select).remove();
return;
}

var cbName = $select.attr("name") + EAEM_SUB_TYPE_CB_SUFFIX;

$(getSubtypeHtml(cbName)).appendTo($select.closest(".coral-Form-fieldwrapper"));
}

function addSubTypeCheckBox(){
var $fieldProperties = $("#" + $(this).data("id") + "-properties"),
$multiFieldsConfig = $fieldProperties.find("input[value='" + MF_RES_TYPE + "']"),
$mField, cbName, $cb, url;

_.each($multiFieldsConfig, function(mField){
$mField = $(mField);

cbName = $mField.attr("name") + EAEM_SUB_TYPE_CB_SUFFIX;

if(!_.isEmpty(getPhotoGalleryCheckBox($mField))){
return;
}

$cb = $(getSubtypeHtml(cbName)).appendTo($mField.closest(".coral-Form-fieldwrapper"));

url = $mField.closest("form").attr("action") + "/jcr:content/model/cq:dialog/"
+ mField.name.substring(0, mField.name.lastIndexOf("/")) + ".json";

enablePhotoGalleryCheckBox(cbName, $cb, url);
});
}

function enablePhotoGalleryCheckBox(cbName, $cb, url){
$.ajax({url: url, async: false}).done(function (data) {
if (_.isEmpty(data[cbName.substring(cbName.lastIndexOf("/") + 1)])) {
return;
}

$cb[0].checked = true;
});
}

function getPhotoGalleryCheckBox($mField){
var cbName = $mField.attr("name") + EAEM_SUB_TYPE_CB_SUFFIX;

return $mField.closest(".coral-Form-fieldwrapper").find("[name='" + cbName + "']");
}

function getSubtypeHtml(cbName){
return '<coral-checkbox class="coral-Form-field" name="' + cbName + '" value="' + EAEM_SUB_TYPE_PHOTO_GALLERY + '">' +
EAEM_SUB_TYPE_PHOTO_GALLERY_LABEL +
'</coral-checkbox>';
}

function isModelEditor(){
return window.location.pathname.startsWith(CF_MODEL_URL);
}
})(jQuery, jQuery(document));

(function($, $document) {
var CF_MODEL_URL = "/mnt/overlay/dam/cfm/models/editor/content/editor.html",
EAEM_SUB_TYPE_PHOTO_GALLERY = "EAEM_PHOTO_GALLERY",
MASTER = "master",
CFM_EDITOR_SEL = ".content-fragment-editor",
MF_SELECTOR = "coral-multifield",
EAEM_CARD_CAPTION = "eaem-card-caption",
EAEM_SUB_TYPE_CB_SUFFIX = "EaemSubType",
EAEM_CAPTION = "eaem-caption",
photoGalleryAutocompletes = {},
CF_MODEL = "";

if(!isModelEditor()){
window.Dam.CFM.Core.registerReadyHandler(extendFragmentEditor);
}

function extendFragmentEditor(){
extendAutoCompletes();

extendRequestSave();
}

function extendAutoCompletes(){
loadModelUrl();

$(MF_SELECTOR).on("coral-collection:add", function(event){
Coral.commons.ready(event.detail.item, addImageCard);
});

$(MF_SELECTOR).on("coral-collection:remove", removeImageCard);

$(MF_SELECTOR).each(function(index, mField){
Coral.commons.ready(mField, loadPhotoGalleryImages);
})
}

function loadPhotoGalleryImages(mField){
var $multiField = $(mField);

if(!isPhotoGalleryEnabled($multiField.attr("data-granite-coral-multifield-name"))){
return;
}

_.each($multiField[0].items.getAll(), function(item) {
var $content = $(item.content),
$imageReference = $content.find("foundation-autocomplete");

var mfData = JSON.parse($imageReference.val());

$imageReference.val(mfData.path);

showImage.call($imageReference[0]);

$content.find("input[name='" + EAEM_CAPTION + "']").val(mfData.caption);
});
}

function addImageCard(mfItem){
var $imageReference = $(mfItem).find("foundation-autocomplete");

if(_.isEmpty($imageReference)){
return;
}

if(!isPhotoGalleryEnabled($imageReference.attr("name"))){
return;
}

if(!_.isEmpty($imageReference.val())){
showImage.call($imageReference[0]);
}else{
$(getImageLabel()).insertBefore($imageReference);
$imageReference.on("change", showImage);
}
}

function isPhotoGalleryEnabled(mfName){
return photoGalleryAutocompletes[mfName];
}

function showImage(){
var $imageReference = $(this),
imageUrl = this.value,
$mfContent = $imageReference.closest("coral-multifield-item-content"),
$imageCaption = $mfContent.find("." + EAEM_CARD_CAPTION);

if(_.isEmpty(this.value)){
$imageCaption.remove();
return;
}

var fileName = imageUrl.substring(imageUrl.lastIndexOf("/") + 1);

imageUrl = imageUrl + "/_jcr_content/renditions/cq5dam.thumbnail.319.319.png";

if(!_.isEmpty($imageCaption)){
$imageCaption.find("img", imageUrl);
return;
}

$(getCardContent(imageUrl, fileName)).appendTo($mfContent);

$mfContent.css("margin-bottom", "20px");
}

function removeImageCard(event){
var $mfItem = $(event.detail.item);

$mfItem.find("." + EAEM_CARD_CAPTION).remove();
}

function loadModelUrl(){
$.ajax({url: window.Dam.CFM.EditSession.fragment.urlBase + "/jcr:content/data.json", async: false}).done(function (data) {
CF_MODEL = data["cq:model"];
});

$.ajax({url: CF_MODEL + "/jcr:content/model/cq:dialog/content/items.1.json", async: false}).done(handler);

function handler(data){
var subType;

_.each(data, function(value){
if(!_.isObject(value)){
return;
}

subType = value["sling:resourceType" + EAEM_SUB_TYPE_CB_SUFFIX];

photoGalleryAutocompletes[value["name"]] = (subType === EAEM_SUB_TYPE_PHOTO_GALLERY);
})
}
}

function getCardContent(imageUrl, fileName){
return '<div class="' + EAEM_CARD_CAPTION + '">' +
'<div>' +
'<coral-card fixedwidth assetwidth="200" assetheight="200">' +
'<coral-card-asset><img src="' + imageUrl + '"></coral-card-asset>' +
'<coral-card-content>' +
'<coral-card-title>' + fileName + '</coral-card-title>' +
'</coral-card-content>' +
'</coral-card>' +
'</div>' +
'<div>' +
'<label class="coral-Form-fieldlabel"> Caption</label>' +
'<input is="coral-textfield" class="coral-Form-field" name="' + EAEM_CAPTION + '" placeholder="Enter caption">' +
'</div>' +
'</div>'

}

function getImageLabel(){
return '<label class="coral-Form-fieldlabel"> Image</label>';
}

function getPhotoGalleryData(){
var mfData = {}, values, $fields;

_.each(photoGalleryAutocompletes, function(isPhotoGallery, refName){
if(!isPhotoGallery){
return;
}

var $multiField = $("coral-multifield[data-granite-coral-multifield-name='" + refName + "']");

if(_.isEmpty($multiField)){
return;
}

mfData[refName] = [];

_.each($multiField[0].items.getAll(), function(item) {
var $content = $(item.content), data = {};

data["path"] = $content.find("input[name='" + refName + "']").val();
data["caption"] = $content.find("input[name='" + EAEM_CAPTION + "']").val();

mfData[refName].push(JSON.stringify(data));
});
});

return mfData;
}

function extendRequestSave(){
var CFM = window.Dam.CFM,
orignFn = CFM.editor.Page.requestSave;

CFM.editor.Page.requestSave = requestSave;

function requestSave(callback, options) {
orignFn.call(this, callback, options);

var mfsData = getPhotoGalleryData();

if(_.isEmpty(mfsData)){
return;
}

var url = CFM.EditSession.fragment.urlBase + ".cfm.content.json",
variation = getVariation(),
createNewVersion = (options && !!options.newVersion) || false;

var data = {
":type": "multiple",
":newVersion": createNewVersion,
"_charset_": "utf-8"
};

if(variation !== MASTER){
data[":variation"] = variation;
}

var request = {
url: url,
method: "post",
dataType: "json",
data: _.merge(data, mfsData),
cache: false
};

CFM.RequestManager.schedule({
request: request,
type: CFM.RequestManager.REQ_BLOCKING,
condition: CFM.RequestManager.COND_EDITSESSION,
ui: (options && options.ui)
})
}
}

function getVariation(){
var variation = $(CFM_EDITOR_SEL).data('variation');

variation = variation || "master";

return variation;
}

function isModelEditor(){
return window.location.pathname.startsWith(CF_MODEL_URL);
}
})(jQuery, jQuery(document));


AEM 6510 - Multiselect Assets in Autocomplete Picker if Auto Complete exists in Multifield

$
0
0

Goal


In the Picker of Auto Complete granite/ui/components/coral/foundation/form/pathfield add feature to browse and select multiple assets (selecting multiple assets using search was not implemented). In the following demo, multifield (with autocomplete) was added in a content fragment model

Demo | Package Install | Github


Select Multiple in Picker



Assets Selected


Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-multiselect-assets-in-picker

2) Create node /apps/eaem-multiselect-assets-in-picker/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [granite.ui.coral.foundation, String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-multiselect-assets-in-picker/clientlib/js.txt, add

                        multiselect-assets.js

4) Create file (nt:file) /apps/eaem-multiselect-assets-in-picker/clientlib/multiselect-assets.js, add the following code

(function($, $document) {
var MF_SELECTOR = "coral-multifield",
CORAL_MF_ITEM = "coral-multifield-item",
EAEM_ITEM_VALUE = "data-eaem-item-value",
SELECTED_TAGS_DIV = "eaem-column-view-selections",
FOUNDATION_SELECTIONS_CHANGE = "foundation-selections-change",
FOUNDATION_SELECTIONS_ITEM = "foundation-selections-item",
FOUNDATION_COLLECTION = ".foundation-collection",
FOUNDATION_COLLECTION_ITEM_ID = "foundation-collection-item-id",
$assetsContainer,
extended = false;

$document.on("foundation-contentloaded", handleAssetPicker);

function handleAssetPicker(){
if(extended){
return;
}

var $autoCompletes = $("foundation-autocomplete");

if(_.isEmpty($autoCompletes)){
return;
}

extended = true;

_.each($autoCompletes, function(autoCompete){
var $autoComplete = $(autoCompete),
acName = $autoComplete.attr("name"),
$nearestMF = $autoComplete.closest("coral-multifield[data-granite-coral-multifield-name='" + acName + "']");

if(_.isEmpty($nearestMF)){
return;
}

$nearestMF.on("coral-collection:add", function(event){
Coral.commons.ready(event.detail.item, function(mfItem){
var $autoComplete = $(mfItem).find("foundation-autocomplete");

if(_.isEmpty($autoComplete)){
return;
}

extendPicker($autoComplete[0]);
});
});

extendPicker($autoComplete[0]);
});
}

function extendPicker(pathField){
var origShowPicker = pathField._showPicker,
origSetSelections = pathField._setSelections;

pathField._showPicker = function(){
origShowPicker.call(this);

var $dialog = $(this._picker.el),
$columnView = $dialog.find("coral-columnview");

addSelectedSection($columnView);

$dialog.on(FOUNDATION_SELECTIONS_CHANGE, FOUNDATION_COLLECTION, collectAssets);
};

pathField._setSelections = function(selections, deferChangeEvent){
var $autoComplete = $(this),
acName = $autoComplete.attr("name"),
$assets = $assetsContainer.find("coral-tag"),
$nearestMF = $autoComplete.closest("coral-multifield[data-granite-coral-multifield-name='" + acName + "']"),
mfAddEle = $nearestMF[0].querySelector("[coral-multifield-add]"),
existingValues = [], thisNotFilled = true;

_.each($nearestMF[0].items.getAll(), function(item) {
existingValues.push($(item.content).find("foundation-autocomplete").val());
});

_.each($assets, function(asset){
if(existingValues.includes(asset.value)){
return;
}

if(thisNotFilled){
$autoComplete[0].value = asset.value;
thisNotFilled = false;
return;
}

mfAddEle.click();

var $lastItem = $nearestMF.find(CORAL_MF_ITEM).last();

$lastItem.attr(EAEM_ITEM_VALUE, asset.value);

Coral.commons.ready($lastItem[0], setMultifieldItem);
});
};

function setMultifieldItem(lastItem){
var $imageReference = $(lastItem).find("foundation-autocomplete"),
assetPath = $(lastItem).attr(EAEM_ITEM_VALUE);

if(_.isEmpty($imageReference)){
return;
}

$imageReference.val(assetPath);
}
}

function collectAssets(){
var selectedAssets = {};

$("." + FOUNDATION_SELECTIONS_ITEM).each(function(index, asset){
var $asset = $(asset),
assetPath = $asset.data(FOUNDATION_COLLECTION_ITEM_ID);

if(_.isEmpty(assetPath)){
return;
}

selectedAssets[assetPath] = $asset.find(".foundation-collection-item-title").html();
});

var $submit = $(this).closest("coral-dialog").find(".granite-pickerdialog-submit");

if(_.isEmpty(selectedAssets)){
$submit.prop("disabled", _.isEmpty(getSelectedAssetsInContainer()));
return;
}

buildSelectedContainer(selectedAssets, $assetsContainer);

$(this).adaptTo("foundation-selections").clear();

$submit.prop("disabled", false);
}

function buildSelectedContainer(selectedAssets, $container) {
var $tagList = $container.find("coral-taglist");

_.each(selectedAssets, function (text, value) {
$tagList.append(getAssetHtml(text, value));
});
}

function getAssetHtml(title, value){
return '<coral-tag class="coral3-Tag" value="' + value + '">' +
'<coral-tag-label>' + title + '</coral-tag-label>' +
'</coral-tag>';
}

function addSelectedSection($columnView){
$columnView.css("height", "70%");

$assetsContainer = $("<div/>").appendTo($columnView.parent());

var html = "<div style='text-align:center; padding:1px; background-color: rgba(0,0,0,0.05)'>" +
"<h3>Selected Assets</h3>" +
"</div>" +
"<div style='margin: 15px' id='" + SELECTED_TAGS_DIV + "'>" +
"<coral-taglist class='coral3-TagList'></coral-taglist>" +
"</div>";

$(html).appendTo($assetsContainer);

$assetsContainer.find("coral-taglist").on("change", function(){
$assetsContainer.closest("coral-dialog").find(".granite-pickerdialog-submit").prop("disabled", _.isEmpty(this.values));
})
}

function getSelectedAssetsInContainer(){
var $assetList = $assetsContainer.find("coral-taglist");

if(_.isEmpty($assetList)){
return [];
}

return $assetList[0].values;
}
})(jQuery, jQuery(document));

Viewing all 513 articles
Browse latest View live




Latest Images