Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying no group to render a shape #16903

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ namespace OrchardCore.DisplayManagement.Descriptors;

public class PlacementInfo
{
private static readonly char[] _delimiters = [':', '#', '@', '%', '|'];
public const string HiddenLocation = "-";

public const char PositionDelimiter = ':';

public const char TabDelimiter = '#';

public const char GroupDelimiter = '@';

public const char CardDelimiter = '%';

public const char ColumnDelimiter = '|';

private static readonly char[] _delimiters = [PositionDelimiter, TabDelimiter, GroupDelimiter, CardDelimiter, ColumnDelimiter];

public string Location { get; set; }
public string Source { get; set; }
Expand All @@ -15,7 +27,13 @@ public class PlacementInfo

public bool IsLayoutZone()
{
return Location.StartsWith("/", StringComparison.OrdinalIgnoreCase);
return Location.StartsWith('/');
}

public bool IsHidden()
{
// If there are no placement or it's explicitly noop then its hidden.
return string.IsNullOrEmpty(Location) || Location == HiddenLocation;
}

/// <summary>
Expand Down Expand Up @@ -49,7 +67,7 @@ public string[] GetZones()

public string GetPosition()
{
var contentDelimiter = Location.IndexOf(':');
var contentDelimiter = Location.IndexOf(PositionDelimiter);
if (contentDelimiter == -1)
{
return DefaultPosition ?? "";
Expand All @@ -66,7 +84,7 @@ public string GetPosition()

public string GetTab()
{
var tabDelimiter = Location.IndexOf('#');
var tabDelimiter = Location.IndexOf(TabDelimiter);
if (tabDelimiter == -1)
{
return "";
Expand All @@ -87,7 +105,7 @@ public string GetTab()
/// </summary>
public string GetGroup()
{
var groupDelimiter = Location.IndexOf('@');
var groupDelimiter = Location.IndexOf(GroupDelimiter);
if (groupDelimiter == -1)
{
return null;
Expand All @@ -108,7 +126,7 @@ public string GetGroup()
/// </summary>
public string GetCard()
{
var cardDelimiter = Location.IndexOf('%');
var cardDelimiter = Location.IndexOf(CardDelimiter);
if (cardDelimiter == -1)
{
return null;
Expand All @@ -129,18 +147,18 @@ public string GetCard()
/// </summary>
public string GetColumn()
{
var colDelimeter = Location.IndexOf('|');
if (colDelimeter == -1)
var colDelimiter = Location.IndexOf(ColumnDelimiter);
if (colDelimiter == -1)
{
return null;
}

var nextDelimiter = Location.IndexOfAny(_delimiters, colDelimeter + 1);
var nextDelimiter = Location.IndexOfAny(_delimiters, colDelimiter + 1);
if (nextDelimiter == -1)
{
return Location[(colDelimeter + 1)..];
return Location[(colDelimiter + 1)..];
}

return Location[(colDelimeter + 1)..nextDelimiter];
return Location[(colDelimiter + 1)..nextDelimiter];
}
}
49 changes: 34 additions & 15 deletions src/OrchardCore/OrchardCore.DisplayManagement/Views/ShapeResult.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Primitives;
using OrchardCore.DisplayManagement.Descriptors;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Implementation;
Expand All @@ -9,17 +10,19 @@ namespace OrchardCore.DisplayManagement.Views;

public class ShapeResult : IDisplayResult
{
private readonly string _shapeType;
private readonly Func<IBuildShapeContext, ValueTask<IShape>> _shapeBuilder;
private readonly Func<IShape, Task> _initializingAsync;

private string _defaultLocation;
private Dictionary<string, string> _otherLocations;
private string _name;
private string _differentiator;
private string _prefix;
private string _cacheId;
private readonly string _shapeType;
private readonly Func<IBuildShapeContext, ValueTask<IShape>> _shapeBuilder;
private readonly Func<IShape, Task> _initializingAsync;
private Dictionary<string, string> _otherLocations;
private StringValues _groupIds;

private Action<CacheContext> _cache;
private string _groupId;
private Action<ShapeDisplayContext> _displaying;
private Func<IShape, Task> _processingAsync;
private Func<Task<bool>> _renderPredicateAsync;
Expand Down Expand Up @@ -82,31 +85,44 @@ private async Task ApplyImplementationAsync(BuildShapeContext context, string di
}

// If no placement is found, use the default location.
placement ??= new PlacementInfo() { Location = _defaultLocation };
placement ??= new PlacementInfo
{
Location = _defaultLocation,
};

// If a placement was found without actual location, use the default.
// It can happen when just setting alternates or wrappers for instance.
placement.Location ??= _defaultLocation;

placement.DefaultPosition ??= context.DefaultPosition;

// If there are no placement or it's explicitly noop then stop rendering execution.
if (string.IsNullOrEmpty(placement.Location) || placement.Location == "-")
// If the placement should be hidden, then stop rendering execution.
if (placement.IsHidden())
{
return;
}

// Parse group placement.
_groupId = placement.GetGroup() ?? _groupId;
var groupId = placement.GetGroup();

if (!string.IsNullOrEmpty(groupId))
{
OnGroup(groupId);
}

// If the shape's group doesn't match the currently rendered one, return.
if (!string.Equals(context.GroupId ?? string.Empty, _groupId ?? string.Empty, StringComparison.OrdinalIgnoreCase))
if (_groupIds.Count > 0 && !_groupIds.Contains(context.GroupId ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
return;
}

if (_groupIds.Count == 0 && !string.IsNullOrEmpty(context.GroupId))
{
return;
}

// If a condition has been applied to this result evaluate it only if the shape has been placed.
if (_renderPredicateAsync != null && !(await _renderPredicateAsync()))
if (_renderPredicateAsync != null && !await _renderPredicateAsync())
{
return;
}
Expand Down Expand Up @@ -293,13 +309,16 @@ public ShapeResult Differentiator(string differentiator)
}

/// <summary>
/// Sets the group identifier the shape will be rendered in.
/// Sets the group identifiers the shape will be rendered in.
/// </summary>
/// <param name="groupId"></param>
/// <param name="groupIds"></param>
/// <returns></returns>
public ShapeResult OnGroup(string groupId)
public ShapeResult OnGroup(params string[] groupIds)
{
_groupId = groupId;
ArgumentNullException.ThrowIfNull(groupIds);

_groupIds = StringValues.Concat(_groupIds, groupIds.Where(x => x != null).ToArray());

return this;
}

Expand Down
88 changes: 88 additions & 0 deletions test/OrchardCore.Tests/DisplayManagement/ShapeResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Descriptors;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Implementation;
using OrchardCore.DisplayManagement.Layout;
using OrchardCore.DisplayManagement.Theming;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Environment.Extensions;
using OrchardCore.Scripting;
using OrchardCore.Tests.DisplayManagement.Stubs;
using OrchardCore.Tests.Stubs;

namespace OrchardCore.Tests.DisplayManagement;

public class ShapeResultTests
{
[Theory]
[InlineData("groupOne", "gRoUpTWo")] // case insensitive check.
[InlineData("groupOne", "groupOne")]
[InlineData("", "")]
[InlineData("", null)]
[InlineData(null, "")]
[InlineData(null, null)]
public async Task Shape_WhenCalled_ReturnShapeWhenGroupIsMatched(string groupId, string renderingGroupId)
{
var serviceProvider = GetServiceProvider(new GroupDisplayDriverStub(groupId));

var displayManager = serviceProvider.GetRequiredService<IDisplayManager<GroupModel>>();
var model = new GroupModel();

var shape = await displayManager.BuildEditorAsync(model, updater: null, isNew: false, groupId: renderingGroupId);

var testZone = shape.GetProperty<IShape>(GroupDisplayDriverStub.ZoneName);

Assert.NotNull(testZone);

var shapeModel = testZone.Items[0] as ShapeViewModel<GroupModel>;

Assert.NotNull(shapeModel);
Assert.Equal(shapeModel.Value.Value, model.Value);
}

[Theory]
[InlineData("groupOne", "groupTwo")]
[InlineData("", "groupTwo")]
[InlineData(null, "groupTwo")]
[InlineData("groupOne", "")]
[InlineData("groupOne", null)]
public async Task Shape_WhenCalled_ReturnNullWhenIncorrectGroupIsSpecified(string groupId, string renderingGroupId)
{
var serviceProvider = GetServiceProvider(new GroupDisplayDriverStub(groupId));

var displayManager = serviceProvider.GetRequiredService<IDisplayManager<GroupModel>>();
var model = new GroupModel();

var shape = await displayManager.BuildEditorAsync(model, updater: null, isNew: false, groupId: renderingGroupId);

var testZone = shape.GetProperty<IShape>(GroupDisplayDriverStub.ZoneName);

Assert.Null(testZone);
}

private static ServiceProvider GetServiceProvider(IDisplayDriver<GroupModel> driver)
{
var serviceCollection = new ServiceCollection();

serviceCollection.AddScripting()
.AddLogging()
.AddScoped<ILoggerFactory, NullLoggerFactory>()
.AddScoped<IThemeManager, ThemeManager>()
.AddScoped<IShapeFactory, DefaultShapeFactory>()
.AddScoped<IExtensionManager, StubExtensionManager>()
.AddScoped<IShapeTableManager, TestShapeTableManager>()
.AddScoped<ILayoutAccessor, LayoutAccessor>()
.AddScoped(sp => driver)
.AddScoped(typeof(IDisplayManager<>), typeof(DisplayManager<>));

var shapeTable = new ShapeTable
(
new Dictionary<string, ShapeDescriptor>(StringComparer.OrdinalIgnoreCase),
new Dictionary<string, ShapeBinding>(StringComparer.OrdinalIgnoreCase)
);

serviceCollection.AddSingleton(shapeTable);

return serviceCollection.BuildServiceProvider();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;

namespace OrchardCore.Tests.DisplayManagement.Stubs;

internal sealed class GroupDisplayDriverStub : DisplayDriver<GroupModel>
{
public const string ZoneName = "TestZone";

private readonly string[] _groupIds;

public GroupDisplayDriverStub(params string[] groupIds)
{
_groupIds = groupIds ?? [];
}

public override IDisplayResult Edit(GroupModel model, BuildEditorContext context)
{
var result = View("test", model)
.Location(ZoneName);

result.OnGroup(_groupIds);

return result;
}
}

internal sealed class GroupModel
{
public string Value { get; set; } = "some value";
}