Monday, December 8, 2008

Extending Share 2 - Adding a New Content button to Document Library

In the Extending Share 1 blog entry, we created a share extension project. Now we will write code to add a 'new content' button to the document library section of Share, based on the code created in the learning surf 1-6 blog entries.
NOTE: the complete sourceocde for this project can be downloaded from here.
Step 1 - Create a new form component
We will create a ui web script to open a create content form. In the 'config/alfresco/site-webscripts' source folder in package 'com.orbitz.components.documentlibrary', add the following files:

create-content.get.desc.xml
<webscript>
<shortname>create-content</shortname>
<description>Create Content module primarily for Document Library</description>
<url>/components/documentlibrary/create-content</url>
</webscript>
create-content.get.html.ftl
<div id="${args.htmlid}-dialog" class="create-folder">
<div class="hd">${msg("title")}</div>
<div class="bd">
<form id="${args.htmlid}-form" action="" method="post" accept-charset="utf-8">
<div class="yui-g">
<h2>${msg("header")}:</h2>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-name">${msg("label.name")}:</label></div>
<div class="yui-u"><input id="${args.htmlid}-name" type="text" name="name" tabindex="1" />&nbsp;*</div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-title">${msg("label.title")}:</label></div>
<div class="yui-u"><input id="${args.htmlid}-title" type="text" name="title" tabindex="2" /></div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-description">${msg("label.description")}:</label></div>
<div class="yui-u"><textarea id="${args.htmlid}-description" name="description" rows="3" cols="20" tabindex="3" ></textarea></div>
</div>
<div class="yui-gd">
<div class="yui-u first"><label for="${args.htmlid}-content">${msg("label.content")}:</label></div>
<div class="yui-u"><textarea id="${args.htmlid}-content" name="content" rows="3" cols="20" tabindex="3" ></textarea></div>
</div>
<div class="bdft">
<input type="button" id="${args.htmlid}-ok" value="${msg("button.ok")}" tabindex="4" />
<input type="button" id="${args.htmlid}-cancel" value="${msg("button.cancel")}" tabindex="5" />
</div>
</form>
</div>
</div>

</code></pre>
create-content.get.properties
## Titles
title=New Content
header=New Content Details

## Labels
label.name=Name
label.title=Title
label.description=Description
label.content=Content
To test the new form,
  1. Deploy the web script by running the 'deploy' an task. (Make sure that share was started at least once so that the share war is expanded in the webapps/share directory of tomcat, and that the APP_TOMCAT_HOME and TOMCAT_HOME is properly set. This will copy the web scripts to the share directory.
  2. Start tomcat
  3. Check the ui web script by going to the url http://localhost:8080/share/service/components/documentlibrary/create-content?htmlid which should render the form (without any CSS, though).

Step 2 - Create a new toolbar for Document Library adding the 'New Content' button
The document library toolbar will be modified to render the 'New Content' button linked to an action handler that will open the create-content form via the simple dialog mechanism. To do this, we will create a new toolbar component '/orbitz/components/documentlibrary/toolbar' that will augment the existing '/components/documentlibrary/toolbar' with minimal code replication. This is a bit dangerous since it is entirely possible that the document library toolbar will change over time, so perhaps overriding it in this way is not the best approach, however, I have tried to minimize the exposure to code change as much as possible. if others have a better approach, I would gladly accept it. The source for this new component will also be in the 'config/alfresco/site-webscripts' source folder in the 'com.orbitz.components.documentlibrary' package, with the following files:
toobar.get.desc.xml
This component description alters the url to '/orbitz/component/documentlibrary/toolbar' which will have to be mapped for the 'toolbar' the region of the documentlibary template.
<webscript>
<shortname>DocLib Toolbar</shortname>
<description>Document Library: Toolbar Component</description>
<url>/orbitz/components/documentlibrary/toolbar</url>
</webscript>

toolbar.get.head.ftl
This is a copy of the original 'toolbar.get.head.ftl' with the addition of DocListOrbitzToolbar Assets including 'orbitz.toolbar.css' and 'orbitz.toolbar.js' which provide custom styles and behavior for the 'New Content' button.
<!-- DocListOrbitzToolbar Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/components/documentlibrary/orbitz.toolbar.css" />
<script type="text/javascript" src="${page.url.context}/components/documentlibrary/orbitz.toolbar.js"></script>
<!-- DocListToolbar Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/components/documentlibrary/toolbar.css" />
<script type="text/javascript" src="${page.url.context}/components/documentlibrary/toolbar.js"></script>
<!-- Simple Dialog Assets -->
<script type="text/javascript" src="${page.url.context}/modules/simple-dialog.js"></script>
<!-- File-Upload Assets -->
<link rel="stylesheet" type="text/css" href="${page.url.context}/modules/flash-upload.css" />
<script type="text/javascript" src="${page.url.context}/modules/flash-upload.js"></script>
<link rel="stylesheet" type="text/css" href="${page.url.context}/modules/html-upload.css" />
<script type="text/javascript" src="${page.url.context}/modules/html-upload.js"></script>
<script type="text/javascript" src="${page.url.context}/modules/file-upload.js"></script>

toolbar.get.html.ftl
This is a copy of the original 'toolbar.get.html.ftl' with a new javascript block instantiating 'alfresco.DocListOrbitzToolbar' as well as 'Alfresco.DocListToolbar'. This new javascript object will be defined in the new javascript file 'orbitz.toolbar.js' referenced in 'toolbar.get.head.ftl' above. Also, the 'new-content' button is added beneath the 'new folder' button.

<script type="text/javascript">//<![CDATA[
new Alfresco.DocListToolbar("${args.htmlid}").setOptions(
{
siteId: "${page.url.templateArgs.site!""}"
}).setMessages(
${messages}
);
new Alfresco.DocListOrbitzToolbar("${args.htmlid}").setOptions(
{
siteId: "${page.url.templateArgs.site!""}"
}).setMessages(
${messages}
);
//]]></script>
<div id="${args.htmlid}-body" class="toolbar">

<div id="${args.htmlid}-headerBar" class="header-bar flat-button">
<div class="new-folder hideable DocListTree"><button id="${args.htmlid}-newFolder-button" name="newFolder">${msg("button.new-folder")}</button></div>
<div class="new-content hideable DocListTree"><button id="${args.htmlid}-newContent-button" name="newContent">${msg("button.new-content")}</button></div>
<div class="separator hideable DocListTree">&nbsp;</div>
<div class="file-upload hideable DocListTree"><button id="${args.htmlid}-fileUpload-button" name="fileUpload">${msg("button.upload")}</button></div>
<div class="separator hideable DocListTree">&nbsp;</div>
<div class="selected-items hideable DocListTree DocListFilter DocListTags">
<button class="no-access-check" id="${args.htmlid}-selectedItems-button" name="doclist-selectedItems-button">${msg("menu.selected-items")}</button>
<div id="${args.htmlid}-selectedItems-menu" class="yuimenu">
<div class="bd">
<ul>
<li><a rel="" href="#"><span class="onActionCopyTo">${msg("menu.selected-items.copy")}</span></a></li>
<li><a rel="" href="#"><span class="onActionMoveTo">${msg("menu.selected-items.move")}</span></a></li>
<li><a rel="delete" href="#"><span class="onActionDelete">${msg("menu.selected-items.delete")}</span></a></li>
<li><a type="document" rel="" href="#"><span class="onActionAssignWorkflow">${msg("menu.selected-items.assign-workflow")}</span></a></li>
<li><a rel="permissions" href="#"><span class="onActionManagePermissions">${msg("menu.selected-items.manage-permissions")}</span></a></li>
<li><hr/></li>
<li><a rel="" href="#"><span class="onActionDeselectAll">${msg("menu.selected-items.deselect-all")}</span></a></li>
</ul>
</div>
</div>
</div>
<div class="rss-feed"><button id="${args.htmlid}-rssFeed-button" name="rssFeed">${msg("link.rss-feed")}</button></div>
</div>

<div id="${args.htmlid}-navBar" class="nav-bar flat-button">
<div class="folder-up hideable DocListTree"><button class="no-access-check" id="${args.htmlid}-folderUp-button" name="folderUp">${msg("button.up")}</button></div>
<div class="separator hideable DocListTree">&nbsp;</div>
<div id="${args.htmlid}-breadcrumb" class="breadcrumb hideable DocListTree"></div>
<div id="${args.htmlid}-description" class="description hideable DocListFilter DocListTags"></div>
</div>

</div>


toobar.get.properties
This is a copy of the original toolbar.get.properties with only a new couple of new properties:
  • button.new-content
  • message.new-content
## Buttons
button.delete=Delete
button.new-folder=New Folder
button.new-content=New Content
button.up=Up
button.upload=Upload

## Links
link.rss-feed=RSS Feed

## Drop-down Menus
menu.selected-items=Selected Items...
menu.selected-items.copy=Copy to...
menu.selected-items.move=Move to...
menu.selected-items.delete=Delete
menu.selected-items.assign-workflow=Assign Workflow...
menu.selected-items.manage-permissions=Manage Permissions...
menu.selected-items.deselect-all=Deselect All

## Pop-up Messages
message.new-folder.success=Folder '{0}' created
message.new-content.success=Content '{0}' created
message.new-folder.failure=Could not create '{0}'
message.new-content.failure=Could not create '{0}'
message.multiple-delete.success=Successfully deleted {0} item(s)
message.multiple-delete.failure=Could not delete items
title.multiple-delete.confirm=Confirm Multiple Delete
message.multiple-delete.confirm=Are you sure you want to delete the following {0} items?
message.multiple-delete.please-wait=Please wait. Files being deleted...

## Toolbar Modes
description.path=
description.path.more=
description.all=All Documents in the Document Library
description.all.more=
description.editingMe=Documents I'm Editing
description.editingMe.more=(working copies)
description.editingOthers=Documents Others are Editing
description.editingOthers.more=(working copies)
description.recentlyModified=Documents Recently Modified
description.recentlyModified.more=
description.recentlyAdded=Documents Added Recently
description.recentlyAdded.more=
description.tag=Documents and Folders Tagged with
description.tag.more={0}
Step 3, create the javascript to support the toolbar

Within the 'source/web' source folder 'components.documentlibrary' package, the new javascript and css assets referenced in 'toolbar.get.head.ftl' are defined:

orbitz.toolbar.js
This javascript creates a new object: 'Alfresco.DocListOrbitzToolbar' instantiated in 'toolbar.get.html.ftl' that decorates the 'newContent' button as a YUI button mapped to the 'onNewContent' action handler to open the new-content form using the SimpleDialog mechanism
/**
* DocumentList Toolbar component.
*
* @namespace Alfresco
* @class Alfresco.DocListToolbar
*/
(function()
{
/**
* YUI Library aliases
*/
var Dom = YAHOO.util.Dom,
Event = YAHOO.util.Event,
Element = YAHOO.util.Element;

/**
* Alfresco Slingshot aliases
*/
var $html = Alfresco.util.encodeHTML;

/**
* DocListToolbar constructor.
*
* @param {String} htmlId The HTML id of the parent element
* @return {Alfresco.DocListToolbar} The new DocListToolbar instance
* @constructor
*/
Alfresco.DocListOrbitzToolbar = function(htmlId)
{
// Mandatory properties
this.name = "Alfresco.DocListOrbitzToolbar";
this.id = htmlId;

// Initialise prototype properties
this.widgets = {};
this.modules = {};
this.selectedFiles = [];
this.currentFilter =
{
filterId: "",
filterOwner: "",
filterData: ""
};

// Register this component
Alfresco.util.ComponentManager.register(this);

// Load YUI Components
Alfresco.util.YUILoaderHelper.require(["button", "menu", "container"], this.onComponentsLoaded, this);

// Decoupled event listeners
//YAHOO.Bubbling.on("pathChanged", this.onPathChanged, this);
//YAHOO.Bubbling.on("folderRenamed", this.onPathChanged, this);
//YAHOO.Bubbling.on("filterChanged", this.onFilterChanged, this);
YAHOO.Bubbling.on("deactivateAllControls", this.onDeactivateAllControls, this);
//YAHOO.Bubbling.on("selectedFilesChanged", this.onSelectedFilesChanged, this);
YAHOO.Bubbling.on("userAccess", this.onUserAccess, this);

return this;
};

Alfresco.DocListOrbitzToolbar.prototype =
{
/**
* Object container for initialization options
*
* @property options
* @type object
*/
options:
{
/**
* Current siteId.
*
* @property siteId
* @type string
*/
siteId: "",

/**
* ContainerId representing root container
*
* @property containerId
* @type string
* @default "documentLibrary"
*/
containerId: "documentLibrary",

/**
* Number of multi-file uploads before grouping the Activity Post
*
* @property groupActivitiesAt
* @type int
* @default 5
*/
groupActivitiesAt: 5,

/**
* Flag indicating whether navigation bar is visible or not.
*
* @property hideNavBar
* @type boolean
*/
hideNavBar: false
},

/**
* Current path being browsed.
*
* @property currentPath
* @type string
*/
currentPath: "",

/**
* Current filter to choose toolbar view and populate description.
*
* @property currentFilter
* @type string
*/
currentFilter: null,

/**
* FileUpload module instance.
*
* @property fileUpload
* @type Alfresco.module.FileUpload
*/
fileUpload: null,

/**
* Object container for storing YUI widget instances.
*
* @property widgets
* @type object
*/
widgets: null,

/**
* Object container for storing module instances.
*
* @property modules
* @type object
*/
modules: null,

/**
* Array of selected states for visible files.
*
* @property selectedFiles
* @type array
*/
selectedFiles: null,

/**
* Set multiple initialization options at once.
*
* @method setOptions
* @param obj {object} Object literal specifying a set of options
* @return {Alfresco.DocListToolbar} returns 'this' for method chaining
*/
setOptions: function DLTB_setOptions(obj)
{
this.options = YAHOO.lang.merge(this.options, obj);
return this;
},

/**
* Set messages for this component.
*
* @method setMessages
* @param obj {object} Object literal specifying a set of messages
* @return {Alfresco.DocListToolbar} returns 'this' for method chaining
*/
setMessages: function DLTB_setMessages(obj)
{
Alfresco.util.addMessages(obj, this.name);
return this;
},

/**
* Fired by YUILoaderHelper when required component script files have
* been loaded into the browser.
*
* @method onComponentsLoaded
*/
onComponentsLoaded: function DLTB_onComponentsLoaded()
{
Event.onContentReady(this.id, this.onReady, this, true);
},

/**
* Fired by YUI when parent element is available for scripting.
* Component initialisation, including instantiation of YUI widgets and event listener binding.
*
* @method onReady
*/
onReady: function DLTB_onReady()
{

// New Content button: user needs "create" access
this.widgets.newContent = Alfresco.util.createYUIButton(this, "newContent-button", this.onNewContent,
{
disabled: true,
value: "create"
});

// Finally show the component body here to prevent UI artifacts on YUI button decoration
//Dom.setStyle(this.id + "-body", "visibility", "visible");
},


/**
* YUI WIDGET EVENT HANDLERS
* Handlers for standard events fired from YUI widgets, e.g. "click"
*/



/**
* New Content button click handler
*
* @method onNewContent
* @param e {object} DomEvent
* @param p_obj {object} Object passed back from addListener method
*/
onNewContent: function DLTB_onNewContent(e, p_obj)
{
var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "slingshot/doclib/action/folder/site/{site}/{container}/{path}",
{
site: this.options.siteId,
container: this.options.containerId,
path: this.currentPath
});

var doSetupFormsValidation = function DLTB_oNF_doSetupFormsValidation(p_form)
{
// Validation
// Name: mandatory value
p_form.addValidation(this.id + "-createContent-name", Alfresco.forms.validation.mandatory, null, "keyup");
// Name: valid filename
p_form.addValidation(this.id + "-createContent-name", Alfresco.forms.validation.nodeName, null, "keyup");
p_form.setShowSubmitStateDynamically(true, false);
};

if (!this.modules.createContent)
{
this.modules.createContent = new Alfresco.module.SimpleDialog(this.id + "-createContent").setOptions(
{
width: "30em",
templateUrl: Alfresco.constants.URL_SERVICECONTEXT + "components/documentlibrary/create-content",
actionUrl: actionUrl,
doSetupFormsValidation:
{
fn: doSetupFormsValidation,
scope: this
},
firstFocus: this.id + "-createContent-name",
onSuccess:
{
fn: function DLTB_onCreateContent_callback(response)
{
var file = response.json.results[0];
YAHOO.Bubbling.fire("folderCreated",
{
name: file.name,
parentPath: file.parentPath,fileCopied
nodeRef: file.nodeRef
});
Alfresco.util.PopupManager.displayMessage(
{
text: this._msg("message.new-content.success", file.name)
});
},
scope: this
}
});
}
else
{
this.modules.createContent.setOptions(
{
actionUrl: actionUrl,
clearForm: true
});
}
this.modules.createContent.show();
},
/**
* Deactivate All Controls event handler
*
* @method onDeactivateAllControls
* @param layer {object} Event fired
* @param args {array} Event parameters (depends on event type)
*/
onDeactivateAllControls: function DLTB_onDeactivateAllControls(layer, args)
{
var widget;
for (widget in this.widgets)
{
if (this.widgets.hasOwnProperty(widget))
{
this.widgets[widget].set("disabled", true);
}
}
},

/**
* User Access event handler
*
* @method onUserAccess
* @param layer {object} Event fired
* @param args {array} Event parameters (depends on event type)
*/
onUserAccess: function DLTB_onUserAccess(layer, args)
{
var obj = args[1];
if ((obj !== null) && (obj.userAccess !== null))
{
var widget, widgetPermissions, index;
for (index in this.widgets)
{
if (this.widgets.hasOwnProperty(index))
{
widget = this.widgets[index];
if (widget.get("srcelement").className != "no-access-check")
{
widget.set("disabled", false);
if (widget.get("value") !== null)
{
widgetPermissions = widget.get("value").split(",");
for (var i = 0, ii = widgetPermissions.length; i < ii; i++)
{
if (!obj.userAccess[widgetPermissions[i]])
{
widget.set("disabled", true);
break;
}
}
}
}
}
}
}
},

/**
* Gets a custom message
*
* @method _msg
* @param messageId {string} The messageId to retrieve
* @return {string} The custom message
* @private
*/
_msg: function DLTB__msg(messageId)
{
return Alfresco.util.message.call(this, messageId, "Alfresco.DocListOrbitzToolbar", Array.prototype.slice.call(arguments).slice(1));
}

};
})();
This script at first will be configured to run the web script to create a new folder:
var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "slingshot/doclib/action/folder/site/{site}/{container}/{path}",
In the following steps, we will create our own script that will create a new file.

orbitz.toolbar.css

This css defines the images and style used for the 'newContent' button:
.toolbar .new-content button
{
background: transparent url(images/content-new-16.png) no-repeat 12px 4px;
padding-left: 32px;
}

.toolbar .new-content .yui-button-disabled button
{
background-image: url(images/content-new-disabled-16.png);
}
these images are copies of the slingshot project's 'source/web/components/images/edit-blog-16.png' image.

Step 4 - Create the component to map to the 'toolbar' region of the document library template

Finally in the 'config/alfresco/site-data' source folder in the 'components' package, we override the existing 'toolbar' region in the 'documentlibrary' template to reference the /orbitz/components/documentlibrary/toolbar' component:

template.toolbar.documentlibrary.xml
<?xml version='1.0' encoding='UTF-8'?>
<component>
<scope>template</scope>
<region-id>toolbar</region-id>
<source-id>documentlibrary</source-id>
<url>/orbitz/components/documentlibrary/toolbar</url>
</component>
Step 5 - Deploy and Test
First, we need to make sure the 'share' war is installed and expanded in the tomcat host. I use the Alfresco project build.xml's
ant incremental-slingshot-tomcat-exploded
But for a war distribtuion, just make sure it is run at least once.

To use the build.xml, make sure to set APP_TOMCAT_HOME and TOMCAT_HOME environment variables as recommended in the previous blog article: Extending Share 1 - Creating a Share Extension Project

To build and deploy the share webapp expanded in the tomcat directory. Now we can deploy our extension code from the 'deals-share-extension' project's build.xml file:
ant deploy
This will create the 'deals-share-ext.jar' jar file and copy to the share/WEB-INF/lib directory, copy the configuration files from the config/alfresco/** source folders to the share/weB-INF/classes directory, and copy the web assets from the source/web source folders to the share root directory.

We can test our toolbar code directly using the url: http://localhost:8080/share/service/orbitz/components/documentlibrary/toolbar?htmlid , but this will not render with CSS, and without CSS, the YUI components wont work.

Now we can test within the Share:
  1. Launch share http://localhost:8080/share, opens to the share dashboard
  2. Create and/or select a Site, opens to the site dashboard
  3. Select the 'document library' section should render our new toolbar with the 'New Content' button
  4. Press the 'New Content' button and the 'create-content' form should open
  5. Enter the new content's name, title, description and content int he form and press 'ok' and the dialog should close, but instead of a file created, we should see a new folder created. In the next steps we will create the web script necessary to create a new file.
Step 6 - Creating a custom web script action

Now we need to create a webscript service to use instead of the slingshot/doclib/action/folder service. for this blog, I will extend the action services to implement the following url pattern:

POST slingshot/doclib/action/file/site/{site}/{container}/{path}

To do this, we will add a new webscript service to the remote alfresco.war by uploading it to the Data Dictionary space. Later, we can separate this to our own amp module. As a starting point, we will copy the folder.post.* files to create file.post.* files in Remote API/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/action directory to create the following files:
  • file.post.desc.xml
  • file.post.json.ftl
  • file.post.json.js
Now we can modify these files to do the work of creating a file instead of a folder. for 'file.post.desc.xml' we modify the url pattern to the following:
<webscript>
<shortname>folder</shortname>
<description>Document List Action - Create folder</description>
<url>/slingshot/doclib/action/file/site/{site}/{container}</url>
<url>/slingshot/doclib/action/file/site/{site}/{container}/{path}</url>
<format default="json">argument</format>
<authentication>user</authentication>
<transaction>required</transaction>
</webscript>

file.post.json.ftl will be changed, it imports the action.lib.ftl to create a standard results response json format. But I will combine the import. Its contents are the following:
<#macro resultsJSON results>
<#escape x as jsonUtils.encodeJSONString(x)>
{
"totalResults": ${results?size},
"overallSuccess": ${overallSuccess?string},
"successCount": ${successCount},
"failureCount": ${failureCount},
"results":
[
<#list results as r>
{
<#list r?keys as key>
<#assign value = r[key]>
<#if value?is_number || value?is_boolean>
"${key}": ${value?string}<#if key_has_next>,</#if>
<#else>
"${key}": "${value}"<#if key_has_next>,</#if>
</#if>
</#list>
}<#if r_has_next>,</#if>
</#list>
]
}
</#escape>
</#macro>
<@resultsJSON results=results />

file.post.json.js needs to be modified to handle a file instead of a folder.

The main changes I will make to this script:
  1. get a 'content' field from the json request object from the create-content form that we added previously.
     if (!json.isNull("content"))
    {
    content = json.get("content");
    }
  2. Rename variables folderName, folderDescription, folderTitle, folderPath to fileName, fileDescription, fileTitle, fileDescription and filePath respectively.
  3. Create a file node instead of a folder node
    var fileNode = parentNode.createFile(fileName);

  4. set the content on the newly created fileNode variable arfter the fileNode.save(); method
          fileNode.save();
    // Add uifacets aspect for the web client
    fileNode.content = content;
  5. Change messaging to reflect saving a file, not a folder.
The completed file.post.json.js looks like this:
/**
* Document List Component: action
*
* For a single-asset action, template paramters address the asset.
* For multi-asset actions, template parameters address the source or destination node,
* and a JSON body addresses the assets involved in the action.
* (note: HTTP DELETE methods must use URI)
*
* @param uri {string} site/{siteId}/{containerId}/{filepath} : full path to file or folder name involved in the action
* @param uri {string} node/{store_type}/{store_id}/{id}/{filepath} : full path to file or folder name involved in the action
*/

/**
* Main script entry point
* @method main
*/
function main()
{
// Params object contains commonly-used arguments
var params = {};
var files, rootNode;

if (url.templateArgs.store_type != undefined)
{
params = getNodeRefInputParams();
}
else if (url.templateArgs.site != undefined)
{
params = getSiteInputParams();
}
if (typeof params == "string")
{
status.setCode(status.STATUS_BAD_REQUEST, params);
return;
}

// Resolve path if available
var path = url.templateArgs.path;
// Path might be null for the root folder
if (!path)
{
path = "";
}
// Remove any leading or trailing "/" from the path
// Fix-up parent path to have no leading or trailing slashes
if (path.length > 0)
{
var aPaths = path.split("/");
while (aPaths[0] === "")
{
aPaths.shift();
}
while (aPaths[aPaths.length-1] === "")
{
aPaths.pop();
}
path = aPaths.join("/");
}
params.path = path;

// Multiple input files in the JSON body?
files = getMultipleInputValues("nodeRefs");
if (typeof files != "string")
{
params.files = files;
}

// Check runAction function is provided the action's webscript
if (typeof runAction != "function")
{
status.setCode(status.STATUS_BAD_REQUEST, "Action webscript must provide runAction() function.");
return;
}

// Actually run the action
var results = runAction(params);
if ((results !== null) && (results !== undefined))
{
if (typeof results == "string")
{
status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, results);
}
else if (typeof results.status == "object")
{
// Status fields have been manually set
status.redirect = true;
for (var s in results.status)
{
status[s] = results.status[s];
}
}
else
{
/**
* NOTE: Webscripts run within one transaction only.
* If a single operation fails, the transaction is marked for rollback and all
* previous (successful) operations are also therefore rolled back.
* We therefore need to scan the results for a failed operation and mark the entire
* set of operations as failed.
*/
var overallSuccess = true;
var successCount = 0;
var failureCount = 0;
for (var i = 0, j = results.length; i < j; i++)
{
overallSuccess = overallSuccess && results[i].success;
results[i].success ? ++successCount : ++failureCount;
}
model.overallSuccess = overallSuccess;
model.successCount = successCount;
model.failureCount = failureCount;
model.results = results;
}
}
}


/**
* Get and check existence of mandatory input parameters (Site-based)
*
* @method getSiteInputParams
* @return {object|string} object literal containing parameters value or string error
*/
function getSiteInputParams()
{
var params = {};
var error = null;
var template = url.template;

try
{
var siteId, containerId, sideNode, rootNode;

// Try to get the parameters from the URI
siteId = url.templateArgs.site;
containerId = url.templateArgs.container;

// SiteId
if (template.indexOf("{site}") != -1)
{
if ((siteId === null) || (siteId.length === 0))
{
return "'site' parameter is missing.";
}

// Find the site
siteNode = siteService.getSite(siteId);
if (siteNode === null)
{
return "Site '" + siteId + "' not found.";
}

// ContainerId
if (template.indexOf("{container}") != -1)
{
if ((containerId === null) || (containerId.length === 0))
{
return "'container' parameter is missing.";
}

// Find the component container
var rootNode = siteNode.getContainer(containerId);
if (rootNode === null)
{
rootNode = siteNode.createContainer(containerId);
if (rootNode === null)
{
return "Component container '" + containerId + "' not found in '" + siteId + "'.";
}
}
}

// Populate the return object
params =
{
usingNodeRef: false,
siteId: siteId,
containerId: containerId,
siteNode: siteNode,
rootNode: rootNode
}
}
}
catch(e)
{
error = e.toString();
}

// Return the params object, or the error string if it was set
return (error !== null ? error : params);
}

/**
* Get and check existence of mandatory input parameters (nodeRef-based)
*
* @method getNodeRefInputParams
* @return {object|string} object literal containing parameters value or string error
*/
function getNodeRefInputParams()
{
var params = {};
var error = null;

try
{
// First try to get the parameters from the URI
var storeType = url.templateArgs.store_type;
var storeId = url.templateArgs.store_id;
var id = url.templateArgs.id;

var nodeRef = storeType + "://" + storeId + "/" + id;
var rootNode = null;

if (nodeRef == "alfresco://company/home")
{
rootNode = companyhome;
}
else if (nodeRef == "alfresco://user/home")
{
rootNode = userhome;
}
else
{
rootNode = search.findNode(nodeRef);

if (rootNode === null)
{
return "'" + nodeRef + "' is not a valid nodeRef.";
}
}

// Populate the return object
params =
{
usingNodeRef: true,
nodeRef: nodeRef,
rootNode: rootNode
}
}
catch(e)
{
error = e.toString();
}

// Return the params object, or the error string if it was set
return (error !== null ? error : params);
}

/**
* Get multiple input values
*
* @method getMultipleInputValues
* @return {array|string} Array containing multiple values, or string error
*/
function getMultipleInputValues(param)
{
var values = [];
var error = null;

try
{
// Was a JSON parameter list supplied?
if (typeof json == "object")
{
if (!json.isNull(param))
{
var jsonValues = json.get(param);
// Convert from JSONArray to JavaScript array
for (var i = 0, j = jsonValues.length(); i < j; i++)
{
values.push(jsonValues.get(i));
}
}
}
}
catch(e)
{
error = e.toString();
}

// Return the values array, or the error string if it was set
return (error !== null ? error : values);
}


/**
* Obtain the asset node for the given rootNode and filepath
*
* @method getAssetNode
* @param p_rootNode {object} valid repository node
* @param p_assetPath {string} rootNode-relative path to asset
* @return {object|string} valid repository node or string error
*/
function getAssetNode(p_rootNode, p_assetPath)
{
var assetNode = p_rootNode;
var error = null;

try
{
if (p_assetPath && (p_assetPath.length > 0))
{
assetNode = assetNode.childByNamePath(p_assetPath);
}

if (assetNode === null)
{
return "Asset '" + p_assetPath + " not found.";
}
}
catch(e)
{
error = e.toString();
}

// Return the node object, or the error string if it was set
return (error !== null ? error : assetNode);
}

/**
* Create file action
* @method POST
* @param uri {string} /{siteId}/{containerId}/{filepath}
* @param json.name {string} New file name
* @param json.title {string} Title metadata
* @param json.description {string} Description metadata
* @param json.content {string} Content of file
*/

/**
* Entrypoint required by action.lib.js
*
* @method runAction
* @param p_params {object} common parameters
* @return {object|null} object representation of action result
*/
function runAction(p_params)
{
var results;

try
{
// Mandatory: json.name
if (json.isNull("name"))
{
status.setCode(status.STATUS_BAD_REQUEST, "File name is a mandatory parameter.");
return;
}
var fileName = json.get("name");

var parentPath = p_params.path;
var filePath = parentPath + "/" + fileName;

// Check file doesn't already exist
var existsNode = getAssetNode(p_params.rootNode, filePath);
if (typeof existsNode == "object")
{
status.setCode(status.STATUS_BAD_REQUEST, "File '" + filePath + "' already exists.");
return;
}

// Check parent exists
var parentNode = getAssetNode(p_params.rootNode, parentPath);
if (typeof parentNode == "string")
{
status.setCode(status.STATUS_NOT_FOUND, "Parent folder '" + parentPath + "' not found.");
return;
}

// Title and description
var fileTitle = "";
var fileDescription = "";
if (!json.isNull("title"))
{
fileTitle = json.get("title");
}
if (!json.isNull("description"))
{
fileDescription = json.get("description");
}
if (!json.isNull("content"))
{
content = json.get("content");
}
// Create the folder and apply metadata
var fileNode = parentNode.createFile(fileName);
// Always add title & description, default icon
fileNode.properties["cm:title"] = fileTitle;
fileNode.properties["cm:description"] = fileDescription.substr(0, 100);
fileNode.properties["app:icon"] = "space-icon-default";
fileNode.save();
// Add uifacets aspect for the web client
fileNode.content = content;
fileNode.addAspect("app:uifacets");

// Construct the result object
results = [
{
id: filePath,
name: fileName,
parentPath: parentPath,
nodeRef: fileNode.nodeRef.toString(),
action: "createFile",
success: true
}];
}
catch(e)
{
status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, e.toString());
return;
}

return results;
}

/* Bootstrap action script */
main();

This service uses the action.lib.js and action.lib.ftl libraries to simplify coding of similar functions, including: checkin, checkout, cancel-checkout, copy-to, move-to, file or folder delete and others. The action.lib.js is included in the folder.post.json.js script and provides a main() entry point to do common setup, clean up and host shared functions. The main entry point calls the runAction() function defined in the specific action script 'folder.post.json.js', effectively implementing a strategy pattern.

Step 7 - upload, build and test
Log into the alfresco server as admin. Navigate to Company Home > Data Dictionary > Web Scripts > org > alfresco

Create a new directory 'doclib' and copy the three files in to this directory: 'file.post.desc.xml', 'file.post.json.ftl', and 'file.post.json.js'.
Navigate to http://localhost:8080/alfresco/service/index and select 'refresh webscripts' button.

To view the new service, use the url http://localhost:8080/alfresco/service/index/uri/slingshot/doclib/action/file/site/%7Bsite%7D/%7Bcontainer%7D which should show the service correctly added.

Step 8 - alter 'New Content' button action to invoke new service
Now we have to use this new service we have created when we press the 'new content' button orbitz-toolbar.js onNewContent action line to read:
         var actionUrl = YAHOO.lang.substitute(Alfresco.constants.PROXY_URI + "slingshot/doclib/action/file/site/{site}/{container}/{path}",

this will call our new slingshot service to create a file, which in turn will call the new remote api service we added to alfresco.

Step 9 - deploy and test
Now, you should be able to press 'New Content' in share Document Library and create a new content item.

2 comments:

langust said...

Are provided sources working? Or I should go through this tutorial and modify them at first?

Ryan said...

Has anyone successfully deployed this code?

I've found at least one error in the code written in the blog, but I don't see the same error in the packaged code.

I've deployed the code from the attached package and firebug is showing the following error:

"this._events is undefined"

on line:

"}this.constrainY=true;},resetConstraints...dTo",{type:"beforeAppendTo",target:E});\n"

in file:
utilities.js


obviously this is a vague effect error and not the cause. I'm still trying to debug the code to figure out what's up.

The error is preventing the the document library "frame" from rendering on the Document Library page.

version 3.2 Alfresco Share.