manage conflict (generic way) use toast to show error (#588)

* manage conflict (generic way) use toast to show error

* fix httpservice after merge conflict, adapt to use new repository
This commit is contained in:
Cédric Michel
2021-10-25 23:32:07 +02:00
committed by GitHub
parent 935167daaf
commit 8f55b1b56a
17 changed files with 337 additions and 29 deletions

View File

@@ -1,14 +0,0 @@
using System;
namespace Microsoft.eShopWeb.ApplicationCore.Exceptions
{
public class DuplicateCatalogItemNameException : Exception
{
public DuplicateCatalogItemNameException(string message, int duplicateItemId) : base(message)
{
DuplicateItemId = duplicateItemId;
}
public int DuplicateItemId { get; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Microsoft.eShopWeb.ApplicationCore.Exceptions
{
public class DuplicateException : Exception
{
public DuplicateException(string message) : base(message)
{
}
}
}

View File

@@ -0,0 +1,13 @@
using Ardalis.Specification;
using Microsoft.eShopWeb.ApplicationCore.Entities;
namespace Microsoft.eShopWeb.ApplicationCore.Specifications
{
public class CatalogItemNameSpecification : Specification<CatalogItem>
{
public CatalogItemNameSpecification(string catalogItemName)
{
Query.Where(item => string.Compare(catalogItemName, item.Name, true) == 0);
}
}
}

View File

@@ -0,0 +1,88 @@
using BlazorAdmin.Services;
using Microsoft.AspNetCore.Components;
using System;
namespace BlazorAdmin.Helpers
{
public class ToastComponent : ComponentBase, IDisposable
{
[Inject]
ToastService ToastService
{
get;
set;
}
protected string Heading
{
get;
set;
}
protected string Message
{
get;
set;
}
protected bool IsVisible
{
get;
set;
}
protected string BackgroundCssClass
{
get;
set;
}
protected string IconCssClass
{
get;
set;
}
protected override void OnInitialized()
{
ToastService.OnShow += ShowToast;
ToastService.OnHide += HideToast;
}
private void ShowToast(string message, ToastLevel level)
{
BuildToastSettings(level, message);
IsVisible = true;
StateHasChanged();
}
private void HideToast()
{
IsVisible = false;
StateHasChanged();
}
private void BuildToastSettings(ToastLevel level, string message)
{
switch (level)
{
case ToastLevel.Info:
BackgroundCssClass = "bg-info";
IconCssClass = "info";
Heading = "Info";
break;
case ToastLevel.Success:
BackgroundCssClass = "bg-success";
IconCssClass = "check";
Heading = "Success";
break;
case ToastLevel.Warning:
BackgroundCssClass = "bg-warning";
IconCssClass = "exclamation";
Heading = "Warning";
break;
case ToastLevel.Error:
BackgroundCssClass = "bg-danger";
IconCssClass = "times";
Heading = "Error";
break;
}
Message = message;
}
public void Dispose()
{
ToastService.OnShow -= ShowToast;
}
}
}

View File

@@ -106,13 +106,13 @@
@code {
[Parameter]
public IEnumerable<CatalogBrand> Brands { get; set; }
[Parameter]
public IEnumerable<CatalogType> Types { get; set; }
[Parameter]
public IEnumerable<CatalogBrand> Brands { get; set; }
[Parameter]
public IEnumerable<CatalogType> Types { get; set; }
[Parameter]
public EventCallback<string> OnSaveClick { get; set; }
[Parameter]
public EventCallback<string> OnSaveClick { get; set; }
private string LoadPicture => string.IsNullOrEmpty(_item.PictureBase64) ? string.Empty : $"data:image/png;base64, {_item.PictureBase64}";
private bool HasPicture => !string.IsNullOrEmpty(_item.PictureBase64);
@@ -124,13 +124,17 @@
private async Task CreateClick()
{
await CatalogItemService.Create(_item);
await OnSaveClick.InvokeAsync(null);
await Close();
var result = await CatalogItemService.Create(_item);
if (result != null)
{
await OnSaveClick.InvokeAsync(null);
await Close();
}
}
public async Task Open()
{
Logger.LogInformation("Now loading... /Catalog/Create");
await new Css(JSRuntime).HideBodyOverflow();

View File

@@ -25,14 +25,16 @@ namespace BlazorAdmin
builder.Services.AddScoped(sp => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<ToastService>();
builder.Services.AddScoped<HttpService>();
builder.Services.AddScoped<ILocalStorageService, LocalStorageService>();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddScoped(sp => (CustomAuthStateProvider)sp.GetRequiredService<AuthenticationStateProvider>());
builder.Services.AddBlazorServices();
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));

View File

@@ -33,7 +33,8 @@ namespace BlazorAdmin.Services
public async Task<CatalogItem> Create(CreateCatalogItemRequest catalogItem)
{
return (await _httpService.HttpPost<CreateCatalogItemResponse>("catalog-items", catalogItem)).CatalogItem;
var response = await _httpService.HttpPost<CreateCatalogItemResponse>("catalog-items", catalogItem);
return response?.CatalogItem;
}
public async Task<CatalogItem> Edit(CatalogItem catalogItem)

View File

@@ -1,4 +1,5 @@
using BlazorShared;
using BlazorShared.Models;
using System.Net.Http;
using System.Text;
using System.Text.Json;
@@ -9,11 +10,14 @@ namespace BlazorAdmin.Services
public class HttpService
{
private readonly HttpClient _httpClient;
private readonly ToastService _toastService;
private readonly string _apiUrl;
public HttpService(HttpClient httpClient, BaseUrlConfiguration baseUrlConfiguration)
public HttpService(HttpClient httpClient, BaseUrlConfiguration baseUrlConfiguration, ToastService toastService)
{
_httpClient = httpClient;
_toastService = toastService;
_apiUrl = baseUrlConfiguration.ApiBase;
}
@@ -49,6 +53,12 @@ namespace BlazorAdmin.Services
var result = await _httpClient.PostAsync($"{_apiUrl}{uri}", content);
if (!result.IsSuccessStatusCode)
{
var exception = JsonSerializer.Deserialize<ErrorDetails>(await result.Content.ReadAsStringAsync(), new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
_toastService.ShowToast($"Error : {exception.Message}", ToastLevel.Error);
return null;
}
@@ -63,13 +73,13 @@ namespace BlazorAdmin.Services
var result = await _httpClient.PutAsync($"{_apiUrl}{uri}", content);
if (!result.IsSuccessStatusCode)
{
_toastService.ShowToast("Error", ToastLevel.Error);
return null;
}
return await FromHttpResponseMessage<T>(result);
}
private StringContent ToJson(object obj)
{
return new StringContent(JsonSerializer.Serialize(obj), Encoding.UTF8, "application/json");

View File

@@ -0,0 +1,55 @@
using System;
using System.Timers;
namespace BlazorAdmin.Services
{
public enum ToastLevel
{
Info,
Success,
Warning,
Error
}
public class ToastService : IDisposable
{
public event Action<string, ToastLevel> OnShow;
public event Action OnHide;
private Timer Countdown;
public void ShowToast(string message, ToastLevel level)
{
OnShow?.Invoke(message, level);
StartCountdown();
}
private void StartCountdown()
{
SetCountdown();
if (Countdown.Enabled)
{
Countdown.Stop();
Countdown.Start();
}
else
{
Countdown.Start();
}
}
private void SetCountdown()
{
if (Countdown == null)
{
Countdown = new Timer(3000);
Countdown.Elapsed += HideToast;
Countdown.AutoReset = false;
}
}
private void HideToast(object source, ElapsedEventArgs args)
{
OnHide?.Invoke();
}
public void Dispose()
{
Countdown?.Dispose();
}
}
}

View File

@@ -3,6 +3,7 @@
@inherits BlazorAdmin.Helpers.BlazorLayoutComponent
<AuthorizeView Roles=@BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS>
<div class="sidebar">
<NavMenu />
@@ -10,11 +11,13 @@
</AuthorizeView>
<div class="main">
<div class="top-row px-4">
<a href="https://github.com/dotnet-architecture/eShopOnWeb" target="_blank" class="ml-md-auto">About eShopOnWeb</a>
</div>
<div class="content px-4">
<Toast></Toast>
@Body
</div>
</div>

View File

@@ -0,0 +1,13 @@
@inherits BlazorAdmin.Helpers.ToastComponent
@namespace BlazorAdmin.Shared
<div class="toast @(IsVisible ? "toast-visible" : null) @BackgroundCssClass">
<div class="toast-icon">
<i class="fa fa-@IconCssClass" aria-hidden="true"></i>
</div>
<div class="toast-body">
<h5>@Heading</h5>
<p>@Message</p>
</div>
</div>

View File

@@ -153,6 +153,53 @@ a, .btn-link {
overflow: hidden !important;
}
.toast {
display: none;
padding: 1.5rem;
color: #fff;
z-index: 99999;
position: absolute;
width: 25rem;
top: 2rem;
border-radius: 1rem;
left: 50%;
}
.toast-icon {
display: flex;
flex-direction: column;
justify-content: center;
padding: 01rem;
font-size: 2.5rem;
}
.toast-body {
display: flex;
flex-direction: column;
flex: 1;
padding-left: 1rem;
}
.toast-body p {
margin-bottom: 0;
}
.toast-visible {
display: flex;
flex-direction: row;
animation: fadein 1.5s;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (max-width: 767.98px) {
.main .top-row:not(.auth) {
display: none;

View File

@@ -0,0 +1,14 @@
using System.Text.Json;
namespace BlazorShared.Models
{
public class ErrorDetails
{
public int StatusCode { get; set; }
public string Message { get; set; }
public override string ToString()
{
return JsonSerializer.Serialize(this);
}
}
}

View File

@@ -3,9 +3,10 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Exceptions;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using Swashbuckle.AspNetCore.Annotations;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -38,8 +39,14 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{
var response = new CreateCatalogItemResponse(request.CorrelationId());
var newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri);
var catalogItemNameSpecification = new CatalogItemNameSpecification(request.Name);
var existingCataloogItem = await _itemRepository.CountAsync(catalogItemNameSpecification, cancellationToken);
if (existingCataloogItem > 0)
{
throw new DuplicateException($"A catalogItem with name {request.Name} already exists");
}
var newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri);
newItem = await _itemRepository.AddAsync(newItem, cancellationToken);
if (newItem.Id != 0)

View File

@@ -0,0 +1,46 @@
using BlazorShared.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopWeb.ApplicationCore.Exceptions;
using System;
using System.Net;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi.MiddleWares
{
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
public ExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
await HandleExceptionAsync(httpContext, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
if (exception is DuplicateException duplicationException)
{
context.Response.StatusCode = (int)HttpStatusCode.Conflict;
await context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = duplicationException.Message
}.ToString());
}
}
}
}

View File

@@ -14,6 +14,7 @@ using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Infrastructure.Logging;
using Microsoft.eShopWeb.Infrastructure.Services;
using Microsoft.eShopWeb.PublicApi.MiddleWares;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -178,6 +179,8 @@ namespace Microsoft.eShopWeb.PublicApi
app.UseDeveloperExceptionPage();
}
app.UseMiddleware<ExceptionMiddleware>();
app.UseHttpsRedirection();
app.UseRouting();

View File

@@ -158,6 +158,8 @@ namespace Microsoft.eShopWeb.Web
services.AddBlazoredLocalStorage();
services.AddServerSideBlazor();
services.AddScoped<ToastService>();
services.AddScoped<HttpService>();
services.AddBlazorServices();