diff --git a/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml index 0d175858cc..5f2ea8db1c 100644 --- a/.github/workflows/admin-sample.cd.yml +++ b/.github/workflows/admin-sample.cd.yml @@ -115,7 +115,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --module Admin --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --module Admin --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --brouter false - name: Update core appsettings.json uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2 diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml index 40e410bd07..2143f22a84 100644 --- a/.github/workflows/bit.full.ci.yml +++ b/.github/workflows/bit.full.ci.yml @@ -354,13 +354,13 @@ jobs: - name: Build sample configuration 1 run: | - dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire --redis --multitenant + dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire --redis --multitenant --brouter dotnet build TestProject/TestProject.sln -p:InvariantGlobalization=false -p:Environment=Staging -p:EnforceCodeStyleInBuild=true rm -r "TestProject" - name: Build sample configuration 2 run: | - dotnet new bit-bp --name TestProject2 --database Other --filesStorage S3 --api Standalone --captcha None --pipeline None --module None --offlineDb false --appInsights false --sentry false --signalR false --notification false --cloudflare false --ads false --aspire true --redis false --multitenant false + dotnet new bit-bp --name TestProject2 --database Other --filesStorage S3 --api Standalone --captcha None --pipeline None --module None --offlineDb false --appInsights false --sentry false --signalR false --notification false --cloudflare false --ads false --aspire true --redis false --multitenant false --brouter false dotnet build TestProject2/TestProject2.slnx -p:InvariantGlobalization=true -p:Environment=Development -p:EnforceCodeStyleInBuild=true rm -r "TestProject2" diff --git a/.github/workflows/sales-module-demo.cd.yml b/.github/workflows/sales-module-demo.cd.yml index eac993269a..b7252be890 100644 --- a/.github/workflows/sales-module-demo.cd.yml +++ b/.github/workflows/sales-module-demo.cd.yml @@ -41,7 +41,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --appInsights --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads + cd ../../../ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --appInsights --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --brouter false - name: Update core appsettings.json uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2 @@ -106,7 +106,7 @@ jobs: cd src\Templates\Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ..\..\..\ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --sentry --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads + cd ..\..\..\ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --sentry --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads --brouter false - name: Update core appsettings.json uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2 diff --git a/.github/workflows/todo-sample.cd.yml b/.github/workflows/todo-sample.cd.yml index f5eb1142f1..29ed510d91 100644 --- a/.github/workflows/todo-sample.cd.yml +++ b/.github/workflows/todo-sample.cd.yml @@ -42,7 +42,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --api Standalone --redis --multitenant false + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --api Standalone --redis --multitenant false --brouter false - name: Use Bit.ResxTranslator run: | @@ -95,7 +95,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --api Standalone --multitenant false + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --api Standalone --multitenant false --brouter false - name: Use Bit.ResxTranslator run: | @@ -158,7 +158,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false --brouter false - name: Update core appsettings.json uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2 @@ -207,7 +207,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --multitenant false + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --multitenant false --brouter false - name: Update core appsettings.json uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2 @@ -261,7 +261,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false --brouter false - name: Update core appsettings.json uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2 @@ -314,7 +314,7 @@ jobs: cd src\Templates\Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ..\..\..\ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads --multitenant false + cd ..\..\..\ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads --multitenant false --brouter false - name: Use Bit.ResxTranslator run: | @@ -376,7 +376,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false --brouter false - name: Extract Android signing key from env uses: timheuer/base64-to-file@dfa5a5dd38803cb4f5c3a081eeb28e8362d43e9c # v2 @@ -477,7 +477,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false --brouter false - name: Use Bit.ResxTranslator run: | diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json index 078f6639f3..c3592cf4b1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json @@ -62,6 +62,9 @@ { "id": "ads" }, + { + "id": "brouter" + }, { "id": "apiServerUrl", "isVisible": false diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index a519821f89..561cc0796b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -241,6 +241,13 @@ "defaultValue": "false", "description": "Adds GoogleAds integration." }, + "brouter": { + "displayName": "Use bit Brouter?", + "type": "parameter", + "datatype": "bool", + "defaultValue": "true", + "description": "Replaces the built-in Blazor Router with bit Brouter, bringing advanced routing features - nested routes, route guards, data loaders, keep-alive pages and view transitions - inspired by the routers of Vue.js, React and Angular." + }, "nameToLower": { "type": "generated", "generator": "casing", @@ -690,6 +697,12 @@ "src/Client/Boilerplate.Client.Core/Infrastructure/Services/Contracts/IAdsService.cs", "src/Client/Boilerplate.Client.Core/Infrastructure/Services/AdsService.cs" ] + }, + { + "condition": "(brouter != true)", + "exclude": [ + "src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs" + ] } ] } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj index fb3fa68fad..855f249cd4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj @@ -18,6 +18,8 @@ + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs index efe22dfcc4..3847194201 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs @@ -3,11 +3,17 @@ namespace Boilerplate.Client.Core.Components; public partial class AppRouteDataPublisher : AppComponentBase { + private RouteData? lastPublishedRouteData; + [Parameter] public RouteData? RouteData { get; set; } - protected override async Task OnInitAsync() + protected override async Task OnParamsSetAsync() { - await base.OnInitAsync(); + await base.OnParamsSetAsync(); + + // RouteData is a fresh instance per navigation; skip redundant publishes on plain re-renders. + if (ReferenceEquals(lastPublishedRouteData, RouteData)) return; + lastPublishedRouteData = RouteData; PubSubService.Publish(ClientAppMessages.ROUTE_DATA_UPDATED, RouteData); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs new file mode 100644 index 0000000000..ed31aca188 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs @@ -0,0 +1,125 @@ +namespace Boilerplate.Client.Core.Components.Pages; + +/// +/// bit Brouter route-lifecycle hooks for pages deriving from . +/// +/// +/// Brouter treats routing as more than "match a URL and mount a component". A route can be marked +/// KeepAlive (see Routes.razor): instead of being torn down when you navigate away, the +/// page's component instance is retained and merely hidden, then revealed again on +/// return - so scroll position, grid paging/sorting/filtering, unsaved input, in-flight state, etc. all +/// survive the round-trip. And whether a page is kept alive or not, Brouter gives it a much richer +/// lifecycle than Blazor's bare OnInitialized/Dispose pair. +/// +/// +public abstract partial class AppPageBase : IBrouterRoute +{ + /// + /// + /// + protected virtual async ValueTask OnActivated(BrouterRouteActivation activation) { } + public async ValueTask OnActivatedAsync(BrouterRouteActivation activation) + { + try + { + await OnActivated(activation); + } + catch (Exception exp) + { + ExceptionHandler.Handle(exp); + } + } + + /// + /// + /// + protected virtual async ValueTask OnDeactivated(BrouterRouteDeactivation deactivation) { } + public async ValueTask OnDeactivatedAsync(BrouterRouteDeactivation deactivation) + { + try + { + await OnDeactivated(deactivation); + } + catch (Exception exp) + { + ExceptionHandler.Handle(exp); + } + } + + /// + /// + /// + protected virtual async ValueTask OnDeactivating(BrouterRouteDeactivatingContext context) { } + public async ValueTask OnDeactivatingAsync(BrouterRouteDeactivatingContext context) + { + try + { + await OnDeactivating(context); + } + catch (Exception exp) + { + ExceptionHandler.Handle(exp); + } + } + + /// + /// + /// + protected virtual async ValueTask OnRenavigated(BrouterRouteRenavigation renavigation) { } + public async ValueTask OnRenavigatedAsync(BrouterRouteRenavigation renavigation) + { + try + { + await OnRenavigated(renavigation); + } + catch (Exception exp) + { + ExceptionHandler.Handle(exp); + } + } + + /// + /// + /// + protected virtual async ValueTask OnRenavigating(BrouterRouteRenavigatingContext context) { } + public async ValueTask OnRenavigatingAsync(BrouterRouteRenavigatingContext context) + { + try + { + await OnRenavigating(context); + } + catch (Exception exp) + { + ExceptionHandler.Handle(exp); + } + } + + // The following code enables Brouter route-lifecycle hooks for pages inheriting from AppPageBase. + + [CascadingParameter] private BrouterRouteContext? routeContext { get; set; } + private bool brouterRegistered; + + public override Task SetParametersAsync(ParameterView parameters) + { + var task = base.SetParametersAsync(parameters); + + if (brouterRegistered is false && routeContext is not null) + { + routeContext.Register(this); + brouterRegistered = true; + } + + return task; + } + + protected override ValueTask DisposeAsync(bool disposing) + { + if (brouterRegistered) + { + routeContext?.Unregister(this); + brouterRegistered = false; + } + + return base.DisposeAsync(disposing); + } +} 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 13bdd90665..d66146a8b4 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 @@ -1,4 +1,3 @@ - namespace Boilerplate.Client.Core.Components.Pages; public abstract partial class AppPageBase : AppComponentBase diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor index 177e52e078..c2f965967a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor @@ -33,8 +33,8 @@ Class="categories-grid" OnRead="LoadCategories" Loading="isLoading" - Pageable Filterable Resizable - ShowToolbar ShowCsvExport ShowExcelExport + Pageable Filterable Resizable Reorderable + ShowToolbar ShowColumnChooser ShowCsvExport ShowExcelExport PageSize="10" Strings="DataGridStrings"> - { - NavigationManager.RefreshCurrentPage(); - }); - //#endif - } - - //#if (signalR == true) - protected override async ValueTask DisposeAsync(bool disposing) - { - await base.DisposeAsync(disposing); - - unsubscribe?.Invoke(); - } - //#endif -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs index 4cddcdbc84..31c1dda1d1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs @@ -1,3 +1,4 @@ +//+:cnd:noEmit using Boilerplate.Shared.Features.Dashboard; namespace Boilerplate.Client.Core.Components.Pages.Dashboard; @@ -8,17 +9,26 @@ public partial class OverallStatsWidget private bool isLoading; private OverallAnalyticsStatsDataResponseDto dto = new(); + //#if (signalR == true) + private Action? unsubscribe; + //#endif protected override async Task OnInitAsync() { await base.OnInitAsync(); + //#if (signalR == true) + // Instead of reloading the whole app, refresh only this widget's data when the dashboard changes. + unsubscribe = PubSubService.Subscribe(SharedAppMessages.DASHBOARD_DATA_CHANGED, async _ => await InvokeAsync(GetData)); + //#endif + await GetData(); } private async Task GetData() { isLoading = true; + StateHasChanged(); try { @@ -27,6 +37,16 @@ private async Task GetData() finally { isLoading = false; + StateHasChanged(); } } + + //#if (signalR == true) + protected override async ValueTask DisposeAsync(bool disposing) + { + await base.DisposeAsync(disposing); + + unsubscribe?.Invoke(); + } + //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs index 0d09c6e22d..336d0412ef 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs @@ -1,3 +1,4 @@ +//+:cnd:noEmit using Boilerplate.Shared.Features.Dashboard; namespace Boilerplate.Client.Core.Components.Pages.Dashboard; @@ -8,26 +9,18 @@ public partial class ProductsCountPerCategoryWidget private bool isLoading; private BitChartConfig config = default!; + //#if (signalR == true) + private Action? unsubscribe; + //#endif protected override async Task OnInitAsync() { await base.OnInitAsync(); - config = new BitChartConfig - { - Type = BitChartType.Bar, - Options = new BitChartOptions - { - Plugins = new BitChartPluginOptions - { - Legend = new BitChartLegendOptions { Display = false } - }, - Scales = - { - ["y"] = new BitChartScaleOptions { Id = "y", Type = BitChartScaleType.Linear, BeginAtZero = true } - } - } - }; + //#if (signalR == true) + // Instead of reloading the whole app, refresh only this widget's data when the dashboard changes. + unsubscribe = PubSubService.Subscribe(SharedAppMessages.DASHBOARD_DATA_CHANGED, async _ => await InvokeAsync(GetData)); + //#endif await GetData(); } @@ -35,9 +28,27 @@ protected override async Task OnInitAsync() private async Task GetData() { isLoading = true; + StateHasChanged(); try { + // A fresh config is built on each load so re-fetches replace the previous data instead of appending to it. + config = new BitChartConfig + { + Type = BitChartType.Bar, + Options = new BitChartOptions + { + Plugins = new BitChartPluginOptions + { + Legend = new BitChartLegendOptions { Display = false } + }, + Scales = + { + ["y"] = new BitChartScaleOptions { Id = "y", Type = BitChartScaleType.Linear, BeginAtZero = true } + } + } + }; + var data = await dashboardController.GetProductsCountPerCategoryStats(CurrentCancellationToken); config.Data.Labels.AddRange(data.Select(d => d.CategoryName ?? string.Empty)); @@ -50,6 +61,16 @@ private async Task GetData() finally { isLoading = false; + StateHasChanged(); } } + + //#if (signalR == true) + protected override async ValueTask DisposeAsync(bool disposing) + { + await base.DisposeAsync(disposing); + + unsubscribe?.Invoke(); + } + //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs index 772b985c42..9a685962d1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs @@ -1,3 +1,4 @@ +//+:cnd:noEmit using Boilerplate.Shared.Features.Dashboard; namespace Boilerplate.Client.Core.Components.Pages.Dashboard; @@ -8,22 +9,18 @@ public partial class ProductsPercentageWidget private bool isLoading; private BitChartConfig config = default!; + //#if (signalR == true) + private Action? unsubscribe; + //#endif protected override async Task OnInitAsync() { await base.OnInitAsync(); - config = new BitChartConfig - { - Type = BitChartType.Pie, - Options = new BitChartOptions - { - Plugins = new BitChartPluginOptions - { - Legend = new BitChartLegendOptions { Position = BitChartPosition.Right } - } - } - }; + //#if (signalR == true) + // Instead of reloading the whole app, refresh only this widget's data when the dashboard changes. + unsubscribe = PubSubService.Subscribe(SharedAppMessages.DASHBOARD_DATA_CHANGED, async _ => await InvokeAsync(GetData)); + //#endif await GetData(); } @@ -31,9 +28,23 @@ protected override async Task OnInitAsync() private async Task GetData() { isLoading = true; + StateHasChanged(); try { + // A fresh config is built on each load so re-fetches replace the previous data instead of appending to it. + config = new BitChartConfig + { + Type = BitChartType.Pie, + Options = new BitChartOptions + { + Plugins = new BitChartPluginOptions + { + Legend = new BitChartLegendOptions { Position = BitChartPosition.Right } + } + } + }; + var data = await dashboardController.GetProductsPercentagePerCategoryStats(CurrentCancellationToken); config.Data.Labels.AddRange(data.Select(d => d.CategoryName ?? string.Empty)); @@ -46,6 +57,16 @@ private async Task GetData() finally { isLoading = false; + StateHasChanged(); } } + + //#if (signalR == true) + protected override async ValueTask DisposeAsync(bool disposing) + { + await base.DisposeAsync(disposing); + + unsubscribe?.Invoke(); + } + //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor index 08491926e8..6a0bfade9f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor @@ -44,8 +44,8 @@ Class="products-grid" OnRead="LoadProducts" Loading="isLoading" - Pageable Filterable Resizable - ShowToolbar ShowCsvExport ShowExcelExport + Pageable Filterable Resizable Reorderable + ShowToolbar ShowColumnChooser ShowCsvExport ShowExcelExport PageSize="10" Strings="DataGridStrings"> - + + @*#if (brouter == true)*@ + + @*#if (module == "Admin")*@ + @* Keep-alive: pages that host a BitDataGrid whose add/edit lives on a separate page + (e.g. ProductsPage -> AddOrEditProductPage) keep their grid state - current page, + sorting and filtering - when the user navigates to the detail page and back, instead + of resetting to page 1. These hand-declared routes shadow the discovered @@page routes + of ProductsPage; authorization keeps flowing through the Found template below. *@ + + + @*#endif*@ + + @*#endif*@ @@ -22,7 +36,7 @@ - + - \ No newline at end of file + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs index 156c13a96c..f0dc7bdaf7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs @@ -1,3 +1,4 @@ +//+:cnd:noEmit namespace Boilerplate.Client.Core.Components; public partial class Routes @@ -35,3 +36,21 @@ await Task.Run(async () => }); } } + +/// +/// This class is only a workaround for limitations we faced in razor files inside .NET project templates. +/// You could simply delete it in your own project and use the base class directly in Routes.razor file. +/// +public class AppRouter : + //#if (brouter == true) + Brouter +//#else +//#if (IsInsideProjectTemplate == true) +/* +//#endif +Microsoft.AspNetCore.Components.Routing.Router +//#if (IsInsideProjectTemplate == true) +*/ +//#endif +//#endif +{ } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor index 605d7594ee..71bfca0266 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor @@ -14,6 +14,9 @@ @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Web.Virtualization @using Bit.Butil +@*#if (brouter == true)*@ +@using Bit.Brouter +@*#endif*@ @using Bit.BlazorUI @using Boilerplate @using Boilerplate.Shared diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs index 14c5e1aa14..3f11b1b7a0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs @@ -82,6 +82,9 @@ public IServiceCollection AddClientCoreProjectServices(IConfiguration configurat .ValidateOnStart(); services.AddBitButilServices(); + //#if (brouter == true) + services.AddBitBrouterServices(); + //#endif services.AddBitBlazorUIServices(); services.AddBitBlazorUIExtrasServices(trySingleton: AppPlatform.IsBlazorHybrid); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs index 2c5adad35a..e2127cb9cb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs @@ -25,7 +25,7 @@ public string GetRelativePath() /// public void RefreshCurrentPage() { - navigationManager.NavigateTo(navigationManager.GetUriPath(), forceLoad: false, replace: true); + navigationManager.NavigateTo(navigationManager.GetUriPath(), forceLoad: true, replace: true); } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs index e22508dd1c..050562497e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs @@ -39,10 +39,8 @@ public void ConfigureServices() { BaseAddress = new Uri(configuration.GetServerAddress(), UriKind.Absolute) }; - if (sp.GetRequiredService().WebAppUrl is Uri origin) - { - httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString()); - } + var origin = sp.GetRequiredService().WebAppUrl ?? httpClient.BaseAddress; + httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString()); return httpClient; }); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs index 734dd76c92..eaba5c8a66 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs @@ -34,10 +34,8 @@ public void AddClientWindowsProjectServices(IConfiguration configuration) { BaseAddress = new Uri(configuration.GetServerAddress(), UriKind.Absolute) }; - if (sp.GetRequiredService().WebAppUrl is Uri origin) - { - httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString()); - } + var origin = sp.GetRequiredService().WebAppUrl ?? httpClient.BaseAddress; + httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString()); return httpClient; }); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 29f53fff68..e2cc79e817 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -3,6 +3,7 @@ + diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor index d4f45c2ba5..1e3fcefd33 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor @@ -1,4 +1,4 @@ -@page "/templates/create-project" +@page "/templates/create-project" @page "/boilerplate/create-project" @inherits AppComponentBase @@ -408,6 +408,21 @@ Helps you earn money by displaying reward-based Google ads. Demo + +
+ bit Brouter + +
+
+ + @GetBrouterCommand() + + bit Brouter replaces Blazor's built-in Router and brings the powerful routing features that developers enjoy in Vue.js, React and Angular to the Blazor ecosystem: nested routing, async route guards, data loaders, keep-alive pages that preserve their state across navigation, named and typed routes, and animated view transitions. +
+
+ It stays a drop-in replacement, so your existing @@page routes, layouts and authorization keep working unchanged while you opt into the advanced features where you need them. +
+

diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs index 032c7b7f2a..fbb8cfca27 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs @@ -17,6 +17,7 @@ public partial class Templates05CreateProjectPage private Parameter redis = new() { Value = false, Default = false }; private Parameter aspire = new() { Value = true, Default = true }; private Parameter multiTenant = new() { Value = true, Default = true }; + private Parameter brouter = new() { Value = true, Default = true }; private Parameter captcha = new() { @@ -180,6 +181,11 @@ private string GetFinalCommand() finalCommand.Append(GetMultiTenantCommand()); } + if (brouter.IsModified) + { + finalCommand.Append(GetBrouterCommand()); + } + return finalCommand.ToString(); } @@ -273,6 +279,11 @@ private string GetMultiTenantCommand() return $"--multitenant{(multiTenant.Value ? string.Empty : " false")} "; } + private string GetBrouterCommand() + { + return $"--brouter{(brouter.Value ? string.Empty : " false")} "; + } + private class Parameter { public T? Value { get; set; }