Edit metadata of multiple documents

check what fields to edit

A customer of ours had a need to change properties to the same value for a bulk of documents at once. So we created an action in the document library where you could select a bunch of documents of the same content type, and then choose “edit multiple” from the actions menu. You are then presented with the gui shown to the left. Tick the checkbox next to the field to enable it for bulk edit. When hitting the save button all the documents selected are updated with the values at once.

First we need to indicate the document type in the document library listing, otherwise it will be hard for the user to know which of the documents that share the same metadata, and therefore could be subject to a bulk change. This is done by registering a new renderer in javascript:

(function () {
    if (Alfresco.DocumentList) {
        YAHOO.Bubbling.fire("registerRenderer", {
            propertyName: "type",
            renderer: function (record, label) {
                var key = 'type.' + record.jsNode.type.replace(":", "_");

                return '<span class="item">' + Alfresco.util.message('label.nodeType') +
                       ': ' + Alfresco.util.message(key) + '</span>';

Include the javascript file in a DocLibCustom directive:

<config evaluator="string-compare" condition="DocLibCustom">
    <js src="components/documentlibrary/documentlist-display-type.js" />

The result will be something like this (with our customisation highlighted):


Configure our new bulk action

<config evaluator="string-compare" condition="DocumentLibrary">
  <action type="action-link" id="onActionEditMultipleDocumentMetadata" asset="document" permission="Write" label="actions.editMultipleDocumentMetadata" />
window.RPLP = window.RPLP || {};
window.RPLP.globalNodeRefs = [];
(function () {
     * YUI Library aliases
    var Dom = YAHOO.util.Dom,
        Event = YAHOO.util.Event;

     * Alfresco Slingshot aliases
    var $html = Alfresco.util.encodeHTML,
        $combine = Alfresco.util.combinePaths,
        $siteURL = Alfresco.util.siteURL,
        $isValueSet = Alfresco.util.isValueSet;

    YAHOO.Bubbling.fire("registerAction", {
        actionName: "onActionEditMultipleDocumentMetadata",
        fn: function rplp_onActionEditMultipleDocumentMetadata(record) {
            var type = record[0].jsNode.type;

            // Check that all the nodes are of same type
            var allSame = true;
            var nodeRefs = [];
            for (var i = 0, ii = record.length; i < ii; i++) {
                jsNode = record[i].jsNode;
                if (type !== jsNode.type) {
                    allSame = false;
            window.RPLP.globalNodeRefs = nodeRefs;

            if (allSame === false) {
                    text: this.msg("rplp.actions.editMultipleDocumentMetadata.multipleTypes"),
                    displayTime: 2.0
            } else { // All are of same type

                // Spawn form
                var scope = this,
                    nodeRef = record[0].nodeRef,
                    jsNode = record[0].jsNode;

                // Intercept before dialog show
                var doBeforeDialogShow = function dlA_onActionDetails_doBeforeDialogShow(p_form, p_dialog) {
                    // Dialog title
                    var fileSpan = '<span class="light">' + this.msg("rplp.actions.editMultipleDocumentMetadata.dialogTitle") + '</span>';

                        [p_dialog.id + "-dialogTitle", scope.msg("edit-details.title", fileSpan)]

                    // Edit metadata link button
                    this.widgets.editMetadata = Alfresco.util.createYUIButton(p_dialog, "editMetadata", null, {
                        type: "link",
                        label: scope.msg("edit-details.label.edit-metadata"),
                        href: $siteURL("edit-metadata?nodeRef=" + nodeRef)

                var templateUrl = YAHOO.lang.substitute(Alfresco.constants.URL_SERVICECONTEXT + "components/form?itemKind={itemKind}&itemId={itemId}&destination={destination}&mode={mode}&submitType={submitType}&formId={formId}&showCancelButton=true", {
                    itemKind: "node",
                    itemId: nodeRef,
                    mode: "edit",
                    submitType: "json",
                    formId: "multiple-edit-metadata"

                // Using Forms Service, so always create new instance
                var editDetails = new Alfresco.module.SimpleDialog(this.id + "-editDetails-" + Alfresco.util.generateDomId());

                    width: "auto",
                    templateUrl: templateUrl,
                    actionUrl: null,
                    destroyOnHide: true,
                    doBeforeDialogShow: {
                        fn: doBeforeDialogShow,
                        scope: this
                    onSuccess: {
                        fn: function dlA_onActionDetails_success(response) {
                            // Reload the node's metadata
                            var webscriptPath = "components/documentlibrary/data";
                            if ($isValueSet(this.options.siteId)) {
                                webscriptPath += "/site/" + encodeURIComponent(this.options.siteId)
                                url: $combine(Alfresco.constants.URL_SERVICECONTEXT, webscriptPath, "/node/", jsNode.nodeRef.uri) + "?view=" + this.actionsView,
                                successCallback: {
                                    fn: function dlA_onActionDetails_refreshSuccess(response) {

                                        // Display success message
                                            text: this.msg("message.details.success")

                                        // Refresh the document list...
                                    scope: this
                                failureCallback: {
                                    fn: function dlA_onActionDetails_refreshFailure(response) {
                                            text: this.msg("message.details.failure")
                                    scope: this
                        scope: this
                    onFailure: {
                        fn: function dLA_onActionDetails_failure(response) {
                            var failureMsg = this.msg("message.details.failure");
                            if (response.json && response.json.message.indexOf("Failed to persist field 'prop_cm_name'") !== -1) {
                                failureMsg = this.msg("message.details.failure.name");
                                text: failureMsg
                        scope: this


Our new action first declares a global javascript variable to hold an array of all nodeRefs (window.RPLP.globalNodeRefs = [];) that should be bulk edited. I found no other convenient way to pass them on to the ftl-template that later on will render the form and place those in a hidden form field. When the action gets fired it first checks that all nodes requested for bulk edit are of the same type before moving on to call the forms engine to render the form. When calling the forms engine we will do this with a new formId, “multiple-edit-metadata”. This way we can configure our form with xml the regular way.

Don’t forget to configure the message keys in the message resource bundle as well.

Configure multiple-edit-metadata forms for types

For every content-type you want to bulk-edit, create an xml config with “multiple-edit-metadata” as formId:

<config evaluator="string-compare" condition="DocumentLibrary">
      <!-- Form used when editing multiple nodes of same type at the same time. -->
      <form id="multiple-edit-metadata">
        <edit-form template="/se/redpill/alfresco/share/global/components/form/multiple-node-simple-metadata.ftl" />
          <show id="rplp:externalReference" />
          <hide id="rplp:documentStatusType" />
          <hide id="rplp:reviewStatusType" />
          <show id="rplp:securityClassificationType" />
          <show id="rplp:applicationRecipient" />
          <show id="rplp:applicationType" />
          <field id="rplp:externalReference">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/textfield-cb.ftl">
              <control-param name="styleClass">autoWidth</control-param>

          <field id="rplp:documentStatusType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>

          <field id="rplp:reviewStatusType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>

          <field id="rplp:securityClassificationType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>
          <field id="rplp:applicationRecipient">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/textfield-cb.ftl">
              <control-param name="styleClass">autoWidth</control-param>

          <field id="rplp:applicationType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>

We will use a custom freemarker template for the edit-form to be able to pass the array of nodes on to the backend service. Notice the javascript at the end of the template poplulating the hidden input value.

<#if formUI == "true">
   <@formLib.renderFormsRuntime formId=formId />

<div id="${args.htmlid}-dialog">
   <div id="${args.htmlid}-dialogTitle" class="hd"></div>
   <div class="bd">

      <div id="${formId}-container" class="form-container">
         <#if form.showCaption?exists && form.showCaption>
            <div id="${formId}-caption" class="caption"><span class="mandatory-indicator">*</span>${msg("form.required.fields")}</div>
         <form id="${formId}" method="${form.method}" accept-charset="utf-8" enctype="${form.enctype}" action="${form.submissionUrl}">
            <input type="hidden" id="muliple-edit-nodeRefs" />
            <div id="${formId}-fields" class="form-fields">
               <#list form.structure as item>
                  <#if item.kind == "set">
                     <@formLib.renderSet set=item />
                     <@formLib.renderField field=form.fields[item.id] />

            <div class="bdft">
               <input id="${formId}-submit" type="submit" value="${msg("form.button.submit.label")}" />
               &nbsp;<input id="${formId}-cancel" type="button" value="${msg("form.button.cancel.label")}" />


<script type="text/javascript">
YAHOO.util.Event.onAvailable('muliple-edit-nodeRefs', function(){
  // Set which nodeRefs are involved in the multiple update.
  YAHOO.util.Dom.get('muliple-edit-nodeRefs').value = window.RPLP.globalNodeRefs.join();


One of the drawbacks with this solution is that every form component have been duplicated. This is done because of the checkbox that sits next to every input type. If you want to edit the field you need to first tick the box. It would problaby be possible to use javascript instead to post-process the DOM-tree and dynamically add a checkbox next to every input. That way there will be no need to add a form component for every input type. However we only had a few input types and were a bit short of time, so I made one whenever needed. Below is the ftl-code for the regular textbox.

  <div class="yui-g">
    <div class="yui-u first">
      <div class="form-field">
          <label for="${fieldHtmlId}">${field.label?html}:<#if field.mandatory><span class="mandatory-indicator">${msg("form.required.fields.marker")}</span></#if></label>
          <input id="${fieldHtmlId}" name="${field.name}" tabindex="0"
                 <#if field.control.params.password??>type="password"<#else>type="text"</#if>
                 <#if field.control.params.styleClass??>class="${field.control.params.styleClass}"</#if>
                 <#if field.control.params.style??>style="${field.control.params.style}"</#if>
                 <#if field.value?is_number>value="${field.value?c}"<#else>value="${field.value?html}"</#if>
                 <#if field.description??>title="${field.description}"</#if>
                 <#if field.control.params.maxLength??>maxlength="${field.control.params.maxLength}"<#else>maxlength="1024"</#if> 
                 <#if field.control.params.size??>size="${field.control.params.size}"</#if> 
                 disabled="true" />
          <@formLib.renderFieldHelp field=field />
    <div class="yui-u">
      <div class="form-field">
        <input class="formsCheckBox" id="${fieldHtmlId}-entry" type="checkbox" tabindex="0"  
                   onchange='disableSiblingInputField("${fieldHtmlId}");' />
        <label for="${fieldHtmlId}-entry" class="checkbox">Redigera</label>

<script type="text/javascript">//<![CDATA[
function disableSiblingInputField(fieldId){
  var fieldToDisable = YAHOO.util.Dom.get(fieldId);
  if (fieldToDisable.disabled === true){
    fieldToDisable.disabled = false;
  }else {
    fieldToDisable.disabled = true;

The backend
The last thing needed to make all the parts work, was to subclass the NodeFormProcessor on the repository side and call the persist method in a loop for all nodes in our array:

<!-- override the form processor to be able to edit multiple nodes at once. -->  
  <bean id="nodeFormProcessor"
      <property name="filterRegistry" ref="nodeFilterRegistry" />
      <property name="matchPattern">
package se.redpill.alfresco.repo.forms;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.forms.FormData;
import org.alfresco.repo.forms.FormData.FieldData;
import org.alfresco.repo.forms.processor.node.NodeFormProcessor;
import org.alfresco.service.cmr.repository.NodeRef;
import org.apache.log4j.Logger;

 * The node form processor is overridden in order to be able to meet 
 * the requirement to update properties of muliple nodes at the same
 * time. This implementation will try to save every requested node, 
 * and collect info on failed ones. If there was one failure an exception
 * will be thrown after successful persistance of the other ones in order
 * to inform the user.
 * @author erik.billerby@redpill-linpro.com
public class RplpNodeFormProcessor extends NodeFormProcessor {
  private static final Logger logger = Logger.getLogger(RplpNodeFormProcessor.class);
  protected static String MULTIPLE_NODE_REFS_FIELD_NAME = "muliple-edit-nodeRefs";

  protected NodeRef internalPersist(NodeRef item, FormData data) {

    List<String> nodeRefs = null;
    List<NodeRef> failedNodeRefs = new ArrayList<>();
    for (FieldData fieldData : data) {

      String fieldName = fieldData.getName();
      if (MULTIPLE_NODE_REFS_FIELD_NAME.equals(fieldName)) {
        if (logger.isDebugEnabled()){
          logger.debug("This is a call to update properties for multiple nodes at once.");
        String value = (String) fieldData.getValue();
        nodeRefs = Arrays.asList(value.split("\\s*,\\s*"));
        // we found it, no need to continue the loop
    if (nodeRefs != null) {
      for (String nodeRefString : nodeRefs) {
        NodeRef nodeRef = new NodeRef(nodeRefString);
        try {
          super.internalPersist(nodeRef, data);
        }catch(Exception e){
      // After saving all nodes possible check if we got any error and throw exception
      // to inform user on which nodes we could not update.
      if (!failedNodeRefs.isEmpty()){
        StringBuilder sb = new StringBuilder();
        for (NodeRef failedNode: failedNodeRefs){
          String name = (String) nodeService.getProperty(failedNode, ContentModel.PROP_NAME);
        throw new AlfrescoRuntimeException("rplp.exception.update-multiple-nodes.failedNodes", new String[] { sb.toString() });
      return item;
    } else {
      // This is a normal single node call.
      return super.internalPersist(item, data);

This entry was posted in Alfresco, Javascript and tagged , , . Bookmark the permalink.

13 Responses to Edit metadata of multiple documents

  1. Carl Nordenfelt says:

    I think this is a great end user enhancement. With the standard edit properties dialogue you often end up clicking and clicking and clicking…

  2. Sulyman Korj says:

    this is very nice example. I am new in alfresco but I very interested on this, please can you give me some details when I can but this code can you give me step by step which file we need to modify it and but this code inside it.

  3. Michael Nelson says:

    I would like to know the actual file names that need to be edited and can this be built into a separate amp file, so that every time I perform an upgrade of the main Alfresco code, I don’t overwrite these modifications?


    • Erik Billerby says:

      @Michael, my solution is actually exactly in that way. Only regular alfresco extension points are used and the solution is built in to two amps, one for the repo side and one for the share side. So the file names are really not relevant since I create new ones, the important thing is to hook them in at the right place.

  4. leonardo says:

    Hi, I need this module (hopefully in a .jar), how can I get it?

  5. Useful info. Lucky me I discovered your site by accident,
    and I’m shocked why this accident didn’t happened earlier!
    I bookmarked it.

  6. Hughesnet says:

    It’s an remarkable paragraph for all the web viewers; they will obtain advantage from it I am sure.|

  7. Birthday says:

    Why viewers still make use of to read news papers when in this
    technological globe all is accessible on net?

  8. Pretty great post. I simply stumbled upon your weblog and wished to mention that I have truly enjoyed browsing your weblog posts.
    In any case I’ll be subscribing to your rss feed and I’m hoping
    you write again very soon!

  9. Greetings I am so glad I found your weblog, I really found you by accident, while I was looking on Google for
    something else, Regardless I am here now and would just like to say thanks for a marvelous post and a all round thrilling blog (I also
    love the theme/design), I don’t have time to
    read through it all at the moment but I have book-marked
    it and also added your RSS feeds, so when I have time I
    will be back to read much more, Please do keep up the awesome

  10. First off I would like to say awesome blog! I had a quick question which I’d like to ask if you do not mind. I was interested to find out how you center yourself and clear your thoughts before writing. I’ve had a difficult time clearing my mind in getting my ideas out there. I truly do enjoy writing but it just seems like the first 10 to 15 minutes are generally wasted simply just trying to figure out how to begin. Any recommendations or hints? Cheers!|

Comments are closed.