diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
index 983ae71ad..524b897f2 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
@@ -353,11 +353,11 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
var linked = dataSource in users;
// Add page entry
- $scope.accountPages.push(new PageDefinition(
- translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME',
- '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username),
- linked ? 'linked' : 'unlinked'
- ));
+ $scope.accountPages.push(new PageDefinition({
+ name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME',
+ url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username),
+ className : linked ? 'linked' : 'unlinked'
+ }));
});
diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user.css b/guacamole/src/main/webapp/app/manage/styles/manage-user.css
index a24d3e569..7d6f077db 100644
--- a/guacamole/src/main/webapp/app/manage/styles/manage-user.css
+++ b/guacamole/src/main/webapp/app/manage/styles/manage-user.css
@@ -28,14 +28,14 @@
text-transform: none;
}
-.manage-user .settings-tabs .page-list li.unlinked a[href],
-.manage-user .settings-tabs .page-list li.linked a[href] {
+.manage-user .page-tabs .page-list li.unlinked a[href],
+.manage-user .page-tabs .page-list li.linked a[href] {
padding-right: 2.5em;
position: relative;
}
-.manage-user .settings-tabs .page-list li.unlinked a[href]:before,
-.manage-user .settings-tabs .page-list li.linked a[href]:before {
+.manage-user .page-tabs .page-list li.unlinked a[href]:before,
+.manage-user .page-tabs .page-list li.linked a[href]:before {
content: ' ';
position: absolute;
right: 0;
@@ -47,19 +47,19 @@
background-position: center;
}
-.manage-user .settings-tabs .page-list li.unlinked a[href]:before {
+.manage-user .page-tabs .page-list li.unlinked a[href]:before {
background-image: url('images/plus.png');
}
-.manage-user .settings-tabs .page-list li.unlinked a[href] {
+.manage-user .page-tabs .page-list li.unlinked a[href] {
opacity: 0.5;
}
-.manage-user .settings-tabs .page-list li.unlinked a[href]:hover,
-.manage-user .settings-tabs .page-list li.unlinked a[href].current {
+.manage-user .page-tabs .page-list li.unlinked a[href]:hover,
+.manage-user .page-tabs .page-list li.unlinked a[href].current {
opacity: 1;
}
-.manage-user .settings-tabs .page-list li.linked a[href]:before {
+.manage-user .page-tabs .page-list li.linked a[href]:before {
background-image: url('images/checkmark.png');
}
diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
index 442ee5116..04cd593a6 100644
--- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html
+++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
@@ -27,7 +27,7 @@ THE SOFTWARE.
{{user.username}}
-
+
diff --git a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js
index bd5f909c5..04ecf7a77 100644
--- a/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js
+++ b/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js
@@ -33,7 +33,7 @@ angular.module('navigation').directive('guacPageList', [function guacPageList()
/**
* The array of pages to display.
*
- * @type Page[]
+ * @type PageDefinition[]
*/
pages : '='
@@ -42,13 +42,119 @@ angular.module('navigation').directive('guacPageList', [function guacPageList()
templateUrl: 'app/navigation/templates/guacPageList.html',
controller: ['$scope', '$injector', function guacPageListController($scope, $injector) {
- // Get required services
+ // Required types
+ var PageDefinition = $injector.get('PageDefinition');
+
+ // Required services
var $location = $injector.get('$location');
+ /**
+ * The URL of the currently-displayed page.
+ *
+ * @type String
+ */
+ var currentURL = $location.url();
+
+ /**
+ * The names associated with the current page, if the current page
+ * is known. The value of this property corresponds to the value of
+ * PageDefinition.name. Though PageDefinition.name may be a String,
+ * this will always be an Array.
+ *
+ * @type String[]
+ */
+ var currentPageName = [];
+
+ /**
+ * Array of each level of the page list, where a level is defined
+ * by a mapping of names (translation strings) to the
+ * PageDefinitions corresponding to those names.
+ *
+ * @type Object.
[]
+ */
+ $scope.levels = [];
+
+ /**
+ * Returns the names associated with the given page, in
+ * hierarchical order. If the page is only associated with a single
+ * name, and that name is not stored as an array, it will be still
+ * be returned as an array containing a single item.
+ *
+ * @param {PageDefinition} page
+ * The page to return the names of.
+ *
+ * @return {String[]}
+ * An array of all names associated with the given page, in
+ * hierarchical order.
+ */
+ var getPageNames = function getPageNames(page) {
+
+ // If already an array, simply return the name
+ if (angular.isArray(page.name))
+ return page.name;
+
+ // Otherwise, transform into array
+ return [page.name];
+
+ };
+
+ /**
+ * Adds the given PageDefinition to the overall set of pages
+ * displayed by this guacPageList, automatically updating the
+ * available levels ($scope.levels) and the contents of those
+ * levels.
+ *
+ * @param {PageDefinition} page
+ * The PageDefinition to add.
+ *
+ * @param {Number} weight
+ * The sorting weight to use for the page if it does not
+ * already have an associated weight.
+ */
+ var addPage = function addPage(page, weight) {
+
+ // Pull all names for page
+ var names = getPageNames(page);
+
+ // Copy the hierarchy of this page into the displayed levels
+ // as far as is relevant for the currently-displayed page
+ for (var i = 0; i < names.length; i++) {
+
+ // Create current level, if it doesn't yet exist
+ var pages = $scope.levels[i];
+ if (!pages)
+ pages = $scope.levels[i] = {};
+
+ // Get the name at the current level
+ var name = names[i];
+
+ // Determine whether this page definition is part of the
+ // hierarchy containing the current page
+ var isCurrentPage = (currentPageName[i] === name);
+
+ // Store new page if it doesn't yet exist at this level
+ if (!pages[name]) {
+ pages[name] = new PageDefinition({
+ name : name,
+ url : isCurrentPage ? currentURL : page.url,
+ className : page.className,
+ weight : page.weight || weight
+ });
+ }
+
+ // If the name at this level no longer matches the
+ // hierarchy of the current page, do not go any deeper
+ if (currentPageName[i] !== name)
+ break;
+
+ }
+
+ };
+
/**
* Navigate to the given page.
*
- * @param {Page} page
+ * @param {PageDefinition} page
* The page to navigate to.
*/
$scope.navigateToPage = function navigateToPage(page) {
@@ -58,16 +164,67 @@ angular.module('navigation').directive('guacPageList', [function guacPageList()
/**
* Tests whether the given page is the page currently being viewed.
*
- * @param {Page} page
+ * @param {PageDefinition} page
* The page to test.
*
* @returns {Boolean}
* true if the given page is the current page, false otherwise.
*/
$scope.isCurrentPage = function isCurrentPage(page) {
- return $location.url() === page.url;
+ return currentURL === page.url;
};
+ /**
+ * Given an arbitrary map of PageDefinitions, returns an array of
+ * those PageDefinitions, sorted by weight.
+ *
+ * @param {Object.<*, PageDefinition>} level
+ * A map of PageDefinitions with arbitrary keys. The value of
+ * each key is ignored.
+ *
+ * @returns {PageDefinition[]}
+ * An array of all PageDefinitions in the given map, sorted by
+ * weight.
+ */
+ $scope.getPages = function getPages(level) {
+
+ var pages = [];
+
+ // Convert contents of level to a flat array of pages
+ angular.forEach(level, function addPageFromLevel(page) {
+ pages.push(page);
+ });
+
+ // Sort page array by weight
+ pages.sort(function comparePages(a, b) {
+ return a.weight - b.weight;
+ });
+
+ return pages;
+
+ };
+
+ // Update page levels whenever pages changes
+ $scope.$watch('pages', function setPages(pages) {
+
+ // Determine current page name
+ currentPageName = [];
+ angular.forEach(pages, function findCurrentPageName(page) {
+
+ // If page is current page, store its names
+ if ($scope.isCurrentPage(page))
+ currentPageName = getPageNames(page);
+
+ });
+
+ // Reset contents of levels
+ $scope.levels = [];
+
+ // Add all page definitions
+ angular.forEach(pages, addPage);
+
+ });
+
}] // end controller
};
diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
index 0cb9f530c..ef2280d37 100644
--- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js
+++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
@@ -41,31 +41,16 @@ angular.module('navigation').factory('userPageService', ['$injector',
var service = {};
- /**
- * Construct a new PageDefinition object with the given name and url.
- *
- * @constructor
- * @param {String} name
- * The i18n key for the name of the page.
- *
- * @param {String} url
- * The URL of the page.
- */
- var PageDefinition = function PageDefinition(name, url) {
- this.name = name;
- this.url = url;
- };
-
/**
* The home page to assign to a user if they can navigate to more than one
* page.
*
* @type PageDefinition
*/
- var SYSTEM_HOME_PAGE = new PageDefinition(
- 'USER_MENU.ACTION_NAVIGATE_HOME',
- '/'
- );
+ var SYSTEM_HOME_PAGE = new PageDefinition({
+ name : 'USER_MENU.ACTION_NAVIGATE_HOME',
+ url : '/'
+ });
/**
* Returns an appropriate home page for the current user.
@@ -101,14 +86,14 @@ angular.module('navigation').factory('userPageService', ['$injector',
// Only one connection present, use as home page
if (connection) {
- homePage = new PageDefinition(
- connection.name,
- '/client/' + ClientIdentifier.toString({
+ homePage = new PageDefinition({
+ name : connection.name,
+ url : '/client/' + ClientIdentifier.toString({
dataSource : dataSource,
type : ClientIdentifier.Types.CONNECTION,
id : connection.identifier
})
- );
+ });
}
// Only one balancing group present, use as home page
@@ -116,14 +101,14 @@ angular.module('navigation').factory('userPageService', ['$injector',
&& connectionGroup.type === ConnectionGroup.Type.BALANCING
&& _.isEmpty(connectionGroup.childConnections)
&& _.isEmpty(connectionGroup.childConnectionGroups)) {
- homePage = new PageDefinition(
- connectionGroup.name,
- '/client/' + ClientIdentifier.toString({
+ homePage = new PageDefinition({
+ name : connectionGroup.name,
+ url : '/client/' + ClientIdentifier.toString({
dataSource : dataSource,
type : ClientIdentifier.Types.CONNECTION_GROUP,
id : connectionGroup.identifier
})
- );
+ });
}
}
@@ -247,33 +232,33 @@ angular.module('navigation').factory('userPageService', ['$injector',
// If user can manage sessions, add link to sessions management page
if (canManageSessions) {
- pages.push(new PageDefinition(
- 'USER_MENU.ACTION_MANAGE_SESSIONS',
- '/settings/sessions'
- ));
+ pages.push(new PageDefinition({
+ name : 'USER_MENU.ACTION_MANAGE_SESSIONS',
+ url : '/settings/sessions'
+ }));
}
// If user can manage users, add link to user management page
if (canManageUsers) {
- pages.push(new PageDefinition(
- 'USER_MENU.ACTION_MANAGE_USERS',
- '/settings/users'
- ));
+ pages.push(new PageDefinition({
+ name : 'USER_MENU.ACTION_MANAGE_USERS',
+ url : '/settings/users'
+ }));
}
// If user can manage connections, add link to connections management page
if (canManageConnections) {
- pages.push(new PageDefinition(
- 'USER_MENU.ACTION_MANAGE_CONNECTIONS',
- '/settings/connections'
- ));
+ pages.push(new PageDefinition({
+ name : 'USER_MENU.ACTION_MANAGE_CONNECTIONS',
+ url : '/settings/connections'
+ }));
}
// Add link to user preferences (always accessible)
- pages.push(new PageDefinition(
- 'USER_MENU.ACTION_MANAGE_PREFERENCES',
- '/settings/preferences'
- ));
+ pages.push(new PageDefinition({
+ name : 'USER_MENU.ACTION_MANAGE_PREFERENCES',
+ url : '/settings/preferences'
+ }));
return pages;
};
@@ -338,10 +323,10 @@ angular.module('navigation').factory('userPageService', ['$injector',
// Add generic link to the first-available settings page
if (settingsPages.length) {
- pages.push(new PageDefinition(
- 'USER_MENU.ACTION_MANAGE_SETTINGS',
- settingsPages[0].url
- ));
+ pages.push(new PageDefinition({
+ name : 'USER_MENU.ACTION_MANAGE_SETTINGS',
+ url : settingsPages[0].url
+ }));
}
return pages;
diff --git a/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css
new file mode 100644
index 000000000..28b4de263
--- /dev/null
+++ b/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2015 Glyptodon LLC
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+.page-tabs .page-list ul {
+ margin: 0;
+ padding: 0;
+ background: rgba(0, 0, 0, 0.0125);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.page-tabs .page-list ul + ul {
+ font-size: 0.75em;
+}
+
+.page-tabs .page-list li {
+ display: inline-block;
+ list-style: none;
+}
+
+.page-tabs .page-list li a[href] {
+ display: block;
+ color: black;
+ text-decoration: none;
+ padding: 0.75em 1em;
+}
+
+.page-tabs .page-list li a[href]:visited {
+ color: black;
+}
+
+.page-tabs .page-list li a[href]:hover {
+ background-color: #CDA;
+}
+
+.page-tabs .page-list li a[href].current,
+.page-tabs .page-list li a[href].current:hover {
+ background: rgba(0,0,0,0.3);
+ cursor: default;
+}
diff --git a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html
index 35d12c31e..c5d131e9e 100644
--- a/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html
+++ b/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html
@@ -1,4 +1,4 @@
-
+
diff --git a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js
index 0b5d00b1e..ba702b014 100644
--- a/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js
+++ b/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js
@@ -30,37 +30,46 @@ angular.module('navigation').factory('PageDefinition', [function definePageDefin
* an arbitrary, human-readable name.
*
* @constructor
- * @param {String} name
- * The the name of the page, which should be a translation table key.
- *
- * @param {String} url
- * The URL of the page.
- *
- * @param {String} [className='']
- * The CSS class name to associate with this page, if any.
+ * @param {PageDefinition|Object} template
+ * The object whose properties should be copied within the new
+ * PageDefinition.
*/
- var PageDefinition = function PageDefinition(name, url, className) {
+ var PageDefinition = function PageDefinition(template) {
/**
* The the name of the page, which should be a translation table key.
+ * Alternatively, this may also be a list of names, where the final
+ * name represents the page and earlier names represent categorization.
+ * Those categorical names may be rendered hierarchically as a system
+ * of menus, tabs, etc.
*
- * @type String
+ * @type String|String[]
*/
- this.name = name;
+ this.name = template.name;
/**
* The URL of the page.
*
* @type String
*/
- this.url = url;
+ this.url = template.url;
/**
- * The CSS class name to associate with this page, if any.
+ * The CSS class name to associate with this page, if any. This will be
+ * an empty string by default.
*
* @type String
*/
- this.className = className || '';
+ this.className = template.className || '';
+
+ /**
+ * A numeric value denoting the relative sort order when compared to
+ * other sibling PageDefinitions. If unspecified, sort order is
+ * determined by the system using the PageDefinition.
+ *
+ * @type Number
+ */
+ this.weight = template.weight;
};
diff --git a/guacamole/src/main/webapp/app/settings/styles/settings.css b/guacamole/src/main/webapp/app/settings/styles/settings.css
index 67453a266..deb4b8cc1 100644
--- a/guacamole/src/main/webapp/app/settings/styles/settings.css
+++ b/guacamole/src/main/webapp/app/settings/styles/settings.css
@@ -34,36 +34,3 @@
text-align: center;
margin: 1em 0;
}
-
-.settings-tabs .page-list {
- margin: 0;
- padding: 0;
- background: rgba(0, 0, 0, 0.0125);
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
-}
-
-.settings-tabs .page-list li {
- display: inline-block;
- list-style: none;
-}
-
-.settings-tabs .page-list li a[href] {
- display: block;
- color: black;
- text-decoration: none;
- padding: 0.75em 1em;
-}
-
-.settings-tabs .page-list li a[href]:visited {
- color: black;
-}
-
-.settings-tabs .page-list li a[href]:hover {
- background-color: #CDA;
-}
-
-.settings-tabs .page-list li a[href].current,
-.settings-tabs .page-list li a[href].current:hover {
- background: rgba(0,0,0,0.3);
- cursor: default;
-}
diff --git a/guacamole/src/main/webapp/app/settings/templates/settings.html b/guacamole/src/main/webapp/app/settings/templates/settings.html
index 3c7f26e84..97142467f 100644
--- a/guacamole/src/main/webapp/app/settings/templates/settings.html
+++ b/guacamole/src/main/webapp/app/settings/templates/settings.html
@@ -28,7 +28,7 @@ THE SOFTWARE.
-