Skip to content

Commit

Permalink
Merge pull request #79 from Lombiq/issue/OSOE-652
Browse files Browse the repository at this point in the history
OSOE-652: Better favicon inclusion in Lombiq.BaseTheme
  • Loading branch information
Psichorex authored Jul 6, 2023
2 parents b52fa1f + 5d1f29b commit 1447ea5
Show file tree
Hide file tree
Showing 23 changed files with 459 additions and 32 deletions.
17 changes: 8 additions & 9 deletions Lombiq.BaseTheme.Samples/Manifest.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
using OrchardCore.DisplayManagement.Manifest;
using static Lombiq.BaseTheme.Constants.FeatureIds;
using Lombiq.BaseTheme.Attributes;

// Theme manifests in Orchard Core are similar to module manifests (see "Module manifest" section in the Training Demo),
// except you have to use the Theme attribute and set the BaseTheme value to the constant at
// Lombiq.BaseTheme.Constants.FeatureIds.BaseTheme from the Lombiq.BaseTheme project.
[assembly: Theme(
// except you have to use the Theme attribute. DerivedTheme is a specific variant of Theme where the BaseTheme property
// is automatically set to the constant at Lombiq.BaseTheme.Constants.FeatureIds.BaseTheme from the Lombiq.BaseTheme
// project and it has some additional properties.
[assembly: DerivedTheme(
Name = "Lombiq Base Theme - Samples",
Author = "Lombiq Technologies",
Version = "0.0.1",
Website = "https://github.com/Lombiq/Orchard-Base-Theme",
Description = "A sample theme that builds on Lombiq Base Theme.",
// A base theme is another theme project. Orchard Core Display Management first searches your theme and then the
// base theme for template alternates. Besides that, it's similar to a dependency in modules, so any services
// registered in the base theme are also accessible.
BaseTheme = BaseTheme
// This is a new property in DerivedTheme. By setting it to a static resource you can define a default icon for this
// theme. You can define other "link" resources too, using the Link property.
Favicon = "~/Lombiq.BaseTheme.Samples/icons/favicon.ico"
)]

// Steps you need to do outside of this project:
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"steps": [
// You can add an icon as a regular media file and then pick it from the editor, or just have media and settings
// steps in your recipe like below.
{
"name": "media",
"Files": [
{
"SourcePath": "Icons/favicon.ico",
"TargetPath": "Icons/favicon.ico"
},
{
"SourcePath": "Icons/oc-favicon.ico",
"TargetPath": "Icons/oc-favicon.ico"
}
]
},
{
"name": "settings",
"BaseThemeSettings": {
// The HideMenu can be used if you don't want to use the Boostrap main menu widget injected by the base theme.
"HideMenu": false,
// You can set a single icon using a media path, so the same as the TargetPath in the recipe's media step.
"Icon": "Icons/favicon.ico",
// The time stamp is a UTC DateTime.Ticks value, you can make it any random long number in the recipe because
// the chance that the next save will have the exact same ticks is vanishingly low.
"TimeStamp": 638240128517358149
}
}
]
}

// END OF TRAINING SECTION: Set up favicon using recipe migrations
20 changes: 20 additions & 0 deletions Lombiq.BaseTheme.Samples/Migrations/RecipeMigrations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Lombiq.HelpfulLibraries.OrchardCore.Data;
using OrchardCore.Recipes.Services;

namespace Lombiq.BaseTheme.Samples.Migrations;

// Migrations based on the RecipeMigrationsBase class have a default CreateAsync method that invokes the recipe in the
// same directory called "{module-or-theme-id}.UpdateFrom0.recipe.json". For any subsequent update migrations, you can
// create an UpdateFrom1Async, UpdateFrom2Async, etc as usual, but all you have to put in it is ExecuteAsync(N) to
// invoke the corresponding "{module-or-theme-id}.UpdateFromN.recipe.json" recipe and return the incremented version
// number.
// If you just want a static default icon, check out the DerivedTheme.Favicon in Manifest.cs!
public class RecipeMigrations : RecipeMigrationsBase
{
public RecipeMigrations(IRecipeMigrator recipeMigrator)
: base(recipeMigrator)
{
}
}

// NEXT STATION: Migrations/Lombiq.BaseTheme.Samples.UpdateFrom0.recipe.json
1 change: 1 addition & 0 deletions Lombiq.BaseTheme.Samples/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ You can start with any of the top-level sections, but the indented sections shou
- [Layout injection](Views/Widget-LayoutInjection.cshtml)
- [Sass styling and structure](Assets/Styles/site.scss)
- [Front-end navigation via the `"main"` menu](Services/AccountNavigationProvider.cs)
- [Set up favicon using recipe migrations](Migrations/RecipeMigrations.cs)
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ protected override void Build(NavigationBuilder builder)
}

// END OF TRAINING SECTION: Front-end navigation via the "main" menu

// NEXT STATION: Migrations/RecipeMigrations.cs
21 changes: 6 additions & 15 deletions Lombiq.BaseTheme.Samples/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using Lombiq.BaseTheme.Samples.Migrations;
using Lombiq.DataTables.Samples.Navigation;
using Lombiq.HelpfulLibraries.OrchardCore.ResourceManagement;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OrchardCore.Data.Migration;
using OrchardCore.Modules;
using OrchardCore.Navigation;
using OrchardCore.ResourceManagement;
using System;
using System.Threading.Tasks;
using static Lombiq.BaseTheme.Samples.Constants.FeatureIds;

namespace Lombiq.BaseTheme.Samples;

Expand All @@ -24,24 +24,15 @@ public override void ConfigureServices(IServiceCollection services)

// This service provides configuration to the ResourceFilterMiddleware.
services.AddScoped<IResourceFilterProvider, ResourceFilters>();

// The recipe migration is used to add the media items and Base Theme settings required for the correct favicon.
services.AddDataMigration<RecipeMigrations>();
}

public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) =>
// In this theme we inject the style resources using the ResourceFilterMiddleware which needs to be enabled with
// this extension. See: https://github.com/Lombiq/Helpful-Libraries/blob/dev/Lombiq.HelpfulLibraries.OrchardCore/Docs/ResourceManagement.md
app.UseResourceFilters();

// Certain browsers expect the site's favicon to be in the default location and try to load it from anyway. If
// you add a <link> element you will still get an unnecessary lost GET request because of that, and of course it
// contributes to the page size. It's better to change what ~/favicon.ico means instead.
// See https://orcharddojo.net/blog/how-to-add-a-favicon-under-favicon-ico-in-orchard-core-orchard-nuggets
app.Map("/favicon.ico", appBuilder => appBuilder.Run(context =>
{
context.Response.Redirect($"/{BaseThemeSamples}/icons/favicon.ico", permanent: true);
return Task.CompletedTask;
}));
}
}

// NEXT STATION: Services/ResourceFilters.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Atata;
using Lombiq.Tests.UI.Extensions;
using Lombiq.Tests.UI.Services;
using OpenQA.Selenium;
Expand Down Expand Up @@ -87,4 +88,35 @@ await context.SetContentPickerByDisplayTextAsync(
.Trim()
.ShouldBe("My Content");
}

public static async Task TestBaseThemeSiteSettingsAsync(
this UITestContext context,
Func<Task> selectFromMediaLibraryAsync = null,
By byIcon = null)
{
await context.GoToAdminRelativeUrlAsync("/Lombiq.BaseTheme/Admin/Index");
await context.SetCheckboxValueAsync(By.Id("HideMenu"), isChecked: true);

var byDeleteButton = By.CssSelector("#Editor .delete-button").OfAnyVisibility();
while (context.Exists(byDeleteButton.Safely())) await context.ClickReliablyOnAsync(byDeleteButton);

selectFromMediaLibraryAsync ??= async () =>
{
await context.ClickReliablyOnAsync(By.XPath("//div[contains(@class, 'folder-name') and contains(., 'Icons')]"));
await context.ClickReliablyOnAsync(By.XPath(
"//tr[contains(@class, 'media-item') and .//div[contains(@class, 'media-name-cell') and contains(., ' oc-favicon.ico ')]]"));
await context.ClickReliablyOnAsync(By.ClassName("mediaFieldSelectButton"));
};
byIcon ??= By.CssSelector("head link[href*='/media/Icons/oc-favicon.ico'][rel='shortcut icon'][type='image/x-icon']");

await context.ClickReliablyOnAsync(By.CssSelector("#Editor .btn-group .btn-secondary:not([disabled]):not(.disabled)"));
await selectFromMediaLibraryAsync();

await context.ClickReliablyOnAsync(By.ClassName("save"));
context.ShouldBeSuccess("Site settings updated successfully.");

await context.GoToHomePageAsync();
context.Exists(byIcon.OfAnyVisibility());
context.Missing(By.CssSelector("#navigation .menuWidget__content"));
}
}
20 changes: 20 additions & 0 deletions Lombiq.BaseTheme/Attributes/DerivedThemeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Lombiq.BaseTheme.Constants;
using OrchardCore.DisplayManagement.Manifest;
using OrchardCore.ResourceManagement;
using System;
using System.Collections.Generic;

namespace Lombiq.BaseTheme.Attributes;

/// <summary>
/// Indicates a theme derived from <c>Lombiq.BaseTheme</c>.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class DerivedThemeAttribute : ThemeAttribute
{
public IEnumerable<LinkEntry> Links { get; set; }
public string Favicon { get; set; }

public DerivedThemeAttribute() =>
BaseTheme = FeatureIds.BaseTheme;
}
123 changes: 123 additions & 0 deletions Lombiq.BaseTheme/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using Lombiq.BaseTheme.Models;
using Lombiq.BaseTheme.ViewModels;
using Lombiq.HelpfulExtensions.Extensions.ContentTypes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Entities;
using OrchardCore.Media.Fields;
using OrchardCore.Media.Settings;
using OrchardCore.Media.ViewModels;
using OrchardCore.Modules;
using OrchardCore.Settings;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Lombiq.BaseTheme.Controllers;

// This controller is there for editing the BaseThemeSettings. We can't use a site settings driver for this, because you
// can't declare admin-accessible shapes in a site theme.
public class AdminController : Controller
{
private readonly IClock _clock;
private readonly INotifier _notifier;
private readonly ISiteService _siteService;
private readonly IShapeFactory _shapeFactory;
private readonly IHtmlLocalizer<AdminController> H;

public AdminController(
IClock clock,
INotifier notifier,
ISiteService siteService,
IShapeFactory shapeFactory,
IHtmlLocalizer<AdminController> htmlLocalizer)
{
_clock = clock;
_notifier = notifier;
_siteService = siteService;
_shapeFactory = shapeFactory;

H = htmlLocalizer;
}

public async Task<IActionResult> Index()
{
var section = (await _siteService.LoadSiteSettingsAsync()).As<BaseThemeSettings>();

var model = new BaseThemeSettingsViewModel
{
HideMenu = section.HideMenu,
Icon = section.Icon,
Editor = await _shapeFactory.CreateAsync<EditMediaFieldViewModel>("MediaField_Edit", editor =>
{
var part = CreatePart(section);

editor.Paths = string.IsNullOrWhiteSpace(section.Icon)
? "[]"
: JsonConvert.SerializeObject(new[] { new { path = section.Icon } });
editor.Field = part.Icon;
editor.Part = part;
editor.PartFieldDefinition = new ContentPartFieldDefinition(
new ContentFieldDefinition(nameof(BaseThemeSettingsPart.Icon)),
nameof(BaseThemeSettingsPart.Icon),
JObject.FromObject(new Dictionary<string, object>
{
[nameof(MediaFieldSettings)] = new MediaFieldSettings { Multiple = false },
}))
{
PartDefinition = new ContentPartDefinition(nameof(BaseThemeSettingsPart)),
};
}),
};

model.Editor.Metadata.OnDisplaying(context => context.DisplayContext.HtmlFieldPrefix = nameof(model.Editor));

return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update([FromForm] BaseThemeSettingsViewModel viewModel)
{
var siteSettings = await _siteService.LoadSiteSettingsAsync();
siteSettings.Alter<BaseThemeSettings>(nameof(BaseThemeSettings), settings =>
{
settings.TimeStamp = _clock.UtcNow.Ticks;
settings.Icon = viewModel.Icon;
settings.HideMenu = viewModel.HideMenu;
});

await _siteService.UpdateSiteSettingsAsync(siteSettings);
await _notifier.SuccessAsync(H["Site settings updated successfully."]);

return RedirectToAction(nameof(Index));
}

private static BaseThemeSettingsPart CreatePart(BaseThemeSettings section)
{
var content = new ContentItem { ContentType = ContentTypes.Empty };

content.Weld(new BaseThemeSettingsPart
{
ContentItem = content,
Icon = new MediaField
{
ContentItem = content,
MediaTexts = new[] { section.Icon },
Paths = new[] { section.Icon },
},
});

return content.As<BaseThemeSettingsPart>();
}

public class BaseThemeSettingsPart : ContentPart
{
public MediaField Icon { get; set; } = new();
}
}
1 change: 1 addition & 0 deletions Lombiq.BaseTheme/Lombiq.BaseTheme.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<ItemGroup>
<PackageReference Include="OrchardCore.Theme.Targets" Version="1.6.0" />
<PackageReference Include="OrchardCore.Menu" Version="1.6.0" />
<PackageReference Include="OrchardCore.Media" Version="1.6.0" />
<PackageReference Include="OrchardCore.ContentManagement" Version="1.6.0" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.6.0" />
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.6.0" />
Expand Down
6 changes: 5 additions & 1 deletion Lombiq.BaseTheme/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
Description = "The base frontend theme for shared code that is not specific to a specific project's theme." +
"Warning: themes using this as the base remove the stock Bootstrap resource. If you switch to a different " +
"theme, please reload the tenant from Configuration → Tenants in the admin menu.",
Dependencies = new[] { Lombiq.HelpfulExtensions.FeatureIds.Widgets }
Dependencies = new[]
{
Lombiq.HelpfulExtensions.FeatureIds.ContentTypes,
Lombiq.HelpfulExtensions.FeatureIds.Widgets,
}
)]
8 changes: 8 additions & 0 deletions Lombiq.BaseTheme/Models/BaseThemeSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Lombiq.BaseTheme.Models;

public class BaseThemeSettings
{
public long TimeStamp { get; set; }
public string Icon { get; set; }
public bool HideMenu { get; set; }
}
27 changes: 27 additions & 0 deletions Lombiq.BaseTheme/Navigation/BaseThemeSettingsAdminMenu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Lombiq.BaseTheme.Controllers;
using Lombiq.BaseTheme.Permissions;
using Lombiq.HelpfulLibraries.OrchardCore.Navigation;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Localization;
using OrchardCore.Navigation;

namespace Lombiq.BaseTheme.Navigation;

public class BaseThemeSettingsAdminMenu : AdminMenuNavigationProviderBase
{
public BaseThemeSettingsAdminMenu(
IHttpContextAccessor hca,
IStringLocalizer<BaseThemeSettingsAdminMenu> stringLocalizer)
: base(hca, stringLocalizer)
{
}

protected override void Build(NavigationBuilder builder) =>
builder.Add(T["Configuration"], configuration => configuration
.Add(T["Settings"], settings => settings
.Add(T["Base Theme"], T["Base Theme"], baseTheme => baseTheme
.ActionTask<AdminController>(_hca.HttpContext, controller => controller.Index())
.Permission(BaseThemeSettingsPermissions.ManageBaseThemeSettings)
.LocalNav()
)));
}
13 changes: 13 additions & 0 deletions Lombiq.BaseTheme/Permissions/BaseThemeSettingsPermissions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Lombiq.HelpfulLibraries.OrchardCore.Users;
using OrchardCore.Security.Permissions;
using System.Collections.Generic;

namespace Lombiq.BaseTheme.Permissions;

public class BaseThemeSettingsPermissions : AdminPermissionBase
{
public static readonly Permission ManageBaseThemeSettings =
new(nameof(ManageBaseThemeSettings), "Manage Lombiq.BaseTheme Settings.");

protected override IEnumerable<Permission> AdminPermissions => new[] { ManageBaseThemeSettings };
}
Loading

0 comments on commit 1447ea5

Please sign in to comment.