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:
@@ -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; }
|
||||
}
|
||||
}
|
||||
14
src/ApplicationCore/Exceptions/DuplicateException.cs
Normal file
14
src/ApplicationCore/Exceptions/DuplicateException.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.eShopWeb.ApplicationCore.Exceptions
|
||||
{
|
||||
|
||||
public class DuplicateException : Exception
|
||||
{
|
||||
public DuplicateException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/BlazorAdmin/Helpers/ToastComponent.cs
Normal file
88
src/BlazorAdmin/Helpers/ToastComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
55
src/BlazorAdmin/Services/ToastService.cs
Normal file
55
src/BlazorAdmin/Services/ToastService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
13
src/BlazorAdmin/Shared/Toast.razor
Normal file
13
src/BlazorAdmin/Shared/Toast.razor
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
14
src/BlazorShared/Models/ErrorDetails.cs
Normal file
14
src/BlazorShared/Models/ErrorDetails.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
46
src/PublicApi/MiddleWares/ExceptionMiddleware.cs
Normal file
46
src/PublicApi/MiddleWares/ExceptionMiddleware.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -158,6 +158,8 @@ namespace Microsoft.eShopWeb.Web
|
||||
services.AddBlazoredLocalStorage();
|
||||
services.AddServerSideBlazor();
|
||||
|
||||
|
||||
services.AddScoped<ToastService>();
|
||||
services.AddScoped<HttpService>();
|
||||
services.AddBlazorServices();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user