From 14f0b1ba2464b3a382ee4b1b97572b8e0d780663 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Fri, 8 Nov 2024 16:17:45 +0330 Subject: [PATCH] feat(templates): add Diagnostic page to Boilerplate #9123 (#9127) --- .../Components/ClientAppCoordinator.cs | 24 +--- .../Components/Layout/AuthorizedHeader.razor | 2 +- .../Components/Layout/DiagnosticModal.razor | 55 +++++++++ .../Layout/DiagnosticModal.razor.cs | 111 ++++++++++++++++++ .../Layout/DiagnosticModal.razor.scss | 23 ++++ .../Components/Layout/DiagnosticSpacer.razor | 15 +++ .../Components/Layout/IdentityHeader.razor | 2 +- .../Components/Layout/JsBridge.razor | 4 + .../Components/Layout/JsBridge.razor.cs | 34 ++++++ .../Components/Layout/MessageBox.razor.scss | 8 +- .../Components/Layout/NavBar.razor.scss | 6 +- .../Components/Layout/NavPanel.razor.scss | 2 +- .../Components/Layout/RootContainer.razor | 2 +- .../Layout/RootContainer.razor.scss | 2 - .../Components/Layout/RootLayout.razor | 3 + .../Components/Layout/RootLayout.razor.cs | 9 ++ .../Components/Layout/RootLayout.razor.scss | 15 ++- .../Components/Pages/AppPageBase.cs | 2 +- .../Categories/CategoriesPage.razor | 2 +- .../Categories/CategoriesPage.razor.scss | 15 ++- .../Authorized/Products/ProductsPage.razor | 2 +- .../Products/ProductsPage.razor.scss | 15 ++- .../Pages/Authorized/Todo/TodoPage.razor | 3 +- .../Pages/Authorized/Todo/TodoPage.razor.scss | 9 ++ .../Pages/Identity/SignIn/SignInPanel.razor | 3 +- .../Identity/SignIn/SignInPanel.razor.scss | 16 +++ .../Pages/Identity/SignUp/SignUpPage.razor | 3 +- .../Identity/SignUp/SignUpPage.razor.scss | 12 ++ .../IClientCoreServiceCollectionExtensions.cs | 1 - .../Extensions/ILoggingBuilderExtensions.cs | 1 - .../Boilerplate.Client.Core/Scripts/app.ts | 39 +++++- .../Contracts/CurrentScopeProvider.cs | 19 --- .../Services/DiagnosticLog/DiagnosticLog.cs | 16 +++ .../DiagnosticLog/DiagnosticLogger.cs | 48 +------- .../DiagnosticLog/DiagnosticLoggerProvider.cs | 11 +- .../Services/PubSubMessages.cs | 3 +- .../Boilerplate.Client.Core/Styles/app.scss | 12 +- .../compilerconfig.json | 6 + .../wwwroot/index.html | 2 +- .../Components/App.razor | 2 +- .../Program.Services.cs | 2 - .../Extensions/ICollectionExtensions.cs | 5 + 42 files changed, 433 insertions(+), 133 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.scss create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticSpacer.razor create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/JsBridge.razor create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/JsBridge.razor.cs delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/CurrentScopeProvider.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs index f54acce386..67ab65068f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs @@ -19,6 +19,7 @@ public partial class ClientAppCoordinator : AppComponentBase { //#if (signalr == true) private HubConnection? hubConnection; + [AutoInject] private IServiceProvider serviceProvider = default!; //#endif //#if (notification == true) [AutoInject] private IPushNotificationService pushNotificationService = default!; @@ -237,27 +238,4 @@ protected override async ValueTask DisposeAsync(bool disposing) await base.DisposeAsync(disposing); } - - [AutoInject] - private IServiceProvider serviceProvider - { - set => currentServiceProvider = value; - get => currentServiceProvider!; - } - - private static IServiceProvider? currentServiceProvider; - public static IServiceProvider? CurrentServiceProvider - { - get - { - if (AppPlatform.IsBlazorHybridOrBrowser is false) - throw new InvalidOperationException($"{nameof(CurrentServiceProvider)} is only available in Blazor Hybrid or blazor web assembly."); - - return currentServiceProvider; - } - private set - { - currentServiceProvider = value; - } - } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AuthorizedHeader.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AuthorizedHeader.razor index d4cb247b10..504d51e8c8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AuthorizedHeader.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AuthorizedHeader.razor @@ -22,7 +22,7 @@ } - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor new file mode 100644 index 0000000000..9e5a944203 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor @@ -0,0 +1,55 @@ +@inherits AppComponentBase + +
+ + + + Diagnostic + + + + + + + + + + + + Nothing to show! + + + @($"{logIndex.index + 1}. [{logIndex.item.CreatedOn.ToString("HH:mm:ss")}]") + + @logIndex.item.Message + + + + + + + +
\ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.cs new file mode 100644 index 0000000000..0ec12ff11b --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.cs @@ -0,0 +1,111 @@ +using Boilerplate.Client.Core.Services.DiagnosticLog; +using Microsoft.Extensions.Logging; + +namespace Boilerplate.Client.Core.Components.Layout; + +/// +/// This modal can be opened by clicking 7 times on the spacer of the header or by pressing Ctrl+Shift+X. +/// Also by calling `App.showDiagnostic` function using the dev-tools console. +/// +public partial class DiagnosticModal : IDisposable +{ + private bool isOpen; + private string? searchText; + private bool isDescendingSort; + private Action unsubscribe = default!; + private IEnumerable filterLogLevels = []; + private IEnumerable allLogs = default!; + private IEnumerable filteredLogs = default!; + private BitBasicList<(DiagnosticLog, int)> logStackRef = default!; + private readonly LogLevel[] defaultFilterLogLevels = [LogLevel.Warning, LogLevel.Error, LogLevel.Critical]; + private readonly BitDropdownItem[] logLevelItems = Enum.GetValues().Select(v => new BitDropdownItem() { Value = v, Text = v.ToString() }).ToArray(); + + + [AutoInject] private Clipboard clipboard = default!; + + + protected override Task OnInitAsync() + { + unsubscribe = PubSubService.Subscribe(PubSubMessages.SHOW_DIAGNOSTIC_MODAL, async _ => + { + isOpen = true; + allLogs = [.. DiagnosticLogger.Store]; + HandleOnLogLevelFilter(defaultFilterLogLevels); + await InvokeAsync(StateHasChanged); + }); + + return base.OnInitAsync(); + } + + + private void HandleOnSearchChange(string? text) + { + searchText = text; + FilterLogs(); + } + + private void HandleOnLogLevelFilter(BitDropdownItem[] items) + { + HandleOnLogLevelFilter(items.Select(i => i.Value)); + } + + private void HandleOnLogLevelFilter(IEnumerable logLevels) + { + filterLogLevels = logLevels; + FilterLogs(); + } + + private void HandleOnSortClick() + { + isDescendingSort = !isDescendingSort; + FilterLogs(); + } + + private void FilterLogs() + { + filteredLogs = allLogs.WhereIf(string.IsNullOrEmpty(searchText) is false, l => l.Message?.Contains(searchText!, StringComparison.InvariantCultureIgnoreCase) is true) + .Where(l => filterLogLevels.Contains(l.Level)); + if (isDescendingSort) + { + filteredLogs = filteredLogs.OrderByDescending(l => l.CreatedOn); + } + else + { + filteredLogs = filteredLogs.OrderBy(l => l.CreatedOn); + } + } + + private async Task CopyException(DiagnosticLog log) + { + var stateToCopy = string.Join(Environment.NewLine, log.State?.Select(i => $"{i.Key}: {i.Value}") ?? []); + + await clipboard.WriteText($"{log.Message}{Environment.NewLine}{log.Exception?.ToString()}{Environment.NewLine}{stateToCopy}"); + } + + private async Task GoTop() + { + await logStackRef.RootElement.Scroll(0, 0); + } + + + private static BitColor GetColor(LogLevel level) + { + return level switch + { + LogLevel.Trace => BitColor.PrimaryForeground, + LogLevel.Debug => BitColor.PrimaryForeground, + LogLevel.Information => BitColor.Primary, + LogLevel.Warning => BitColor.Warning, + LogLevel.Error => BitColor.Error, + LogLevel.Critical => BitColor.Error, + LogLevel.None => BitColor.SecondaryForeground, + _ => BitColor.TertiaryForeground + }; + } + + + public void Dispose() + { + unsubscribe?.Invoke(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.scss new file mode 100644 index 0000000000..f135d108e8 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticModal.razor.scss @@ -0,0 +1,23 @@ +@import '../../Styles/abstracts/_media-queries.scss'; + +section { + width: 100%; + height: 100%; +} + +::deep { + .container { + padding: 1rem; + } + + .stack { + overflow: auto; + } + + .go-top-button { + left: 50%; + position: fixed; + transform: translateX(-50%); + bottom: calc(var(--app-inset-bottom) + 1rem); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticSpacer.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticSpacer.razor new file mode 100644 index 0000000000..ab98d46c8c --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/DiagnosticSpacer.razor @@ -0,0 +1,15 @@ +@inherits AppComponentBase + + + +@code { + private int clickCount = 0; + private async Task HandleOnClick() + { + if (++clickCount == 7) + { + clickCount = 0; + PubSubService.Publish(PubSubMessages.SHOW_DIAGNOSTIC_MODAL); + } + } +} \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/IdentityHeader.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/IdentityHeader.razor index 21a0c2df58..a6a9834611 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/IdentityHeader.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/IdentityHeader.razor @@ -30,7 +30,7 @@ } - + ? dotnetObj; + /// + /// at the rendering time of this component (the component is added to the `RootLayout`) + /// it registers an instance of the `DotNetObjectReference` into the js code (look at the `app.ts` file), + /// so we can later use it to call .net methods. + /// + protected override async Task OnAfterFirstRenderAsync() + { + dotnetObj = DotNetObjectReference.Create(this); + + await JSRuntime.InvokeVoidAsync("App.registerJsBridge", dotnetObj); + + await base.OnAfterFirstRenderAsync(); + } + + + /// + /// you can add any other method like this to utilize the bridge between js and .net code. + /// + [JSInvokable(nameof(ShowDiagnostic))] + public async Task ShowDiagnostic() + { + PubSubService.Publish(PubSubMessages.SHOW_DIAGNOSTIC_MODAL); + } + + public void Dispose() + { + dotnetObj?.Dispose(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.scss index 0200f1866a..663287364a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.scss @@ -3,7 +3,7 @@ section { padding: 1rem; min-width: 20rem; - max-height: var(--app-max-height-vh); + max-height: var(--app-height); @include lt-md { min-width: unset; @@ -12,8 +12,8 @@ section { ::deep { .root { - width: var(--app-max-width); - height: var(--app-max-height); + width: var(--app-width); + height: var(--app-height); top: var(--app-inset-top); left: var(--app-inset-left); right: var(--app-inset-right); @@ -28,7 +28,7 @@ section { } .stack { - max-height: calc(var(--app-max-height-vh) - 3rem); + max-height: calc(var(--app-height) - 3rem); } .body { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor.scss index eec9596d5b..b38dfc96a2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor.scss @@ -2,14 +2,14 @@ @import '../../Styles/abstracts/_bit-css-variables.scss'; section { - bottom: 0; width: 100%; display: flex; height: 3.5rem; - position: sticky; - max-width: 100vw; + position: fixed; align-items: center; + max-width: var(--app-width); justify-content: space-around; + bottom: var(--app-inset-bottom); background-color: $bit-color-background-primary; border-top: 1px solid $bit-color-border-tertiary; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor.scss index 0ea354e0a7..07b411db2e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor.scss @@ -6,7 +6,7 @@ section { padding: 1rem; position: sticky; overflow: hidden auto; - height: var(--app-max-height-vh); + height: var(--app-height); min-width: var(--nav-menu-width); max-width: var(--nav-menu-width); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor index 3912d42a94..4af91cdba3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor @@ -8,7 +8,7 @@
-
+
@ChildContent diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor.scss index ceb9256c17..64d032ac15 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootContainer.razor.scss @@ -33,8 +33,6 @@ height: 100%; display: flex; overflow: auto; - min-height: 100%; - min-height: unset; position: relative; scroll-behavior: smooth; overscroll-behavior: none; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor index ecbfa48c2a..0d2efb8e5d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor @@ -45,3 +45,6 @@ + + + \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.cs index 04bd35b591..2cdf2012ec 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.cs @@ -15,6 +15,7 @@ public partial class RootLayout : IDisposable private Action unsubscribeRouteDataUpdated = default!; + [AutoInject] private Keyboard keyboard = default!; [AutoInject] private ThemeService themeService = default!; [AutoInject] private PubSubService pubSubService = default!; [AutoInject] private AuthenticationManager authManager = default!; @@ -56,6 +57,7 @@ protected override async Task OnInitializedAsync() SetCurrentUrl(); currentTheme = await themeService.GetCurrentTheme(); + await keyboard.Add(ButilKeyCodes.KeyX, OpenDiagnosticModal, ButilModifiers.Ctrl | ButilModifiers.Shift); await base.OnInitializedAsync(); } catch (Exception exp) @@ -141,6 +143,11 @@ private void SetIsCrossLayout() isCrossLayoutPage = true; } + private void OpenDiagnosticModal() + { + pubSubService.Publish(PubSubMessages.SHOW_DIAGNOSTIC_MODAL); + } + private string GetMainCssClass() { @@ -163,5 +170,7 @@ public void Dispose() unsubscribeThemeChange?.Invoke(); unsubscribeCultureChange?.Invoke(); unsubscribeRouteDataUpdated?.Invoke(); + + _ = keyboard?.DisposeAsync(); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss index 4c6efc9a66..c238500a1e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss @@ -7,7 +7,7 @@ main { @include lt-md { height: unset; - min-height: 100vh; + min-height: var(--app-height); } &.unauthenticated { @@ -21,7 +21,6 @@ main { .body { width: 100%; padding: 2rem; - min-height: 100%; padding-top: 5rem; } } @@ -48,7 +47,6 @@ main { width: 30%; padding: 4rem; min-width: 35rem; - min-height: 100%; padding-top: 5rem; background-color: $bit-color-background-secondary; border-inline-end: 1px solid $bit-color-border-secondary; @@ -58,6 +56,7 @@ main { border: none; min-width: unset; padding-inline: 1rem; + background-color: $bit-color-background-primary; } } } @@ -76,7 +75,7 @@ main { @include lt-md { height: unset; - min-height: 100vh; + padding-bottom: 3.5rem; } } @@ -85,6 +84,14 @@ main { flex-grow: 1; padding: 1rem; flex-direction: column; + + @include lt-md { + gap: 1rem; + } + + @include lt-sm { + gap: 0; + } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs index eacc7ae38b..524748a752 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs @@ -26,7 +26,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (string.IsNullOrEmpty(Title) is false) { - PubSubService.Publish(PubSubMessages.PAGE_TITLE_CHANGED, (Title, Subtitle)); + PubSubService.Publish(PubSubMessages.PAGE_TITLE_CHANGED, (Title, Subtitle), persistent: true); } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor index 4ef5825da8..df85275898 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor @@ -12,7 +12,7 @@ @Localizer[nameof(AppStrings.AddCategory)] -
+
-
+
-
-
+ diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.scss index 19ad147aa9..b22d8d1fb4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.scss @@ -50,5 +50,14 @@ section { .todo-list { width: 100%; background-color: var(--bit-clr-bg-sec); + height: calc(var(--app-height) - 14rem); + + @include lt-md { + height: calc(var(--app-height) - 17rem); + } + + @include lt-sm { + height: calc(var(--app-height) - 18rem); + } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor index abd0b69e49..28620fea49 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor @@ -12,7 +12,8 @@ - @Localizer[AppStrings.Or] + @Localizer[AppStrings.Or] + @Localizer[AppStrings.Or] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.scss index a7782bf36b..1baca747b3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPanel.razor.scss @@ -1,4 +1,20 @@ +@import '../../../../Styles/abstracts/_media-queries.scss'; + section { width: 100%; height: 100%; } + +::deep { + .lg-sep { + @include lt-md { + display: none; + } + } + + .sm-sep { + @include gt-sm { + display: none; + } + } +} \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor index ebcca4b552..af6bc32736 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor @@ -17,7 +17,8 @@ - @Localizer[AppStrings.Or] + @Localizer[AppStrings.Or] + @Localizer[AppStrings.Or] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.scss index 938d22e6d8..50d0d089dd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor.scss @@ -9,4 +9,16 @@ section { form { width: 304px; } + + .lg-sep { + @include lt-md { + display: none; + } + } + + .sm-sep { + @include gt-sm { + display: none; + } + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs index e5d3df4eb4..b0b74618df 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs @@ -44,7 +44,6 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddSessioned(sp => (AuthenticationManager)sp.GetRequiredService()); services.AddSingleton(sp => configuration.Get()!); - services.AddSingleton(_ => new CurrentScopeProvider(() => ClientAppCoordinator.CurrentServiceProvider)); services.AddOptions() .Bind(configuration) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs index 3be6eb6673..474e6d82fa 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs @@ -6,7 +6,6 @@ public static class ILoggingBuilderExtensions { public static ILoggingBuilder AddDiagnosticLogger(this ILoggingBuilder builder) { - builder.Services.AddSessioned(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); return builder; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/app.ts b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/app.ts index 51a3499a82..c220530dc5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/app.ts +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Scripts/app.ts @@ -1,5 +1,16 @@ //+:cnd:noEmit class App { + private static jsBridgeObj: DotNetObject; + + public static registerJsBridge(dotnetObj: DotNetObject) { + // For additional details, see the JsBridge.cs file. + App.jsBridgeObj = dotnetObj; + } + + public static showDiagnostic() { + return App.jsBridgeObj?.invokeMethodAsync("ShowDiagnostic"); + } + public static applyBodyElementClasses(cssClasses: string[], cssVariables: any): void { cssClasses?.forEach(c => document.body.classList.add(c)); Object.keys(cssVariables).forEach(key => document.body.style.setProperty(key, cssVariables[key])); @@ -15,13 +26,16 @@ class App { //#if (notification == true) public static async getDeviceInstallation(vapidPublicKey: string) { - if (await Notification.requestPermission() != "granted") - return null; + if (!("Notification" in window)) return null; + + if (await Notification.requestPermission() != "granted") return null; + const registration = await navigator.serviceWorker.ready; if (!registration) return null; + const pushManager = registration.pushManager; - if (pushManager == null) - return; + if (pushManager == null) return null; + let subscription = await pushManager.getSubscription(); if (subscription == null) { subscription = await pushManager.subscribe({ @@ -39,6 +53,23 @@ class App { declare class BitTheme { static init(options: any): void; }; +interface DotNetObject { + invokeMethod(methodIdentifier: string, ...args: any[]): T; + invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise; + dispose(): void; +} + +(function () { + setCssWindowSizes(); + + window.addEventListener('resize', setCssWindowSizes); + + function setCssWindowSizes() { + document.documentElement.style.setProperty('--win-width', `${window.innerWidth}px`); + document.documentElement.style.setProperty('--win-height', `${window.innerHeight}px`); + } +}()); + BitTheme.init({ system: true, onChange: (newTheme: string, oldThem: string) => { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/CurrentScopeProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/CurrentScopeProvider.cs deleted file mode 100644 index b4cec08809..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/CurrentScopeProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Boilerplate.Client.Core.Components; - -namespace Boilerplate.Client.Core.Services.Contracts; - -/// -/// Provides the current scope's . -/// -/// In different hosting environments, this delegate returns the `IServiceProvider` from: -/// -/// - **Blazor Server, SSR, and Pre-rendering:** `HttpContextAccessor.HttpContext.RequestServices` -/// - **Blazor WebAssembly and Hybrid:** Gets it from -/// -/// The delegate may return `null` in the following scenarios: -/// -/// - When there's no active `HttpContext` in backend environments. -/// - When the Routes.razor page is not loaded yet. -/// -/// The current scope's `IServiceProvider`, or `null` if not available. -public delegate IServiceProvider? CurrentScopeProvider(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs new file mode 100644 index 0000000000..a3b76bbb0a --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLog.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Logging; + +namespace Boilerplate.Client.Core.Services.DiagnosticLog; + +public class DiagnosticLog +{ + public DateTimeOffset CreatedOn { get; set; } + + public LogLevel Level { get; set; } + + public string? Message { get; set; } + + public Exception? Exception { get; set; } + + public IDictionary? State { get; set; } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs index 687a4e3ecb..9f25214154 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLogger.cs @@ -2,9 +2,11 @@ namespace Boilerplate.Client.Core.Services.DiagnosticLog; -public partial class DiagnosticLogger(CurrentScopeProvider scopeProvider) : ILogger, IDisposable +public partial class DiagnosticLogger : ILogger, IDisposable { - private ConcurrentQueue> states = new(); + public static ConcurrentBag Store { get; } = []; + + private IDictionary? currentState; public string? CategoryName { get; set; } @@ -13,8 +15,7 @@ public partial class DiagnosticLogger(CurrentScopeProvider scopeProvider) : ILog { if (state is IDictionary data) { - data[nameof(CategoryName)] = CategoryName; - states.Enqueue(data); + currentState = data; } return this; @@ -31,44 +32,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except var message = formatter(state, exception); - states.TryDequeue(out var currentState); - - var scope = scopeProvider.Invoke(); - - if (scope is null) - return; - - // Store logs in the memory to be shown later. - - var jsRuntime = scope.GetRequiredService(); - - if (jsRuntime.IsInitialized() is false) - return; - - var console = scope.GetRequiredService(); - - switch (logLevel) - { - case LogLevel.Trace: - case LogLevel.Debug: - console!.Log(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState); - break; - case LogLevel.Information: - console!.Info(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState); - break; - case LogLevel.Warning: - console!.Warn(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState); - break; - case LogLevel.Error: - case LogLevel.Critical: - console!.Error(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState); - break; - case LogLevel.None: - break; - default: - console!.Log(message, $"{Environment.NewLine}Category:", CategoryName, $"{Environment.NewLine}State:", currentState); - break; - } + Store.Add(new() { CreatedOn = DateTimeOffset.Now, Level = logLevel, Message = message, Exception = exception, State = currentState?.ToDictionary(i => i.Key, i => i.Value?.ToString()) }); } public void Dispose() diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs index 1368f70e6c..74b2cccbd9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DiagnosticLog/DiagnosticLoggerProvider.cs @@ -9,21 +9,16 @@ namespace Boilerplate.Client.Core.Services.DiagnosticLog; /// Provides a custom logger that outputs log messages to the browser's console and allows for selective display of logs /// within the application UI for enhanced diagnostics. /// -[ProviderAlias("DevInsights")] +[ProviderAlias("DiagnosticLogger")] public partial class DiagnosticLoggerProvider : ILoggerProvider { - [AutoInject] private CurrentScopeProvider scopeProvider = default!; - public ILogger CreateLogger(string categoryName) { - return new DiagnosticLogger(scopeProvider) + return new DiagnosticLogger() { CategoryName = categoryName }; } - public void Dispose() - { - - } + public void Dispose() { } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubMessages.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubMessages.cs index e45786d21d..269e227ee6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubMessages.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubMessages.cs @@ -4,12 +4,13 @@ public static partial class PubSubMessages { public const string SHOW_SNACK = nameof(SHOW_SNACK); public const string SHOW_MESSAGE = nameof(SHOW_MESSAGE); - public const string OPEN_NAV_PANEL = nameof(OPEN_NAV_PANEL); public const string THEME_CHANGED = nameof(THEME_CHANGED); + public const string OPEN_NAV_PANEL = nameof(OPEN_NAV_PANEL); public const string CULTURE_CHANGED = nameof(CULTURE_CHANGED); public const string PROFILE_UPDATED = nameof(PROFILE_UPDATED); public const string PAGE_TITLE_CHANGED = nameof(PAGE_TITLE_CHANGED); public const string ROUTE_DATA_UPDATED = nameof(ROUTE_DATA_UPDATED); + public const string SHOW_DIAGNOSTIC_MODAL = nameof(SHOW_DIAGNOSTIC_MODAL); public const string UPDATE_IDENTITY_HEADER_BACK_LINK = nameof(UPDATE_IDENTITY_HEADER_BACK_LINK); public const string IDENTITY_HEADER_BACK_LINK_CLICKED = nameof(IDENTITY_HEADER_BACK_LINK_CLICKED); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss index 85da4366f8..1430a522c7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss @@ -5,10 +5,14 @@ --app-inset-left: env(safe-area-inset-left); --app-inset-right: env(safe-area-inset-right); --app-inset-bottom: env(safe-area-inset-bottom); - --app-max-width: calc(100% - var(--app-inset-left) - var(--app-inset-right)); - --app-max-height: calc(100% - var(--app-inset-top) - var(--app-inset-bottom)); - --app-max-width-vw: calc(100vw - var(--app-inset-left) - var(--app-inset-right)); - --app-max-height-vh: calc(100vh - var(--app-inset-top) - var(--app-inset-bottom)); + //-- + --app-width-vw: calc(100vw - var(--app-inset-left) - var(--app-inset-right)); + --app-height-vh: calc(100vh - var(--app-inset-top) - var(--app-inset-bottom)); + --app-width-per: calc(100% - var(--app-inset-left) - var(--app-inset-right)); + --app-height-per: calc(100% - var(--app-inset-top) - var(--app-inset-bottom)); + --app-width: calc(var(--win-width) - var(--app-inset-left) - var(--app-inset-right)); + --app-height: calc(var(--win-height) - var(--app-inset-top) - var(--app-inset-bottom)); + //-- --app-inset-inline-start: var(--app-inset-left); --app-inset-inline-end: var(--app-inset-right); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/compilerconfig.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/compilerconfig.json index b873ca7316..b836243a4e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/compilerconfig.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/compilerconfig.json @@ -18,6 +18,12 @@ "minify": { "enabled": false }, "options": { "sourceMap": false } }, + { + "outputFile": "Components/Layout/DiagnosticModal.razor.css", + "inputFile": "Components/Layout/DiagnosticModal.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, { "outputFile": "Components/Layout/IdentityHeader.razor.css", "inputFile": "Components/Layout/IdentityHeader.razor.scss", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/wwwroot/index.html b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/wwwroot/index.html index d003f4814e..d904f4e1ea 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/wwwroot/index.html +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/wwwroot/index.html @@ -4,7 +4,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor index e3bec3d599..aa3c170748 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor @@ -20,7 +20,7 @@ @*#if (captcha == "reCaptcha")*@ diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Services.cs index 98c7f9d265..876f94037b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Services.cs @@ -116,8 +116,6 @@ private static void AddBlazor(WebApplicationBuilder builder) return httpClient; }); - services.AddSingleton(sp => new CurrentScopeProvider(() => sp.GetRequiredService().HttpContext?.RequestServices)); - services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ICollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ICollectionExtensions.cs index ca12ef1795..841ae86d21 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ICollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ICollectionExtensions.cs @@ -11,4 +11,9 @@ public static async Task> ToListAsync(this IAsyncEnumerable items, } return results; } + + public static IEnumerable<(T item, int index)> Indexed(this IEnumerable source) + { + return source.Select((item, index) => (item, index)); + } }