Skip to content

Commit

Permalink
Show recipe error instead if throwing an exeption (#16148)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Hisham Bin Ateya <[email protected]>
Co-authored-by: Tony Han <[email protected]>
  • Loading branch information
3 people authored May 25, 2024
1 parent 95bbd6d commit ff88852
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.ContentManagement.Metadata.Records;
Expand All @@ -16,9 +17,14 @@ public class ContentDefinitionStep : IRecipeStepHandler
{
private readonly IContentDefinitionManager _contentDefinitionManager;

public ContentDefinitionStep(IContentDefinitionManager contentDefinitionManager)
protected readonly IStringLocalizer S;

public ContentDefinitionStep(
IContentDefinitionManager contentDefinitionManager,
IStringLocalizer<ContentDefinitionStep> stringLocalizer)
{
_contentDefinitionManager = contentDefinitionManager;
S = stringLocalizer;
}

public async Task ExecuteAsync(RecipeExecutionContext context)
Expand All @@ -35,20 +41,21 @@ public async Task ExecuteAsync(RecipeExecutionContext context)
var newType = await _contentDefinitionManager.LoadTypeDefinitionAsync(contentType.Name)
?? new ContentTypeDefinition(contentType.Name, contentType.DisplayName);

await UpdateContentTypeAsync(newType, contentType);
await UpdateContentTypeAsync(newType, contentType, context);
}

foreach (var contentPart in step.ContentParts)
{
var newPart = await _contentDefinitionManager.LoadPartDefinitionAsync(contentPart.Name)
?? new ContentPartDefinition(contentPart.Name);

await UpdateContentPartAsync(newPart, contentPart);
await UpdateContentPartAsync(newPart, contentPart, context);
}
}

private Task UpdateContentTypeAsync(ContentTypeDefinition type, ContentTypeDefinitionRecord record)
=> _contentDefinitionManager.AlterTypeDefinitionAsync(type.Name, builder =>
private Task UpdateContentTypeAsync(ContentTypeDefinition type, ContentTypeDefinitionRecord record, RecipeExecutionContext context)
{
return _contentDefinitionManager.AlterTypeDefinitionAsync(type.Name, builder =>
{
if (!string.IsNullOrEmpty(record.DisplayName))
{
Expand All @@ -60,31 +67,44 @@ private Task UpdateContentTypeAsync(ContentTypeDefinition type, ContentTypeDefin
{
if (string.IsNullOrEmpty(part.PartName))
{
throw new InvalidOperationException($"Unable to add content-part to the '{type.Name}' content-type. The part name cannot be null or empty.");
context.Errors.Add(S["Unable to add content-part to the '{0}' content-type. The part name cannot be null or empty.", type.Name]);
continue;
}
builder.WithPart(part.Name, part.PartName, partBuilder => partBuilder.MergeSettings(part.Settings));
}
});
}

private Task UpdateContentPartAsync(ContentPartDefinition part, ContentPartDefinitionRecord record)
=> _contentDefinitionManager.AlterPartDefinitionAsync(part.Name, builder =>
private Task UpdateContentPartAsync(ContentPartDefinition part, ContentPartDefinitionRecord record, RecipeExecutionContext context)
{
return _contentDefinitionManager.AlterPartDefinitionAsync(part.Name, builder =>
{
builder.MergeSettings(record.Settings);
foreach (var field in record.ContentPartFieldDefinitionRecords)
{
if (string.IsNullOrEmpty(field.Name))
{
context.Errors.Add(S["Unable to add content-field to the '{0}' content-part. The part name cannot be null or empty.", part.Name]);
continue;
}
builder.WithField(field.Name, fieldBuilder =>
{
fieldBuilder.OfType(field.FieldName);
fieldBuilder.MergeSettings(field.Settings);
});
}
});
}

private sealed class ContentDefinitionStepModel
{
public ContentTypeDefinitionRecord[] ContentTypes { get; set; } = [];

public ContentPartDefinitionRecord[] ContentParts { get; set; } = [];
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
Expand All @@ -6,26 +7,44 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using OrchardCore.Deployment.Remote.Services;
using OrchardCore.Deployment.Remote.ViewModels;
using OrchardCore.Deployment.Services;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Recipes.Models;

namespace OrchardCore.Deployment.Remote.Controllers
{
public class ImportRemoteInstanceController : Controller
{
private readonly RemoteClientService _remoteClientService;
private readonly IDeploymentManager _deploymentManager;
private readonly INotifier _notifier;
private readonly ILogger _logger;
private readonly IDataProtector _dataProtector;

protected readonly IHtmlLocalizer H;
protected readonly IStringLocalizer S;

public ImportRemoteInstanceController(
IDataProtectionProvider dataProtectionProvider,
RemoteClientService remoteClientService,
IDeploymentManager deploymentManager)
IDeploymentManager deploymentManager,
INotifier notifier,
IHtmlLocalizer<ImportRemoteInstanceController> htmlLocalizer,
IStringLocalizer<ImportRemoteInstanceController> stringLocalizer,
ILogger<ImportRemoteInstanceController> logger)
{
_deploymentManager = deploymentManager;
_notifier = notifier;
_logger = logger;
_remoteClientService = remoteClientService;
H = htmlLocalizer;
S = stringLocalizer;
_dataProtector = dataProtectionProvider.CreateProtector("OrchardCore.Deployment").ToTimeLimitedDataProtector();
}

Expand Down Expand Up @@ -70,6 +89,18 @@ public async Task<IActionResult> Import(ImportViewModel model)

await _deploymentManager.ImportDeploymentPackageAsync(new PhysicalFileProvider(tempArchiveFolder));
}
catch (RecipeExecutionException e)
{
_logger.LogError(e, "Unable to import a recipe from deployment plan.");

await _notifier.ErrorAsync(H["The deployment plan failed with the following errors: {0}", string.Join(' ', e.StepResult.Errors.SelectMany(x => x.Value))]);
}
catch (Exception e)
{
_logger.LogError(e, "Unexpected error occurred while executing a deployment plan.");

await _notifier.ErrorAsync(H["Unexpected error occurred while executing a deployment plan."]);
}
finally
{
if (System.IO.File.Exists(tempArchiveName))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
Expand All @@ -8,11 +10,13 @@
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using OrchardCore.Admin;
using OrchardCore.Deployment.Services;
using OrchardCore.Deployment.ViewModels;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Mvc.Utilities;
using OrchardCore.Recipes.Models;

namespace OrchardCore.Deployment.Controllers
{
Expand All @@ -22,21 +26,24 @@ public class ImportController : Controller
private readonly IDeploymentManager _deploymentManager;
private readonly IAuthorizationService _authorizationService;
private readonly INotifier _notifier;
private readonly ILogger _logger;

protected readonly IHtmlLocalizer H;
protected readonly IStringLocalizer S;

public ImportController(
IDeploymentManager deploymentManager,
IAuthorizationService authorizationService,
INotifier notifier,
ILogger<ImportController> logger,
IHtmlLocalizer<ImportController> htmlLocalizer,
IStringLocalizer<ImportController> stringLocalizer
)
{
_deploymentManager = deploymentManager;
_authorizationService = authorizationService;
_notifier = notifier;

_logger = logger;
H = htmlLocalizer;
S = stringLocalizer;
}
Expand Down Expand Up @@ -91,6 +98,18 @@ public async Task<IActionResult> Import(IFormFile importedPackage)

await _notifier.SuccessAsync(H["Deployment package imported."]);
}
catch (RecipeExecutionException e)
{
_logger.LogError(e, "Unable to import a deployment package.");

await _notifier.ErrorAsync(H["The import failed with the following errors: {0}", string.Join(' ', e.StepResult.Errors.SelectMany(x => x.Value))]);
}
catch (Exception e)
{
_logger.LogError(e, "Unable to import a deployment package.");

await _notifier.ErrorAsync(H["Unexpected error occurred while importing the deployment package."]);
}
finally
{
if (System.IO.File.Exists(tempArchiveName))
Expand Down Expand Up @@ -132,7 +151,7 @@ public async Task<IActionResult> Json(ImportJsonViewModel model)

if (!model.Json.IsJson(JOptions.Document))
{
ModelState.AddModelError(nameof(model.Json), S["The recipe is written in an incorrect json format."]);
ModelState.AddModelError(nameof(model.Json), S["The recipe is written in an incorrect JSON format."]);
}

if (ModelState.IsValid)
Expand All @@ -146,7 +165,19 @@ public async Task<IActionResult> Json(ImportJsonViewModel model)

await _deploymentManager.ImportDeploymentPackageAsync(new PhysicalFileProvider(tempArchiveFolder));

await _notifier.SuccessAsync(H["Recipe imported."]);
await _notifier.SuccessAsync(H["Recipe imported successfully!"]);
}
catch (RecipeExecutionException e)
{
_logger.LogError(e, "Unable to import a recipe from JSON input.");

ModelState.AddModelError(nameof(model.Json), string.Join(' ', e.StepResult.Errors.SelectMany(x => x.Value)));
}
catch (Exception e)
{
_logger.LogError(e, "Unable to import a recipe from JSON input.");

ModelState.AddModelError(string.Empty, S["Unexpected error occurred while importing the recipe."]);
}
finally
{
Expand Down
16 changes: 11 additions & 5 deletions src/OrchardCore.Modules/OrchardCore.Layers/Recipes/LayerStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OrchardCore.Json;
using OrchardCore.Layers.Models;
Expand All @@ -25,16 +26,20 @@ public class LayerStep : IRecipeStepHandler
private readonly IEnumerable<IConditionFactory> _factories;
private readonly JsonSerializerOptions _serializationOptions;

protected readonly IStringLocalizer S;

public LayerStep(
ILayerService layerService,
IConditionIdGenerator conditionIdGenerator,
IEnumerable<IConditionFactory> factories,
IOptions<DocumentJsonSerializerOptions> serializationOptions)
IOptions<DocumentJsonSerializerOptions> serializationOptions,
IStringLocalizer<LayerStep> stringLocalizer)
{
_layerService = layerService;
_conditionIdGenerator = conditionIdGenerator;
_factories = factories;
_serializationOptions = serializationOptions.Value.SerializerOptions;
S = stringLocalizer;
}

public async Task ExecuteAsync(RecipeExecutionContext context)
Expand Down Expand Up @@ -76,7 +81,9 @@ public async Task ExecuteAsync(RecipeExecutionContext context)
}
else
{
throw new InvalidOperationException($"The layer '{nameof(layer.Name)}' is required.");
context.Errors.Add(S["The layer '{0}' is required.", layer.Name]);

continue;
}

if (layerStep.LayerRule != null)
Expand Down Expand Up @@ -112,10 +119,9 @@ public async Task ExecuteAsync(RecipeExecutionContext context)

if (unknownTypes.Count != 0)
{
var prefix = "No changes have been made. The following types of conditions cannot be added:";
var suffix = "Please ensure that the related features are enabled to add these types of conditions.";
context.Errors.Add(S["No changes have been made. The following types of conditions cannot be added: {0}. Please ensure that the related features are enabled to add these types of conditions.", string.Join(", ", unknownTypes)]);

throw new InvalidOperationException($"{prefix} {string.Join(", ", unknownTypes)}. {suffix}");
return;
}

await _layerService.UpdateAsync(allLayers);
Expand Down
3 changes: 2 additions & 1 deletion src/OrchardCore.Modules/OrchardCore.Recipes/AdminMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder)
builder
.Add(S["Configuration"], configuration => configuration
.Add(S["Recipes"], S["Recipes"].PrefixPosition(), recipes => recipes
.AddClass("recipes").Id("recipes")
.AddClass("recipes")
.Id("recipes")
.Permission(StandardPermissions.SiteOwner)
.Action("Index", "Admin", "OrchardCore.Recipes")
.LocalNav()
Expand Down
Loading

0 comments on commit ff88852

Please sign in to comment.