Drag and Drop a folder in IBM Content Navigator

Nowadays, everyone seems to think dropping a folder is somehow optional. They all have given up on the File API “Directories and System” (except Google which at least implemented it). I have to admit, I clearly don’t understand because in the ECM world, that just a must-have feature…

Anyway, since this is something our users are expecting, especially since there are used to it with the Workplace XT (via the applet), we had to find a workaround. Actually we found two which are quite complementary. One is using the Chrome Directories and System API, which really allows a true folder drop, but way better than Workplace XT since it’s applet free and therefore mobile supported. The other is supported by all browsers and requires the user to only zip the folder and drop the zip. It’ is only 1 second extra work for the user instead of the hours it would have to spend to create all the folder hierarchy manually and upload files folder by folder. This seems like a good alternative and again, it is supported by every brother.

Let’s present these two workarounds quickly. I won’t explain the whole implementation since it might get quite complex but instead, I will explain how to address the problem.

1. Supported by all browser, zip and drop

This is the easiest way I came up for the user, it only requires the user to zip the folder and drop it. Then it acts like a normal add. How does it work?

That actually not so hard, the unzipping is done entirely by the client (browser) in JavaScript, I’m using the zip,js library (great work from Gildas Lormeau by the way). That way we finally use a bit the client resources and not overload the server. I get all entries, which give me Blob object,. From the filename and the Blob, I can build W3C File API object as given by a classic drop which I will use to do the add. I also give the Wizard the path within the object because it needs to create the directory.

This is actually not so hard, what is a lot harder is implementing the Add Wizard able to add multiple files from different types without being a nightmare for the user (The built-in Add Wizard of ICN allows only one File Type at a time if you associated several entry templates). Also this Wizard needs to be able to create/re-use folders to place documents in the same folder they were in the zip. That the part is too much work and I won’t explain it. You can actually see what it’s like in the video, the wizard classifies by most relevant Entry Template, then generic one if there is none for the File Type, or no Entry template if there is none associated on the folder. The good thing is that when you have this wizard, you can also use it for the Chrome drop folder or any other multi-add operation.

Implementation details:

First you need to find the right entry point to hook up the ICN behavior and replace it with your smart drop. I did that in two points:

  • When dropping external files on the content pane (right pane)
  • When dropping external files on a Tree node (left pane)

Here is how to hook up this two entry points

aspect.around(DndFromDesktopAddDoc.prototype, "displayDialog", function (originalFunction) {
    return function (files, targetItem, teamspace, defaultClass) {
        // Here you can overwrite the behavior of when ICN is displaying the
        // standard Add Document Wizard for the files to drop
        // Basically you would check if the file is a unique zip, and launch your
        // Smart wizard instead of the basic one
    };
});
aspect.around(Tree.prototype, "onExternalDrop", function (original) {
    return function (evt) {
        // Here you can overwrite the behavior of when ICN is displaying the
        // standard Add Document Wizard for the files to drop
        // Basically you would check if the event's files (evt.dataTransfer.files) contains
        // a unique zip, and launch your Smart wizard instead of the basic one
    };
});

Then you need to check if you got only one zip file dropped, and if it is, deflate the zip file, build File object from the Blob and the entry name, and call your wizard with that:

aspect.around(DndFromDesktopAddDoc.prototype, "displayDialog", function (originalFunction) {
    return function (files, targetItem, teamspace, defaultClass) {
        if (files && files.length > 0) {
            if (this.addContentItem) {
                this.addContentItem.destroyRecursive();
            }
            outerThis.processFiles(files, targetItem, this);
        }
    };
});

aspect.around(Tree.prototype, "onExternalDrop", function (original) {
    return function (evt) {

        if (evt.dataTransfer.files.length > 0) {

            if (!outerThis.isSingleZip(evt.dataTransfer.files)) {
                return original.apply(this, [evt]);
            }

            // This is for us
            evt.preventDefault();
            evt.stopPropagation();
            var targetItem = this._getExternalDropTargetItem(evt),
                files = evt.dataTransfer.files;
            if (targetItem && this._eventHasFiles(evt) && this._numberWithinMaxDocToAdd(files)) {
                var success = lang.hitch(this, function () {
                    if (this.addContentItem) {
                        this.addContentItem.destroyRecursive();
                    }

                    if (targetItem.isInstanceOf(ecm.model.Favorite)) {
                        targetItem = targetItem.item;
                    }

                    outerThis.processFiles(files, targetItem, this);

                });
                if (targetItem.isInstanceOf && targetItem.isInstanceOf(ecm.model.Favorite) && !targetItem.item) {
                    targetItem.retrieveFavorite(lang.hitch(this, function (obj) {
                        if (!this._dndModel.hasPrivilegeToAddTo(targetItem)) {
                            this._dndModel.addCannotDropErrorMessage(targetItem.item.name);
                        } else {
                            this._verifyCanAddFiles(evt.dataTransfer.files).then(success, this._verifyCanAddFilesFailure);
                        }
                    }));
                } else {
                    this._verifyCanAddFiles(evt.dataTransfer.files).then(success, this._verifyCanAddFilesFailure);
                }
            }
            return false;
        }

    };
});

With the processFiles function actually doing the conversion form the zip files to W3C files:

processFiles: function (files, targetItem, originalThis) {
    var readyToProceed = new Deferred();

    // Here we are droping one archive (zip), unzip all and build the files from it
    if (files.length == 1 && files[0].name.indexOf('.zip', files[0].name.length - ".zip".length) !== -1) {
        // We convert the entries from the zip in files
        this.getEntries(files[0]).then(lang.hitch(this, function (entries) {
            var waitForAll = [], newEntries = [], i;
            for (i = 0; i < entries.length; i++) {
                if (!entries[i].directory) {
                    var def = new Deferred();
                    waitForAll.push(def);
                    this.getEntryFile(entries[i], def).then(function (res) {
                        var file = new File([res.blob], res.entry.filename);
                        file.displayname = res.entry.filename;
                        file.folderpath = res.entry.filename.substring(0, res.entry.filename.lastIndexOf("/"));
                        console.log('path set to: ' + file.folderpath);
                        newEntries.push(file);
                        res.def.resolve();
                    });
                }
            }

            all(waitForAll).then(function () {
                files = newEntries;
                readyToProceed.resolve();
            });
        }));

    } else {
        readyToProceed.resolve();
    }

    readyToProceed.then(lang.hitch(originalThis, function () {

        // Check the number of item since ICN could check because it thinks it's only one file
        if (ecm.model.desktop && ecm.model.desktop.maxNumberDocToAdd < files.length) {
            ecm.model.desktop.addMessage(Message.createErrorMessage("add_document_too_many_items_error", [
                ecm.model.desktop.maxNumberDocToAdd,
                files.length
            ]));
            return;
        }
        // Display the Wizard
        this.addContentItem = new MultiDocWizard({
            checkin: false,
            repository: targetItem.repository,
            parentFolder: targetItem
        });
        this.addContentItem.show(files);
    }));
},
getEntries: function (file) {
    var res = new Deferred();
    zip.createReader(new zip.BlobReader(file), function (zipReader) {
        zipReader.getEntries(function (entries) {
            res.resolve(entries);
        });
    }, null);
    return res.promise;
},
getEntryFile: function (entry, def) {
    var res = new Deferred();
    var writer = new zip.BlobWriter();
    entry.getData(writer, function (blob) {
        res.resolve({
            'entry': entry,
            'blob': blob,
            'def': def
        });
    }, null);
    return res.promise;
}

 

2. Using the Chrome Drop folder

Thanks Chrome! At least you are progressing on this topic because no one seems to care… They implemented the File API Directories and System, which allow writing folder when the user drop it, without extra permission (well the user drop it so I guess he knows 🙂 ). Then you can read all entries, all sub-subfolders and get from those entries the W3C File object. Then you add as extra data on the file the path within the repository since it will be use by the Wizard to add the file in the right subfolder. Then give all that to your magical Wizard and you’re done.

Here is a demo:

Implementation details:

And for those who are interested by the implementation, here are some details. First, as for the zip drop, you need to find the right entry point to hook up the ICN behavior and replace it with your smart drop. I did that in two points:

  • When dropping external files on the content pane (right pane)
  • When dropping external files on a Tree node (left pane)

However this time we need to catch the drop sooner than for the zip on the content pane because we want to override the error message saying we can not drop folder. Here is how to hook up this two entry points

aspect.around(DndFromDesktop.prototype, "onExternalDrop", function (originalFunction) {
    return function (evt) {
        if (evt.dataTransfer && evt.dataTransfer.items &&
                evt.dataTransfer.items.length &&
                evt.dataTransfer.items[0].webkitGetAsEntry() &&
                evt.dataTransfer.items[0].webkitGetAsEntry().isDirectory) {

            // Get the webit entry
            var entry = evt.dataTransfer.items[0].webkitGetAsEntry();

            // Get all files in the folder
            var allFiles = [];
            outerThis.readDir(entry, allFiles).then(lang.hitch(this, function () {

                // Check if number is not over limit
                if (ecm.model.desktop && ecm.model.desktop.maxNumberDocToAdd < allFiles.length) {
                    ecm.model.desktop.addMessage(Message.createErrorMessage("add_document_too_many_items_error", [
                        ecm.model.desktop.maxNumberDocToAdd,
                        allFiles.length
                    ]));
                    return;
                }
                // Launch the Add Wizard with the files in the folder
                var row = this._getExternalDropTargetRow(evt);
                var rowItem = row ? row.item() : null;
                var targetItem = this._getTargetItem(rowItem);
                this.addContentItem = new MultiDocWizard({
                    checkin: false,
                    repository: targetItem.repository,
                    parentFolder: targetItem
                });
                this.addContentItem.show(allFiles);
            }));

        } else {
            // This is not a single folder, run normal drop
            originalFunction.apply(this, [evt]);
        }

    };
});


aspect.around(Tree.prototype, "onExternalDrop", function (original) {
    return function (evt) {

        if (evt.dataTransfer.files.length > 0) {

            // If we are dropping a single zip, deal with it, if not acall original so other strageties
            // can deal with it (ChromeDrop or if not standard drop using MultiDocWizard)
            if (!outerThis.isChromeDrop(evt)) {
                return original.apply(this, [evt]);
            }

            // This is for us
            evt.preventDefault();
            evt.stopPropagation();

            // Get the webit entry
            var entry = evt.dataTransfer.items[0].webkitGetAsEntry();

            // Get all files in the folder
            var allFiles = [];
            outerThis.readDir(entry, allFiles).then(lang.hitch(this, function () {

                // Check if number is not over limit
                if (ecm.model.desktop && ecm.model.desktop.maxNumberDocToAdd < allFiles.length) {
                    ecm.model.desktop.addMessage(Message.createErrorMessage("add_document_too_many_items_error", [
                        ecm.model.desktop.maxNumberDocToAdd,
                        allFiles.length
                    ]));
                    return;
                }
                // Launch the Add Wizard with the files in the folder
                var targetItem = this._getExternalDropTargetItem(evt);
                this.addContentItem = new MultiDocWizard({
                    checkin: false,
                    repository: targetItem.repository,
                    parentFolder: targetItem
                });
                this.addContentItem.show(allFiles);
            }));
        }

    };
});

Then, you need to convert the webkit entry into an array of files to give to your wizard. That’s what does the readDir function. Here is the code of this function.

/**
 * Read en entry and resolve when the file has been added to the allFiles array
 * @param entry the webkit entry
 * @ allFiles {@File[]} the array of file where add the file
 * @return Promise a promise resolving when the file has been added to the array
 */
readFile: function (entry, allFiles) {
    var res = new Deferred();
    entry.file(lang.hitch(this, function (file) {
        file.folderpath = entry.fullPath.substring(0, entry.fullPath.lastIndexOf("/"));
        file.displayname = entry.fullPath;
        allFiles.push(file);
        res.resolve();
    }));
    return res.promise;
},
/**
 * Read en entry which is a directory, returns all contains files (recursively) in the allFiles array
 * and resolve when it's done
 * @param entry the webkit entry
 * @ allFiles {@File[]} the array of file where add the file
 * @return Promise a promise resolving when all files have been added to the array
 */
readDir: function (entry, allFiles) {
    var res = new Deferred();
    var dirReader = entry.createReader();

    this.readEntries(dirReader, allFiles).then(function () {
        res.resolve();
    });

    return res.promise;
},
/**
 * Read one directory reader, returns all contains files (recursively) in the allFiles array
 * and resolve when it's done
 * @param dirReader the directory reader
 * @ allFiles {@File[]} the array of file where add the file
 * @return Promise a promise resolving when all files have been added to the array
 */
readEntries: function (dirReader, allFiles) {
    var res = new Deferred();
    dirReader.readEntries(lang.hitch(this, function (results) {
        var allSubFolderPromises = [];
        if (!results.length) {
            res.resolve();
        } else {
            var i;
            for (i = 0; i < results.length; i++) {
                if (results[i].isDirectory) {
                    var subdirReader = results[i].createReader();
                    allSubFolderPromises.push(this.readEntries(subdirReader, allFiles));
                } else {
                    // We read the file and also wait for it
                    allSubFolderPromises.push(this.readFile(results[i], allFiles));
                }
            }
            // Also wait for me
            allSubFolderPromises.push(this.readEntries(dirReader, allFiles));

            // Then wai everyone
            all(allSubFolderPromises).then(function () {
                res.resolve();
            });
        }
    }), null);
    return res.promise;
}

14 thoughts on “Drag and Drop a folder in IBM Content Navigator

  1. Aerith

    Hi!
    Thank you very much for your post!
    Unfortunately I’m no developer and only a FileNet Admin, but it’s somethin ga lot of our customers always wanted 😀

    Reply
    1. Guillaume Post author

      Hi Aerith,

      As ours! That’s why I implemented it :). If you have no one to implement this, what about the Share and Sync feature in ICN 2.0.3. Wouldn’t it cover your need? There are limitations: you can’t use entry templates and that’s only for mobiles/Windows users but that does allow adding folders and is pretty neat to work on content.

      Just a thought, that could please your customers.

      Reply
  2. n7down

    Hi, would you mind going into a bit more detail on how you did this – I would love to implement this, but I’m kinda at a loss as to how you incorporated your code into content navigator. Did you create a content navigator plugin and override the DND functionality? If how did you do that? Thanks!

    Reply
    1. Guillaume Post author

      Hi n7down,

      Indeed, I wrote a new plugin and in the main JS file, use aspect to override the default D&D behavior to use mine instead. You can see my code in the entry point section.

      If you want to implement that, you need to define what are your requirements.Implementation complexity can vary a lot for a small functionality difference. If you want to implement this, the quickest way,would be to implement a drop zip, which is easier to implement and more widely supported than the Chrome drop, without offering Entry Template choice or Properties editing to the users. If you are not dealing with entry template, implementation will be a lot easier, because you won’t have to implement a fancy wizard to allow user to select ET and properties. Basically it will just be overriding the default D&D behavior by transferring the zip to a ICN service. In this service you can just unzip the file and create all folders/documents with classic types keeping the inner zip folder hierarchy. That’s the easiest and most efficient but users won;t be able to set entry template nor properties.

      To summarize, the main step are:

      If you want to allow user to change properties or select Entry Templates, this is more complicated because you’ll have to implement a smart wizard classifying files by types and associating entry template, then send all these information (selected ET with properties values) along with the zip file in order for your service to use them when creating documents/folders.

      Hope that helps. This is a complex implementation and I won’t be able to explain everything in one comment. However fell free to ask more specific questions when you start working on this and I’ll be happy to answer.

      Reply
  3. ahmed hany

    Hi Guillaume,
    This is a great post,but i need to know if i can open the D&D wizard form from any action in window,for example if i create a context menu in windows and i want to view th D&D form after clicking any action on my menu for a document in my windows file system …. can i view the D&D form ???

    Reply
    1. Guillaume Post author

      Hi Ahmed,

      My opinion is that that would be the wrong way to do it. Starting the wizard needs to have some context and makes sense only from a node in ICN.

      To do what you are trying to achieve, I would either use IBM® Content Navigator Sync which looks like what you need, or I would write a small program using the Java API and call it from the context menu with the file path and folder path as parameter to know what you add and where to add it.

      Hope that helps you.

      Reply
  4. shatabdi

    Hi Guillaume,

    When I am trying to drag and drop a file then D&D wizard or Add Document Dialog box is not getting open. Can you please suggest what can be the problem.

    Thanks

    Reply
  5. Geeta

    Hi Guillaume,

    While doing D&D of document the Add Dialog Box is not getting open in my system. Can you please suggest what can be problem.

    Thanks

    Reply
    1. Guillaume Post author

      Hi Geeta,

      What are you trying to drop? If this a folder, that’s not supposed to work with an ootb ICN. This post is about an enhancement I’ve made. Also where are you dropping it in ICN? Could you please provide more details? Thank you.

      Reply
  6. pablo

    hello Mr Guillaume, great job!!!
    tell me, how to view the big yellow folder in the left panel in navigator ? According to the video.
    regards

    Reply
    1. Guillaume Post author

      Hi Pablo, sorry for the late answer. I guess you’re talking about the extra feature on the left. This is just a new feature, this one in particular is the one from the Reb Book.

      Reply
  7. Umesh

    Hi, I have overidden Dnd to display customized dialog. But I don’t know how to get the inputstream of the external file which got drag & dropped. Can someone help me on this?

    Reply
  8. Umesh

    Hi, I have overridden DnD to display my custom dialog. But I’m sure on how to get input stream of the drag & dropped files. Can someone help me on this.

    Reply

Leave a Reply