← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 14401: Merge from Mark Polak. Implements new menu solution with customizable order of apps.

 

Merge authors:
  Mark Polak (markpo)
------------------------------------------------------------
revno: 14401 [merge]
committer: Lars Helge Øverland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Tue 2014-03-25 13:28:24 +0100
message:
  Merge from Mark Polak. Implements new menu solution with customizable order of apps.
added:
  dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/MenuController.java
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/menu.css
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.menu.js
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.translate.js
modified:
  dhis-2/dhis-services/dhis-service-i18n/src/main/resources/i18n_global.properties
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/about/modules.vm
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/main.vm
  dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/menu/action/GetModulesAction.java
  dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/module/Module.java
  dhis-2/pom.xml


--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk

Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-services/dhis-service-i18n/src/main/resources/i18n_global.properties'
--- dhis-2/dhis-services/dhis-service-i18n/src/main/resources/i18n_global.properties	2014-03-24 13:22:36 +0000
+++ dhis-2/dhis-services/dhis-service-i18n/src/main/resources/i18n_global.properties	2014-03-24 21:39:15 +0000
@@ -768,3 +768,15 @@
 
 recover_success_message=Please check the email account which you registered for this username. We have sent you instructions on how to restore your password.	
 recover_error_message=Sorry, we were not able to restore your account. The user name might be invalid, your account might not permit restore or you might have entered an invalid email address for your account.
+
+#-- App Menu ----------------------------------------------------------------#
+applications=Apps
+more_applications=More apps
+app_manager=Apps
+app_favorite_description=This icon indicates an app that will be displayed in your dropdown menu.
+app_draggable_description=Items are draggable to change their order, so it's easy to customize your favorite apps.
+
+app_order_custom=My own order (Custom)
+
+app_order_name_asc=Name (Ascending)
+app_order_name_desc=Name (Descending)

=== added file 'dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/MenuController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/MenuController.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/MenuController.java	2014-03-25 12:28:24 +0000
@@ -0,0 +1,71 @@
+package org.hisp.dhis.api.controller;
+
+/*
+ * Copyright (c) 2004-2013, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import org.hisp.dhis.dxf2.utils.JacksonUtils;
+import org.hisp.dhis.user.CurrentUserService;
+import org.hisp.dhis.user.User;
+import org.hisp.dhis.user.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.io.InputStream;
+import java.util.List;
+
+@Controller
+@RequestMapping( value = MenuController.RESOURCE_PATH )
+public class MenuController
+{
+    public static final String RESOURCE_PATH = "/menu";
+
+    @Autowired
+    private CurrentUserService currentUserService;
+
+    @Autowired
+    private UserService userService;
+
+    @RequestMapping( method = RequestMethod.POST, consumes = "application/json" )
+    @ResponseStatus( value = HttpStatus.NO_CONTENT )
+    public void saveMenuOrder( InputStream input )
+        throws Exception
+    {
+        List<String> apps = JacksonUtils.fromJson( input, List.class );
+
+        User user = currentUserService.getCurrentUser();
+
+        user.getApps().clear();
+        user.getApps().addAll( apps );
+
+        userService.updateUser( user );
+    }
+}

=== modified file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/about/modules.vm'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/about/modules.vm	2013-09-26 18:17:41 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/about/modules.vm	2014-03-19 23:35:11 +0000
@@ -1,10 +1,15 @@
 
-<ul class="moduleList">
-#foreach( $module in $menuModules )
-#if ( $module.icon ) ## detect if app
-	#introListImgApp( $module )
-#else
-	#introListImgItem( "${module.defaultAction}" "${module.name}" "${module.name}" )
-#end
-#end
-</ul>
\ No newline at end of file
+<div class="app-manager-header ui-helper-clearfix">
+    <h3>$i18n.getString( "app_manager")</h3>
+    <p>
+      <i class="fa fa-bookmark"></i> $i18n.getString( "app_favorite_description" )<br>
+      <i class="fa fa-arrows"></i> $i18n.getString( "app_draggable_description" )
+    </p>
+    <select id="menuOrderBy">
+        <option selected value="custom">-- $i18n.getString( "app_order_custom" ) --</option>
+        <option value="name-asc">$i18n.getString( "app_order_name_asc" )</option>
+        <option value="name-desc">$i18n.getString( "app_order_name_desc" )</option>
+    </select>
+</div>
+
+<div id="appsMenu"></div>

=== added file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/menu.css'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/menu.css	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/menu.css	2014-03-25 10:25:49 +0000
@@ -0,0 +1,264 @@
+.app-menu:after {
+    clear: both;
+    content: "";
+}
+
+.app-menu ul,
+.app-menu li {
+    list-style: none;
+}
+
+.app-manager-header,
+.app-menu {
+    margin: 0 auto;
+    position: relative;
+    width: 808px;
+}
+
+.app-menu li {
+    float: left;
+    position: relative;
+}
+
+#menuOrderBy {
+    float: right;
+}
+
+.app-menu .app-menu-item,
+.app-menu .app-menu-placeholder {
+    margin: 10px;
+    display: block;
+    border: 1px solid lightgray;
+    background: rgba(245, 245, 245, .9);
+    width: 225px;
+    height: 56px;
+    border-radius: 5px;
+    padding: 10px;
+    box-shadow: 1px 1px 7px lightgray;
+}
+
+.app-menu .app-menu-item img {
+    max-height: 36px;
+    max-width: 36px;
+    display: block;
+    left: 25px;
+    top: 29px;
+    position: absolute;
+}
+
+.app-menu .app-menu-item span {
+    font-size: 1.25em;
+    padding-left: 48px;
+    display: block;
+    padding-top: 15px;
+}
+
+.app-menu .app-menu-placeholder,
+.app-menu .app-menu-item:hover {
+    border: 1px dotted rgba(0, 0, 0, .2);
+}
+
+.app-menu-item-description {
+    display: none;
+}
+
+.app-menu-item-description .fa.fa-arrows {
+    font-size: 1.3em;
+    position: absolute;
+    right: 5px;
+    top: 5px;
+}
+
+/**
+ * When hovered display the description and hide the favorites icon
+ */
+.app-menu li:hover a .app-menu-item-description {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    background: #276696;
+    display: block;
+    margin: 10px;
+    padding: 5px 10px;
+    font-size: 1em;
+    color: #fff;
+    border-radius: 5px;
+}
+
+.app-menu-item .app-menu-item-description span {
+    color: #7fc9fd;
+    display: block;
+    font-size: 1em;
+    font-weight: bold;
+    padding: 0;
+    padding-bottom: 5px;
+    margin: 0;
+}
+
+.app-menu li:hover a .fa.fa-bookmark {
+    display: none;
+}
+
+.app-menu a.app-menu-item .fa.fa-bookmark {
+    font-size: 2em;
+    position: absolute;
+    right: 15px;
+    top: 10px;
+}
+
+/**
+ * IE8 hack to display all items after the 9th item differently
+ * TODO: When support for IE8 is dropped this can be changed to a nth style selector
+ */
+.app-menu li + li + li + li + li + li + li + li + li + li a.app-menu-item .fa.fa-bookmark {
+    display: none;
+}
+
+/**
+ * App menu item
+ */
+.menuLink {
+    width: 105px;
+}
+
+.menuLink i.fa {
+    font-size: 2em;
+    padding-right: 10px;
+    position: relative;
+    top: 5px;
+}
+
+#menuLink1 {
+    left: 762px;
+}
+
+/**
+ * App menu dropdown box
+ */
+.app-menu-dropdown {
+    background-color: #fff;
+    background-color: rgba(255, 255, 255, 0.95);
+    border: 1px solid #bbb;
+    border-radius: 2px;
+    box-shadow: 0px 0px 3px #bbb;
+    color: #000;
+    opacity: 1;
+    overflow-y: inherit;
+    padding: 10px;
+    width: 360px;
+}
+
+.app-menu-dropdown ul {
+    margin: 0;
+}
+
+.app-menu-dropdown li {
+    float: left;
+}
+
+.app-menu-dropdown img {
+    padding-left: 36px;
+    padding-right: 36px;
+    padding-top: 15px;
+    padding-bottom: 5px;
+}
+
+.app-menu-dropdown span {
+    display: block;
+    height: 30px;
+    padding-left: 10px;
+    padding-right: 10px;
+    text-align: center;
+    width: 100px;
+}
+
+.app-menu-dropdown .caret-up-background,
+.app-menu-dropdown .caret-up-border {
+    border-left: 10px solid transparent;
+    border-right: 10px solid transparent;
+    width: 0;
+    height: 0;
+    position: absolute;
+}
+
+.app-menu-dropdown .caret-up-background {
+    border-bottom: 10px solid #fff;
+    top: -9px;
+}
+
+.app-menu-dropdown .caret-up-border {
+    border-bottom: 10px solid #bbb;
+    top: -10px;
+}
+
+.app-menu-dropdown a.app-menu-item {
+    color: #000;
+    display: block;
+    height: 110px;
+    padding: 0;
+    width: 120px;
+}
+
+.app-menu-dropdown a.app-menu-item:hover span {
+    color: #fff;
+    padding-left: 10px;
+    padding-right: 10px;
+    text-align: center;
+    width:100px;
+}
+
+.apps-menu-more {
+    display: table;
+    height: 30px;
+    padding-top: 10px;
+    text-align: center;
+    width: 100%;
+}
+
+.apps-menu-more a {
+    color: #4A89BA;
+    border: 1px solid #4A89BA;
+    border-radius: 5px;
+    display: table-cell;
+    vertical-align: middle;
+}
+
+.apps-menu-more a:hover {
+    color: #fff;
+    background-color: #4A89BA;
+}
+
+/**
+ * Overrides
+ * TODO: Merge these with the main stylesheet
+ */
+#menuDropDown1 {
+    top: 55px;
+    left: 603px;
+}
+
+#menuDropDown3 {
+    top: 55px;
+    left: 733px;
+}
+
+hr.app-separator {
+    border: none;
+    border-top: 1px solid #4A89BA;
+    height: 1px;
+    position: absolute;
+    top: 288px;
+    width: 100%;
+}
+
+#menuDropDown1 .caret-up-background,
+#menuDropDown1 .caret-up-border {
+    left: 182px;
+}
+
+#menuDropDown3 .caret-up-background,
+#menuDropDown3 .caret-up-border {
+    left: 182px;
+}

=== added file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.menu.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.menu.js	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.menu.js	2014-03-24 21:37:25 +0000
@@ -0,0 +1,467 @@
+"use strict";
+/*
+ * Copyright (c) 2004-2014, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Created by Mark Polak on 28/01/14.
+ *
+ * @see Underscore.js (http://underscorejs.org)
+ */
+(function (dhis2, _, undefined) {
+    var MAX_FAVORITES = 9,
+        /**
+         * Object that represents the list of menu items
+         * and managers the order of the items to be saved.
+         */
+        menuItemsList = (function () {
+            var menuOrder = [],
+                menuItems = {};
+
+            return {
+                getItem: function (key) {
+                    return menuItems[key];
+                },
+                setItem: function (key, item) {
+                    menuOrder.push(key);
+                    menuItems[key] = item;
+                },
+                list: function () {
+                    var result = [];
+
+                    menuOrder.forEach(function (element, index, array) {
+                        result.push(menuItems[element]);
+                    });
+
+                    return result;
+                },
+                setOrder: function (order) {
+                    menuOrder = order;
+                },
+                getOrder: function () {
+                    return menuOrder;
+                }
+            }
+        })();
+
+    dhis2.util.namespace( 'dhis2.menu' );
+
+    dhis2.menu = function () {
+        var that = {},
+            menuReady = false,
+            menuItems = menuItemsList,
+            callBacks = [], //Array of callbacks to call when serviced is updated
+            onceCallBacks = [];
+
+        /***********************************************************************
+         * Private methods
+         **********************************************************************/
+
+        function processTranslations(translations) {
+            var itemIndex,
+                items = dhis2.menu.getApps();
+
+            items.forEach(function (element, index, items) {
+                if (element.id && translations[element.id]) {
+                    items[index].name = translations.get(element.id);
+                }
+                if (element.description === '' && translations.get('intro_' + element.id) !== element.id){
+                    element.description = translations['intro_' + element.id];
+                }
+            });
+
+            setReady();
+        }
+
+        function setReady() {
+            menuReady = true;
+            executeCallBacks();
+        }
+
+        function isReady() {
+            return menuReady;
+        }
+
+        /**
+         * Execute any callbacks that are set onto the callbacks array
+         */
+        function executeCallBacks() {
+            var onceCallBack, callBackIndex;
+
+            //If not ready or no menu items
+            if ( ! isReady() || menuItems === {})
+                return false;
+
+            //Execute the single time callbacks
+            while (onceCallBacks.length !== 0) {
+                onceCallBack = onceCallBacks.pop();
+                onceCallBack(menuItems);
+            }
+            callBacks.forEach(function (callback, index, callBacks) {
+               callback.apply(dhis2.menu, [menuItems]);
+            });
+        }
+
+        //TODO: Function seems complicated and can be improved perhaps
+        /**
+         * Sort apps (objects with a name property) by name
+         *
+         * @param apps
+         * @param inverse {boolean} Return the elements in an inverted order (DESC sort)
+         * @returns {Array}
+         */
+        function sortAppsByName (apps, inverse) {
+            var smaller = [],
+                bigger = [],
+                center = Math.floor(apps.length / 2),
+                comparisonResult,
+                result;
+
+            //If nothing left to sort return the app list
+            if (apps.length <= 1)
+                return apps;
+
+            center = apps[center];
+            apps.forEach(function (app, index, apps) {
+                comparisonResult = center.name.localeCompare(app.name);
+                if (comparisonResult <= -1) {
+                    bigger.push(app);
+                }
+                if (comparisonResult >= 1) {
+                    smaller.push(app);
+                }
+            });
+
+            smaller = sortAppsByName(smaller);
+            bigger = sortAppsByName(bigger);
+
+            result = smaller.concat([center]).concat(bigger);
+
+            return inverse ? result.reverse() : result;
+        }
+
+        /***********************************************************************
+         * Public methods
+         **********************************************************************/
+
+        that.displayOrder = 'custom';
+
+        that.getMenuItems = function () {
+            return menuItems;
+        }
+
+        /**
+         * Get the max number of favorites
+         */
+        that.getMaxFavorites = function () {
+            return MAX_FAVORITES;
+        }
+
+        /**
+         * Order the menuItems by a given list
+         *
+         * @param orderedIdList
+         * @returns {{}}
+         */
+        that.orderMenuItemsByList = function (orderedIdList) {
+            menuItems.setOrder(orderedIdList);
+
+            executeCallBacks();
+
+            return that;
+        };
+
+        that.updateFavoritesFromList = function (orderedIdList) {
+            var newFavsIds = orderedIdList.slice(0, MAX_FAVORITES),
+                oldFavsIds = menuItems.getOrder().slice(0, MAX_FAVORITES),
+                currentOrder = menuItems.getOrder(),
+                newOrder;
+
+            //Take the new favorites as the new order
+            newOrder = newFavsIds;
+
+            //Find the favorites that were pushed out and add  them to the list on the top of the order
+            oldFavsIds.forEach(function (id, index, ids) {
+                if (-1 === newFavsIds.indexOf(id)) {
+                    newOrder.push(id);
+                }
+            });
+
+            //Loop through the remaining current order to add the remaining apps to the new order
+            currentOrder.forEach(function (id, index, ids) {
+                //Add id to the order when it's not already in there
+                if (-1 === newOrder.indexOf(id)) {
+                    newOrder.push(id);
+                }
+            });
+
+            menuItems.setOrder(newOrder);
+
+            executeCallBacks();
+
+            return that;
+        }
+
+        /**
+         * Adds the menu items given to the menu
+         */
+        that.addMenuItems = function (items) {
+            var keysToTranslate = [];
+
+            items.forEach(function (item, index, items) {
+                item.id = item.name;
+                keysToTranslate.push(item.name);
+                if(item.description === "") {
+                    keysToTranslate.push("intro_" + item.name);
+                }
+                menuItems.setItem(item.id, item);
+            });
+
+            dhis2.translate.get(keysToTranslate, processTranslations);
+        };
+
+        /**
+         * Subscribe to the service
+         *
+         * @param callback {function} Function that should be run when service gets updated
+         * @param onlyOnce {boolean} Callback should only be run once on the next update
+         * @returns boolean Returns false when callback is not a function
+         */
+        that.subscribe = function (callback, onlyOnce) {
+            var once = onlyOnce ? true : false;
+
+            if ( ! _.isFunction(callback)) {
+                return false;
+            }
+
+            if (menuItems !== undefined) {
+                callback(menuItems);
+            }
+
+            if (true === once) {
+                onceCallBacks.push(callback);
+            } else {
+                callBacks.push(callback);
+            }
+            return true;
+        };
+
+        /**
+         * Get the favorite apps
+         *
+         * @returns {Array}
+         */
+        that.getFavorites = function () {
+            return menuItems.list().slice(0, MAX_FAVORITES);
+        };
+
+        /**
+         * Get the current menuItems
+         */
+        that.getApps = function () {
+            return menuItems.list();
+        };
+
+        /**
+         * Get non favorite apps
+         */
+        that.getNonFavoriteApps = function () {
+            return menuItems.list().slice(MAX_FAVORITES);
+        };
+
+        that.sortNonFavAppsByName = function (inverse) {
+            return sortAppsByName(that.getNonFavoriteApps(), inverse);
+        }
+
+        /**
+         * Gets the applist based on the current display order
+         *
+         * @returns {Array} Array of app objects
+         */
+        that.getOrderedAppList = function () {
+            var favApps = dhis2.menu.getFavorites(),
+                nonFavApps = that.getNonFavoriteApps();
+            switch (that.displayOrder) {
+                case 'name-asc':
+                    nonFavApps = that.sortNonFavAppsByName();
+                    break;
+                case 'name-desc':
+                    nonFavApps = that.sortNonFavAppsByName(true);
+                    break;
+            }
+            return favApps.concat(nonFavApps);;
+        }
+
+        that.updateOrder = function (reorderedApps) {
+            switch (dhis2.menu.displayOrder) {
+                case 'name-asc':
+                case 'name-desc':
+                    that.updateFavoritesFromList(reorderedApps);
+                    break;
+
+                default:
+                    //Update the menu object with the changed order
+                    that.orderMenuItemsByList(reorderedApps);
+                    break;
+            }
+        }
+
+        that.save = function (saveMethod) {
+            if ( ! _.isFunction(saveMethod)) {
+                return false;
+            }
+
+            return saveMethod(that.getMenuItems().getOrder());
+        }
+
+        return that;
+    }();
+})(dhis2, _);
+
+/**
+ * Created by Mark Polak on 28/01/14.
+ *
+ * @description jQuery part of the menu
+ *
+ * @see jQuery (http://jquery.com)
+ */
+(function ($, menu, undefined) {
+    var markup = '',
+        selector = 'appsMenu';
+
+    markup += '<li data-id="${id}" data-app-name="${name}" data-app-action="${defaultAction}">';
+    markup += '  <a href="${defaultAction}" class="app-menu-item" title="${name}">';
+    markup += '    <img src="${icon}" onError="javascript: this.onerror=null; this.src = \'../icons/program.png\';">';
+    markup += '    <span>${name}</span>';
+    markup += '    <div class="app-menu-item-description"><span>${name}</span><i class="fa fa-arrows"></i>${description}</div>';
+    markup += '  </a>';
+    markup += '</li>';
+
+    $.template('appMenuItemTemplate', markup);
+
+    function renderDropDownFavorites() {
+        var selector = '#menuDropDown1 .menuDropDownBox',
+            favorites = dhis2.menu.getFavorites();
+
+        $(selector).parent().addClass('app-menu-dropdown ui-helper-clearfix');
+        $(selector).html('');
+        return $.tmpl( "appMenuItemTemplate", favorites).appendTo(selector);
+    }
+
+    function renderAppManager(selector) {
+        var apps = dhis2.menu.getOrderedAppList();
+        $('#' + selector).html('');
+        $('#' + selector).append($('<ul></ul><hr class="app-separator">').addClass('ui-helper-clearfix'));
+        $('#' + selector).addClass('app-menu');
+        $.tmpl( "appMenuItemTemplate", apps).appendTo('#' + selector + ' ul');
+
+        //Add favorites icon to all the menu items in the manager
+        $('#' + selector + ' ul li').each(function (index, item) {
+            $(item).children('a').append($('<i class="fa fa-bookmark"></i>'));
+        });
+    }
+
+    /**
+     * Saves the given order to the server using jquery ajax
+     *
+     * @param menuOrder {Array}
+     */
+    function saveOrder(menuOrder) {
+        if (menuOrder.length !== 0) {
+            //Persist the order on the server
+            $.ajax({
+                contentType:"application/json; charset=utf-8",
+                data: JSON.stringify(menuOrder),
+                dataType: "json",
+                type:"POST",
+                url: "../api/menu/"
+            }).success(function () {
+                //TODO: Give user feedback for successful save
+            }).error(function () {
+                //TODO: Give user feedback for failure to save
+                //TODO: Translate this error message
+                alert('Unable to save your app order to the server.');
+            });
+        }
+    }
+
+    /**
+     * Render the menumanager and the dropdown meny and attach the update handler
+     */
+    //TODO: Rename this as the name is not very clear to what it does
+    function renderMenu() {
+        var options = {
+                placeholder: 'app-menu-placeholder',
+                connectWith: '.app-menu ul',
+                update: function (event, ui) {
+                    var reorderedApps = $("#" + selector + " ul"). sortable('toArray', {attribute: "data-id"});
+
+                    dhis2.menu.updateOrder(reorderedApps);
+                    dhis2.menu.save(saveOrder);
+
+                    //Render the dropdown menu
+                    renderDropDownFavorites();
+                },
+                //Constrict the draggable elements to the parent element
+                containment: 'parent'
+            };
+
+        renderAppManager(selector);
+        renderDropDownFavorites();
+
+        $('.app-menu ul').sortable(options).disableSelection();
+    }
+
+    menu.subscribe(renderMenu);
+
+    /**
+     * jQuery events that communicate with the web api
+     * TODO: Check the urls (they seem to be specific to the dev location atm)
+     */
+    $(function () {
+        $.ajax('../dhis-web-commons/menu/getModules.action').success(function (data) {
+            if (typeof data.modules === 'object') {
+                menu.addMenuItems(data.modules);
+            }
+        }).error(function () {
+            //TODO: Translate this error message
+            alert('Can not load apps from server.');
+        });
+
+        /**
+         * Event handler for the sort order box
+         */
+        $('#menuOrderBy').change(function (event) {
+            var orderBy = $(event.target).val();
+
+            dhis2.menu.displayOrder = orderBy;
+
+            renderMenu();
+        });
+    });
+
+})(jQuery, dhis2.menu);

=== added file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.translate.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.translate.js	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/dhis2/dhis2.translate.js	2014-03-24 20:21:10 +0000
@@ -0,0 +1,105 @@
+"use strict";
+/*
+ * Copyright (c) 2004-2014, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Created by Mark Polak on 28/01/14.
+ *
+ * @see Underscore.js (http://underscorejs.org)
+ */
+dhis2.util.namespace( 'dhis2.translate' );
+
+(function ($,  _, translate, undefined) {
+    var translationCache = {
+        get: function (key) {
+            if (this.hasOwnProperty(key))
+                return this[key];
+            return key;
+        }
+    };
+
+    /**
+     * Adds translations to the translation cache (overrides already existing ones)
+     *
+     * @param translations {Object}
+     */
+    function  addToCache(translations) {
+        translationCache = _.extend(translationCache, translations);
+    }
+
+    /**
+     * Asks the server for the translations of the given {translatekeys} and calls {callback}
+     * when a successful response is received.
+     *
+     * @param translateKeys {Array}
+     * @param callback {function}
+     */
+    function getTranslationsFromServer(translateKeys, callback) {
+        $.ajax({
+            url:"../api/i18n",
+            type:"POST",
+            data: JSON.stringify(translateKeys),
+            contentType:"application/json; charset=utf-8",
+            dataType:"json"
+        }).success(function (data) {
+                addToCache(data);
+                if (typeof callback === 'function') {
+                    callback(translationCache);
+                }
+            });
+    }
+
+    /**
+     * Translates the given keys in the {translate} array and calls callback when request is successful
+     * callback currently gets passed an object with all translations that are in the local cache
+     *
+     * @param translate {Array}
+     * @param callback {function}
+     */
+    translate.get = function (translate, callback) {
+        var translateKeys = [],
+            key;
+
+        //Only ask for the translations that we do not already have
+        translate.forEach(function (text, index, translate) {
+            if ( ! (text in translationCache)) {
+                translateKeys.push(text);
+            }
+        });
+
+        if (translateKeys.length > 0) {
+            //Ask for translations of the app names
+            getTranslationsFromServer(translateKeys, callback);
+        } else {
+            //Call backback right away when we have everything in cache
+            callback(translationCache);
+        }
+
+    };
+
+})(jQuery, _, dhis2.translate);

=== modified file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/main.vm'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/main.vm	2014-01-23 14:09:55 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/main.vm	2014-03-19 23:35:11 +0000
@@ -13,6 +13,7 @@
     <link type="text/css" rel="stylesheet" media="screen,print" href="../dhis-web-commons/css/${stylesheet}?_rev=$!{buildRevision}" />
     <link type="text/css" rel="stylesheet" media="screen,print" href="../dhis-web-commons/css/widgets.css?_rev=$!{buildRevision}" />
     <link type="text/css" rel="stylesheet" media="print" href="../dhis-web-commons/css/print.css?_rev=$!{buildRevision}" />
+    <link type="text/css" rel="stylesheet" media="screen" href="../dhis-web-commons/css/menu.css?_rev=$!{buildRevision}" />
     #foreach ( $style in $stylesheets )
     <link type="text/css" rel="stylesheet" href="${style}?_rev=$!{buildRevision}">
     #end
@@ -65,6 +66,8 @@
     <script type="text/javascript" src="../dhis-web-commons/javascripts/dhis2/dhis2.storage.js?_rev=$!{buildRevision}"></script>
     <script type="text/javascript" src="../dhis-web-commons/javascripts/dhis2/dhis2.contextmenu.js?_rev=$!{buildRevision}"></script>
     <script type="text/javascript" src="../dhis-web-commons/javascripts/dhis2/dhis2.appcache.js?_rev=$!{buildRevision}"></script>
+    <script type="text/javascript" src="../dhis-web-commons/javascripts/dhis2/dhis2.translate.js?_rev=$!{buildRevision}"></script>
+    <script type="text/javascript" src="../dhis-web-commons/javascripts/dhis2/dhis2.menu.js?_rev=$!{buildRevision}"></script>
     <script type="text/javascript" src="../dhis-web-commons/i18nJavaScript.action?_rev=$!{buildRevision}"></script>
     <script type="text/javascript" src="../main.js?_rev=$!{buildRevision}"></script>
     <script type="text/javascript" src="../request.js?_rev=$!{buildRevision}"></script>
@@ -84,44 +87,66 @@
       </span>
       
       <ul id="menuLinkArea">
-      #if( $maintenanceModules.size() > 0 )
-      <li><a id="menuLink1" href="../dhis-web-commons-about/modules.action" class="menuLink">$i18n.getString( "maintenance" )</a></li>
-      #end
-      #if( $serviceModules.size() > 0 )
-      <li><a id="menuLink2" href="../dhis-web-commons-about/modules.action" class="menuLink">$i18n.getString( "services" )</a></li>
-      #end
-      <li><a id="menuLink3" href="../dhis-web-commons-about/functions.action" class="menuLink">$i18n.getString( "profile" )</a></li>
-      </ul>
-
-      #if( $maintenanceModules.size() > 0 )
-      <div id="menuDropDown1" class="menuDropDownArea" >
-      <ul class="menuDropDownBox">
-      #foreach( $module in $maintenanceModules )
-        <li><a href="${module.defaultAction}">$i18n.getString( $module.name )&nbsp;</a></li>
-      #end
-      </ul>
-      </div>
-      #end
-
-      #if( $serviceModules.size() > 0 )
-      <div id="menuDropDown2" class="menuDropDownArea">
-      <ul class="menuDropDownBox">
-      #foreach( $module in $serviceModules )
-        <li><a href="${module.defaultAction}">$i18n.getString( $module.name )&nbsp;</a></li>
-      #end
-      </ul>
-      </div>
-      #end
-
-	  <div id="menuDropDown3" class="menuDropDownArea">
-	  <ul class="menuDropDownBox">
-  	    <li><a href="../dhis-web-commons-about/userSettings.action">$i18n.getString( "settings" )&nbsp;</a></li>
-        <li><a href="../dhis-web-commons-about/showUpdateUserProfileForm.action">$i18n.getString( "profile" )&nbsp;</a></li>
-  	    <li><a href="../dhis-web-commons-about/showUpdateUserAccountForm.action">$i18n.getString( "account" )&nbsp;</a></li>
-        <li><a href="../dhis-web-commons-about/help.action">$i18n.getString( "help" )&nbsp;</a></li>
-        <li><a href="../dhis-web-commons-security/logout.action">$i18n.getString( "log_out" )&nbsp;</a></li>
-        <li><a href="../dhis-web-commons-about/about.action">$i18n.getString( "about_dhis2" )&nbsp;</a></li>
-      </ul>
+        <li>
+            <a id="menuLink1" href="../dhis-web-commons-about/modules.action" class="menuLink">
+              <i class="fa fa-th"></i>$i18n.getString( "applications" )
+            </a>
+        </li>
+        <li>
+            <a id="menuLink3" href="../dhis-web-commons-about/functions.action" class="menuLink">
+              <i class="fa fa-user"></i>$i18n.getString( "profile" )
+          </a>
+        </li>
+      </ul>
+
+      <div id="menuDropDown1" class="menuDropDownArea app-menu-dropdown" >
+        <div class="caret-up-border"></div>
+        <div class="caret-up-background"></div>
+        <ul class="menuDropDownBox"></ul>
+        <div class="apps-menu-more"><a href="../dhis-web-commons-about/modules.action">$i18n.getString( "more_applications" )</a></div>
+      </div>
+
+	  <div id="menuDropDown3" class="menuDropDownArea app-menu-dropdown">
+        <div class="caret-up-border"></div>
+        <div class="caret-up-background"></div>
+        <ul class="menuDropDownBox">
+          <li>
+              <a class="app-menu-item" href="../dhis-web-commons-about/userSettings.action">
+                <img src="../icons/usersettings.png" alt="$i18n.getString( "settings" )">
+                <span>$i18n.getString( "settings" )&nbsp;</span>
+              </a>
+          </li>
+          <li>
+              <a class="app-menu-item" href="../dhis-web-commons-about/showUpdateUserProfileForm.action">
+                  <img src="../icons/function-profile.png" alt="$i18n.getString( "profile" )">
+                  <span>$i18n.getString( "profile" )&nbsp;</span>
+              </a>
+          </li>
+          <li>
+              <a class="app-menu-item" href="../dhis-web-commons-about/showUpdateUserAccountForm.action">
+                <img src="../icons/function-account.png" alt="$i18n.getString( "account" )">
+                <span>$i18n.getString( "account" )&nbsp;</span>
+              </a>
+          </li>
+          <li>
+              <a class="app-menu-item" href="../dhis-web-commons-about/help.action">
+                <img src="../icons/function-help-center.png" alt="$i18n.getString( "help" )">
+                <span>$i18n.getString( "help" )&nbsp;</span>
+              </a>
+          </li>
+          <li>
+              <a class="app-menu-item" href="../dhis-web-commons-security/logout.action">
+                <img src="../icons/function-log-out.png" alt="$i18n.getString( "log_out" )">
+                <span>$i18n.getString( "log_out" )&nbsp;</span>
+              </a>
+          </li>
+          <li>
+              <a class="app-menu-item" href="../dhis-web-commons-about/about.action">
+                <img src="../icons/function-about-dhis2.png" alt="$i18n.getString( "about_dhis2" )">
+                <span>$i18n.getString( "about_dhis2" )&nbsp;</span>
+              </a>
+          </li>
+        </ul>
 	  </div>
 	  
       <span id="showLeftBar">

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/menu/action/GetModulesAction.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/menu/action/GetModulesAction.java	2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/menu/action/GetModulesAction.java	2014-03-25 12:28:24 +0000
@@ -28,13 +28,11 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
 import org.hisp.dhis.user.CurrentUserService;
-import org.hisp.dhis.user.User;
 import org.hisp.dhis.webportal.module.Module;
 import org.hisp.dhis.webportal.module.ModuleManager;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -59,34 +57,29 @@
     {
         return modules;
     }
-    
+
     @Override
     public String execute()
         throws Exception
     {
         modules = manager.getAccessibleMenuModulesAndApps();
-        
-        User user = currentUserService.getCurrentUser();
-        
-        final List<String> userApps = user.getApps();        
-        final List<String> allApps = new ArrayList<String>();
-        
-        for ( Module module : modules )
-        {
-            allApps.add( module.getName() );
-        }        
-        
-        Collections.sort( modules, new Comparator<Module>()
-        {
-            @Override
-            public int compare( Module m1, Module m2 )
+
+        final List<String> userApps = currentUserService.getCurrentUser().getApps();
+        
+        if ( userApps != null )
+        {
+            Collections.sort( modules, new Comparator<Module>()
             {
-                Integer i1 = userApps.indexOf( m1.getName() );
-                Integer i2 = userApps.indexOf( m2.getName() );
-                
-                return i1 != -1 ? ( i2 != -1 ? i1.compareTo( i2 ) : -1 ) : 1;
-            }
-        } );        
+                @Override
+                public int compare( Module m1, Module m2 )
+                {
+                    Integer i1 = userApps.indexOf( m1.getName() );
+                    Integer i2 = userApps.indexOf( m2.getName() );
+                    
+                    return i1 != -1 ? ( i2 != -1 ? i1.compareTo( i2 ) : -1 ) : 1;
+                }
+            } );
+        }
         
         return SUCCESS;
     }

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/module/Module.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/module/Module.java	2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/webportal/module/Module.java	2014-03-25 12:28:24 +0000
@@ -85,7 +85,10 @@
         boolean hasIcon = app.getIcons() != null && app.getIcons().getIcon48() != null;
         
         String defaultAction = app.getLaunchUrl();
-        String icon = hasIcon ? app.getFolderName() + File.separator + app.getIcons().getIcon48() : null;
+
+        String icon = hasIcon ? icon = app.getBaseUrl() + File.separator + app.getFolderName() +
+            File.separator + app.getIcons().getIcon48() : null;
+
         String description = TextUtils.subString( app.getDescription(), 0, 80 );
         
         Module module = new Module( app.getName(), app.getName(), defaultAction );
@@ -97,7 +100,7 @@
     
     public String getIconFallback()
     {
-        return icon != null ? icon : name + ".png";
+        return icon != null ? icon : ".." + File.separator + "icons" + File.separator + name + ".png";
     }
     
     // -------------------------------------------------------------------------

=== modified file 'dhis-2/pom.xml'
--- dhis-2/pom.xml	2014-03-24 13:16:05 +0000
+++ dhis-2/pom.xml	2014-03-24 21:39:15 +0000
@@ -15,7 +15,7 @@
   </prerequisites>
 
   <description>
-    The District Health Information System deals with registering,
+    The District Health Information System 2 deals with registering,
     aggregating and reporting statistical health data. The goal is to allow users to analyze
     and use this data to guide local action. The system is based around an goals of empowering
     users, by allowing them to decide what to register and report data for.