Adding local nav functionality.

--HG--
branch : dev
This commit is contained in:
Andre Rodrigues 2011-02-11 12:45:28 -08:00
parent 0b92a82708
commit 042968453b
11 changed files with 360 additions and 70 deletions

View File

@ -1,33 +1,67 @@
using System.Linq;
using System.Web.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
using Castle.Core;
using Moq;
using NUnit.Framework;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Security;
using Orchard.Tests.Stubs;
using Orchard.UI.Navigation;
namespace Orchard.Tests.UI.Navigation {
[TestFixture]
public class MenuFilterTests {
private const string FirstLevel1Action = "FirstLevel1";
private const string SecondLevel1Action = "SecondLevel1";
private const string SecondLevel2Action = "SecondLevel2";
private const string ThirdLevel1Action = "ThirdLevel1";
private const string ThirdLevel2Action = "ThirdLevel2";
private const string ThirdLevel3Action = "ThirdLevel3";
private const string ThirdLevel4Action = "ThirdLevel4";
private const string FourthLevel1Action = "FourthLevel1";
private const string FourthLevel2Action = "FourthLevel2";
private const string FourthLevel3Action = "FourthLevel3";
private const string FourthLevel4Action = "FourthLevel4";
private static AuthorizationContext GetAuthorizationContext<TController>() where TController : ControllerBase, new() {
var controllerDescriptor = new ReflectedControllerDescriptor(typeof(TController));
var controllerContext = new ControllerContext(new StubHttpContext(), new RouteData(), new TController());
return new AuthorizationContext(
controllerContext,
controllerDescriptor.FindAction(controllerContext, "Index"));
[Test]
public void MockNavManagerWorks() {
var main = GetNavigationManager().Object.BuildMenu("main");
Assert.That(main.Count(), Is.EqualTo(1));
}
private static IAuthorizer GetAuthorizer(bool result) {
var authorizer = new Mock<IAuthorizer>();
authorizer
.Setup(x => x.Authorize(StandardPermissions.AccessAdminPanel, It.IsAny<LocalizedString>())).
Returns(result);
return authorizer.Object;
[Test]
public void FindSelectedPathScenario2() {
NavigationBuilder navigationBuilder = BuildMenuScenario2();
IEnumerable<MenuItem> menuItems = navigationBuilder.Build();
MenuItem firstLevel1 = FindMenuItem(menuItems, "X");
MenuItem secondLevel2 = FindMenuItem(menuItems, "B");
MenuItem thirdLevel2 = FindMenuItem(menuItems, "D");
MenuItem fourthLevel3 = FindMenuItem(menuItems, "G");
RouteData fourthLevel3RouteData = GetRouteData(fourthLevel3);
Stack<MenuItem> selectionStack = MenuFilterAccessor.FindSelectedPathAccessor(menuItems, fourthLevel3RouteData);
Assert.That(selectionStack.Pop(), Is.EqualTo(firstLevel1));
Assert.That(selectionStack.Pop(), Is.EqualTo(secondLevel2));
Assert.That(selectionStack.Pop(), Is.EqualTo(thirdLevel2));
Assert.That(selectionStack.Pop(), Is.EqualTo(fourthLevel3));
Assert.That(selectionStack.Count, Is.EqualTo(0));
}
[Test]
public void FindParentLocalTaskScenario2() {
NavigationBuilder navigationBuilder = BuildMenuScenario2();
IEnumerable<MenuItem> menuItems = navigationBuilder.Build();
MenuItem fourthLevel3 = FindMenuItem(menuItems, "G");
RouteData fourthLevel3RouteData = GetRouteData(fourthLevel3);
Stack<MenuItem> selectedPath = MenuFilterAccessor.FindSelectedPathAccessor(menuItems, fourthLevel3RouteData);
MenuItem parentNode = MenuFilterAccessor.FindParentLocalTaskAccessor(selectedPath);
Assert.That(parentNode, Is.EqualTo(FindMenuItem(menuItems, "B")));
}
private static Mock<INavigationManager> GetNavigationManager() {
var mainMenu = new[] { new MenuItem { Text = "The Main Menu" } };
var adminMenu = new[] { new MenuItem { Text = "The Admin Menu" } };
@ -37,23 +71,76 @@ namespace Orchard.Tests.UI.Navigation {
return navigationManager;
}
[Test]
public void MockNavManagerWorks() {
var main = GetNavigationManager().Object.BuildMenu("main");
Assert.That(main.Count(), Is.EqualTo(1));
private static NavigationBuilder BuildMenuScenario1() {
NavigationBuilder navigationBuilder = new NavigationBuilder();
navigationBuilder.Add(new LocalizedString("X"), "0",
menu => menu
.Add(new LocalizedString("A"), "0", subMenu => subMenu.Action("Index", "Admin", new { area = "Area" })
.Add(new LocalizedString("B"), "0", item => item.Action("Index", "Admin", new { area = "Area" }))
.Add(new LocalizedString("C"), "1", item => item.Action("Index", "Admin", new { area = "Area" }).LocalTask())))
.Add(new LocalizedString("D"), "1", subMenu => subMenu.Action("Index", "Admin", new { area = "Area" }).LocalTask()
.Add(new LocalizedString("E"), "0", item => item.Action("Index", "Admin", new { area = "Area" }))
.Add(new LocalizedString("F"), "1", item => item.Action("Index", "Admin", new { area = "Area" }).LocalTask()));
return navigationBuilder;
}
}
private static NavigationBuilder BuildMenuScenario2() {
NavigationBuilder navigationBuilder = new NavigationBuilder();
navigationBuilder.Add(new LocalizedString("X"), "0",
menu => menu
.Add(new LocalizedString("A"), "0", item => item.Action(SecondLevel1Action, "Admin", new { area = "Area" }))
.Add(new LocalizedString("B"), "1",
subMenu => subMenu
.Add(new LocalizedString("C"), "0", item => item.Action(ThirdLevel1Action, "Admin", new { area = "Area" }).LocalTask())
.Add(new LocalizedString("D"), "1",
subSubMenu => subSubMenu.LocalTask()
.Add(new LocalizedString("E"), "0", item => item.Action(FourthLevel1Action, "Admin", new { area = "Area" }).LocalTask())
.Add(new LocalizedString("F"), "1", item => item.Action(FourthLevel2Action, "Admin", new { area = "Area" }).LocalTask())
.Add(new LocalizedString("G"), "2", item => item.Action(FourthLevel3Action, "Admin", new { area = "Area" }))
.Add(new LocalizedString("W"), "3", item => item.Action(FourthLevel4Action, "Admin", new { area = "Area" })))));
public class NormalController : Controller {
public ActionResult Index() {
return View();
return navigationBuilder;
}
}
public class AdminController : Controller {
public ActionResult Index() {
return View();
protected static MenuItem FindMenuItem(IEnumerable<MenuItem> menuItems, string text) {
Queue<MenuItem> remainingItems = new Queue<MenuItem>(menuItems);
while (remainingItems.Count > 0) {
MenuItem currentMenuItem = remainingItems.Dequeue();
if (currentMenuItem.Text.Equals(text)) {
return currentMenuItem;
}
currentMenuItem.Items.ForEach(remainingItems.Enqueue);
}
return null;
}
private static RouteData GetRouteData(MenuItem menuItem) {
RouteData routeData = new RouteData();
routeData.Values["area"] = menuItem.RouteValues["area"];
routeData.Values["controller"] = menuItem.RouteValues["controller"];
routeData.Values["action"] = menuItem.RouteValues["action"];
return routeData;
}
private class MenuFilterAccessor : MenuFilter {
public MenuFilterAccessor(INavigationManager navigationManager,
IWorkContextAccessor workContextAccessor,
IShapeFactory shapeFactory) :
base(navigationManager, workContextAccessor, shapeFactory) {}
public static Stack<MenuItem> FindSelectedPathAccessor(IEnumerable<MenuItem> menuItems, RouteData currentRouteData) {
return SetSelectedPath(menuItems, currentRouteData);
}
public static MenuItem FindParentLocalTaskAccessor(Stack<MenuItem> selectedPath) {
return FindParentLocalTask(selectedPath);
}
}
}
}

View File

@ -4,8 +4,7 @@ Author: Orchard Team http://www.orchardproject.net
Copyright: 2010, Orchard. All Rights Reserved
*/
/* Color Palette
/* Color Palette
**************************************************************
Background: #2d2f25
@ -16,8 +15,6 @@ Main Accent:
Links: 1e5d7d
*/
/* Reset
***************************************************************/
@ -61,8 +58,7 @@ header, footer, aside, nav, article { display: block; }
/* Clearing Floats
***************************************************************/
.group:after
{
.group:after {
content: ".";
display: block;
height: 0;
@ -109,7 +105,7 @@ html {
color:#333;
}
body {
body {
font-size: 81.3%;
color: #333;
background: #fff;
@ -183,7 +179,7 @@ number of columns: 24; actual width: 946; column width: 26; gutter width:14
#header {
overflow:hidden;
}
#content {
#layout-content {
overflow:auto;
padding:1.4em;
background:#fcfcfc;
@ -305,6 +301,12 @@ form.link button:hover {
padding:.4em 0 0 .4em;
font-size:1.308em;
}
.menu-local-admin li {
display: inline;
}
.menu-local-admin li.middle {
padding-left: 10px;
}
.section-new {
border-top:1px solid #d3d3d3;
border-bottom:1px solid #d3d3d3;

View File

@ -4,3 +4,4 @@ Author: Jon Wall
Tags: hidden, admin
Description: An admin theme not to be used for the site so don't click "Activate" (or "Uninstall"). In the near future admin themes won't be mixed in with site themes.
Website: http://www.orchardproject.net
Zones: Header, Messages, BeforeContent, LocalNavigation, Content, AfterContent, Footer

View File

@ -7,33 +7,75 @@
@{
Style.Include("site.css");
Style.Include("ie.css").UseCondition("lte IE 8").SetAttribute("media", "screen, projection");
Style.Include("ie6.css").UseCondition("lte IE 6").SetAttribute("media", "screen, projection");
Style.Include("ie.css").UseCondition("lte IE 8").SetAttribute("media", "screen, projection");
Style.Include("ie6.css").UseCondition("lte IE 6").SetAttribute("media", "screen, projection");
Script.Require("jQuery");
Script.Require("ShapesBase");
Script.Include("admin.js");
/* Some useful shortcuts or settings ***************************************************************/ Func<dynamic, dynamic> Zone = x => Display(x); // Zone as an alias for Display to help make it obvious when we're displaying zones /* Inserting some ad hoc shapes ***************************************************************/
// these are just hacked together to fire existing partials... can change
Model.Header.Add(Display.Header());
// experimentation
var thisUser = Html.Resolve<IAuthenticationService>().GetAuthenticatedUser();
Model.Header.Add(Display.User(CurrentUser: thisUser));
Model.Footer.Add(Display.OrchardVersion());
}
@if (Model.Header != null) {
<div id="header" role="banner">
@Display(Model.Header)</div>
<div id="content">
@Zone(Model.Header)
</div>
}
<div id="layout-content">
<div id="navshortcut">
<a href="#Menu-admin">
@T("Skip to navigation")</a></div>
@T("Skip to navigation")
</a>
</div>
<div id="main" role="main">
@if (Model.Messages != null) {
<div id="messages">
@Display(Model.Messages)
@Zone(Model.Messages)
</div>
@Display(Model.Content)</div>
}
@if (Model.BeforeContent != null) {
<div id="before-content">
@Zone(Model.BeforeContent)
</div>
}
@if (Model.LocalNavigation != null) {
<div id="before-content">
@Zone(Model.LocalNavigation)
</div>
}
@if (Model.Content != null) {
<div id="content">
@Zone(Model.Content)
</div>
}
@if (Model.AfterContent != null) {
<div id="after-content">
@Zone(Model.AfterContent)
</div>
}
</div>
@if (Model.Navigation != null) {
<div id="menu">
@Display(Model.Navigation)</div>
@Zone(Model.Navigation)
</div>
}
</div>
@if (Model.Footer != null) {
<div id="footer" role="contentinfo">
@Display(Model.Footer)
</div>
@Zone(Model.Footer)
</div>
}

View File

@ -31,9 +31,9 @@
var firstLevelTag = Tag(firstLevelMenuItem, "li");
@firstLevelTag.StartElement
<h3>@sectionHeaderMarkup</h3>
if (secondLevelMenuItems.Count() > 1 || !firstLevelMenuItem.LinkToFirstChild) {
if (secondLevelMenuItems.Where(menuItem => !menuItem.LocalNav).Count() > 1 || !firstLevelMenuItem.LinkToFirstChild) {
<ul class="menuItems">
@foreach(var secondLevelMenuItem in secondLevelMenuItems) {
@foreach (var secondLevelMenuItem in secondLevelMenuItems.Where(menuItem => !menuItem.LocalNav)) {
<li>
<a href="@secondLevelMenuItem.Href">@secondLevelMenuItem.Text</a>
</li>

View File

@ -0,0 +1,41 @@
@using System.Web.Routing;
@using Orchard.Utility.Extensions;
@{
IEnumerable<dynamic> firstLevelMenuItems = Model;
Model.Attributes.Add("role", "local-navigation");
var tag = Tag(Model, "ul");
}
@tag.StartElement
@foreach(var firstLevelMenuItem in Model) {
if (firstLevelMenuItem.LocalNav) {
string sectionHeaderText = firstLevelMenuItem.Text;
var sectionHeaderMarkup = firstLevelMenuItem.RouteValues != null || HasText(firstLevelMenuItem.Url)
? Html.Link(sectionHeaderText, (string)firstLevelMenuItem.Href)
: new HtmlString(string.Format("<span>{0}</span>", Html.Encode(sectionHeaderText)));
if (firstLevelMenuItem == firstLevelMenuItems.First()) {
firstLevelMenuItem.Classes.Add("first");
}
if (firstLevelMenuItem != firstLevelMenuItems.First()) {
firstLevelMenuItem.Classes.Add("middle");
}
if (firstLevelMenuItem == firstLevelMenuItems.Last()) {
firstLevelMenuItem.Classes.Add("last");
}
if (firstLevelMenuItem.Selected) {
firstLevelMenuItem.Classes.Add("selected");
}
firstLevelMenuItem.Classes.Add("local-section-" + sectionHeaderText.HtmlClassify());
var firstLevelTag = Tag(firstLevelMenuItem, "li");
@firstLevelTag.StartElement
@sectionHeaderMarkup
@firstLevelTag.EndElement
}
}
@tag.EndElement

View File

@ -119,6 +119,9 @@
<ItemGroup>
<Content Include="TheThemeMachine\Placement.info" />
</ItemGroup>
<ItemGroup>
<Content Include="TheAdmin\Views\Menu__local_admin.cshtml" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.DisplayManagement;
using Orchard.Mvc.Filters;
using Orchard.UI.Admin;
@ -11,10 +13,10 @@ namespace Orchard.UI.Navigation {
private readonly IWorkContextAccessor _workContextAccessor;
private readonly dynamic _shapeFactory;
public MenuFilter(
INavigationManager navigationManager,
IWorkContextAccessor workContextAccessor,
public MenuFilter(INavigationManager navigationManager,
IWorkContextAccessor workContextAccessor,
IShapeFactory shapeFactory) {
_navigationManager = navigationManager;
_workContextAccessor = workContextAccessor;
_shapeFactory = shapeFactory;
@ -22,35 +24,37 @@ namespace Orchard.UI.Navigation {
public void OnResultExecuting(ResultExecutingContext filterContext) {
// should only run on a full view rendering result
if (!(filterContext.Result is ViewResult))
if (!(filterContext.Result is ViewResult)) {
return;
}
var workContext = _workContextAccessor.GetContext(filterContext);
WorkContext workContext = _workContextAccessor.GetContext(filterContext);
var menuName = "main";
if (AdminFilter.IsApplied(filterContext.RequestContext))
string menuName = "main";
if (AdminFilter.IsApplied(filterContext.RequestContext)) {
menuName = "admin";
}
var menuItems = _navigationManager.BuildMenu(menuName);
IEnumerable<MenuItem> menuItems = _navigationManager.BuildMenu(menuName);
var menuShape = _shapeFactory.Menu().MenuName(menuName);
// Set the currently selected path
Stack<MenuItem> selectedPath = SetSelectedPath(menuItems, filterContext.RouteData);
// Populate main nav
dynamic menuShape = _shapeFactory.Menu().MenuName(menuName);
PopulateMenu(_shapeFactory, menuShape, menuShape, menuItems);
workContext.Layout.Navigation.Add(menuShape);
// Populate local nav
dynamic localMenuShape = _shapeFactory.Menu().MenuName(string.Format("local_{0}", menuName));
PopulateLocalMenu(_shapeFactory, localMenuShape, localMenuShape, selectedPath);
workContext.Layout.LocalNavigation.Add(localMenuShape);
}
private void PopulateMenu(dynamic shapeFactory, dynamic parentShape, dynamic menu, IEnumerable<MenuItem> menuItems) {
foreach (MenuItem menuItem in menuItems) {
dynamic menuItemShape = BuildMenuItemShape(shapeFactory, parentShape, menu, menuItem);
foreach (var menuItem in menuItems) {
var menuItemShape = shapeFactory.MenuItem()
.Text(menuItem.Text)
.Href(menuItem.Href)
.LinkToFirstChild(menuItem.LinkToFirstChild)
.RouteValues(menuItem.RouteValues)
.Item(menuItem)
.Menu(menu)
.Parent(parentShape);
if (menuItem.Items != null && menuItem.Items.Any()) {
PopulateMenu(shapeFactory, menuItemShape, menu, menuItem.Items);
}
@ -59,6 +63,108 @@ namespace Orchard.UI.Navigation {
}
}
/// <summary>
/// Populates the local menu starting from the first non local task parent.
/// </summary>
/// <param name="shapeFactory">The shape factory.</param>
/// <param name="parentShape">The menu parent shape.</param>
/// <param name="menu">The menu shape.</param>
/// <param name="selectedPath">The selection path.</param>
protected void PopulateLocalMenu(dynamic shapeFactory, dynamic parentShape, dynamic menu, Stack<MenuItem> selectedPath) {
MenuItem parentMenuItem = FindParentLocalTask(selectedPath);
// find childs tabs and expand them
if (parentMenuItem != null && parentMenuItem.Items != null && parentMenuItem.Items.Any()) {
PopulateMenu(shapeFactory, parentShape, menu, parentMenuItem.Items);
}
}
/// <summary>
/// Identifies the currently selected path, starting from the selected node.
/// </summary>
/// <param name="menuItems">All the menuitems in the navigation menu.</param>
/// <param name="currentRouteData">The current route data.</param>
/// <returns>A stack with the selection path being the last node the currently selected one.</returns>
protected static Stack<MenuItem> SetSelectedPath(IEnumerable<MenuItem> menuItems, RouteData currentRouteData) {
foreach(MenuItem menuItem in menuItems) {
if (RouteMatches(menuItem, currentRouteData)) {
menuItem.Selected = true;
Stack<MenuItem> selectedPath = new Stack<MenuItem>();
selectedPath.Push(menuItem);
return selectedPath;
}
if (menuItem.Items != null && menuItem.Items.Any()) {
Stack<MenuItem> selectedPath = SetSelectedPath(menuItem.Items, currentRouteData);
if (selectedPath != null) {
menuItem.Selected = true;
selectedPath.Push(menuItem);
return selectedPath;
}
}
}
return null;
}
/// <summary>
/// Find the first level in the selection path, starting from the bottom, that is not a local task.
/// </summary>
/// <param name="selectedPath">The selection path stack. The bottom node is the currently selected one.</param>
/// <returns>The first node, starting from the bottom, that is not a local task. Otherwise, null.</returns>
protected static MenuItem FindParentLocalTask(Stack<MenuItem> selectedPath) {
if (selectedPath != null) {
MenuItem parentMenuItem = selectedPath.Pop();
if (parentMenuItem != null) {
while (selectedPath.Count > 0) {
MenuItem currentMenuItem = selectedPath.Pop();
if (currentMenuItem.LocalNav) {
return parentMenuItem;
}
parentMenuItem = currentMenuItem;
}
}
}
return null;
}
/// <summary>
/// Determines if a menu item corresponds to a given route.
/// </summary>
/// <param name="menuItem">The menu item.</param>
/// <param name="currentRouteData">The route data.</param>
/// <returns>True if the menu item's action corresponds to the route data; false otherwise.</returns>
protected static bool RouteMatches(MenuItem menuItem, RouteData currentRouteData) {
return menuItem.RouteValues != null &&
string.Equals((string) menuItem.RouteValues["area"], (string) currentRouteData.Values["area"], StringComparison.OrdinalIgnoreCase) &&
string.Equals((string) menuItem.RouteValues["controller"], (string) currentRouteData.Values["controller"], StringComparison.OrdinalIgnoreCase) &&
string.Equals((string) menuItem.RouteValues["action"], (string) currentRouteData.Values["action"], StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Builds a menu item shape.
/// </summary>
/// <param name="shapeFactory">The shape factory.</param>
/// <param name="parentShape">The parent shape.</param>
/// <param name="menu">The menu shape.</param>
/// <param name="menuItem">The menu item to build the shape for.</param>
/// <returns>The menu item shape.</returns>
protected dynamic BuildMenuItemShape(dynamic shapeFactory, dynamic parentShape, dynamic menu, MenuItem menuItem) {
return shapeFactory.MenuItem()
.Text(menuItem.Text)
.Href(menuItem.Href)
.LinkToFirstChild(menuItem.LinkToFirstChild)
.LocalNav(menuItem.LocalNav)
.Selected(menuItem.Selected)
.RouteValues(menuItem.RouteValues)
.Item(menuItem)
.Menu(menu)
.Parent(parentShape);
}
public void OnResultExecuted(ResultExecutedContext filterContext) { }
}
}

View File

@ -15,6 +15,8 @@ namespace Orchard.UI.Navigation {
public string Href { get; set; }
public string Position { get; set; }
public bool LinkToFirstChild { get; set; }
public bool LocalNav { get; set; }
public bool Selected { get; set; }
public RouteValueDictionary RouteValues { get; set; }
public IEnumerable<MenuItem> Items { get; set; }
public IEnumerable<Permission> Permissions { get; set; }

View File

@ -38,6 +38,11 @@ namespace Orchard.UI.Navigation {
return this;
}
public NavigationItemBuilder LocalTask(bool value = true) {
_item.LocalNav = value;
return this;
}
public new IEnumerable<MenuItem> Build() {
_item.Items = base.Build();
return new[] { _item };

View File

@ -68,6 +68,7 @@ namespace Orchard.UI.Navigation {
Permissions = item.Permissions,
Position = item.Position,
RouteValues = item.RouteValues,
LocalNav = item.LocalNav,
Text = item.Text,
Url = item.Url,
LinkToFirstChild = item.LinkToFirstChild,