b1b7525759
Build and Push Docker Image / build-and-push (push) Successful in 1m21s
- Change default request culture to "nl-BE" and reorder supported cultures; remove "nl-NL" - Refactor RegistryAdmin second-hand preference to use a string property for better nullable handling in the dropdown
809 lines
29 KiB
C#
809 lines
29 KiB
C#
using BirthList.Domain.Entities;
|
|
using BirthList.Web.Authorization;
|
|
using BirthList.Web.Features.Registries;
|
|
using BirthList.Web.Services;
|
|
using Blazored.TextEditor;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
using Microsoft.JSInterop;
|
|
|
|
namespace BirthList.Web.Components.Pages;
|
|
|
|
[Authorize]
|
|
public partial class RegistryAdmin : ComponentBase
|
|
{
|
|
[Parameter] public Guid RegistryId { get; set; }
|
|
|
|
[Inject] private RegistryService RegistryService { get; set; } = null!;
|
|
[Inject] private RegistryMetadataService RegistryMetadataService { get; set; } = null!;
|
|
[Inject] private RegistryAuthorizationService RegistryAuthorizationService { get; set; } = null!;
|
|
[Inject] private RegistryUserContext RegistryUserContext { get; set; } = null!;
|
|
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
|
|
[Inject] private SmtpEmailSender EmailSender { get; set; } = null!;
|
|
[Inject] private SmtpConfigurationStatusService SmtpConfigurationStatusService { get; set; } = null!;
|
|
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
|
|
|
protected RegistrySettingsEditModel SettingsModel { get; } = new();
|
|
protected RegistryItemEditModel ItemModel { get; private set; } = new();
|
|
protected string PreferSecondHandString
|
|
{
|
|
get => ItemModel.PreferSecondHand switch
|
|
{
|
|
null => "",
|
|
true => "true",
|
|
false => "false"
|
|
};
|
|
set => ItemModel.PreferSecondHand = value switch
|
|
{
|
|
"true" => true,
|
|
"false" => false,
|
|
_ => null
|
|
};
|
|
}
|
|
protected IReadOnlyList<RegistryItemEditModel> Items { get; private set; } = [];
|
|
protected IReadOnlyList<RegistryItemCategoryEditModel> ItemCategories { get; private set; } = [];
|
|
protected string? NewCategoryName { get; set; }
|
|
protected string? CategoryRenameName { get; set; }
|
|
protected Guid? EditingCategoryId { get; set; }
|
|
protected string? ItemManagementMessage { get; private set; }
|
|
protected IReadOnlyList<RegistryAdminDisplayModel> Admins { get; private set; } = [];
|
|
protected IReadOnlyList<RegistryAccessibleUserAddressViewModel> AccessibleUserAddresses { get; private set; } = [];
|
|
protected bool IsAuthorized { get; private set; }
|
|
protected bool IsSmtpConfigured { get; private set; }
|
|
protected string? InviteEmail { get; set; }
|
|
protected string? InviteLink { get; private set; }
|
|
protected BlazoredTextEditor? TextEditor { get; set; }
|
|
protected string ActiveTab { get; set; } = "items";
|
|
protected RenderFragment ToolbarContent => builder =>
|
|
{
|
|
builder.AddMarkupContent(0, @"<span class='ql-formats'><select class='ql-size'><option value='small'></option><option selected></option><option value='large'></option><option value='huge'></option></select><select class='ql-header'><option selected></option><option value='1'></option><option value='2'></option><option value='3'></option><option value='4'></option><option value='5'></option></select></span>");
|
|
builder.AddMarkupContent(1, @"<span class='ql-formats'><button class='ql-bold'></button><button class='ql-italic'></button><button class='ql-underline'></button><button class='ql-strike'></button></span>");
|
|
builder.AddMarkupContent(2, @"<span class='ql-formats'><select class=""ql-color""></select><select class=""ql-background""></select></span>");
|
|
builder.AddMarkupContent(3, @"<span class='ql-formats'><button class='ql-list' value='ordered'></button><button class='ql-list' value='bullet'></button></span>");
|
|
builder.AddMarkupContent(4, @"<span class='ql-formats'><button class='ql-link'></button><button class='ql-clean'></button></span>");
|
|
};
|
|
|
|
private bool _pendingEditorLoad;
|
|
private string? _lastLoadedHeaderContentHtml;
|
|
private Guid? _draggedItemId;
|
|
private Guid? _draggedCategoryId;
|
|
private Guid? _itemDropCategoryId;
|
|
private Guid? _itemDropTargetItemId;
|
|
private bool _itemDropAfterTarget;
|
|
private Guid? _categoryDropTargetId;
|
|
private bool _categoryDropAfterTarget;
|
|
private bool _categoryDropAtEnd;
|
|
private bool _dropOperationInProgress;
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
{
|
|
IsAuthorized = false;
|
|
return;
|
|
}
|
|
|
|
IsAuthorized = await RegistryAuthorizationService.IsRegistryAdminAsync(RegistryId, userId, CancellationToken.None).ConfigureAwait(false);
|
|
if (!IsAuthorized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IsSmtpConfigured = SmtpConfigurationStatusService.IsConfigured();
|
|
await LoadAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (TextEditor is null || !_pendingEditorLoad)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await TextEditor.LoadHTMLContent(SettingsModel.HeaderContentHtml ?? string.Empty).ConfigureAwait(false);
|
|
_lastLoadedHeaderContentHtml = SettingsModel.HeaderContentHtml ?? string.Empty;
|
|
_pendingEditorLoad = false;
|
|
}
|
|
catch (JSException)
|
|
{
|
|
// Editor scripts may still be initializing on first render in some environments.
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Ignore transient prerender/circuit timing issues.
|
|
}
|
|
}
|
|
|
|
protected void SetActiveTab(string tab)
|
|
{
|
|
ActiveTab = tab;
|
|
}
|
|
|
|
protected string GetTabClass(string tab)
|
|
{
|
|
return ActiveTab == tab ? "active" : "";
|
|
}
|
|
|
|
protected string GetTabPaneClass(string tab)
|
|
{
|
|
return ActiveTab == tab ? "show active" : "";
|
|
}
|
|
|
|
protected async Task SaveSettingsAsync()
|
|
{
|
|
if (TextEditor is not null)
|
|
{
|
|
try
|
|
{
|
|
SettingsModel.HeaderContentHtml = await TextEditor.GetHTML().ConfigureAwait(false);
|
|
}
|
|
catch (JSException)
|
|
{
|
|
// Preserve existing content when editor JS isn't ready yet.
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Preserve existing content when interop isn't available.
|
|
}
|
|
}
|
|
|
|
await RegistryService.UpdateRegistrySettingsAsync(RegistryId, SettingsModel, CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
|
|
protected async Task SaveItemAsync()
|
|
{
|
|
ItemManagementMessage = null;
|
|
|
|
if (!ItemModel.CategoryId.HasValue && ItemCategories.Count > 0)
|
|
{
|
|
ItemModel.CategoryId = ItemCategories[0].Id;
|
|
}
|
|
|
|
await RegistryService.UpsertRegistryItemAsync(RegistryId, ItemModel, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
|
|
ItemModel = new RegistryItemEditModel
|
|
{
|
|
CurrencyCode = SettingsModel.CurrencyCode,
|
|
DesiredQuantity = 1,
|
|
CategoryId = ItemCategories.FirstOrDefault()?.Id
|
|
};
|
|
}
|
|
|
|
protected async Task DeleteItemAsync(Guid? itemId)
|
|
{
|
|
if (!itemId.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ItemManagementMessage = null;
|
|
await RegistryService.DeleteRegistryItemAsync(RegistryId, itemId.Value, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
protected void EditItem(RegistryItemEditModel model)
|
|
{
|
|
ItemManagementMessage = null;
|
|
|
|
ItemModel = new RegistryItemEditModel
|
|
{
|
|
Id = model.Id,
|
|
CategoryId = model.CategoryId,
|
|
Name = model.Name,
|
|
PictureUrl = model.PictureUrl,
|
|
ProductUrl = model.ProductUrl,
|
|
Description = model.Description,
|
|
PriceAmount = model.PriceAmount,
|
|
CurrencyCode = model.CurrencyCode,
|
|
DesiredQuantity = model.DesiredQuantity,
|
|
ParticipationAllowed = model.ParticipationAllowed,
|
|
ParticipationTargetAmount = model.ParticipationTargetAmount,
|
|
PreferSecondHand = model.PreferSecondHand,
|
|
IsGiven = model.IsGiven
|
|
};
|
|
}
|
|
|
|
protected async Task AddCategoryAsync()
|
|
{
|
|
ItemManagementMessage = null;
|
|
|
|
if (string.IsNullOrWhiteSpace(NewCategoryName))
|
|
{
|
|
ItemManagementMessage = "Category name is required.";
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await RegistryService.AddRegistryItemCategoryAsync(RegistryId, NewCategoryName, CancellationToken.None).ConfigureAwait(false);
|
|
NewCategoryName = null;
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
|
|
ItemModel.CategoryId ??= ItemCategories.LastOrDefault()?.Id;
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
ItemManagementMessage = ex.Message;
|
|
}
|
|
}
|
|
|
|
protected void StartCategoryRename(RegistryItemCategoryEditModel category)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(category);
|
|
ItemManagementMessage = null;
|
|
EditingCategoryId = category.Id;
|
|
CategoryRenameName = category.Name;
|
|
}
|
|
|
|
protected void CancelCategoryRename()
|
|
{
|
|
EditingCategoryId = null;
|
|
CategoryRenameName = null;
|
|
}
|
|
|
|
protected async Task SaveCategoryRenameAsync(Guid categoryId)
|
|
{
|
|
ItemManagementMessage = null;
|
|
|
|
if (string.IsNullOrWhiteSpace(CategoryRenameName))
|
|
{
|
|
ItemManagementMessage = "Category name is required.";
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await RegistryService.RenameRegistryItemCategoryAsync(RegistryId, categoryId, CategoryRenameName, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
EditingCategoryId = null;
|
|
CategoryRenameName = null;
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
ItemManagementMessage = ex.Message;
|
|
}
|
|
}
|
|
|
|
protected async Task RemoveCategoryAsync(Guid categoryId)
|
|
{
|
|
ItemManagementMessage = null;
|
|
|
|
if (ItemCategories.Count <= 1)
|
|
{
|
|
ItemManagementMessage = "At least one category is required.";
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await RegistryService.RemoveRegistryItemCategoryAsync(RegistryId, categoryId, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
|
|
if (ItemModel.CategoryId == categoryId)
|
|
{
|
|
ItemModel.CategoryId = ItemCategories.FirstOrDefault()?.Id;
|
|
}
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
ItemManagementMessage = ex.Message;
|
|
}
|
|
}
|
|
|
|
protected void OnItemDragStart(Guid itemId)
|
|
{
|
|
_draggedItemId = itemId;
|
|
_draggedCategoryId = null;
|
|
_itemDropCategoryId = null;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
_categoryDropTargetId = null;
|
|
_categoryDropAfterTarget = false;
|
|
_categoryDropAtEnd = false;
|
|
}
|
|
|
|
protected void OnItemDragEnd()
|
|
{
|
|
_draggedItemId = null;
|
|
_itemDropCategoryId = null;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
}
|
|
|
|
protected void OnCategoryDragStart(Guid categoryId)
|
|
{
|
|
_draggedCategoryId = categoryId;
|
|
_draggedItemId = null;
|
|
_itemDropCategoryId = null;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
_categoryDropTargetId = null;
|
|
_categoryDropAfterTarget = false;
|
|
_categoryDropAtEnd = false;
|
|
}
|
|
|
|
protected void OnCategoryDragEnd()
|
|
{
|
|
_draggedCategoryId = null;
|
|
_categoryDropTargetId = null;
|
|
_categoryDropAfterTarget = false;
|
|
_categoryDropAtEnd = false;
|
|
}
|
|
|
|
protected static void OnDragOver(DragEventArgs _)
|
|
{
|
|
}
|
|
|
|
protected async Task OnItemDragOverAsync(Guid categoryId, Guid targetItemId, DragEventArgs args)
|
|
{
|
|
if (!_draggedItemId.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_itemDropCategoryId = categoryId;
|
|
_itemDropTargetItemId = targetItemId;
|
|
|
|
var isBottomHalf = await IsPointerInBottomHalfAsync($"item-row-{targetItemId}", args.ClientY).ConfigureAwait(false);
|
|
_itemDropAfterTarget = isBottomHalf;
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
protected void OnCategoryItemsDragOver(Guid categoryId)
|
|
{
|
|
if (!_draggedItemId.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var category = ItemCategories.FirstOrDefault(x => x.Id == categoryId);
|
|
if (category is null || category.Items.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_itemDropCategoryId = categoryId;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
}
|
|
|
|
protected async Task OnCategoryDragOverAsync(Guid categoryId, DragEventArgs args)
|
|
{
|
|
if (!_draggedCategoryId.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_categoryDropTargetId = categoryId;
|
|
_categoryDropAfterTarget = await IsPointerInBottomHalfAsync($"category-card-{categoryId}", args.ClientY).ConfigureAwait(false);
|
|
_categoryDropAtEnd = false;
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
protected void OnCategoryListEndDragOver()
|
|
{
|
|
if (!_draggedCategoryId.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_categoryDropTargetId = null;
|
|
_categoryDropAfterTarget = false;
|
|
_categoryDropAtEnd = true;
|
|
}
|
|
|
|
protected string GetItemDropClass(Guid categoryId, Guid itemId)
|
|
{
|
|
if (_itemDropCategoryId != categoryId || _itemDropTargetItemId != itemId)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return _itemDropAfterTarget ? "drop-target-item-after" : "drop-target-item-before";
|
|
}
|
|
|
|
protected string GetCategoryItemsDropClass(Guid categoryId)
|
|
{
|
|
return _itemDropCategoryId == categoryId && !_itemDropTargetItemId.HasValue
|
|
? "drop-target-item-end"
|
|
: string.Empty;
|
|
}
|
|
|
|
protected string GetCategoryDropClass(Guid categoryId)
|
|
{
|
|
if (_categoryDropTargetId != categoryId)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return _categoryDropAfterTarget ? "drop-target-category-after" : "drop-target-category-before";
|
|
}
|
|
|
|
protected string GetCategoryListEndDropClass()
|
|
{
|
|
return _categoryDropAtEnd ? "drop-target-category-list-end" : string.Empty;
|
|
}
|
|
|
|
protected async Task OnItemDropAsync(Guid categoryId, Guid targetItemId)
|
|
{
|
|
if (!_draggedItemId.HasValue || _dropOperationInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dropOperationInProgress = true;
|
|
try
|
|
{
|
|
var draggedItem = Items.FirstOrDefault(x => x.Id == _draggedItemId.Value);
|
|
var targetCategory = ItemCategories.FirstOrDefault(x => x.Id == categoryId);
|
|
|
|
if (draggedItem is null || targetCategory is null)
|
|
{
|
|
_draggedItemId = null;
|
|
_itemDropCategoryId = null;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
return;
|
|
}
|
|
|
|
var targetItems = targetCategory.Items.Where(x => x.Id.HasValue).ToList();
|
|
var targetIndex = targetItems.FindIndex(x => x.Id == targetItemId);
|
|
if (targetIndex < 0)
|
|
{
|
|
targetIndex = targetItems.Count;
|
|
}
|
|
|
|
var placeAfter = _itemDropTargetItemId == targetItemId && _itemDropAfterTarget;
|
|
if (placeAfter)
|
|
{
|
|
targetIndex++;
|
|
}
|
|
|
|
if (draggedItem.CategoryId == categoryId)
|
|
{
|
|
var currentIndex = targetItems.FindIndex(x => x.Id == _draggedItemId.Value);
|
|
if (currentIndex >= 0 && currentIndex < targetIndex)
|
|
{
|
|
targetIndex--;
|
|
}
|
|
}
|
|
|
|
await RegistryService.MoveRegistryItemAsync(RegistryId, _draggedItemId.Value, categoryId, targetIndex, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
|
|
_draggedItemId = null;
|
|
_itemDropCategoryId = null;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
}
|
|
finally
|
|
{
|
|
_dropOperationInProgress = false;
|
|
}
|
|
}
|
|
|
|
protected async Task OnCategoryItemsDropAsync(Guid categoryId)
|
|
{
|
|
if (!_draggedItemId.HasValue || _dropOperationInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dropOperationInProgress = true;
|
|
try
|
|
{
|
|
var targetCategory = ItemCategories.FirstOrDefault(x => x.Id == categoryId);
|
|
if (targetCategory is null)
|
|
{
|
|
_draggedItemId = null;
|
|
_itemDropCategoryId = null;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
return;
|
|
}
|
|
|
|
var targetIndex = targetCategory.Items.Count;
|
|
await RegistryService.MoveRegistryItemAsync(RegistryId, _draggedItemId.Value, categoryId, targetIndex, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
|
|
_draggedItemId = null;
|
|
_itemDropCategoryId = null;
|
|
_itemDropTargetItemId = null;
|
|
_itemDropAfterTarget = false;
|
|
}
|
|
finally
|
|
{
|
|
_dropOperationInProgress = false;
|
|
}
|
|
}
|
|
|
|
protected async Task OnCategoryDropAsync(Guid targetCategoryId)
|
|
{
|
|
if (!_draggedCategoryId.HasValue || _draggedCategoryId == targetCategoryId || _dropOperationInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dropOperationInProgress = true;
|
|
try
|
|
{
|
|
var targetIndex = ItemCategories.ToList().FindIndex(x => x.Id == targetCategoryId);
|
|
if (targetIndex < 0)
|
|
{
|
|
_draggedCategoryId = null;
|
|
_categoryDropTargetId = null;
|
|
_categoryDropAfterTarget = false;
|
|
return;
|
|
}
|
|
|
|
var placeAfter = _categoryDropTargetId == targetCategoryId && _categoryDropAfterTarget;
|
|
if (placeAfter)
|
|
{
|
|
targetIndex++;
|
|
}
|
|
|
|
var sourceIndex = ItemCategories.ToList().FindIndex(x => x.Id == _draggedCategoryId.Value);
|
|
if (sourceIndex >= 0 && sourceIndex < targetIndex)
|
|
{
|
|
targetIndex--;
|
|
}
|
|
|
|
await RegistryService.MoveRegistryCategoryAsync(RegistryId, _draggedCategoryId.Value, targetIndex, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
|
|
_draggedCategoryId = null;
|
|
_categoryDropTargetId = null;
|
|
_categoryDropAfterTarget = false;
|
|
_categoryDropAtEnd = false;
|
|
}
|
|
finally
|
|
{
|
|
_dropOperationInProgress = false;
|
|
}
|
|
}
|
|
|
|
protected async Task OnCategoryListEndDropAsync()
|
|
{
|
|
if (!_draggedCategoryId.HasValue || _dropOperationInProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dropOperationInProgress = true;
|
|
try
|
|
{
|
|
await RegistryService.MoveRegistryCategoryAsync(RegistryId, _draggedCategoryId.Value, ItemCategories.Count, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
|
|
_draggedCategoryId = null;
|
|
_categoryDropTargetId = null;
|
|
_categoryDropAfterTarget = false;
|
|
_categoryDropAtEnd = false;
|
|
}
|
|
finally
|
|
{
|
|
_dropOperationInProgress = false;
|
|
}
|
|
}
|
|
|
|
protected async Task FetchMetadataAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ItemModel.ProductUrl))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
|
var actorUserId = string.IsNullOrWhiteSpace(userId) ? "unknown-user" : userId;
|
|
|
|
async Task LogMetadataAsync(UserActionType actionType, string details)
|
|
{
|
|
try
|
|
{
|
|
await RegistryService.LogUserActionAsync(
|
|
RegistryId,
|
|
actorUserId,
|
|
actionType,
|
|
ItemModel.Id,
|
|
TruncateLogDetails(details),
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// Keep autofetch UX stable even if logging cannot be persisted.
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
var metadata = await RegistryMetadataService.FetchAsync(ItemModel.ProductUrl, CancellationToken.None).ConfigureAwait(false);
|
|
if (metadata is null)
|
|
{
|
|
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=no-metadata").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.NormalizedUrl))
|
|
{
|
|
ItemModel.ProductUrl = metadata.NormalizedUrl;
|
|
}
|
|
|
|
if ((string.IsNullOrWhiteSpace(ItemModel.Name) || string.Equals(ItemModel.Name, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Title))
|
|
{
|
|
ItemModel.Name = metadata.Title;
|
|
}
|
|
|
|
if ((string.IsNullOrWhiteSpace(ItemModel.Description) || string.Equals(ItemModel.Description, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Description))
|
|
{
|
|
ItemModel.Description = metadata.Description;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(ItemModel.PictureUrl) && !string.IsNullOrWhiteSpace(metadata.ImageUrl))
|
|
{
|
|
ItemModel.PictureUrl = metadata.ImageUrl;
|
|
}
|
|
|
|
if (!ItemModel.PriceAmount.HasValue && metadata.PriceAmount.HasValue)
|
|
{
|
|
ItemModel.PriceAmount = metadata.PriceAmount;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(metadata.CurrencyCode))
|
|
{
|
|
ItemModel.CurrencyCode = metadata.CurrencyCode;
|
|
}
|
|
|
|
var summary = $"metadata-fetch success; url={ItemModel.ProductUrl}; title={(string.IsNullOrWhiteSpace(metadata.Title) ? "-" : metadata.Title)}; image={(string.IsNullOrWhiteSpace(metadata.ImageUrl) ? "no" : "yes")}; price={(metadata.PriceAmount.HasValue ? metadata.PriceAmount.Value.ToString("0.00") : "-")}; currency={(string.IsNullOrWhiteSpace(metadata.CurrencyCode) ? "-" : metadata.CurrencyCode)}";
|
|
await LogMetadataAsync(UserActionType.MetadataFetchSucceeded, summary).ConfigureAwait(false);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=http; message={ex.Message}").ConfigureAwait(false);
|
|
}
|
|
catch (TaskCanceledException ex)
|
|
{
|
|
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=timeout-or-cancel; message={ex.Message}").ConfigureAwait(false);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=invalid-operation; message={ex.Message}").ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=unexpected-{ex.GetType().Name}; message={ex.Message}").ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private static string TruncateLogDetails(string details)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(details))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return details.Length <= 500 ? details : details[..500];
|
|
}
|
|
|
|
protected async Task CreateInviteAsync()
|
|
{
|
|
var inviteId = await RegistryService.CreateAdminInviteAsync(RegistryId, InviteEmail, TimeSpan.FromDays(7), CancellationToken.None).ConfigureAwait(false);
|
|
var token = await RegistryService.GetInviteTokenAsync(inviteId, CancellationToken.None).ConfigureAwait(false);
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
{
|
|
return;
|
|
}
|
|
|
|
InviteLink = NavigationManager.ToAbsoluteUri($"/registry/{RegistryId}/invite/{Uri.EscapeDataString(token)}").AbsoluteUri;
|
|
if (!string.IsNullOrWhiteSpace(InviteEmail))
|
|
{
|
|
await EmailSender.SendInviteAsync(InviteEmail, InviteLink).ConfigureAwait(false);
|
|
}
|
|
|
|
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
|
|
protected async Task DeleteAdminAsync(string? userId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await RegistryService.RemoveRegistryAdminAsync(RegistryId, userId, CancellationToken.None).ConfigureAwait(false);
|
|
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
|
|
protected void AddContributionAmountQrCode()
|
|
{
|
|
SettingsModel.ContributionAmountQrCodes.Add(new ContributionAmountQrCodeModel());
|
|
}
|
|
|
|
protected void RemoveContributionAmountQrCode(ContributionAmountQrCodeModel model)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(model);
|
|
SettingsModel.ContributionAmountQrCodes.Remove(model);
|
|
}
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
var settings = await RegistryService.GetRegistrySettingsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
|
if (settings is not null)
|
|
{
|
|
SettingsModel.BabyName = settings.BabyName;
|
|
SettingsModel.BirthDate = settings.BirthDate;
|
|
SettingsModel.HeaderContentHtml = settings.HeaderContentHtml;
|
|
SettingsModel.ShippingAddress = settings.ShippingAddress;
|
|
SettingsModel.CurrencyCode = settings.CurrencyCode;
|
|
SettingsModel.ThemeKey = settings.ThemeKey;
|
|
SettingsModel.BankAccountDisplayName = settings.BankAccountDisplayName;
|
|
SettingsModel.BankAccountIban = settings.BankAccountIban;
|
|
SettingsModel.BankAccountBic = settings.BankAccountBic;
|
|
SettingsModel.ShowBankAccountName = settings.ShowBankAccountName;
|
|
SettingsModel.ContributionQrCodeUrl = settings.ContributionQrCodeUrl;
|
|
SettingsModel.ContributionAmountQrCodes = settings.ContributionAmountQrCodes;
|
|
|
|
var currentHeaderContent = SettingsModel.HeaderContentHtml ?? string.Empty;
|
|
_pendingEditorLoad = currentHeaderContent != _lastLoadedHeaderContentHtml;
|
|
}
|
|
|
|
Admins = await RegistryService.GetRegistryAdminsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
|
AccessibleUserAddresses = await RegistryService.GetRegistryAccessibleUserAddressesAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
|
await ReloadItemManagementAsync().ConfigureAwait(false);
|
|
ItemModel = new RegistryItemEditModel
|
|
{
|
|
CurrencyCode = SettingsModel.CurrencyCode,
|
|
DesiredQuantity = 1,
|
|
CategoryId = ItemCategories.FirstOrDefault()?.Id
|
|
};
|
|
}
|
|
|
|
private async Task ReloadItemManagementAsync()
|
|
{
|
|
var categories = await RegistryService.GetRegistryItemCategoriesAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
|
var items = await RegistryService.GetRegistryItemsAsync(RegistryId, CancellationToken.None).ConfigureAwait(false);
|
|
|
|
Items = items;
|
|
|
|
var itemsByCategory = items
|
|
.Where(x => x.CategoryId.HasValue)
|
|
.GroupBy(x => x.CategoryId!.Value)
|
|
.ToDictionary(g => g.Key, g => (IReadOnlyList<RegistryItemEditModel>)g.ToList());
|
|
|
|
ItemCategories = categories
|
|
.OrderBy(x => x.SortOrder)
|
|
.Select(category => new RegistryItemCategoryEditModel
|
|
{
|
|
Id = category.Id,
|
|
Name = category.Name,
|
|
SortOrder = category.SortOrder,
|
|
Items = itemsByCategory.TryGetValue(category.Id, out var categoryItems)
|
|
? categoryItems
|
|
: []
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
private async Task<bool> IsPointerInBottomHalfAsync(string elementId, double clientY)
|
|
{
|
|
try
|
|
{
|
|
return await JSRuntime.InvokeAsync<bool>("birthListDrag.isPointerInBottomHalf", elementId, clientY).ConfigureAwait(false);
|
|
}
|
|
catch (JSException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|