Skip to content

Commit

Permalink
Auto Attach the DashboardPart to content types with DashboardWidget s…
Browse files Browse the repository at this point in the history
…tereotype (#16911)

---------

Co-authored-by: Zoltán Lehóczky <zoltan.lehoczky@lombiq.com>
  • Loading branch information
MikeAlhayek and Piedone authored Oct 31, 2024
1 parent 773c11c commit 8dcefad
Show file tree
Hide file tree
Showing 22 changed files with 607 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OrchardCore.AdminDashboard;

public static class AdminDashboardConstants
{
public const string Stereotype = "DashboardWidget";
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,6 @@ public async Task<IActionResult> Update([FromForm] DashboardPartViewModel[] part

private async Task<Dictionary<string, ContentTypeDefinition>> GetDashboardWidgetsAsync()
=> (await _contentDefinitionManager.ListTypeDefinitionsAsync())
.Where(t => t.StereotypeEquals("DashboardWidget"))
.Where(t => t.StereotypeEquals(AdminDashboardConstants.Stereotype))
.ToDictionary(ctd => ctd.Name, ctd => ctd);
}
14 changes: 8 additions & 6 deletions src/OrchardCore.Modules/OrchardCore.AdminDashboard/Migrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,10 @@ await SchemaBuilder.AlterIndexTableAsync<DashboardPartIndex>(table => table
"Position")
);

await _contentDefinitionManager.AlterPartDefinitionAsync("DashboardPart", builder => builder
.Attachable()
.WithDescription("Provides a way to add widgets to a dashboard.")
);

await _recipeMigrator.ExecuteAsync($"dashboard-widgets{RecipesConstants.RecipeExtension}", this);

// Shortcut other migration steps on new content definition schemas.
return 3;
return 4;
}

public async Task<int> UpdateFrom1Async()
Expand All @@ -62,4 +57,11 @@ await SchemaBuilder.AlterIndexTableAsync<DashboardPartIndex>(table => table

return 3;
}

public async Task<int> UpdateFrom3Async()
{
await _contentDefinitionManager.DeletePartDefinitionAsync("DashboardPart");

return 4;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Text.Json.Nodes;
using OrchardCore.AdminDashboard.Models;
using OrchardCore.ContentManagement.Metadata.Records;
using OrchardCore.ContentManagement.Metadata.Settings;
using OrchardCore.ContentTypes.Events;
using OrchardCore.Modules;

namespace OrchardCore.AdminDashboard.Services;

public sealed class DashboardPartContentTypeDefinitionHandler : IContentDefinitionHandler
{
/// <summary>
/// Adds the <see cref="DashboardPart"/> to the content type definition when the stereotype is set to 'DashboardWidget'.
/// This occurs during the content type building process, allowing the content type to function as a dashboard widget.
/// </summary>
public void ContentTypeBuilding(ContentTypeBuildingContext context)
{
if (!context.Record.Settings.TryGetPropertyValue(nameof(ContentTypeSettings), out var node))
{
return;
}

var settings = node.ToObject<ContentTypeSettings>();

if (settings.Stereotype == null || !string.Equals(settings.Stereotype, AdminDashboardConstants.Stereotype, StringComparison.OrdinalIgnoreCase))
{
return;
}

if (context.Record.ContentTypePartDefinitionRecords.Any(x => x.Name.EqualsOrdinalIgnoreCase(nameof(DashboardPart))))
{
return;
}

context.Record.ContentTypePartDefinitionRecords.Add(new ContentTypePartDefinitionRecord
{
Name = nameof(DashboardPart),
PartName = nameof(DashboardPart),
Settings = new JsonObject()
{
[nameof(ContentSettings)] = JObject.FromObject(new ContentSettings
{
IsSystemDefined = true,
}),
},
});
}

/// <summary>
/// Marks the part on the content type as a system type to prevent its removal.
/// This ensures that the part remains integral to the content type and cannot be deleted.
/// </summary>
public void ContentTypePartBuilding(ContentTypePartBuildingContext context)
{
if (!context.Record.PartName.EqualsOrdinalIgnoreCase(nameof(DashboardPart)))
{
return;
}

var settings = context.Record.Settings[nameof(ContentSettings)]?.ToObject<ContentSettings>()
?? new ContentSettings();

settings.IsSystemDefined = true;

context.Record.Settings[nameof(ContentSettings)] = JObject.FromObject(settings);
}

/// <summary>
/// Creates a definition if the Record is null and the part name is 'DashboardPart'.
/// This ensures that the 'DashboardPart' has a valid definition when it is missing.
/// </summary>
public void ContentPartBuilding(ContentPartBuildingContext context)
{
if (context.Record is not null || context.PartName != nameof(DashboardPart))
{
return;
}

context.Record = new ContentPartDefinitionRecord()
{
Name = context.PartName,
Settings = new JsonObject()
{
[nameof(ContentPartSettings)] = JObject.FromObject(new ContentPartSettings
{
Attachable = false,
Reusable = false,
}),
[nameof(ContentSettings)] = JObject.FromObject(new ContentSettings
{
IsSystemDefined = true,
}),
},
};
}

public void ContentPartFieldBuilding(ContentPartFieldBuildingContext context)
{
}
}
2 changes: 2 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.AdminDashboard/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using OrchardCore.AdminDashboard.Services;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentTypes.Events;
using OrchardCore.Data;
using OrchardCore.Data.Migration;
using OrchardCore.Modules;
Expand Down Expand Up @@ -42,6 +43,7 @@ public override void ConfigureServices(IServiceCollection services)
services.AddScoped<IContentDisplayDriver, DashboardContentDisplayDriver>();

services.AddDataMigration<Migrations>();
services.AddScoped<IContentDefinitionHandler, DashboardPartContentTypeDefinitionHandler>();
}

public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,14 @@ public async Task<ActionResult> RemovePart(string id, string name)

var typeViewModel = await _contentDefinitionService.LoadTypeAsync(id);

if (typeViewModel == null || !typeViewModel.TypeDefinition.Parts.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)))
if (typeViewModel == null)
{
return NotFound();
}

var partDefinition = typeViewModel.TypeDefinition.Parts.FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));

if (partDefinition == null)
{
return NotFound();
}
Expand All @@ -440,8 +447,8 @@ public async Task<ActionResult> ListParts()

return View(new ListContentPartsViewModel
{
// only user-defined parts (not code as they are not configurable)
Parts = await _contentDefinitionService.GetPartsAsync(true/*metadataPartsOnly*/)
// Only user-defined parts (not code as they are not configurable).
Parts = await _contentDefinitionService.GetPartsAsync(metadataPartsOnly: true)
});
}

Expand Down
Loading

0 comments on commit 8dcefad

Please sign in to comment.