Write a filtered Repository File Selector Dojo dialog

This tutorial will explain how to write a dialog allowing users to choose a file in a repository with a pretty tree selector. We will also add a feature to filter files based on extension. The tree will only show documents ending with a valid filter.

This is the final result:

ICN_FileChooser_01

Inner template

BaseDialog is using an inner template, which needs to be assigned to the contentString field of our class. Our dialog will be using the ecm.widget.FolderTree widget to let user choose a document. That means the template ./templates/DocumentSelector.html will be:

<div class="documentSelectorDialog">
	<div data-dojo-type="ecm.widget.FolderTree" data-dojo-attach-point="folderTree" showFoldersOnly=false></div>
</div>

We now have a FolderTree in our dialog, but we still need to configure it to use our repository, filter files, and so on.

Dojo class

Skeleton

We are going to extend ecm.widget.dialog.BaseDialog, and inject our template for the content (be careful to not inject the template as main template of the Dialog, template, but in the content area template, contentString).

define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/dom",
    "dojo/dom-class",
    "dojo/dom-construct",
    "ecm/widget/dialog/BaseDialog",
    "ecm/widget/FolderTree",
    "dojo/text!./templates/DocumentSelector.html"
], function (
    declare,
    lang,
    dom,
    domClass,
    domConstruct,
    BaseDialog,
    FolderTree,
    template
) {

    return declare([ BaseDialog], {
        // Inject our template in the content area of the dialog
        contentString : template,
        /**
         * Repository to use to display the tree, must be defined 
         * at instantiation time
         */
        repository: null,
        /**
         * If set to false, user can also select folder regardless
         * the filter, can be override at instantiation time
         */
        documentOnly: true,
        /**
         * An array of string. If set, only documents endings 
         * with one of these string will be displayed and selectable
         */
        filters: null,
        // We want users to be able to give parameters at instantiation time
        /**
         * Create a new dialog to select a document
         * @param args Object as {repository: Repository, execute: function, (opt) documentOnly: boolean, (opt) filters: string[]}
         */
        constructor : function (args) {
            if (args) {
                lang.mixin(this, args);
            }
        },

        postCreate : function () {
            var methodName = "postCreate";
            this.logEntry(methodName);
            this.inherited(arguments);
            this.setResizable(true);
            this.setTitle("Select a document");
            
            // Adding a select button to our dialog, hooked to the select method
            this.selectButton = this.addButton("Select", lang.hitch(this, this.select), false, true);
            
            // We want to the dialog to be big enough, this style is .largeMessageDialog {height: 90%;width: 90%;}
            domClass.add(this.domNode, "largeMessageDialog");
            
            // Setting the repository on the tree
            this.folderTree.setRepository(this.repository);
            
            this.logExit(methodName);
        },
        
        /**
         * Called by the select button
         */
        select: function () {
            var item = this.folderTree.getSelectedItem();
            this.hide();
            this.execute(item);
        },
        
        /**
         * Overwrite this method to do something when the user selects a document
         */
        execute: function (item) {
            // Overwrite this with your process using the selected item  
        }

    });

});

This is a good start, we now have a dialog with a tree, but the tree is displaying every document without using the filters, sizing of the folder tree isn’t that good, and we can click the Select button basically at anytime.

Here is how to use it in an action:

performAction: function (repository, itemList, callback, teamspace, resultSet, parameterMap) {
    // Calling the standard Check Out Action Handler
    var item = itemList[0];
            
    var d = new DocumentSelector({
        repository: item.repository,
        execute: function (item) {
            alert(item.name);
        }
    });
    d.show();
}

Filtering

Filter what is displayed in the tree

This part is a bit complex, because the Folder Tree does not allow filtering. However, under the cover, it is using the retrieveFolderContents method, which allows this with the criterias parameter. But again, this method does not allow to filter only documents, it’s applying the filter on folders as well, and we don’t want that since we want all folders plus and filtered files (or we wouldn’t have any folder since they don’t have extension). In order to do that, we will do some aspect hacking on it to make a double request (one forfolders, one for filtered files), while the TreeFolder model thinks it’s asking for evertyhing.

Add this to your postCreate method:
(You need to import the following classes as well: dojo/_base/array, dojo/aspect, dojo/Deferred, dojo/promise/all)

if (this.filters) {

    // If we have a filter, we will have to do a complex hack to query files with filter
    // without filtering folders

    // We have to wait for the tree model to be set, because at this time it is not yet
    aspect.after(this.folderTree, "_retrieveItemResponse", lang.hitch(this, function (res) {

        var thisFilter = this.filters;

        // on the getChildren of the tree model, hook up the parentItem's retrieveFolderContents
        // to make our double call
        aspect.before(this.folderTree.getTreeModel(), "getChildren", function (parentItem, onComplete) {
            // Replace the original retrieveFolderContents to call it twice for foler once,
            // and for filtered files a second time, then wait for both answer before calling callback
            var handle = aspect.around(parentItem, "retrieveFolderContents", function (original) {
                return function (foldersOnly, callback, orderBy, descending, noCache, teamspaceId, filterType, parent, criteria) {
                    // Instead of calling retrieveFolderContents once with no filter, call it once for the folders without folder
                    // and once with a filter, wait for both ansswer and call the callback with that
                    handle.remove();
                    var newCriteria = {
                        type: "OR", // OR or AND
                        conditions: []
                    };


                    array.forEach(thisFilter, function (e) {
                        newCriteria.conditions.push({
                            name: "{NAME}", // Can only be {NAME} or {ALL}, which to a OR on all searchable fields
                            condition: "endWith", // can only be contain, startWith or endWidth, can NOT be =
                            value: e // Can be anything
                        });
                    });

                    var def1 = new Deferred(), def2 = new Deferred();
                    parentItem.retrieveFolderContents(true, function (resultSet) {
                        def1.resolve(resultSet);
                    }, orderBy, descending, noCache, teamspaceId, filterType);

                    parentItem.retrieveFolderContents(false, function (resultSet) {
                        def2.resolve(resultSet);
                    }, orderBy, descending, noCache, teamspaceId, filterType, parent, newCriteria);

                    all([def1, def2]).then(function (res) {
                        res[0].items = res[0].items.concat(res[1].items);
                        callback(res[0]);
                    });
                };
            });
            return [parentItem, onComplete];
        });
        return res;
    }));
}

Now we can call our dialog with filters:

var d = new DocumentSelector({
    repository: item.repository,
    filters: [".png"],
    execute: function (item) {
        alert(item.name);
    }
});
d.show();

Now we are seing only png documents in our tree:

ICN_FileChooser_02

Adding validation

We also want to enable the Select button only if we are allowed to select. Now that we are showing only filtered files, that should be correct for files so we won’t check the filters, but we still need to check if we are allowed to select folders or not.

Add a isValid method to your class:

isValid: function (item) {
    if (item.isFolder()) {
        return !this.documentOnly;
    } else {
        return true;
    }
},

And listen to select event to enable or disable the Select button depending of the item. Add this to your postCreate method:

aspect.after(this.folderTree, "onItemSelected", lang.hitch(this, function (item, node) {
    if (this.isValid(item)) {
        this.selectButton.set("disabled", false);
    } else {
        this.selectButton.set("disabled", true);
    }
}), true);

Adding double click select

Users like to be able to double click instead of having to reach the Select button to choose, so hook up the double click event to valid, without forgetting to check the item is valid. Add this to your postCreate method:

aspect.after(this.folderTree, "onItemDblClick", lang.hitch(this, function () {
    var item = this.folderTree.getSelectedItem();
    if (this.isValid(item)) {
        this.select();
    }
}), true);

Deal with sizing issue

As described here, there is bug in the FolderTree widget, that lets it be correctly sized only contained directly in another dojo widget. Since that’s not what I did here, I fixed it by overwriting the resize method for the folder tree, and called it as well when resizing the window.

Add this in your postConstuct method:
(You need to import the following classes as well: dojo/dom-geometry as domGeom)

this.folderTree.resize = function () {
    var pn = this.getParent();
    if (pn && pn.contentArea) {
        var box = domGeom.getContentBox(pn.contentArea);
        domGeom.setMarginBox(this.domNode, {l: box.l, t: 0, h: box.h, w: box.w});
    }

    if (this._tree && this._tree.domNode) {
        this._tree.resize(domGeom.getMarginBox(this.domNode));
    }
};

And we also want to overwrite the resize method of our dialog to rezise the tree when resized. Add this method to your DocumentSelector class:

resize: function () {
    this.inherited(arguments);
    this.folderTree.resize();
},

And that’s it! At this point you should have a nice dialog with a tree correctly sized, responsive and user friendly by showing only filtered files and having a double click feature.

Here is the full class:

define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/_base/array",
    "dojo/dom",
    "dojo/dom-class",
    "dojo/dom-construct",
    "dojo/dom-geometry",
    "ecm/widget/dialog/BaseDialog",
    "ecm/widget/FolderTree",
    "dojo/aspect",
    "dojo/Deferred",
    "dojo/promise/all",
    "dojo/text!./templates/DocumentSelector.html"
], function (
    declare,
    lang,
    array,
    dom,
    domClass,
    domConstruct,
    domGeom,
    BaseDialog,
    FolderTree,
    aspect,
    Deferred,
    all,
    template
) {

    return declare([ BaseDialog], {
        // Inject our template in the content area of the dialog
        contentString : template,
        /**
         * Repository to use to display the tree, must be defined 
         * at instantiation time
         */
        repository: null,
        /**
         * If set to false, user can also select folder regardless
         * the filter, can be override at instantiation time
         */
        documentOnly: true,
        /**
         * An array of string. If set, only documents endings 
         * with one of these string will be displayed and selectable
         */
        filters: null,
        // We want users to be able to give parameters at instantiation time
        /**
         * Create a new dialog to select a document
         * @param args Object as {repository: Repository, execute: function, (opt) documentOnly: boolean, (opt) filters: string[]}
         */
        constructor : function (args) {
            if (args) {
                lang.mixin(this, args);
            }
        },

        postCreate : function () {
            var methodName = "postCreate";
            this.logEntry(methodName);
            this.inherited(arguments);
            this.setResizable(true);
            this.setTitle("Select a document");
            
            // Adding a select button to our dialog, hooked to the select method
            this.selectButton = this.addButton("Select", lang.hitch(this, this.select), false, true);
            
            // We want to the dialog to be big enough, this style is .largeMessageDialog {height: 90%;width: 90%;}
            domClass.add(this.domNode, "largeMessageDialog");
            
            // Setting the repository on the tree
            this.folderTree.setRepository(this.repository);
            
            this.folderTree.resize = function () {
                var pn = this.getParent();
                if (pn && pn.contentArea) {
                    var box = domGeom.getContentBox(pn.contentArea);
                    domGeom.setMarginBox(this.domNode, {l: box.l, t: 0, h: box.h, w: box.w});
                }

                if (this._tree && this._tree.domNode) {
                    this._tree.resize(domGeom.getMarginBox(this.domNode));
                }
            };
            
            if (this.filters) {
                
                // If we have a filter, we will have to do a complex hack to query files with filter
                // without filtering folders
                
                // We have to wait for the tree model to be set
                aspect.after(this.folderTree, "_retrieveItemResponse", lang.hitch(this, function (res) {

                    var thisFilter = this.filters;

                    // on the getChildren, hook up the parentItem's retrieveFolderContents to add our criterias
                    aspect.before(this.folderTree.getTreeModel(), "getChildren", function (parentItem, onComplete) {

                        var handle = aspect.around(parentItem, "retrieveFolderContents", function (original) {
                            return function (foldersOnly, callback, orderBy, descending, noCache, teamspaceId, filterType, parent, criteria) {
                                // Instead of calling retrieveFolderContents once with no filter, call it once for the folders without folder
                                // and once with a filter, wait for both ansswer and call the callback with that
                                handle.remove();
                                var newCriteria = {
                                    type: "OR", // OR or AND
                                    conditions: []
                                };


                                array.forEach(thisFilter, function (e) {
                                    newCriteria.conditions.push({
                                        name: "{NAME}", // Can only be {NAME} or {ALL}, which to a OR on all searchable fields
                                        condition: "endWith", // can only be contain, startWith or endWidth, can NOT be =
                                        value: e // Can be anything
                                    });
                                });

                                var def1 = new Deferred(), def2 = new Deferred();
                                parentItem.retrieveFolderContents(true, function (resultSet) {
                                    def1.resolve(resultSet);
                                }, orderBy, descending, noCache, teamspaceId, filterType);

                                parentItem.retrieveFolderContents(false, function (resultSet) {
                                    def2.resolve(resultSet);
                                }, orderBy, descending, noCache, teamspaceId, filterType, parent, newCriteria);

                                all([def1, def2]).then(function (res) {
                                    res[0].items = res[0].items.concat(res[1].items);
                                    callback(res[0]);
                                });
                            };
                        });
                        return [parentItem, onComplete];
                    });
                    return res;
                }));
            }
            
            aspect.after(this.folderTree, "onItemSelected", lang.hitch(this, function (item, node) {
                if (this.isValid(item)) {
                    this.selectButton.set("disabled", false);
                } else {
                    this.selectButton.set("disabled", true);
                }
            }), true);
            
            aspect.after(this.folderTree, "onItemDblClick", lang.hitch(this, function () {
                var item = this.folderTree.getSelectedItem();
                if (this.isValid(item)) {
                    this.select();
                }
            }), true);
            
            this.logExit(methodName);
        },
        
        resize: function () {
            this.inherited(arguments);
            this.folderTree.resize();
        },
        
        isValid: function (item) {
            if (item.isFolder()) {
                return !this.documentOnly;
            } else {
                return true;
            }
        },
        
        /**
         * Called by the select button
         */
        select: function () {
            var item = this.folderTree.getSelectedItem();
            this.hide();
            this.execute(item);
        },
        
        /**
         * Overwrite this method to do something when the user selects a document
         */
        execute: function (item) {
            // Overwrite this with your process using the selected item  
        }

    });

});

And use it as follow:

var d = new DocumentSelector({
    repository: item.repository, // Mandatory, the repository to use
    filters: [".png", "jpg", "jpeg"], // Optional, can put as many as you need
    documentOnly: true, // default true, false will allow user to select folder as well
    execute: function (item) { 
        alert(item.name); // What to do when user has chosen
    }
});
d.show();

3 thoughts on “Write a filtered Repository File Selector Dojo dialog

  1. Deep Thakkae

    Hi Guillaume,

    I am stuck with downloading documents from the search results along with the csv file which contents the metadata.
    Please help me in sorting out this.

    Thanks,
    Deep

    Reply

Leave a Reply