Home

check what fields to edit

Background
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.

Implementation
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
(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:

1
2
3
4
5
<config evaluator="string-compare" condition="DocLibCustom">
  <dependencies>
    <js src="components/documentlibrary/documentlist-display-type.js" />
  </dependencies>
</config>

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

Selection_041

Configure our new bulk action

1
2
3
4
5
6
7
<config evaluator="string-compare" condition="DocumentLibrary">
 
 <multi-select>
  <action type="action-link" id="onActionEditMultipleDocumentMetadata" asset="document" permission="Write" label="actions.editMultipleDocumentMetadata" />
 </multi-select>
 
</config>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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;
                    break;
                }
                nodeRefs.push(record[i].nodeRef);
            }
            window.RPLP.globalNodeRefs = nodeRefs;
            if (allSame === false) {
                Alfresco.util.PopupManager.displayMessage({
                    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>';
                    Alfresco.util.populateHTML(
                        [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());
                editDetails.setOptions({
                    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)
                            }
                            Alfresco.util.Ajax.request({
                                url: $combine(Alfresco.constants.URL_SERVICECONTEXT, webscriptPath, "/node/", jsNode.nodeRef.uri) + "?view=" + this.actionsView,
                                successCallback: {
                                    fn: function dlA_onActionDetails_refreshSuccess(response) {
                                        // Display success message
                                        Alfresco.util.PopupManager.displayMessage({
                                            text: this.msg("message.details.success")
                                        });
                                        // Refresh the document list...
                                        YAHOO.Bubbling.fire("metadataRefresh");
                                    },
                                    scope: this
                                },
                                failureCallback: {
                                    fn: function dlA_onActionDetails_refreshFailure(response) {
                                        Alfresco.util.PopupManager.displayMessage({
                                            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");
                            }
                            Alfresco.util.PopupManager.displayMessage({
                                text: failureMsg
                            });
                        },
                        scope: this
                    }
                }).show();
            }
        }
    });
})();

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<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" />
        <field-visibility>
          <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-visibility>
        <appearance>
          <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>
            </control>
          </field>
          <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>
            </control>
          </field>
          <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>
            </control>
          </field>
          <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>
            </control>
          </field>
          <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>
            </control>
          </field>
          <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>
            </control>
          </field>
        </appearance>
      </form>
</config>

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<#if formUI == "true">
   <@formLib.renderFormsRuntime formId=formId />
</#if>
<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>
         </#if>
      
         <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 />
                  <#else>
                     <@formLib.renderField field=form.fields[item.id] />
                  </#if>
               </#list>
            </div>
            <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")}" />
            </div>
      
         </form>
      </div>
   </div>
</div>
<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();
});
</script>

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  <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>
    </div>
    <div class="yui-u">
      <div class="form-field">
        <br/>
        <input class="formsCheckBox" id="${fieldHtmlId}-entry" type="checkbox" tabindex="0"
                   onchange='disableSiblingInputField("${fieldHtmlId}");' />
        <label for="${fieldHtmlId}-entry" class="checkbox">Redigera</label>
      </div>
    </div>
 </div>
<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;
  }
}
//]]></script>

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:

1
2
3
4
5
6
7
8
9
<!-- override the form processor to be able to edit multiple nodes at once. --> 
  <bean id="nodeFormProcessor"
      class="se.redpill.alfresco.repo.forms.RplpNodeFormProcessor"
      parent="baseFormProcessor">
      <property name="filterRegistry" ref="nodeFilterRegistry" />
      <property name="matchPattern">
         <value>node</value>
      </property>
   </bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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";
  @Override
  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
        break;
      }
    }
    if (nodeRefs != null) {
      for (String nodeRefString : nodeRefs) {
        NodeRef nodeRef = new NodeRef(nodeRefString);
        try {
          super.internalPersist(nodeRef, data);
        }catch(Exception e){
          failedNodeRefs.add(nodeRef);
        }
      }
      
      // 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);
          sb.append(name).append(",");
        }
        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.

7 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?

    Thanks!

    • 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. These coupons can also be wonderful gifting suggestions
    for your buddies and family. The first place that I would look
    for diaper coupons is the official website of the brand that
    you prefer to buy. If it were not for the frequent online coupons, I most likely would not
    be shopping in that store.

  5. Simply want to say your article is as astounding. The clarity for your
    publish is just great and i can assume you are a professional on this subject.
    Well together with your permission let me to seize your RSS feed to keep
    up to date with approaching post. Thank you 1,000,000 and please continue the enjoyable work.

Publicités

Laisser un commentaire

Choisissez une méthode de connexion pour poster votre commentaire:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s