GUAC-586: Implement generic and hierarchical page tabbed page lists.

This commit is contained in:
Michael Jumper
2015-09-02 15:29:35 -07:00
parent b3614aef58
commit 36c1c853f9
10 changed files with 301 additions and 123 deletions

View File

@@ -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'
}));
});

View File

@@ -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');
}

View File

@@ -27,7 +27,7 @@ THE SOFTWARE.
<h2>{{user.username}}</h2>
<guac-user-menu></guac-user-menu>
</div>
<div class="settings-tabs">
<div class="page-tabs">
<guac-page-list pages="accountPages" ng-show="showAccountTabs()"></guac-page-list>
</div>

View File

@@ -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.<String, PageDefinition>[]
*/
$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
};

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
<ul class="page-list">
<div class="page-list">
<!--
Copyright (C) 2015 Glyptodon LLC
@@ -22,11 +22,13 @@
-->
<!-- Navigation links -->
<li ng-repeat="page in pages" class="{{page.className}}">
<a class="home" ng-click="navigateToPage(page)"
ng-class="{current: isCurrentPage(page)}" href="#{{page.url}}">
{{page.name | translate}}
</a>
</li>
<ul class="page-list-level" ng-repeat="level in levels track by $index">
<li ng-repeat="page in getPages(level)" class="{{page.className}}">
<a class="home" ng-click="navigateToPage(page)"
ng-class="{current: isCurrentPage(page)}" href="#{{page.url}}">
{{page.name | translate}}
</a>
</li>
</ul>
</ul>
</div>

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -28,7 +28,7 @@ THE SOFTWARE.
</div>
<!-- Available tabs -->
<div class="settings-tabs">
<div class="page-tabs">
<guac-page-list pages="settingsPages" ng-show="showAvailableTabs()"></guac-page-list>
</div>