AEM Cloud Service - Assets Metadata Editor set Tag Fields Required

Goal

AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)

Add support for Required in Assets Metadata Editor for fields using the Tags widget. Product out of the box does not support required attribute for Tag fields in the schema builder

Demo | Package Install | Github


Tags Required Error


Schema Extension for Required


Solution

1) Add a service user eaem-service-user in repo init script ui.config\src\main\content\jcr_root\apps\eaem-meta-tags-required\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config

scripts=[
        "
        create service user eaem-service-user with path system/cq:services/experience-aem
        set principal ACL for eaem-service-user
                allow jcr:all on /conf
        end
        "
]


2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-meta-tags-required\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
        xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
        xmlns:jcr="http://www.jcp.org/jcr/1.0"
        jcr:primaryType="sling:OsgiConfig"
        user.mapping="[eaem-meta-tags-required.core:eaem-service-user=[eaem-service-user]]"/>


3) Add a client library /apps/eaem-meta-tags-required/clientlibs/metadata-tags-required/tags-required-config with categories=dam.admin.ui.coral.schemaeditor.formbuilder.v2 to extend the Metadata Schema Form Builder and add configuration for Tags Required

(function($, $document) {
    var TAG_FIELD_RES_TYPE = "cq/gui/components/coral/common/form/tagfield",
        RULES_PANEL = "#field-rules",
        REQUIRED_CASCADING = "/granite:data/requiredCascading",
        F_CONTENT_PATH = "foundation-content-path",
        REQUIRED_CHECKBOX_CSS = "eaem-dam-required";

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

    function init(){
        $document.on("click", ".form-fields > li", function(e) {
            e.stopPropagation();
            e.preventDefault();

            addTagsRequiredConfig(this);
        });
    }

    function addTagsRequiredConfig(field){
        var $tagsCheck = $(field).find("[value='" + TAG_FIELD_RES_TYPE + "']"),
            $rulesPanel = $(RULES_PANEL);

        if(_.isEmpty($tagsCheck) || !_.isEmpty($rulesPanel.find("." + REQUIRED_CHECKBOX_CSS))){
            return;
        }

        var $tagsReadonlyConfig = $(field).find("coral-checkbox[name$='/readOnly']");

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

        var configName = $tagsReadonlyConfig.attr("name"),
            reqConfigName = configName.substring(0, configName.lastIndexOf("/")) + REQUIRED_CASCADING,
            nodeName = configName.substring(0, configName.lastIndexOf("/"));

        $tagsReadonlyConfig = $rulesPanel.find("coral-checkbox[name='" + configName + "']");

        nodeName = nodeName.substring(nodeName.lastIndexOf("/") + 1);

        $(getRequiredCheckbox(reqConfigName, isRequiredSet(nodeName))).insertAfter($tagsReadonlyConfig);
    }

    function isRequiredSet(nodeName){
        var schemaPath = $("." + F_CONTENT_PATH).data(F_CONTENT_PATH),
            isRequired = false;

        if(!schemaPath){
            return isRequired;
        }

        schemaPath = "/bin/querybuilder.json?p.hits=full&p.nodedepth=2&path=" + schemaPath + "&nodename=" + nodeName;

        $.ajax({url : schemaPath, async: false}).done(function(data){
            if(!data || _.isEmpty(data.hits)){
                return;
            }

            isRequired = (data.hits[0]["granite:data"]["requiredCascading"] == "always");
        });

        return isRequired;
    }

    function getRequiredCheckbox(configName, checked){
        return  '<coral-checkbox class="coral-Form-field ' + REQUIRED_CHECKBOX_CSS + '" name="'
                    + configName + '" ' + (checked ? 'checked' : ' ') + ' value="always">Required</coral-checkbox>'
                + '<input type="hidden" name="' + configName + '@Delete" value="true">';
    }
}(jQuery, jQuery(document)));


4) Form Builder converts the Required checkbox requiredCascading value to true from the set value always during save. To set the value back to always add a metadata schema listener apps.experienceaem.assets.core.listeners.MetadataSchemaListener

package apps.experienceaem.assets.core.listeners;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.util.HashMap;
import java.util.Map;

@Component(service = EventHandler.class,
        immediate = true,
        property = {
                EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/ADDED",
                EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/CHANGED",
                EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/REMOVED",
                EventConstants.EVENT_FILTER + "=" + "(path=/conf/global/settings/dam/adminui-extension/metadataschema/experience-aem/*)"
        }
)
public class MetadataSchemaListener implements EventHandler {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private static final String EAEM_SERVICE_USER = "eaem-service-user";
    private static final String TAG_FIELD_RES_TYPE = "cq/gui/components/coral/common/form/tagfield";
    private static final String REQUIRED_CASCADING = "requiredCascading";

    @Reference
    private ResourceResolverFactory factory;
    @Reference
    private QueryBuilder builder;

    /**
     * Event handler
     *
     * @param event
     */
    public void handleEvent(final Event event) {
        logger.debug("Resource event: {} at: {}", event.getTopic(), event.getProperty(SlingConstants.PROPERTY_PATH));
        ResourceResolver resourceResolver = getServiceResourceResolver(factory);
        try {
            Query query = builder.createQuery(PredicateGroup.create(getQueryPredicateMap()), resourceResolver.adaptTo(Session.class));
            SearchResult result = query.getResult();
            ValueMap resVM = null;

            for (Hit hit : result.getHits()) {
                Resource res = resourceResolver.getResource(hit.getPath());

                if(res == null){
                    continue;
                }

                resVM = res.getValueMap();

                if (TAG_FIELD_RES_TYPE.equals(resVM.get("resourceType", String.class))){
                    setRequiredCascading(res);
                }
            }
            if (resourceResolver.hasChanges()) {
                resourceResolver.commit();
            }
        } catch (RepositoryException | PersistenceException e) {
            logger.error("Exception occured at handleEvent() , reason {}", e.getMessage(), e);
        }
    }

    private void setRequiredCascading(Resource referencesRes) {
        ModifiableValueMap mvm = referencesRes.getChild("granite:data").adaptTo(ModifiableValueMap.class);

        if("true".equals(mvm.get(REQUIRED_CASCADING, String.class))){
            mvm.put(REQUIRED_CASCADING, "always");
        }
    }

    private Map<String, String> getQueryPredicateMap() {
        Map<String, String> map = new HashMap<>();
        map.put("path", "/conf/global/settings/dam/adminui-extension/metadataschema/experience-aem");
        map.put("property", "resourceType");
        map.put("property.1_value", "cq/gui/components/coral/common/form/tagfield");
        return map;
    }

    public ResourceResolver getServiceResourceResolver(ResourceResolverFactory resourceResolverFactory) {
        Map<String, Object> subServiceUser = new HashMap<>();
        subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER);
        try {
            return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
        } catch (LoginException ex) {
            logger.error("Could not login as SubService user {}, exiting SearchService service.", EAEM_SERVICE_USER, ex);
            return null;
        }
    }
}


5) Metadata editor has support for Required attribute, to trigger it on page load, create a client library /apps/eaem-meta-tags-required/clientlibs/metadata-tags-required/tags-required-in-editor with categories=dam.gui.coral.metadataeditor and add the following code

(function($, $document) {
    $document.on("foundation-contentloaded", validateOnLoad);

    function validateOnLoad(){
        var $tagFields = $("foundation-autocomplete[required='true']");

        _.each($tagFields, function(tagField){
            var validation = $(tagField).adaptTo("foundation-validation");

            validation.checkValidity();

            validation.updateUI();
        })
    }
}(jQuery, jQuery(document)));

No comments:

Post a Comment