Skip to content

Commit

Permalink
Merge pull request #30 from adnanzameer/feature/categories-hierarchy-…
Browse files Browse the repository at this point in the history
…handling

categories hierarchy handling & routing improvements
  • Loading branch information
ErvinsA authored Jan 23, 2025
2 parents f6c0241 + f4ec076 commit 3f06ae0
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 33 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ Instead of going to admin mode to manage categories, you now do it in edit mode,
- Possibility to hide root categories based on [AllowedTypes] setting
- ShowDefaultCategoryProperty - default to **false**
- Allows you to show the default Episerver category property
- UseUrlPathForCategoryRetrieval - default to **false**
- Enables retrieval of categories that match the exact URL path instead of searching for a URL segment.

You can configure categories in Startup.cs. Below is an example where we change the category seperator:

Expand All @@ -102,7 +104,8 @@ In addition, the configuration can be read from the `appsettings.json`:
"CategorySeparator": "__",
"DisableCategoryAsLinkableType": false,
"HideDisallowedRootCategories": false,
"ShowDefaultCategoryProperty": false
"ShowDefaultCategoryProperty": false,
"UseUrlPathForCategoryRetrieval": false
}
}
```
Expand Down Expand Up @@ -148,6 +151,10 @@ public interface ICategoryContentLoader
IEnumerable<T> GetSiteCategories<T>() where T : CategoryData;
//... + overloads
bool TryGet<T>(ContentReference categoryLink, out T category) where T : CategoryData;
//... + overloads
IEnumerable<T> GetCategoriesBySegment<T>(string urlSegment) where T : CategoryData;
//... + overloads
T GetCategoryByPath<T>(string path) where T : CategoryData;
}
```

Expand All @@ -173,7 +180,7 @@ Two routes are mapped during initialization. One for site categories and one for

![for this site routing](/docs/for-this-site.jpg)

Using above example, the URL "/siteassets/topics/sports/" would be routed to the site category called "Sports". Similarly you could go to "/globalassets/topics/global-category-1" for the global category "Global category 1".
As illustrated in the given example, if the UseUrlPathForCategoryRetrieval is enabled (set to true), navigating to the URL "/topics/sports/" directs you to the site's "Sports" category. In a similar manner, accessing "/global-topics/global-category-1" takes you to the "Global category 1". On the other hand, when UseUrlPathForCategoryRetrieval is disabled (set to false), the URL "/topics/sports/" will retrieve all categories associated with the segment "sports". Likewise, the URL "/global-topics/global-category-1" will fetch all categories, both global and site-specific, linked with the segment "global-category-1"

### ICategoryRoutableContent interface

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ namespace Geta.Optimizely.Categories.Configuration
public class CategoriesOptions
{
public string CategorySeparator { get; set; } = "__";
public bool UseUrlPathForCategoryRetrieval { get; set; }
public bool DisableCategoryAsLinkableType { get; set; }
public bool HideDisallowedRootCategories { get; set; }
public bool ShowDefaultCategoryProperty { get; set; }
}
}
}
120 changes: 117 additions & 3 deletions src/Geta.Optimizely.Categories/DefaultCategoryContentLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ public class DefaultCategoryContentLoader : ICategoryContentLoader
{
protected readonly IContentRepository ContentRepository;
protected readonly LanguageResolver LanguageResolver;

public DefaultCategoryContentLoader(IContentRepository contentRepository, LanguageResolver languageResolver)
protected readonly CategorySettings CategorySettings;
public DefaultCategoryContentLoader(IContentRepository contentRepository, LanguageResolver languageResolver, CategorySettings categorySettings)
{
ContentRepository = contentRepository;
LanguageResolver = languageResolver;
CategorySettings = categorySettings;
}

public virtual T Get<T>(ContentReference categoryLink) where T : CategoryData
Expand Down Expand Up @@ -95,6 +96,105 @@ public virtual T GetFirstBySegment<T>(ContentReference parentLink, string urlSeg
return categories.FirstOrDefault(x => x.RouteSegment.Equals(urlSegment, StringComparison.InvariantCultureIgnoreCase));
}

public IEnumerable<T> GetCategoriesBySegment<T>(string urlSegment) where T : CategoryData
{
return GetCategoriesBySegment<T>(urlSegment, CreateDefaultLoadOptions());
}

public IEnumerable<T> GetCategoriesBySegment<T>(string urlSegment, CultureInfo culture) where T : CategoryData
{
var loaderOptions = new LoaderOptions
{
LanguageLoaderOption.Specific(culture)
};

return GetCategoriesBySegment<T>(urlSegment, loaderOptions);
}

public IEnumerable<T> GetCategoriesBySegment<T>(string urlSegment, LoaderOptions loaderOptions) where T : CategoryData
{
var categories = new List<T>();

if (SiteDefinition.Current.SiteAssetsRoot != SiteDefinition.Current.GlobalAssetsRoot)
{
var siteCategories =
GetCategoriesBySegment<T>(ContentRepository.GetOrCreateSiteCategoriesRoot(), urlSegment, loaderOptions);

if (siteCategories != null && siteCategories.Any())
{
categories.AddRange(siteCategories);
}
}

var globalCategories = GetCategoriesBySegment<T>(ContentRepository.GetOrCreateGlobalCategoriesRoot(), urlSegment, loaderOptions);

if (globalCategories != null && globalCategories.Any())
{
categories.AddRange(globalCategories);
}

return categories;
}

public virtual IEnumerable<T> GetCategoriesBySegment<T>(ContentReference parentLink, string urlSegment, LoaderOptions loaderOptions) where T : CategoryData
{
var descendents = ContentRepository.GetDescendents(parentLink);

var categories = ContentRepository
.GetItems(descendents, loaderOptions)
.OfType<T>();

return categories.Where(x => x.RouteSegment.Equals(urlSegment, StringComparison.InvariantCultureIgnoreCase));
}

public T GetCategoryByPath<T>(string path) where T : CategoryData
{
return GetCategoryByPath<T>(path, CreateDefaultLoadOptions());
}

public T GetCategoryByPath<T>(string path, CultureInfo culture) where T : CategoryData
{
var loaderOptions = new LoaderOptions
{
LanguageLoaderOption.Specific(culture)
};

return GetCategoryByPath<T>(path, loaderOptions);
}

public virtual T GetCategoryByPath<T>(string path, LoaderOptions loaderOptions) where T : CategoryData
{
if (SiteDefinition.Current.SiteAssetsRoot != SiteDefinition.Current.GlobalAssetsRoot)
{
var siteCategory = GetCategoryByPath<T>(ContentRepository.GetOrCreateSiteCategoriesRoot(), path, loaderOptions);

if (siteCategory != null)
{
return siteCategory;
}
}

return GetCategoryByPath<T>(ContentRepository.GetOrCreateGlobalCategoriesRoot(), path, loaderOptions);
}


public virtual T GetCategoryByPath<T>(ContentReference parentLink, string path, LoaderOptions loaderOptions) where T : CategoryData
{
var trimmedUrl = path.TrimEnd('/');
var urlSegment = trimmedUrl.Split('/').Last();

// Efficiently fetch descendants once and filter in-memory.
var descendants = ContentRepository.GetDescendents(parentLink);
var categories = ContentRepository
.GetItems(descendants, loaderOptions)
.OfType<T>()
.Where(x => x.RouteSegment.Equals(urlSegment, StringComparison.InvariantCultureIgnoreCase));

return categories
.FirstOrDefault(
category => CategoryPath(category).Equals(trimmedUrl, StringComparison.InvariantCultureIgnoreCase));
}

public virtual IEnumerable<T> GetGlobalCategories<T>() where T : CategoryData
{
return GetChildren<T>(ContentRepository.GetOrCreateGlobalCategoriesRoot());
Expand Down Expand Up @@ -145,5 +245,19 @@ protected virtual LoaderOptions CreateDefaultListOptions()
LanguageLoaderOption.Fallback(LanguageResolver.GetPreferredCulture())
};
}

private string CategoryPath(CategoryData category, string path = "")
{
path = "/" + category.RouteSegment + path;

if (ContentRepository.TryGet<CategoryData>(category.ParentLink, out var parentCategory) && parentCategory != null)
{
return CategoryPath(parentCategory, path);
}

path = path.TrimEnd('/');

return path.TrimStart('/');
}
}
}
}
18 changes: 17 additions & 1 deletion src/Geta.Optimizely.Categories/ICategoryContentLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ public interface ICategoryContentLoader

T GetFirstBySegment<T>(ContentReference parentLink, string urlSegment, LoaderOptions loaderOptions) where T : CategoryData;

IEnumerable<T> GetCategoriesBySegment<T>(string urlSegment) where T : CategoryData;

IEnumerable<T> GetCategoriesBySegment<T>(string urlSegment, CultureInfo culture) where T : CategoryData;

IEnumerable<T> GetCategoriesBySegment<T>(string urlSegment, LoaderOptions loaderOptions) where T : CategoryData;

IEnumerable<T> GetCategoriesBySegment<T>(ContentReference parentLink, string urlSegment, LoaderOptions loaderOptions) where T : CategoryData;

T GetCategoryByPath<T>(string path) where T : CategoryData;

T GetCategoryByPath<T>(string path, CultureInfo culture) where T : CategoryData;

T GetCategoryByPath<T>(string path, LoaderOptions loaderOptions) where T : CategoryData;

T GetCategoryByPath<T>(ContentReference parentLink, string path, LoaderOptions loaderOptions) where T : CategoryData;

IEnumerable<T> GetGlobalCategories<T>() where T : CategoryData;

IEnumerable<T> GetGlobalCategories<T>(CultureInfo culture) where T : CategoryData;
Expand All @@ -39,4 +55,4 @@ public interface ICategoryContentLoader

bool TryGet<T>(ContentReference categoryLink, out T category) where T : CategoryData;
}
}
}
32 changes: 27 additions & 5 deletions src/Geta.Optimizely.Categories/Routing/CategoryModelBinder.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
// Copyright (c) Geta Digital. All rights reserved.
// Licensed under Apache-2.0. See the LICENSE file in the project root for more information

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EPiServer.Core;
using EPiServer.Globalization;
using EPiServer.Shell.Web;
using EPiServer.Web.Routing;
using Geta.Optimizely.Categories.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;

namespace Geta.Optimizely.Categories.Routing
{
public class CategoryModelBinder : IModelBinder
{
private readonly ICategoryContentLoader _categoryContentLoader;
private readonly IPageRouteHelper _pageRouteHelper;
private readonly CategoriesOptions _configuration;

private readonly string _trailingSlash = "/";

public CategoryModelBinder(
ICategoryContentLoader categoryContentLoader,
IPageRouteHelper pageRouteHelper)
IPageRouteHelper pageRouteHelper,
IOptions<CategoriesOptions> options)
{
_categoryContentLoader = categoryContentLoader;
_pageRouteHelper = pageRouteHelper;
_configuration = options.Value;
}

public Task BindModelAsync(ModelBindingContext bindingContext)
Expand All @@ -39,11 +48,24 @@ private IList<CategoryData> GetCategoriesFromRequest(HttpRequest request)
{
return new List<CategoryData>();
}

var culture = (_pageRouteHelper.Content as ILocale).Language ?? ContentLanguage.PreferredCulture;

return categorySegments.Select(x => _categoryContentLoader.GetFirstBySegment<CategoryData>(x, culture))
.ToList();
var preferredCulture = (_pageRouteHelper.Content as ILocale).Language ?? ContentLanguage.PreferredCulture;

var categories = new List<CategoryData>();

foreach (var categorySegment in categorySegments)
{
if (_configuration.UseUrlPathForCategoryRetrieval)
{
categories.Add(_categoryContentLoader.GetCategoryByPath<CategoryData>(categorySegment, preferredCulture));
}
else
{
categories.AddRange(_categoryContentLoader.GetCategoriesBySegment<CategoryData>(categorySegment, preferredCulture));
}
}

return categories.Distinct().ToList();
}
}
}
Loading

0 comments on commit 3f06ae0

Please sign in to comment.