mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-10 09:19:24 +01:00
parent
d6d14b4170
commit
e0a0406825
14 changed files with 230 additions and 61 deletions
|
@ -25,5 +25,6 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||||
public string Body { get; set; }
|
public string Body { get; set; }
|
||||||
public string ActionLink { get; set; }
|
public string ActionLink { get; set; }
|
||||||
public bool Seen { get; set; }
|
public bool Seen { get; set; }
|
||||||
|
public string StoreId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
|
@ -23,7 +22,6 @@ namespace BTCPayServer.Data
|
||||||
public byte[] Blob { get; set; }
|
public byte[] Blob { get; set; }
|
||||||
public string Blob2 { get; set; }
|
public string Blob2 { get; set; }
|
||||||
|
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||||
{
|
{
|
||||||
builder.Entity<NotificationData>()
|
builder.Entity<NotificationData>()
|
||||||
|
|
|
@ -2461,7 +2461,7 @@ namespace BTCPayServer.Tests
|
||||||
var ctrl = acc.GetController<UINotificationsController>();
|
var ctrl = acc.GetController<UINotificationsController>();
|
||||||
var newVersion = MockVersionFetcher.MOCK_NEW_VERSION;
|
var newVersion = MockVersionFetcher.MOCK_NEW_VERSION;
|
||||||
|
|
||||||
var vm = Assert.IsType<Models.NotificationViewModels.IndexViewModel>(
|
var vm = Assert.IsType<Models.NotificationViewModels.NotificationIndexViewModel>(
|
||||||
Assert.IsType<ViewResult>(await ctrl.Index()).Model);
|
Assert.IsType<ViewResult>(await ctrl.Index()).Model);
|
||||||
|
|
||||||
Assert.True(vm.Skip == 0);
|
Assert.True(vm.Skip == 0);
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Filters;
|
using BTCPayServer.Filters;
|
||||||
using BTCPayServer.Models.NotificationViewModels;
|
using BTCPayServer.Models.NotificationViewModels;
|
||||||
using BTCPayServer.Security;
|
|
||||||
using BTCPayServer.Services;
|
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
using BTCPayServer.Services.Notifications.Blobs;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -23,35 +19,59 @@ namespace BTCPayServer.Controllers
|
||||||
[Route("notifications/{action:lowercase=Index}")]
|
[Route("notifications/{action:lowercase=Index}")]
|
||||||
public class UINotificationsController : Controller
|
public class UINotificationsController : Controller
|
||||||
{
|
{
|
||||||
|
private readonly ApplicationDbContextFactory _factory;
|
||||||
|
private readonly StoreRepository _storeRepo;
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly NotificationManager _notificationManager;
|
private readonly NotificationManager _notificationManager;
|
||||||
|
|
||||||
public UINotificationsController(
|
public UINotificationsController(
|
||||||
|
StoreRepository storeRepo,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
NotificationManager notificationManager)
|
NotificationManager notificationManager,
|
||||||
|
ApplicationDbContextFactory factory)
|
||||||
{
|
{
|
||||||
|
_storeRepo = storeRepo;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_notificationManager = notificationManager;
|
_notificationManager = notificationManager;
|
||||||
|
_factory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Index(int skip = 0, int count = 50, int timezoneOffset = 0)
|
public async Task<IActionResult> Index(NotificationIndexViewModel model = null)
|
||||||
{
|
{
|
||||||
|
model ??= new NotificationIndexViewModel { Skip = 0 };
|
||||||
|
var timezoneOffset = model.TimezoneOffset ?? 0;
|
||||||
|
model.Status ??= "Unread";
|
||||||
|
ViewBag.Status = model.Status;
|
||||||
if (!ValidUserClaim(out var userId))
|
if (!ValidUserClaim(out var userId))
|
||||||
return RedirectToAction("Index", "UIHome");
|
return RedirectToAction("Index", "UIHome");
|
||||||
|
|
||||||
var res = await _notificationManager.GetNotifications(new NotificationsQuery()
|
var stores = await _storeRepo.GetStoresByUserId(userId);
|
||||||
{
|
model.Stores = stores.Where(store => !store.Archived).OrderBy(s => s.StoreName).ToList();
|
||||||
Skip = skip,
|
|
||||||
Take = count,
|
|
||||||
UserId = userId
|
|
||||||
});
|
|
||||||
|
|
||||||
var model = new IndexViewModel() { Skip = skip, Count = count, Items = res.Items, Total = res.Count };
|
|
||||||
|
await using var dbContext = _factory.CreateContext();
|
||||||
|
|
||||||
|
var searchTerm = string.IsNullOrEmpty(model.SearchText) ? model.SearchTerm : $"{model.SearchText},{model.SearchTerm}";
|
||||||
|
var fs = new SearchString(searchTerm, timezoneOffset);
|
||||||
|
model.Search = fs;
|
||||||
|
|
||||||
|
var res = await _notificationManager.GetNotifications(new NotificationsQuery
|
||||||
|
{
|
||||||
|
Skip = model.Skip,
|
||||||
|
Take = model.Count,
|
||||||
|
UserId = userId,
|
||||||
|
SearchText = model.SearchText,
|
||||||
|
Type = fs.GetFilterArray("type"),
|
||||||
|
Stores = fs.GetFilterArray("store"),
|
||||||
|
Seen = model.Status == "Unread" ? false : null
|
||||||
|
});
|
||||||
|
model.Items = res.Items;
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)]
|
||||||
public async Task<IActionResult> FlipRead(string id)
|
public async Task<IActionResult> FlipRead(string id)
|
||||||
|
|
|
@ -237,7 +237,7 @@ namespace BTCPayServer.HostedServices
|
||||||
if (InvoiceEventNotification.HandlesEvent(b.Name))
|
if (InvoiceEventNotification.HandlesEvent(b.Name))
|
||||||
{
|
{
|
||||||
await _notificationSender.SendNotification(new StoreScope(b.Invoice.StoreId),
|
await _notificationSender.SendNotification(new StoreScope(b.Invoice.StoreId),
|
||||||
new InvoiceEventNotification(b.Invoice.Id, b.Name));
|
new InvoiceEventNotification(b.Invoice.Id, b.Name, b.Invoice.StoreId));
|
||||||
}
|
}
|
||||||
if (b.Name == InvoiceEvent.Created)
|
if (b.Name == InvoiceEvent.Created)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.NotificationViewModels
|
namespace BTCPayServer.Models.NotificationViewModels
|
||||||
{
|
{
|
||||||
public class IndexViewModel : BasePagingViewModel
|
public class IndexViewModel : BasePagingViewModel
|
||||||
{
|
{
|
||||||
public List<NotificationViewModel> Items { get; set; }
|
public List<NotificationViewModel> Items { get; set; } = [];
|
||||||
|
public string SearchText { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
public SearchString Search { get; set; }
|
||||||
public override int CurrentPageCount => Items.Count;
|
public override int CurrentPageCount => Items.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class NotificationIndexViewModel : IndexViewModel
|
||||||
|
{
|
||||||
|
public List<StoreData> Stores { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
|
||||||
{
|
{
|
||||||
vm.Identifier = notification.Identifier;
|
vm.Identifier = notification.Identifier;
|
||||||
vm.Type = notification.NotificationType;
|
vm.Type = notification.NotificationType;
|
||||||
|
vm.StoreId = notification.StoreId;
|
||||||
vm.Body =
|
vm.Body =
|
||||||
"A payment that was made to an approved payout by an external wallet is waiting for your confirmation.";
|
"A payment that was made to an approved payout by an external wallet is waiting for your confirmation.";
|
||||||
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),
|
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),
|
||||||
|
|
|
@ -44,6 +44,7 @@ internal class InviteAcceptedNotification : BaseNotification
|
||||||
{
|
{
|
||||||
vm.Identifier = notification.Identifier;
|
vm.Identifier = notification.Identifier;
|
||||||
vm.Type = notification.NotificationType;
|
vm.Type = notification.NotificationType;
|
||||||
|
vm.StoreId = notification.StoreId;
|
||||||
vm.Body = $"User {notification.UserEmail} accepted the invite to {notification.StoreName}.";
|
vm.Body = $"User {notification.UserEmail} accepted the invite to {notification.StoreName}.";
|
||||||
vm.ActionLink = linkGenerator.GetPathByAction(nameof(UIStoresController.StoreUsers),
|
vm.ActionLink = linkGenerator.GetPathByAction(nameof(UIStoresController.StoreUsers),
|
||||||
"UIStores",
|
"UIStores",
|
||||||
|
|
|
@ -54,6 +54,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
|
||||||
}
|
}
|
||||||
vm.Identifier = notification.Identifier;
|
vm.Identifier = notification.Identifier;
|
||||||
vm.Type = notification.NotificationType;
|
vm.Type = notification.NotificationType;
|
||||||
|
vm.StoreId = notification?.StoreId;
|
||||||
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIInvoiceController.Invoice),
|
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIInvoiceController.Invoice),
|
||||||
"UIInvoice",
|
"UIInvoice",
|
||||||
new { invoiceId = notification.InvoiceId }, _options.RootPath);
|
new { invoiceId = notification.InvoiceId }, _options.RootPath);
|
||||||
|
@ -64,10 +65,11 @@ namespace BTCPayServer.Services.Notifications.Blobs
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public InvoiceEventNotification(string invoiceId, string invoiceEvent)
|
public InvoiceEventNotification(string invoiceId, string invoiceEvent, string storeId)
|
||||||
{
|
{
|
||||||
InvoiceId = invoiceId;
|
InvoiceId = invoiceId;
|
||||||
Event = invoiceEvent;
|
Event = invoiceEvent;
|
||||||
|
StoreId = storeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HandlesEvent(string invoiceEvent)
|
public static bool HandlesEvent(string invoiceEvent)
|
||||||
|
@ -77,6 +79,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
|
||||||
|
|
||||||
public string InvoiceId { get; set; }
|
public string InvoiceId { get; set; }
|
||||||
public string Event { get; set; }
|
public string Event { get; set; }
|
||||||
|
public string StoreId { get; set; }
|
||||||
public override string Identifier => Event is null ? TYPE : Event.ToStringLowerInvariant();
|
public override string Identifier => Event is null ? TYPE : Event.ToStringLowerInvariant();
|
||||||
public override string NotificationType => TYPE;
|
public override string NotificationType => TYPE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
|
||||||
{
|
{
|
||||||
vm.Identifier = notification.Identifier;
|
vm.Identifier = notification.Identifier;
|
||||||
vm.Type = notification.NotificationType;
|
vm.Type = notification.NotificationType;
|
||||||
|
vm.StoreId = notification.StoreId;
|
||||||
vm.Body = (notification.Status ?? PayoutState.AwaitingApproval) switch
|
vm.Body = (notification.Status ?? PayoutState.AwaitingApproval) switch
|
||||||
{
|
{
|
||||||
PayoutState.AwaitingApproval => "A new payout is awaiting for approval",
|
PayoutState.AwaitingApproval => "A new payout is awaiting for approval",
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Notifications
|
namespace BTCPayServer.Services.Notifications
|
||||||
{
|
{
|
||||||
|
@ -28,7 +25,6 @@ namespace BTCPayServer.Services.Notifications
|
||||||
_handlersByNotificationType = handlers.ToDictionary(h => h.NotificationType);
|
_handlersByNotificationType = handlers.ToDictionary(h => h.NotificationType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<(List<NotificationViewModel> Items, int? Count)> GetSummaryNotifications(string userId, bool cachedOnly)
|
public async Task<(List<NotificationViewModel> Items, int? Count)> GetSummaryNotifications(string userId, bool cachedOnly)
|
||||||
{
|
{
|
||||||
var cacheKey = GetNotificationsCacheId(userId);
|
var cacheKey = GetNotificationsCacheId(userId);
|
||||||
|
@ -68,7 +64,7 @@ namespace BTCPayServer.Services.Notifications
|
||||||
|
|
||||||
var queryables = GetNotificationsQueryable(dbContext, query);
|
var queryables = GetNotificationsQueryable(dbContext, query);
|
||||||
var items = (await queryables.withPaging.ToListAsync()).Select(ToViewModel).Where(model => model != null).ToList();
|
var items = (await queryables.withPaging.ToListAsync()).Select(ToViewModel).Where(model => model != null).ToList();
|
||||||
|
items = FilterNotifications(items, query);
|
||||||
int? count = null;
|
int? count = null;
|
||||||
if (query.Seen is false)
|
if (query.Seen is false)
|
||||||
{
|
{
|
||||||
|
@ -134,6 +130,34 @@ namespace BTCPayServer.Services.Notifications
|
||||||
return (queryable, queryable2);
|
return (queryable, queryable2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<NotificationViewModel> FilterNotifications(List<NotificationViewModel> notifications, NotificationsQuery query)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(query.SearchText))
|
||||||
|
{
|
||||||
|
notifications = notifications.Where(data => data.Body.Contains(query.SearchText)).ToList();
|
||||||
|
}
|
||||||
|
if (query.Type?.Length > 0)
|
||||||
|
{
|
||||||
|
if (query.Type?.Length > 0)
|
||||||
|
{
|
||||||
|
if (query.Type.Contains("userupdate"))
|
||||||
|
{
|
||||||
|
notifications = notifications.Where(n => n.Type.Equals("inviteaccepted", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
n.Type.Equals("userapproval", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
notifications = notifications.Where(n => query.Type.Contains(n.Type, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query.Stores?.Length > 0)
|
||||||
|
{
|
||||||
|
notifications = notifications.Where(n => !string.IsNullOrEmpty(n.StoreId) && query.Stores.Contains(n.StoreId, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<NotificationViewModel>> ToggleSeen(NotificationsQuery notificationsQuery, bool? setSeen)
|
public async Task<List<NotificationViewModel>> ToggleSeen(NotificationsQuery notificationsQuery, bool? setSeen)
|
||||||
{
|
{
|
||||||
await using var dbContext = _factory.CreateContext();
|
await using var dbContext = _factory.CreateContext();
|
||||||
|
@ -195,5 +219,8 @@ namespace BTCPayServer.Services.Notifications
|
||||||
public int? Skip { get; set; }
|
public int? Skip { get; set; }
|
||||||
public int? Take { get; set; }
|
public int? Take { get; set; }
|
||||||
public bool? Seen { get; set; }
|
public bool? Seen { get; set; }
|
||||||
|
public string SearchText { get; set; }
|
||||||
|
public string[] Type { get; set; }
|
||||||
|
public string[] Stores { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,11 @@ namespace BTCPayServer.Services.Notifications
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BaseNotification GetBaseNotification(NotificationData notificationData)
|
||||||
|
{
|
||||||
|
return notificationData.HasTypedBlob<BaseNotification>().GetBlob();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string[]> GetUsers(INotificationScope scope, string notificationIdentifier)
|
private async Task<string[]> GetUsers(INotificationScope scope, string notificationIdentifier)
|
||||||
{
|
{
|
||||||
await using var ctx = _contextFactory.CreateContext();
|
await using var ctx = _contextFactory.CreateContext();
|
||||||
|
|
|
@ -1,38 +1,137 @@
|
||||||
@model BTCPayServer.Models.NotificationViewModels.IndexViewModel
|
@model BTCPayServer.Models.NotificationViewModels.NotificationIndexViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Notifications";
|
ViewData["Title"] = "Notifications";
|
||||||
|
string status = ViewBag.Status;
|
||||||
|
var statusFilterCount = CountArrayFilter("type");
|
||||||
|
var storesFilterCount = CountArrayFilter("store");
|
||||||
}
|
}
|
||||||
|
|
||||||
<partial name="_StatusMessage" />
|
@functions
|
||||||
|
{
|
||||||
|
private int CountArrayFilter(string type) =>
|
||||||
|
Model.Search.ContainsFilter(type) ? Model.Search.GetFilterArray(type).Length : 0;
|
||||||
|
|
||||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
private bool HasArrayFilter(string type, string key = null) =>
|
||||||
|
Model.Search.ContainsFilter(type) && (key is null || Model.Search.GetFilterArray(type).Contains(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@section PageHeadContent
|
||||||
|
{
|
||||||
|
<style>
|
||||||
|
.dropdown > .btn {
|
||||||
|
min-width: 7rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
@@media (max-width: 568px) {
|
||||||
|
#SearchText {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="sticky-header">
|
||||||
<h2 class="my-1">@ViewData["Title"]</h2>
|
<h2 class="my-1">@ViewData["Title"]</h2>
|
||||||
<a id="NotificationSettings" asp-controller="UIManage" asp-action="NotificationSettings" class="btn btn-secondary">
|
<a id="NotificationSettings" asp-controller="UIManage" asp-action="NotificationSettings" class="btn btn-secondary d-flex align-items-center">
|
||||||
<vc:icon symbol="settings" />
|
<vc:icon symbol="nav-store-settings" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<partial name="_StatusMessage" />
|
||||||
|
<form asp-route-status="@Model.Status" class="d-flex flex-wrap align-items-center gap-md-4 gap-sm-0 mb-4 col-xxl-8" asp-action="Index" method="get">
|
||||||
|
<input asp-for="Count" type="hidden" />
|
||||||
|
<input asp-for="TimezoneOffset" type="hidden" />
|
||||||
|
<input type="hidden" asp-for="Status" value="@Model.Status" />
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 col-lg-4 mb-3 mb-md-0">
|
||||||
|
<input asp-for="SearchText" class="form-control" placeholder="Search…" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group col-12 col-md-auto mb-3 mb-md-0" role="group" aria-label="View Notification">
|
||||||
|
<a class="btn @((status == "All") ? "btn-primary" : "btn-outline-secondary")"
|
||||||
|
asp-controller="UINotifications"
|
||||||
|
asp-action="Index"
|
||||||
|
asp-route-Status="All">
|
||||||
|
All
|
||||||
|
</a>
|
||||||
|
<a class="btn @((status == "Unread") ? "btn-primary" : "btn-outline-secondary")"
|
||||||
|
asp-controller="UINotifications"
|
||||||
|
asp-action="Index"
|
||||||
|
asp-route-Status="Unread">
|
||||||
|
Unread
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown col-12 col-md-auto mb-3 mb-md-0">
|
||||||
|
<button id="StatusOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret w-100 w-md-auto" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
@if (statusFilterCount > 0)
|
||||||
|
{
|
||||||
|
<span>@statusFilterCount Type</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>All Type</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="StatusOptionsToggle">
|
||||||
|
<a asp-action="Index" asp-route-count="@Model.Count" asp-route-status="@Model.Status" asp-route-searchTerm="@Model.Search.Toggle("type", "invoicestate")" class="dropdown-item @(HasArrayFilter("type", "invoicestate") ? "custom-active" : "")">Invoice</a>
|
||||||
|
<a asp-action="Index" asp-route-count="@Model.Count" asp-route-status="@Model.Status" asp-route-searchTerm="@Model.Search.Toggle("type", "payout")" class="dropdown-item @(HasArrayFilter("type", "payout") ? "custom-active" : "")">Payouts</a>
|
||||||
|
<a asp-action="Index" asp-route-count="@Model.Count" asp-route-status="@Model.Status" asp-route-searchTerm="@Model.Search.Toggle("type", "newversion")" class="dropdown-item @(HasArrayFilter("type", "newversion") ? "custom-active" : "")">New Version</a>
|
||||||
|
<a asp-action="Index" asp-route-count="@Model.Count" asp-route-status="@Model.Status" asp-route-searchTerm="@Model.Search.Toggle("type", "pluginupdate")" class="dropdown-item @(HasArrayFilter("type", "pluginupdate") ? "custom-active" : "")">Plugin Updates</a>
|
||||||
|
<a asp-action="Index" asp-route-count="@Model.Count" asp-route-status="@Model.Status" asp-route-searchTerm="@Model.Search.Toggle("type", "userupdate")" class="dropdown-item @(HasArrayFilter("type", "userupdate") ? "custom-active" : "")">User Updates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown col-12 col-md-auto">
|
||||||
|
<button id="StoresOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret w-100 w-md-auto" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
@if (storesFilterCount > 0)
|
||||||
|
{
|
||||||
|
<span>@storesFilterCount Store</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>All Stores</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="StoresOptionsToggle">
|
||||||
|
@foreach (var store in Model.Stores)
|
||||||
|
{
|
||||||
|
<a asp-action="Index"
|
||||||
|
asp-route-count="@Model.Count"
|
||||||
|
asp-route-status="@Model.Status"
|
||||||
|
asp-route-searchTerm="@Model.Search.Toggle("store", store.Id)"
|
||||||
|
class="dropdown-item @(HasArrayFilter("store", store.Id) ? "custom-active" : "")">
|
||||||
|
@store.StoreName
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@if (Model.Items.Count > 0)
|
@if (Model.Items.Count > 0)
|
||||||
{
|
{
|
||||||
<form method="post" asp-action="MassAction">
|
<form method="post" asp-action="MassAction">
|
||||||
@if (Model.Items.Any())
|
@if (Model.Items.Any())
|
||||||
{
|
{
|
||||||
<div class="table-responsive-md">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mass-action">
|
<table class="table table-hover mass-action">
|
||||||
<thead class="mass-action-head">
|
<thead class="mass-action-head">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="mass-action-select-col only-for-js">
|
<th class="mass-action-select-col only-for-js">
|
||||||
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select-all" />
|
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select-all" />
|
||||||
</th>
|
</th>
|
||||||
|
<th>Message</th>
|
||||||
<th class="date-col">
|
<th class="date-col">
|
||||||
<div class="d-flex align-items-center gap-1">
|
<div class="d-flex align-items-center gap-1">
|
||||||
Date
|
Date
|
||||||
<button type="button" class="btn btn-link p-0 switch-time-format" title="Switch date format">
|
<button type="button" class="btn btn-link p-0 switch-time-format only-for-js" title="Switch date format">
|
||||||
<vc:icon symbol="time" />
|
<vc:icon symbol="time" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th>Message</th>
|
<th class="text-end text-success">Actions</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<thead class="mass-action-actions">
|
<thead class="mass-action-actions">
|
||||||
|
@ -40,30 +139,31 @@
|
||||||
<th class="mass-action-select-col only-for-js">
|
<th class="mass-action-select-col only-for-js">
|
||||||
<input type="checkbox" class="form-check-input mass-action-select-all" />
|
<input type="checkbox" class="form-check-input mass-action-select-all" />
|
||||||
</th>
|
</th>
|
||||||
<th colspan="6">
|
<th colspan="3">
|
||||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
|
<div class="d-flex flex-wrap align-items-center justify-content-start gap-3">
|
||||||
<div>
|
|
||||||
<strong class="mass-action-selected-count">0</strong>
|
|
||||||
selected
|
|
||||||
</div>
|
|
||||||
<div class="d-inline-flex align-items-center gap-3">
|
<div class="d-inline-flex align-items-center gap-3">
|
||||||
|
@if (Model.Status == "Unread")
|
||||||
|
{
|
||||||
<button type="submit" name="command" value="mark-seen" class="btn btn-link gap-1">
|
<button type="submit" name="command" value="mark-seen" class="btn btn-link gap-1">
|
||||||
<vc:icon symbol="actions-show" />
|
<vc:icon symbol="actions-show" />
|
||||||
Mark seen
|
Mark as seen
|
||||||
</button>
|
|
||||||
<button type="submit" name="command" value="mark-unseen" class="btn btn-link gap-1">
|
|
||||||
<vc:icon symbol="actions-hide" />
|
|
||||||
Mark unseen
|
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
<button type="submit" name="command" value="delete" class="btn btn-link gap-1">
|
<button type="submit" name="command" value="delete" class="btn btn-link gap-1">
|
||||||
<vc:icon symbol="actions-trash" />
|
<vc:icon symbol="actions-trash" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="vr"></div>
|
||||||
|
<div>
|
||||||
|
<strong class="mass-action-selected-count">0</strong>
|
||||||
|
selected
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var item in Model.Items)
|
@foreach (var item in Model.Items)
|
||||||
{
|
{
|
||||||
|
@ -71,19 +171,23 @@
|
||||||
<td class="only-for-js mass-action-select-col">
|
<td class="only-for-js mass-action-select-col">
|
||||||
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select" value="@item.Id" />
|
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select" value="@item.Id" />
|
||||||
</td>
|
</td>
|
||||||
<td class="date-col">@item.Created.ToBrowserDate()</td>
|
|
||||||
<td>
|
<td>
|
||||||
@item.Body
|
@item.Body
|
||||||
</td>
|
</td>
|
||||||
|
<td class="date-col fw-normal">@item.Created.ToTimeAgo()</td>
|
||||||
|
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="d-inline-flex align-items-center gap-3">
|
<div class="d-inline-flex align-items-center gap-3">
|
||||||
<button class="btn btn-link p-0 btn-toggle-seen" type="submit" name="command" value="flip-individual:@(item.Id)">
|
|
||||||
<span>Mark</span> <span class="seen-text"></span>
|
|
||||||
</button>
|
|
||||||
@if (!string.IsNullOrEmpty(item.ActionLink))
|
@if (!string.IsNullOrEmpty(item.ActionLink))
|
||||||
{
|
{
|
||||||
<a href="@item.ActionLink" class="btn btn-link p-0" rel="noreferrer noopener">Details</a>
|
<a href="@item.ActionLink" class="btn btn-link p-0" rel="noreferrer noopener">Details</a>
|
||||||
}
|
}
|
||||||
|
@if (!item.Seen)
|
||||||
|
{
|
||||||
|
<button class="btn btn-link p-0 btn-toggle-seen text-nowrap" type="submit" name="command" value="flip-individual:@(item.Id)">
|
||||||
|
<span>Mark as seen</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -103,20 +207,15 @@ else
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.notification-row.loading {
|
.notification-row.loading {
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seen-text::after {
|
|
||||||
content: "seen";
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.seen td .seen-text::after {
|
|
||||||
content: "unseen";
|
|
||||||
}
|
|
||||||
|
|
||||||
tr td {
|
tr td {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,11 @@ a.unobtrusive-link {
|
||||||
transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Time format button */
|
||||||
|
.switch-time-format {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Icon and text, used for warnings of additional info text. Adjust spacing and color via utility classes. */
|
/* Icon and text, used for warnings of additional info text. Adjust spacing and color via utility classes. */
|
||||||
.info-note {
|
.info-note {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
Loading…
Add table
Reference in a new issue